From ebc5f7eafb42b9649bc63280018e9996c5ffa5bf Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 10:20:47 +0200 Subject: [PATCH 01/23] =?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 | 43 +++-- 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 ++++++++ test/python/test_mlir.py | 59 +++++++ 14 files changed, 559 insertions(+), 64 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 874d0c1496..31b2448574 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -861,7 +861,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( @@ -869,10 +870,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, @@ -880,7 +882,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( @@ -888,10 +891,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) { @@ -917,6 +921,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.") @@ -952,7 +965,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", @@ -1217,22 +1231,27 @@ 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(), + "sites"_a = nb::none(), nb::kw_only(), + "parameters"_a = std::vector>{}, R"pb(Check whether the target supports an operation. Args: name: Operation name. Recognized aliases are normalized. arity: Number of qubits used by the operation. num_parameters: Number of real-valued parameters. None accepts any count. - sites: Ordered target site IDs. None checks support on any placement.)pb"); + sites: Ordered target site IDs. None checks support on any placement. + parameters: Known parameter values. Omitted or None values require + unrestricted support.)pb"); nb::class_( m, "TargetEnvironment", diff --git a/bindings/patterns.txt b/bindings/patterns.txt index 751611d59f..85c1f4c425 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 8bc9d98bc3..2a2482006d 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -219,7 +219,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 59494f41f3..14cf6593d0 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -180,6 +180,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 6381378d53..b73ef3812e 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. @@ -400,16 +412,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 bb0d81ebec..f5b63b2da8 100644 --- a/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td @@ -212,7 +212,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 f1b6800111..86952e754b 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" @@ -379,21 +380,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"); @@ -416,6 +418,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())) { @@ -430,15 +446,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) {} @@ -459,6 +477,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_; @@ -510,11 +533,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; @@ -683,7 +706,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)) { @@ -711,7 +735,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; + }); }); } @@ -751,6 +788,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { OperationCapability::Arity::Kind::Variadic) && operation.arity().accepts(arity) && operation.numParameters() == numParameters && + operation.fixedParameters().empty() && operation.siteTuples().empty(); }); }; @@ -777,6 +815,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) { @@ -977,10 +1022,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(); } @@ -1148,6 +1207,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); } @@ -1176,10 +1251,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) { @@ -1204,9 +1281,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); @@ -1292,10 +1371,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 f1a19761a1..40e8bf5895 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -250,9 +250,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"; } @@ -261,6 +262,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 7f74d0879d..fb3039c801 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -755,7 +755,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)); @@ -791,6 +792,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 = @@ -1008,6 +1027,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: @@ -1317,9 +1337,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 1a3d46722f..ef1a812439 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" @@ -41,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -895,6 +897,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 37d021ec88..c7b78e68e1 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" @@ -327,6 +328,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/test/python/test_mlir.py b/test/python/test_mlir.py index e8a98dc537..f95fbf0f00 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -654,6 +654,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_symbolic_su2_compiles_and_binds_after_export() -> None: """Compile symbolic SU2 circuits with bindable parameters and exact phase.""" From 999622116b506636f4ca2566a4689a1197fa133c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 11:09:17 +0200 Subject: [PATCH 02/23] =?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 ++++++++++++------ test/python/test_mlir.py | 22 +++- 13 files changed, 405 insertions(+), 128 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 31b2448574..4370c37c07 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -966,7 +966,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", @@ -976,7 +990,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 14cf6593d0..8c31c6ccba 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -186,9 +186,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 b73ef3812e..26653e7976 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 e639da2daa..acafad346b 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 86952e754b..f690fb51b2 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) { @@ -798,6 +851,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { }); }; std::optional singleQubit; + std::optional fixedRotation; if (supportsOnEverySite(GateKind::U)) { singleQubit = SingleQubitBasis::U; } else if (supportsOnEverySite(GateKind::X) && @@ -815,13 +869,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) { @@ -877,6 +961,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 809a5c167c..2bb5d3f3c3 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -921,7 +921,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 bd5ca0c8ee..9aca8ce60f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -1091,13 +1091,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 fb3039c801..3e361c457c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -747,16 +747,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)); @@ -792,19 +794,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; } @@ -1027,7 +1057,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: @@ -1319,12 +1349,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; } @@ -1337,9 +1367,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; @@ -1348,7 +1379,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 ef1a812439..3fd9014e52 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -1016,10 +1016,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 c7b78e68e1..439b0337c8 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -329,38 +329,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/test/python/test_mlir.py b/test/python/test_mlir.py index f95fbf0f00..bf9059cc84 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -668,7 +668,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]) @@ -683,13 +689,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), ]), @@ -703,8 +713,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 0756b4e2c963cf60ba7c43382018cbe9fcc7fc60 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 11:22:16 +0200 Subject: [PATCH 03/23] =?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 4370c37c07..f0514f457a 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -966,18 +966,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 8c31c6ccba..ea4b5c108c 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -186,11 +186,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 26653e7976..422addc8f8 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 f690fb51b2..ac2f0d1160 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") { @@ -869,7 +884,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( @@ -877,34 +892,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 3e361c457c..a701ac8db7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -754,11 +754,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)); @@ -794,48 +792,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; @@ -1057,7 +1068,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: @@ -1370,17 +1381,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 3fd9014e52..d55bc2e10a 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -1016,7 +1016,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 439b0337c8..677c2ec326 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -329,48 +329,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 bf9059cc84..3246ebee85 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -668,11 +668,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) @@ -690,17 +690,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), ]), ) @@ -713,7 +721,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 ef290ca4edb2ce268b14649a7897c8e0f695d30e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 11:27:26 +0200 Subject: [PATCH 04/23] =?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 ac2f0d1160..ca9edd17a5 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 a701ac8db7..8eee42b068 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -1390,7 +1390,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. @@ -1401,10 +1402,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 677c2ec326..6dfc320ee9 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -329,17 +329,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.)); @@ -384,7 +394,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 8dac7479e532262dc9460438d3548a7a944f11dd Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 11:30:37 +0200 Subject: [PATCH 05/23] =?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 b29be1ad2cc5b50269a340c1e0936c08f3434e2e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 12:54:00 +0200 Subject: [PATCH 06/23] =?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 b1fde10313..2d6c3865d8 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -25,7 +25,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -411,6 +413,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 12ced14d0cbc787e6be2f7784b900a5626ade808 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 18 Sep 2026 13:25:22 +0200 Subject: [PATCH 07/23] =?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 | 51 +++++++++++++++---- 2 files changed, 46 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 6dfc320ee9..31ec65dddc 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -362,7 +362,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(), @@ -378,14 +378,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 c79d81ab27e6ed752158ec5d5badc1f0194c1396 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 21 Sep 2026 14:10:09 +0200 Subject: [PATCH 08/23] =?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 ++++++------------- test/python/test_mlir.py | 6 -- 6 files changed, 102 insertions(+), 102 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 f0514f457a..34b801980d 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -969,20 +969,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."); @@ -991,10 +977,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 8eee42b068..0180c1fad6 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" @@ -795,59 +797,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/test/python/test_mlir.py b/test/python/test_mlir.py index 3246ebee85..1a2334b5cd 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -669,12 +669,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 a15954ab0aa92b7849f0b5995526443299f02cd4 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 21 Sep 2026 15:23:50 +0200 Subject: [PATCH 09/23] =?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 7b96dded04..32e53758b9 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -31,6 +31,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 0180c1fad6..bd0e6f6449 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" From 7eb869f5a921e64f1850fe50b308bcf88bcb8bb6 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 25 Sep 2026 14:04:22 +0200 Subject: [PATCH 10/23] =?UTF-8?q?=F0=9F=90=9B=20Preserve=20fixed-pulse=20l?= =?UTF-8?q?owering=20during=20fusion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Keep fixed-pulse targets out of the new target-aware fusion stage. Generic fusion lacks their pulse descriptor and rotation simplification can undo constrained angles, causing assertions or repeated rewrites. The existing fixed-pulse lowering handles these targets directly. Existing full-unitary synthesis tests cover constant and runtime angles across all supported rotation axes. Assisted-by: GPT-6 via Codex --- .../QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 9aca8ce60f..5dede7fc24 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -1349,8 +1349,11 @@ struct TargetNativeSynthesisPass final signalPassFailure(); return; } + // Fixed-pulse lowering must preserve constrained rotation parameters. if (targetBasis && - targetBasis->singleQubit != CompilerTarget::SingleQubitBasis::U) { + targetBasis->singleQubit != CompilerTarget::SingleQubitBasis::U && + targetBasis->singleQubit != + CompilerTarget::SingleQubitBasis::FixedRotation) { RewritePatternSet patterns(&getContext()); decomposition::populateParameterizedSingleQubitRunCompositionPatterns( patterns, targetBasis->singleQubit, &target); From 453e8251bd8af2cdd646c2103aacb46eb8770c09 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 29 Sep 2026 10:41:07 +0200 Subject: [PATCH 11/23] =?UTF-8?q?=E2=9A=A1=20Reuse=20cached=20fixed-pulse?= =?UTF-8?q?=20bases=20during=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 The native-cost routing code now queries the target basis repeatedly. Borrow its cached pulse recipe through routing and synthesis instead of copying the vector at each query and emission. Keep Python property access independent through an explicit copy policy. Check fixed-pulse cost estimates against native emission and full-unitary semantics. Document the remaining fusion and synthesis limits. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 12 +++--- bindings/mlir/register_mlir.cpp | 1 + docs/mlir/target_compilation.md | 5 +++ mlir/include/mqt/Compiler/Target.h | 4 +- .../QCO/Transforms/Decomposition/Weyl.h | 2 +- mlir/lib/Compiler/Target.cpp | 2 +- mlir/lib/Compiler/TargetCompilation.cpp | 2 +- .../QCO/Transforms/Decomposition/Weyl.cpp | 2 +- .../QCO/Transforms/Mapping/Mapping.cpp | 2 +- .../NativeSynthesis/TargetSynthesis.cpp | 19 +++++---- .../NativeSynthesis/test_target_synthesis.cpp | 40 +++++++++++++++++++ 11 files changed, 70 insertions(+), 21 deletions(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 639fa9c8d0..7f3a798889 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -29,14 +29,16 @@ 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. +Routing and synthesis borrow the cached basis, including its pulse sequence; +Python property access retains value semantics. ## 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 386 cases, including numerical and symbolic input -gates. Generated stubs, repository lint, and full changed-file C++ lint passed. +The rebased compiler suite passed 243 tests, native synthesis passed 78, and +mapping passed 125. Native synthesis includes 1,944 full-matrix cases across all +six axis pairs and a regression comparing routing costs with fixed-pulse +emission. Python fixed-parameter tests passed 362 cases, including numeric and +symbolic input gates. Generated stubs are unchanged. ## Follow-up diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 34b801980d..628a5d45b2 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -1225,6 +1225,7 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); }, "Recognized native gates supported by the target.") .def_prop_ro("synthesis_basis", &mlir::CompilerTarget::synthesisBasis, + nb::rv_policy::copy, "A target-wide single-qubit basis with an optional " "entangler, or None when no single-qubit basis is usable.") .def( diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index ea4b5c108c..3a3b940283 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -198,6 +198,11 @@ 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. +Fixed-pulse targets use individual single-qubit lowering; generic run fusion can +change the required pulse angles. Two-qubit run fusion remains available. +Routing costs count native two-qubit gates, not the number of fixed pulses. +Constraints on other gate families are checked, but do not create additional +synthesis bases. Parameter ranges and relations are not supported. 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 422addc8f8..772c7a1963 100644 --- a/mlir/include/mqt/Compiler/Target.h +++ b/mlir/include/mqt/Compiler/Target.h @@ -468,7 +468,9 @@ class CompilerTarget { [[nodiscard]] llvm::ArrayRef supportedGates() const noexcept; /// Return a globally usable single-qubit basis with an optional entangler. - [[nodiscard]] std::optional synthesisBasis() const noexcept; + /// The cached basis remains valid while this target or a copy exists. + [[nodiscard]] const std::optional& + synthesisBasis() const noexcept; /// Materialize the source target facts as a typed MLIR attribute. [[nodiscard]] mqt::CompilationTargetAttr diff --git a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Weyl.h index b78d5085a7..89dd0cffd1 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -351,6 +351,6 @@ decomposeUnitary2QWeyl(const Matrix4x4& target, [[nodiscard]] SynthesizedUnitary2Q emitUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value qubit1, const TwoQubitNativeDecomposition& decomposition, - CompilerTarget::SynthesisBasis basis); + const CompilerTarget::SynthesisBasis& basis); } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index ca9edd17a5..0b13d5ff7b 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -1423,7 +1423,7 @@ ArrayRef CompilerTarget::supportedGates() const noexcept { return storage_->supportedGates; } -std::optional +const std::optional& CompilerTarget::synthesisBasis() const noexcept { return storage_->basis; } diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index e9a7155af9..aaad7b085e 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -211,7 +211,7 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(qco::createFuseTwoQubitGates(target)); /// Non-U targets fuse during native synthesis, avoiding an intermediate U /// representation and its symbolic phase correction. - if (const auto basis = target.synthesisBasis(); + if (const auto& basis = target.synthesisBasis(); !basis || basis->singleQubit == CompilerTarget::SingleQubitBasis::U) { /// The U optimizer also merges dynamic controlled bodies into native U /// gates and preserves isolated gates. Native synthesis does not yet cover diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 2bb5d3f3c3..f0ee7d4066 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -900,7 +900,7 @@ decomposeUnitary2QWeyl(const Matrix4x4& target, SynthesizedUnitary2Q emitUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value qubit1, const TwoQubitNativeDecomposition& decomposition, - const CompilerTarget::SynthesisBasis basis) { + const CompilerTarget::SynthesisBasis& basis) { if (!basis.entangler) { llvm::reportFatalInternalError( "two-qubit emission requires a synthesis-basis entangler"); diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 916690165b..2026aa386d 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -508,7 +508,7 @@ struct MappingPass : impl::MappingPassBase { std::optional nativeSwapCost; void prepareNativeCosts(Operation* root) { - const auto basis = target.synthesisBasis(); + const auto& basis = target.synthesisBasis(); if (!basis || !basis->entangler || target.nativeOperationsKind() != CompilerTarget::NativeOperations::Kind::Explicit) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 5dede7fc24..8199c1a929 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -744,7 +744,7 @@ NativeCostAnalysis::operationCost(UnitaryOpInterface operation, std::optional NativeCostAnalysis::matrixCost(const Matrix4x4& matrix, const CompilerTarget& target, Sites sites) { - const auto basis = target.synthesisBasis(); + const auto& basis = target.synthesisBasis(); if (!basis || !basis->entangler) { return std::nullopt; } @@ -1184,7 +1184,7 @@ static bool reducesNativeCost(const FusableTwoQubitRun& run, size_t fusedCost, /// Without a target, the original operation count is a conservative bound. static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, const Matrix4x4& headMatrix, - CompilerTarget::SynthesisBasis basis, + const CompilerTarget::SynthesisBasis& basis, const CompilerTarget* target, const SiteMap* sites, NativeCostAnalysis& analysis, bool shrinkOnly) { @@ -1231,7 +1231,7 @@ static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, } static bool fuseTwoQubitGates(IRRewriter& rewriter, ModuleOp moduleOp, - CompilerTarget::SynthesisBasis basis, + const CompilerTarget::SynthesisBasis& basis, NativeCostAnalysis& analysis, const CompilerTarget* target = nullptr, const SiteMap* sites = nullptr, @@ -1267,12 +1267,11 @@ struct FuseTwoQubitGatesPass final protected: void runOnOperation() override { ModuleOp moduleOp = getOperation(); - const auto basis = - target_ ? target_->synthesisBasis() - : std::optional{CompilerTarget::SynthesisBasis{ - .singleQubit = CompilerTarget::SingleQubitBasis::U, - .entangler = CompilerTarget::GateKind::CZ, - }}; + const std::optional defaultBasis{CompilerTarget::SynthesisBasis{ + .singleQubit = CompilerTarget::SingleQubitBasis::U, + .entangler = CompilerTarget::GateKind::CZ, + }}; + const auto& basis = target_ ? target_->synthesisBasis() : defaultBasis; if (!basis || !basis->entangler) { return; } @@ -1344,7 +1343,7 @@ struct TargetNativeSynthesisPass final return; } const CompilerTarget& target = environment.environment().target(); - const auto targetBasis = target.synthesisBasis(); + const auto& targetBasis = target.synthesisBasis(); if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); return; 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 31ec65dddc..74b722b1ce 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -657,6 +657,46 @@ TEST_F(TargetSynthesisTest, ReadOnlyNativeCostMatchesSynthesis) { } } +TEST_F(TargetSynthesisTest, NativeCostMatchesFixedPulseSynthesis) { + const auto target = valid(Target::create( + 2, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(OperationCapability::create("ry", 1, 1)), + valid(OperationCapability::create("rz", 1, 1, {}, std::nullopt, + std::nullopt, {.37})), + valid(OperationCapability::create("cz", 2, 0)), + valid(OperationCapability::create("gphase", 0, 1)), + }))); + auto program = build([](QCOProgramBuilder& builder) { + auto [a, b] = builder.cx(builder.staticQubit(0), builder.staticQubit(1)); + std::tie(a, b) = builder.swap(a, b); + builder.sink(a); + builder.sink(b); + return builder.intConstant(0); + }); + OwningOpRef expected = program->clone(); + const auto shared = mlir::qco::NativeCostTable::precompute( + *program, Target::GateKind::CZ, 2023); + mlir::qco::NativeCostTracker tracker(target, 2023, shared.get()); + for (auto gate : + mainFunction(*program).getOps()) { + tracker.append(gate, std::array{0, 1}); + } + const auto score = tracker.score(); + ASSERT_TRUE(score); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *program, target, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded( + runPass(*program, mlir::qco::createVerifyTargetConformance()))); + size_t emitted = 0; + for (auto gate : + mainFunction(*program).getOps()) { + emitted += static_cast(gate.isTwoQubit()); + } + EXPECT_EQ(score->first, emitted); + expectEquivalent(expected, program); +} + TEST_F(TargetSynthesisTest, NativeCostPreservesSingletonNativeGates) { const auto target = valid( Target::create(2, Connectivity::allToAll(), From 6ef8f84d65d72050c7d572072b6fb5c724457320 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 29 Sep 2026 10:46:48 +0200 Subject: [PATCH 12/23] =?UTF-8?q?=F0=9F=93=9D=20Clarify=20native-only=20ta?= =?UTF-8?q?rget=20compilation=20limits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Matching each input gate does not guarantee successful compilation when no synthesis basis exists. Optimizations can combine native gates into an unsupported gate or angle. Record this existing limitation separately from the fixed-pulse synthesis contract. Assisted-by: GPT-6 via Codex --- docs/mlir/target_compilation.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 3a3b940283..e25e864abc 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -198,6 +198,11 @@ 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. + +Without a usable synthesis basis, full compilation can still reject native gate +sequences when optimization combines them into an unsupported operation or +angle. For example, two native RZ(0.37) pulses may become RZ(0.74). + Fixed-pulse targets use individual single-qubit lowering; generic run fusion can change the required pulse angles. Two-qubit run fusion remains available. Routing costs count native two-qubit gates, not the number of fixed pulses. From d129e20c4437a14f24f7871006277e3616523a5a Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 29 Sep 2026 11:13:15 +0200 Subject: [PATCH 13/23] =?UTF-8?q?=F0=9F=90=9B=20Preserve=20native=20gate?= =?UTF-8?q?=20sequences=20during=20target=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Keep gate-changing canonicalization out of explicit-target cleanup, including the inliner, so targets without a universal basis can compile native sequences. Keep structural cleanup and unrestricted-target optimization unchanged. Use the cached fixed-pulse recipe for numeric run fusion. Preserve native runs unless synthesis reduces their gate count. Cover both target pipelines, exported gate forms, exact phase, and pulse cancellation. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 19 +++-- docs/mlir/target_compilation.md | 11 ++- .../mqt/Dialect/QCO/Transforms/Passes.h | 7 +- mlir/include/mqt/Support/Passes.h | 4 +- mlir/lib/Compiler/Pipeline.cpp | 6 +- mlir/lib/Compiler/TargetCompilation.cpp | 34 +++++--- .../QCO/Transforms/CanonicalizeStructure.cpp | 73 +++++++++++++++++ .../FuseSingleQubitUnitaryRuns.cpp | 9 ++- .../NativeSynthesis/TargetSynthesis.cpp | 19 ++--- mlir/lib/Support/Passes.cpp | 11 ++- .../Compiler/test_compiler_pipeline.cpp | 80 +++++++++++++++++++ .../NativeSynthesis/test_target_synthesis.cpp | 38 +++++++++ 12 files changed, 269 insertions(+), 42 deletions(-) create mode 100644 mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 7f3a798889..8f93282087 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -32,13 +32,22 @@ stay internal; Python exposes target capabilities and the selected basis kind. Routing and synthesis borrow the cached basis, including its pulse sequence; Python property access retains value semantics. +Target cleanup applies structural and classical canonicalization, preserving +gate forms for target-aware synthesis. The inliner's cleanup follows the same +rule. Non-universal targets keep their native sequences; operations outside the +native set still need a usable basis. Numeric fixed-pulse fusion uses the cached +recipe and only shortens already native runs. Symbolic fixed-pulse runs continue +to use individual lowering because symbolic fusion does not model pulse costs. + ## Validation -The rebased compiler suite passed 243 tests, native synthesis passed 78, and -mapping passed 125. Native synthesis includes 1,944 full-matrix cases across all -six axis pairs and a regression comparing routing costs with fixed-pulse -emission. Python fixed-parameter tests passed 362 cases, including numeric and -symbolic input gates. Generated stubs are unchanged. +The compiler suite passed 244 tests, native synthesis passed 79, and mapping +passed 125. Native synthesis includes 1,944 full-matrix cases across all six +axis pairs and a regression comparing routing costs with fixed-pulse emission. +Further regressions cover native-only compilation and OpenQASM export, and +fixed-pulse fusion with exact phase. Python fixed-parameter tests passed 362 +cases, including numeric and symbolic input gates. Generated stubs are +unchanged. ## Follow-up diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index e25e864abc..6f26748843 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -199,12 +199,11 @@ 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. -Without a usable synthesis basis, full compilation can still reject native gate -sequences when optimization combines them into an unsupported operation or -angle. For example, two native RZ(0.37) pulses may become RZ(0.74). - -Fixed-pulse targets use individual single-qubit lowering; generic run fusion can -change the required pulse angles. Two-qubit run fusion remains available. +Target cleanup preserves gate forms, including sequences of native gates on +targets without a synthesis basis. Operations outside the native set still need +a usable synthesis basis. Numeric single-qubit run fusion uses the fixed-pulse +recipe and keeps native runs unless it can shorten them. Symbolic fixed-pulse +runs use individual gate lowering. Two-qubit run fusion remains available. Routing costs count native two-qubit gates, not the number of fixed pulses. Constraints on other gate families are checked, but do not create additional synthesis bases. Parameter ranges and relations are not supported. diff --git a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h index 15b745ab25..ee60b74033 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h @@ -38,6 +38,10 @@ namespace mlir::qco { #define GEN_PASS_REGISTRATION #include "mqt/Dialect/QCO/Transforms/Passes.h.inc" // IWYU pragma: export +/// Canonicalize QCO; optionally preserve gate forms for target synthesis. +/// Identity folding and dead-code elimination remain enabled. +[[nodiscard]] std::unique_ptr createQCOCanonicalizer(bool preserveGates); + /// Create target-independent two-qubit gate fusion. /// /// Runs remain unchanged when numerical decomposition fails. @@ -58,6 +62,7 @@ createDecomposeMultiControlled(const CompilerTarget& target, /// Normalize placed QCO, synthesize native gates, and verify target support. /// Requires a valid target environment and placed qubits. -void populateTargetNativeSynthesisPipeline(OpPassManager& pm); +void populateTargetNativeSynthesisPipeline(OpPassManager& pm, + bool preserveGates = true); } // namespace mlir::qco diff --git a/mlir/include/mqt/Support/Passes.h b/mlir/include/mqt/Support/Passes.h index 143b146d40..31ae2f3dfb 100644 --- a/mlir/include/mqt/Support/Passes.h +++ b/mlir/include/mqt/Support/Passes.h @@ -76,7 +76,9 @@ void populateQCExportPipeline(mlir::OpPassManager& pm); /// Populate a QCO-oriented cleanup pipeline on the given pass manager. /// /// Adds generic cleanup, qtensor shrink-to-fit, and dead-value removal. -void populateQCOCleanupPipeline(mlir::OpPassManager& pm); +/// Set @p preserveGates to leave gate rewriting to target-aware synthesis. +void populateQCOCleanupPipeline(mlir::OpPassManager& pm, + bool preserveGates = false); /// Populate a QIR-oriented cleanup pipeline on the given pass manager. /// diff --git a/mlir/lib/Compiler/Pipeline.cpp b/mlir/lib/Compiler/Pipeline.cpp index ea7a198b74..7af6c896f3 100644 --- a/mlir/lib/Compiler/Pipeline.cpp +++ b/mlir/lib/Compiler/Pipeline.cpp @@ -131,9 +131,9 @@ std::optional QCProgram::intoQIR(QIRProfile profile) && { //===----------------------------------------------------------------------===// bool QCOProgram::cleanup() { - return succeeded( - runQCOTransformPasses(mod(), populateQCOCleanupPipeline, - "failed to run the QCO cleanup pipeline")); + return succeeded(runQCOTransformPasses( + mod(), [](OpPassManager& pm) { populateQCOCleanupPipeline(pm); }, + "failed to run the QCO cleanup pipeline")); } bool QCOProgram::normalizeGlobalPhases() { diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index aaad7b085e..16ce561a5d 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -28,7 +28,6 @@ #include "mlir/Pass/Pass.h" #include "mlir/Pass/PassManager.h" #include "mlir/Support/WalkResult.h" -#include "mlir/Transforms/GreedyPatternRewriteDriver.h" #include "mlir/Transforms/Passes.h" #include "llvm/ADT/STLExtras.h" @@ -194,15 +193,22 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(std::make_unique(environment, false, mapping)); const auto& target = environment.target(); - pm.addPass(createInlinerPass()); + const bool preserveGates = target.nativeOperationsKind() == + CompilerTarget::NativeOperations::Kind::Explicit; + pm.addPass(preserveGates + ? createInlinerPass({}, + [](OpPassManager& nested) { + nested.addPass( + qco::createQCOCanonicalizer(true)); + }) + : createInlinerPass()); pm.addPass(createSymbolDCEPass()); pm.addPass(createSCCPPass()); - populateQCOCleanupPipeline(pm); + populateQCOCleanupPipeline(pm, preserveGates); pm.addPass(qco::createUnrollLoopsForPayload()); pm.addPass(createSCCPPass()); /// Unrolling exposes static tensor slots and unreachable callees. - pm.addPass(createCanonicalizerPass( - GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit))); + pm.addPass(qco::createQCOCanonicalizer(preserveGates)); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); pm.addPass(createSymbolDCEPass()); @@ -212,7 +218,7 @@ void populateTargetCompilationPipeline(OpPassManager& pm, /// Non-U targets fuse during native synthesis, avoiding an intermediate U /// representation and its symbolic phase correction. if (const auto& basis = target.synthesisBasis(); - !basis || basis->singleQubit == CompilerTarget::SingleQubitBasis::U) { + basis && basis->singleQubit == CompilerTarget::SingleQubitBasis::U) { /// The U optimizer also merges dynamic controlled bodies into native U /// gates and preserves isolated gates. Native synthesis does not yet cover /// both behaviors; keep this path until their synthesis contracts agree. @@ -234,7 +240,7 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(qco::createPlacementPass(target)); break; } - qco::populateTargetNativeSynthesisPipeline(pm); + qco::populateTargetNativeSynthesisPipeline(pm, preserveGates); } void populateTargetSynthesisPipeline(OpPassManager& pm, @@ -243,14 +249,22 @@ void populateTargetSynthesisPipeline(OpPassManager& pm, pm.addPass(std::make_unique(environment, true, mapping)); const auto& target = environment.target(); - pm.addPass(createInlinerPass()); + const bool preserveGates = target.nativeOperationsKind() == + CompilerTarget::NativeOperations::Kind::Explicit; + pm.addPass(preserveGates + ? createInlinerPass({}, + [](OpPassManager& nested) { + nested.addPass( + qco::createQCOCanonicalizer(true)); + }) + : createInlinerPass()); pm.addPass(createSymbolDCEPass()); - populateQCOCleanupPipeline(pm); + populateQCOCleanupPipeline(pm, preserveGates); pm.addPass(qco::createLegalizeControlFlow()); pm.addPass(qco::createDecomposeMultiControlled(target)); pm.addPass(qco::createFuseTwoQubitGates(target)); pm.addPass(qco::createPlacementPass(target)); - qco::populateTargetNativeSynthesisPipeline(pm); + qco::populateTargetNativeSynthesisPipeline(pm, preserveGates); } } // namespace mlir diff --git a/mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp b/mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp new file mode 100644 index 0000000000..6d2f03ef1b --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp @@ -0,0 +1,73 @@ +/* + * 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 + */ + +#include "mqt/Dialect/QCO/IR/QCOInterfaces.h" +#include "mqt/Dialect/QCO/Transforms/Passes.h" + +#include "mlir/IR/Dialect.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/OperationSupport.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Rewrite/FrozenRewritePatternSet.h" +#include "mlir/Support/LogicalResult.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "mlir/Transforms/Passes.h" + +#include +#include + +namespace mlir::qco { +namespace { + +/// Gate-changing canonicalization can leave a target's native gate set. +class CanonicalizeStructurePass final + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(CanonicalizeStructurePass) + + LogicalResult initialize(MLIRContext* context) override { + RewritePatternSet patterns(context); + for (auto* dialect : context->getLoadedDialects()) { + dialect->getCanonicalizationPatterns(patterns); + } + for (auto operation : context->getRegisteredOperations()) { + if (!operation.hasInterface()) { + operation.getCanonicalizationPatterns(patterns, context); + } + } + patterns_ = FrozenRewritePatternSet(std::move(patterns)); + return success(); + } + +protected: + void runOnOperation() override { + if (failed(applyPatternsGreedily(getOperation(), patterns_, + GreedyRewriteConfig{}.setMaxIterations( + GreedyRewriteConfig::kNoLimit)))) { + signalPassFailure(); + } + } + +private: + FrozenRewritePatternSet patterns_; +}; + +} // namespace + +std::unique_ptr createQCOCanonicalizer(bool preserveGates) { + if (!preserveGates) { + return createCanonicalizerPass( + GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit)); + } + return std::make_unique(); +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index aa73250a3b..563ce43215 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -155,9 +155,16 @@ struct FuseSingleQubitUnitaryRunsPattern final } FusableRunScan run = scanFusableRun(op, *headMatrix, basis, target); + const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr; + if (target != nullptr) { + const auto& targetBasis = target->synthesisBasis(); + if (targetBasis && targetBasis->fixedRotation) { + fixedRotation = &*targetBasis->fixedRotation; + } + } const auto synthesized = decomposition::synthesizeUnitary1QEuler( rewriter, op.getLoc(), op.getInputQubit(0), run.composed, run.gateCount, - run.hasNonBasisGate, basis); + run.hasNonBasisGate, basis, fixedRotation); if (!synthesized) { return failure(); } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 8199c1a929..754037c6cd 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -1348,14 +1348,15 @@ struct TargetNativeSynthesisPass final signalPassFailure(); return; } - // Fixed-pulse lowering must preserve constrained rotation parameters. if (targetBasis && - targetBasis->singleQubit != CompilerTarget::SingleQubitBasis::U && - targetBasis->singleQubit != - CompilerTarget::SingleQubitBasis::FixedRotation) { + targetBasis->singleQubit != CompilerTarget::SingleQubitBasis::U) { RewritePatternSet patterns(&getContext()); - decomposition::populateParameterizedSingleQubitRunCompositionPatterns( - patterns, targetBasis->singleQubit, &target); + // Symbolic composition does not yet account for fixed-pulse costs. + if (targetBasis->singleQubit != + CompilerTarget::SingleQubitBasis::FixedRotation) { + decomposition::populateParameterizedSingleQubitRunCompositionPatterns( + patterns, targetBasis->singleQubit, &target); + } decomposition::populateFuseSingleQubitUnitaryRunsPatterns( patterns, targetBasis->singleQubit, /*skipControlledBodies=*/true, &target); @@ -1514,10 +1515,10 @@ std::unique_ptr createFuseTwoQubitGates(const CompilerTarget& target) { return std::make_unique(target); } -void populateTargetNativeSynthesisPipeline(OpPassManager& pm) { +void populateTargetNativeSynthesisPipeline(OpPassManager& pm, + bool preserveGates) { /// Placement consumes allocations; native synthesis normalizes phases. - pm.addPass(createCanonicalizerPass( - GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit))); + pm.addPass(createQCOCanonicalizer(preserveGates)); /// Reuse unchanged classical reads before native synthesis splits their uses. pm.addPass(createCSEPass()); pm.addPass(createRemoveDeadValuesPass()); diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 476de8948c..17c2823641 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -24,7 +24,6 @@ #include "mlir/Pass/PassManager.h" #include "mlir/Pass/PassRegistry.h" #include "mlir/Support/LLVM.h" -#include "mlir/Transforms/GreedyPatternRewriteDriver.h" #include "mlir/Transforms/Passes.h" #include "llvm/ADT/ScopeExit.h" @@ -171,9 +170,8 @@ void populateQCCleanupPipeline(OpPassManager& pm) { pm.addPass(createRemoveDeadValuesPass()); } -void populateQCOCleanupPipeline(OpPassManager& pm) { - pm.addPass(createCanonicalizerPass( - GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit))); +void populateQCOCleanupPipeline(OpPassManager& pm, bool preserveGates) { + pm.addPass(qco::createQCOCanonicalizer(preserveGates)); pm.addPass(mlir::mqt::createNormalizeGlobalPhases()); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); @@ -199,8 +197,9 @@ void populateJeffCleanupPipeline(OpPassManager& pm) { } [[nodiscard]] LogicalResult runQCOCleanupPipeline(ModuleOp mod) { - return runWithPassManager(mod, populateQCOCleanupPipeline, - "Failed to run the QCO cleanup pipeline."); + return runWithPassManager( + mod, [](OpPassManager& pm) { populateQCOCleanupPipeline(pm); }, + "Failed to run the QCO cleanup pipeline."); } [[nodiscard]] LogicalResult runQIRCleanupPipeline(ModuleOp mod, diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index b791c619aa..862ab44927 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -4015,6 +4015,86 @@ TEST_F(CompilerPipelineTest, TargetSynthesisResynthesizesTwoQubitBlocks) { makeSparseUCZTarget(true), makePayloadSpecification()))); } +TEST_F(CompilerPipelineTest, TargetPipelinesPreserveNativeGateSequences) { + using Capability = CompilerTarget::OperationCapability; + using Case = std::tuple>>; + for (const auto& [gate, arguments, arity, parameters] : { + Case{"rz", "(0.37)", 1, {.37}}, + Case{"rx", "(pi / 2)", 1, {std::numbers::pi / 2}}, + Case{"s", "", 1, {}}, + Case{"t", "", 1, {}}, + Case{"sx", "", 1, {}}, + Case{"u", "(pi / 2, 0.2, 0.3)", 1, {std::numbers::pi / 2, .2, .3}}, + Case{"rzz", "(0.37)", 2, {.37}}, + }) { + SCOPED_TRACE(gate); + const std::string wires = arity == 1 ? "a" : "a, b"; + const std::string pulse = std::string(gate) + arguments + " " + wires + ";"; + std::string source = "OPENQASM 3.0; include \"stdgates.inc\"; gate pair "; + source.append(wires) + .append(" { ") + .append(pulse) + .append(pulse) + .append(" } qubit[2] q; pair q[0]") + .append(arity == 1 ? ";" : ", q[1];"); + auto input = QCProgram::fromOpenQASMString(source); + ASSERT_TRUE(input); + auto reference = input->copy().intoQCO(); + ASSERT_TRUE(reference); + ASSERT_TRUE(reference->runPassPipeline("inline,symbol-dce")); + for (const bool withSingleQubitBasis : {false, true}) { + SCOPED_TRACE(withSingleQubitBasis); + std::vector capabilities{ + llvm::cantFail(Capability::create(gate, arity, parameters.size(), {}, + std::nullopt, std::nullopt, + parameters)), + llvm::cantFail(Capability::create("gphase", 0, 1)), + }; + if (withSingleQubitBasis) { + capabilities.push_back(llvm::cantFail(Capability::create("u", 1, 3))); + } + const auto target = llvm::cantFail(CompilerTarget::create( + 2, CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::fromOperations(capabilities))); + const TargetEnvironment environment( + target, llvm::cantFail(payloadSpecificationForProgramFormat( + QDMI_PROGRAM_FORMAT_QASM3))); + for (const bool synthesisOnly : {false, true}) { + SCOPED_TRACE(synthesisOnly); + auto program = input->copy().intoQCO(); + ASSERT_TRUE(program); + ASSERT_TRUE(synthesisOnly ? program->synthesizeForTarget(environment) + : program->compileForTarget(environment)); + EXPECT_TRUE(succeeded(verify(program->module()))); + EXPECT_TRUE(succeeded(qco::verifyLinearity(program->module()))); + expectFullUnitaryEqual(reference->module(), program->module(), 2); + } + auto output = + runDefaultPipeline(CompilerInput{input->copy()}, environment); + ASSERT_TRUE(output); + auto exported = QCProgram::fromOpenQASMString( + std::get(*output).source()); + ASSERT_TRUE(exported); + auto roundTrip = std::move(*exported).intoQCO(); + ASSERT_TRUE(roundTrip); + PassManager inlineExport(roundTrip->module().getContext()); + inlineExport.addPass(createInlinerPass({}, [](OpPassManager& nested) { + nested.addPass(qco::createQCOCanonicalizer(true)); + })); + inlineExport.addPass(createSymbolDCEPass()); + ASSERT_TRUE(succeeded(inlineExport.run(roundTrip->module()))); + roundTrip->module().walk( + [&](qco::UnitaryOpInterface gate) { + EXPECT_TRUE(target.supports(gate.getOperation())) + << gate->getName().getStringRef().str(); + return WalkResult::skip(); + }); + expectFullUnitaryEqual(reference->module(), roundTrip->module(), 2); + } + } +} + TEST_F(CompilerPipelineTest, TargetCompilationFusesOnlyWithUsableNativeBasis) { using NativeOperations = CompilerTarget::NativeOperations; using OperationCapability = CompilerTarget::OperationCapability; 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 74b722b1ce..0e461dd1cd 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -426,6 +426,44 @@ TEST_F(TargetSynthesisTest, FixedPulseSynthesisPreservesFullUnitary) { } } +TEST_F(TargetSynthesisTest, FixedPulseFusionPreservesNativeAngles) { + for (const auto angle : {.37, std::numbers::pi / 4, std::numbers::pi / 2}) { + SCOPED_TRACE(angle); + const auto target = valid(Target::create( + 1, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(OperationCapability::create("rx", 1, 1, {}, std::nullopt, + std::nullopt, {angle})), + valid(OperationCapability::create("rz", 1, 1)), + valid(OperationCapability::create("gphase", 0, 1)), + }))); + for (const bool cancel : {false, true}) { + SCOPED_TRACE(cancel); + const auto circuit = [&](QCOProgramBuilder& builder) { + auto qubit = builder.rx(angle, builder.staticQubit(0)); + if (cancel) { + qubit = builder.rz(std::numbers::pi, qubit); + } + qubit = builder.rx(angle, qubit); + if (cancel) { + qubit = builder.rz(-std::numbers::pi, qubit); + } + builder.sink(qubit); + 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()))); + expectEquivalent(expected, actual); + EXPECT_EQ(countOps(*actual), cancel ? 0 : 2); + EXPECT_EQ(countOps(*actual), 0); + } + } +} + TEST_F(TargetSynthesisTest, FixedHalfTurnUsesOnePulse) { for (const auto* const name : {"rx", "ry"}) { for (double half : {std::numbers::pi, -std::numbers::pi}) { From a343bff7507f59aed76367cdc3832dda90567876 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 29 Sep 2026 11:23:30 +0200 Subject: [PATCH 14/23] =?UTF-8?q?=F0=9F=8E=A8=20Match=20pass=20hook=20visi?= =?UTF-8?q?bility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Keep initialization and execution hooks protected, matching the MLIR Pass API. Assisted-by: GPT-6 via Codex --- mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp b/mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp index 6d2f03ef1b..4288876f5e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp @@ -33,6 +33,7 @@ class CanonicalizeStructurePass final public: MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(CanonicalizeStructurePass) +protected: LogicalResult initialize(MLIRContext* context) override { RewritePatternSet patterns(context); for (auto* dialect : context->getLoadedDialects()) { @@ -47,7 +48,6 @@ class CanonicalizeStructurePass final return success(); } -protected: void runOnOperation() override { if (failed(applyPatternsGreedily(getOperation(), patterns_, GreedyRewriteConfig{}.setMaxIterations( From 2fb7694926a76799cb08b854ec7adcba1cb331d3 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 29 Sep 2026 14:31:52 +0200 Subject: [PATCH 15/23] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20target=20?= =?UTF-8?q?synthesis=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Keep shared pulse formulas in the existing Euler utilities and structural canonicalization in target synthesis. Remove the two private files and the extra include path. Keep target cleanup choices local instead of changing the general cleanup API. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 20 +++-- .../QCO/Transforms/Decomposition/Euler.h | 50 +++++++++++++ mlir/include/mqt/Support/Passes.h | 4 +- mlir/lib/Compiler/Pipeline.cpp | 6 +- mlir/lib/Compiler/TargetCompilation.cpp | 41 +++++++---- .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 1 - .../QCO/Transforms/CanonicalizeStructure.cpp | 73 ------------------- .../QCO/Transforms/Decomposition/Euler.cpp | 16 ++-- .../Transforms/Decomposition/PulseSynthesis.h | 66 ----------------- .../NativeSynthesis/TargetSynthesis.cpp | 44 +++++++++++ .../MergeSingleQubitRotationGates.cpp | 4 +- mlir/lib/Support/Passes.cpp | 11 +-- 12 files changed, 149 insertions(+), 187 deletions(-) delete mode 100644 mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp delete 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 8f93282087..fa41e2d46d 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -27,17 +27,21 @@ 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. -Routing and synthesis borrow the cached basis, including its pulse sequence; -Python property access retains value semantics. +Numeric and symbolic synthesis share one fixed-pulse recipe in the existing +Euler utilities. Pulse-plan details stay internal; Python exposes target +capabilities and the selected basis kind. Routing and synthesis borrow the +cached basis, including its pulse sequence; Python property access retains value +semantics. Target cleanup applies structural and classical canonicalization, preserving gate forms for target-aware synthesis. The inliner's cleanup follows the same -rule. Non-universal targets keep their native sequences; operations outside the -native set still need a usable basis. Numeric fixed-pulse fusion uses the cached -recipe and only shortens already native runs. Symbolic fixed-pulse runs continue -to use individual lowering because symbolic fusion does not model pulse costs. +rule. This selection stays within target compilation; general cleanup keeps its +original API. The selector includes QCO lifetime and control-flow patterns, +which cannot be omitted without breaking linearity. Non-universal targets keep +their native sequences; operations outside the native set still need a usable +basis. Numeric fixed-pulse fusion uses the cached recipe and only shortens +already native runs. Symbolic fixed-pulse runs continue to use individual +lowering because symbolic fusion does not model pulse costs. ## Validation diff --git a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h index acafad346b..bfe4a4408a 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -11,13 +11,16 @@ #pragma once #include "mqt/Compiler/Target.h" +#include "mqt/Dialect/MQT/Utils/Parameters.h" #include "mqt/Dialect/QCO/Utils/Matrix.h" #include "mlir/IR/Builders.h" #include "mlir/IR/Location.h" #include "mlir/Support/LLVM.h" +#include #include +#include #include namespace mlir { @@ -132,4 +135,51 @@ void populateParameterizedSingleQubitRunCompositionPatterns( RewritePatternSet& patterns, SingleQubitBasis basis, const CompilerTarget* target = nullptr); +namespace detail { + +/// 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 detail + } // namespace mlir::qco::decomposition diff --git a/mlir/include/mqt/Support/Passes.h b/mlir/include/mqt/Support/Passes.h index 31ae2f3dfb..143b146d40 100644 --- a/mlir/include/mqt/Support/Passes.h +++ b/mlir/include/mqt/Support/Passes.h @@ -76,9 +76,7 @@ void populateQCExportPipeline(mlir::OpPassManager& pm); /// Populate a QCO-oriented cleanup pipeline on the given pass manager. /// /// Adds generic cleanup, qtensor shrink-to-fit, and dead-value removal. -/// Set @p preserveGates to leave gate rewriting to target-aware synthesis. -void populateQCOCleanupPipeline(mlir::OpPassManager& pm, - bool preserveGates = false); +void populateQCOCleanupPipeline(mlir::OpPassManager& pm); /// Populate a QIR-oriented cleanup pipeline on the given pass manager. /// diff --git a/mlir/lib/Compiler/Pipeline.cpp b/mlir/lib/Compiler/Pipeline.cpp index 7af6c896f3..ea7a198b74 100644 --- a/mlir/lib/Compiler/Pipeline.cpp +++ b/mlir/lib/Compiler/Pipeline.cpp @@ -131,9 +131,9 @@ std::optional QCProgram::intoQIR(QIRProfile profile) && { //===----------------------------------------------------------------------===// bool QCOProgram::cleanup() { - return succeeded(runQCOTransformPasses( - mod(), [](OpPassManager& pm) { populateQCOCleanupPipeline(pm); }, - "failed to run the QCO cleanup pipeline")); + return succeeded( + runQCOTransformPasses(mod(), populateQCOCleanupPipeline, + "failed to run the QCO cleanup pipeline")); } bool QCOProgram::normalizeGlobalPhases() { diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index 16ce561a5d..1393227a78 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -16,6 +16,7 @@ #include "mqt/Dialect/MQT/IR/QubitLayout.h" #include "mqt/Dialect/QCO/IR/QCOOps.h" #include "mqt/Dialect/QCO/QCOUtils.h" +#include "mqt/Dialect/MQT/Transforms/Passes.h" #include "mqt/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mqt/Dialect/QCO/Transforms/Passes.h" #include "mqt/Dialect/QTensor/IR/QTensorOps.h" @@ -187,6 +188,20 @@ class PrepareTargetCompilationPass } /* namespace */ +static void populateTargetCleanupPipeline(OpPassManager& pm, + bool preserveGates) { + if (!preserveGates) { + populateQCOCleanupPipeline(pm); + return; + } + pm.addPass(qco::createQCOCanonicalizer(true)); + pm.addPass(mqt::createNormalizeGlobalPhases()); + pm.addPass(createCSEPass()); + pm.addPass(qtensor::createShrinkQTensorToFitPass()); + pm.addPass(createSymbolDCEPass()); + pm.addPass(createRemoveDeadValuesPass()); +} + void populateTargetCompilationPipeline(OpPassManager& pm, const TargetEnvironment& environment, const MappingOptions& mapping) { @@ -195,16 +210,13 @@ void populateTargetCompilationPipeline(OpPassManager& pm, const auto& target = environment.target(); const bool preserveGates = target.nativeOperationsKind() == CompilerTarget::NativeOperations::Kind::Explicit; - pm.addPass(preserveGates - ? createInlinerPass({}, - [](OpPassManager& nested) { - nested.addPass( - qco::createQCOCanonicalizer(true)); - }) - : createInlinerPass()); + pm.addPass(createInlinerPass({}, [preserveGates](OpPassManager& nested) { + nested.addPass(preserveGates ? qco::createQCOCanonicalizer(true) + : createCanonicalizerPass()); + })); pm.addPass(createSymbolDCEPass()); pm.addPass(createSCCPPass()); - populateQCOCleanupPipeline(pm, preserveGates); + populateTargetCleanupPipeline(pm, preserveGates); pm.addPass(qco::createUnrollLoopsForPayload()); pm.addPass(createSCCPPass()); /// Unrolling exposes static tensor slots and unreachable callees. @@ -251,15 +263,12 @@ void populateTargetSynthesisPipeline(OpPassManager& pm, const auto& target = environment.target(); const bool preserveGates = target.nativeOperationsKind() == CompilerTarget::NativeOperations::Kind::Explicit; - pm.addPass(preserveGates - ? createInlinerPass({}, - [](OpPassManager& nested) { - nested.addPass( - qco::createQCOCanonicalizer(true)); - }) - : createInlinerPass()); + pm.addPass(createInlinerPass({}, [preserveGates](OpPassManager& nested) { + nested.addPass(preserveGates ? qco::createQCOCanonicalizer(true) + : createCanonicalizerPass()); + })); pm.addPass(createSymbolDCEPass()); - populateQCOCleanupPipeline(pm, preserveGates); + populateTargetCleanupPipeline(pm, preserveGates); pm.addPass(qco::createLegalizeControlFlow()); pm.addPass(qco::createDecomposeMultiControlled(target)); pm.addPass(qco::createFuseTwoQubitGates(target)); diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 32e53758b9..7b96dded04 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -31,7 +31,6 @@ 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/CanonicalizeStructure.cpp b/mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp deleted file mode 100644 index 4288876f5e..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/CanonicalizeStructure.cpp +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 - */ - -#include "mqt/Dialect/QCO/IR/QCOInterfaces.h" -#include "mqt/Dialect/QCO/Transforms/Passes.h" - -#include "mlir/IR/Dialect.h" -#include "mlir/IR/MLIRContext.h" -#include "mlir/IR/OperationSupport.h" -#include "mlir/IR/PatternMatch.h" -#include "mlir/Pass/Pass.h" -#include "mlir/Rewrite/FrozenRewritePatternSet.h" -#include "mlir/Support/LogicalResult.h" -#include "mlir/Transforms/GreedyPatternRewriteDriver.h" -#include "mlir/Transforms/Passes.h" - -#include -#include - -namespace mlir::qco { -namespace { - -/// Gate-changing canonicalization can leave a target's native gate set. -class CanonicalizeStructurePass final - : public PassWrapper> { -public: - MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(CanonicalizeStructurePass) - -protected: - LogicalResult initialize(MLIRContext* context) override { - RewritePatternSet patterns(context); - for (auto* dialect : context->getLoadedDialects()) { - dialect->getCanonicalizationPatterns(patterns); - } - for (auto operation : context->getRegisteredOperations()) { - if (!operation.hasInterface()) { - operation.getCanonicalizationPatterns(patterns, context); - } - } - patterns_ = FrozenRewritePatternSet(std::move(patterns)); - return success(); - } - - void runOnOperation() override { - if (failed(applyPatternsGreedily(getOperation(), patterns_, - GreedyRewriteConfig{}.setMaxIterations( - GreedyRewriteConfig::kNoLimit)))) { - signalPassFailure(); - } - } - -private: - FrozenRewritePatternSet patterns_; -}; - -} // namespace - -std::unique_ptr createQCOCanonicalizer(bool preserveGates) { - if (!preserveGates) { - return createCanonicalizerPass( - GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit)); - } - return std::make_unique(); -} - -} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index d593c4a8a2..9e5ddf2798 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -14,8 +14,6 @@ #include "mqt/Dialect/QCO/IR/QCOOps.h" #include "mqt/Dialect/QCO/Utils/Matrix.h" -#include "Decomposition/PulseSynthesis.h" - #include "mlir/IR/Builders.h" #include "mlir/IR/Location.h" #include "mlir/IR/Operation.h" @@ -67,12 +65,12 @@ bool isSingleQubitBasisGate( basis == SingleQubitBasis::XZX || basis == SingleQubitBasis::ZSXX; }) - .Case([&](auto rotation) { - return isa(rotation) ? (basis == SingleQubitBasis::ZXZ || - basis == SingleQubitBasis::XZX || - basis == SingleQubitBasis::XYX) - : (basis == SingleQubitBasis::ZYZ || - basis == SingleQubitBasis::XYX); + .Case([&](RYOp) { + return basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::XYX; + }) + .Case([&](RXOp) { + return basis == SingleQubitBasis::ZXZ || + basis == SingleQubitBasis::XZX || basis == SingleQubitBasis::XYX; }) .Case([&](UOp) { return basis == SingleQubitBasis::U; }) .Case([&](auto) { return basis == SingleQubitBasis::ZSXX; }) @@ -376,7 +374,7 @@ struct Unitary1QEulerPlan { ? SynthesisStep::Kind::RX : SynthesisStep::Kind::RY; phase = angles.phase + - emitFixedRotationSequence( + detail::emitFixedRotationSequence( *fixedRotation, angles.theta, angles.phi, angles.lambda, angles.theta, [](double value) { return value; }, [&](double angle) { diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/PulseSynthesis.h b/mlir/lib/Dialect/QCO/Transforms/Decomposition/PulseSynthesis.h deleted file mode 100644 index 5dd516d0d6..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/PulseSynthesis.h +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 754037c6cd..1723b82320 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -30,18 +30,21 @@ #include "mlir/IR/Builders.h" #include "mlir/IR/BuiltinOps.h" #include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Dialect.h" #include "mlir/IR/DialectRegistry.h" #include "mlir/IR/IRMapping.h" #include "mlir/IR/Iterators.h" #include "mlir/IR/MLIRContext.h" #include "mlir/IR/Matchers.h" #include "mlir/IR/Operation.h" +#include "mlir/IR/OperationSupport.h" #include "mlir/IR/PatternMatch.h" #include "mlir/IR/Value.h" #include "mlir/IR/Visitors.h" #include "mlir/Interfaces/FunctionInterfaces.h" #include "mlir/Pass/Pass.h" #include "mlir/Pass/PassManager.h" +#include "mlir/Rewrite/FrozenRewritePatternSet.h" #include "mlir/Support/LLVM.h" #include "mlir/Support/LogicalResult.h" #include "mlir/Support/TypeID.h" @@ -1505,6 +1508,39 @@ struct VerifyTargetConformancePass final } }; +/// Gate-changing canonicalization can leave a target's native gate set. +class CanonicalizeStructurePass final + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(CanonicalizeStructurePass) + +protected: + LogicalResult initialize(MLIRContext* context) override { + RewritePatternSet patterns(context); + for (auto* dialect : context->getLoadedDialects()) { + dialect->getCanonicalizationPatterns(patterns); + } + for (auto operation : context->getRegisteredOperations()) { + if (!operation.hasInterface()) { + operation.getCanonicalizationPatterns(patterns, context); + } + } + patterns_ = FrozenRewritePatternSet(std::move(patterns)); + return success(); + } + + void runOnOperation() override { + if (failed(applyPatternsGreedily(getOperation(), patterns_, + GreedyRewriteConfig{}.setMaxIterations( + GreedyRewriteConfig::kNoLimit)))) { + signalPassFailure(); + } + } + +private: + FrozenRewritePatternSet patterns_; +}; + } // namespace std::unique_ptr createFuseTwoQubitGates() { @@ -1515,6 +1551,14 @@ std::unique_ptr createFuseTwoQubitGates(const CompilerTarget& target) { return std::make_unique(target); } +std::unique_ptr createQCOCanonicalizer(bool preserveGates) { + if (!preserveGates) { + return createCanonicalizerPass( + GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit)); + } + return std::make_unique(); +} + void populateTargetNativeSynthesisPipeline(OpPassManager& pm, bool preserveGates) { /// Placement consumes allocations; native synthesis normalizes phases. diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index bd0e6f6449..78f762363a 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -17,8 +17,6 @@ #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" @@ -817,7 +815,7 @@ static Value emitRuntimeEulerAngles( emit(fixedRotation->freeGate, sumAngles(phi, lambda)); break; } - const double correction = decomposition::emitFixedRotationSequence( + const double correction = decomposition::detail::emitFixedRotationSequence( *fixedRotation, theta, phi, lambda, mqt::valueToConstantDouble(theta.v), constant, [&](Val angle) { emit(fixedRotation->freeGate, angle); }, diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 17c2823641..476de8948c 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -24,6 +24,7 @@ #include "mlir/Pass/PassManager.h" #include "mlir/Pass/PassRegistry.h" #include "mlir/Support/LLVM.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" #include "mlir/Transforms/Passes.h" #include "llvm/ADT/ScopeExit.h" @@ -170,8 +171,9 @@ void populateQCCleanupPipeline(OpPassManager& pm) { pm.addPass(createRemoveDeadValuesPass()); } -void populateQCOCleanupPipeline(OpPassManager& pm, bool preserveGates) { - pm.addPass(qco::createQCOCanonicalizer(preserveGates)); +void populateQCOCleanupPipeline(OpPassManager& pm) { + pm.addPass(createCanonicalizerPass( + GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit))); pm.addPass(mlir::mqt::createNormalizeGlobalPhases()); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); @@ -197,9 +199,8 @@ void populateJeffCleanupPipeline(OpPassManager& pm) { } [[nodiscard]] LogicalResult runQCOCleanupPipeline(ModuleOp mod) { - return runWithPassManager( - mod, [](OpPassManager& pm) { populateQCOCleanupPipeline(pm); }, - "Failed to run the QCO cleanup pipeline."); + return runWithPassManager(mod, populateQCOCleanupPipeline, + "Failed to run the QCO cleanup pipeline."); } [[nodiscard]] LogicalResult runQIRCleanupPipeline(ModuleOp mod, From ea046ec3f4bd5090f398ca200e5673d03dad43d7 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 29 Sep 2026 14:45:34 +0200 Subject: [PATCH 16/23] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Trim=20target=20setu?= =?UTF-8?q?p=20and=20cleanup=20branches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Build fixed pulses directly in their result storage and move the selected recipe into the cached basis. Stop before entangler selection when no single-qubit basis exists. Let the canonicalizer factory choose cleanup without duplicating that branch in the target pipeline. Assisted-by: GPT-6 via Codex --- mlir/lib/Compiler/Target.cpp | 16 ++++++++-------- mlir/lib/Compiler/TargetCompilation.cpp | 6 +----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 0b13d5ff7b..b46a0bad20 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -180,15 +180,15 @@ makeFixedRotationBasis(GateKind gate, GateKind freeGate, double angle) { if (magnitude <= mqt::PARAMETER_COMPARISON_TOLERANCE) { return std::nullopt; } + auto& zAngles = result.quarterTurnAngles; 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.); + zAngles.resize(static_cast(directCount) + 1); const double axis = (!isX ? halfPi : 0.) + (angle < 0. ? pi : 0.); zAngles.front() = axis; zAngles.back() = -axis; - result.quarterTurnAngles = std::move(zAngles); return result; } const double sine = std::sin(angle); @@ -211,13 +211,12 @@ makeFixedRotationBasis(GateKind gate, GateKind freeGate, double angle) { 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); + zAngles.resize(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; } - result.quarterTurnAngles = std::move(zAngles); return result; } @@ -938,6 +937,10 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { } } + if (!singleQubit) { + return std::nullopt; + } + const auto supportsOnEveryCoupling = [&](GateKind gate) { if (sites.size() < 2) { return false; @@ -983,15 +986,12 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { /// NOLINTNEXTLINE(readability-qualified-auto): portable iterator type. const auto entangler = std::ranges::find_if(entanglerPreference, supportsOnEveryCoupling); - if (!singleQubit) { - return std::nullopt; - } return SynthesisBasis{ .singleQubit = *singleQubit, .entangler = entangler == entanglerPreference.end() ? std::nullopt : std::optional{*entangler}, - .fixedRotation = fixedRotation, + .fixedRotation = std::move(fixedRotation), }; } diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index 1393227a78..babe889fae 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -190,11 +190,7 @@ class PrepareTargetCompilationPass static void populateTargetCleanupPipeline(OpPassManager& pm, bool preserveGates) { - if (!preserveGates) { - populateQCOCleanupPipeline(pm); - return; - } - pm.addPass(qco::createQCOCanonicalizer(true)); + pm.addPass(qco::createQCOCanonicalizer(preserveGates)); pm.addPass(mqt::createNormalizeGlobalPhases()); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); From 936976b628a808ecfea93c4e7ffa62e49e95a58d Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 29 Sep 2026 15:05:34 +0200 Subject: [PATCH 17/23] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Centralize=20target?= =?UTF-8?q?=20cleanup=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Reuse the shared QCO cleanup sequence with the target canonicalizer. Select gate preservation in the canonicalizer and inliner factories, removing duplicated cleanup and boolean policy wiring from the target pipelines. Preserve the standard inliner for unrestricted targets. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 15 ++++---- .../mqt/Dialect/QCO/Transforms/Passes.h | 13 ++++--- mlir/include/mqt/Support/Passes.h | 9 +++-- mlir/lib/Compiler/Pipeline.cpp | 6 ++-- mlir/lib/Compiler/TargetCompilation.cpp | 35 ++++--------------- .../NativeSynthesis/TargetSynthesis.cpp | 19 +++++++--- mlir/lib/Support/Passes.cpp | 16 ++++++--- .../Compiler/test_compiler_pipeline.cpp | 4 +-- .../QCO/Transforms/Mapping/test_mapping.cpp | 12 +++---- 9 files changed, 67 insertions(+), 62 deletions(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index fa41e2d46d..07bcd5d279 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -35,13 +35,14 @@ semantics. Target cleanup applies structural and classical canonicalization, preserving gate forms for target-aware synthesis. The inliner's cleanup follows the same -rule. This selection stays within target compilation; general cleanup keeps its -original API. The selector includes QCO lifetime and control-flow patterns, -which cannot be omitted without breaking linearity. Non-universal targets keep -their native sequences; operations outside the native set still need a usable -basis. Numeric fixed-pulse fusion uses the cached recipe and only shortens -already native runs. Symbolic fixed-pulse runs continue to use individual -lowering because symbolic fusion does not model pulse costs. +rule. Target pass factories select canonicalization; target pipelines reuse the +shared QCO cleanup sequence with that canonicalizer. The selector includes QCO +lifetime and control-flow patterns, which cannot be omitted without breaking +linearity. Non-universal targets keep their native sequences; operations outside +the native set still need a usable basis. Numeric fixed-pulse fusion uses the +cached recipe and only shortens already native runs. Symbolic fixed-pulse runs +continue to use individual lowering because symbolic fusion does not model pulse +costs. ## Validation diff --git a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h index ee60b74033..49f790701e 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h @@ -38,9 +38,14 @@ namespace mlir::qco { #define GEN_PASS_REGISTRATION #include "mqt/Dialect/QCO/Transforms/Passes.h.inc" // IWYU pragma: export -/// Canonicalize QCO; optionally preserve gate forms for target synthesis. -/// Identity folding and dead-code elimination remain enabled. -[[nodiscard]] std::unique_ptr createQCOCanonicalizer(bool preserveGates); +/// Canonicalize structure while preserving explicitly supported gate forms. +/// Unrestricted targets use the standard MLIR canonicalizer. +[[nodiscard]] std::unique_ptr +createTargetCanonicalizer(const CompilerTarget& target); + +/// Inline functions using the target's canonicalization policy. +[[nodiscard]] std::unique_ptr +createTargetInliner(const CompilerTarget& target); /// Create target-independent two-qubit gate fusion. /// @@ -63,6 +68,6 @@ createDecomposeMultiControlled(const CompilerTarget& target, /// Normalize placed QCO, synthesize native gates, and verify target support. /// Requires a valid target environment and placed qubits. void populateTargetNativeSynthesisPipeline(OpPassManager& pm, - bool preserveGates = true); + const CompilerTarget& target); } // namespace mlir::qco diff --git a/mlir/include/mqt/Support/Passes.h b/mlir/include/mqt/Support/Passes.h index 143b146d40..7b01ea2a9f 100644 --- a/mlir/include/mqt/Support/Passes.h +++ b/mlir/include/mqt/Support/Passes.h @@ -12,9 +12,11 @@ #include "mqt/Compiler/CompilationOptions.h" +#include "mlir/Pass/Pass.h" #include "mlir/Support/LLVM.h" #include +#include namespace mlir { class ModuleOp; @@ -75,8 +77,11 @@ void populateQCExportPipeline(mlir::OpPassManager& pm); /// Populate a QCO-oriented cleanup pipeline on the given pass manager. /// -/// Adds generic cleanup, qtensor shrink-to-fit, and dead-value removal. -void populateQCOCleanupPipeline(mlir::OpPassManager& pm); +/// Adds canonicalization, qtensor shrink-to-fit, and dead-value removal. +/// A null canonicalizer selects the standard MLIR canonicalizer. +void populateQCOCleanupPipeline( + mlir::OpPassManager& pm, + std::unique_ptr canonicalizer = nullptr); /// Populate a QIR-oriented cleanup pipeline on the given pass manager. /// diff --git a/mlir/lib/Compiler/Pipeline.cpp b/mlir/lib/Compiler/Pipeline.cpp index ea7a198b74..7af6c896f3 100644 --- a/mlir/lib/Compiler/Pipeline.cpp +++ b/mlir/lib/Compiler/Pipeline.cpp @@ -131,9 +131,9 @@ std::optional QCProgram::intoQIR(QIRProfile profile) && { //===----------------------------------------------------------------------===// bool QCOProgram::cleanup() { - return succeeded( - runQCOTransformPasses(mod(), populateQCOCleanupPipeline, - "failed to run the QCO cleanup pipeline")); + return succeeded(runQCOTransformPasses( + mod(), [](OpPassManager& pm) { populateQCOCleanupPipeline(pm); }, + "failed to run the QCO cleanup pipeline")); } bool QCOProgram::normalizeGlobalPhases() { diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index babe889fae..0d2a888154 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -16,7 +16,6 @@ #include "mqt/Dialect/MQT/IR/QubitLayout.h" #include "mqt/Dialect/QCO/IR/QCOOps.h" #include "mqt/Dialect/QCO/QCOUtils.h" -#include "mqt/Dialect/MQT/Transforms/Passes.h" #include "mqt/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mqt/Dialect/QCO/Transforms/Passes.h" #include "mqt/Dialect/QTensor/IR/QTensorOps.h" @@ -188,35 +187,20 @@ class PrepareTargetCompilationPass } /* namespace */ -static void populateTargetCleanupPipeline(OpPassManager& pm, - bool preserveGates) { - pm.addPass(qco::createQCOCanonicalizer(preserveGates)); - pm.addPass(mqt::createNormalizeGlobalPhases()); - pm.addPass(createCSEPass()); - pm.addPass(qtensor::createShrinkQTensorToFitPass()); - pm.addPass(createSymbolDCEPass()); - pm.addPass(createRemoveDeadValuesPass()); -} - void populateTargetCompilationPipeline(OpPassManager& pm, const TargetEnvironment& environment, const MappingOptions& mapping) { pm.addPass(std::make_unique(environment, false, mapping)); const auto& target = environment.target(); - const bool preserveGates = target.nativeOperationsKind() == - CompilerTarget::NativeOperations::Kind::Explicit; - pm.addPass(createInlinerPass({}, [preserveGates](OpPassManager& nested) { - nested.addPass(preserveGates ? qco::createQCOCanonicalizer(true) - : createCanonicalizerPass()); - })); + pm.addPass(qco::createTargetInliner(target)); pm.addPass(createSymbolDCEPass()); pm.addPass(createSCCPPass()); - populateTargetCleanupPipeline(pm, preserveGates); + populateQCOCleanupPipeline(pm, qco::createTargetCanonicalizer(target)); pm.addPass(qco::createUnrollLoopsForPayload()); pm.addPass(createSCCPPass()); /// Unrolling exposes static tensor slots and unreachable callees. - pm.addPass(qco::createQCOCanonicalizer(preserveGates)); + pm.addPass(qco::createTargetCanonicalizer(target)); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); pm.addPass(createSymbolDCEPass()); @@ -248,7 +232,7 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(qco::createPlacementPass(target)); break; } - qco::populateTargetNativeSynthesisPipeline(pm, preserveGates); + qco::populateTargetNativeSynthesisPipeline(pm, target); } void populateTargetSynthesisPipeline(OpPassManager& pm, @@ -257,19 +241,14 @@ void populateTargetSynthesisPipeline(OpPassManager& pm, pm.addPass(std::make_unique(environment, true, mapping)); const auto& target = environment.target(); - const bool preserveGates = target.nativeOperationsKind() == - CompilerTarget::NativeOperations::Kind::Explicit; - pm.addPass(createInlinerPass({}, [preserveGates](OpPassManager& nested) { - nested.addPass(preserveGates ? qco::createQCOCanonicalizer(true) - : createCanonicalizerPass()); - })); + pm.addPass(qco::createTargetInliner(target)); pm.addPass(createSymbolDCEPass()); - populateTargetCleanupPipeline(pm, preserveGates); + populateQCOCleanupPipeline(pm, qco::createTargetCanonicalizer(target)); pm.addPass(qco::createLegalizeControlFlow()); pm.addPass(qco::createDecomposeMultiControlled(target)); pm.addPass(qco::createFuseTwoQubitGates(target)); pm.addPass(qco::createPlacementPass(target)); - qco::populateTargetNativeSynthesisPipeline(pm, preserveGates); + qco::populateTargetNativeSynthesisPipeline(pm, target); } } // namespace mlir diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 1723b82320..fafcf8d842 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -1551,18 +1551,29 @@ std::unique_ptr createFuseTwoQubitGates(const CompilerTarget& target) { return std::make_unique(target); } -std::unique_ptr createQCOCanonicalizer(bool preserveGates) { - if (!preserveGates) { +std::unique_ptr createTargetCanonicalizer(const CompilerTarget& target) { + if (target.nativeOperationsKind() == + CompilerTarget::NativeOperations::Kind::Unrestricted) { return createCanonicalizerPass( GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit)); } return std::make_unique(); } +std::unique_ptr createTargetInliner(const CompilerTarget& target) { + if (target.nativeOperationsKind() == + CompilerTarget::NativeOperations::Kind::Unrestricted) { + return createInlinerPass(); + } + return createInlinerPass({}, [](OpPassManager& nested) { + nested.addPass(std::make_unique()); + }); +} + void populateTargetNativeSynthesisPipeline(OpPassManager& pm, - bool preserveGates) { + const CompilerTarget& target) { /// Placement consumes allocations; native synthesis normalizes phases. - pm.addPass(createQCOCanonicalizer(preserveGates)); + pm.addPass(createTargetCanonicalizer(target)); /// Reuse unchanged classical reads before native synthesis splits their uses. pm.addPass(createCSEPass()); pm.addPass(createRemoveDeadValuesPass()); diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 476de8948c..05c4eb141a 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -32,6 +32,8 @@ #include #include +#include +#include using namespace mlir; @@ -171,9 +173,12 @@ void populateQCCleanupPipeline(OpPassManager& pm) { pm.addPass(createRemoveDeadValuesPass()); } -void populateQCOCleanupPipeline(OpPassManager& pm) { - pm.addPass(createCanonicalizerPass( - GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit))); +void populateQCOCleanupPipeline(OpPassManager& pm, + std::unique_ptr canonicalizer) { + pm.addPass(canonicalizer ? std::move(canonicalizer) + : createCanonicalizerPass( + GreedyRewriteConfig{}.setMaxIterations( + GreedyRewriteConfig::kNoLimit))); pm.addPass(mlir::mqt::createNormalizeGlobalPhases()); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); @@ -199,8 +204,9 @@ void populateJeffCleanupPipeline(OpPassManager& pm) { } [[nodiscard]] LogicalResult runQCOCleanupPipeline(ModuleOp mod) { - return runWithPassManager(mod, populateQCOCleanupPipeline, - "Failed to run the QCO cleanup pipeline."); + return runWithPassManager( + mod, [](OpPassManager& pm) { populateQCOCleanupPipeline(pm); }, + "Failed to run the QCO cleanup pipeline."); } [[nodiscard]] LogicalResult runQIRCleanupPipeline(ModuleOp mod, diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 862ab44927..71c277724a 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -4079,9 +4079,7 @@ TEST_F(CompilerPipelineTest, TargetPipelinesPreserveNativeGateSequences) { auto roundTrip = std::move(*exported).intoQCO(); ASSERT_TRUE(roundTrip); PassManager inlineExport(roundTrip->module().getContext()); - inlineExport.addPass(createInlinerPass({}, [](OpPassManager& nested) { - nested.addPass(qco::createQCOCanonicalizer(true)); - })); + inlineExport.addPass(qco::createTargetInliner(target)); inlineExport.addPass(createSymbolDCEPass()); ASSERT_TRUE(succeeded(inlineExport.run(roundTrip->module()))); roundTrip->module().walk( diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 1abe05dcb2..40088da03d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -3402,7 +3402,7 @@ TEST_F(MappingPassFixture, PreferNativeGateCountThenDepth) { MappingPassOptions{ .ntrials = 4, .seed = 42, .searchMemoryLimit = 0}))); PassManager native(context.get()); - populateTargetNativeSynthesisPipeline(native); + populateTargetNativeSynthesisPipeline(native, target); ASSERT_TRUE(succeeded(native.run(*moduleOp))); ASSERT_TRUE(succeeded(verify(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); @@ -3469,7 +3469,7 @@ TEST_F(MappingPassFixture, PreserveBasisStatesAfterNativeScoredCompilation) { EXPECT_TRUE(succeeded(verifyLinearity(*moduleOp))); EXPECT_TRUE(isExecutable(getEntryPoint(*moduleOp), target)); PassManager synthesis(context.get()); - populateTargetNativeSynthesisPipeline(synthesis); + populateTargetNativeSynthesisPipeline(synthesis, target); ASSERT_TRUE(succeeded(synthesis.run(*moduleOp))); ASSERT_TRUE(succeeded(verify(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); @@ -3562,7 +3562,7 @@ TEST_F(MappingPassFixture, PreserveRegionsWithIdleWiresAcrossCostAvailability) { ASSERT_TRUE(succeeded(verify(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); PassManager native(context.get()); - populateTargetNativeSynthesisPipeline(native); + populateTargetNativeSynthesisPipeline(native, target); ASSERT_TRUE(succeeded(native.run(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); const auto actual = qco::sample(getEntryPoint(*moduleOp), 1, 42); @@ -3829,7 +3829,7 @@ TEST_F(MappingPassFixture, NativeGuidancePreservesAlternatingPairs) { .ntrials = 2, .searchMemoryLimit = budget}))); PassManager native(context.get()); - populateTargetNativeSynthesisPipeline(native); + populateTargetNativeSynthesisPipeline(native, target); ASSERT_TRUE(succeeded(native.run(*moduleOp))); ASSERT_TRUE(succeeded(verify(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); @@ -3889,7 +3889,7 @@ TEST_F(MappingPassFixture, NativeScoringChecksTerminalMeasurementSites) { ASSERT_TRUE(succeeded(runPass( *moduleOp, target, MappingPassOptions{.ntrials = 4, .seed = seed}))); PassManager native(context.get()); - populateTargetNativeSynthesisPipeline(native); + populateTargetNativeSynthesisPipeline(native, target); ASSERT_TRUE(succeeded(native.run(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); const auto actual = qco::sample(getEntryPoint(*moduleOp), 1, 42); @@ -3936,7 +3936,7 @@ module { ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); EXPECT_TRUE(isExecutable(getEntryPoint(*moduleOp), target)); PassManager native(context.get()); - populateTargetNativeSynthesisPipeline(native); + populateTargetNativeSynthesisPipeline(native, target); EXPECT_TRUE(failed(native.run(*moduleOp))); EXPECT_GT(count, 0); } From efa27fb8054e157d2f95a4db4e3e29026d2f085a Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 30 Sep 2026 18:33:24 +0200 Subject: [PATCH 18/23] =?UTF-8?q?=F0=9F=90=9B=20Align=20fixed-target=20Pyt?= =?UTF-8?q?hon=20API=20conventions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the uppercase enum name and display an empty tuple for the supports_operation parameter default. Regenerate the Python stubs and record reproducible validation commands with their local scope. Assisted-by: GPT-5 via Codex --- .agent/plans/fixed-parameter-targets.md | 29 +++++++++++++++++++------ bindings/mlir/register_mlir.cpp | 4 ++-- test/python/test_mlir.py | 2 +- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 07bcd5d279..6d109c8f8f 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -46,13 +46,28 @@ costs. ## Validation -The compiler suite passed 244 tests, native synthesis passed 79, and mapping -passed 125. Native synthesis includes 1,944 full-matrix cases across all six -axis pairs and a regression comparing routing costs with fixed-pulse emission. -Further regressions cover native-only compilation and OpenQASM export, and -fixed-pulse fusion with exact phase. Python fixed-parameter tests passed 362 -cases, including numeric and symbolic input gates. Generated stubs are -unchanged. +Run these commands from the repository root after the build and environment +setup in [AGENTS.md](../../AGENTS.md#build-and-validation). The recorded results +below are from local validation, not hosted CI. + +- `build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler`: 244 + tests passed, including native-only compilation and OpenQASM export. +- `build/release/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/mqt-core-mlir-unittest-target-synthesis`: + 79 tests passed, including 1,944 full-matrix cases across all six axis pairs, + routing-cost agreement, and phase-preserving fixed-pulse fusion. Append + `--gtest_filter='TargetSynthesisTest.*FixedPulse*'` to run only those focused + checks. +- `build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/mqt-core-mlir-unittest-mapping`: + 125 tests passed. +- `uv run --no-sync pytest test/python/test_mlir.py -k 'fixed_parameter or fixed_pulse'`: + 362 cases passed, covering the Python API and numeric and symbolic input + gates. +- `uvx nox -s stubs`: passed; regenerated Python signatures include + `FIXED_ROTATION` and the empty-tuple `parameters` default. + +The native-suite results are from the preceding implementation validation. They +were not rerun for the binding-only changes; the Python cases and stub +generation were. Hosted CI is separate and was not rerun locally. ## Follow-up diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 628a5d45b2..910de9a54f 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -966,7 +966,7 @@ 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("FixedRotation", + .value("FIXED_ROTATION", mlir::CompilerTarget::SingleQubitBasis::FixedRotation); auto synthesisBasis = nb::class_( @@ -1244,7 +1244,7 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); }, "name"_a, "arity"_a, "num_parameters"_a = nb::none(), "sites"_a = nb::none(), nb::kw_only(), - "parameters"_a = std::vector>{}, + "parameters"_a.sig("()") = std::vector>{}, R"pb(Check whether the target supports an operation. Args: diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 1a2334b5cd..7548f74374 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -668,7 +668,7 @@ 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 + assert target.synthesis_basis.single_qubit == CompilerTarget.SingleQubitBasis.FIXED_ROTATION 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 74593d9a0f1cb203d5a3c1b3bf812f7d27c4e19a Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 1 Oct 2026 17:29:36 +0000 Subject: [PATCH 19/23] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Limit=20fixed-pulse?= =?UTF-8?q?=20synthesis=20to=20native=20RX=20gates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Keep fixed-parameter capability matching general while limiting synthesis to unrestricted RZ and native RX quarter turns with optional half turns. Remove arbitrary-angle recipes, coordinate conversion, and pulse storage. Merge the unrestricted RZ axis before and after synthesis, preserving phase, symbolic cancellation, and parameter dominance. Keep fixed RX pulses intact and document the supported hardware scope. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 245 +++++++++++++----- docs/mlir/target_compilation.md | 24 +- mlir/include/mqt/Compiler/Target.h | 16 +- .../QCO/Transforms/Decomposition/Euler.h | 35 ++- mlir/lib/Compiler/Target.cpp | 124 ++------- .../QCO/Transforms/Decomposition/Euler.cpp | 84 ++---- .../NativeSynthesis/TargetSynthesis.cpp | 18 +- .../MergeSingleQubitRotationGates.cpp | 121 +++++---- .../Compiler/test_compiler_target.cpp | 27 +- .../NativeSynthesis/test_target_synthesis.cpp | 240 ++++++++--------- test/python/test_mlir.py | 64 +++-- 11 files changed, 504 insertions(+), 494 deletions(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 6d109c8f8f..1fe91ca8d0 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -1,76 +1,183 @@ -# Fixed-parameter compiler targets +# Fixed-parameter targets for Bench and Braket -Status: complete. +Status: in progress. The #2575 simplification is implemented and tested. Native +ion gates and provider integration remain proposed follow-ups. ## 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. 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. -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 - -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. - -Numeric and symbolic synthesis share one fixed-pulse recipe in the existing -Euler utilities. Pulse-plan details stay internal; Python exposes target -capabilities and the selected basis kind. Routing and synthesis borrow the -cached basis, including its pulse sequence; Python property access retains value -semantics. - -Target cleanup applies structural and classical canonicalization, preserving -gate forms for target-aware synthesis. The inliner's cleanup follows the same -rule. Target pass factories select canonicalization; target pipelines reuse the -shared QCO cleanup sequence with that canonicalizer. The selector includes QCO -lifetime and control-flow patterns, which cannot be omitted without breaking -linearity. Non-universal targets keep their native sequences; operations outside -the native set still need a usable basis. Numeric fixed-pulse fusion uses the -cached recipe and only shortens already native runs. Symbolic fixed-pulse runs -continue to use individual lowering because symbolic fusion does not model pulse -costs. +Compile MQT Bench circuits for its device gate sets, prioritizing native gates +exposed by Amazon Braket for verbatim execution. Separate three contracts: +representing a native operation, synthesizing into a supported native basis, and +serializing a program the provider accepts. Completion requires checking all +three; a compiler target or generic OpenQASM round trip alone is insufficient. + +Keep [Core #2575](https://github.com/munich-quantum-toolkit/core/pull/2575) +focused on fixed-parameter capabilities and Rigetti synthesis. Coordinate native +ion gates through +[Core #2578](https://github.com/munich-quantum-toolkit/core/pull/2578), then +complete Bench import and Braket export in their owning adapters. This plan +records the dependency order without combining those changes into one PR. + +The initial coverage matrix follows the +[Braket native-gate catalogue](https://docs.aws.amazon.com/braket/latest/developerguide/braket-submit-tasks.html) +and +[Bench target definitions](https://github.com/munich-quantum-toolkit/bench/tree/d23df0d5d2791b44633858783e62a7468480f362/src/mqt/bench/targets). + +| Target family | Required native gates | Core representation and work | +| ------------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Rigetti Ankaa / Bench Ankaa-84 | `rx`, `rz`, `iswap`; RX restricted to ±π/2 and ±π | Constrained `RX`, unrestricted `RZ`, existing `iSWAP`; Bench exposes +π and ±π/2 through named gates. | +| IonQ Forte | `gpi`, `gpi2`, `zz` | Native `GPI`/`GPI2`; reuse `RZZ` for Braket `zz`. | +| Bench IonQ Aria | `gpi`, `gpi2`, `ms` | Retain native `MS` support even though the current Braket catalogue lists Forte. | +| IQM Garnet / Emerald | `prx`, `cz` | Reuse Core `R` and `CZ`; check synthesis and external spelling. | +| AQT IBEX-Q1 | `prx`, `xx`, `rz` | Reuse Core `R`, `RXX`, and `RZ`; check synthesis and external spelling. | + +Device fixtures must record their identity and capability snapshot. Derive a +verbatim target from `paradigm.nativeGateSet`, topology, and documented +parameter restrictions, not the broader service `supportedOperations` list. The +current Braket catalogue also lists Rigetti Cepheus, but only specifies Ankaa's +native set; obtain its device metadata before claiming complete current-device +coverage. Preserve existing support for other Bench targets, including IBM and +Quantinuum. QuEra analog programs, pulse control, and general approximate +Clifford+T synthesis are outside this work. + +## Decisions and follow-up design + +### Keep capability matching general; keep synthesis small + +Retain optional finite fixed values per parameter. An omitted value means an +unrestricted parameter; separate capabilities express alternatives and +placements. Matching, serialization, compatibility, and final verification must +agree. Unbound symbolic values cannot satisfy a fixed parameter. Use the +existing absolute comparison tolerance, without periodic wrapping that loses +phase. + +In #2575, `mlir/lib/Compiler/Target.cpp` selects unrestricted RZ with fixed RX +quarter turns and optional half turns. The shared Euler recipe preserves phase +for both pulse signs. The arbitrary-angle solver, 64-pulse fallback, coordinate +transforms, and variable-length pulse plans are removed. The target stores only +the chosen quarter-turn angle and optional half-turn angle. Routing costs and +numeric/symbolic emission use the same chosen basis. + +Most device constants being dyadic fractions of π does not require a rational +angle type, angle snapping, or synthesis for every dyadic fraction. Keep +ordinary floating-point constants. Add another explicit recipe, such as repeated +π/4 pulses, only when an identified device requires it. Other declared fixed +values remain valid for native matching without automatically becoming synthesis +bases. + +Structural/classical cleanup, target-aware inlining, and QCO lifetime and +control-flow canonicalization remain available. Native sequences are preserved +when no universal basis exists. Restricted cleanup merges or cancels adjacent RZ +gates before and after synthesis; it leaves fixed RX pulses alone. Symbolic +merging reuses the existing phase-safe angle normalization. Numeric fusion must +shorten an already native run. General symbolic pulse costing and unrelated +square-root-iSWAP cleanup remain outside this change. + +### Use radians in Core and convert at the producer boundary + +Recommend revising the unreleased #2578 design to use radians consistently with +Core's existing rotations and the +[Braket SDK gate matrices](https://amazon-braket-sdk-python.readthedocs.io/en/stable/_modules/braket/circuits/gates.html). +Bench's IonQ gate classes use turns: import their GPI/GPI2 phases, all three MS +parameters, and ZZ interaction angles with one multiplication by 2π. Identify +these conventions from the producer's gate definitions, not the name `zz` alone. +Braket input/output then requires no unit conversion. A direct IonQ interface +would own its own conversion. + +With radian parameters, `GPI(φ) = i R(π, φ)`, `GPI2(φ) = R(π/2, φ)`, and +`MS(φ₀, φ₁, θ)` is RXX(θ) conjugated by the corresponding RZ phases. Braket +`zz(θ)` is Core `RZZ(θ)`: remove the duplicate ZZ operation and its +matrix/runtime plumbing from #2578. Retain GPI/GPI2/MS identities where needed +to preserve native output. Their names do not justify a new external QIR runtime +ABI; lower them to existing operations at the QIR boundary unless a concrete +consumer requires native QIS entry points. + +GPI/GPI2 have fixed rotation amounts but continuously variable phase arguments. +Do not constrain the GPI2 parameter to π/2. MS phases belong to ordered +operands; its full-entangling interaction is π/2, including Braket's +omitted-argument default. Existing two-qubit synthesis can use a supported +full-entangling MS or RZZ instance. When a provider bounds an interaction +parameter, expose a known-valid synthesis subset, such as fixed RZZ(π/2), rather +than advertising unrestricted RZZ and relying on export-time rejection. Other +supported interaction angles remain representable as fixed capabilities without +promising synthesis from them. The adapter owns device-specific domain +validation; add richer Core constraints only if concrete Bench circuits require +more than this conservative subset. + +### Validate the actual provider payload + +Bench's IonQ models include a free RZ operation, while Braket's native IonQ set +does not. Keep the benchmark model usable, but require strict verbatim targets +to lower RZ into native pulses or absorb it into their phases. Import Bench's +named Rigetti gates through their fixed RX definitions instead of adding new +Core gates. + +The Braket adapter owns native spellings (`prx`, `xx`, `zz`), provider argument +validation, and +[verbatim serialization](https://docs.aws.amazon.com/braket/latest/developerguide/braket-openqasm-verbatim-compilation.html). +Check the final native calls, radian values, and physical placements after +serialization. Custom OpenQASM definitions that expand into rotations and CNOTs +do not establish verbatim compatibility. Follow the provider's box and +measurement rules; IonQ requires all gates in the verbatim region. Apply the +documented +[rewiring requirements](https://docs.aws.amazon.com/braket/latest/developerguide/braket-constructing-circuit.html#verbatim-compilation), +including disabling rewiring for Rigetti. Keep ordinary service compilation and +strict native compilation as distinct target selections. + +## Work remaining, in priority order + +- [ ] **P0 — complete native ion coverage in #2578:** adopt radian conventions, + reuse RZZ, and keep native GPI/GPI2/MS synthesis independent of the + removed arbitrary-angle solver. Reconcile its plan and downstream + dependencies before implementation. Verify full matrices, phase, and + operand order. +- [ ] **P0 — close the consumer contract:** add Bench import and strict Braket + target/export checks for Rigetti, Forte, and Bench's Aria model. + Distinguish Bench-native output from provider-native output and verify + parameter conversion exactly once. These checks are required before + claiming the hardware use case complete, even though the adapter changes + land separately. +- [ ] **P1 — finish the catalogue:** exercise IQM and AQT through existing Core + operations, resolve missing device metadata such as Cepheus, and retain + coverage for other Bench families. Extend recipes only for a demonstrated + catalogue gap. ## Validation -Run these commands from the repository root after the build and environment -setup in [AGENTS.md](../../AGENTS.md#build-and-validation). The recorded results -below are from local validation, not hosted CI. - -- `build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler`: 244 - tests passed, including native-only compilation and OpenQASM export. -- `build/release/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/mqt-core-mlir-unittest-target-synthesis`: - 79 tests passed, including 1,944 full-matrix cases across all six axis pairs, - routing-cost agreement, and phase-preserving fixed-pulse fusion. Append - `--gtest_filter='TargetSynthesisTest.*FixedPulse*'` to run only those focused - checks. -- `build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/mqt-core-mlir-unittest-mapping`: - 125 tests passed. -- `uv run --no-sync pytest test/python/test_mlir.py -k 'fixed_parameter or fixed_pulse'`: - 362 cases passed, covering the Python API and numeric and symbolic input - gates. -- `uvx nox -s stubs`: passed; regenerated Python signatures include - `FIXED_ROTATION` and the empty-tuple `parameters` default. - -The native-suite results are from the preceding implementation validation. They -were not rerun for the binding-only changes; the Python cases and stub -generation were. Hosted CI is separate and was not rerun locally. - -## 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. +Use offline device fixtures and Braket SDK matrices as the consumer reference; +no hardware execution is needed. Check each native gate and representative +compiled Bench circuits. Include numeric and bound symbolic parameters, global +phase, unequal MS phases with reversed operands, the default MS interaction, and +named Rigetti fixed gates. Verify native operation names, parameter values, and +legal placements after export, with failures for unsupported fixed values and +residual non-native gates such as IonQ RZ. + +For #2575, tests retain native acceptance of RX(0.37) while rejecting it as a +synthesis basis. They cover both quarter-turn signs, optional half turns, +symbolic RZ merging and cancellation, parameter dominance, and consistency +between routing costs and emitted pulses. The former six-axis matrix tests are +replaced by 72 full-matrix cases for the supported recipes. Python checks +include bindings as large as 1e300 and the existing +[Qiskit input contract](../../docs/mlir/qiskit.md). + +After building with the repository release preset, run: + +```sh +build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler +build/release/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/mqt-core-mlir-unittest-target-synthesis +build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/mqt-core-mlir-unittest-mapping +build/release/mlir/unittests/Dialect/QCO/Transforms/Decomposition/mqt-core-mlir-unittest-decomposition +build/release/mlir/unittests/Dialect/QCO/Transforms/Optimizations/mqt-core-mlir-unittest-optimizations +QISKIT_NUM_PROCS=1 uv run --no-sync pytest -n 4 test/python/test_mlir.py +uvx nox -s lint +``` + +Run the owning adapter tests for the final payload checks. Follow +[repository guidance](../../AGENTS.md#build-and-validation) for binding stubs +and C++ lint when their implementation changes. + +The simplified implementation passed 245 compiler, 80 native-synthesis, 125 +mapping, 315 decomposition, and 204 optimization tests, plus all 162 Python MLIR +tests. A subsequent complexity review removed obsolete Euler-plan RZ coalescing; +these results include that deletion. Radian ion gates and Braket consumer checks +remain unimplemented follow-ups. diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 6f26748843..d57a173278 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -186,24 +186,22 @@ 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 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 native 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. +unrestricted parameters. Fixed-pulse synthesis requires unrestricted RZ and +RX(π/2) or RX(-π/2), available on every site. This covers Rigetti's native +single-qubit gates. Available RX(π) or RX(-π) pulses shorten suitable +decompositions. Numeric and symbolic input gates share the same recipe and +preserve global phase. + +Other fixed values, including other dyadic fractions of π, remain valid native +capabilities but do not create additional synthesis bases. They require a +supported basis when a non-native operation needs decomposition. Target cleanup preserves gate forms, including sequences of native gates on targets without a synthesis basis. Operations outside the native set still need a usable synthesis basis. Numeric single-qubit run fusion uses the fixed-pulse recipe and keeps native runs unless it can shorten them. Symbolic fixed-pulse -runs use individual gate lowering. Two-qubit run fusion remains available. +runs use individual gate lowering, with adjacent unrestricted RZ operations +merged before and after synthesis. Two-qubit run fusion remains available. Routing costs count native two-qubit gates, not the number of fixed pulses. Constraints on other gate families are checked, but do not create additional synthesis bases. Parameter ranges and relations are not supported. diff --git a/mlir/include/mqt/Compiler/Target.h b/mlir/include/mqt/Compiler/Target.h index 772c7a1963..6087099004 100644 --- a/mlir/include/mqt/Compiler/Target.h +++ b/mlir/include/mqt/Compiler/Target.h @@ -18,7 +18,6 @@ #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" -#include #include #include #include @@ -311,23 +310,14 @@ class CompilerTarget { XYX, ///< `RX(φ) * RY(θ) * RX(λ)`. ZYZ, ///< `RZ(φ) * RY(θ) * RZ(λ)`. ZXZ, ///< `RZ(φ) * RX(θ) * RZ(λ)`. - FixedRotation, ///< An arbitrary rotation and fixed pulses about another - ///< axis. + FixedRotation, ///< `RZ` with fixed `RX(±π/2)` and optional `RX(±π)`. }; - /// Fixed pulse combined with arbitrary rotations about a distinct axis. + /// Fixed RX pulses combined with unrestricted RZ rotations. struct FixedRotationBasis { - GateKind gate; - GateKind freeGate; - double angle; - /// Free rotation angles before, between, and after fixed pulses. - /// Together they implement a local RX(π/2). - std::vector quarterTurnAngles; + double quarterTurnAngle; 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/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h index bfe4a4408a..52b190090d 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -131,15 +131,17 @@ void populateFuseSingleQubitUnitaryRunsPatterns( /// /// The patterns emit @p basis directly. With @p target, preserve native runs /// and only use direct Euler identities, keeping optional fusion exportable. +/// Fixed-RX bases only merge adjacent RZ operations. void populateParameterizedSingleQubitRunCompositionPatterns( RewritePatternSet& patterns, SingleQubitBasis basis, const CompilerTarget* target = nullptr); namespace detail { -/// 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. +/// Emit ZYZ angles using RZ and fixed RX 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, @@ -152,31 +154,24 @@ emitFixedRotationSequence(const CompilerTarget::FixedRotationBasis& basis, 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])); - } - }; + const double offset = basis.quarterTurnAngle < 0. ? pi : 0.; if (matchesTheta(halfPi)) { - emitFree(lambda - constant(halfPi)); - quarterTurn(); - emitFree(phi + constant(halfPi)); + emitFree(lambda + constant(offset - halfPi)); + emitPulse(basis.quarterTurnAngle); + emitFree(phi + constant(halfPi - offset)); return 0.; } if (matchesTheta(pi) && basis.halfTurnAngle) { - const double axis = basis.gate == basis.axes()[0] ? 0. : halfPi; - emitFree(lambda + constant(axis)); + emitFree(lambda); emitPulse(*basis.halfTurnAngle); - emitFree(phi + constant(pi) - constant(axis)); + emitFree(phi + constant(pi)); return *basis.halfTurnAngle < 0. ? pi : 0.; } - emitFree(lambda); - quarterTurn(); + emitFree(lambda + constant(offset)); + emitPulse(basis.quarterTurnAngle); emitFree(theta + constant(pi)); - quarterTurn(); - emitFree(phi + constant(pi)); + emitPulse(basis.quarterTurnAngle); + emitFree(phi + constant(pi - offset)); return pi; } diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index b46a0bad20..038ecbaf5d 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -161,78 +161,6 @@ 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 -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 = 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) { - return std::nullopt; - } - auto& zAngles = result.quarterTurnAngles; - const double directCount = std::round(halfPi / magnitude); - if (directCount >= 1. && directCount <= static_cast(maxPulses) && - std::abs(directCount * magnitude - halfPi) <= - mqt::PARAMETER_COMPARISON_TOLERANCE) { - zAngles.resize(static_cast(directCount) + 1); - const double axis = (!isX ? halfPi : 0.) + (angle < 0. ? pi : 0.); - zAngles.front() = axis; - zAngles.back() = -axis; - return result; - } - 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 = - isX ? (sine < 0. ? halfPi : -halfPi) : (sine < 0. ? pi : 0.); - const double before = halfPi - gamma + eta; - const double after = -gamma - eta - halfPi; - zAngles.resize(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 result; -} - -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") { @@ -888,52 +816,30 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { } else if (supportsOnEverySite(GateKind::RY) && supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZYZ; - } else { - const auto supportsPulse = [&](StringRef name, double angle) { + } else if (supportsOnEverySite(GateKind::RZ)) { + const auto supportsPulse = [&](double angle) { return llvm::all_of(siteIds, [&](SiteId site) { return supportsOperation( - name, 1, 1, ArrayRef(&site, 1), false, + "rx", 1, 1, ArrayRef(&site, 1), false, [angle](size_t) { return std::optional{angle}; }); }); }; - for (GateKind freeGate : {GateKind::RZ, GateKind::RX, GateKind::RY}) { - if (!supportsOnEverySite(freeGate)) { + for (double quarter : {std::numbers::pi / 2., -std::numbers::pi / 2.}) { + if (!supportsPulse(quarter)) { continue; } - 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; + fixedRotation = { + .quarterTurnAngle = quarter, + .halfTurnAngle = std::nullopt, + }; + for (double half : {std::numbers::pi, -std::numbers::pi}) { + if (supportsPulse(half)) { + fixedRotation->halfTurnAngle = half; + break; } - 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); } - } - if (fixedRotation) { singleQubit = SingleQubitBasis::FixedRotation; + break; } } @@ -991,7 +897,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { .entangler = entangler == entanglerPreference.end() ? std::nullopt : std::optional{*entangler}, - .fixedRotation = std::move(fixedRotation), + .fixedRotation = fixedRotation, }; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 9e5ddf2798..79d218559d 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -39,24 +39,18 @@ bool isSingleQubitBasisGate( 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; }); + if (isa(op)) { + return true; + } + if (auto rotation = dyn_cast(op)) { + const auto angle = mqt::valueToDouble(rotation.getTheta()); + return angle && (std::abs(*angle - fixedRotation->quarterTurnAngle) <= + mqt::PARAMETER_COMPARISON_TOLERANCE || + (fixedRotation->halfTurnAngle && + std::abs(*angle - *fixedRotation->halfTurnAngle) <= + mqt::PARAMETER_COMPARISON_TOLERANCE)); + } + return false; } return TypeSwitch(op) .Case([&](RZOp) { @@ -266,18 +260,9 @@ 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)) { - 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; + if (!isNearZeroRotationAngle(angle)) { + steps.emplace_back(kind, angle); } - steps.emplace_back(kind, angle); } /// Appends a native `R(angle, axis)` step for non-negligible angles. @@ -370,9 +355,6 @@ struct Unitary1QEulerPlan { case SingleQubitBasis::FixedRotation: { assert(fixedRotation && "fixed-pulse synthesis requires a pulse descriptor"); - const auto kind = fixedRotation->gate == fixedRotation->axes()[0] - ? SynthesisStep::Kind::RX - : SynthesisStep::Kind::RY; phase = angles.phase + detail::emitFixedRotationSequence( *fixedRotation, angles.theta, angles.phi, angles.lambda, @@ -380,7 +362,9 @@ struct Unitary1QEulerPlan { [&](double angle) { appendRotation(SynthesisStep::Kind::RZ, angle); }, - [&](double angle) { steps.emplace_back(kind, angle); }); + [&](double angle) { + steps.emplace_back(SynthesisStep::Kind::RX, angle); + }); break; } case SingleQubitBasis::ZSXX: { @@ -428,40 +412,8 @@ planUnitary1QEuler(const Matrix2x2& targetMatrix, const SingleQubitBasis basis, return plan; } - 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); + const EulerAngles angles = anglesFromUnitary(targetMatrix, 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/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index fafcf8d842..16493de3d4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -1354,12 +1354,8 @@ struct TargetNativeSynthesisPass final if (targetBasis && targetBasis->singleQubit != CompilerTarget::SingleQubitBasis::U) { RewritePatternSet patterns(&getContext()); - // Symbolic composition does not yet account for fixed-pulse costs. - if (targetBasis->singleQubit != - CompilerTarget::SingleQubitBasis::FixedRotation) { - decomposition::populateParameterizedSingleQubitRunCompositionPatterns( - patterns, targetBasis->singleQubit, &target); - } + decomposition::populateParameterizedSingleQubitRunCompositionPatterns( + patterns, targetBasis->singleQubit, &target); decomposition::populateFuseSingleQubitUnitaryRunsPatterns( patterns, targetBasis->singleQubit, /*skipControlledBodies=*/true, &target); @@ -1435,6 +1431,16 @@ struct TargetNativeSynthesisPass final signalPassFailure(); return; } + if (targetBasis && targetBasis->singleQubit == + CompilerTarget::SingleQubitBasis::FixedRotation) { + RewritePatternSet patterns(&getContext()); + decomposition::populateParameterizedSingleQubitRunCompositionPatterns( + patterns, targetBasis->singleQubit, &target); + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { + signalPassFailure(); + return; + } + } if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); } diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index 78f762363a..b71f1cde0c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -740,6 +740,48 @@ static Val sumAngles(Val lhs, Val rhs) { return lhs + rhs; } +/// Merge only the unrestricted axis of a fixed-RX target. +static LogicalResult mergeParameterizedRZ(RZOp op, PatternRewriter& rewriter) { + auto next = dyn_cast(*op.getQubitOut().user_begin()); + if (!next || next->getBlock() != op->getBlock() || + (mqt::valueToConstantDouble(op.getTheta()) && + mqt::valueToConstantDouble(next.getTheta()))) { + return failure(); + } + const auto negates = [](Value lhs, Value rhs) { + if (auto negation = lhs.getDefiningOp()) { + return negation.getOperand() == rhs; + } + if (auto product = lhs.getDefiningOp()) { + return (product.getLhs() == rhs && + mqt::valueToConstantDouble(product.getRhs()) == -1.) || + (product.getRhs() == rhs && + mqt::valueToConstantDouble(product.getLhs()) == -1.); + } + return false; + }; + if (negates(op.getTheta(), next.getTheta()) || + negates(next.getTheta(), op.getTheta())) { + rewriter.replaceOp(next, op.getQubitIn()); + } else { + rewriter.setInsertionPoint(next); + const auto loc = next.getLoc(); + const auto angle = sumAngles(normalizeGateAngle(Val{ + .v = op.getTheta(), + .rewriter = &rewriter, + .loc = loc, + }), + normalizeGateAngle(Val{ + .v = next.getTheta(), + .rewriter = &rewriter, + .loc = loc, + })); + rewriter.replaceOpWithNewOp(next, op.getQubitIn(), angle.v); + } + rewriter.eraseOp(op); + return success(); +} + static void emitParameterizedGPhaseIfNeeded(RewriterBase& rewriter, Location loc, Val phase) { if (!isConstantAngle(phase)) { @@ -754,9 +796,11 @@ 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; + const bool usesZYZAngles = + basis == decomposition::SingleQubitBasis::ZYZ || + basis == decomposition::SingleQubitBasis::ZXZ || + basis == decomposition::SingleQubitBasis::ZSXX || + basis == decomposition::SingleQubitBasis::FixedRotation; if (usesZYZAngles && isConstantAngle(theta)) { qubit = emitRotationIfNeeded(rewriter, loc, qubit, sumAngles(phi, lambda)); @@ -798,28 +842,15 @@ static Value emitRuntimeEulerAngles( 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: - qubit = emitRotationIfNeeded(rewriter, loc, qubit, angle); - break; - case CompilerTarget::GateKind::RY: - qubit = emitRotationIfNeeded(rewriter, loc, qubit, angle); - break; - default: - qubit = emitRotationIfNeeded(rewriter, loc, qubit, angle); - break; - } - }; - if (isConstantAngle(theta)) { - emit(fixedRotation->freeGate, sumAngles(phi, lambda)); - break; - } const double correction = decomposition::detail::emitFixedRotationSequence( *fixedRotation, theta, phi, lambda, mqt::valueToConstantDouble(theta.v), constant, - [&](Val angle) { emit(fixedRotation->freeGate, angle); }, - [&](double angle) { emit(fixedRotation->gate, constant(angle)); }); + [&](Val angle) { + qubit = emitRotationIfNeeded(rewriter, loc, qubit, angle); + }, + [&](double angle) { + qubit = RXOp::create(rewriter, loc, qubit, angle).getQubitOut(); + }); if (correction != 0.) { phase = phase + constant(correction); } @@ -1364,44 +1395,8 @@ void decomposition::synthesizeParameterizedUnitary1Q( } else { const auto angles = directZYZAnglesFromGate(unitary, rewriter, consts); qubit = unitary.getInputQubit(0); - 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. - 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); - } + qubit = emitRuntimeEulerAngles(rewriter, op->getLoc(), qubit, angles, + basis, consts, fixedRotation); } rewriter.replaceOp(op, qubit); return; @@ -1421,9 +1416,13 @@ namespace mlir::qco::decomposition { void populateParameterizedSingleQubitRunCompositionPatterns( RewritePatternSet& patterns, SingleQubitBasis basis, const CompilerTarget* target) { + RZOp::getCanonicalizationPatterns(patterns, patterns.getContext()); + if (basis == SingleQubitBasis::FixedRotation) { + patterns.add(mergeParameterizedRZ); + return; + } RXOp::getCanonicalizationPatterns(patterns, patterns.getContext()); RYOp::getCanonicalizationPatterns(patterns, patterns.getContext()); - RZOp::getCanonicalizationPatterns(patterns, patterns.getContext()); POp::getCanonicalizationPatterns(patterns, patterns.getContext()); patterns.add(patterns.getContext(), basis, target); diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index d55bc2e10a..e42b9ea520 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -1020,8 +1020,15 @@ TEST(CompilerTargetTest, ResolvesFixedPulseBasisOnlyOnEverySite) { EXPECT_FALSE(target.synthesisBasis()->entangler); } -TEST(CompilerTargetTest, RejectsDegenerateOrExcessiveFixedPulseSynthesis) { - for (double angle : {0., std::numbers::pi, 2. * std::numbers::pi, 1e-8}) { +TEST(CompilerTargetTest, KeepsUnsupportedPulseAnglesNativeWithoutSynthesis) { + for (double angle : { + 0., + .37, + std::numbers::pi / 4., + std::numbers::pi, + 2. * std::numbers::pi, + 1e-8, + }) { const auto target = valid(Target::create( 1, Connectivity::allToAll(), NativeOperations::fromOperations({ @@ -1035,6 +1042,22 @@ TEST(CompilerTargetTest, RejectsDegenerateOrExcessiveFixedPulseSynthesis) { } } +TEST(CompilerTargetTest, FixedPulseSynthesisRequiresRZAndRX) { + for (const auto& [free, pulse] : + {std::pair{"rx", "ry"}, std::pair{"rz", "ry"}}) { + const auto target = valid(Target::create( + 1, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(OperationCapability::create(free, 1, 1)), + valid(OperationCapability::create("gphase", 0, 1)), + valid(OperationCapability::create(pulse, 1, 1, {}, std::nullopt, + std::nullopt, + {std::numbers::pi / 2.})), + }))); + EXPECT_FALSE(target.synthesisBasis()); + } +} + 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 0e461dd1cd..735cd7ef4a 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -329,97 +329,75 @@ TEST_F(TargetSynthesisTest, TargetPassesRequireTypedEnvironment) { } TEST_F(TargetSynthesisTest, FixedPulseSynthesisPreservesFullUnitary) { - 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.}) { + for (const std::optional halfTurn : { + std::optional{}, + std::optional{std::numbers::pi}, + std::optional{-std::numbers::pi}, + }) { + SCOPED_TRACE(testing::Message() + << pulseAngle << " " << halfTurn.value_or(0.)); + std::vector operations{ + valid(OperationCapability::create("rx", 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( + "rx", 1, 1, {}, std::nullopt, std::nullopt, {halfTurn}))); } - 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}, + 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 std::optional parameterIndex : { + std::optional{}, + std::optional{0U}, + std::optional{1U}, }) { - 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)), - }; - if (halfTurn) { - operations.push_back(valid(OperationCapability::create( - name, 1, 1, {}, std::nullopt, std::nullopt, {halfTurn}))); + 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)); } - 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 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); - } + 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); } } } @@ -427,7 +405,7 @@ TEST_F(TargetSynthesisTest, FixedPulseSynthesisPreservesFullUnitary) { } TEST_F(TargetSynthesisTest, FixedPulseFusionPreservesNativeAngles) { - for (const auto angle : {.37, std::numbers::pi / 4, std::numbers::pi / 2}) { + for (const auto angle : {std::numbers::pi / 2, -std::numbers::pi / 2}) { SCOPED_TRACE(angle); const auto target = valid(Target::create( 1, Connectivity::allToAll(), @@ -464,33 +442,60 @@ TEST_F(TargetSynthesisTest, FixedPulseFusionPreservesNativeAngles) { } } -TEST_F(TargetSynthesisTest, FixedHalfTurnUsesOnePulse) { - 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(), - 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); +TEST_F(TargetSynthesisTest, FixedPulseRZMergingPreservesParameterDominance) { + const auto target = + valid(Target::create(1, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(OperationCapability::create( + "rx", 1, 1, {}, std::nullopt, std::nullopt, + {std::numbers::pi / 2.})), + valid(OperationCapability::create("rz", 1, 1)), + }))); + auto program = mlir::parseSourceString(R"mlir( + module { + func.func @main(%a: f64, %b: f64) { + %q = qco.static 0 : !qco.qubit + %q1 = qco.rz(%a) %q : !qco.qubit -> !qco.qubit + %late = arith.negf %b : f64 + %q2 = qco.rz(%late) %q1 : !qco.qubit -> !qco.qubit + qco.sink %q2 : !qco.qubit + return + } } + )mlir", + context.get()); + ASSERT_TRUE(program); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *program, target, mlir::qco::createTargetNativeSynthesis()))); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*program))); + EXPECT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*program))); + EXPECT_EQ(countOps(*program), 1); +} + +TEST_F(TargetSynthesisTest, FixedHalfTurnUsesOnePulse) { + for (double half : {std::numbers::pi, -std::numbers::pi}) { + const auto target = valid(Target::create( + 1, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(OperationCapability::create("rx", 1, 1, {}, std::nullopt, + std::nullopt, + {std::numbers::pi / 2.})), + valid(OperationCapability::create("rx", 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), 1); } } @@ -699,9 +704,10 @@ TEST_F(TargetSynthesisTest, NativeCostMatchesFixedPulseSynthesis) { const auto target = valid(Target::create( 2, Connectivity::allToAll(), NativeOperations::fromOperations({ - valid(OperationCapability::create("ry", 1, 1)), - valid(OperationCapability::create("rz", 1, 1, {}, std::nullopt, - std::nullopt, {.37})), + valid(OperationCapability::create("rz", 1, 1)), + valid(OperationCapability::create("rx", 1, 1, {}, std::nullopt, + std::nullopt, + {std::numbers::pi / 2.})), valid(OperationCapability::create("cz", 2, 0)), valid(OperationCapability::create("gphase", 0, 1)), }))); diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 7548f74374..b60c7b8c50 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -683,26 +683,15 @@ 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"]) -@pytest.mark.parametrize( - ("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, free: str, pulse: str, pulse_angle: float -) -> None: - """Compile into arbitrary and fixed rotations about distinct axes, preserving phase.""" +@pytest.mark.parametrize("pulse_angle", [np.pi / 2, -np.pi / 2]) +def test_fixed_pulse_compilation_preserves_phase(theta: float | str, gate: str, pulse_angle: float) -> None: + """Compile into RZ and fixed RX quarter turns, 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(free, 1, 1), + CompilerTarget.OperationCapability("rx", 1, 1, fixed_parameters=[pulse_angle]), + CompilerTarget.OperationCapability("rz", 1, 1), CompilerTarget.OperationCapability("gphase", 0, 1), ]), ) @@ -715,8 +704,8 @@ def test_fixed_pulse_compilation_preserves_phase( 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, free} - assert all(item.operation.params == [pulse_angle] for item in result.data if item.operation.name == pulse) + assert set(result.count_ops()) <= {"rx", "rz"} + assert all(item.operation.params == [pulse_angle] 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( @@ -725,6 +714,45 @@ def test_fixed_pulse_compilation_preserves_phase( ) +@requires_qiskit_translation +@pytest.mark.parametrize("shape", ["sum", "cancel", "after_synthesis"]) +def test_fixed_pulse_merges_symbolic_rz(shape: str) -> None: + """Merge the unrestricted axis without changing fixed RX pulses or phase.""" + num_qubits = 2 if shape == "cancel" else 1 + target = CompilerTarget( + num_qubits, + 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), + ]), + ) + a, b = qiskit.circuit.ParameterVector("angle", 2) + source = QuantumCircuit(num_qubits) + if shape == "after_synthesis": + source.rx(a, 0) + else: + source.rz(a, 0) + source.rz(-a if shape == "cancel" else b, 0) + if shape == "cancel": + # Keep the named input in use: Qiskit export rejects unused inputs. + source.rz(a, 1) + program = QCProgram.from_qiskit(source).to_qco() + program.compile_for_target(_test_target_environment(target)) + result = program.to_qiskit(target=target) + assert result.count_ops().get("rz", 0) == {"sum": 1, "cancel": 1, "after_synthesis": 3}[shape] + assert all(item.operation.params == [np.pi / 2] for item in result.data if item.operation.name == "rx") + for lhs, rhs in [(0.4, -0.1), (1e20, 1.0), (1e300, -1e300)]: + values = {a: lhs, b: rhs} + assert np.allclose( + Operator(result.assign_parameters(values, strict=False)).data, + Operator(source.assign_parameters(values, strict=False)).data, + atol=1e-10, + rtol=0, + ) + + @requires_qiskit_translation def test_symbolic_su2_compiles_and_binds_after_export() -> None: """Compile symbolic SU2 circuits with bindable parameters and exact phase.""" From 284d8b73efaafd745fac90ba73e894e2d8e100f6 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 1 Oct 2026 22:36:20 +0000 Subject: [PATCH 20/23] =?UTF-8?q?=F0=9F=A7=AA=20Cover=20fixed-pulse=20targ?= =?UTF-8?q?ets=20with=20routed=20layouts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Exercise RZ, fixed RX quarter turns, and iSWAP in the existing layout export regression after rebasing onto #2553. Verify native pulse angles and full-unitary semantics through placement, routing, and QC export. Record the refreshed integration checks; no production changes are needed. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 13 ++++++++----- test/python/test_mlir_qiskit_translation.py | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 1fe91ca8d0..8b864036ef 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -168,7 +168,7 @@ build/release/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/mqt-core-mli build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/mqt-core-mlir-unittest-mapping build/release/mlir/unittests/Dialect/QCO/Transforms/Decomposition/mqt-core-mlir-unittest-decomposition build/release/mlir/unittests/Dialect/QCO/Transforms/Optimizations/mqt-core-mlir-unittest-optimizations -QISKIT_NUM_PROCS=1 uv run --no-sync pytest -n 4 test/python/test_mlir.py +QISKIT_NUM_PROCS=1 uv run --no-sync pytest -n 4 test/python/test_mlir.py test/python/test_mlir_qiskit_translation.py uvx nox -s lint ``` @@ -176,8 +176,11 @@ Run the owning adapter tests for the final payload checks. Follow [repository guidance](../../AGENTS.md#build-and-validation) for binding stubs and C++ lint when their implementation changes. -The simplified implementation passed 245 compiler, 80 native-synthesis, 125 -mapping, 315 decomposition, and 204 optimization tests, plus all 162 Python MLIR -tests. A subsequent complexity review removed obsolete Euler-plan RZ coalescing; -these results include that deletion. Radian ion gates and Braket consumer checks +After rebasing onto the layout support in #2553, local validation passed 254 +compiler, 80 native-synthesis, 126 mapping, 315 decomposition, and 204 +optimization tests, plus all 600 Python MLIR and Qiskit translation tests. The +layout export regression now also checks fixed RX/RZ/iSWAP targets through +placement and routing. Static fixed-pulse compilation and synthesis retain +physical sites and reject nonadjacent interactions. The integration review +required no production changes. Radian ion gates and Braket consumer checks remain unimplemented follow-ups. diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 94f98d7292..b6c07ae300 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -4659,8 +4659,18 @@ def test_transpiler_added_ancillas_and_routing_round_trip() -> None: @pytest.mark.parametrize("routed", [False, True]) -def test_native_mapping_exports_full_qiskit_layout(*, routed: bool) -> None: +@pytest.mark.parametrize("basis", ["u", "fixed_rx"]) +def test_native_mapping_exports_full_qiskit_layout(*, routed: bool, basis: str) -> None: """Qiskit removes initial placement and routing, including workspace swaps.""" + operations = ( + [CompilerTarget.OperationCapability("u", 1, 3), CompilerTarget.OperationCapability("cz", 2, 0)] + if basis == "u" + else [ + CompilerTarget.OperationCapability("rz", 1, 1), + CompilerTarget.OperationCapability("rx", 1, 1, fixed_parameters=[np.pi / 2]), + CompilerTarget.OperationCapability("iswap", 2, 0), + ] + ) target = CompilerTarget( "sparse line", [CompilerTarget.Site(site) for site in [10, 30, 20, 40]], @@ -4670,8 +4680,7 @@ def test_native_mapping_exports_full_qiskit_layout(*, routed: bool) -> None: else CompilerTarget.Connectivity.all_to_all() ), native_operations=CompilerTarget.NativeOperations([ - CompilerTarget.OperationCapability("u", 1, 3), - CompilerTarget.OperationCapability("cz", 2, 0), + *operations, CompilerTarget.OperationCapability("gphase", 0, 1), ]), ) @@ -4684,6 +4693,9 @@ def test_native_mapping_exports_full_qiskit_layout(*, routed: bool) -> None: program = QCProgram.from_qiskit(circuit).to_qco() program.compile_for_target(_test_target_environment(target)) exported = program.to_qiskit(target=target) + if basis == "fixed_rx": + assert set(exported.count_ops()) <= {"rx", "rz", "iswap"} + assert all(gate.operation.params == [np.pi / 2] for gate in exported.data if gate.operation.name == "rx") layout = exported.layout assert layout is not None assert sorted(layout.initial_index_layout()) == list(range(4)) From fd433a2dd1b27e4d1a753092812482f1edf13e5a Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 2 Oct 2026 00:29:04 +0000 Subject: [PATCH 21/23] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Reuse=20ZSXX=20for?= =?UTF-8?q?=20constrained=20target=20pulses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Use the existing ZSXX decomposition for named SX/X gates and signed fixed RX quarter turns with optional half turns. Require a usable single-qubit synthesis basis for target compilation and remove special cleanup APIs. Preserve distinct instruction names and fixed parameter alternatives in Qiskit target import and circuit export. Keep phase and placement checks, and prioritize current Braket devices while leaving ion gates to #2578. Refresh the contract documentation and record native, Python, lint, and executable documentation validation after independent design review. Assisted-by: GPT-6 via Codex --- .agent/plans/fixed-parameter-targets.md | 309 ++++++++---------- bindings/mlir/qiskit/Qiskit2_5.cpp | 71 +++- bindings/mlir/register_mlir.cpp | 22 +- bindings/patterns.txt | 1 + docs/mlir/qiskit.md | 20 +- docs/mlir/target_compilation.md | 57 ++-- mlir/include/mqt/Compiler/Target.h | 39 ++- mlir/include/mqt/Compiler/TargetCompilation.h | 5 +- mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td | 5 +- .../QCO/Transforms/Decomposition/Euler.h | 58 +--- .../mqt/Dialect/QCO/Transforms/Passes.h | 12 +- mlir/include/mqt/Support/Passes.h | 9 +- mlir/lib/Compiler/Pipeline.cpp | 6 +- mlir/lib/Compiler/Target.cpp | 45 ++- mlir/lib/Compiler/TargetCompilation.cpp | 29 +- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 6 +- .../QCO/Transforms/Decomposition/Euler.cpp | 113 +++---- .../QCO/Transforms/Decomposition/Weyl.cpp | 3 +- .../FuseSingleQubitUnitaryRuns.cpp | 12 +- .../NativeSynthesis/TargetSynthesis.cpp | 71 +--- .../MergeSingleQubitRotationGates.cpp | 100 +++--- mlir/lib/Support/Passes.cpp | 16 +- .../Compiler/test_compiler_pipeline.cpp | 119 +++---- .../Compiler/test_compiler_target.cpp | 8 +- .../QCO/Transforms/Mapping/test_mapping.cpp | 12 +- .../NativeSynthesis/test_target_synthesis.cpp | 29 +- python/mqt/core/mlir.pyi | 17 +- test/python/test_mlir.py | 2 +- test/python/test_mlir_qiskit_target.py | 143 +++++++- test/python/test_mlir_qiskit_translation.py | 4 +- 30 files changed, 687 insertions(+), 656 deletions(-) diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md index 8b864036ef..de87b9ced9 100644 --- a/.agent/plans/fixed-parameter-targets.md +++ b/.agent/plans/fixed-parameter-targets.md @@ -1,166 +1,131 @@ -# Fixed-parameter targets for Bench and Braket - -Status: in progress. The #2575 simplification is implemented and tested. Native -ion gates and provider integration remain proposed follow-ups. - -## Goal and scope - -Compile MQT Bench circuits for its device gate sets, prioritizing native gates -exposed by Amazon Braket for verbatim execution. Separate three contracts: -representing a native operation, synthesizing into a supported native basis, and -serializing a program the provider accepts. Completion requires checking all -three; a compiler target or generic OpenQASM round trip alone is insufficient. - -Keep [Core #2575](https://github.com/munich-quantum-toolkit/core/pull/2575) -focused on fixed-parameter capabilities and Rigetti synthesis. Coordinate native -ion gates through -[Core #2578](https://github.com/munich-quantum-toolkit/core/pull/2578), then -complete Bench import and Braket export in their owning adapters. This plan -records the dependency order without combining those changes into one PR. - -The initial coverage matrix follows the -[Braket native-gate catalogue](https://docs.aws.amazon.com/braket/latest/developerguide/braket-submit-tasks.html) -and -[Bench target definitions](https://github.com/munich-quantum-toolkit/bench/tree/d23df0d5d2791b44633858783e62a7468480f362/src/mqt/bench/targets). - -| Target family | Required native gates | Core representation and work | -| ------------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| Rigetti Ankaa / Bench Ankaa-84 | `rx`, `rz`, `iswap`; RX restricted to ±π/2 and ±π | Constrained `RX`, unrestricted `RZ`, existing `iSWAP`; Bench exposes +π and ±π/2 through named gates. | -| IonQ Forte | `gpi`, `gpi2`, `zz` | Native `GPI`/`GPI2`; reuse `RZZ` for Braket `zz`. | -| Bench IonQ Aria | `gpi`, `gpi2`, `ms` | Retain native `MS` support even though the current Braket catalogue lists Forte. | -| IQM Garnet / Emerald | `prx`, `cz` | Reuse Core `R` and `CZ`; check synthesis and external spelling. | -| AQT IBEX-Q1 | `prx`, `xx`, `rz` | Reuse Core `R`, `RXX`, and `RZ`; check synthesis and external spelling. | - -Device fixtures must record their identity and capability snapshot. Derive a -verbatim target from `paradigm.nativeGateSet`, topology, and documented -parameter restrictions, not the broader service `supportedOperations` list. The -current Braket catalogue also lists Rigetti Cepheus, but only specifies Ankaa's -native set; obtain its device metadata before claiming complete current-device -coverage. Preserve existing support for other Bench targets, including IBM and -Quantinuum. QuEra analog programs, pulse control, and general approximate -Clifford+T synthesis are outside this work. - -## Decisions and follow-up design - -### Keep capability matching general; keep synthesis small - -Retain optional finite fixed values per parameter. An omitted value means an -unrestricted parameter; separate capabilities express alternatives and -placements. Matching, serialization, compatibility, and final verification must -agree. Unbound symbolic values cannot satisfy a fixed parameter. Use the -existing absolute comparison tolerance, without periodic wrapping that loses -phase. - -In #2575, `mlir/lib/Compiler/Target.cpp` selects unrestricted RZ with fixed RX -quarter turns and optional half turns. The shared Euler recipe preserves phase -for both pulse signs. The arbitrary-angle solver, 64-pulse fallback, coordinate -transforms, and variable-length pulse plans are removed. The target stores only -the chosen quarter-turn angle and optional half-turn angle. Routing costs and -numeric/symbolic emission use the same chosen basis. - -Most device constants being dyadic fractions of π does not require a rational -angle type, angle snapping, or synthesis for every dyadic fraction. Keep -ordinary floating-point constants. Add another explicit recipe, such as repeated -π/4 pulses, only when an identified device requires it. Other declared fixed -values remain valid for native matching without automatically becoming synthesis -bases. - -Structural/classical cleanup, target-aware inlining, and QCO lifetime and -control-flow canonicalization remain available. Native sequences are preserved -when no universal basis exists. Restricted cleanup merges or cancels adjacent RZ -gates before and after synthesis; it leaves fixed RX pulses alone. Symbolic -merging reuses the existing phase-safe angle normalization. Numeric fusion must -shorten an already native run. General symbolic pulse costing and unrelated -square-root-iSWAP cleanup remain outside this change. - -### Use radians in Core and convert at the producer boundary - -Recommend revising the unreleased #2578 design to use radians consistently with -Core's existing rotations and the -[Braket SDK gate matrices](https://amazon-braket-sdk-python.readthedocs.io/en/stable/_modules/braket/circuits/gates.html). -Bench's IonQ gate classes use turns: import their GPI/GPI2 phases, all three MS -parameters, and ZZ interaction angles with one multiplication by 2π. Identify -these conventions from the producer's gate definitions, not the name `zz` alone. -Braket input/output then requires no unit conversion. A direct IonQ interface -would own its own conversion. - -With radian parameters, `GPI(φ) = i R(π, φ)`, `GPI2(φ) = R(π/2, φ)`, and -`MS(φ₀, φ₁, θ)` is RXX(θ) conjugated by the corresponding RZ phases. Braket -`zz(θ)` is Core `RZZ(θ)`: remove the duplicate ZZ operation and its -matrix/runtime plumbing from #2578. Retain GPI/GPI2/MS identities where needed -to preserve native output. Their names do not justify a new external QIR runtime -ABI; lower them to existing operations at the QIR boundary unless a concrete -consumer requires native QIS entry points. - -GPI/GPI2 have fixed rotation amounts but continuously variable phase arguments. -Do not constrain the GPI2 parameter to π/2. MS phases belong to ordered -operands; its full-entangling interaction is π/2, including Braket's -omitted-argument default. Existing two-qubit synthesis can use a supported -full-entangling MS or RZZ instance. When a provider bounds an interaction -parameter, expose a known-valid synthesis subset, such as fixed RZZ(π/2), rather -than advertising unrestricted RZZ and relying on export-time rejection. Other -supported interaction angles remain representable as fixed capabilities without -promising synthesis from them. The adapter owns device-specific domain -validation; add richer Core constraints only if concrete Bench circuits require -more than this conservative subset. - -### Validate the actual provider payload - -Bench's IonQ models include a free RZ operation, while Braket's native IonQ set -does not. Keep the benchmark model usable, but require strict verbatim targets -to lower RZ into native pulses or absorb it into their phases. Import Bench's -named Rigetti gates through their fixed RX definitions instead of adding new -Core gates. - -The Braket adapter owns native spellings (`prx`, `xx`, `zz`), provider argument -validation, and -[verbatim serialization](https://docs.aws.amazon.com/braket/latest/developerguide/braket-openqasm-verbatim-compilation.html). -Check the final native calls, radian values, and physical placements after -serialization. Custom OpenQASM definitions that expand into rotations and CNOTs -do not establish verbatim compatibility. Follow the provider's box and -measurement rules; IonQ requires all gates in the verbatim region. Apply the -documented -[rewiring requirements](https://docs.aws.amazon.com/braket/latest/developerguide/braket-constructing-circuit.html#verbatim-compilation), -including disabling rewiring for Rigetti. Keep ordinary service compilation and -strict native compilation as distinct target selections. - -## Work remaining, in priority order - -- [ ] **P0 — complete native ion coverage in #2578:** adopt radian conventions, - reuse RZZ, and keep native GPI/GPI2/MS synthesis independent of the - removed arbitrary-angle solver. Reconcile its plan and downstream - dependencies before implementation. Verify full matrices, phase, and - operand order. -- [ ] **P0 — close the consumer contract:** add Bench import and strict Braket - target/export checks for Rigetti, Forte, and Bench's Aria model. - Distinguish Bench-native output from provider-native output and verify - parameter conversion exactly once. These checks are required before - claiming the hardware use case complete, even though the adapter changes - land separately. -- [ ] **P1 — finish the catalogue:** exercise IQM and AQT through existing Core - operations, resolve missing device metadata such as Cepheus, and retain - coverage for other Bench families. Extend recipes only for a demonstrated - catalogue gap. +# Fixed-parameter compiler targets + +Status: implementation and local validation complete on main `bb0bf98dc`. + +## Scope and ownership + +[Core #2575](https://github.com/munich-quantum-toolkit/core/pull/2575) supports +fixed operation parameters, multiple native alternatives, Qiskit target +exchange, and RX pulse synthesis with RZ and an existing entangler such as CZ. +Current Braket devices guide coverage. Existing Core `R`, `RXX`, `RZZ`, and `CZ` +represent the corresponding PRX, XX, ZZ, and CZ operations; representing an +operation does not establish a complete synthesis basis or provider execution +support. + +`CompilerTarget` owns capability validation and basis selection. Native +synthesis owns phase-correct lowering. Qiskit import/export owns named +instruction alternatives. Provider adapters own verbatim syntax, physical +labels, parameter domains, and submission constraints. + +## Device evidence + +An unfiltered, paginated AWS `SearchDevices` scan on 2026-10-01 at 23:55 UTC +covered all five [Braket regions][devices]: `us-east-1`, `us-west-1`, +`us-west-2`, `eu-north-1`, and `eu-west-2`. `GetDevice` inspected every online +regional entry. The six online gate QPUs advertised these +`paradigm.nativeGateSet` values: + +| Device | Region | Native operations | +| ----------------------- | ---------- | ---------------------------------------------- | +| Rigetti Cepheus-1-108Q | us-west-1 | `rx`, `rz`, `cz`, `barrier` | +| IonQ Forte-1 | us-east-1 | `GPI`, `GPI2`, `ZZ` | +| IonQ Forte-Enterprise-1 | us-east-1 | `GPI`, `GPI2`, `ZZ` | +| IQM Garnet | eu-north-1 | `cz`, `prx`, `cc_prx`, `measure_ff`, `barrier` | +| IQM Emerald | eu-north-1 | `cz`, `prx`, `cc_prx`, `measure_ff`, `barrier` | +| AQT IBEX Q1 | eu-north-1 | `prx`, `xx`, `rz` | + +Aquila was online but accepts analog Hamiltonian programs. SV1 and DM1 were +online simulators, with no native gate set. Ankaa-3 and IonQ Aria were retired. +Use Cepheus/CZ for the current Rigetti example; retain general iSWAP support. + +All six gate QPUs advertised verbatim, physical qubits, sparse indices, and +subset measurements. Partial verbatim boxes were enabled except on IonQ. Only +IonQ allowed unassigned measurements. The broader `supportedOperations` list +describes compiled inputs and must not replace the native whitelist. Physical +labels also need translation: Cepheus had 107 active sites in `0..107`, +excluding `8`, while IQM labels started at `1`. Refresh topology instead of +embedding these labels. + +[AWS documents][gates] Rigetti RX angles as ±π/2 and ±π. The live gate metadata +contains no numerical parameter domains; no additional Braket acceptance bounds +were verified. In particular, direct AQT API bounds and retired IonQ MS bounds +must not be assumed to constrain Braket Forte ZZ. Braket angles use radians. + +## Implementation decisions + +The closest ecosystem model is [Qiskit's target][qiskit-target]: standard gate +instances carry fixed parameters and distinct names for alternatives. PennyLane +separates [operation capabilities][pennylane-capabilities] from +[decomposition predicates][pennylane-decomposition]. CUDA-Q selects +[backend lowering passes][cudaq-backend]; Microsoft's neutral-atom simulator +uses [SX/RZ/CZ lowering][qdk-native]. These support separating capability facts, +small decomposition recipes, and provider execution rules; they do not justify a +general fixed-angle solver or a common schema across all SDKs. + +- Keep finite fixed values per parameter. Omitted values are unrestricted; + repeated capabilities form a union across values and placements. Unbound + parameters cannot satisfy fixed constraints. Preserve absolute matching + tolerance and phase; do not wrap parameters during capability matching. +- Reuse the existing `ZSXX` decomposition for IBM SX/X and fixed RX pulses with + unrestricted RZ. Replace `FixedRotation` with optional pulse choices in the + synthesis configuration. Account for the phase between SX/X and RX exactly. + Both quarter-turn signs and optional half turns are valid choices; emission + and routing costs must use the selected capabilities. +- Require a complete supported synthesis basis for target compilation. This + permits ordinary canonicalization before final native lowering and removes + special cleanup passes for incomplete native-only targets. Capability queries + can still represent such targets. Final output must satisfy the target. +- Preserve named Qiskit instructions and their constant parameters on target + import and export. Multiple RX variants must survive together, including + different placements. Do not infer operation semantics from an arbitrary name. +- Keep general Euler helpers independent of target policy. Do not introduce an + arbitrary-angle solver, rational angle representation, or six-axis pulse + framework without a concrete device need. Rewrite user documentation around + the supported contract and examples. + +[Core #2578](https://github.com/munich-quantum-toolkit/core/pull/2578) is +already stacked on #2575 and owns GPI/GPI2 coverage. On rebase, reassess its +unreleased turn-based gates: prefer radians and existing RZZ, prioritizing Forte +over retired Aria MS while preserving GPi's phase relative to `R(π, φ)`. Its GPI +pulse recipe is independent of fixed RX synthesis. Existing R/PRX covers IQM and +AQT unitary representations. Provider serialization and IQM +[experimental feedforward][dynamic] remain separate work, including feedback +groups and result semantics. Follow [verbatim rules][verbatim] at the adapter +boundary; fixed parameters do not provide complete execution support. ## Validation -Use offline device fixtures and Braket SDK matrices as the consumer reference; -no hardware execution is needed. Check each native gate and representative -compiled Bench circuits. Include numeric and bound symbolic parameters, global -phase, unequal MS phases with reversed operands, the default MS interaction, and -named Rigetti fixed gates. Verify native operation names, parameter values, and -legal placements after export, with failures for unsupported fixed values and -residual non-native gates such as IonQ RZ. +- [x] Finish capability-driven ZSXX lowering and remove obsolete cleanup APIs. +- [x] Validate multiple named/fixed Qiskit capabilities through target round + trips. +- [x] Verify phase, signs, optional half turns, symbolic inputs, routing costs, + and rejection of unsupported compilation targets; review the final diff. +- [x] Regenerate binding stubs and run the required native, Python, and lint + checks. + +Local validation on the revised implementation passed 985 native tests (254 +compiler, 86 native synthesis, 126 mapping, 315 decomposition, 204 optimization) +and 631 Python MLIR/Qiskit tests. Binding stubs were regenerated. Independent +correctness and Ponytail reviews found a missing `canonical_name` stub override +and obsolete includes; both were corrected. No remaining design or correctness +finding was identified. Repository lint and full changed-file C++ lint passed. + +The docs session now rebuilds the local package with documentation generation +enabled, preventing cached builds from omitting MLIR reference pages. The full +executable documentation build passed with no Sphinx warnings, followed by the +generated-page link check. + +Refresh the device catalogue with these read-only commands; keep AWS CLI +pagination enabled and repeat the lookup for each returned online device ARN: -For #2575, tests retain native acceptance of RX(0.37) while rejecting it as a -synthesis basis. They cover both quarter-turn signs, optional half turns, -symbolic RZ merging and cancellation, parameter dominance, and consistency -between routing costs and emitted pulses. The former six-axis matrix tests are -replaced by 72 full-matrix cases for the supported recipes. Python checks -include bindings as large as 1e300 and the existing -[Qiskit input contract](../../docs/mlir/qiskit.md). +```sh +for region in us-east-1 us-west-1 us-west-2 eu-north-1 eu-west-2; do + aws braket search-devices --profile braket --region "$region" --filters '[]' +done +aws braket get-device --profile braket --region "$region" --device-arn "$device_arn" +``` -After building with the repository release preset, run: +After building the release preset, run: ```sh build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler @@ -168,19 +133,21 @@ build/release/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/mqt-core-mli build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/mqt-core-mlir-unittest-mapping build/release/mlir/unittests/Dialect/QCO/Transforms/Decomposition/mqt-core-mlir-unittest-decomposition build/release/mlir/unittests/Dialect/QCO/Transforms/Optimizations/mqt-core-mlir-unittest-optimizations -QISKIT_NUM_PROCS=1 uv run --no-sync pytest -n 4 test/python/test_mlir.py test/python/test_mlir_qiskit_translation.py +QISKIT_NUM_PROCS=1 uv run --no-sync pytest test/python/test_mlir*.py +uvx nox -s stubs +uvx nox -s cpp-lint uvx nox -s lint +uvx nox --non-interactive -s docs ``` -Run the owning adapter tests for the final payload checks. Follow -[repository guidance](../../AGENTS.md#build-and-validation) for binding stubs -and C++ lint when their implementation changes. - -After rebasing onto the layout support in #2553, local validation passed 254 -compiler, 80 native-synthesis, 126 mapping, 315 decomposition, and 204 -optimization tests, plus all 600 Python MLIR and Qiskit translation tests. The -layout export regression now also checks fixed RX/RZ/iSWAP targets through -placement and routing. Static fixed-pulse compilation and synthesis retain -physical sites and reject nonadjacent interactions. The integration review -required no production changes. Radian ion gates and Braket consumer checks -remain unimplemented follow-ups. +Adapter tests must also inspect native spellings, radians, placements, and +rules. +[devices]: https://docs.aws.amazon.com/braket/latest/developerguide/braket-devices.html +[gates]: https://docs.aws.amazon.com/braket/latest/developerguide/braket-submit-tasks.html +[dynamic]: https://docs.aws.amazon.com/braket/latest/developerguide/braket-experimental-capabilities.html +[verbatim]: https://docs.aws.amazon.com/braket/latest/developerguide/braket-constructing-circuit.html#verbatim-compilation +[qiskit-target]: https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.transpiler.Target +[pennylane-capabilities]: https://github.com/PennyLaneAI/pennylane/blob/master/pennylane/devices/capabilities.py +[pennylane-decomposition]: https://github.com/PennyLaneAI/pennylane/blob/master/pennylane/devices/preprocess.py +[cudaq-backend]: https://nvidia.github.io/cuda-quantum/latest/using/extending/backend.html +[qdk-native]: https://learn.microsoft.com/en-us/azure/quantum/neutral-atom-noise-models diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index 412877104b..28498b00bc 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -10,6 +10,7 @@ #include "mqt/Compiler/Target.h" #include "mqt/Dialect/MQT/IR/QubitLayout.h" +#include "mqt/Dialect/MQT/Utils/Parameters.h" #include "mqt/Dialect/QC/Translation/StandardGate.h" #include "QiskitTranslation.h" @@ -40,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -2221,17 +2223,29 @@ class NativeCircuitWriter final : public CircuitWriter { const std::vector& qubits, const std::vector& parameters) override { const auto* gate = versionGate(mapping); - /// Only P and U3 have alternative names in this Qiskit gate table. - if (target_ != nullptr && gate != nullptr && - (mapping.gate == mlir::qc::StandardGate::P || - mapping.gate == mlir::qc::StandardGate::U3)) { + std::string nativeName; + if (target_ != nullptr && gate != nullptr) { for (const auto& operation : target_->operations()) { - const auto* candidate = versionGate(operation.name()); + const auto* candidate = versionGate(operation.canonicalName()); if (candidate == nullptr || candidate->translation != mapping || !operation.arity().accepts(qubits.size()) || - operation.numParameters() != parameters.size()) { + operation.numParameters() != parameters.size() || + !std::ranges::all_of( + std::views::iota(size_t{0}, operation.fixedParameters().size()), + [&](size_t index) { + const auto expected = operation.fixedParameters()[index]; + const auto* actual = parameters[index].getNumber(); + return !expected || + (actual != nullptr && + std::abs(actual->value - *expected) <= + mlir::mqt::PARAMETER_COMPARISON_TOLERANCE); + })) { continue; } + if (const auto* named = versionGate(operation.name()); + named != nullptr && named->translation == mapping) { + candidate = named; + } if (operation.siteTuples().empty() || std::ranges::any_of(operation.siteTuples(), [&](const auto& tuple) { return std::ranges::equal(tuple.sites(), qubits, {}, {}, @@ -2240,6 +2254,7 @@ class NativeCircuitWriter final : public CircuitWriter { }); })) { gate = candidate; + nativeName = operation.name().str(); break; } } @@ -2257,6 +2272,23 @@ class NativeCircuitWriter final : public CircuitWriter { throw std::runtime_error("Qiskit gate '" + std::string(gate->name) + "' has incompatible arity"); } + if (!nativeName.empty() && nativeName != gate->name) { + if (!standardGates_.is_valid()) { + standardGates_ = nb::module_::import_("qiskit.circuit.library") + .attr("get_standard_gate_name_mapping")(); + } + auto operation = + standardGates_[nb::str(gate->name.data(), gate->name.size())].attr( + "to_mutable")(); + nb::list values; + for (const auto& parameter : parameters) { + values.append(pythonParameter(parameter)); + } + operation.attr("params") = values; + operation.attr("name") = nb::str(nativeName.data(), nativeName.size()); + pythonCircuit_.attr("append")(operation, pythonQubits(qubits)); + return; + } if (std::ranges::all_of(parameters, [](const Parameter& parameter) { return parameter.getNumber() != nullptr; })) { @@ -2279,7 +2311,7 @@ class NativeCircuitWriter final : public CircuitWriter { for (const auto& parameter : parameters) { values.append(pythonParameter(parameter)); } - if (!standardGates_.is_valid()) { + if (!standardInstruction_.is_valid()) { standardGates_ = nb::module_::import_("qiskit.circuit.library") .attr("get_standard_gate_name_mapping")(); standardInstruction_ = nb::module_::import_("qiskit.circuit") @@ -2785,7 +2817,6 @@ class NativeTranslation final : public VersionedTranslation { circuit.attr("Barrier"), circuit.attr("Delay"), circuit.attr("ControlFlowOp"), controlFlow.attr("BreakLoopOp"), controlFlow.attr("ContinueLoopOp"), circuit.attr("Store")); - const auto parameter = circuit.attr("Parameter")("_mqt_target_parameter"); const auto available = target.attr("operation_names"); std::set names; for (auto item : nb::borrow(operationNames.is_none() @@ -2828,6 +2859,7 @@ class NativeTranslation final : public VersionedTranslation { std::string reason; size_t numParameters = 0; + std::vector> fixedParameters; if (nb::isinstance(instruction, circuit.attr("ControlledGate")) && nb::cast(instruction.attr("ctrl_state")) != closedControlState( @@ -2835,14 +2867,26 @@ class NativeTranslation final : public VersionedTranslation { reason = "open controls"; } else if (!nativeName) { reason = "custom or unsupported operation"; - } else if (operationName != *nativeName) { + } else if (operationName != *nativeName && + (*nativeName == "measure" || *nativeName == "reset")) { reason = "custom operation name"; } else { const nb::object parameters = instruction.attr("params"); numParameters = nb::len(parameters); - const auto slots = std::vector(numParameters, parameter); - bool supported = nb::cast(target.attr("instruction_supported")( - operationName, nb::arg("parameters") = slots)); + bool supported = true; + for (auto value : nb::borrow(parameters)) { + if (nb::isinstance(value, circuit.attr("ParameterExpression")) && + nb::len(nb::borrow(value.attr("parameters"))) != 0) { + fixedParameters.emplace_back(std::nullopt); + } else { + double number = 0.0; + if (!nb::try_cast(value, number) || !std::isfinite(number)) { + supported = false; + break; + } + fixedParameters.emplace_back(number); + } + } if (supported && nb::cast(target.attr("gate_has_angle_bounds")( operationName))) { for (const auto bound : { @@ -2889,7 +2933,8 @@ class NativeTranslation final : public VersionedTranslation { } const auto arity = nb::cast(instruction.attr("num_qubits")); auto capability = takeResult(Target::OperationCapability::create( - operationName, arity, numParameters, std::move(placements))); + operationName, arity, numParameters, std::move(placements), + std::nullopt, std::nullopt, std::move(fixedParameters), *nativeName)); if (arity == 2) { if (qargs.is_none()) { allToAll = true; diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 910de9a54f..ee5b2277aa 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -862,7 +862,8 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); siteTuples, const std::optional duration, const std::optional fidelity, - std::vector> fixedParameters) { + std::vector> fixedParameters, + std::optional canonicalName) { constructFromExpected( self, mlir::CompilerTarget::OperationCapability::create( @@ -870,11 +871,13 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity, std::move(fixedParameters))); + duration, fidelity, std::move(fixedParameters), + std::move(canonicalName))); }, "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), "duration"_a = nb::none(), "fidelity"_a = nb::none(), nb::kw_only(), - "fixed_parameters"_a = std::vector>{}) + "fixed_parameters"_a = std::vector>{}, + "canonical_name"_a = nb::none()) .def( "__init__", [](mlir::CompilerTarget::OperationCapability& self, std::string name, @@ -883,7 +886,8 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); siteTuples, const std::optional duration, const std::optional fidelity, - std::vector> fixedParameters) { + std::vector> fixedParameters, + std::optional canonicalName) { constructFromExpected( self, mlir::CompilerTarget::OperationCapability::create( @@ -891,11 +895,13 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity, std::move(fixedParameters))); + duration, fidelity, std::move(fixedParameters), + std::move(canonicalName))); }, "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), "duration"_a = nb::none(), "fidelity"_a = nb::none(), nb::kw_only(), - "fixed_parameters"_a = std::vector>{}) + "fixed_parameters"_a = std::vector>{}, + "canonical_name"_a = nb::none()) .def_prop_ro( "name", [](const mlir::CompilerTarget::OperationCapability& operation) { @@ -965,9 +971,7 @@ 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("FIXED_ROTATION", - mlir::CompilerTarget::SingleQubitBasis::FixedRotation); + .value("ZXZ", mlir::CompilerTarget::SingleQubitBasis::ZXZ); auto synthesisBasis = nb::class_( compilerTarget, "SynthesisBasis", diff --git a/bindings/patterns.txt b/bindings/patterns.txt index 85c1f4c425..7eb7ae43b4 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -140,6 +140,7 @@ mqt\.core\.mlir\.CompilerTarget\.OperationCapability\.__init__$: fidelity: float | None = None, *, fixed_parameters: Sequence[float | None] = (), + canonical_name: str | None = None, ) -> None: \doc diff --git a/docs/mlir/qiskit.md b/docs/mlir/qiskit.md index 09a41b89bd..f2e0ad5b17 100644 --- a/docs/mlir/qiskit.md +++ b/docs/mlir/qiskit.md @@ -25,10 +25,12 @@ target = CompilerTarget.from_qiskit(backend) The result is an independent structural snapshot. A backend supplies the default target name; pass `name` to override it. Qiskit standard gates, measurement, -reset, and their ordered qubit placements become native capabilities. Parameter -slots must accept arbitrary values. The compiler uses canonical gate names; -`to_qiskit(target=target)` emits applicable standard names such as `u1` and `u3` -where the target uses those legacy spellings. +reset, and their ordered qubit placements become native capabilities. Each +parameter slot can be unrestricted or fixed to one finite value. Multiple +instructions of the same gate, such as `RXGate(pi / 2)` and `RXGate(pi)`, retain +their distinct target names and placements. The compiler uses canonical gate +names; `to_qiskit(target=target)` selects the target name by gate, parameters, +and ordered sites. This also preserves legacy spellings such as `u1` and `u3`. Routing connectivity is undirected and comes from the retained two-qubit gates. Gate applicability keeps its original qubit order. The target needs a known, @@ -36,11 +38,11 @@ positive qubit count and a connected routing graph. Global phase is always allowed as circuit metadata. By default, unrepresentable gates are omitted with a warning. This includes -custom gates or names, fixed angles, restricted angle bounds, open controls, and -controlled gates other than CX and CZ. Delay, barrier, classical control flow, -and operations with no applicable qubits are omitted without a warning. Pass -`operation_names` to retain a subset; every selected name must be representable. -Calibration, timing, and scheduling data are not copied. +custom gates, restricted angle bounds, renamed measurement or reset, open +controls, and controlled gates other than CX and CZ. Delay, barrier, classical +control flow, and operations with no applicable qubits are omitted without a +warning. Pass `operation_names` to retain a subset; every selected name must be +representable. Calibration, timing, and scheduling data are not copied. The snapshot does not guarantee that compilation can synthesize the requested circuit. Target-native synthesis requires a common single-qubit basis across diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index d57a173278..ed68b952d7 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -180,31 +180,35 @@ 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. Fixed-pulse synthesis requires unrestricted RZ and -RX(π/2) or RX(-π/2), available on every site. This covers Rigetti's native -single-qubit gates. Available RX(π) or RX(-π) pulses shorten suitable -decompositions. Numeric and symbolic input gates share the same recipe and -preserve global phase. - -Other fixed values, including other dyadic fractions of π, remain valid native -capabilities but do not create additional synthesis bases. They require a -supported basis when a non-native operation needs decomposition. - -Target cleanup preserves gate forms, including sequences of native gates on -targets without a synthesis basis. Operations outside the native set still need -a usable synthesis basis. Numeric single-qubit run fusion uses the fixed-pulse -recipe and keeps native runs unless it can shorten them. Symbolic fixed-pulse -runs use individual gate lowering, with adjacent unrestricted RZ operations -merged before and after synthesis. Two-qubit run fusion remains available. -Routing costs count native two-qubit gates, not the number of fixed pulses. -Constraints on other gate families are checked, but do not create additional -synthesis bases. Parameter ranges and relations are not supported. +`fixed_parameters` constrains individual parameters to finite constants. A +nonempty list has one entry per parameter; `None` leaves a parameter +unrestricted. An omitted or empty list leaves every parameter unrestricted. +Multiple capabilities for the same operation form a union of supported values +and placements. For example, these capabilities accept four RX angles: + +```python +from math import pi + +rx_pulses = [ + CompilerTarget.OperationCapability("rx", 1, 1, fixed_parameters=[angle]) for angle in (pi / 2, -pi / 2, pi, -pi) +] +``` + +Constants match with absolute tolerance `1e-15`, without angle wrapping. Unbound +symbolic values cannot satisfy fixed parameters. Parameter ranges and relations +are not represented. A device-specific instruction name can specify its compiler +operation with `canonical_name`, for example +`OperationCapability("rx_90", 1, 1, fixed_parameters=[pi / 2], canonical_name="rx")`. +The reported name remains available to exporters. + +Target compilation requires a single-qubit synthesis basis available on every +site. The `ZSXX` basis uses unrestricted RZ and either SX or fixed RX(π/2) or +RX(-π/2). X or fixed RX(±π) provides an optional shorter half-turn sequence. The +compiler selects supported pulses from the capabilities and preserves the +global-phase difference between named X/SX gates and RX rotations. Additional +fixed angles remain valid capabilities without changing the synthesis recipe. +Targets without a usable basis support capability queries and conformance +checks, but cannot be passed to target compilation or target synthesis. Use plain tuples for placements without calibration. Use `CompilerTarget.SiteTuple([1, 0], duration=40, fidelity=0.99)` to attach @@ -217,8 +221,7 @@ preserve the entry sites. Unsupported or inconsistent site transfers are diagnosed, including after all-to-all placement. A synthesis basis must provide the same one-qubit gate family on every site. Its entangler is optional: one-qubit synthesis does not need one. Two-qubit synthesis requires an entangler -on every routing edge in at least one direction. A native operation does not -need a synthesis basis. +on every routing edge in at least one direction. Mapping explores one initial-layout trial per available logical CPU by default, using LLVM's affinity-aware CPU count with a minimum of one. An explicit diff --git a/mlir/include/mqt/Compiler/Target.h b/mlir/include/mqt/Compiler/Target.h index 6087099004..9ec7ab58fc 100644 --- a/mlir/include/mqt/Compiler/Target.h +++ b/mlir/include/mqt/Compiler/Target.h @@ -154,8 +154,9 @@ class CompilerTarget { /// An operation capability described by a target. /// - /// The reported name is retained verbatim while - /// @ref canonicalName contains its normalized compiler spelling. Operations + /// The reported name is retained verbatim while @ref canonicalName identifies + /// its compiler operation. By default it is derived from the reported name; + /// an explicit override describes named instances such as rx_90. Operations /// with no site tuples are generally applicable. A nonempty list gives all /// supported ordered placements. Missing tuple calibration values inherit /// the operation defaults. @@ -197,7 +198,8 @@ class CompilerTarget { std::vector siteTuples = {}, std::optional duration = std::nullopt, std::optional fidelity = std::nullopt, - std::vector> fixedParameters = {}); + std::vector> fixedParameters = {}, + std::optional canonicalName = std::nullopt); /// Create a validated operation capability. [[nodiscard]] static llvm::Expected @@ -205,12 +207,13 @@ class CompilerTarget { std::vector siteTuples = {}, std::optional duration = std::nullopt, std::optional fidelity = std::nullopt, - std::vector> fixedParameters = {}); + std::vector> fixedParameters = {}, + std::optional canonicalName = std::nullopt); /// Return the exact reported operation name. [[nodiscard]] llvm::StringRef name() const noexcept; - /// Return the canonical lower-case compiler operation name. + /// Return the compiler operation name, independently of its reported name. [[nodiscard]] llvm::StringRef canonicalName() const noexcept; /// Return the accepted operation arity. @@ -303,30 +306,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(λ)`. - FixedRotation, ///< `RZ` with fixed `RX(±π/2)` and optional `RX(±π)`. + U, ///< `U(θ, φ, λ)`. + ZSXX, ///< RZ and X quarter turns, with optional X half turns. + R, ///< XYX synthesis expressed with `R(θ, φ)`. + XZX, ///< `RX(φ) * RZ(θ) * RX(λ)`. + XYX, ///< `RX(φ) * RY(θ) * RX(λ)`. + ZYZ, ///< `RZ(φ) * RY(θ) * RZ(λ)`. + ZXZ, ///< `RZ(φ) * RX(θ) * RZ(λ)`. }; - /// Fixed RX pulses combined with unrestricted RZ rotations. - struct FixedRotationBasis { + /// Native RX pulses implementing the X rotations of the ZSXX basis. + struct RXPulses { double quarterTurnAngle; std::optional halfTurnAngle; - friend bool operator==(const FixedRotationBasis&, - const FixedRotationBasis&) = default; + friend bool operator==(const RXPulses&, const RXPulses&) = default; }; /// One single-qubit basis and optional entangler usable across the target. struct SynthesisBasis { SingleQubitBasis singleQubit; std::optional entangler; - std::optional fixedRotation; + std::optional rxPulses; + /// Whether the named X shortcut is available when using native SX gates. + bool hasX = true; friend bool operator==(const SynthesisBasis&, const SynthesisBasis&) = default; diff --git a/mlir/include/mqt/Compiler/TargetCompilation.h b/mlir/include/mqt/Compiler/TargetCompilation.h index 7b627edf14..e802d91979 100644 --- a/mlir/include/mqt/Compiler/TargetCompilation.h +++ b/mlir/include/mqt/Compiler/TargetCompilation.h @@ -26,6 +26,8 @@ class OpPassManager; /// register inliner extensions for its callable dialects. /// Input must use structured QCO/SCF control flow. Normalize CFG branches /// before calling this pipeline. Runtime assertions are allowed. +/// Explicit native-operation targets must provide a single-qubit synthesis +/// basis so native synthesis can lower the results of standard cleanup. /// The supplied environment is authoritative: the pipeline attaches it to the /// module and shares its prepared target with every target-dependent pass. /// The environment must remain unchanged during pipeline execution. @@ -43,7 +45,8 @@ void populateTargetCompilationPipeline(OpPassManager& pm, /// two-qubit runs in the native basis, and verifies target conformance. Input /// must use structured QCO/SCF control flow. The supplied environment is /// authoritative and must remain unchanged during pipeline execution. -/// Use runWithCompilationOptions to apply compilation-wide seed and +/// Explicit native-operation targets must provide a single-qubit synthesis +/// basis. Use runWithCompilationOptions to apply compilation-wide seed and /// instrumentation settings when running this pipeline. void populateTargetSynthesisPipeline(OpPassManager& pm, const TargetEnvironment& environment, diff --git a/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td index f5b63b2da8..999f846b3d 100644 --- a/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td @@ -214,6 +214,8 @@ def NativeOperationAttr : MQTAttr<"NativeOperation", "native_operation"> { general applicability; a nonempty list gives all supported ordered placements. Optional `fixed_parameters` contains one entry per parameter: a finite f64 value for a fixed parameter, or `unit` for an unrestricted one. + Optional `canonical_name` identifies the compiler operation when the + reported name is a device-specific instruction name. The following example records a directional controlled-X operation: ```mlir @@ -227,7 +229,8 @@ def NativeOperationAttr : MQTAttr<"NativeOperation", "native_operation"> { MQTArrayRefParameter<"SiteTupleAttr">:$site_tuples, MQTOptionalUInt64Parameter<>:$duration, OptionalParameter<"FloatAttr">:$fidelity, - OptionalParameter<"ArrayAttr">:$fixed_parameters); + OptionalParameter<"ArrayAttr">:$fixed_parameters, + OptionalParameter<"StringAttr">:$canonical_name); let assemblyFormat = "`<` struct(params) `>`"; let genVerifyDecl = 1; } diff --git a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h index 52b190090d..ca689e1a6a 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -11,16 +11,13 @@ #pragma once #include "mqt/Compiler/Target.h" -#include "mqt/Dialect/MQT/Utils/Parameters.h" #include "mqt/Dialect/QCO/Utils/Matrix.h" #include "mlir/IR/Builders.h" #include "mlir/IR/Location.h" #include "mlir/Support/LLVM.h" -#include #include -#include #include namespace mlir { @@ -63,10 +60,8 @@ struct SynthesizedUnitary1Q { }; /// Returns whether @p op belongs to @p basis. -/// Fixed-pulse bases require their target's @p fixedRotation descriptor. -[[nodiscard]] bool isSingleQubitBasisGate( - Operation* op, SingleQubitBasis basis, - const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr); +[[nodiscard]] bool isSingleQubitBasisGate(Operation* op, + SingleQubitBasis basis); /// Extracts `(theta, phi, lambda, phase)` of @p matrix in @p basis. /// @@ -94,7 +89,7 @@ struct SynthesizedUnitary1Q { [[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); + const CompilerTarget::SynthesisBasis* targetBasis = nullptr); /// Materializes one accumulated phase correction when needed. /// @@ -109,12 +104,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. +/// The optional target basis selects native pulse forms and shortcuts. /// /// @pre `canSynthesizeParameterizedUnitary1Q(op)` is true. void synthesizeParameterizedUnitary1Q( RewriterBase& rewriter, Operation* op, SingleQubitBasis basis, - const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr); + const CompilerTarget::SynthesisBasis* targetBasis = nullptr); /// Populates @p patterns with the single-qubit run fusion rewrite for /// @p basis (the reusable core of `fuse-single-qubit-unitary-runs`). @@ -131,50 +126,9 @@ void populateFuseSingleQubitUnitaryRunsPatterns( /// /// The patterns emit @p basis directly. With @p target, preserve native runs /// and only use direct Euler identities, keeping optional fusion exportable. -/// Fixed-RX bases only merge adjacent RZ operations. +/// ZSXX targets only merge adjacent RZ operations symbolically. void populateParameterizedSingleQubitRunCompositionPatterns( RewritePatternSet& patterns, SingleQubitBasis basis, const CompilerTarget* target = nullptr); -namespace detail { - -/// Emit ZYZ angles using RZ and fixed RX 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 double offset = basis.quarterTurnAngle < 0. ? pi : 0.; - if (matchesTheta(halfPi)) { - emitFree(lambda + constant(offset - halfPi)); - emitPulse(basis.quarterTurnAngle); - emitFree(phi + constant(halfPi - offset)); - return 0.; - } - if (matchesTheta(pi) && basis.halfTurnAngle) { - emitFree(lambda); - emitPulse(*basis.halfTurnAngle); - emitFree(phi + constant(pi)); - return *basis.halfTurnAngle < 0. ? pi : 0.; - } - emitFree(lambda + constant(offset)); - emitPulse(basis.quarterTurnAngle); - emitFree(theta + constant(pi)); - emitPulse(basis.quarterTurnAngle); - emitFree(phi + constant(pi - offset)); - return pi; -} - -} // namespace detail - } // namespace mlir::qco::decomposition diff --git a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h index 49f790701e..15b745ab25 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h @@ -38,15 +38,6 @@ namespace mlir::qco { #define GEN_PASS_REGISTRATION #include "mqt/Dialect/QCO/Transforms/Passes.h.inc" // IWYU pragma: export -/// Canonicalize structure while preserving explicitly supported gate forms. -/// Unrestricted targets use the standard MLIR canonicalizer. -[[nodiscard]] std::unique_ptr -createTargetCanonicalizer(const CompilerTarget& target); - -/// Inline functions using the target's canonicalization policy. -[[nodiscard]] std::unique_ptr -createTargetInliner(const CompilerTarget& target); - /// Create target-independent two-qubit gate fusion. /// /// Runs remain unchanged when numerical decomposition fails. @@ -67,7 +58,6 @@ createDecomposeMultiControlled(const CompilerTarget& target, /// Normalize placed QCO, synthesize native gates, and verify target support. /// Requires a valid target environment and placed qubits. -void populateTargetNativeSynthesisPipeline(OpPassManager& pm, - const CompilerTarget& target); +void populateTargetNativeSynthesisPipeline(OpPassManager& pm); } // namespace mlir::qco diff --git a/mlir/include/mqt/Support/Passes.h b/mlir/include/mqt/Support/Passes.h index 7b01ea2a9f..143b146d40 100644 --- a/mlir/include/mqt/Support/Passes.h +++ b/mlir/include/mqt/Support/Passes.h @@ -12,11 +12,9 @@ #include "mqt/Compiler/CompilationOptions.h" -#include "mlir/Pass/Pass.h" #include "mlir/Support/LLVM.h" #include -#include namespace mlir { class ModuleOp; @@ -77,11 +75,8 @@ void populateQCExportPipeline(mlir::OpPassManager& pm); /// Populate a QCO-oriented cleanup pipeline on the given pass manager. /// -/// Adds canonicalization, qtensor shrink-to-fit, and dead-value removal. -/// A null canonicalizer selects the standard MLIR canonicalizer. -void populateQCOCleanupPipeline( - mlir::OpPassManager& pm, - std::unique_ptr canonicalizer = nullptr); +/// Adds generic cleanup, qtensor shrink-to-fit, and dead-value removal. +void populateQCOCleanupPipeline(mlir::OpPassManager& pm); /// Populate a QIR-oriented cleanup pipeline on the given pass manager. /// diff --git a/mlir/lib/Compiler/Pipeline.cpp b/mlir/lib/Compiler/Pipeline.cpp index 7af6c896f3..ea7a198b74 100644 --- a/mlir/lib/Compiler/Pipeline.cpp +++ b/mlir/lib/Compiler/Pipeline.cpp @@ -131,9 +131,9 @@ std::optional QCProgram::intoQIR(QIRProfile profile) && { //===----------------------------------------------------------------------===// bool QCOProgram::cleanup() { - return succeeded(runQCOTransformPasses( - mod(), [](OpPassManager& pm) { populateQCOCleanupPipeline(pm); }, - "failed to run the QCO cleanup pipeline")); + return succeeded( + runQCOTransformPasses(mod(), populateQCOCleanupPipeline, + "failed to run the QCO cleanup pipeline")); } bool QCOProgram::normalizeGlobalPhases() { diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 038ecbaf5d..4cc6339415 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -384,10 +384,11 @@ CompilerTarget::OperationCapability::create( std::string name, size_t arity, size_t numParameters, std::vector siteTuples, std::optional duration, std::optional fidelity, - std::vector> fixedParameters) { + std::vector> fixedParameters, + std::optional canonicalName) { return create(std::move(name), Arity::fixed(arity), numParameters, std::move(siteTuples), duration, fidelity, - std::move(fixedParameters)); + std::move(fixedParameters), std::move(canonicalName)); } llvm::Expected @@ -395,11 +396,17 @@ 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()) { + std::vector> fixedParameters, + std::optional canonicalName) { + if (canonicalOperationName(name).empty()) { return invalidTarget("Compiler target operation name must not be empty"); } + auto canonical = + canonicalOperationName(std::move(canonicalName).value_or(name)); + if (canonical.empty()) { + return invalidTarget( + "Compiler target canonical operation name must not be empty"); + } if (auto error = validateFidelity(fidelity, "Compiler target operation fidelity")) { return std::move(error); @@ -444,7 +451,7 @@ CompilerTarget::OperationCapability::create( } } - return OperationCapability(std::move(name), std::move(canonicalName), arity, + return OperationCapability(std::move(name), std::move(canonical), arity, numParameters, std::move(siteTuples), duration, fidelity, std::move(fixedParameters)); } @@ -798,13 +805,14 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { }); }; std::optional singleQubit; - std::optional fixedRotation; + std::optional rxPulses; + bool hasX = true; if (supportsOnEverySite(GateKind::U)) { singleQubit = SingleQubitBasis::U; - } else if (supportsOnEverySite(GateKind::X) && - supportsOnEverySite(GateKind::SX) && + } else if (supportsOnEverySite(GateKind::SX) && supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZSXX; + hasX = supportsOnEverySite(GateKind::X); } else if (supportsOnEverySite(GateKind::R)) { singleQubit = SingleQubitBasis::R; } else if (supportsOnEverySite(GateKind::RX) && @@ -828,17 +836,17 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { if (!supportsPulse(quarter)) { continue; } - fixedRotation = { + rxPulses = { .quarterTurnAngle = quarter, .halfTurnAngle = std::nullopt, }; for (double half : {std::numbers::pi, -std::numbers::pi}) { if (supportsPulse(half)) { - fixedRotation->halfTurnAngle = half; + rxPulses->halfTurnAngle = half; break; } } - singleQubit = SingleQubitBasis::FixedRotation; + singleQubit = SingleQubitBasis::ZSXX; break; } } @@ -897,7 +905,8 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { .entangler = entangler == entanglerPreference.end() ? std::nullopt : std::optional{*entangler}, - .fixedRotation = fixedRotation, + .rxPulses = rxPulses, + .hasX = hasX, }; } @@ -1060,7 +1069,10 @@ CompilerTarget::create(const mqt::CompilationTargetAttr attribute) { operationAttr.getName().getValue().str(), arity, static_cast(operationAttr.getNumParameters()), std::move(siteTuples), operationAttr.getDuration(), fidelity, - std::move(fixedParameters)); + std::move(fixedParameters), + operationAttr.getCanonicalName() + ? std::optional{operationAttr.getCanonicalName().getValue().str()} + : std::nullopt); if (!operation) { return operation.takeError(); } @@ -1405,7 +1417,10 @@ CompilerTarget::materialize(MLIRContext& context) const { operationAttrs.emplace_back(mqt::NativeOperationAttr::get( &context, builder.getStringAttr(operation.name()), arityAttr, operation.numParameters(), siteTupleAttrs, operation.duration(), - fidelityAttr, fixedParameters)); + fidelityAttr, fixedParameters, + operation.canonicalName() != canonicalOperationName(operation.name()) + ? builder.getStringAttr(operation.canonicalName()) + : StringAttr{})); } const auto connectivity = connectivityKind() == Connectivity::Kind::AllToAll diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index 0d2a888154..d823b3ffed 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -28,6 +28,7 @@ #include "mlir/Pass/Pass.h" #include "mlir/Pass/PassManager.h" #include "mlir/Support/WalkResult.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" #include "mlir/Transforms/Passes.h" #include "llvm/ADT/STLExtras.h" @@ -128,6 +129,15 @@ class PrepareTargetCompilationPass signalPassFailure(); return; } + const auto& target = environment_.target(); + if (target.nativeOperationsKind() == + CompilerTarget::NativeOperations::Kind::Explicit && + !target.synthesisBasis()) { + getOperation().emitError( + "target compilation requires a single-qubit synthesis basis"); + signalPassFailure(); + return; + } if (failed(mqt::verifyQuantumAllocations(getOperation()))) { signalPassFailure(); return; @@ -193,14 +203,15 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(std::make_unique(environment, false, mapping)); const auto& target = environment.target(); - pm.addPass(qco::createTargetInliner(target)); + pm.addPass(createInlinerPass()); pm.addPass(createSymbolDCEPass()); pm.addPass(createSCCPPass()); - populateQCOCleanupPipeline(pm, qco::createTargetCanonicalizer(target)); + populateQCOCleanupPipeline(pm); pm.addPass(qco::createUnrollLoopsForPayload()); pm.addPass(createSCCPPass()); /// Unrolling exposes static tensor slots and unreachable callees. - pm.addPass(qco::createTargetCanonicalizer(target)); + pm.addPass(createCanonicalizerPass( + GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit))); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); pm.addPass(createSymbolDCEPass()); @@ -209,8 +220,8 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(qco::createFuseTwoQubitGates(target)); /// Non-U targets fuse during native synthesis, avoiding an intermediate U /// representation and its symbolic phase correction. - if (const auto& basis = target.synthesisBasis(); - basis && basis->singleQubit == CompilerTarget::SingleQubitBasis::U) { + if (const auto basis = target.synthesisBasis(); + !basis || basis->singleQubit == CompilerTarget::SingleQubitBasis::U) { /// The U optimizer also merges dynamic controlled bodies into native U /// gates and preserves isolated gates. Native synthesis does not yet cover /// both behaviors; keep this path until their synthesis contracts agree. @@ -232,7 +243,7 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(qco::createPlacementPass(target)); break; } - qco::populateTargetNativeSynthesisPipeline(pm, target); + qco::populateTargetNativeSynthesisPipeline(pm); } void populateTargetSynthesisPipeline(OpPassManager& pm, @@ -241,14 +252,14 @@ void populateTargetSynthesisPipeline(OpPassManager& pm, pm.addPass(std::make_unique(environment, true, mapping)); const auto& target = environment.target(); - pm.addPass(qco::createTargetInliner(target)); + pm.addPass(createInlinerPass()); pm.addPass(createSymbolDCEPass()); - populateQCOCleanupPipeline(pm, qco::createTargetCanonicalizer(target)); + populateQCOCleanupPipeline(pm); pm.addPass(qco::createLegalizeControlFlow()); pm.addPass(qco::createDecomposeMultiControlled(target)); pm.addPass(qco::createFuseTwoQubitGates(target)); pm.addPass(qco::createPlacementPass(target)); - qco::populateTargetNativeSynthesisPipeline(pm, target); + qco::populateTargetNativeSynthesisPipeline(pm); } } // namespace mlir diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 40e8bf5895..7d867548b1 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -253,10 +253,14 @@ LogicalResult NativeOperationAttr::verify( const OperationArityAttr arity, const uint64_t numParameters, const ArrayRef siteTuples, const std::optional /*duration*/, const FloatAttr fidelity, - const ArrayAttr fixedParameters) { + const ArrayAttr fixedParameters, const StringAttr canonicalName) { if (name.getValue().trim().empty()) { return emitError() << "compiler target operation name must not be empty"; } + if (canonicalName && canonicalName.getValue().trim().empty()) { + return emitError() + << "compiler target canonical operation name must not be empty"; + } if (failed(verifyFidelity(emitError, fidelity, "compiler target operation fidelity"))) { return failure(); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 79d218559d..e99307996c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -23,7 +23,6 @@ #include "llvm/ADT/TypeSwitch.h" #include "llvm/Support/ErrorHandling.h" -#include #include #include #include @@ -33,25 +32,7 @@ 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"); - if (isa(op)) { - return true; - } - if (auto rotation = dyn_cast(op)) { - const auto angle = mqt::valueToDouble(rotation.getTheta()); - return angle && (std::abs(*angle - fixedRotation->quarterTurnAngle) <= - mqt::PARAMETER_COMPARISON_TOLERANCE || - (fixedRotation->halfTurnAngle && - std::abs(*angle - *fixedRotation->halfTurnAngle) <= - mqt::PARAMETER_COMPARISON_TOLERANCE)); - } - return false; - } +bool isSingleQubitBasisGate(Operation* op, SingleQubitBasis basis) { return TypeSwitch(op) .Case([&](RZOp) { return basis == SingleQubitBasis::ZYZ || @@ -212,7 +193,6 @@ EulerAngles anglesFromUnitary(const Matrix2x2& matrix, switch (basis) { case SingleQubitBasis::ZYZ: case SingleQubitBasis::ZSXX: - case SingleQubitBasis::FixedRotation: return paramsZYZ(matrix); case SingleQubitBasis::ZXZ: return paramsZXZ(matrix); @@ -280,9 +260,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, - const CompilerTarget::FixedRotationBasis* fixedRotation) { + void appendDecomposition(const EulerAngles& angles, + const SingleQubitBasis basis, + const CompilerTarget::SynthesisBasis* targetBasis) { if (isNearZeroRotationAngle(angles.theta) && isNearZeroRotationAngle(angles.phi) && isNearZeroRotationAngle(angles.lambda)) { @@ -295,7 +275,6 @@ struct Unitary1QEulerPlan { case SingleQubitBasis::ZYZ: case SingleQubitBasis::ZXZ: case SingleQubitBasis::ZSXX: - case SingleQubitBasis::FixedRotation: appendRotation(SynthesisStep::Kind::RZ, angles.phi + angles.lambda); break; @@ -352,45 +331,54 @@ struct Unitary1QEulerPlan { angles.lambda); phase = angles.phase; break; - case SingleQubitBasis::FixedRotation: { - assert(fixedRotation && - "fixed-pulse synthesis requires a pulse descriptor"); - phase = angles.phase + - detail::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(SynthesisStep::Kind::RX, angle); - }); - break; - } case SingleQubitBasis::ZSXX: { constexpr double pi = std::numbers::pi; constexpr double halfPi = std::numbers::pi / 2.0; constexpr double quarterPi = std::numbers::pi / 4.0; + const auto* pulses = targetBasis != nullptr && targetBasis->rxPulses + ? &*targetBasis->rxPulses + : nullptr; + const double offset = + pulses != nullptr && pulses->quarterTurnAngle < 0. ? pi : 0.; + const double pulsePhase = pulses != nullptr ? 0. : quarterPi; + const auto quarterTurn = [&] { + if (pulses != nullptr) { + steps.emplace_back(SynthesisStep::Kind::RX, pulses->quarterTurnAngle); + } else { + steps.emplace_back(SynthesisStep::Kind::SX); + } + }; if (isNearZeroRotationAngle(angles.theta - halfPi)) { - appendRotation(SynthesisStep::Kind::RZ, angles.lambda - halfPi); - steps.emplace_back(SynthesisStep::Kind::SX); - appendRotation(SynthesisStep::Kind::RZ, angles.phi + halfPi); - phase = angles.phase - quarterPi; + appendRotation(SynthesisStep::Kind::RZ, + angles.lambda + offset - halfPi); + quarterTurn(); + appendRotation(SynthesisStep::Kind::RZ, angles.phi + halfPi - offset); + phase = angles.phase - pulsePhase; return; } - appendRotation(SynthesisStep::Kind::RZ, angles.lambda); - if (isNearZeroRotationAngle(angles.theta - pi)) { - steps.emplace_back(SynthesisStep::Kind::X); - phase = angles.phase - halfPi; - } else { - steps.emplace_back(SynthesisStep::Kind::SX); - appendRotation(SynthesisStep::Kind::RZ, angles.theta + pi); - steps.emplace_back(SynthesisStep::Kind::SX); - phase = angles.phase + halfPi; + const bool hasHalfTurn = + pulses != nullptr ? pulses->halfTurnAngle.has_value() + : targetBasis == nullptr || targetBasis->hasX; + if (hasHalfTurn && isNearZeroRotationAngle(angles.theta - pi)) { + appendRotation(SynthesisStep::Kind::RZ, angles.lambda); + if (pulses != nullptr) { + steps.emplace_back(SynthesisStep::Kind::RX, *pulses->halfTurnAngle); + phase = angles.phase + (*pulses->halfTurnAngle < 0. ? pi : 0.); + } else { + steps.emplace_back(SynthesisStep::Kind::X); + phase = angles.phase - halfPi; + } + appendRotation(SynthesisStep::Kind::RZ, angles.phi + pi); + return; } - appendRotation(SynthesisStep::Kind::RZ, angles.phi + pi); + appendRotation(SynthesisStep::Kind::RZ, angles.lambda + offset); + quarterTurn(); + appendRotation(SynthesisStep::Kind::RZ, angles.theta + pi); + quarterTurn(); + appendRotation(SynthesisStep::Kind::RZ, angles.phi + pi - offset); + phase = angles.phase + pi - 2. * pulsePhase; break; } } @@ -406,14 +394,14 @@ struct Unitary1QEulerPlan { /// @return Planned gate sequence and optional global phase. [[nodiscard]] static Unitary1QEulerPlan planUnitary1QEuler(const Matrix2x2& targetMatrix, const SingleQubitBasis basis, - const CompilerTarget::FixedRotationBasis* fixedRotation) { + const CompilerTarget::SynthesisBasis* targetBasis) { Unitary1QEulerPlan plan; if (targetMatrix.isApprox(Matrix2x2::identity())) { return plan; } const EulerAngles angles = anglesFromUnitary(targetMatrix, basis); - plan.appendDecomposition(angles, basis, fixedRotation); + plan.appendDecomposition(angles, basis, targetBasis); return plan; } @@ -469,13 +457,14 @@ std::optional parseSingleQubitBasis(StringRef basis) { .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 CompilerTarget::FixedRotationBasis* fixedRotation) { +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::SynthesisBasis* targetBasis) { const Unitary1QEulerPlan plan = - planUnitary1QEuler(composed, basis, fixedRotation); + planUnitary1QEuler(composed, basis, targetBasis); 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 f0ee7d4066..9c0ba0611f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -921,8 +921,7 @@ 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, - basis.fixedRotation ? &*basis.fixedRotation : nullptr); + /*hasNonBasisGate=*/true, basis.singleQubit, &basis); wire = synthesized->qubit; globalPhase += synthesized->globalPhase; }; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 563ce43215..914f8b5bd4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -155,16 +155,12 @@ struct FuseSingleQubitUnitaryRunsPattern final } FusableRunScan run = scanFusableRun(op, *headMatrix, basis, target); - const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr; - if (target != nullptr) { - const auto& targetBasis = target->synthesisBasis(); - if (targetBasis && targetBasis->fixedRotation) { - fixedRotation = &*targetBasis->fixedRotation; - } - } + const auto* targetBasis = target != nullptr && target->synthesisBasis() + ? &*target->synthesisBasis() + : nullptr; const auto synthesized = decomposition::synthesizeUnitary1QEuler( rewriter, op.getLoc(), op.getInputQubit(0), run.composed, run.gateCount, - run.hasNonBasisGate, basis, fixedRotation); + run.hasNonBasisGate, basis, targetBasis); if (!synthesized) { return failure(); } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 16493de3d4..619226d11a 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -30,21 +30,18 @@ #include "mlir/IR/Builders.h" #include "mlir/IR/BuiltinOps.h" #include "mlir/IR/BuiltinTypes.h" -#include "mlir/IR/Dialect.h" #include "mlir/IR/DialectRegistry.h" #include "mlir/IR/IRMapping.h" #include "mlir/IR/Iterators.h" #include "mlir/IR/MLIRContext.h" #include "mlir/IR/Matchers.h" #include "mlir/IR/Operation.h" -#include "mlir/IR/OperationSupport.h" #include "mlir/IR/PatternMatch.h" #include "mlir/IR/Value.h" #include "mlir/IR/Visitors.h" #include "mlir/Interfaces/FunctionInterfaces.h" #include "mlir/Pass/Pass.h" #include "mlir/Pass/PassManager.h" -#include "mlir/Rewrite/FrozenRewritePatternSet.h" #include "mlir/Support/LLVM.h" #include "mlir/Support/LogicalResult.h" #include "mlir/Support/TypeID.h" @@ -1095,14 +1092,12 @@ static LogicalResult synthesizeTargetOperation( "its unitary matrix is not available at compile time"); } decomposition::synthesizeParameterizedUnitary1Q( - rewriter, operation, basis->singleQubit, - basis->fixedRotation ? &*basis->fixedRotation : nullptr); + rewriter, operation, basis->singleQubit, &*basis); return success(); } const auto synthesized = decomposition::synthesizeUnitary1QEuler( rewriter, operation->getLoc(), op.getInputQubit(0), matrix, - /*runSize=*/1, /*hasNonBasisGate=*/true, basis->singleQubit, - basis->fixedRotation ? &*basis->fixedRotation : nullptr); + /*runSize=*/1, /*hasNonBasisGate=*/true, basis->singleQubit, &*basis); if (!synthesized) { llvm::reportFatalInternalError( "target single-qubit basis failed to synthesize a unitary matrix"); @@ -1431,8 +1426,8 @@ struct TargetNativeSynthesisPass final signalPassFailure(); return; } - if (targetBasis && targetBasis->singleQubit == - CompilerTarget::SingleQubitBasis::FixedRotation) { + if (targetBasis && + targetBasis->singleQubit == CompilerTarget::SingleQubitBasis::ZSXX) { RewritePatternSet patterns(&getContext()); decomposition::populateParameterizedSingleQubitRunCompositionPatterns( patterns, targetBasis->singleQubit, &target); @@ -1514,39 +1509,6 @@ struct VerifyTargetConformancePass final } }; -/// Gate-changing canonicalization can leave a target's native gate set. -class CanonicalizeStructurePass final - : public PassWrapper> { -public: - MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(CanonicalizeStructurePass) - -protected: - LogicalResult initialize(MLIRContext* context) override { - RewritePatternSet patterns(context); - for (auto* dialect : context->getLoadedDialects()) { - dialect->getCanonicalizationPatterns(patterns); - } - for (auto operation : context->getRegisteredOperations()) { - if (!operation.hasInterface()) { - operation.getCanonicalizationPatterns(patterns, context); - } - } - patterns_ = FrozenRewritePatternSet(std::move(patterns)); - return success(); - } - - void runOnOperation() override { - if (failed(applyPatternsGreedily(getOperation(), patterns_, - GreedyRewriteConfig{}.setMaxIterations( - GreedyRewriteConfig::kNoLimit)))) { - signalPassFailure(); - } - } - -private: - FrozenRewritePatternSet patterns_; -}; - } // namespace std::unique_ptr createFuseTwoQubitGates() { @@ -1557,29 +1519,10 @@ std::unique_ptr createFuseTwoQubitGates(const CompilerTarget& target) { return std::make_unique(target); } -std::unique_ptr createTargetCanonicalizer(const CompilerTarget& target) { - if (target.nativeOperationsKind() == - CompilerTarget::NativeOperations::Kind::Unrestricted) { - return createCanonicalizerPass( - GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit)); - } - return std::make_unique(); -} - -std::unique_ptr createTargetInliner(const CompilerTarget& target) { - if (target.nativeOperationsKind() == - CompilerTarget::NativeOperations::Kind::Unrestricted) { - return createInlinerPass(); - } - return createInlinerPass({}, [](OpPassManager& nested) { - nested.addPass(std::make_unique()); - }); -} - -void populateTargetNativeSynthesisPipeline(OpPassManager& pm, - const CompilerTarget& target) { +void populateTargetNativeSynthesisPipeline(OpPassManager& pm) { /// Placement consumes allocations; native synthesis normalizes phases. - pm.addPass(createTargetCanonicalizer(target)); + pm.addPass(createCanonicalizerPass( + GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit))); /// Reuse unchanged classical reads before native synthesis splits their uses. pm.addPass(createCSEPass()); pm.addPass(createRemoveDeadValuesPass()); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index b71f1cde0c..40bc8ff945 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -740,7 +740,7 @@ static Val sumAngles(Val lhs, Val rhs) { return lhs + rhs; } -/// Merge only the unrestricted axis of a fixed-RX target. +/// Merge the unrestricted RZ axis without changing a target's X pulses. static LogicalResult mergeParameterizedRZ(RZOp op, PatternRewriter& rewriter) { auto next = dyn_cast(*op.getQubitOut().user_begin()); if (!next || next->getBlock() != op->getBlock() || @@ -793,14 +793,12 @@ static Value emitRuntimeEulerAngles( RewriterBase& rewriter, Location loc, Value qubit, RuntimeEulerAngles angles, decomposition::SingleQubitBasis basis, const ScalarConsts& consts, - const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr) { + const CompilerTarget::SynthesisBasis* targetBasis = 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::FixedRotation; + 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)); @@ -836,45 +834,59 @@ static Value emitRuntimeEulerAngles( qubit = UOp::create(rewriter, loc, qubit, theta.v, phi.v, lambda.v) .getQubitOut(); break; - case decomposition::SingleQubitBasis::FixedRotation: { - assert(fixedRotation && - "fixed-pulse synthesis requires a pulse descriptor"); + case decomposition::SingleQubitBasis::ZSXX: { + constexpr double pi = std::numbers::pi; + constexpr double halfPi = pi / 2.; + const auto* pulses = targetBasis != nullptr && targetBasis->rxPulses + ? &*targetBasis->rxPulses + : nullptr; + const double offset = + pulses != nullptr && pulses->quarterTurnAngle < 0. ? pi : 0.; + const double pulsePhase = pulses != nullptr ? 0. : pi / 4.; const auto constant = [&](double value) { return Val::constant(rewriter, loc, value); }; - const double correction = decomposition::detail::emitFixedRotationSequence( - *fixedRotation, theta, phi, lambda, mqt::valueToConstantDouble(theta.v), - constant, - [&](Val angle) { - qubit = emitRotationIfNeeded(rewriter, loc, qubit, angle); - }, - [&](double angle) { - qubit = RXOp::create(rewriter, loc, qubit, angle).getQubitOut(); - }); - if (correction != 0.) { - phase = phase + constant(correction); + const auto quarterTurn = [&] { + qubit = pulses + ? RXOp::create(rewriter, loc, qubit, pulses->quarterTurnAngle) + .getQubitOut() + : SXOp::create(rewriter, loc, qubit).getQubitOut(); + }; + if (isConstantAngle(theta, halfPi)) { + qubit = emitRotationIfNeeded(rewriter, loc, qubit, + lambda + constant(offset - halfPi)); + quarterTurn(); + qubit = emitRotationIfNeeded(rewriter, loc, qubit, + phi + constant(halfPi - offset)); + phase = phase - constant(pulsePhase); + break; } - break; - } - case decomposition::SingleQubitBasis::ZSXX: - if (isConstantAngle(theta, std::numbers::pi / 2.0)) { - const auto halfPi = - Val::constant(rewriter, loc, std::numbers::pi / 2.0); - const auto quarterPi = - Val::constant(rewriter, loc, std::numbers::pi / 4.0); - qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda - halfPi); - qubit = SXOp::create(rewriter, loc, qubit).getQubitOut(); - qubit = emitRotationIfNeeded(rewriter, loc, qubit, phi + halfPi); - phase = phase - quarterPi; + const bool hasHalfTurn = pulses != nullptr + ? pulses->halfTurnAngle.has_value() + : targetBasis == nullptr || targetBasis->hasX; + if (hasHalfTurn && isConstantAngle(theta, pi)) { + qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda); + if (pulses != nullptr) { + qubit = RXOp::create(rewriter, loc, qubit, *pulses->halfTurnAngle) + .getQubitOut(); + phase = phase + constant(*pulses->halfTurnAngle < 0. ? pi : 0.); + } else { + qubit = XOp::create(rewriter, loc, qubit).getQubitOut(); + phase = phase - constant(halfPi); + } + qubit = emitRotationIfNeeded(rewriter, loc, qubit, phi + consts.pi); break; } - phase = phase + consts.pi / consts.two; - qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda); - qubit = SXOp::create(rewriter, loc, qubit).getQubitOut(); + qubit = emitRotationIfNeeded(rewriter, loc, qubit, + lambda + constant(offset)); + quarterTurn(); qubit = emitRotationIfNeeded(rewriter, loc, qubit, theta + consts.pi); - qubit = SXOp::create(rewriter, loc, qubit).getQubitOut(); - qubit = emitRotationIfNeeded(rewriter, loc, qubit, phi + consts.pi); + quarterTurn(); + qubit = emitRotationIfNeeded(rewriter, loc, qubit, + phi + constant(pi - offset)); + phase = phase + constant(pi - 2. * pulsePhase); break; + } case decomposition::SingleQubitBasis::R: qubit = emitRIfNeeded(rewriter, loc, qubit, lambda, consts.zero); qubit = emitRIfNeeded(rewriter, loc, qubit, theta, consts.pi / consts.two); @@ -1073,7 +1085,6 @@ struct MergeSingleQubitRotationGatesPattern final case decomposition::SingleQubitBasis::U: return 1; case decomposition::SingleQubitBasis::ZSXX: - case decomposition::SingleQubitBasis::FixedRotation: return 5; case decomposition::SingleQubitBasis::ZYZ: case decomposition::SingleQubitBasis::ZXZ: @@ -1367,10 +1378,10 @@ bool decomposition::canSynthesizeParameterizedUnitary1Q(Operation* op) { void decomposition::synthesizeParameterizedUnitary1Q( RewriterBase& rewriter, Operation* op, SingleQubitBasis basis, - const CompilerTarget::FixedRotationBasis* fixedRotation) { + const CompilerTarget::SynthesisBasis* targetBasis) { assert(canSynthesizeParameterizedUnitary1Q(op) && "operation must support parameterized one-qubit synthesis"); - if (isSingleQubitBasisGate(op, basis, fixedRotation)) { + if (isSingleQubitBasisGate(op, basis)) { return; } @@ -1385,8 +1396,7 @@ void decomposition::synthesizeParameterizedUnitary1Q( } const bool usesDirectZYZAngles = basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::ZXZ || - basis == SingleQubitBasis::ZSXX || - basis == SingleQubitBasis::FixedRotation; + basis == SingleQubitBasis::ZSXX; if (basis == SingleQubitBasis::U || usesDirectZYZAngles) { const auto consts = makeConsts(rewriter, op->getLoc()); Value qubit; @@ -1396,7 +1406,7 @@ void decomposition::synthesizeParameterizedUnitary1Q( const auto angles = directZYZAnglesFromGate(unitary, rewriter, consts); qubit = unitary.getInputQubit(0); qubit = emitRuntimeEulerAngles(rewriter, op->getLoc(), qubit, angles, - basis, consts, fixedRotation); + basis, consts, targetBasis); } rewriter.replaceOp(op, qubit); return; @@ -1417,7 +1427,7 @@ void populateParameterizedSingleQubitRunCompositionPatterns( RewritePatternSet& patterns, SingleQubitBasis basis, const CompilerTarget* target) { RZOp::getCanonicalizationPatterns(patterns, patterns.getContext()); - if (basis == SingleQubitBasis::FixedRotation) { + if (basis == SingleQubitBasis::ZSXX && target != nullptr) { patterns.add(mergeParameterizedRZ); return; } diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 05c4eb141a..476de8948c 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -32,8 +32,6 @@ #include #include -#include -#include using namespace mlir; @@ -173,12 +171,9 @@ void populateQCCleanupPipeline(OpPassManager& pm) { pm.addPass(createRemoveDeadValuesPass()); } -void populateQCOCleanupPipeline(OpPassManager& pm, - std::unique_ptr canonicalizer) { - pm.addPass(canonicalizer ? std::move(canonicalizer) - : createCanonicalizerPass( - GreedyRewriteConfig{}.setMaxIterations( - GreedyRewriteConfig::kNoLimit))); +void populateQCOCleanupPipeline(OpPassManager& pm) { + pm.addPass(createCanonicalizerPass( + GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit))); pm.addPass(mlir::mqt::createNormalizeGlobalPhases()); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); @@ -204,9 +199,8 @@ void populateJeffCleanupPipeline(OpPassManager& pm) { } [[nodiscard]] LogicalResult runQCOCleanupPipeline(ModuleOp mod) { - return runWithPassManager( - mod, [](OpPassManager& pm) { populateQCOCleanupPipeline(pm); }, - "Failed to run the QCO cleanup pipeline."); + return runWithPassManager(mod, populateQCOCleanupPipeline, + "Failed to run the QCO cleanup pipeline."); } [[nodiscard]] LogicalResult runQIRCleanupPipeline(ModuleOp mod, diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 71c277724a..aaa731402b 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2823,7 +2823,11 @@ TEST_F(CompilerPipelineTest, StaticNativeThreeQubitGateSurvives) { {llvm::cantFail(CompilerTarget::SiteTuple::create({0, 1, 2}))})); const auto target = llvm::cantFail(CompilerTarget::create( 3, CompilerTarget::Connectivity::fromCouplings({{0, 1}, {1, 2}}), - CompilerTarget::NativeOperations::fromOperations({native}))); + CompilerTarget::NativeOperations::fromOperations({ + native, + llvm::cantFail( + CompilerTarget::OperationCapability::create("u", 1, 3)), + }))); auto program = QCOProgram::fromMLIRString(R"mlir(module { func.func @main() attributes {mqt.entry_point} { %a = qco.static 0 : !qco.qubit @@ -2924,11 +2928,12 @@ TEST_F(CompilerPipelineTest, IndexedPlacementRetainsTargetAndPayloadChecks) { "for int i in [0:1] { x q[i]; } c = measure q;"; using OperationCapability = CompilerTarget::OperationCapability; using Native = CompilerTarget::NativeOperations; - const auto x = llvm::cantFail(OperationCapability::create("x", 1, 0)); + const auto u = llvm::cantFail(OperationCapability::create("u", 1, 3)); const auto measure = llvm::cantFail(OperationCapability::create("measure", 1, 0)); - const auto localX = llvm::cantFail(OperationCapability::create( - "x", 1, 0, {llvm::cantFail(CompilerTarget::SiteTuple::create({0}))})); + const auto localMeasure = llvm::cantFail(OperationCapability::create( + "measure", 1, 0, + {llvm::cantFail(CompilerTarget::SiteTuple::create({0}))})); const auto targetWith = [](size_t capacity, const std::vector& operations) { return llvm::cantFail(CompilerTarget::create( @@ -2938,10 +2943,10 @@ TEST_F(CompilerPipelineTest, IndexedPlacementRetainsTargetAndPayloadChecks) { const auto payload = llvm::cantFail(payloadSpecificationForProgramFormat( QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE)); for (const auto& [target, expected] : { - std::pair{targetWith(1, {x, measure}), "target site count"}, - std::pair{targetWith(2, {localX, measure}), - "cannot lower operation"}, - std::pair{targetWith(2, {measure}), "cannot lower operation"}, + std::pair{targetWith(1, {u, measure}), "target site count"}, + std::pair{targetWith(2, {u, localMeasure}), + "target does not support operation"}, + std::pair{targetWith(2, {u}), "target does not support operation"}, }) { auto qc = QCProgram::fromOpenQASMString(source); ASSERT_TRUE(qc); @@ -2956,7 +2961,7 @@ TEST_F(CompilerPipelineTest, IndexedPlacementRetainsTargetAndPayloadChecks) { EXPECT_FALSE(program->compileForTarget(TargetEnvironment(target, payload))); EXPECT_TRUE(StringRef(diagnostics).contains(expected)) << diagnostics; } - const auto target = targetWith(2, {x, measure}); + const auto target = targetWith(2, {u, measure}); for (const auto format : {QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, QDMI_PROGRAM_FORMAT_QASM3}) { auto result = runDefaultPipeline( @@ -4015,80 +4020,28 @@ TEST_F(CompilerPipelineTest, TargetSynthesisResynthesizesTwoQubitBlocks) { makeSparseUCZTarget(true), makePayloadSpecification()))); } -TEST_F(CompilerPipelineTest, TargetPipelinesPreserveNativeGateSequences) { +TEST_F(CompilerPipelineTest, TargetPipelinesRequireSynthesisBasis) { using Capability = CompilerTarget::OperationCapability; - using Case = std::tuple>>; - for (const auto& [gate, arguments, arity, parameters] : { - Case{"rz", "(0.37)", 1, {.37}}, - Case{"rx", "(pi / 2)", 1, {std::numbers::pi / 2}}, - Case{"s", "", 1, {}}, - Case{"t", "", 1, {}}, - Case{"sx", "", 1, {}}, - Case{"u", "(pi / 2, 0.2, 0.3)", 1, {std::numbers::pi / 2, .2, .3}}, - Case{"rzz", "(0.37)", 2, {.37}}, + for (const auto& native : { + llvm::cantFail(Capability::create("s", 1, 0)), + llvm::cantFail(Capability::create("rx", 1, 1, {}, std::nullopt, + std::nullopt, {0.37})), }) { - SCOPED_TRACE(gate); - const std::string wires = arity == 1 ? "a" : "a, b"; - const std::string pulse = std::string(gate) + arguments + " " + wires + ";"; - std::string source = "OPENQASM 3.0; include \"stdgates.inc\"; gate pair "; - source.append(wires) - .append(" { ") - .append(pulse) - .append(pulse) - .append(" } qubit[2] q; pair q[0]") - .append(arity == 1 ? ";" : ", q[1];"); - auto input = QCProgram::fromOpenQASMString(source); - ASSERT_TRUE(input); - auto reference = input->copy().intoQCO(); - ASSERT_TRUE(reference); - ASSERT_TRUE(reference->runPassPipeline("inline,symbol-dce")); - for (const bool withSingleQubitBasis : {false, true}) { - SCOPED_TRACE(withSingleQubitBasis); - std::vector capabilities{ - llvm::cantFail(Capability::create(gate, arity, parameters.size(), {}, - std::nullopt, std::nullopt, - parameters)), - llvm::cantFail(Capability::create("gphase", 0, 1)), - }; - if (withSingleQubitBasis) { - capabilities.push_back(llvm::cantFail(Capability::create("u", 1, 3))); - } - const auto target = llvm::cantFail(CompilerTarget::create( - 2, CompilerTarget::Connectivity::allToAll(), - CompilerTarget::NativeOperations::fromOperations(capabilities))); - const TargetEnvironment environment( - target, llvm::cantFail(payloadSpecificationForProgramFormat( - QDMI_PROGRAM_FORMAT_QASM3))); - for (const bool synthesisOnly : {false, true}) { - SCOPED_TRACE(synthesisOnly); - auto program = input->copy().intoQCO(); - ASSERT_TRUE(program); - ASSERT_TRUE(synthesisOnly ? program->synthesizeForTarget(environment) - : program->compileForTarget(environment)); - EXPECT_TRUE(succeeded(verify(program->module()))); - EXPECT_TRUE(succeeded(qco::verifyLinearity(program->module()))); - expectFullUnitaryEqual(reference->module(), program->module(), 2); - } - auto output = - runDefaultPipeline(CompilerInput{input->copy()}, environment); - ASSERT_TRUE(output); - auto exported = QCProgram::fromOpenQASMString( - std::get(*output).source()); - ASSERT_TRUE(exported); - auto roundTrip = std::move(*exported).intoQCO(); - ASSERT_TRUE(roundTrip); - PassManager inlineExport(roundTrip->module().getContext()); - inlineExport.addPass(qco::createTargetInliner(target)); - inlineExport.addPass(createSymbolDCEPass()); - ASSERT_TRUE(succeeded(inlineExport.run(roundTrip->module()))); - roundTrip->module().walk( - [&](qco::UnitaryOpInterface gate) { - EXPECT_TRUE(target.supports(gate.getOperation())) - << gate->getName().getStringRef().str(); - return WalkResult::skip(); - }); - expectFullUnitaryEqual(reference->module(), roundTrip->module(), 2); + const auto target = llvm::cantFail(CompilerTarget::create( + 1, CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::fromOperations({native}))); + EXPECT_FALSE(target.synthesisBasis()); + for (const bool synthesisOnly : {false, true}) { + auto input = QCProgram::fromOpenQASMString( + "OPENQASM 3.0; include \"stdgates.inc\"; qubit q; s q;"); + ASSERT_TRUE(input); + auto program = std::move(*input).intoQCO(); + ASSERT_TRUE(program); + const auto before = program->str(); + const TargetEnvironment environment(target, makePayloadSpecification()); + EXPECT_FALSE(synthesisOnly ? program->synthesizeForTarget(environment) + : program->compileForTarget(environment)); + EXPECT_EQ(program->str(), before); } } } @@ -4171,6 +4124,10 @@ TEST_F(CompilerPipelineTest, TargetCompilationFusesOnlyWithUsableNativeBasis) { auto program = QCOProgram::fromModule(ownedContext, std::move(moduleOp)); ASSERT_TRUE(program); + if (!testCase.target.synthesisBasis()) { + EXPECT_FALSE(program->compileForTarget(environment)); + continue; + } ASSERT_TRUE(program->compileForTarget(environment)); ASSERT_TRUE(verify(program->module()).succeeded()); ASSERT_TRUE(qco::verifyLinearity(program->module()).succeeded()); diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index e42b9ea520..7f497b04e7 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -899,8 +899,8 @@ TEST(CompilerTargetTest, EnforcesExactOrderedOperationApplicability) { 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})); + "prx_90", 1, 2, {valid(SiteTuple::create({0}))}, std::nullopt, + std::nullopt, {std::numbers::pi / 2., std::nullopt}, "r")); const auto target = valid(Target::create(2, Connectivity::allToAll(), NativeOperations::fromOperations({rotation}))); @@ -924,6 +924,8 @@ TEST(CompilerTargetTest, MatchesFixedParametersAndPreservesPlacements) { const auto attribute = target.materialize(context); const auto restored = valid(Target::create(attribute)); EXPECT_EQ(restored.materialize(context), attribute); + EXPECT_EQ(restored.operations()[0].name(), "prx_90"); + EXPECT_EQ(restored.operations()[0].canonicalName(), "r"); EXPECT_EQ(restored.operations()[0].fixedParameters(), rotation.fixedParameters()); EXPECT_TRUE( @@ -1016,7 +1018,7 @@ TEST(CompilerTargetTest, ResolvesFixedPulseBasisOnlyOnEverySite) { NativeOperations::fromOperations(operations))); ASSERT_TRUE(target.synthesisBasis()); EXPECT_EQ(target.synthesisBasis()->singleQubit, - Target::SingleQubitBasis::FixedRotation); + Target::SingleQubitBasis::ZSXX); EXPECT_FALSE(target.synthesisBasis()->entangler); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 40088da03d..1abe05dcb2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -3402,7 +3402,7 @@ TEST_F(MappingPassFixture, PreferNativeGateCountThenDepth) { MappingPassOptions{ .ntrials = 4, .seed = 42, .searchMemoryLimit = 0}))); PassManager native(context.get()); - populateTargetNativeSynthesisPipeline(native, target); + populateTargetNativeSynthesisPipeline(native); ASSERT_TRUE(succeeded(native.run(*moduleOp))); ASSERT_TRUE(succeeded(verify(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); @@ -3469,7 +3469,7 @@ TEST_F(MappingPassFixture, PreserveBasisStatesAfterNativeScoredCompilation) { EXPECT_TRUE(succeeded(verifyLinearity(*moduleOp))); EXPECT_TRUE(isExecutable(getEntryPoint(*moduleOp), target)); PassManager synthesis(context.get()); - populateTargetNativeSynthesisPipeline(synthesis, target); + populateTargetNativeSynthesisPipeline(synthesis); ASSERT_TRUE(succeeded(synthesis.run(*moduleOp))); ASSERT_TRUE(succeeded(verify(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); @@ -3562,7 +3562,7 @@ TEST_F(MappingPassFixture, PreserveRegionsWithIdleWiresAcrossCostAvailability) { ASSERT_TRUE(succeeded(verify(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); PassManager native(context.get()); - populateTargetNativeSynthesisPipeline(native, target); + populateTargetNativeSynthesisPipeline(native); ASSERT_TRUE(succeeded(native.run(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); const auto actual = qco::sample(getEntryPoint(*moduleOp), 1, 42); @@ -3829,7 +3829,7 @@ TEST_F(MappingPassFixture, NativeGuidancePreservesAlternatingPairs) { .ntrials = 2, .searchMemoryLimit = budget}))); PassManager native(context.get()); - populateTargetNativeSynthesisPipeline(native, target); + populateTargetNativeSynthesisPipeline(native); ASSERT_TRUE(succeeded(native.run(*moduleOp))); ASSERT_TRUE(succeeded(verify(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); @@ -3889,7 +3889,7 @@ TEST_F(MappingPassFixture, NativeScoringChecksTerminalMeasurementSites) { ASSERT_TRUE(succeeded(runPass( *moduleOp, target, MappingPassOptions{.ntrials = 4, .seed = seed}))); PassManager native(context.get()); - populateTargetNativeSynthesisPipeline(native, target); + populateTargetNativeSynthesisPipeline(native); ASSERT_TRUE(succeeded(native.run(*moduleOp))); ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); const auto actual = qco::sample(getEntryPoint(*moduleOp), 1, 42); @@ -3936,7 +3936,7 @@ module { ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); EXPECT_TRUE(isExecutable(getEntryPoint(*moduleOp), target)); PassManager native(context.get()); - populateTargetNativeSynthesisPipeline(native, target); + populateTargetNativeSynthesisPipeline(native); EXPECT_TRUE(failed(native.run(*moduleOp))); EXPECT_GT(count, 0); } 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 735cd7ef4a..8248137f44 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -328,30 +328,45 @@ TEST_F(TargetSynthesisTest, TargetPassesRequireTypedEnvironment) { << diagnostics; } -TEST_F(TargetSynthesisTest, FixedPulseSynthesisPreservesFullUnitary) { - for (double pulseAngle : {std::numbers::pi / 2., -std::numbers::pi / 2.}) { +TEST_F(TargetSynthesisTest, ZSXXSynthesisPreservesFullUnitary) { + for (const std::optional pulseAngle : { + std::optional{}, + std::optional{std::numbers::pi / 2.}, + std::optional{-std::numbers::pi / 2.}, + }) { for (const std::optional halfTurn : { std::optional{}, std::optional{std::numbers::pi}, std::optional{-std::numbers::pi}, }) { + if (!pulseAngle && halfTurn && *halfTurn < 0.) { + continue; + } SCOPED_TRACE(testing::Message() - << pulseAngle << " " << halfTurn.value_or(0.)); + << pulseAngle.value_or(0.) << " " << halfTurn.value_or(0.)); std::vector operations{ - valid(OperationCapability::create("rx", 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)), }; + operations.push_back( + pulseAngle + ? valid(OperationCapability::create("rx", 1, 1, {}, std::nullopt, + std::nullopt, {pulseAngle})) + : valid(OperationCapability::create("sx", 1, 0))); if (halfTurn) { - operations.push_back(valid(OperationCapability::create( - "rx", 1, 1, {}, std::nullopt, std::nullopt, {halfTurn}))); + operations.push_back( + pulseAngle + ? valid(OperationCapability::create( + "rx", 1, 1, {}, std::nullopt, std::nullopt, {halfTurn})) + : valid(OperationCapability::create("x", 1, 0))); } const auto target = valid(Target::create(2, Connectivity::allToAll(), NativeOperations::fromOperations(operations))); ASSERT_TRUE(target.synthesisBasis()); + EXPECT_EQ(target.synthesisBasis()->singleQubit, + Target::SingleQubitBasis::ZSXX); 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)); diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 3105156ea4..4d32a20f49 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -295,6 +295,9 @@ class CompilerTarget: site_tuples: Sequence[CompilerTarget.SiteTuple | Sequence[int]] | None = None, duration: int | None = None, fidelity: float | None = None, + *, + fixed_parameters: Sequence[float | None] = (), + canonical_name: str | None = None, ) -> None: ... @property def name(self) -> str: @@ -316,6 +319,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.""" @@ -513,7 +520,13 @@ 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: """Check whether the target supports an operation. @@ -522,6 +535,8 @@ class CompilerTarget: arity: Number of qubits used by the operation. num_parameters: Number of real-valued parameters. None accepts any count. sites: Ordered target site IDs. None checks support on any placement. + parameters: Known parameter values. Omitted or None values require + unrestricted support. """ class TargetEnvironment: diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index b60c7b8c50..2469c558be 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -668,7 +668,7 @@ 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.FIXED_ROTATION + assert target.synthesis_basis.single_qubit == CompilerTarget.SingleQubitBasis.ZSXX 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]) diff --git a/test/python/test_mlir_qiskit_target.py b/test/python/test_mlir_qiskit_target.py index b6edf888ac..081bfdecf6 100644 --- a/test/python/test_mlir_qiskit_target.py +++ b/test/python/test_mlir_qiskit_target.py @@ -10,8 +10,10 @@ from __future__ import annotations +from math import pi from typing import cast +import numpy as np import pytest from qiskit import QuantumCircuit from qiskit.circuit import Gate, Measure, Parameter, Reset @@ -23,7 +25,9 @@ CXGate, GlobalPhaseGate, PhaseGate, + RXGate, RZGate, + SXGate, U1Gate, U3Gate, UGate, @@ -88,24 +92,41 @@ def test_backend_and_global_operations() -> None: def test_fixed_parameter_constraints() -> None: - """A fixed angle cannot become an arbitrary rotation.""" + """Named discrete angles remain separate capabilities of one gate.""" source = Target(num_qubits=1) - source.add_instruction(RZGate(0.5)) - with pytest.raises(ValueError, match="parameter constraints for 'rz'"): - CompilerTarget.from_qiskit(source, operation_names=["rz"]) - with ( - pytest.warns(UserWarning, match="parameter constraints") as warnings, - pytest.raises(ValueError, match="no representable"), - ): - CompilerTarget.from_qiskit(source) - assert warnings[0].filename == __file__ + source.add_instruction(RXGate(pi / 2), name="rx_90") + source.add_instruction(RXGate(pi), name="rx_180") + source.add_instruction(RZGate(Parameter("angle"))) + target = CompilerTarget.from_qiskit(source) + operations = {operation.name: operation for operation in target.operations} + assert operations["rx_90"].canonical_name == operations["rx_180"].canonical_name == "rx" + assert operations["rx_90"].fixed_parameters == [pi / 2] + assert operations["rx_180"].fixed_parameters == [pi] + assert operations["rz"].fixed_parameters == [] + for angle in (pi / 2, pi): + assert target.supports_operation("rx", 1, 1, parameters=[angle]) + for angle in (None, -pi / 2, 0.3): + assert not target.supports_operation("rx", 1, 1, parameters=[angle]) + + +def test_partially_fixed_parameter_slots() -> None: + """Bound expressions become fixed values; remaining symbols are wildcards.""" + angle = Parameter("angle") + source = Target(num_qubits=1) + source.add_instruction(UGate(angle.bind({angle: pi / 2}), angle / 2, 0.0), name="native_u") + target = CompilerTarget.from_qiskit(source) + operation = next(operation for operation in target.operations if operation.name == "native_u") + assert operation.canonical_name == "u" + assert operation.fixed_parameters == [pi / 2, None, 0.0] + assert target.supports_operation("u", 1, 3, parameters=[pi / 2, None, 0.0]) + assert not target.supports_operation("u", 1, 3, parameters=[None, None, 0.0]) @pytest.mark.filterwarnings("error:Cannot represent.*:UserWarning") def test_target_warning_as_error() -> None: """A native conversion warning respects the caller's warning filters.""" source = Target(num_qubits=1) - source.add_instruction(RZGate(0.5)) + source.add_instruction(RZGate(Parameter("angle")), angle_bounds=[(-1.0, 1.0)]) with pytest.raises(UserWarning, match="parameter constraints"): CompilerTarget.from_qiskit(source) @@ -141,13 +162,14 @@ def test_restricted_operation_set() -> None: """An explicit subset can omit an unrepresentable operation.""" source = Target(num_qubits=1) source.add_instruction(XGate()) - source.add_instruction(RZGate(0.5)) + source.add_instruction(RZGate(Parameter("angle")), angle_bounds=[(-1.0, 1.0)]) converted = CompilerTarget.from_qiskit(source, operation_names=["x"]) assert {operation.name for operation in converted.operations} == {"x", "gphase"} - with pytest.warns(UserWarning, match="parameter constraints"): + with pytest.warns(UserWarning, match="parameter constraints") as warnings: converted = CompilerTarget.from_qiskit(source) assert {operation.name for operation in converted.operations} == {"x", "gphase"} + assert warnings[0].filename == __file__ def test_angle_bounds_and_open_controls() -> None: @@ -163,7 +185,7 @@ def test_angle_bounds_and_open_controls() -> None: def test_custom_names_and_operations() -> None: - """Reject renamed standard gates and custom gates with standard names.""" + """Names do not override gate identity; custom gates remain unsupported.""" source = Target(num_qubits=1) source.add_instruction(XGate(), name="native_x") source.add_instruction(Gate("x", 1, [])) @@ -171,14 +193,103 @@ def test_custom_names_and_operations() -> None: source.add_instruction(GlobalPhaseGate(0.5)) with pytest.warns(UserWarning, match="custom"): converted = CompilerTarget.from_qiskit(source) - assert {operation.name for operation in converted.operations} == {"rz", "gphase"} - for name in ("native_x", "x"): + assert {operation.name for operation in converted.operations} == {"native_x", "rz", "gphase"} + assert converted.supports_operation("x", 1, 0) + source.add_instruction(Measure(), name="native_measure") + source.add_instruction(Reset(), name="native_reset") + for name in ("x", "native_measure", "native_reset"): with pytest.raises(ValueError, match="custom"): CompilerTarget.from_qiskit(source, operation_names=[name]) with pytest.raises(ValueError, match="no native gate applicability"): CompilerTarget.from_qiskit(source, operation_names=["global_phase"]) +@pytest.mark.parametrize("symbolic", [False, True]) +def test_compile_named_fixed_rotations(*, symbolic: bool) -> None: + """Compilation exports executable target names with exact gate phases.""" + source = Target(num_qubits=2) + source.add_instruction(RXGate(pi / 2), name="quarter_turn") + source.add_instruction(RXGate(pi), name="half_turn") + source.add_instruction(RZGate(Parameter("angle"))) + source.add_instruction(CXGate()) + target = CompilerTarget.from_qiskit(source) + circuit = QuantumCircuit(2) + circuit.rx(pi, 0) + circuit.ry(Parameter("theta") if symbolic else 0.3, 1) + circuit.cx(0, 1) + program = QCProgram.from_qiskit(circuit).to_qco() + program.compile_for_target(TargetEnvironment(target, PayloadSpecification(PayloadFormat("openqasm", "3.0")))) + exported = program.to_qiskit(target=target) + assert any(item.operation.name == "quarter_turn" for item in exported.data) + assert all( + source.instruction_supported( + item.operation.name, + tuple(exported.find_bit(qubit).index for qubit in item.qubits), + parameters=item.operation.params, + ) + for item in exported.data + ) + if symbolic: + exported = exported.assign_parameters({"theta": 0.7}) + circuit = circuit.assign_parameters({"theta": 0.7}) + assert np.allclose(Operator(exported).data, Operator(circuit).data) + restored = QCProgram.from_qiskit(exported).to_qiskit() + assert np.allclose(Operator(restored).data, Operator(circuit).data) + + +def test_export_fixed_rotations_by_parameters_and_sites() -> None: + """Select aliases by values and placement, including colliding gate names.""" + source = Target(num_qubits=2) + source.add_instruction(RXGate(pi / 2), {(0,): None}, name="a_quarter") + source.add_instruction(RXGate(pi), {(0,): None}, name="b_half") + source.add_instruction(RXGate(Parameter("angle")), {(0,): None}, name="z_variable") + source.add_instruction(RXGate(pi / 2), {(1,): None}, name="ry") + source.add_instruction(CXGate()) + target = CompilerTarget.from_qiskit(source) + circuit = QuantumCircuit(2) + circuit.rx(pi / 2, 0) + circuit.rx(pi, 0) + circuit.rx(Parameter("theta"), 0) + circuit.rx(pi / 2, 1) + program = QCProgram.from_mlir_str("""module { + func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} { + %quarter = arith.constant 1.5707963267948966 : f64 + %half = arith.constant 3.141592653589793 : f64 + %a = qc.static 0 : !qc.qubit + %b = qc.static 1 : !qc.qubit + qc.rx(%quarter) %a : !qc.qubit + qc.rx(%half) %a : !qc.qubit + qc.rx(%theta) %a : !qc.qubit + qc.rx(%quarter) %b : !qc.qubit + return + } +} +""") + exported = program.to_qiskit(target=target) + assert [item.operation.name for item in exported.data] == ["a_quarter", "b_half", "z_variable", "ry"] + assert all(item.operation.base_class is RXGate for item in exported.data) + restored = QCProgram.from_qiskit(exported).to_qiskit() + assert np.allclose( + Operator(restored.assign_parameters({"theta": 0.3})).data, + Operator(circuit.assign_parameters({"theta": 0.3})).data, + ) + + +def test_named_sx_and_x_keep_phase() -> None: + """SX and X keep their exact matrices instead of becoming RX aliases.""" + source = Target(num_qubits=1) + source.add_instruction(SXGate(), name="native_sx") + source.add_instruction(XGate(), name="native_x") + target = CompilerTarget.from_qiskit(source) + circuit = QuantumCircuit(1) + circuit.sx(0) + circuit.x(0) + program = QCProgram.from_openqasm_str('OPENQASM 3.0; include "stdgates.inc"; sx $0; x $0;') + exported = program.to_qiskit(target=target) + assert [item.operation.name for item in exported.data] == ["native_sx", "native_x"] + assert np.allclose(Operator(exported).data, Operator(circuit).data) + + @pytest.mark.parametrize("qco", [False, True]) @pytest.mark.parametrize("symbolic", [False, True]) def test_export_standard_aliases_on_ordered_sites(*, qco: bool, symbolic: bool) -> None: diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index b6c07ae300..31190c374e 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -4668,7 +4668,7 @@ def test_native_mapping_exports_full_qiskit_layout(*, routed: bool, basis: str) else [ CompilerTarget.OperationCapability("rz", 1, 1), CompilerTarget.OperationCapability("rx", 1, 1, fixed_parameters=[np.pi / 2]), - CompilerTarget.OperationCapability("iswap", 2, 0), + CompilerTarget.OperationCapability("cz", 2, 0), ] ) target = CompilerTarget( @@ -4694,7 +4694,7 @@ def test_native_mapping_exports_full_qiskit_layout(*, routed: bool, basis: str) program.compile_for_target(_test_target_environment(target)) exported = program.to_qiskit(target=target) if basis == "fixed_rx": - assert set(exported.count_ops()) <= {"rx", "rz", "iswap"} + assert set(exported.count_ops()) <= {"rx", "rz", "cz"} assert all(gate.operation.params == [np.pi / 2] for gate in exported.data if gate.operation.name == "rx") layout = exported.layout assert layout is not None From 4c72462a778fc71b88a05bd433b0619d82b494b1 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 2 Oct 2026 00:29:12 +0000 Subject: [PATCH 22/23] =?UTF-8?q?=F0=9F=90=9B=20Rebuild=20documentation=20?= =?UTF-8?q?artifacts=20in=20nox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Force the docs session to rebuild Core with its documentation CMake options. A cached editable wheel can omit generated MLIR reference pages, causing missing includes and secondary heading warnings in a clean tree. The strict executable documentation build and generated-page link check pass with the rebuilt artifacts. Assisted-by: GPT-6 via Codex --- noxfile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/noxfile.py b/noxfile.py index f82e5da2d9..e035694ee9 100755 --- a/noxfile.py +++ b/noxfile.py @@ -331,6 +331,8 @@ def docs(session: nox.Session) -> None: "--no-dev", # do not auto-install dev dependencies "--no-build-isolation-package", "mqt-core", # build the project without isolation + "--reinstall-package", + "mqt-core", # apply documentation CMake options instead of a cached wheel "sphinx-autobuild" if serve else "sphinx-build", "-n", # nitpicky mode "-T", # full tracebacks From d1cb96f313a81bab7e85015f1b30bd443276a96a Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 2 Oct 2026 10:26:32 +0200 Subject: [PATCH 23/23] =?UTF-8?q?=F0=9F=90=9B=20Pass=20the=20optional=20na?= =?UTF-8?q?tive=20name=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Avoid dereferencing and reconstructing the optional operation name when creating target capabilities. This preserves behavior and resolves the clang-tidy diagnostic. Assisted-by: GPT-6 via Codex --- bindings/mlir/qiskit/Qiskit2_5.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index 28498b00bc..b1a26b1d1e 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -2934,7 +2934,7 @@ class NativeTranslation final : public VersionedTranslation { const auto arity = nb::cast(instruction.attr("num_qubits")); auto capability = takeResult(Target::OperationCapability::create( operationName, arity, numParameters, std::move(placements), - std::nullopt, std::nullopt, std::move(fixedParameters), *nativeName)); + std::nullopt, std::nullopt, std::move(fixedParameters), nativeName)); if (arity == 2) { if (qargs.is_none()) { allToAll = true;