From 784ddfa0704772c20291dbb9d250baa6c59c0a33 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 10:20:47 +0200 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9C=A8=20Support=20fixed-parameter=20c?= =?UTF-8?q?ompiler=20targets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 馃 *AI text below* 馃 Keep native parameter restrictions in target matching, serialized target attributes, synthesis-basis selection, and conformance verification. Support arbitrary RZ with fixed RX(pi/2) pulses for native synthesis. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 37 +++++ bindings/mlir/register_mlir.cpp | 41 +++-- bindings/patterns.txt | 2 + docs/glossary.md | 2 +- docs/mlir/target_compilation.md | 10 ++ mlir/include/mqt/Compiler/Target.h | 44 ++++-- mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td | 7 +- mlir/lib/Compiler/Target.cpp | 147 ++++++++++++++---- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 22 ++- .../QCO/Transforms/Decomposition/Euler.cpp | 31 +++- .../MergeSingleQubitRotationGates.cpp | 28 +++- .../Compiler/test_compiler_target.cpp | 125 +++++++++++++++ .../NativeSynthesis/test_target_synthesis.cpp | 66 ++++++++ python/mqt/core/mlir.pyi | 18 ++- test/python/test_mlir.py | 59 +++++++ 15 files changed, 574 insertions(+), 65 deletions(-) create mode 100644 .agent/plans/fixed-parameter-targets.md diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md new file mode 100644 index 0000000000..2b611e62b5 --- /dev/null +++ b/.agent/plans/fixed-parameter-targets.md @@ -0,0 +1,37 @@ +# Fixed-parameter compiler targets + +Status: implementation and local validation complete. + +## Goal and scope + +Allow operation capabilities to restrict individual parameters to finite fixed +values. Unspecified parameters remain unrestricted. Target matching, serialized +attributes, synthesis-basis selection, and final verification must preserve the +same restrictions. Symbolic values cannot satisfy a fixed parameter. + +Add synthesis using arbitrary RZ and RX(蟺/2), using existing rotation +operations. This basis covers fixed X-axis pulses without new vendor gate +operations. Direct native-gate targets are a separate Core change. + +## Decisions + +Use optional fixed values per parameter, not a general constraint language. +Multiple capabilities describe alternative fixed values and placements. Match +constants with the existing absolute parameter-comparison tolerance, without +reducing angles modulo a period: doing so could lose global phase. + +Only inspect parameter values for constrained capabilities. Unrestricted targets +keep their existing pipelines. Basis selection must never treat a fixed-angle +rotation as an arbitrary rotation. + +## Validation + +Compiler and native-synthesis unit suites passed, including full-unitary phase +comparisons, parameter restrictions, invalid attributes, and ordered placements. +Python binding tests cover fixed and symbolic input gates. Stub generation, repository lint, and full changed-file C++ lint passed. + +## Follow-up + +Direct GPI/GPI2, MS, and ZZ target support is separate work. It will own gate +conventions and synthesis in Core. The downstream adapter will then expose these +capabilities and Rigetti's fixed rotations. diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index ef18816cc1..7fc4bf3472 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -811,7 +811,8 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::optional> siteTuples, const std::optional duration, - const std::optional fidelity) { + const std::optional fidelity, + std::vector> fixedParameters) { constructFromExpected( self, mlir::CompilerTarget::OperationCapability::create( @@ -819,10 +820,11 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity)); + duration, fidelity, std::move(fixedParameters))); }, "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), - "duration"_a = nb::none(), "fidelity"_a = nb::none()) + "duration"_a = nb::none(), "fidelity"_a = nb::none(), nb::kw_only(), + "fixed_parameters"_a = std::vector>{}) .def( "__init__", [](mlir::CompilerTarget::OperationCapability& self, std::string name, @@ -830,7 +832,8 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::optional> siteTuples, const std::optional duration, - const std::optional fidelity) { + const std::optional fidelity, + std::vector> fixedParameters) { constructFromExpected( self, mlir::CompilerTarget::OperationCapability::create( @@ -838,10 +841,11 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity)); + duration, fidelity, std::move(fixedParameters))); }, "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), - "duration"_a = nb::none(), "fidelity"_a = nb::none()) + "duration"_a = nb::none(), "fidelity"_a = nb::none(), nb::kw_only(), + "fixed_parameters"_a = std::vector>{}) .def_prop_ro( "name", [](const mlir::CompilerTarget::OperationCapability& operation) { @@ -867,6 +871,15 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); }, "Supported ordered placements with optional calibration; empty means " "general applicability.") + .def_prop_ro( + "fixed_parameters", + [](const mlir::CompilerTarget::OperationCapability& operation) { + return std::vector>( + operation.fixedParameters().begin(), + operation.fixedParameters().end()); + }, + "Fixed values or None per parameter; empty means unrestricted. " + "Constants use absolute tolerance 1e-15 without angle wrapping.") .def_prop_ro("duration", &mlir::CompilerTarget::OperationCapability::duration, "The raw default duration, if available.") @@ -902,7 +915,8 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); .value("XZX", mlir::CompilerTarget::SingleQubitBasis::XZX) .value("XYX", mlir::CompilerTarget::SingleQubitBasis::XYX) .value("ZYZ", mlir::CompilerTarget::SingleQubitBasis::ZYZ) - .value("ZXZ", mlir::CompilerTarget::SingleQubitBasis::ZXZ); + .value("ZXZ", mlir::CompilerTarget::SingleQubitBasis::ZXZ) + .value("ZRX90", mlir::CompilerTarget::SingleQubitBasis::ZRX90); auto synthesisBasis = nb::class_( compilerTarget, "SynthesisBasis", @@ -1142,15 +1156,20 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); [](const mlir::CompilerTarget& target, const std::string_view name, const size_t arity, const std::optional numParameters, const std::optional>& - sites) { + sites, + const std::vector>& parameters) { if (sites) { return target.supportsOperation(name, arity, numParameters, - *sites); + *sites, parameters); } - return target.supportsOperation(name, arity, numParameters); + return target.supportsOperation(name, arity, numParameters, + std::nullopt, parameters); }, "name"_a, "arity"_a, "num_parameters"_a = nb::none(), - "sites"_a = nb::none(), "Whether the target supports an operation."); + "sites"_a = nb::none(), nb::kw_only(), + "parameters"_a = std::vector>{}, + "Whether the target supports an operation. Omitted or None parameter " + "values require unrestricted support."); nb::class_( m, "TargetEnvironment", diff --git a/bindings/patterns.txt b/bindings/patterns.txt index d8de3abc91..df8b40953f 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -138,6 +138,8 @@ mqt\.core\.mlir\.CompilerTarget\.OperationCapability\.__init__$: site_tuples: Sequence[CompilerTarget.SiteTuple | Sequence[int]] | None = None, duration: int | None = None, fidelity: float | None = None, + *, + fixed_parameters: Sequence[float | None] = (), ) -> None: \doc diff --git a/docs/glossary.md b/docs/glossary.md index 6d2fb59e4b..3b0e527e44 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -202,7 +202,7 @@ compiler target operation capability **Preferred term:** operation capability. **Accepted aliases:** none. A compiler target's description of a supported operation, including its name, - arity, parameters, placements, and optional calibration data. Represented by + arity, parameter count and optional fixed values, placements, and optional calibration data. Represented by `CompilerTarget::OperationCapability` in C++ and `CompilerTarget.OperationCapability` in Python. An MLIR operation is an IR instance, not this capability description. diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 33fdd528a5..982622d689 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -166,6 +166,16 @@ placements without calibration in this list, and omit operations that are not available anywhere. Structural and program-format constructs are not compiler-target operations. +Restrict individual gate parameters with `fixed_parameters`. For example, +`CompilerTarget.OperationCapability("rx", 1, 1, fixed_parameters=[math.pi / 2])` +accepts only RX(蟺/2). A nonempty list has one entry per parameter; `None` leaves +that parameter unrestricted. Multiple capabilities can describe different fixed +values or placements. Constants match with absolute tolerance `1e-15`, without +angle wrapping; symbolic values do not match fixed values. Omit the list for +unrestricted parameters. The compiler can synthesize arbitrary one-qubit gates +with RZ and fixed RX(蟺/2) on every site. Other fixed-angle sets are accepted for +matching native operations but do not automatically provide a synthesis basis. + Use plain tuples for placements without calibration. Use `CompilerTarget.SiteTuple([1, 0], duration=40, fidelity=0.99)` to attach calibration to a placement; both forms can appear in the same list. diff --git a/mlir/include/mqt/Compiler/Target.h b/mlir/include/mqt/Compiler/Target.h index 10f57b10b9..c0b89fdb50 100644 --- a/mlir/include/mqt/Compiler/Target.h +++ b/mlir/include/mqt/Compiler/Target.h @@ -196,14 +196,16 @@ class CompilerTarget { create(std::string name, size_t arity, size_t numParameters, std::vector siteTuples = {}, std::optional duration = std::nullopt, - std::optional fidelity = std::nullopt); + std::optional fidelity = std::nullopt, + std::vector> fixedParameters = {}); /// Create a validated operation capability. [[nodiscard]] static llvm::Expected create(std::string name, Arity arity, size_t numParameters, std::vector siteTuples = {}, std::optional duration = std::nullopt, - std::optional fidelity = std::nullopt); + std::optional fidelity = std::nullopt, + std::vector> fixedParameters = {}); /// Return the exact reported operation name. [[nodiscard]] llvm::StringRef name() const noexcept; @@ -217,6 +219,13 @@ class CompilerTarget { /// Return the number of real-valued operation parameters. [[nodiscard]] size_t numParameters() const noexcept; + /// Fixed parameter values; nullopt accepts any value. Empty is + /// unrestricted. Nonempty lists contain numParameters() entries. Constants + /// match with absolute tolerance 1e-15, without reducing angles modulo a + /// period. + [[nodiscard]] llvm::ArrayRef> + fixedParameters() const noexcept; + /// Return all supported ordered placements, or empty for general support. [[nodiscard]] llvm::ArrayRef siteTuples() const noexcept; @@ -231,12 +240,14 @@ class CompilerTarget { Arity arity, size_t numParameters, std::vector siteTuples, std::optional duration, - std::optional fidelity); + std::optional fidelity, + std::vector> fixedParameters); std::string name_; std::string canonicalName_; Arity arity_; size_t numParameters_; + std::vector> fixedParameters_; std::vector siteTuples_; std::optional duration_; std::optional fidelity_; @@ -292,13 +303,14 @@ class CompilerTarget { /// Recognized globally usable single-qubit synthesis basis. enum class SingleQubitBasis : uint8_t { - U, ///< `U(胃, 蠁, 位)`. - ZSXX, ///< `RZ` / `SX` / `X` synthesis via a ZYZ decomposition. - R, ///< XYX synthesis expressed with `R(胃, 蠁)`. - XZX, ///< `RX(蠁) * RZ(胃) * RX(位)`. - XYX, ///< `RX(蠁) * RY(胃) * RX(位)`. - ZYZ, ///< `RZ(蠁) * RY(胃) * RZ(位)`. - ZXZ, ///< `RZ(蠁) * RX(胃) * RZ(位)`. + U, ///< `U(胃, 蠁, 位)`. + ZSXX, ///< `RZ` / `SX` / `X` synthesis via a ZYZ decomposition. + R, ///< XYX synthesis expressed with `R(胃, 蠁)`. + XZX, ///< `RX(蠁) * RZ(胃) * RX(位)`. + XYX, ///< `RX(蠁) * RY(胃) * RX(位)`. + ZYZ, ///< `RZ(蠁) * RY(胃) * RZ(位)`. + ZXZ, ///< `RZ(蠁) * RX(胃) * RZ(位)`. + ZRX90, ///< Arbitrary `RZ` and fixed `RX(蟺/2)` pulses. }; /// One single-qubit basis and optional entangler usable across the target. @@ -390,16 +402,24 @@ class CompilerTarget { /// Return operation capabilities in reported order. [[nodiscard]] llvm::ArrayRef operations() const noexcept; - /// Return whether an operation capability is supported by the target. + /// Return whether an operation supports unrestricted parameter values. [[nodiscard]] bool supportsOperation(llvm::StringRef name, size_t arity, std::optional numParameters = std::nullopt) const; - /// Return whether an operation capability is supported on ordered sites. + /// Return whether an operation supports unrestricted values on ordered sites. [[nodiscard]] bool supportsOperation(llvm::StringRef name, size_t arity, std::optional numParameters, llvm::ArrayRef sites) const; + /// Check parameter values, optionally on ordered sites. + /// Unknown values require unrestricted support. + [[nodiscard]] bool + supportsOperation(llvm::StringRef name, size_t arity, + std::optional numParameters, + std::optional> sites, + llvm::ArrayRef> parameters) const; + /// Return whether a QCO operation is supported. [[nodiscard]] bool supports(::mlir::Operation* operation) const; diff --git a/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td index 693b56755c..f749e65f96 100644 --- a/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td @@ -183,7 +183,9 @@ def NativeOperationAttr : MQTAttr<"NativeOperation", "native_operation"> { The operation records its spelling, arity, parameter count, and optional global or site-specific calibration data. An empty site-tuple list means general applicability; a nonempty list gives all supported ordered - placements. The following example records a directional controlled-X operation: + placements. Optional `fixed_parameters` contains one entry per parameter: + a finite f64 value for a fixed parameter, or `unit` for an unrestricted one. + The following example records a directional controlled-X operation: ```mlir #mqt.native_operation { "uint64_t":$num_parameters, MQTArrayRefParameter<"SiteTupleAttr">:$site_tuples, MQTOptionalUInt64Parameter<>:$duration, - OptionalParameter<"FloatAttr">:$fidelity); + OptionalParameter<"FloatAttr">:$fidelity, + OptionalParameter<"ArrayAttr">:$fixed_parameters); let assemblyFormat = "`<` struct(params) `>`"; let genVerifyDecl = 1; } diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 1e0fb0aeaf..b435f0c675 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -16,6 +16,7 @@ #include "mqt/Dialect/QCO/IR/QCOOps.h" #include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" #include "mlir/IR/MLIRContext.h" #include "mlir/IR/Operation.h" #include "mlir/Support/LLVM.h" @@ -377,21 +378,22 @@ CompilerTarget::OperationCapability::Arity::Arity(Kind kind, : kind_(kind), value_(value) {} llvm::Expected -CompilerTarget::OperationCapability::create(std::string name, size_t arity, - size_t numParameters, - std::vector siteTuples, - std::optional duration, - std::optional fidelity) { +CompilerTarget::OperationCapability::create( + std::string name, size_t arity, size_t numParameters, + std::vector siteTuples, std::optional duration, + std::optional fidelity, + std::vector> fixedParameters) { return create(std::move(name), Arity::fixed(arity), numParameters, - std::move(siteTuples), duration, fidelity); + std::move(siteTuples), duration, fidelity, + std::move(fixedParameters)); } llvm::Expected -CompilerTarget::OperationCapability::create(std::string name, Arity arity, - size_t numParameters, - std::vector siteTuples, - std::optional duration, - std::optional fidelity) { +CompilerTarget::OperationCapability::create( + std::string name, Arity arity, size_t numParameters, + std::vector siteTuples, std::optional duration, + std::optional fidelity, + std::vector> fixedParameters) { auto canonicalName = canonicalOperationName(name); if (canonicalName.empty()) { return invalidTarget("Compiler target operation name must not be empty"); @@ -414,6 +416,20 @@ CompilerTarget::OperationCapability::create(std::string name, Arity arity, "Compiler target zero-arity operation cannot contain site tuples"); } + if (!fixedParameters.empty() && fixedParameters.size() != numParameters) { + return invalidTarget( + "Compiler target fixed parameters must match its parameter count"); + } + if (llvm::any_of(fixedParameters, [](const auto value) { + return value && !std::isfinite(*value); + })) { + return invalidTarget("Compiler target fixed parameters must be finite"); + } + if (llvm::none_of(fixedParameters, + [](const auto value) { return value.has_value(); })) { + fixedParameters.clear(); + } + llvm::SmallDenseSet> uniqueSiteCombinations; for (const auto& siteTuple : siteTuples) { if (!arity.accepts(siteTuple.sites().size())) { @@ -428,15 +444,17 @@ CompilerTarget::OperationCapability::create(std::string name, Arity arity, return OperationCapability(std::move(name), std::move(canonicalName), arity, numParameters, std::move(siteTuples), duration, - fidelity); + fidelity, std::move(fixedParameters)); } CompilerTarget::OperationCapability::OperationCapability( std::string name, std::string canonicalName, Arity arity, size_t numParameters, std::vector siteTuples, - std::optional duration, std::optional fidelity) + std::optional duration, std::optional fidelity, + std::vector> fixedParameters) : name_(std::move(name)), canonicalName_(std::move(canonicalName)), arity_(arity), numParameters_(numParameters), + fixedParameters_(std::move(fixedParameters)), siteTuples_(std::move(siteTuples)), duration_(duration), fidelity_(fidelity) {} @@ -457,6 +475,11 @@ size_t CompilerTarget::OperationCapability::numParameters() const noexcept { return numParameters_; } +ArrayRef> +CompilerTarget::OperationCapability::fixedParameters() const noexcept { + return fixedParameters_; +} + ArrayRef CompilerTarget::OperationCapability::siteTuples() const noexcept { return siteTuples_; @@ -508,11 +531,11 @@ struct CompilerTarget::Storage { [[nodiscard]] llvm::Error initialize(); void computeDistances(size_t source, MutableArrayRef row) const; - [[nodiscard]] bool - supportsOperation(StringRef name, size_t arity, - std::optional numParameters, - std::optional> orderedSites = std::nullopt, - bool variadicOnly = false) const; + [[nodiscard]] bool supportsOperation( + StringRef name, size_t arity, std::optional numParameters, + std::optional> orderedSites = std::nullopt, + bool variadicOnly = false, + function_ref(size_t)> parameterAt = nullptr) const; [[nodiscard]] bool supportsGate( GateKind gate, std::optional> orderedSites = std::nullopt) const; @@ -681,7 +704,8 @@ llvm::Error CompilerTarget::Storage::initialize() { bool CompilerTarget::Storage::supportsOperation( StringRef operationName, size_t arity, std::optional numParameters, - std::optional> orderedSites, bool variadicOnly) const { + std::optional> orderedSites, bool variadicOnly, + function_ref(size_t)> parameterAt) const { const auto canonical = canonicalOperationName(operationName); if (canonical.empty() || arity > sites.size() || (orderedSites && orderedSites->size() != arity)) { @@ -709,7 +733,20 @@ bool CompilerTarget::Storage::supportsOperation( operation.arity().accepts(arity) && (!numParameters || operation.numParameters() == *numParameters) && (!orderedSites || operation.siteTuples().empty() || - operationSites[index].contains(*orderedSites)); + operationSites[index].contains(*orderedSites)) && + llvm::all_of(llvm::enumerate(operation.fixedParameters()), + [&](const auto entry) { + const auto expected = entry.value(); + if (!expected) { + return true; + } + const auto actual = parameterAt + ? parameterAt(entry.index()) + : std::nullopt; + return actual && + std::abs(*actual - *expected) <= + mqt::PARAMETER_COMPARISON_TOLERANCE; + }); }); } @@ -749,6 +786,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { OperationCapability::Arity::Kind::Variadic) && operation.arity().accepts(arity) && operation.numParameters() == numParameters && + operation.fixedParameters().empty() && operation.siteTuples().empty(); }); }; @@ -775,6 +813,13 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { } else if (supportsOnEverySite(GateKind::RY) && supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZYZ; + } else if (supportsOnEverySite(GateKind::RZ) && + llvm::all_of(siteIds, [&](SiteId site) { + return supportsOperation( + "rx", 1, 1, ArrayRef(&site, 1), false, + [](size_t) { return std::optional{std::numbers::pi / 2.}; }); + })) { + singleQubit = SingleQubitBasis::ZRX90; } const auto supportsOnEveryCoupling = [&](GateKind gate) { @@ -975,10 +1020,24 @@ CompilerTarget::create(const mqt::CompilationTargetAttr attribute) { static_cast(operationAttr.getArity().getValue())) : OperationCapability::Arity::variadic( static_cast(operationAttr.getArity().getValue())); + std::vector> fixedParameters; + if (auto parameters = operationAttr.getFixedParameters()) { + for (Attribute parameter : parameters) { + auto value = dyn_cast(parameter); + if ((!value && !isa(parameter)) || + (value && !value.getType().isF64())) { + return invalidTarget("Compiler target fixed parameters must be " + "finite f64 values or unit"); + } + fixedParameters.emplace_back( + value ? std::optional{value.getValueAsDouble()} : std::nullopt); + } + } auto operation = OperationCapability::create( operationAttr.getName().getValue().str(), arity, static_cast(operationAttr.getNumParameters()), - std::move(siteTuples), operationAttr.getDuration(), fidelity); + std::move(siteTuples), operationAttr.getDuration(), fidelity, + std::move(fixedParameters)); if (!operation) { return operation.takeError(); } @@ -1126,6 +1185,22 @@ bool CompilerTarget::supportsOperation(StringRef operationName, size_t arity, sites); } +bool CompilerTarget::supportsOperation( + StringRef operationName, size_t arity, std::optional numParameters, + std::optional> sites, + ArrayRef> parameters) const { + if (!parameters.empty()) { + if (numParameters && *numParameters != parameters.size()) { + return false; + } + numParameters = parameters.size(); + } + return storage_->supportsOperation( + operationName, arity, numParameters, sites, false, [&](size_t index) { + return parameters.empty() ? std::nullopt : parameters[index]; + }); +} + bool CompilerTarget::supports(::mlir::Operation* operation) const { return supportsImpl(operation, std::nullopt); } @@ -1154,10 +1229,12 @@ bool CompilerTarget::supportsImpl(::mlir::Operation* operation, if (body.getNumQubits() != controlled.getNumTargets()) { return false; } - if (storage_->supportsOperation(body.getBaseSymbol(), - controlled.getNumQubits(), - body.getNumParams(), sites, - /*variadicOnly=*/true)) { + if (storage_->supportsOperation( + body.getBaseSymbol(), controlled.getNumQubits(), + body.getNumParams(), sites, + /*variadicOnly=*/true, [&](size_t index) { + return mqt::valueToDouble(body.getParameter(index)); + })) { return true; } if (controlled.getNumControls() != 1 || controlled.getNumTargets() != 1) { @@ -1182,9 +1259,11 @@ bool CompilerTarget::supportsImpl(::mlir::Operation* operation, return true; } } - return storage_->supportsOperation(unitary.getBaseSymbol(), - unitary.getNumQubits(), - unitary.getNumParams(), sites); + return storage_->supportsOperation( + unitary.getBaseSymbol(), unitary.getNumQubits(), unitary.getNumParams(), + sites, false, [&](size_t index) { + return mqt::valueToDouble(unitary.getParameter(index)); + }); } if (isa(operation)) { return storage_->supportsOperation("measure", 1, 0, sites); @@ -1270,10 +1349,20 @@ CompilerTarget::materialize(MLIRContext& context) const { : mqt::OperationArityKind::Variadic; const auto arityAttr = mqt::OperationArityAttr::get( &context, arityKind, operation.arity().value()); + ArrayAttr fixedParameters; + if (!operation.fixedParameters().empty()) { + SmallVector values; + for (const auto parameter : operation.fixedParameters()) { + values.push_back(parameter + ? Attribute(builder.getF64FloatAttr(*parameter)) + : Attribute(builder.getUnitAttr())); + } + fixedParameters = builder.getArrayAttr(values); + } operationAttrs.emplace_back(mqt::NativeOperationAttr::get( &context, builder.getStringAttr(operation.name()), arityAttr, operation.numParameters(), siteTupleAttrs, operation.duration(), - fidelityAttr)); + fidelityAttr, fixedParameters)); } const auto connectivity = connectivityKind() == Connectivity::Kind::AllToAll diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 91d0af2d05..92381fe59e 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -248,9 +248,10 @@ OperationArityAttr::verify(const function_ref emitError, LogicalResult NativeOperationAttr::verify( const function_ref emitError, const StringAttr name, - const OperationArityAttr arity, const uint64_t /*numParameters*/, + const OperationArityAttr arity, const uint64_t numParameters, const ArrayRef siteTuples, - const std::optional /*duration*/, const FloatAttr fidelity) { + const std::optional /*duration*/, const FloatAttr fidelity, + const ArrayAttr fixedParameters) { if (name.getValue().trim().empty()) { return emitError() << "compiler target operation name must not be empty"; } @@ -259,6 +260,23 @@ LogicalResult NativeOperationAttr::verify( return failure(); } + if (fixedParameters) { + if (fixedParameters.size() != numParameters) { + return emitError() << "compiler target fixed parameters must match its " + "parameter count"; + } + for (Attribute parameter : fixedParameters) { + if (isa(parameter)) { + continue; + } + auto value = dyn_cast(parameter); + if (!value || !value.getType().isF64() || !value.getValue().isFinite()) { + return emitError() << "compiler target fixed parameters must be finite " + "f64 values or unit"; + } + } + } + if (!siteTuples.empty() && arity.getKind() == OperationArityKind::Variadic) { return emitError() << "compiler target variadic operation cannot contain site tuples"; diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 1fb1069db9..594c54aa06 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -38,12 +38,18 @@ bool isSingleQubitBasisGate(Operation* op, SingleQubitBasis basis) { return basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::ZXZ || basis == SingleQubitBasis::XZX || - basis == SingleQubitBasis::ZSXX; + basis == SingleQubitBasis::ZSXX || + basis == SingleQubitBasis::ZRX90; }) .Case([&](RYOp) { return basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::XYX; }) - .Case([&](RXOp) { + .Case([&](RXOp rotation) { + if (basis == SingleQubitBasis::ZRX90) { + const auto angle = mqt::valueToDouble(rotation.getTheta()); + return angle && std::abs(*angle - std::numbers::pi / 2.) <= + mqt::PARAMETER_COMPARISON_TOLERANCE; + } return basis == SingleQubitBasis::ZXZ || basis == SingleQubitBasis::XZX || basis == SingleQubitBasis::XYX; }) @@ -193,6 +199,7 @@ EulerAngles anglesFromUnitary(const Matrix2x2& matrix, switch (basis) { case SingleQubitBasis::ZYZ: case SingleQubitBasis::ZSXX: + case SingleQubitBasis::ZRX90: return paramsZYZ(matrix); case SingleQubitBasis::ZXZ: return paramsZXZ(matrix); @@ -274,6 +281,7 @@ struct Unitary1QEulerPlan { case SingleQubitBasis::ZYZ: case SingleQubitBasis::ZXZ: case SingleQubitBasis::ZSXX: + case SingleQubitBasis::ZRX90: appendRotation(SynthesisStep::Kind::RZ, angles.phi + angles.lambda); break; @@ -330,6 +338,24 @@ struct Unitary1QEulerPlan { angles.lambda); phase = angles.phase; break; + case SingleQubitBasis::ZRX90: { + constexpr double pi = std::numbers::pi; + constexpr double halfPi = pi / 2.; + if (isNearZeroRotationAngle(angles.theta - halfPi)) { + appendRotation(SynthesisStep::Kind::RZ, angles.lambda - halfPi); + steps.emplace_back(SynthesisStep::Kind::RX, halfPi); + appendRotation(SynthesisStep::Kind::RZ, angles.phi + halfPi); + phase = angles.phase; + } else { + appendRotation(SynthesisStep::Kind::RZ, angles.lambda); + steps.emplace_back(SynthesisStep::Kind::RX, halfPi); + appendRotation(SynthesisStep::Kind::RZ, angles.theta + pi); + steps.emplace_back(SynthesisStep::Kind::RX, halfPi); + appendRotation(SynthesisStep::Kind::RZ, angles.phi + pi); + phase = angles.phase + pi; + } + break; + } case SingleQubitBasis::ZSXX: { constexpr double pi = std::numbers::pi; constexpr double halfPi = std::numbers::pi / 2.0; @@ -428,6 +454,7 @@ std::optional parseSingleQubitBasis(StringRef basis) { .Case("xyx", SingleQubitBasis::XYX) .Case("u", SingleQubitBasis::U) .Case("zsxx", SingleQubitBasis::ZSXX) + .Case("zrx90", SingleQubitBasis::ZRX90) .Case("r", SingleQubitBasis::R) .Default(std::nullopt); } diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index 077e3aefc7..c73c885ff2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -739,7 +739,8 @@ static Value emitRuntimeEulerAngles(RewriterBase& rewriter, Location loc, const bool usesZYZAngles = basis == decomposition::SingleQubitBasis::ZYZ || basis == decomposition::SingleQubitBasis::ZXZ || - basis == decomposition::SingleQubitBasis::ZSXX; + basis == decomposition::SingleQubitBasis::ZSXX || + basis == decomposition::SingleQubitBasis::ZRX90; if (usesZYZAngles && isConstantAngle(theta)) { qubit = emitRotationIfNeeded(rewriter, loc, qubit, sumAngles(phi, lambda)); @@ -775,6 +776,24 @@ static Value emitRuntimeEulerAngles(RewriterBase& rewriter, Location loc, qubit = UOp::create(rewriter, loc, qubit, theta.v, phi.v, lambda.v) .getQubitOut(); break; + case decomposition::SingleQubitBasis::ZRX90: { + const auto halfPi = + Val::constant(rewriter, loc, std::numbers::pi / 2.); + if (isConstantAngle(theta, std::numbers::pi / 2.)) { + qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda - halfPi); + qubit = RXOp::create(rewriter, loc, qubit, halfPi.v).getQubitOut(); + qubit = emitRotationIfNeeded(rewriter, loc, qubit, phi + halfPi); + } else { + qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda); + qubit = RXOp::create(rewriter, loc, qubit, halfPi.v).getQubitOut(); + qubit = + emitRotationIfNeeded(rewriter, loc, qubit, theta + consts.pi); + qubit = RXOp::create(rewriter, loc, qubit, halfPi.v).getQubitOut(); + qubit = emitRotationIfNeeded(rewriter, loc, qubit, phi + consts.pi); + phase = phase + consts.pi; + } + break; + } case decomposition::SingleQubitBasis::ZSXX: if (isConstantAngle(theta, std::numbers::pi / 2.0)) { const auto halfPi = @@ -997,6 +1016,7 @@ struct MergeSingleQubitRotationGatesPattern final case decomposition::SingleQubitBasis::U: return 1; case decomposition::SingleQubitBasis::ZSXX: + case decomposition::SingleQubitBasis::ZRX90: return 5; case decomposition::SingleQubitBasis::ZYZ: case decomposition::SingleQubitBasis::ZXZ: @@ -1206,9 +1226,9 @@ void decomposition::synthesizeParameterizedUnitary1Q(RewriterBase& rewriter, unitary.getParameter(0), axis); return; } - const bool usesDirectZYZAngles = basis == SingleQubitBasis::ZYZ || - basis == SingleQubitBasis::ZXZ || - basis == SingleQubitBasis::ZSXX; + const bool usesDirectZYZAngles = + basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::ZXZ || + basis == SingleQubitBasis::ZSXX || basis == SingleQubitBasis::ZRX90; if (basis == SingleQubitBasis::U || usesDirectZYZAngles) { const auto consts = makeConsts(rewriter, op->getLoc()); Value qubit; diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 2afaa37c86..af648bec6c 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -24,6 +24,7 @@ #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Diagnostics.h" #include "mlir/IR/DialectRegistry.h" #include "mlir/IR/Location.h" #include "mlir/IR/MLIRContext.h" @@ -40,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -804,6 +806,129 @@ TEST(CompilerTargetTest, EnforcesExactOrderedOperationApplicability) { target.supportsOperation("device.operation", 3, 0, {30, 20, 10})); } +TEST(CompilerTargetTest, MatchesFixedParametersAndPreservesPlacements) { + const auto rotation = valid(OperationCapability::create( + "r", 1, 2, {valid(SiteTuple::create({0}))}, std::nullopt, std::nullopt, + {std::numbers::pi / 2., std::nullopt})); + const auto target = + valid(Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations({rotation}))); + EXPECT_FALSE(target.supports(GateKind::R)); + EXPECT_FALSE(target.supportsOperation("r", 1, 2)); + EXPECT_FALSE(target.synthesisBasis()); + const std::array site{0}; + EXPECT_TRUE(target.supportsOperation("r", 1, 2, site, + {std::numbers::pi / 2., std::nullopt})); + EXPECT_FALSE(target.supportsOperation("r", 1, 2, std::array{1}, + {std::numbers::pi / 2., 0.})); + EXPECT_FALSE(target.supportsOperation("r", 1, 2, site, {std::nullopt, 0.})); + EXPECT_FALSE(target.supportsOperation( + "r", 1, 2, site, {std::numbers::pi / 2. + 2. * std::numbers::pi, 0.})); + EXPECT_FALSE(target.supportsOperation("r", 1, 1, site, {1., 0.})); + EXPECT_TRUE(target.supportsOperation("r", 1, std::nullopt, site, + {std::numbers::pi / 2., 0.})); + + mlir::MLIRContext context; + context.loadDialect(); + const auto attribute = target.materialize(context); + const auto restored = valid(Target::create(attribute)); + EXPECT_EQ(restored.materialize(context), attribute); + EXPECT_EQ(restored.operations()[0].fixedParameters(), + rotation.fixedParameters()); + EXPECT_TRUE( + restored.supportsOperation("r", 1, 2, site, {std::numbers::pi / 2., 1.})); +} + +TEST(CompilerTargetTest, RejectsInvalidFixedParameters) { + expectInvalid( + OperationCapability::create("rx", 1, 1, {}, std::nullopt, std::nullopt, + {0., 1.}), + "Compiler target fixed parameters must match its parameter count"); + for (double value : { + std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN(), + }) { + expectInvalid(OperationCapability::create("rx", 1, 1, {}, std::nullopt, + std::nullopt, {value}), + "Compiler target fixed parameters must be finite"); + } + const auto unrestricted = valid( + OperationCapability::create("u", 1, 3, {}, std::nullopt, std::nullopt, + {std::nullopt, std::nullopt, std::nullopt})); + EXPECT_TRUE(unrestricted.fixedParameters().empty()); +} + +TEST(CompilerTargetTest, RejectsMalformedFixedParameterAttributes) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::ScopedDiagnosticHandler handler( + &context, [](mlir::Diagnostic&) { return mlir::success(); }); + for (const auto* parameters : { + "[unit, unit]", + "[1 : i64]", + "[1.0 : f32]", + "[0x7FF0000000000000 : f64]", + }) { + SCOPED_TRACE(parameters); + const auto source = + std::string{"#mqt.native_operation, " + "num_parameters = 1, fixed_parameters = "} + + parameters + ">"; + EXPECT_FALSE(mlir::parseAttribute(source, &context)); + } +} + +TEST(CompilerTargetTest, ChecksFixedValuesInsideNativeControls) { + const auto target = + valid(Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(OperationCapability::create( + "rz", Arity::variadic(1), 1, {}, + std::nullopt, std::nullopt, {0.25})), + }))); + mlir::MLIRContext context; + context.loadDialect(); + for (double angle : {0.25, 0.5}) { + auto program = mlir::qco::QCOProgramBuilder::build( + &context, [&](mlir::qco::QCOProgramBuilder& builder) { + auto [control, qubit] = builder.crz(angle, builder.staticQubit(0), + builder.staticQubit(1)); + builder.sink(control); + builder.sink(qubit); + return builder.intConstant(0); + }); + program->walk([&](mlir::qco::CtrlOp controlled) { + EXPECT_EQ(target.supports(controlled), angle == 0.25); + }); + } +} + +TEST(CompilerTargetTest, ResolvesFixedPulseBasisOnlyOnEverySite) { + auto operations = std::vector{ + valid(OperationCapability::create("rz", 1, 1)), + valid(OperationCapability::create( + "rx", 1, 1, {valid(SiteTuple::create({0}))}, std::nullopt, + std::nullopt, {std::numbers::pi / 2.})), + valid(OperationCapability::create("rxx", 2, 1, {}, std::nullopt, + std::nullopt, {std::numbers::pi / 4.})), + }; + EXPECT_FALSE( + valid(Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations(operations))) + .synthesisBasis()); + operations.emplace_back(valid(OperationCapability::create( + "rx", 1, 1, {valid(SiteTuple::create({1}))}, std::nullopt, std::nullopt, + {std::numbers::pi / 2.}))); + const auto target = + valid(Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations(operations))); + ASSERT_TRUE(target.synthesisBasis()); + EXPECT_EQ(target.synthesisBasis()->singleQubit, + Target::SingleQubitBasis::ZRX90); + EXPECT_FALSE(target.synthesisBasis()->entangler); +} + TEST(CompilerTargetTest, ResolvesSingleQubitBasisWithoutEntangler) { for (size_t numSites : {1U, 2U}) { SCOPED_TRACE(numSites); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index a3979af1da..72d21aebf7 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -11,6 +11,7 @@ #include "mqt/Compiler/Target.h" #include "mqt/Compiler/TargetEnvironment.h" #include "mqt/Dialect/MQT/IR/MQTDialect.h" +#include "mqt/Dialect/MQT/Utils/Parameters.h" #include "mqt/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mqt/Dialect/QCO/IR/QCODialect.h" #include "mqt/Dialect/QCO/IR/QCOOps.h" @@ -320,6 +321,71 @@ TEST_F(TargetSynthesisTest, TargetPassesRequireTypedEnvironment) { << diagnostics; } +TEST_F(TargetSynthesisTest, FixedPulseSynthesisPreservesFullUnitary) { + const auto target = valid(Target::create( + 2, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(OperationCapability::create("rx", 1, 1, {}, std::nullopt, + std::nullopt, + {std::numbers::pi / 2.})), + valid(OperationCapability::create("rz", 1, 1)), + valid(OperationCapability::create("cz", 2, 0)), + valid(OperationCapability::create("gphase", 0, 1)), + }))); + for (double theta : {0., 0.37, std::numbers::pi / 2., std::numbers::pi}) { + SCOPED_TRACE(theta); + const auto circuit = [&](QCOProgramBuilder& builder) { + auto q0 = builder.u(theta, 0.42, -0.31, builder.staticQubit(0)); + auto q1 = builder.rx(-0.73, builder.staticQubit(1)); + auto [control, targetQubit] = builder.cx(q0, q1); + builder.sink(control); + builder.sink(builder.ry(theta, targetQubit)); + return builder.intConstant(0); + }; + auto expected = build(circuit); + auto actual = build(circuit); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *actual, target, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded( + runPass(*actual, mlir::qco::createVerifyTargetConformance()))); + EXPECT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*actual))); + expectEquivalent(expected, actual); + actual->walk([&](mlir::qco::RXOp rotation) { + EXPECT_EQ(mlir::mqt::valueToDouble(rotation.getTheta()), + std::numbers::pi / 2.); + }); + } +} + +TEST_F(TargetSynthesisTest, FixedParametersRejectNonNativeValues) { + const auto target = valid(Target::create( + 1, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(OperationCapability::create("rz", 1, 1, {}, std::nullopt, + std::nullopt, {0.25})), + }))); + for (double angle : {0.25, 0.5}) { + auto program = build([&](QCOProgramBuilder& builder) { + auto qubit = builder.rz(angle, builder.staticQubit(0)); + builder.sink(qubit); + return builder.intConstant(0); + }); + if (angle == 0.25) { + EXPECT_TRUE(mlir::succeeded(runTargetPass( + *program, target, mlir::qco::createVerifyTargetConformance()))); + } else { + EXPECT_FALSE( + expectTargetFailure(*program, target, + mlir::qco::createVerifyTargetConformance()) + .empty()); + EXPECT_NE(expectTargetFailure(*program, target, + mlir::qco::createTargetNativeSynthesis()) + .find("no usable synthesis basis"), + std::string::npos); + } + } +} + TEST_F(TargetSynthesisTest, TwoQubitGateFusionRequiresStrictImprovement) { const auto adjacentCx = [](QCOProgramBuilder& builder) { const auto q0Input = builder.staticQubit(0); diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 612f68d4a1..adf86400e2 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -293,6 +293,8 @@ class CompilerTarget: site_tuples: Sequence[CompilerTarget.SiteTuple | Sequence[int]] | None = None, duration: int | None = None, fidelity: float | None = None, + *, + fixed_parameters: Sequence[float | None] = (), ) -> None: ... @property def name(self) -> str: @@ -314,6 +316,10 @@ class CompilerTarget: def site_tuples(self) -> list[CompilerTarget.SiteTuple]: """Supported ordered placements with optional calibration; empty means general applicability.""" + @property + def fixed_parameters(self) -> list[float | None]: + """Fixed values or None per parameter; empty means unrestricted. Constants use absolute tolerance 1e-15 without angle wrapping.""" + @property def duration(self) -> int | None: """The raw default duration, if available.""" @@ -374,6 +380,8 @@ class CompilerTarget: ZXZ = 6 + ZRX90 = 7 + class SynthesisBasis: """One synthesis basis usable across the complete target.""" @@ -484,9 +492,15 @@ class CompilerTarget: """A target-wide single-qubit basis with an optional entangler, or None when no single-qubit basis is usable.""" def supports_operation( - self, name: str, arity: int, num_parameters: int | None = None, sites: Sequence[int] | None = None + self, + name: str, + arity: int, + num_parameters: int | None = None, + sites: Sequence[int] | None = None, + *, + parameters: Sequence[float | None] = [], ) -> bool: - """Whether the target supports an operation.""" + """Whether the target supports an operation. Omitted or None parameter values require unrestricted support.""" class TargetEnvironment: """A compiler target and its selected payload specification.""" diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index bbd968c101..9c550d0ae2 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -655,6 +655,65 @@ def test_target_compiles_single_qubit_gates_without_entangler(num_sites: int) -> assert np.allclose(Operator(result).data, Operator(source).data) +@pytest.mark.parametrize("arity", [1, CompilerTarget.OperationArity.fixed(1)]) +def test_fixed_parameter_target_capability(arity: int | CompilerTarget.OperationArity) -> None: + """Fixed target values survive bindings and restrict support queries.""" + pulse = CompilerTarget.OperationCapability("rx", arity, 1, fixed_parameters=[np.pi / 2]) + assert pulse.fixed_parameters == [np.pi / 2] + target = CompilerTarget( + 1, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations([ + pulse, + CompilerTarget.OperationCapability("rz", 1, 1), + ]), + ) + assert target.synthesis_basis is not None + assert target.synthesis_basis.single_qubit == CompilerTarget.SingleQubitBasis.ZRX90 + assert not target.supports_operation("rx", 1, 1) + assert target.supports_operation("rx", 1, parameters=[np.pi / 2], sites=[0]) + assert not target.supports_operation("rx", 1, parameters=[np.pi]) + assert not target.supports_operation("rx", 1, parameters=[None]) + assert target.supports_operation("rz", 1, parameters=[None]) + with pytest.raises(ValueError, match="parameter count"): + CompilerTarget.OperationCapability("rx", arity, 1, fixed_parameters=[0.0, None]) + with pytest.raises(ValueError, match="finite"): + CompilerTarget.OperationCapability("rx", arity, 1, fixed_parameters=[np.inf]) + + +@requires_qiskit_translation +@pytest.mark.parametrize("theta", [0.0, np.pi / 2, np.pi, 0.47, "symbolic"]) +@pytest.mark.parametrize("gate", ["u", "rx", "p"]) +def test_fixed_pulse_compilation_preserves_phase(theta: float | str, gate: str) -> None: + """Compile into RZ and fixed RX pulses without changing global phase.""" + target = CompilerTarget( + 1, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations([ + CompilerTarget.OperationCapability("rx", 1, 1, fixed_parameters=[np.pi / 2]), + CompilerTarget.OperationCapability("rz", 1, 1), + CompilerTarget.OperationCapability("gphase", 0, 1), + ]), + ) + parameter = qiskit.circuit.Parameter("theta") if isinstance(theta, str) else theta + source = QuantumCircuit(1, global_phase=0.19) + if gate == "u": + source.u(parameter, 0.32, -0.17, 0) + else: + getattr(source, gate)(parameter, 0) + program = QCProgram.from_qiskit(source).to_qco() + program.compile_for_target(_test_target_environment(target)) + result = program.to_qiskit(target=target) + assert set(result.count_ops()) <= {"rx", "rz"} + assert all(item.operation.params == [np.pi / 2] for item in result.data if item.operation.name == "rx") + for value in [-0.6, 0.0, np.pi / 2, np.pi]: + bindings = {parameter: value} if isinstance(parameter, qiskit.circuit.Parameter) else {} + assert np.allclose( + Operator(result.assign_parameters(bindings)).data, + Operator(source.assign_parameters(bindings)).data, + ) + + @requires_qiskit_translation def test_target_compilation_exports_canonical_physical_qiskit_circuit() -> None: """Export a mapped program with the complete compiler target.""" From 04e0c705e40c3ac8e5209b573a2db86448876e98 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 08:42:36 +0000 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agent/plans/fixed-parameter-targets.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 2b611e62b5..faabdca956 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -28,7 +28,8 @@ rotation as an arbitrary rotation. Compiler and native-synthesis unit suites passed, including full-unitary phase comparisons, parameter restrictions, invalid attributes, and ordered placements. -Python binding tests cover fixed and symbolic input gates. Stub generation, repository lint, and full changed-file C++ lint passed. +Python binding tests cover fixed and symbolic input gates. Stub generation, +repository lint, and full changed-file C++ lint passed. ## Follow-up From 1b4c2fa26ef6f942d4be4565357a75e27d6649f7 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 11:09:17 +0200 Subject: [PATCH 03/11] =?UTF-8?q?=E2=9C=A8=20Derive=20synthesis=20from=20f?= =?UTF-8?q?ixed=20X/Y=20pulse=20angles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 馃 *AI text below* 馃 Build a reusable exact quarter-turn decomposition from the native pulse angle. Support either axis and sign, fractional and non-Clifford angles, and optional half turns without an angle catalog or another compiler pass. Bound pulse expansion and preserve phase for numeric and symbolic inputs. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 14 ++- bindings/mlir/register_mlir.cpp | 21 +++- docs/mlir/target_compilation.md | 14 ++- mlir/include/mqt/Compiler/Target.h | 29 +++-- .../QCO/Transforms/Decomposition/Euler.h | 20 ++-- mlir/lib/Compiler/Target.cpp | 99 ++++++++++++++-- .../QCO/Transforms/Decomposition/Euler.cpp | 105 ++++++++++++----- .../QCO/Transforms/Decomposition/Weyl.cpp | 3 +- .../NativeSynthesis/TargetSynthesis.cpp | 8 +- .../MergeSingleQubitRotationGates.cpp | 74 ++++++++---- .../Compiler/test_compiler_target.cpp | 17 ++- .../NativeSynthesis/test_target_synthesis.cpp | 107 ++++++++++++------ python/mqt/core/mlir.pyi | 20 +++- test/python/test_mlir.py | 22 +++- 14 files changed, 424 insertions(+), 129 deletions(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 2b611e62b5..ecbf7261b5 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -1,6 +1,6 @@ # Fixed-parameter compiler targets -Status: implementation and local validation complete. +Status: generic fixed-pulse synthesis implemented; validation in progress. ## Goal and scope @@ -9,9 +9,10 @@ values. Unspecified parameters remain unrestricted. Target matching, serialized attributes, synthesis-basis selection, and final verification must preserve the same restrictions. Symbolic values cannot satisfy a fixed parameter. -Add synthesis using arbitrary RZ and RX(蟺/2), using existing rotation -operations. This basis covers fixed X-axis pulses without new vendor gate -operations. Direct native-gate targets are a separate Core change. +Derive synthesis from arbitrary RZ and a target-declared fixed X/Y pulse. +Precompute an effective quarter-turn sequence from its actual angle; use native +half turns when available. Bound construction to 64 pulses per effective quarter +turn. Direct native-gate targets are a separate Core change. ## Decisions @@ -28,7 +29,10 @@ rotation as an arbitrary rotation. Compiler and native-synthesis unit suites passed, including full-unitary phase comparisons, parameter restrictions, invalid attributes, and ordered placements. -Python binding tests cover fixed and symbolic input gates. Stub generation, repository lint, and full changed-file C++ lint passed. +Python binding tests cover fixed and symbolic input gates. Earlier validation +passed for the initial fixed-pulse implementation. The generic construction +passes full-unitary tests across both axes, signs, fractional and non-Clifford +angles, and optional half turns. Final Python and lint checks remain. ## Follow-up diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 7fc4bf3472..534d040cef 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -916,7 +916,21 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); .value("XYX", mlir::CompilerTarget::SingleQubitBasis::XYX) .value("ZYZ", mlir::CompilerTarget::SingleQubitBasis::ZYZ) .value("ZXZ", mlir::CompilerTarget::SingleQubitBasis::ZXZ) - .value("ZRX90", mlir::CompilerTarget::SingleQubitBasis::ZRX90); + .value("ZFixedRotation", + mlir::CompilerTarget::SingleQubitBasis::ZFixedRotation); + + nb::class_( + compilerTarget, "FixedRotationBasis", + "Fixed X/Y pulse selected for synthesis with arbitrary RZ.") + .def_ro("gate", &mlir::CompilerTarget::FixedRotationBasis::gate) + .def_ro("angle", &mlir::CompilerTarget::FixedRotationBasis::angle, + "Native pulse angle in radians.") + .def_prop_ro("quarter_turn_pulses", + [](const mlir::CompilerTarget::FixedRotationBasis& basis) { + return basis.quarterTurnZAngles.size() - 1; + }) + .def_ro("half_turn_angle", + &mlir::CompilerTarget::FixedRotationBasis::halfTurnAngle); auto synthesisBasis = nb::class_( compilerTarget, "SynthesisBasis", @@ -926,7 +940,10 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); &mlir::CompilerTarget::SynthesisBasis::singleQubit, "The single-qubit synthesis basis.") .def_ro("entangler", &mlir::CompilerTarget::SynthesisBasis::entangler, - "The two-qubit entangler, or None when none is usable."); + "The two-qubit entangler, or None when none is usable.") + .def_ro("fixed_rotation", + &mlir::CompilerTarget::SynthesisBasis::fixedRotation, + "Fixed-pulse decomposition, or None for other bases."); nb::enum_( compilerTarget, "ConnectivityKind", "The target connectivity model.") diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 982622d689..c92b1e8a45 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -172,9 +172,17 @@ accepts only RX(蟺/2). A nonempty list has one entry per parameter; `None` leave that parameter unrestricted. Multiple capabilities can describe different fixed values or placements. Constants match with absolute tolerance `1e-15`, without angle wrapping; symbolic values do not match fixed values. Omit the list for -unrestricted parameters. The compiler can synthesize arbitrary one-qubit gates -with RZ and fixed RX(蟺/2) on every site. Other fixed-angle sets are accepted for -matching native operations but do not automatically provide a synthesis basis. +unrestricted parameters. With unrestricted RZ, the compiler derives a synthesis +sequence from a fixed RX or RY angle available on every site. This covers +positive and negative quarter turns, 45掳 pulses, and non-Clifford angles such as +0.37 radians. The sequence is computed once per target and reused for numeric +and symbolic input gates. Available RX/RY half turns shorten suitable +decompositions. + +Zero and integer-蟺 pulses do not supply the required mixing. The constructive +method also rejects angles that require more than 64 fixed pulses per effective +quarter turn, to bound circuit expansion. These restrictions affect synthesis; +matching fixed native operations remains available for every finite angle. Use plain tuples for placements without calibration. Use `CompilerTarget.SiteTuple([1, 0], duration=40, fidelity=0.99)` to attach diff --git a/mlir/include/mqt/Compiler/Target.h b/mlir/include/mqt/Compiler/Target.h index c0b89fdb50..e53315f017 100644 --- a/mlir/include/mqt/Compiler/Target.h +++ b/mlir/include/mqt/Compiler/Target.h @@ -303,20 +303,33 @@ class CompilerTarget { /// Recognized globally usable single-qubit synthesis basis. enum class SingleQubitBasis : uint8_t { - U, ///< `U(胃, 蠁, 位)`. - ZSXX, ///< `RZ` / `SX` / `X` synthesis via a ZYZ decomposition. - R, ///< XYX synthesis expressed with `R(胃, 蠁)`. - XZX, ///< `RX(蠁) * RZ(胃) * RX(位)`. - XYX, ///< `RX(蠁) * RY(胃) * RX(位)`. - ZYZ, ///< `RZ(蠁) * RY(胃) * RZ(位)`. - ZXZ, ///< `RZ(蠁) * RX(胃) * RZ(位)`. - ZRX90, ///< Arbitrary `RZ` and fixed `RX(蟺/2)` pulses. + U, ///< `U(胃, 蠁, 位)`. + ZSXX, ///< `RZ` / `SX` / `X` synthesis via a ZYZ decomposition. + R, ///< XYX synthesis expressed with `R(胃, 蠁)`. + XZX, ///< `RX(蠁) * RZ(胃) * RX(位)`. + XYX, ///< `RX(蠁) * RY(胃) * RX(位)`. + ZYZ, ///< `RZ(蠁) * RY(胃) * RZ(位)`. + ZXZ, ///< `RZ(蠁) * RX(胃) * RZ(位)`. + ZFixedRotation, ///< Arbitrary `RZ` and fixed X/Y rotation pulses. + }; + + /// Fixed X/Y pulse used to implement an effective positive RX(蟺/2). + struct FixedRotationBasis { + GateKind gate; + double angle; + /// RZ angles before, between, and after copies of the fixed pulse. + std::vector quarterTurnZAngles; + std::optional halfTurnAngle; + + friend bool operator==(const FixedRotationBasis&, + const FixedRotationBasis&) = default; }; /// One single-qubit basis and optional entangler usable across the target. struct SynthesisBasis { SingleQubitBasis singleQubit; std::optional entangler; + std::optional fixedRotation; friend bool operator==(const SynthesisBasis&, const SynthesisBasis&) = default; diff --git a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h index d1a62dd328..22bfb189d2 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -60,8 +60,10 @@ struct SynthesizedUnitary1Q { }; /// Returns whether @p op belongs to @p basis. -[[nodiscard]] bool isSingleQubitBasisGate(Operation* op, - SingleQubitBasis basis); +/// Fixed-pulse bases require their target's @p fixedRotation descriptor. +[[nodiscard]] bool isSingleQubitBasisGate( + Operation* op, SingleQubitBasis basis, + const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr); /// Extracts `(theta, phi, lambda, phase)` of @p matrix in @p basis. /// @@ -86,10 +88,10 @@ struct SynthesizedUnitary1Q { /// @param basis The single-qubit synthesis basis. /// @return The synthesized qubit and correction, or `std::nullopt` if synthesis /// is skipped. -[[nodiscard]] std::optional -synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, - const Matrix2x2& composed, std::size_t runSize, - bool hasNonBasisGate, SingleQubitBasis basis); +[[nodiscard]] std::optional synthesizeUnitary1QEuler( + OpBuilder& builder, Location loc, Value qubit, const Matrix2x2& composed, + std::size_t runSize, bool hasNonBasisGate, SingleQubitBasis basis, + const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr); /// Materializes one accumulated phase correction when needed. /// @@ -104,10 +106,12 @@ void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase); /// Synthesizes one supported runtime-parameterized operation in @p basis. /// /// Leaves operations that already belong to @p basis unchanged. +/// Fixed-pulse bases require their target's @p fixedRotation descriptor. /// /// @pre `canSynthesizeParameterizedUnitary1Q(op)` is true. -void synthesizeParameterizedUnitary1Q(RewriterBase& rewriter, Operation* op, - SingleQubitBasis basis); +void synthesizeParameterizedUnitary1Q( + RewriterBase& rewriter, Operation* op, SingleQubitBasis basis, + const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr); /// Populates @p patterns with the single-qubit run fusion rewrite for /// @p basis (the reusable core of `fuse-single-qubit-unitary-runs`). diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index b435f0c675..dcb27204fb 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -159,6 +159,59 @@ constexpr std::array GATE_SPECIFICATIONS{ }, }; +// Construct an effective RX(蟺/2) from a fixed X/Y pulse and free Z rotations. +// The two-pulse construction has reachable polar angle 2 asin(|sin(angle)|). +static std::optional +makeFixedRotationBasis(GateKind gate, double angle) { + constexpr double pi = std::numbers::pi; + constexpr double halfPi = pi / 2.; + constexpr size_t maxPulses = 64; + const double magnitude = std::abs(angle); + if (magnitude <= mqt::PARAMETER_COMPARISON_TOLERANCE) { + return std::nullopt; + } + const double directCount = std::round(halfPi / magnitude); + if (directCount >= 1. && directCount <= static_cast(maxPulses) && + std::abs(directCount * magnitude - halfPi) <= + mqt::PARAMETER_COMPARISON_TOLERANCE) { + std::vector zAngles(static_cast(directCount) + 1, 0.); + const double axis = + (gate == GateKind::RY ? halfPi : 0.) + (angle < 0. ? pi : 0.); + zAngles.front() = axis; + zAngles.back() = -axis; + return CompilerTarget::FixedRotationBasis{gate, angle, std::move(zAngles), + std::nullopt}; + } + const double sine = std::sin(angle); + const double reach = 2. * std::asin(std::min(1., std::abs(sine))); + if (reach <= mqt::PARAMETER_COMPARISON_TOLERANCE) { + return std::nullopt; + } + const double count = std::ceil(halfPi / reach); + if (count > static_cast(maxPulses / 2)) { + return std::nullopt; + } + const auto blocks = static_cast(count); + const double theta = halfPi / count; + const double cosine = + std::clamp(std::sin(theta / 2.) / std::abs(sine), 0., 1.); + const double middle = 2. * std::acos(cosine); + const double gamma = + std::atan2(std::sin(middle / 2.), std::cos(angle) * cosine); + const double eta = gate == GateKind::RX ? (sine < 0. ? halfPi : -halfPi) + : (sine < 0. ? pi : 0.); + const double before = halfPi - gamma + eta; + const double after = -gamma - eta - halfPi; + std::vector zAngles(2 * blocks + 1); + zAngles.front() = before; + for (size_t block = 0; block < blocks; ++block) { + zAngles[2 * block + 1] = middle; + zAngles[2 * block + 2] = block + 1 == blocks ? after : after + before; + } + return CompilerTarget::FixedRotationBasis{gate, angle, std::move(zAngles), + std::nullopt}; +} + } // namespace [[nodiscard]] static std::string canonicalOperationName(StringRef name) { @@ -796,6 +849,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { }); }; std::optional singleQubit; + std::optional fixedRotation; if (supportsOnEverySite(GateKind::U)) { singleQubit = SingleQubitBasis::U; } else if (supportsOnEverySite(GateKind::X) && @@ -813,13 +867,43 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { } else if (supportsOnEverySite(GateKind::RY) && supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZYZ; - } else if (supportsOnEverySite(GateKind::RZ) && - llvm::all_of(siteIds, [&](SiteId site) { - return supportsOperation( - "rx", 1, 1, ArrayRef(&site, 1), false, - [](size_t) { return std::optional{std::numbers::pi / 2.}; }); - })) { - singleQubit = SingleQubitBasis::ZRX90; + } else if (supportsOnEverySite(GateKind::RZ)) { + const auto supportsPulse = [&](StringRef name, double angle) { + return llvm::all_of(siteIds, [&](SiteId site) { + return supportsOperation( + name, 1, 1, ArrayRef(&site, 1), false, + [angle](size_t) { return std::optional{angle}; }); + }); + }; + for (const auto& operation : operations) { + if ((operation.canonicalName() != "rx" && + operation.canonicalName() != "ry") || + operation.numParameters() != 1 || + operation.fixedParameters().empty() || + !operation.fixedParameters()[0]) { + continue; + } + const auto gate = + operation.canonicalName() == "rx" ? GateKind::RX : GateKind::RY; + auto candidate = + makeFixedRotationBasis(gate, *operation.fixedParameters()[0]); + if (!candidate || + (fixedRotation && candidate->quarterTurnZAngles.size() >= + fixedRotation->quarterTurnZAngles.size()) || + !supportsPulse(operation.canonicalName(), candidate->angle)) { + continue; + } + for (double half : {std::numbers::pi, -std::numbers::pi}) { + if (supportsPulse(operation.canonicalName(), half)) { + candidate->halfTurnAngle = half; + break; + } + } + fixedRotation = std::move(candidate); + } + if (fixedRotation) { + singleQubit = SingleQubitBasis::ZFixedRotation; + } } const auto supportsOnEveryCoupling = [&](GateKind gate) { @@ -875,6 +959,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { .entangler = entangler == entanglerPreference.end() ? std::nullopt : std::optional{*entangler}, + .fixedRotation = fixedRotation, }; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 594c54aa06..d7e9a3cde5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -20,9 +20,11 @@ #include "mlir/IR/Value.h" #include "mlir/Support/LLVM.h" +#include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/TypeSwitch.h" #include "llvm/Support/ErrorHandling.h" +#include #include #include #include @@ -32,26 +34,36 @@ namespace mlir::qco::decomposition { -bool isSingleQubitBasisGate(Operation* op, SingleQubitBasis basis) { +bool isSingleQubitBasisGate( + Operation* op, SingleQubitBasis basis, + const CompilerTarget::FixedRotationBasis* fixedRotation) { return TypeSwitch(op) .Case([&](RZOp) { return basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::ZXZ || basis == SingleQubitBasis::XZX || basis == SingleQubitBasis::ZSXX || - basis == SingleQubitBasis::ZRX90; + basis == SingleQubitBasis::ZFixedRotation; }) - .Case([&](RYOp) { - return basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::XYX; - }) - .Case([&](RXOp rotation) { - if (basis == SingleQubitBasis::ZRX90) { + .Case([&](auto rotation) { + const bool isX = isa(rotation); + if (basis == SingleQubitBasis::ZFixedRotation) { + if (!fixedRotation || + isX != (fixedRotation->gate == CompilerTarget::GateKind::RX)) { + return false; + } const auto angle = mqt::valueToDouble(rotation.getTheta()); - return angle && std::abs(*angle - std::numbers::pi / 2.) <= - mqt::PARAMETER_COMPARISON_TOLERANCE; + return angle && (std::abs(*angle - fixedRotation->angle) <= + mqt::PARAMETER_COMPARISON_TOLERANCE || + (fixedRotation->halfTurnAngle && + std::abs(*angle - *fixedRotation->halfTurnAngle) <= + mqt::PARAMETER_COMPARISON_TOLERANCE)); } - return basis == SingleQubitBasis::ZXZ || - basis == SingleQubitBasis::XZX || basis == SingleQubitBasis::XYX; + return isX ? (basis == SingleQubitBasis::ZXZ || + basis == SingleQubitBasis::XZX || + basis == SingleQubitBasis::XYX) + : (basis == SingleQubitBasis::ZYZ || + basis == SingleQubitBasis::XYX); }) .Case([&](UOp) { return basis == SingleQubitBasis::U; }) .Case([&](auto) { return basis == SingleQubitBasis::ZSXX; }) @@ -199,7 +211,7 @@ EulerAngles anglesFromUnitary(const Matrix2x2& matrix, switch (basis) { case SingleQubitBasis::ZYZ: case SingleQubitBasis::ZSXX: - case SingleQubitBasis::ZRX90: + case SingleQubitBasis::ZFixedRotation: return paramsZYZ(matrix); case SingleQubitBasis::ZXZ: return paramsZXZ(matrix); @@ -247,9 +259,18 @@ struct Unitary1QEulerPlan { /// @param kind The rotation axis (RZ/RY/RX) /// @param angle The rotation angle in radians. void appendRotation(const SynthesisStep::Kind kind, const double angle) { - if (!isNearZeroRotationAngle(angle)) { - steps.emplace_back(kind, angle); + if (isNearZeroRotationAngle(angle)) { + return; } + if (kind == SynthesisStep::Kind::RZ && !steps.empty() && + steps.back().kind == kind) { + steps.back().theta += angle; + if (isNearZeroRotationAngle(steps.back().theta)) { + steps.pop_back(); + } + return; + } + steps.emplace_back(kind, angle); } /// Appends a native `R(angle, axis)` step for non-negligible angles. @@ -267,8 +288,9 @@ struct Unitary1QEulerPlan { /// /// @param angles The angles to use for the decomposition. /// @param basis The basis to use for the decomposition. - void appendDecomposition(const EulerAngles& angles, - const SingleQubitBasis basis) { + void + appendDecomposition(const EulerAngles& angles, const SingleQubitBasis basis, + const CompilerTarget::FixedRotationBasis* fixedRotation) { if (isNearZeroRotationAngle(angles.theta) && isNearZeroRotationAngle(angles.phi) && isNearZeroRotationAngle(angles.lambda)) { @@ -281,7 +303,7 @@ struct Unitary1QEulerPlan { case SingleQubitBasis::ZYZ: case SingleQubitBasis::ZXZ: case SingleQubitBasis::ZSXX: - case SingleQubitBasis::ZRX90: + case SingleQubitBasis::ZFixedRotation: appendRotation(SynthesisStep::Kind::RZ, angles.phi + angles.lambda); break; @@ -338,19 +360,40 @@ struct Unitary1QEulerPlan { angles.lambda); phase = angles.phase; break; - case SingleQubitBasis::ZRX90: { + case SingleQubitBasis::ZFixedRotation: { + assert(fixedRotation && + "fixed-pulse synthesis requires a pulse descriptor"); constexpr double pi = std::numbers::pi; constexpr double halfPi = pi / 2.; + const auto kind = fixedRotation->gate == CompilerTarget::GateKind::RX + ? SynthesisStep::Kind::RX + : SynthesisStep::Kind::RY; + const double axis = kind == SynthesisStep::Kind::RY ? halfPi : 0.; + const auto quarterTurn = [&] { + appendRotation(SynthesisStep::Kind::RZ, + fixedRotation->quarterTurnZAngles.front()); + for (double zAngle : + ArrayRef(fixedRotation->quarterTurnZAngles).drop_front()) { + steps.emplace_back(kind, fixedRotation->angle); + appendRotation(SynthesisStep::Kind::RZ, zAngle); + } + }; if (isNearZeroRotationAngle(angles.theta - halfPi)) { appendRotation(SynthesisStep::Kind::RZ, angles.lambda - halfPi); - steps.emplace_back(SynthesisStep::Kind::RX, halfPi); + quarterTurn(); appendRotation(SynthesisStep::Kind::RZ, angles.phi + halfPi); phase = angles.phase; + } else if (isNearZeroRotationAngle(angles.theta - pi) && + fixedRotation->halfTurnAngle) { + appendRotation(SynthesisStep::Kind::RZ, angles.lambda + axis); + steps.emplace_back(kind, *fixedRotation->halfTurnAngle); + appendRotation(SynthesisStep::Kind::RZ, angles.phi + pi - axis); + phase = angles.phase + (*fixedRotation->halfTurnAngle < 0. ? pi : 0.); } else { appendRotation(SynthesisStep::Kind::RZ, angles.lambda); - steps.emplace_back(SynthesisStep::Kind::RX, halfPi); + quarterTurn(); appendRotation(SynthesisStep::Kind::RZ, angles.theta + pi); - steps.emplace_back(SynthesisStep::Kind::RX, halfPi); + quarterTurn(); appendRotation(SynthesisStep::Kind::RZ, angles.phi + pi); phase = angles.phase + pi; } @@ -394,15 +437,15 @@ struct Unitary1QEulerPlan { /// @param basis Native gate basis. /// @return Planned gate sequence and optional global phase. [[nodiscard]] static Unitary1QEulerPlan -planUnitary1QEuler(const Matrix2x2& targetMatrix, - const SingleQubitBasis basis) { +planUnitary1QEuler(const Matrix2x2& targetMatrix, const SingleQubitBasis basis, + const CompilerTarget::FixedRotationBasis* fixedRotation) { Unitary1QEulerPlan plan; if (targetMatrix.isApprox(Matrix2x2::identity())) { return plan; } const EulerAngles angles = anglesFromUnitary(targetMatrix, basis); - plan.appendDecomposition(angles, basis); + plan.appendDecomposition(angles, basis, fixedRotation); return plan; } @@ -454,17 +497,17 @@ std::optional parseSingleQubitBasis(StringRef basis) { .Case("xyx", SingleQubitBasis::XYX) .Case("u", SingleQubitBasis::U) .Case("zsxx", SingleQubitBasis::ZSXX) - .Case("zrx90", SingleQubitBasis::ZRX90) .Case("r", SingleQubitBasis::R) .Default(std::nullopt); } -std::optional -synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, - const Matrix2x2& composed, const std::size_t runSize, - const bool hasNonBasisGate, - const SingleQubitBasis basis) { - const Unitary1QEulerPlan plan = planUnitary1QEuler(composed, basis); +std::optional synthesizeUnitary1QEuler( + OpBuilder& builder, Location loc, Value qubit, const Matrix2x2& composed, + const std::size_t runSize, const bool hasNonBasisGate, + const SingleQubitBasis basis, + const CompilerTarget::FixedRotationBasis* fixedRotation) { + const Unitary1QEulerPlan plan = + planUnitary1QEuler(composed, basis, fixedRotation); if (!hasNonBasisGate && runSize <= plan.gateCount()) { return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 330c2f3ba4..cf4ff20cb0 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -926,7 +926,8 @@ emitUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value qubit1, const auto emitFactor = [&](Value& wire, std::size_t index) { const auto synthesized = synthesizeUnitary1QEuler( builder, loc, wire, factors[index], /*runSize=*/0, - /*hasNonBasisGate=*/true, basis.singleQubit); + /*hasNonBasisGate=*/true, basis.singleQubit, + basis.fixedRotation ? &*basis.fixedRotation : nullptr); wire = synthesized->qubit; globalPhase += synthesized->globalPhase; }; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 281e920fab..4866e27aee 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -528,13 +528,15 @@ static LogicalResult synthesizeTargetOperation( return unsupported( "its unitary matrix is not available at compile time"); } - decomposition::synthesizeParameterizedUnitary1Q(rewriter, operation, - basis->singleQubit); + decomposition::synthesizeParameterizedUnitary1Q( + rewriter, operation, basis->singleQubit, + basis->fixedRotation ? &*basis->fixedRotation : nullptr); return success(); } const auto synthesized = decomposition::synthesizeUnitary1QEuler( rewriter, operation->getLoc(), op.getInputQubit(0), matrix, - /*runSize=*/1, /*hasNonBasisGate=*/true, basis->singleQubit); + /*runSize=*/1, /*hasNonBasisGate=*/true, basis->singleQubit, + basis->fixedRotation ? &*basis->fixedRotation : nullptr); if (!synthesized) { llvm::reportFatalInternalError( "target single-qubit basis failed to synthesize a unitary matrix"); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index c73c885ff2..5f9abedfeb 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -731,16 +731,18 @@ static void emitParameterizedGPhaseIfNeeded(RewriterBase& rewriter, } } -static Value emitRuntimeEulerAngles(RewriterBase& rewriter, Location loc, - Value qubit, RuntimeEulerAngles angles, - decomposition::SingleQubitBasis basis, - const ScalarConsts& consts) { +static Value emitRuntimeEulerAngles( + RewriterBase& rewriter, Location loc, Value qubit, + RuntimeEulerAngles angles, decomposition::SingleQubitBasis basis, + const ScalarConsts& consts, + const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr) { auto [theta, phi, lambda, phase] = angles; - const bool usesZYZAngles = basis == decomposition::SingleQubitBasis::ZYZ || - basis == decomposition::SingleQubitBasis::ZXZ || - basis == decomposition::SingleQubitBasis::ZSXX || - basis == decomposition::SingleQubitBasis::ZRX90; + const bool usesZYZAngles = + basis == decomposition::SingleQubitBasis::ZYZ || + basis == decomposition::SingleQubitBasis::ZXZ || + basis == decomposition::SingleQubitBasis::ZSXX || + basis == decomposition::SingleQubitBasis::ZFixedRotation; if (usesZYZAngles && isConstantAngle(theta)) { qubit = emitRotationIfNeeded(rewriter, loc, qubit, sumAngles(phi, lambda)); @@ -776,19 +778,47 @@ static Value emitRuntimeEulerAngles(RewriterBase& rewriter, Location loc, qubit = UOp::create(rewriter, loc, qubit, theta.v, phi.v, lambda.v) .getQubitOut(); break; - case decomposition::SingleQubitBasis::ZRX90: { + case decomposition::SingleQubitBasis::ZFixedRotation: { + assert(fixedRotation && + "fixed-pulse synthesis requires a pulse descriptor"); const auto halfPi = Val::constant(rewriter, loc, std::numbers::pi / 2.); + const bool isX = fixedRotation->gate == CompilerTarget::GateKind::RX; + const auto axis = + Val::constant(rewriter, loc, isX ? 0. : std::numbers::pi / 2.); + const auto emitPulse = [&](double angle) { + qubit = isX ? RXOp::create(rewriter, loc, qubit, angle).getQubitOut() + : RYOp::create(rewriter, loc, qubit, angle).getQubitOut(); + }; + const auto quarterTurn = [&] { + for (const auto [index, zAngle] : + llvm::enumerate(fixedRotation->quarterTurnZAngles)) { + if (index != 0) { + emitPulse(fixedRotation->angle); + } + qubit = emitRotationIfNeeded( + rewriter, loc, qubit, Val::constant(rewriter, loc, zAngle)); + } + }; if (isConstantAngle(theta, std::numbers::pi / 2.)) { qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda - halfPi); - qubit = RXOp::create(rewriter, loc, qubit, halfPi.v).getQubitOut(); + quarterTurn(); qubit = emitRotationIfNeeded(rewriter, loc, qubit, phi + halfPi); + } else if (isConstantAngle(theta, std::numbers::pi) && + fixedRotation->halfTurnAngle) { + qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda + axis); + emitPulse(*fixedRotation->halfTurnAngle); + qubit = emitRotationIfNeeded(rewriter, loc, qubit, + phi + consts.pi - axis); + if (*fixedRotation->halfTurnAngle < 0.) { + phase = phase + consts.pi; + } } else { qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda); - qubit = RXOp::create(rewriter, loc, qubit, halfPi.v).getQubitOut(); + quarterTurn(); qubit = emitRotationIfNeeded(rewriter, loc, qubit, theta + consts.pi); - qubit = RXOp::create(rewriter, loc, qubit, halfPi.v).getQubitOut(); + quarterTurn(); qubit = emitRotationIfNeeded(rewriter, loc, qubit, phi + consts.pi); phase = phase + consts.pi; } @@ -1016,7 +1046,7 @@ struct MergeSingleQubitRotationGatesPattern final case decomposition::SingleQubitBasis::U: return 1; case decomposition::SingleQubitBasis::ZSXX: - case decomposition::SingleQubitBasis::ZRX90: + case decomposition::SingleQubitBasis::ZFixedRotation: return 5; case decomposition::SingleQubitBasis::ZYZ: case decomposition::SingleQubitBasis::ZXZ: @@ -1208,12 +1238,12 @@ bool decomposition::canSynthesizeParameterizedUnitary1Q(Operation* op) { return op != nullptr && isa(op); } -void decomposition::synthesizeParameterizedUnitary1Q(RewriterBase& rewriter, - Operation* op, - SingleQubitBasis basis) { +void decomposition::synthesizeParameterizedUnitary1Q( + RewriterBase& rewriter, Operation* op, SingleQubitBasis basis, + const CompilerTarget::FixedRotationBasis* fixedRotation) { assert(canSynthesizeParameterizedUnitary1Q(op) && "operation must support parameterized one-qubit synthesis"); - if (isSingleQubitBasisGate(op, basis)) { + if (isSingleQubitBasisGate(op, basis, fixedRotation)) { return; } @@ -1226,9 +1256,10 @@ void decomposition::synthesizeParameterizedUnitary1Q(RewriterBase& rewriter, unitary.getParameter(0), axis); return; } - const bool usesDirectZYZAngles = - basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::ZXZ || - basis == SingleQubitBasis::ZSXX || basis == SingleQubitBasis::ZRX90; + const bool usesDirectZYZAngles = basis == SingleQubitBasis::ZYZ || + basis == SingleQubitBasis::ZXZ || + basis == SingleQubitBasis::ZSXX || + basis == SingleQubitBasis::ZFixedRotation; if (basis == SingleQubitBasis::U || usesDirectZYZAngles) { const auto consts = makeConsts(rewriter, op->getLoc()); Value qubit; @@ -1237,7 +1268,8 @@ void decomposition::synthesizeParameterizedUnitary1Q(RewriterBase& rewriter, } else { qubit = emitRuntimeEulerAngles( rewriter, op->getLoc(), unitary.getInputQubit(0), - directZYZAnglesFromGate(unitary, rewriter, consts), basis, consts); + directZYZAnglesFromGate(unitary, rewriter, consts), basis, consts, + fixedRotation); } rewriter.replaceOp(op, qubit); return; diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index af648bec6c..5a14b1ac69 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -925,10 +925,25 @@ TEST(CompilerTargetTest, ResolvesFixedPulseBasisOnlyOnEverySite) { NativeOperations::fromOperations(operations))); ASSERT_TRUE(target.synthesisBasis()); EXPECT_EQ(target.synthesisBasis()->singleQubit, - Target::SingleQubitBasis::ZRX90); + Target::SingleQubitBasis::ZFixedRotation); EXPECT_FALSE(target.synthesisBasis()->entangler); } +TEST(CompilerTargetTest, RejectsDegenerateOrExcessiveFixedPulseSynthesis) { + for (double angle : {0., std::numbers::pi, 2. * std::numbers::pi, 1e-8}) { + const auto target = valid(Target::create( + 1, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(OperationCapability::create("rz", 1, 1)), + valid(OperationCapability::create("rx", 1, 1, {}, std::nullopt, + std::nullopt, {angle})), + }))); + EXPECT_FALSE(target.synthesisBasis()); + EXPECT_TRUE(target.supportsOperation( + "rx", 1, 1, std::nullopt, std::array, 1>{angle})); + } +} + TEST(CompilerTargetTest, ResolvesSingleQubitBasisWithoutEntangler) { for (size_t numSites : {1U, 2U}) { SCOPED_TRACE(numSites); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index 72d21aebf7..13be99ccbd 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -322,38 +322,81 @@ TEST_F(TargetSynthesisTest, TargetPassesRequireTypedEnvironment) { } TEST_F(TargetSynthesisTest, FixedPulseSynthesisPreservesFullUnitary) { - const auto target = valid(Target::create( - 2, Connectivity::allToAll(), - NativeOperations::fromOperations({ - valid(OperationCapability::create("rx", 1, 1, {}, std::nullopt, - std::nullopt, - {std::numbers::pi / 2.})), - valid(OperationCapability::create("rz", 1, 1)), - valid(OperationCapability::create("cz", 2, 0)), - valid(OperationCapability::create("gphase", 0, 1)), - }))); - for (double theta : {0., 0.37, std::numbers::pi / 2., std::numbers::pi}) { - SCOPED_TRACE(theta); - const auto circuit = [&](QCOProgramBuilder& builder) { - auto q0 = builder.u(theta, 0.42, -0.31, builder.staticQubit(0)); - auto q1 = builder.rx(-0.73, builder.staticQubit(1)); - auto [control, targetQubit] = builder.cx(q0, q1); - builder.sink(control); - builder.sink(builder.ry(theta, targetQubit)); - return builder.intConstant(0); - }; - auto expected = build(circuit); - auto actual = build(circuit); - ASSERT_TRUE(mlir::succeeded(runTargetPass( - *actual, target, mlir::qco::createTargetNativeSynthesis()))); - ASSERT_TRUE(mlir::succeeded( - runPass(*actual, mlir::qco::createVerifyTargetConformance()))); - EXPECT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*actual))); - expectEquivalent(expected, actual); - actual->walk([&](mlir::qco::RXOp rotation) { - EXPECT_EQ(mlir::mqt::valueToDouble(rotation.getTheta()), - std::numbers::pi / 2.); - }); + for (const auto name : {"rx", "ry"}) { + for (double pulseAngle : + {std::numbers::pi / 2., -std::numbers::pi / 2., std::numbers::pi / 4., + -std::numbers::pi / 4., std::numbers::pi / 3., .37, -.73, 2.7, 4.2}) { + for (const std::optional halfTurn : + {std::optional{}, std::optional{std::numbers::pi}, + std::optional{-std::numbers::pi}}) { + SCOPED_TRACE(testing::Message() << name << " " << pulseAngle << " " + << halfTurn.value_or(0.)); + std::vector operations{ + valid(OperationCapability::create(name, 1, 1, {}, std::nullopt, + std::nullopt, {pulseAngle})), + valid(OperationCapability::create("rz", 1, 1)), + valid(OperationCapability::create("cz", 2, 0)), + valid(OperationCapability::create("gphase", 0, 1)), + }; + if (halfTurn) { + operations.push_back(valid(OperationCapability::create( + name, 1, 1, {}, std::nullopt, std::nullopt, {*halfTurn}))); + } + const auto target = + valid(Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations(operations))); + ASSERT_TRUE(target.synthesisBasis()); + for (double theta : + {0., .37, std::numbers::pi / 2., std::numbers::pi}) { + const auto circuit = [&](QCOProgramBuilder& builder) { + auto q0 = builder.u(theta, .42, -.31, builder.staticQubit(0)); + auto q1 = builder.rx(-.73, builder.staticQubit(1)); + auto [control, targetQubit] = builder.cx(q0, q1); + builder.sink(control); + builder.sink(builder.ry(theta, targetQubit)); + return builder.intConstant(0); + }; + auto expected = build(circuit); + auto actual = build(circuit); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *actual, target, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded( + runPass(*actual, mlir::qco::createVerifyTargetConformance()))); + EXPECT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*actual))); + expectEquivalent(expected, actual); + } + } + } + } +} + +TEST_F(TargetSynthesisTest, FixedHalfTurnUsesOnePulse) { + for (const auto name : {"rx", "ry"}) { + for (double half : {std::numbers::pi, -std::numbers::pi}) { + const auto target = valid(Target::create( + 1, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(OperationCapability::create(name, 1, 1, {}, std::nullopt, + std::nullopt, {.37})), + valid(OperationCapability::create(name, 1, 1, {}, std::nullopt, + std::nullopt, {half})), + valid(OperationCapability::create("rz", 1, 1)), + valid(OperationCapability::create("gphase", 0, 1)), + }))); + const auto circuit = [](QCOProgramBuilder& builder) { + builder.sink( + builder.u(std::numbers::pi, .42, -.31, builder.staticQubit(0))); + return builder.intConstant(0); + }; + auto expected = build(circuit); + auto actual = build(circuit); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *actual, target, mlir::qco::createTargetNativeSynthesis()))); + expectEquivalent(expected, actual); + EXPECT_EQ(countOps(*actual) + + countOps(*actual), + 1); + } } } diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index adf86400e2..b79b502431 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -380,7 +380,21 @@ class CompilerTarget: ZXZ = 6 - ZRX90 = 7 + ZFixedRotation = 7 + + class FixedRotationBasis: + """Fixed X/Y pulse selected for synthesis with arbitrary RZ.""" + + @property + def gate(self) -> CompilerTarget.GateKind: ... + @property + def angle(self) -> float: + """Native pulse angle in radians.""" + + @property + def quarter_turn_pulses(self) -> int: ... + @property + def half_turn_angle(self) -> float | None: ... class SynthesisBasis: """One synthesis basis usable across the complete target.""" @@ -393,6 +407,10 @@ class CompilerTarget: def entangler(self) -> CompilerTarget.GateKind | None: """The two-qubit entangler, or None when none is usable.""" + @property + def fixed_rotation(self) -> CompilerTarget.FixedRotationBasis | None: + """Fixed-pulse decomposition, or None for other bases.""" + class ConnectivityKind(enum.Enum): """The target connectivity model.""" diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 9c550d0ae2..4e659238ad 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -669,7 +669,13 @@ def test_fixed_parameter_target_capability(arity: int | CompilerTarget.Operation ]), ) assert target.synthesis_basis is not None - assert target.synthesis_basis.single_qubit == CompilerTarget.SingleQubitBasis.ZRX90 + assert target.synthesis_basis.single_qubit == CompilerTarget.SingleQubitBasis.ZFixedRotation + fixed = target.synthesis_basis.fixed_rotation + assert fixed is not None + assert fixed.gate == CompilerTarget.GateKind.RX + assert fixed.angle == np.pi / 2 + assert fixed.quarter_turn_pulses == 1 + assert fixed.half_turn_angle is None assert not target.supports_operation("rx", 1, 1) assert target.supports_operation("rx", 1, parameters=[np.pi / 2], sites=[0]) assert not target.supports_operation("rx", 1, parameters=[np.pi]) @@ -684,13 +690,17 @@ def test_fixed_parameter_target_capability(arity: int | CompilerTarget.Operation @requires_qiskit_translation @pytest.mark.parametrize("theta", [0.0, np.pi / 2, np.pi, 0.47, "symbolic"]) @pytest.mark.parametrize("gate", ["u", "rx", "p"]) -def test_fixed_pulse_compilation_preserves_phase(theta: float | str, gate: str) -> None: - """Compile into RZ and fixed RX pulses without changing global phase.""" +@pytest.mark.parametrize( + ("pulse", "pulse_angle"), + [("rx", np.pi / 2), ("rx", -np.pi / 2), ("ry", np.pi / 2), ("rx", np.pi / 4), ("ry", -0.37)], +) +def test_fixed_pulse_compilation_preserves_phase(theta: float | str, gate: str, pulse: str, pulse_angle: float) -> None: + """Compile into RZ and target-defined X/Y pulses without changing global phase.""" target = CompilerTarget( 1, connectivity=CompilerTarget.Connectivity.all_to_all(), native_operations=CompilerTarget.NativeOperations([ - CompilerTarget.OperationCapability("rx", 1, 1, fixed_parameters=[np.pi / 2]), + CompilerTarget.OperationCapability(pulse, 1, 1, fixed_parameters=[pulse_angle]), CompilerTarget.OperationCapability("rz", 1, 1), CompilerTarget.OperationCapability("gphase", 0, 1), ]), @@ -704,8 +714,8 @@ def test_fixed_pulse_compilation_preserves_phase(theta: float | str, gate: str) program = QCProgram.from_qiskit(source).to_qco() program.compile_for_target(_test_target_environment(target)) result = program.to_qiskit(target=target) - assert set(result.count_ops()) <= {"rx", "rz"} - assert all(item.operation.params == [np.pi / 2] for item in result.data if item.operation.name == "rx") + assert set(result.count_ops()) <= {pulse, "rz"} + assert all(item.operation.params == [pulse_angle] for item in result.data if item.operation.name == pulse) for value in [-0.6, 0.0, np.pi / 2, np.pi]: bindings = {parameter: value} if isinstance(parameter, qiskit.circuit.Parameter) else {} assert np.allclose( From 95829bc3a65c19f5c13a27ebf0050024024a966e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 11:22:16 +0200 Subject: [PATCH 04/11] =?UTF-8?q?=E2=9C=A8=20Generalize=20fixed-pulse=20sy?= =?UTF-8?q?nthesis=20across=20axes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 馃 *AI text below* 馃 Use a cyclic coordinate frame to support fixed RX, RY, or RZ pulses with arbitrary rotations around a distinct axis. Keep symbolic angles algebraic and preserve global phase. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 5 +- bindings/mlir/register_mlir.cpp | 9 +- docs/mlir/target_compilation.md | 7 +- mlir/include/mqt/Compiler/Target.h | 29 +++-- mlir/lib/Compiler/Target.cpp | 91 ++++++++++------ .../QCO/Transforms/Decomposition/Euler.cpp | 94 +++++++++++----- .../MergeSingleQubitRotationGates.cpp | 100 +++++++++++++----- .../Compiler/test_compiler_target.cpp | 2 +- .../NativeSynthesis/test_target_synthesis.cpp | 88 ++++++++------- test/python/test_mlir.py | 24 +++-- 10 files changed, 291 insertions(+), 158 deletions(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index ecbf7261b5..506afaba17 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -9,7 +9,8 @@ values. Unspecified parameters remain unrestricted. Target matching, serialized attributes, synthesis-basis selection, and final verification must preserve the same restrictions. Symbolic values cannot satisfy a fixed parameter. -Derive synthesis from arbitrary RZ and a target-declared fixed X/Y pulse. +Derive synthesis from one arbitrary rotation axis and a fixed pulse about a +different axis. Support all distinct RX/RY/RZ pairs through cyclic coordinates. Precompute an effective quarter-turn sequence from its actual angle; use native half turns when available. Bound construction to 64 pulses per effective quarter turn. Direct native-gate targets are a separate Core change. @@ -31,7 +32,7 @@ Compiler and native-synthesis unit suites passed, including full-unitary phase comparisons, parameter restrictions, invalid attributes, and ordered placements. Python binding tests cover fixed and symbolic input gates. Earlier validation passed for the initial fixed-pulse implementation. The generic construction -passes full-unitary tests across both axes, signs, fractional and non-Clifford +passes full-unitary tests across axes, signs, fractional and non-Clifford angles, and optional half turns. Final Python and lint checks remain. ## Follow-up diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 534d040cef..55d77e7c74 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -916,18 +916,19 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); .value("XYX", mlir::CompilerTarget::SingleQubitBasis::XYX) .value("ZYZ", mlir::CompilerTarget::SingleQubitBasis::ZYZ) .value("ZXZ", mlir::CompilerTarget::SingleQubitBasis::ZXZ) - .value("ZFixedRotation", - mlir::CompilerTarget::SingleQubitBasis::ZFixedRotation); + .value("FixedRotation", + mlir::CompilerTarget::SingleQubitBasis::FixedRotation); nb::class_( compilerTarget, "FixedRotationBasis", - "Fixed X/Y pulse selected for synthesis with arbitrary RZ.") + "Fixed pulse and arbitrary rotation selected for synthesis.") .def_ro("gate", &mlir::CompilerTarget::FixedRotationBasis::gate) + .def_ro("free_gate", &mlir::CompilerTarget::FixedRotationBasis::freeGate) .def_ro("angle", &mlir::CompilerTarget::FixedRotationBasis::angle, "Native pulse angle in radians.") .def_prop_ro("quarter_turn_pulses", [](const mlir::CompilerTarget::FixedRotationBasis& basis) { - return basis.quarterTurnZAngles.size() - 1; + return basis.quarterTurnAngles.size() - 1; }) .def_ro("half_turn_angle", &mlir::CompilerTarget::FixedRotationBasis::halfTurnAngle); diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index c92b1e8a45..37cbdc37ca 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -172,11 +172,12 @@ accepts only RX(蟺/2). A nonempty list has one entry per parameter; `None` leave that parameter unrestricted. Multiple capabilities can describe different fixed values or placements. Constants match with absolute tolerance `1e-15`, without angle wrapping; symbolic values do not match fixed values. Omit the list for -unrestricted parameters. With unrestricted RZ, the compiler derives a synthesis -sequence from a fixed RX or RY angle available on every site. This covers +unrestricted parameters. The compiler derives a synthesis sequence from one +unrestricted rotation axis and a fixed angle about a different axis, available +on every site. Any distinct pair of RX, RY, and RZ is supported. This covers positive and negative quarter turns, 45掳 pulses, and non-Clifford angles such as 0.37 radians. The sequence is computed once per target and reused for numeric -and symbolic input gates. Available RX/RY half turns shorten suitable +and symbolic input gates. Available native half turns shorten suitable decompositions. Zero and integer-蟺 pulses do not supply the required mixing. The constructive diff --git a/mlir/include/mqt/Compiler/Target.h b/mlir/include/mqt/Compiler/Target.h index e53315f017..1b0297e1cd 100644 --- a/mlir/include/mqt/Compiler/Target.h +++ b/mlir/include/mqt/Compiler/Target.h @@ -18,6 +18,7 @@ #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" +#include #include #include #include @@ -303,24 +304,30 @@ class CompilerTarget { /// Recognized globally usable single-qubit synthesis basis. enum class SingleQubitBasis : uint8_t { - U, ///< `U(胃, 蠁, 位)`. - ZSXX, ///< `RZ` / `SX` / `X` synthesis via a ZYZ decomposition. - R, ///< XYX synthesis expressed with `R(胃, 蠁)`. - XZX, ///< `RX(蠁) * RZ(胃) * RX(位)`. - XYX, ///< `RX(蠁) * RY(胃) * RX(位)`. - ZYZ, ///< `RZ(蠁) * RY(胃) * RZ(位)`. - ZXZ, ///< `RZ(蠁) * RX(胃) * RZ(位)`. - ZFixedRotation, ///< Arbitrary `RZ` and fixed X/Y rotation pulses. + U, ///< `U(胃, 蠁, 位)`. + ZSXX, ///< `RZ` / `SX` / `X` synthesis via a ZYZ decomposition. + R, ///< XYX synthesis expressed with `R(胃, 蠁)`. + XZX, ///< `RX(蠁) * RZ(胃) * RX(位)`. + XYX, ///< `RX(蠁) * RY(胃) * RX(位)`. + ZYZ, ///< `RZ(蠁) * RY(胃) * RZ(位)`. + ZXZ, ///< `RZ(蠁) * RX(胃) * RZ(位)`. + FixedRotation, ///< An arbitrary rotation and fixed pulses about another + ///< axis. }; - /// Fixed X/Y pulse used to implement an effective positive RX(蟺/2). + /// Fixed pulse combined with arbitrary rotations about a distinct axis. struct FixedRotationBasis { GateKind gate; + GateKind freeGate; double angle; - /// RZ angles before, between, and after copies of the fixed pulse. - std::vector quarterTurnZAngles; + /// Free rotation angles before, between, and after fixed pulses. + /// Together they implement a local RX(蟺/2). + std::vector quarterTurnAngles; std::optional halfTurnAngle; + /// Physical gates for local X/Y/Z; local Z is the free rotation axis. + [[nodiscard]] std::array axes() const; + friend bool operator==(const FixedRotationBasis&, const FixedRotationBasis&) = default; }; diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index dcb27204fb..e7b3c6480b 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -159,13 +159,16 @@ constexpr std::array GATE_SPECIFICATIONS{ }, }; -// Construct an effective RX(蟺/2) from a fixed X/Y pulse and free Z rotations. +// Work in a cyclic coordinate frame with the free rotation axis as Z. // The two-pulse construction has reachable polar angle 2 asin(|sin(angle)|). static std::optional -makeFixedRotationBasis(GateKind gate, double angle) { +makeFixedRotationBasis(GateKind gate, GateKind freeGate, double angle) { constexpr double pi = std::numbers::pi; constexpr double halfPi = pi / 2.; constexpr size_t maxPulses = 64; + CompilerTarget::FixedRotationBasis result{ + gate, freeGate, angle, {}, std::nullopt}; + const bool isX = gate == result.axes()[0]; const double magnitude = std::abs(angle); if (magnitude <= mqt::PARAMETER_COMPARISON_TOLERANCE) { return std::nullopt; @@ -175,12 +178,11 @@ makeFixedRotationBasis(GateKind gate, double angle) { std::abs(directCount * magnitude - halfPi) <= mqt::PARAMETER_COMPARISON_TOLERANCE) { std::vector zAngles(static_cast(directCount) + 1, 0.); - const double axis = - (gate == GateKind::RY ? halfPi : 0.) + (angle < 0. ? pi : 0.); + const double axis = (!isX ? halfPi : 0.) + (angle < 0. ? pi : 0.); zAngles.front() = axis; zAngles.back() = -axis; - return CompilerTarget::FixedRotationBasis{gate, angle, std::move(zAngles), - std::nullopt}; + result.quarterTurnAngles = std::move(zAngles); + return result; } const double sine = std::sin(angle); const double reach = 2. * std::asin(std::min(1., std::abs(sine))); @@ -198,8 +200,8 @@ makeFixedRotationBasis(GateKind gate, double angle) { const double middle = 2. * std::acos(cosine); const double gamma = std::atan2(std::sin(middle / 2.), std::cos(angle) * cosine); - const double eta = gate == GateKind::RX ? (sine < 0. ? halfPi : -halfPi) - : (sine < 0. ? pi : 0.); + const double eta = + isX ? (sine < 0. ? halfPi : -halfPi) : (sine < 0. ? pi : 0.); const double before = halfPi - gamma + eta; const double after = -gamma - eta - halfPi; std::vector zAngles(2 * blocks + 1); @@ -208,12 +210,25 @@ makeFixedRotationBasis(GateKind gate, double angle) { zAngles[2 * block + 1] = middle; zAngles[2 * block + 2] = block + 1 == blocks ? after : after + before; } - return CompilerTarget::FixedRotationBasis{gate, angle, std::move(zAngles), - std::nullopt}; + result.quarterTurnAngles = std::move(zAngles); + return result; } } // namespace +std::array +CompilerTarget::FixedRotationBasis::axes() const { + switch (freeGate) { + case GateKind::RX: + return {GateKind::RY, GateKind::RZ, GateKind::RX}; + case GateKind::RY: + return {GateKind::RZ, GateKind::RX, GateKind::RY}; + default: + assert(freeGate == GateKind::RZ && "free gate must be a rotation"); + return {GateKind::RX, GateKind::RY, GateKind::RZ}; + } +} + [[nodiscard]] static std::string canonicalOperationName(StringRef name) { auto canonical = name.trim().lower(); if (canonical == "prx") { @@ -867,7 +882,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { } else if (supportsOnEverySite(GateKind::RY) && supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZYZ; - } else if (supportsOnEverySite(GateKind::RZ)) { + } else { const auto supportsPulse = [&](StringRef name, double angle) { return llvm::all_of(siteIds, [&](SiteId site) { return supportsOperation( @@ -875,34 +890,44 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { [angle](size_t) { return std::optional{angle}; }); }); }; - for (const auto& operation : operations) { - if ((operation.canonicalName() != "rx" && - operation.canonicalName() != "ry") || - operation.numParameters() != 1 || - operation.fixedParameters().empty() || - !operation.fixedParameters()[0]) { - continue; - } - const auto gate = - operation.canonicalName() == "rx" ? GateKind::RX : GateKind::RY; - auto candidate = - makeFixedRotationBasis(gate, *operation.fixedParameters()[0]); - if (!candidate || - (fixedRotation && candidate->quarterTurnZAngles.size() >= - fixedRotation->quarterTurnZAngles.size()) || - !supportsPulse(operation.canonicalName(), candidate->angle)) { + for (GateKind freeGate : {GateKind::RZ, GateKind::RX, GateKind::RY}) { + if (!supportsOnEverySite(freeGate)) { continue; } - for (double half : {std::numbers::pi, -std::numbers::pi}) { - if (supportsPulse(operation.canonicalName(), half)) { - candidate->halfTurnAngle = half; - break; + for (const auto& operation : operations) { + if ((operation.canonicalName() != "rx" && + operation.canonicalName() != "ry" && + operation.canonicalName() != "rz") || + operation.numParameters() != 1 || + operation.fixedParameters().empty() || + !operation.fixedParameters()[0]) { + continue; + } + const auto gate = operation.canonicalName() == "rx" ? GateKind::RX + : operation.canonicalName() == "ry" ? GateKind::RY + : GateKind::RZ; + if (gate == freeGate) { + continue; + } + auto candidate = makeFixedRotationBasis( + gate, freeGate, *operation.fixedParameters()[0]); + if (!candidate || + (fixedRotation && candidate->quarterTurnAngles.size() >= + fixedRotation->quarterTurnAngles.size()) || + !supportsPulse(operation.canonicalName(), candidate->angle)) { + continue; + } + for (double half : {std::numbers::pi, -std::numbers::pi}) { + if (supportsPulse(operation.canonicalName(), half)) { + candidate->halfTurnAngle = half; + break; + } } + fixedRotation = std::move(candidate); } - fixedRotation = std::move(candidate); } if (fixedRotation) { - singleQubit = SingleQubitBasis::ZFixedRotation; + singleQubit = SingleQubitBasis::FixedRotation; } } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index d7e9a3cde5..d707b90132 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -37,33 +37,41 @@ namespace mlir::qco::decomposition { bool isSingleQubitBasisGate( Operation* op, SingleQubitBasis basis, const CompilerTarget::FixedRotationBasis* fixedRotation) { + if (basis == SingleQubitBasis::FixedRotation) { + assert(fixedRotation && + "fixed-pulse synthesis requires a pulse descriptor"); + return TypeSwitch(op) + .Case([&](auto rotation) { + const auto gate = isa(rotation) ? CompilerTarget::GateKind::RX + : isa(rotation) + ? CompilerTarget::GateKind::RY + : CompilerTarget::GateKind::RZ; + if (gate == fixedRotation->freeGate) { + return true; + } + const auto angle = mqt::valueToDouble(rotation.getTheta()); + return gate == fixedRotation->gate && angle && + (std::abs(*angle - fixedRotation->angle) <= + mqt::PARAMETER_COMPARISON_TOLERANCE || + (fixedRotation->halfTurnAngle && + std::abs(*angle - *fixedRotation->halfTurnAngle) <= + mqt::PARAMETER_COMPARISON_TOLERANCE)); + }) + .Default([](auto) { return false; }); + } return TypeSwitch(op) .Case([&](RZOp) { return basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::ZXZ || basis == SingleQubitBasis::XZX || - basis == SingleQubitBasis::ZSXX || - basis == SingleQubitBasis::ZFixedRotation; + basis == SingleQubitBasis::ZSXX; }) .Case([&](auto rotation) { - const bool isX = isa(rotation); - if (basis == SingleQubitBasis::ZFixedRotation) { - if (!fixedRotation || - isX != (fixedRotation->gate == CompilerTarget::GateKind::RX)) { - return false; - } - const auto angle = mqt::valueToDouble(rotation.getTheta()); - return angle && (std::abs(*angle - fixedRotation->angle) <= - mqt::PARAMETER_COMPARISON_TOLERANCE || - (fixedRotation->halfTurnAngle && - std::abs(*angle - *fixedRotation->halfTurnAngle) <= - mqt::PARAMETER_COMPARISON_TOLERANCE)); - } - return isX ? (basis == SingleQubitBasis::ZXZ || - basis == SingleQubitBasis::XZX || - basis == SingleQubitBasis::XYX) - : (basis == SingleQubitBasis::ZYZ || - basis == SingleQubitBasis::XYX); + return isa(rotation) ? (basis == SingleQubitBasis::ZXZ || + basis == SingleQubitBasis::XZX || + basis == SingleQubitBasis::XYX) + : (basis == SingleQubitBasis::ZYZ || + basis == SingleQubitBasis::XYX); }) .Case([&](UOp) { return basis == SingleQubitBasis::U; }) .Case([&](auto) { return basis == SingleQubitBasis::ZSXX; }) @@ -211,7 +219,7 @@ EulerAngles anglesFromUnitary(const Matrix2x2& matrix, switch (basis) { case SingleQubitBasis::ZYZ: case SingleQubitBasis::ZSXX: - case SingleQubitBasis::ZFixedRotation: + case SingleQubitBasis::FixedRotation: return paramsZYZ(matrix); case SingleQubitBasis::ZXZ: return paramsZXZ(matrix); @@ -303,7 +311,7 @@ struct Unitary1QEulerPlan { case SingleQubitBasis::ZYZ: case SingleQubitBasis::ZXZ: case SingleQubitBasis::ZSXX: - case SingleQubitBasis::ZFixedRotation: + case SingleQubitBasis::FixedRotation: appendRotation(SynthesisStep::Kind::RZ, angles.phi + angles.lambda); break; @@ -360,20 +368,20 @@ struct Unitary1QEulerPlan { angles.lambda); phase = angles.phase; break; - case SingleQubitBasis::ZFixedRotation: { + case SingleQubitBasis::FixedRotation: { assert(fixedRotation && "fixed-pulse synthesis requires a pulse descriptor"); constexpr double pi = std::numbers::pi; constexpr double halfPi = pi / 2.; - const auto kind = fixedRotation->gate == CompilerTarget::GateKind::RX + const auto kind = fixedRotation->gate == fixedRotation->axes()[0] ? SynthesisStep::Kind::RX : SynthesisStep::Kind::RY; const double axis = kind == SynthesisStep::Kind::RY ? halfPi : 0.; const auto quarterTurn = [&] { appendRotation(SynthesisStep::Kind::RZ, - fixedRotation->quarterTurnZAngles.front()); + fixedRotation->quarterTurnAngles.front()); for (double zAngle : - ArrayRef(fixedRotation->quarterTurnZAngles).drop_front()) { + ArrayRef(fixedRotation->quarterTurnAngles).drop_front()) { steps.emplace_back(kind, fixedRotation->angle); appendRotation(SynthesisStep::Kind::RZ, zAngle); } @@ -444,8 +452,40 @@ planUnitary1QEuler(const Matrix2x2& targetMatrix, const SingleQubitBasis basis, return plan; } - const EulerAngles angles = anglesFromUnitary(targetMatrix, basis); + auto matrix = targetMatrix; + if (basis == SingleQubitBasis::FixedRotation) { + assert(fixedRotation && + "fixed-pulse synthesis requires a pulse descriptor"); + // Cyclically permute Pauli coefficients into the local synthesis frame. + if (fixedRotation->freeGate != CompilerTarget::GateKind::RZ) { + const auto w = (matrix(0, 0) + matrix(1, 1)) * 0.5; + const auto x = (matrix(0, 1) + matrix(1, 0)) * 0.5; + const auto y = (matrix(0, 1) - matrix(1, 0)) * std::complex(0., 0.5); + const auto z = (matrix(0, 0) - matrix(1, 1)) * 0.5; + const bool freeX = + fixedRotation->freeGate == CompilerTarget::GateKind::RX; + const auto localX = freeX ? y : z; + const auto localY = freeX ? z : x; + const auto localZ = freeX ? x : y; + const auto it = std::complex(0., 1.) * localY; + matrix = Matrix2x2::fromElements(w + localZ, localX - it, localX + it, + w - localZ); + } + } + const EulerAngles angles = anglesFromUnitary(matrix, basis); plan.appendDecomposition(angles, basis, fixedRotation); + if (basis == SingleQubitBasis::FixedRotation) { + const auto axes = fixedRotation->axes(); + for (auto& step : plan.steps) { + const auto gate = axes[step.kind == SynthesisStep::Kind::RX ? 0 + : step.kind == SynthesisStep::Kind::RY ? 1 + : 2]; + step.kind = gate == CompilerTarget::GateKind::RX ? SynthesisStep::Kind::RX + : gate == CompilerTarget::GateKind::RY + ? SynthesisStep::Kind::RY + : SynthesisStep::Kind::RZ; + } + } return plan; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index 5f9abedfeb..be621a9f18 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -738,11 +738,9 @@ static Value emitRuntimeEulerAngles( const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr) { auto [theta, phi, lambda, phase] = angles; - const bool usesZYZAngles = - basis == decomposition::SingleQubitBasis::ZYZ || - basis == decomposition::SingleQubitBasis::ZXZ || - basis == decomposition::SingleQubitBasis::ZSXX || - basis == decomposition::SingleQubitBasis::ZFixedRotation; + const bool usesZYZAngles = basis == decomposition::SingleQubitBasis::ZYZ || + basis == decomposition::SingleQubitBasis::ZXZ || + basis == decomposition::SingleQubitBasis::ZSXX; if (usesZYZAngles && isConstantAngle(theta)) { qubit = emitRotationIfNeeded(rewriter, loc, qubit, sumAngles(phi, lambda)); @@ -778,48 +776,61 @@ static Value emitRuntimeEulerAngles( qubit = UOp::create(rewriter, loc, qubit, theta.v, phi.v, lambda.v) .getQubitOut(); break; - case decomposition::SingleQubitBasis::ZFixedRotation: { + case decomposition::SingleQubitBasis::FixedRotation: { assert(fixedRotation && "fixed-pulse synthesis requires a pulse descriptor"); const auto halfPi = Val::constant(rewriter, loc, std::numbers::pi / 2.); - const bool isX = fixedRotation->gate == CompilerTarget::GateKind::RX; + const auto axes = fixedRotation->axes(); + const bool isX = fixedRotation->gate == axes[0]; const auto axis = Val::constant(rewriter, loc, isX ? 0. : std::numbers::pi / 2.); + const auto emit = [&](CompilerTarget::GateKind gate, Val angle) { + switch (gate) { + case CompilerTarget::GateKind::RX: + return emitRotationIfNeeded(rewriter, loc, qubit, angle); + case CompilerTarget::GateKind::RY: + return emitRotationIfNeeded(rewriter, loc, qubit, angle); + default: + return emitRotationIfNeeded(rewriter, loc, qubit, angle); + } + }; + const auto emitFree = [&](Val angle) { + return emit(fixedRotation->freeGate, angle); + }; const auto emitPulse = [&](double angle) { - qubit = isX ? RXOp::create(rewriter, loc, qubit, angle).getQubitOut() - : RYOp::create(rewriter, loc, qubit, angle).getQubitOut(); + qubit = + emit(fixedRotation->gate, Val::constant(rewriter, loc, angle)); }; const auto quarterTurn = [&] { for (const auto [index, zAngle] : - llvm::enumerate(fixedRotation->quarterTurnZAngles)) { + llvm::enumerate(fixedRotation->quarterTurnAngles)) { if (index != 0) { emitPulse(fixedRotation->angle); } - qubit = emitRotationIfNeeded( - rewriter, loc, qubit, Val::constant(rewriter, loc, zAngle)); + qubit = emitFree(Val::constant(rewriter, loc, zAngle)); } }; - if (isConstantAngle(theta, std::numbers::pi / 2.)) { - qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda - halfPi); + if (isConstantAngle(theta)) { + qubit = emitFree(sumAngles(phi, lambda)); + } else if (isConstantAngle(theta, std::numbers::pi / 2.)) { + qubit = emitFree(lambda - halfPi); quarterTurn(); - qubit = emitRotationIfNeeded(rewriter, loc, qubit, phi + halfPi); + qubit = emitFree(phi + halfPi); } else if (isConstantAngle(theta, std::numbers::pi) && fixedRotation->halfTurnAngle) { - qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda + axis); + qubit = emitFree(lambda + axis); emitPulse(*fixedRotation->halfTurnAngle); - qubit = emitRotationIfNeeded(rewriter, loc, qubit, - phi + consts.pi - axis); + qubit = emitFree(phi + consts.pi - axis); if (*fixedRotation->halfTurnAngle < 0.) { phase = phase + consts.pi; } } else { - qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda); + qubit = emitFree(lambda); quarterTurn(); - qubit = - emitRotationIfNeeded(rewriter, loc, qubit, theta + consts.pi); + qubit = emitFree(theta + consts.pi); quarterTurn(); - qubit = emitRotationIfNeeded(rewriter, loc, qubit, phi + consts.pi); + qubit = emitFree(phi + consts.pi); phase = phase + consts.pi; } break; @@ -1046,7 +1057,7 @@ struct MergeSingleQubitRotationGatesPattern final case decomposition::SingleQubitBasis::U: return 1; case decomposition::SingleQubitBasis::ZSXX: - case decomposition::SingleQubitBasis::ZFixedRotation: + case decomposition::SingleQubitBasis::FixedRotation: return 5; case decomposition::SingleQubitBasis::ZYZ: case decomposition::SingleQubitBasis::ZXZ: @@ -1259,17 +1270,50 @@ void decomposition::synthesizeParameterizedUnitary1Q( const bool usesDirectZYZAngles = basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::ZXZ || basis == SingleQubitBasis::ZSXX || - basis == SingleQubitBasis::ZFixedRotation; + basis == SingleQubitBasis::FixedRotation; if (basis == SingleQubitBasis::U || usesDirectZYZAngles) { const auto consts = makeConsts(rewriter, op->getLoc()); Value qubit; if (basis == SingleQubitBasis::U) { qubit = emitDirectU(rewriter, unitary, consts); } else { - qubit = emitRuntimeEulerAngles( - rewriter, op->getLoc(), unitary.getInputQubit(0), - directZYZAnglesFromGate(unitary, rewriter, consts), basis, consts, - fixedRotation); + const auto angles = directZYZAnglesFromGate(unitary, rewriter, consts); + qubit = unitary.getInputQubit(0); + if (basis == SingleQubitBasis::FixedRotation && fixedRotation && + fixedRotation->freeGate != CompilerTarget::GateKind::RZ) { + // Keep symbolic angles algebraic: emit each physical Z/Y/Z rotation + // in the cyclic local frame instead of introducing inverse trig. + const auto axes = fixedRotation->axes(); + const auto halfPi = consts.pi / consts.two; + const auto emitPhysical = [&](CompilerTarget::GateKind gate, + Val angle) { + if (isConstantAngle(angle)) { + return; + } + RuntimeEulerAngles local{.theta = consts.zero, + .phi = consts.zero, + .lambda = consts.zero, + .phase = consts.zero}; + if (gate == axes[2]) { + local.lambda = angle; + } else { + local.theta = angle; + if (gate == axes[0]) { + local.phi = -halfPi; + local.lambda = halfPi; + } + } + qubit = emitRuntimeEulerAngles(rewriter, op->getLoc(), qubit, local, + basis, consts, fixedRotation); + }; + emitPhysical(CompilerTarget::GateKind::RZ, angles.lambda); + emitPhysical(CompilerTarget::GateKind::RY, angles.theta); + emitPhysical(CompilerTarget::GateKind::RZ, angles.phi); + emitParameterizedGPhaseIfNeeded(rewriter, op->getLoc(), angles.phase); + } else { + qubit = emitRuntimeEulerAngles(rewriter, op->getLoc(), qubit, angles, + basis, consts, fixedRotation); + } } rewriter.replaceOp(op, qubit); return; diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 5a14b1ac69..cb96737d6e 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -925,7 +925,7 @@ TEST(CompilerTargetTest, ResolvesFixedPulseBasisOnlyOnEverySite) { NativeOperations::fromOperations(operations))); ASSERT_TRUE(target.synthesisBasis()); EXPECT_EQ(target.synthesisBasis()->singleQubit, - Target::SingleQubitBasis::ZFixedRotation); + Target::SingleQubitBasis::FixedRotation); EXPECT_FALSE(target.synthesisBasis()->entangler); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index 13be99ccbd..0fdb54ffaa 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -322,48 +322,54 @@ TEST_F(TargetSynthesisTest, TargetPassesRequireTypedEnvironment) { } TEST_F(TargetSynthesisTest, FixedPulseSynthesisPreservesFullUnitary) { - for (const auto name : {"rx", "ry"}) { - for (double pulseAngle : - {std::numbers::pi / 2., -std::numbers::pi / 2., std::numbers::pi / 4., - -std::numbers::pi / 4., std::numbers::pi / 3., .37, -.73, 2.7, 4.2}) { - for (const std::optional halfTurn : - {std::optional{}, std::optional{std::numbers::pi}, - std::optional{-std::numbers::pi}}) { - SCOPED_TRACE(testing::Message() << name << " " << pulseAngle << " " - << halfTurn.value_or(0.)); - std::vector operations{ - valid(OperationCapability::create(name, 1, 1, {}, std::nullopt, - std::nullopt, {pulseAngle})), - valid(OperationCapability::create("rz", 1, 1)), - valid(OperationCapability::create("cz", 2, 0)), - valid(OperationCapability::create("gphase", 0, 1)), - }; - if (halfTurn) { - operations.push_back(valid(OperationCapability::create( - name, 1, 1, {}, std::nullopt, std::nullopt, {*halfTurn}))); - } - const auto target = - valid(Target::create(2, Connectivity::allToAll(), - NativeOperations::fromOperations(operations))); - ASSERT_TRUE(target.synthesisBasis()); - for (double theta : - {0., .37, std::numbers::pi / 2., std::numbers::pi}) { - const auto circuit = [&](QCOProgramBuilder& builder) { - auto q0 = builder.u(theta, .42, -.31, builder.staticQubit(0)); - auto q1 = builder.rx(-.73, builder.staticQubit(1)); - auto [control, targetQubit] = builder.cx(q0, q1); - builder.sink(control); - builder.sink(builder.ry(theta, targetQubit)); - return builder.intConstant(0); + for (const auto freeName : {"rx", "ry", "rz"}) { + for (const auto name : {"rx", "ry", "rz"}) { + if (llvm::StringRef(freeName) == name) { + continue; + } + for (double pulseAngle : {std::numbers::pi / 2., -std::numbers::pi / 2., + std::numbers::pi / 4., -std::numbers::pi / 4., + std::numbers::pi / 3., .37, -.73, 2.7, 4.2}) { + for (const std::optional halfTurn : + {std::optional{}, std::optional{std::numbers::pi}, + std::optional{-std::numbers::pi}}) { + SCOPED_TRACE(testing::Message() + << freeName << " " << name << " " << pulseAngle << " " + << halfTurn.value_or(0.)); + std::vector operations{ + valid(OperationCapability::create(name, 1, 1, {}, std::nullopt, + std::nullopt, {pulseAngle})), + valid(OperationCapability::create(freeName, 1, 1)), + valid(OperationCapability::create("cz", 2, 0)), + valid(OperationCapability::create("gphase", 0, 1)), }; - auto expected = build(circuit); - auto actual = build(circuit); - ASSERT_TRUE(mlir::succeeded(runTargetPass( - *actual, target, mlir::qco::createTargetNativeSynthesis()))); - ASSERT_TRUE(mlir::succeeded( - runPass(*actual, mlir::qco::createVerifyTargetConformance()))); - EXPECT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*actual))); - expectEquivalent(expected, actual); + if (halfTurn) { + operations.push_back(valid(OperationCapability::create( + name, 1, 1, {}, std::nullopt, std::nullopt, {*halfTurn}))); + } + const auto target = valid( + Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations(operations))); + ASSERT_TRUE(target.synthesisBasis()); + for (double theta : + {0., .37, std::numbers::pi / 2., std::numbers::pi}) { + const auto circuit = [&](QCOProgramBuilder& builder) { + auto q0 = builder.u(theta, .42, -.31, builder.staticQubit(0)); + auto q1 = builder.rx(-.73, builder.staticQubit(1)); + auto [control, targetQubit] = builder.cx(q0, q1); + builder.sink(control); + builder.sink(builder.ry(theta, targetQubit)); + return builder.intConstant(0); + }; + auto expected = build(circuit); + auto actual = build(circuit); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *actual, target, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded( + runPass(*actual, mlir::qco::createVerifyTargetConformance()))); + EXPECT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*actual))); + expectEquivalent(expected, actual); + } } } } diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 4e659238ad..e0d496dd0b 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -669,11 +669,11 @@ def test_fixed_parameter_target_capability(arity: int | CompilerTarget.Operation ]), ) assert target.synthesis_basis is not None - assert target.synthesis_basis.single_qubit == CompilerTarget.SingleQubitBasis.ZFixedRotation + assert target.synthesis_basis.single_qubit == CompilerTarget.SingleQubitBasis.FixedRotation fixed = target.synthesis_basis.fixed_rotation assert fixed is not None assert fixed.gate == CompilerTarget.GateKind.RX - assert fixed.angle == np.pi / 2 + assert fixed.angle == pytest.approx(np.pi / 2) assert fixed.quarter_turn_pulses == 1 assert fixed.half_turn_angle is None assert not target.supports_operation("rx", 1, 1) @@ -691,17 +691,25 @@ def test_fixed_parameter_target_capability(arity: int | CompilerTarget.Operation @pytest.mark.parametrize("theta", [0.0, np.pi / 2, np.pi, 0.47, "symbolic"]) @pytest.mark.parametrize("gate", ["u", "rx", "p"]) @pytest.mark.parametrize( - ("pulse", "pulse_angle"), - [("rx", np.pi / 2), ("rx", -np.pi / 2), ("ry", np.pi / 2), ("rx", np.pi / 4), ("ry", -0.37)], + ("free", "pulse", "pulse_angle"), + [ + (free, pulse, angle) + for free in ("rx", "ry", "rz") + for pulse in ("rx", "ry", "rz") + if pulse != free + for angle in (np.pi / 2, -np.pi / 2, np.pi / 4, -0.37) + ], ) -def test_fixed_pulse_compilation_preserves_phase(theta: float | str, gate: str, pulse: str, pulse_angle: float) -> None: - """Compile into RZ and target-defined X/Y pulses without changing global phase.""" +def test_fixed_pulse_compilation_preserves_phase( + theta: float | str, gate: str, free: str, pulse: str, pulse_angle: float +) -> None: + """Compile into arbitrary and fixed rotations about distinct axes, preserving phase.""" target = CompilerTarget( 1, connectivity=CompilerTarget.Connectivity.all_to_all(), native_operations=CompilerTarget.NativeOperations([ CompilerTarget.OperationCapability(pulse, 1, 1, fixed_parameters=[pulse_angle]), - CompilerTarget.OperationCapability("rz", 1, 1), + CompilerTarget.OperationCapability(free, 1, 1), CompilerTarget.OperationCapability("gphase", 0, 1), ]), ) @@ -714,7 +722,7 @@ def test_fixed_pulse_compilation_preserves_phase(theta: float | str, gate: str, program = QCProgram.from_qiskit(source).to_qco() program.compile_for_target(_test_target_environment(target)) result = program.to_qiskit(target=target) - assert set(result.count_ops()) <= {pulse, "rz"} + assert set(result.count_ops()) <= {pulse, free} assert all(item.operation.params == [pulse_angle] for item in result.data if item.operation.name == pulse) for value in [-0.6, 0.0, np.pi / 2, np.pi]: bindings = {parameter: value} if isinstance(parameter, qiskit.circuit.Parameter) else {} From b30db86a3a32ad18e67e529ecd465a3a780fa1c2 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 11:23:21 +0200 Subject: [PATCH 05/11] =?UTF-8?q?=F0=9F=93=9D=20Refresh=20generic=20rotati?= =?UTF-8?q?on=20target=20bindings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 馃 *AI text below* 馃 Regenerate the public Python API for arbitrary and fixed rotation axes. Assisted-by: GPT-6 via Codex --- python/mqt/core/mlir.pyi | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index b79b502431..6646439328 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -380,14 +380,16 @@ class CompilerTarget: ZXZ = 6 - ZFixedRotation = 7 + FixedRotation = 7 class FixedRotationBasis: - """Fixed X/Y pulse selected for synthesis with arbitrary RZ.""" + """Fixed pulse and arbitrary rotation selected for synthesis.""" @property def gate(self) -> CompilerTarget.GateKind: ... @property + def free_gate(self) -> CompilerTarget.GateKind: ... + @property def angle(self) -> float: """Native pulse angle in radians.""" From c12cefe65a720ce818f048debbaf193056043e4c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 11:27:26 +0200 Subject: [PATCH 06/11] =?UTF-8?q?=F0=9F=8E=A8=20Apply=20C++=20lint=20to=20?= =?UTF-8?q?generic=20pulse=20synthesis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 馃 *AI text below* 馃 Apply explicit aggregate initialization, pointer checks, and test formatting. Assisted-by: GPT-6 via Codex --- mlir/lib/Compiler/Target.cpp | 13 ++++++--- .../MergeSingleQubitRotationGates.cpp | 13 +++++---- .../NativeSynthesis/test_target_synthesis.cpp | 28 +++++++++++++------ 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index e7b3c6480b..5bf6388a96 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -159,6 +159,8 @@ constexpr std::array GATE_SPECIFICATIONS{ }, }; +} // namespace + // Work in a cyclic coordinate frame with the free rotation axis as Z. // The two-pulse construction has reachable polar angle 2 asin(|sin(angle)|). static std::optional @@ -167,7 +169,12 @@ makeFixedRotationBasis(GateKind gate, GateKind freeGate, double angle) { constexpr double halfPi = pi / 2.; constexpr size_t maxPulses = 64; CompilerTarget::FixedRotationBasis result{ - gate, freeGate, angle, {}, std::nullopt}; + .gate = gate, + .freeGate = freeGate, + .angle = angle, + .quarterTurnAngles = {}, + .halfTurnAngle = std::nullopt, + }; const bool isX = gate == result.axes()[0]; const double magnitude = std::abs(angle); if (magnitude <= mqt::PARAMETER_COMPARISON_TOLERANCE) { @@ -190,7 +197,7 @@ makeFixedRotationBasis(GateKind gate, GateKind freeGate, double angle) { return std::nullopt; } const double count = std::ceil(halfPi / reach); - if (count > static_cast(maxPulses / 2)) { + if (count > static_cast(maxPulses) / 2.) { return std::nullopt; } const auto blocks = static_cast(count); @@ -214,8 +221,6 @@ makeFixedRotationBasis(GateKind gate, GateKind freeGate, double angle) { return result; } -} // namespace - std::array CompilerTarget::FixedRotationBasis::axes() const { switch (freeGate) { diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index be621a9f18..ce896b74e9 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -1279,7 +1279,8 @@ void decomposition::synthesizeParameterizedUnitary1Q( } else { const auto angles = directZYZAnglesFromGate(unitary, rewriter, consts); qubit = unitary.getInputQubit(0); - if (basis == SingleQubitBasis::FixedRotation && fixedRotation && + if (basis == SingleQubitBasis::FixedRotation && + fixedRotation != nullptr && fixedRotation->freeGate != CompilerTarget::GateKind::RZ) { // Keep symbolic angles algebraic: emit each physical Z/Y/Z rotation // in the cyclic local frame instead of introducing inverse trig. @@ -1290,10 +1291,12 @@ void decomposition::synthesizeParameterizedUnitary1Q( if (isConstantAngle(angle)) { return; } - RuntimeEulerAngles local{.theta = consts.zero, - .phi = consts.zero, - .lambda = consts.zero, - .phase = consts.zero}; + RuntimeEulerAngles local{ + .theta = consts.zero, + .phi = consts.zero, + .lambda = consts.zero, + .phase = consts.zero, + }; if (gate == axes[2]) { local.lambda = angle; } else { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index 0fdb54ffaa..100de0a2e8 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -322,17 +322,27 @@ TEST_F(TargetSynthesisTest, TargetPassesRequireTypedEnvironment) { } TEST_F(TargetSynthesisTest, FixedPulseSynthesisPreservesFullUnitary) { - for (const auto freeName : {"rx", "ry", "rz"}) { - for (const auto name : {"rx", "ry", "rz"}) { + for (const auto* const freeName : {"rx", "ry", "rz"}) { + for (const auto* const name : {"rx", "ry", "rz"}) { if (llvm::StringRef(freeName) == name) { continue; } - for (double pulseAngle : {std::numbers::pi / 2., -std::numbers::pi / 2., - std::numbers::pi / 4., -std::numbers::pi / 4., - std::numbers::pi / 3., .37, -.73, 2.7, 4.2}) { - for (const std::optional halfTurn : - {std::optional{}, std::optional{std::numbers::pi}, - std::optional{-std::numbers::pi}}) { + for (double pulseAngle : { + std::numbers::pi / 2., + -std::numbers::pi / 2., + std::numbers::pi / 4., + -std::numbers::pi / 4., + std::numbers::pi / 3., + .37, + -.73, + 2.7, + 4.2, + }) { + for (const std::optional halfTurn : { + std::optional{}, + std::optional{std::numbers::pi}, + std::optional{-std::numbers::pi}, + }) { SCOPED_TRACE(testing::Message() << freeName << " " << name << " " << pulseAngle << " " << halfTurn.value_or(0.)); @@ -377,7 +387,7 @@ TEST_F(TargetSynthesisTest, FixedPulseSynthesisPreservesFullUnitary) { } TEST_F(TargetSynthesisTest, FixedHalfTurnUsesOnePulse) { - for (const auto name : {"rx", "ry"}) { + for (const auto* const name : {"rx", "ry"}) { for (double half : {std::numbers::pi, -std::numbers::pi}) { const auto target = valid(Target::create( 1, Connectivity::allToAll(), From 14f33836578a12c66ed67dca24a0fde183cf244a Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 11:30:37 +0200 Subject: [PATCH 07/11] =?UTF-8?q?=F0=9F=93=9D=20Record=20generic=20pulse?= =?UTF-8?q?=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 馃 *AI text below* 馃 Record supported axis pairs, scope, and final validation. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 506afaba17..5572ef4c5f 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -1,6 +1,6 @@ # Fixed-parameter compiler targets -Status: generic fixed-pulse synthesis implemented; validation in progress. +Status: complete. ## Goal and scope @@ -28,12 +28,11 @@ rotation as an arbitrary rotation. ## Validation -Compiler and native-synthesis unit suites passed, including full-unitary phase -comparisons, parameter restrictions, invalid attributes, and ordered placements. -Python binding tests cover fixed and symbolic input gates. Earlier validation -passed for the initial fixed-pulse implementation. The generic construction -passes full-unitary tests across axes, signs, fractional and non-Clifford -angles, and optional half turns. Final Python and lint checks remain. +The compiler suite passed 234 tests; native synthesis passed 64 tests, including +648 full-matrix cases across all six axis pairs, both signs, fractional and +non-Clifford angles, and optional half turns. Python target tests passed 388 +cases, including numerical and symbolic input gates. Generated stubs, repository +lint, and full changed-file C++ lint passed. ## Follow-up From a07c63063853418e40152a1fe0b4e9eb90247590 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 12:54:00 +0200 Subject: [PATCH 08/11] =?UTF-8?q?=F0=9F=90=9B=20Compare=20fixed=20paramete?= =?UTF-8?q?rs=20in=20target=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 馃 *AI text below* 馃 Reject target changes that alter fixed parameter values, including changes between fixed and unrestricted capabilities. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 5 +-- mlir/lib/Compiler/QDMIAdapter.cpp | 1 + .../Compiler/test_compiler_qdmi_adapter.cpp | 32 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 5572ef4c5f..22b6bd05ad 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -7,7 +7,8 @@ Status: complete. Allow operation capabilities to restrict individual parameters to finite fixed values. Unspecified parameters remain unrestricted. Target matching, serialized attributes, synthesis-basis selection, and final verification must preserve the -same restrictions. Symbolic values cannot satisfy a fixed parameter. +same restrictions. Target compatibility also compares these constraints. +Symbolic values cannot satisfy a fixed parameter. Derive synthesis from one arbitrary rotation axis and a fixed pulse about a different axis. Support all distinct RX/RY/RZ pairs through cyclic coordinates. @@ -28,7 +29,7 @@ rotation as an arbitrary rotation. ## Validation -The compiler suite passed 234 tests; native synthesis passed 64 tests, including +The compiler suite passed 235 tests; native synthesis passed 64 tests, including 648 full-matrix cases across all six axis pairs, both signs, fractional and non-Clifford angles, and optional half turns. Python target tests passed 388 cases, including numerical and symbolic input gates. Generated stubs, repository diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index 208abe5a51..4013582d83 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -579,6 +579,7 @@ static bool sameOperation(const CompilerTarget::OperationCapability& lhs, if (lhs.canonicalName() != rhs.canonicalName() || lhs.arity() != rhs.arity() || lhs.numParameters() != rhs.numParameters() || + lhs.fixedParameters() != rhs.fixedParameters() || lhs.siteTuples().size() != rhs.siteTuples().size()) { return false; } diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 82c48e9f1d..ce8a3bfddc 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -24,7 +24,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -410,6 +412,36 @@ TEST(CompilerQDMIAdapterTest, original, mlir::TargetEnvironment(original.target(), constrained)))); } +TEST(CompilerQDMIAdapterTest, CompatibilityPreservesFixedParameters) { + const auto makeEnvironment = + [](std::vector> parameters) { + const auto operation = + llvm::cantFail(CompilerTarget::OperationCapability::create( + "rx", 1, 1, {}, std::nullopt, std::nullopt, + std::move(parameters))); + return mlir::TargetEnvironment( + llvm::cantFail(CompilerTarget::create( + 1, CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::fromOperations({operation}))), + llvm::cantFail(mlir::payloadSpecificationForProgramFormat( + QDMI_PROGRAM_FORMAT_QASM3))); + }; + const auto fixed = makeEnvironment({std::numbers::pi / 2.}); + EXPECT_FALSE(llvm::errorToBool(mlir::validateTargetCompatibility( + fixed, makeEnvironment({std::numbers::pi / 2.})))); + for (const auto& changed : { + makeEnvironment({std::numbers::pi / 4.}), + makeEnvironment({std::nullopt}), + }) { + EXPECT_TRUE( + llvm::errorToBool(mlir::validateTargetCompatibility(fixed, changed))); + EXPECT_TRUE( + llvm::errorToBool(mlir::validateTargetCompatibility(changed, fixed))); + } + EXPECT_FALSE(llvm::errorToBool(mlir::validateTargetCompatibility( + makeEnvironment({}), makeEnvironment({std::nullopt})))); +} + TEST(CompilerQDMIAdapterTest, CompilationCreatesNoJobAndSubmissionChecksContract) { auto library = std::make_shared( From 57b2aea741d28c72802777e2cfdc0263531099cc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 13:25:22 +0200 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=A7=AA=20Cover=20symbolic=20fixed-p?= =?UTF-8?q?ulse=20synthesis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 馃 *AI text below* 馃 Exercise numeric and symbolic angles in the native coverage suite, including phase-sensitive special-angle paths. Pass the optional half turn directly to satisfy clang-tidy. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 8 +-- .../NativeSynthesis/test_target_synthesis.cpp | 53 +++++++++++++++---- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 22b6bd05ad..3cdb1dd604 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -30,10 +30,10 @@ rotation as an arbitrary rotation. ## Validation The compiler suite passed 235 tests; native synthesis passed 64 tests, including -648 full-matrix cases across all six axis pairs, both signs, fractional and -non-Clifford angles, and optional half turns. Python target tests passed 388 -cases, including numerical and symbolic input gates. Generated stubs, repository -lint, and full changed-file C++ lint passed. +1,944 full-matrix cases across all six axis pairs, both signs, fractional and +non-Clifford angles, optional half turns, and numeric or symbolic parameters. +Python target tests passed 388 cases, including numerical and symbolic input +gates. Generated stubs, repository lint, and full changed-file C++ lint passed. ## Follow-up diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index 100de0a2e8..4db24471dc 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -30,6 +30,7 @@ #include "mlir/Dialect/Math/IR/Math.h" #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/IR/Block.h" +#include "mlir/IR/Builders.h" #include "mlir/IR/BuiltinAttributes.h" #include "mlir/IR/BuiltinOps.h" #include "mlir/IR/BuiltinTypes.h" @@ -45,6 +46,7 @@ #include "mlir/Pass/PassManager.h" #include "mlir/Support/LLVM.h" #include "mlir/Support/LogicalResult.h" +#include "mlir/Transforms/Passes.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringRef.h" @@ -355,7 +357,7 @@ TEST_F(TargetSynthesisTest, FixedPulseSynthesisPreservesFullUnitary) { }; if (halfTurn) { operations.push_back(valid(OperationCapability::create( - name, 1, 1, {}, std::nullopt, std::nullopt, {*halfTurn}))); + name, 1, 1, {}, std::nullopt, std::nullopt, {halfTurn}))); } const auto target = valid( Target::create(2, Connectivity::allToAll(), @@ -371,14 +373,47 @@ TEST_F(TargetSynthesisTest, FixedPulseSynthesisPreservesFullUnitary) { builder.sink(builder.ry(theta, targetQubit)); return builder.intConstant(0); }; - auto expected = build(circuit); - auto actual = build(circuit); - ASSERT_TRUE(mlir::succeeded(runTargetPass( - *actual, target, mlir::qco::createTargetNativeSynthesis()))); - ASSERT_TRUE(mlir::succeeded( - runPass(*actual, mlir::qco::createVerifyTargetConformance()))); - EXPECT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*actual))); - expectEquivalent(expected, actual); + for (const std::optional parameterIndex : { + std::optional{}, + std::optional{0U}, + std::optional{1U}, + }) { + SCOPED_TRACE(testing::Message() + << "theta=" << theta << " parameter=" + << (parameterIndex ? std::to_string(*parameterIndex) + : "none")); + auto expected = build(circuit); + auto actual = build(circuit); + auto function = mainFunction(*actual); + if (parameterIndex) { + // Keep theta fixed when phi is symbolic to exercise the + // zero, quarter-turn, and half-turn shortcuts at runtime. + function.insertArgument(0, + mlir::Float64Type::get(context.get()), + {}, function.getLoc()); + auto gate = *function.getOps().begin(); + auto originalParameter = gate.getParameter(*parameterIndex); + originalParameter.replaceAllUsesWith(function.getArgument(0)); + } + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *actual, target, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded(runPass( + *actual, mlir::qco::createVerifyTargetConformance()))); + EXPECT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*actual))); + if (parameterIndex) { + mlir::OpBuilder builder(context.get()); + builder.setInsertionPointToStart(&function.getBody().front()); + auto constant = mlir::arith::ConstantOp::create( + builder, function.getLoc(), + builder.getF64FloatAttr(*parameterIndex == 0 ? theta + : .42)); + function.getArgument(0).replaceAllUsesWith( + constant.getResult()); + ASSERT_TRUE(mlir::succeeded( + runPass(*actual, mlir::createCanonicalizerPass()))); + } + expectEquivalent(expected, actual); + } } } } From 35269c7d471864e6734d823cbe80af89eda9028c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 21 Sep 2026 14:10:09 +0200 Subject: [PATCH 10/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Share=20fixed-pulse?= =?UTF-8?q?=20synthesis=20recipes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 馃 *AI text below* 馃 Use one pulse and phase recipe for numeric and symbolic synthesis. Keep the computed pulse plan internal instead of exposing it through Python. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 5 +- bindings/mlir/register_mlir.cpp | 19 +----- .../QCO/Transforms/Decomposition/Euler.cpp | 42 +++--------- .../Transforms/Decomposition/PulseSynthesis.h | 66 +++++++++++++++++++ .../MergeSingleQubitRotationGates.cpp | 66 ++++++------------- python/mqt/core/mlir.pyi | 20 ------ test/python/test_mlir.py | 6 -- 7 files changed, 102 insertions(+), 122 deletions(-) create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/PulseSynthesis.h diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 3cdb1dd604..639fa9c8d0 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -27,12 +27,15 @@ Only inspect parameter values for constrained capabilities. Unrestricted targets keep their existing pipelines. Basis selection must never treat a fixed-angle rotation as an arbitrary rotation. +Numeric and symbolic synthesis share one fixed-pulse recipe. Pulse-plan details +stay internal; Python exposes target capabilities and the selected basis kind. + ## Validation The compiler suite passed 235 tests; native synthesis passed 64 tests, including 1,944 full-matrix cases across all six axis pairs, both signs, fractional and non-Clifford angles, optional half turns, and numeric or symbolic parameters. -Python target tests passed 388 cases, including numerical and symbolic input +Python target tests passed 386 cases, including numerical and symbolic input gates. Generated stubs, repository lint, and full changed-file C++ lint passed. ## Follow-up diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 55d77e7c74..385fb2ed82 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -919,20 +919,6 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); .value("FixedRotation", mlir::CompilerTarget::SingleQubitBasis::FixedRotation); - nb::class_( - compilerTarget, "FixedRotationBasis", - "Fixed pulse and arbitrary rotation selected for synthesis.") - .def_ro("gate", &mlir::CompilerTarget::FixedRotationBasis::gate) - .def_ro("free_gate", &mlir::CompilerTarget::FixedRotationBasis::freeGate) - .def_ro("angle", &mlir::CompilerTarget::FixedRotationBasis::angle, - "Native pulse angle in radians.") - .def_prop_ro("quarter_turn_pulses", - [](const mlir::CompilerTarget::FixedRotationBasis& basis) { - return basis.quarterTurnAngles.size() - 1; - }) - .def_ro("half_turn_angle", - &mlir::CompilerTarget::FixedRotationBasis::halfTurnAngle); - auto synthesisBasis = nb::class_( compilerTarget, "SynthesisBasis", "One synthesis basis usable across the complete target."); @@ -941,10 +927,7 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); &mlir::CompilerTarget::SynthesisBasis::singleQubit, "The single-qubit synthesis basis.") .def_ro("entangler", &mlir::CompilerTarget::SynthesisBasis::entangler, - "The two-qubit entangler, or None when none is usable.") - .def_ro("fixed_rotation", - &mlir::CompilerTarget::SynthesisBasis::fixedRotation, - "Fixed-pulse decomposition, or None for other bases."); + "The two-qubit entangler, or None when none is usable."); nb::enum_( compilerTarget, "ConnectivityKind", "The target connectivity model.") diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index d707b90132..b3ac0a66f7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -14,13 +14,14 @@ #include "mqt/Dialect/QCO/IR/QCOOps.h" #include "mqt/Dialect/QCO/Utils/Matrix.h" +#include "PulseSynthesis.h" + #include "mlir/IR/Builders.h" #include "mlir/IR/Location.h" #include "mlir/IR/Operation.h" #include "mlir/IR/Value.h" #include "mlir/Support/LLVM.h" -#include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/TypeSwitch.h" #include "llvm/Support/ErrorHandling.h" @@ -371,40 +372,17 @@ struct Unitary1QEulerPlan { case SingleQubitBasis::FixedRotation: { assert(fixedRotation && "fixed-pulse synthesis requires a pulse descriptor"); - constexpr double pi = std::numbers::pi; - constexpr double halfPi = pi / 2.; const auto kind = fixedRotation->gate == fixedRotation->axes()[0] ? SynthesisStep::Kind::RX : SynthesisStep::Kind::RY; - const double axis = kind == SynthesisStep::Kind::RY ? halfPi : 0.; - const auto quarterTurn = [&] { - appendRotation(SynthesisStep::Kind::RZ, - fixedRotation->quarterTurnAngles.front()); - for (double zAngle : - ArrayRef(fixedRotation->quarterTurnAngles).drop_front()) { - steps.emplace_back(kind, fixedRotation->angle); - appendRotation(SynthesisStep::Kind::RZ, zAngle); - } - }; - if (isNearZeroRotationAngle(angles.theta - halfPi)) { - appendRotation(SynthesisStep::Kind::RZ, angles.lambda - halfPi); - quarterTurn(); - appendRotation(SynthesisStep::Kind::RZ, angles.phi + halfPi); - phase = angles.phase; - } else if (isNearZeroRotationAngle(angles.theta - pi) && - fixedRotation->halfTurnAngle) { - appendRotation(SynthesisStep::Kind::RZ, angles.lambda + axis); - steps.emplace_back(kind, *fixedRotation->halfTurnAngle); - appendRotation(SynthesisStep::Kind::RZ, angles.phi + pi - axis); - phase = angles.phase + (*fixedRotation->halfTurnAngle < 0. ? pi : 0.); - } else { - appendRotation(SynthesisStep::Kind::RZ, angles.lambda); - quarterTurn(); - appendRotation(SynthesisStep::Kind::RZ, angles.theta + pi); - quarterTurn(); - appendRotation(SynthesisStep::Kind::RZ, angles.phi + pi); - phase = angles.phase + pi; - } + phase = angles.phase + + emitFixedRotationSequence( + *fixedRotation, angles.theta, angles.phi, angles.lambda, + angles.theta, [](double value) { return value; }, + [&](double angle) { + appendRotation(SynthesisStep::Kind::RZ, angle); + }, + [&](double angle) { steps.emplace_back(kind, angle); }); break; } case SingleQubitBasis::ZSXX: { diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/PulseSynthesis.h b/mlir/lib/Dialect/QCO/Transforms/Decomposition/PulseSynthesis.h new file mode 100644 index 0000000000..5dd516d0d6 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/PulseSynthesis.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mqt/Compiler/Target.h" +#include "mqt/Dialect/MQT/Utils/Parameters.h" + +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +/// Emit local ZYZ angles with fixed pulses, returning the phase correction. +/// Numeric and SSA callers supply constants and emitters for their angle type. +/// Callers handle a statically zero theta by emitting phi + lambda directly. +template +double +emitFixedRotationSequence(const CompilerTarget::FixedRotationBasis& basis, + Angle theta, Angle phi, Angle lambda, + std::optional constantTheta, auto constant, + auto emitFree, auto emitPulse) { + constexpr double pi = std::numbers::pi; + constexpr double halfPi = pi / 2.; + const auto matchesTheta = [&](double value) { + return constantTheta && std::abs(*constantTheta - value) <= + mqt::PARAMETER_COMPARISON_TOLERANCE; + }; + const auto quarterTurn = [&] { + emitFree(constant(basis.quarterTurnAngles.front())); + for (size_t i = 1; i < basis.quarterTurnAngles.size(); ++i) { + emitPulse(basis.angle); + emitFree(constant(basis.quarterTurnAngles[i])); + } + }; + if (matchesTheta(halfPi)) { + emitFree(lambda - constant(halfPi)); + quarterTurn(); + emitFree(phi + constant(halfPi)); + return 0.; + } + if (matchesTheta(pi) && basis.halfTurnAngle) { + const double axis = basis.gate == basis.axes()[0] ? 0. : halfPi; + emitFree(lambda + constant(axis)); + emitPulse(*basis.halfTurnAngle); + emitFree(phi + constant(pi) - constant(axis)); + return *basis.halfTurnAngle < 0. ? pi : 0.; + } + emitFree(lambda); + quarterTurn(); + emitFree(theta + constant(pi)); + quarterTurn(); + emitFree(phi + constant(pi)); + return pi; +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index ce896b74e9..f38f90ebc3 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -17,6 +17,8 @@ #include "mqt/Dialect/QCO/Transforms/Passes.h" #include "mqt/Dialect/QCO/Utils/WireIterator.h" +#include "../Decomposition/PulseSynthesis.h" + #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Math/IR/Math.h" #include "mlir/IR/Builders.h" @@ -779,59 +781,33 @@ static Value emitRuntimeEulerAngles( case decomposition::SingleQubitBasis::FixedRotation: { assert(fixedRotation && "fixed-pulse synthesis requires a pulse descriptor"); - const auto halfPi = - Val::constant(rewriter, loc, std::numbers::pi / 2.); - const auto axes = fixedRotation->axes(); - const bool isX = fixedRotation->gate == axes[0]; - const auto axis = - Val::constant(rewriter, loc, isX ? 0. : std::numbers::pi / 2.); + const auto constant = [&](double value) { + return Val::constant(rewriter, loc, value); + }; const auto emit = [&](CompilerTarget::GateKind gate, Val angle) { switch (gate) { case CompilerTarget::GateKind::RX: - return emitRotationIfNeeded(rewriter, loc, qubit, angle); + qubit = emitRotationIfNeeded(rewriter, loc, qubit, angle); + break; case CompilerTarget::GateKind::RY: - return emitRotationIfNeeded(rewriter, loc, qubit, angle); + qubit = emitRotationIfNeeded(rewriter, loc, qubit, angle); + break; default: - return emitRotationIfNeeded(rewriter, loc, qubit, angle); - } - }; - const auto emitFree = [&](Val angle) { - return emit(fixedRotation->freeGate, angle); - }; - const auto emitPulse = [&](double angle) { - qubit = - emit(fixedRotation->gate, Val::constant(rewriter, loc, angle)); - }; - const auto quarterTurn = [&] { - for (const auto [index, zAngle] : - llvm::enumerate(fixedRotation->quarterTurnAngles)) { - if (index != 0) { - emitPulse(fixedRotation->angle); - } - qubit = emitFree(Val::constant(rewriter, loc, zAngle)); + qubit = emitRotationIfNeeded(rewriter, loc, qubit, angle); + break; } }; if (isConstantAngle(theta)) { - qubit = emitFree(sumAngles(phi, lambda)); - } else if (isConstantAngle(theta, std::numbers::pi / 2.)) { - qubit = emitFree(lambda - halfPi); - quarterTurn(); - qubit = emitFree(phi + halfPi); - } else if (isConstantAngle(theta, std::numbers::pi) && - fixedRotation->halfTurnAngle) { - qubit = emitFree(lambda + axis); - emitPulse(*fixedRotation->halfTurnAngle); - qubit = emitFree(phi + consts.pi - axis); - if (*fixedRotation->halfTurnAngle < 0.) { - phase = phase + consts.pi; - } - } else { - qubit = emitFree(lambda); - quarterTurn(); - qubit = emitFree(theta + consts.pi); - quarterTurn(); - qubit = emitFree(phi + consts.pi); - phase = phase + consts.pi; + emit(fixedRotation->freeGate, sumAngles(phi, lambda)); + break; + } + const double correction = decomposition::emitFixedRotationSequence( + *fixedRotation, theta, phi, lambda, mqt::valueToConstantDouble(theta.v), + constant, + [&](Val angle) { emit(fixedRotation->freeGate, angle); }, + [&](double angle) { emit(fixedRotation->gate, constant(angle)); }); + if (correction != 0.) { + phase = phase + constant(correction); } break; } diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 6646439328..8936f98561 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -382,22 +382,6 @@ class CompilerTarget: FixedRotation = 7 - class FixedRotationBasis: - """Fixed pulse and arbitrary rotation selected for synthesis.""" - - @property - def gate(self) -> CompilerTarget.GateKind: ... - @property - def free_gate(self) -> CompilerTarget.GateKind: ... - @property - def angle(self) -> float: - """Native pulse angle in radians.""" - - @property - def quarter_turn_pulses(self) -> int: ... - @property - def half_turn_angle(self) -> float | None: ... - class SynthesisBasis: """One synthesis basis usable across the complete target.""" @@ -409,10 +393,6 @@ class CompilerTarget: def entangler(self) -> CompilerTarget.GateKind | None: """The two-qubit entangler, or None when none is usable.""" - @property - def fixed_rotation(self) -> CompilerTarget.FixedRotationBasis | None: - """Fixed-pulse decomposition, or None for other bases.""" - class ConnectivityKind(enum.Enum): """The target connectivity model.""" diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index e0d496dd0b..79aa2c55ec 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -670,12 +670,6 @@ def test_fixed_parameter_target_capability(arity: int | CompilerTarget.Operation ) assert target.synthesis_basis is not None assert target.synthesis_basis.single_qubit == CompilerTarget.SingleQubitBasis.FixedRotation - fixed = target.synthesis_basis.fixed_rotation - assert fixed is not None - assert fixed.gate == CompilerTarget.GateKind.RX - assert fixed.angle == pytest.approx(np.pi / 2) - assert fixed.quarter_turn_pulses == 1 - assert fixed.half_turn_angle is None assert not target.supports_operation("rx", 1, 1) assert target.supports_operation("rx", 1, parameters=[np.pi / 2], sites=[0]) assert not target.supports_operation("rx", 1, parameters=[np.pi]) From 2415471757856dac9e95c57842e43476f58e44d6 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 21 Sep 2026 15:23:50 +0200 Subject: [PATCH 11/11] =?UTF-8?q?=F0=9F=94=A7=20Scope=20pulse=20synthesis?= =?UTF-8?q?=20includes=20to=20their=20library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 馃 *AI text below* 馃 Use a private include root for the shared synthesis helper, avoiding parent-directory traversal without installing implementation details. Assisted-by: GPT-6 via Codex --- mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt | 1 + mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp | 2 +- .../Transforms/Optimizations/MergeSingleQubitRotationGates.cpp | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 5b5ab3f7e4..f173e56fbe 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -30,6 +30,7 @@ add_mlir_library( MLIRQCOTransformsIncGen) mqt_mlir_target_use_project_options(MLIRQCOTransforms) +target_include_directories(MLIRQCOTransforms PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) file(GLOB_RECURSE PASSES_HEADERS_SOURCE ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mqt/Dialect/QCO/Transforms/*.h) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index b3ac0a66f7..d593c4a8a2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -14,7 +14,7 @@ #include "mqt/Dialect/QCO/IR/QCOOps.h" #include "mqt/Dialect/QCO/Utils/Matrix.h" -#include "PulseSynthesis.h" +#include "Decomposition/PulseSynthesis.h" #include "mlir/IR/Builders.h" #include "mlir/IR/Location.h" diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index f38f90ebc3..763c985afa 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -17,7 +17,7 @@ #include "mqt/Dialect/QCO/Transforms/Passes.h" #include "mqt/Dialect/QCO/Utils/WireIterator.h" -#include "../Decomposition/PulseSynthesis.h" +#include "Decomposition/PulseSynthesis.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Math/IR/Math.h"