diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md new file mode 100644 index 0000000000..639fa9c8d0 --- /dev/null +++ b/.agent/plans/fixed-parameter-targets.md @@ -0,0 +1,45 @@ +# Fixed-parameter compiler targets + +Status: 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. 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. 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 386 cases, including numerical and symbolic input +gates. Generated stubs, repository lint, and full changed-file C++ lint passed. + +## Follow-up + +Direct GPI/GPI2, MS, and ZZ target support is separate work. It will own gate +conventions and synthesis in Core. The downstream adapter will then expose these +capabilities and Rigetti's fixed rotations. diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index ef18816cc1..385fb2ed82 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -811,7 +811,8 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::optional> siteTuples, const std::optional duration, - const std::optional fidelity) { + const std::optional fidelity, + std::vector> fixedParameters) { constructFromExpected( self, mlir::CompilerTarget::OperationCapability::create( @@ -819,10 +820,11 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity)); + duration, fidelity, std::move(fixedParameters))); }, "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), - "duration"_a = nb::none(), "fidelity"_a = nb::none()) + "duration"_a = nb::none(), "fidelity"_a = nb::none(), nb::kw_only(), + "fixed_parameters"_a = std::vector>{}) .def( "__init__", [](mlir::CompilerTarget::OperationCapability& self, std::string name, @@ -830,7 +832,8 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::optional> siteTuples, const std::optional duration, - const std::optional fidelity) { + const std::optional fidelity, + std::vector> fixedParameters) { constructFromExpected( self, mlir::CompilerTarget::OperationCapability::create( @@ -838,10 +841,11 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity)); + duration, fidelity, std::move(fixedParameters))); }, "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), - "duration"_a = nb::none(), "fidelity"_a = nb::none()) + "duration"_a = nb::none(), "fidelity"_a = nb::none(), nb::kw_only(), + "fixed_parameters"_a = std::vector>{}) .def_prop_ro( "name", [](const mlir::CompilerTarget::OperationCapability& operation) { @@ -867,6 +871,15 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); }, "Supported ordered placements with optional calibration; empty means " "general applicability.") + .def_prop_ro( + "fixed_parameters", + [](const mlir::CompilerTarget::OperationCapability& operation) { + return std::vector>( + operation.fixedParameters().begin(), + operation.fixedParameters().end()); + }, + "Fixed values or None per parameter; empty means unrestricted. " + "Constants use absolute tolerance 1e-15 without angle wrapping.") .def_prop_ro("duration", &mlir::CompilerTarget::OperationCapability::duration, "The raw default duration, if available.") @@ -902,7 +915,9 @@ 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("FixedRotation", + mlir::CompilerTarget::SingleQubitBasis::FixedRotation); auto synthesisBasis = nb::class_( compilerTarget, "SynthesisBasis", @@ -1142,15 +1157,20 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); [](const mlir::CompilerTarget& target, const std::string_view name, const size_t arity, const std::optional numParameters, const std::optional>& - sites) { + sites, + const std::vector>& parameters) { if (sites) { return target.supportsOperation(name, arity, numParameters, - *sites); + *sites, parameters); } - return target.supportsOperation(name, arity, numParameters); + return target.supportsOperation(name, arity, numParameters, + std::nullopt, parameters); }, "name"_a, "arity"_a, "num_parameters"_a = nb::none(), - "sites"_a = nb::none(), "Whether the target supports an operation."); + "sites"_a = nb::none(), nb::kw_only(), + "parameters"_a = std::vector>{}, + "Whether the target supports an operation. Omitted or None parameter " + "values require unrestricted support."); nb::class_( m, "TargetEnvironment", diff --git a/bindings/patterns.txt b/bindings/patterns.txt index d8de3abc91..df8b40953f 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -138,6 +138,8 @@ mqt\.core\.mlir\.CompilerTarget\.OperationCapability\.__init__$: site_tuples: Sequence[CompilerTarget.SiteTuple | Sequence[int]] | None = None, duration: int | None = None, fidelity: float | None = None, + *, + fixed_parameters: Sequence[float | None] = (), ) -> None: \doc diff --git a/docs/glossary.md b/docs/glossary.md index 6d2fb59e4b..3b0e527e44 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -202,7 +202,7 @@ compiler target operation capability **Preferred term:** operation capability. **Accepted aliases:** none. A compiler target's description of a supported operation, including its name, - arity, parameters, placements, and optional calibration data. Represented by + arity, parameter count and optional fixed values, placements, and optional calibration data. Represented by `CompilerTarget::OperationCapability` in C++ and `CompilerTarget.OperationCapability` in Python. An MLIR operation is an IR instance, not this capability description. diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 33fdd528a5..37cbdc37ca 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -166,6 +166,25 @@ 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 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. + Use plain tuples for placements without calibration. Use `CompilerTarget.SiteTuple([1, 0], duration=40, fidelity=0.99)` to attach calibration to a placement; both forms can appear in the same list. diff --git a/mlir/include/mqt/Compiler/Target.h b/mlir/include/mqt/Compiler/Target.h index 10f57b10b9..1b0297e1cd 100644 --- a/mlir/include/mqt/Compiler/Target.h +++ b/mlir/include/mqt/Compiler/Target.h @@ -18,6 +18,7 @@ #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" +#include #include #include #include @@ -196,14 +197,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 +220,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 +241,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,19 +304,39 @@ 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(λ)`. + FixedRotation, ///< An arbitrary rotation and fixed pulses about another + ///< axis. + }; + + /// Fixed pulse combined with arbitrary rotations about a distinct axis. + 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; + 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; }; /// 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; @@ -390,16 +422,24 @@ class CompilerTarget { /// Return operation capabilities in reported order. [[nodiscard]] llvm::ArrayRef operations() const noexcept; - /// Return whether an operation capability is supported by the target. + /// Return whether an operation supports unrestricted parameter values. [[nodiscard]] bool supportsOperation(llvm::StringRef name, size_t arity, std::optional numParameters = std::nullopt) const; - /// Return whether an operation capability is supported on ordered sites. + /// Return whether an operation supports unrestricted values on ordered sites. [[nodiscard]] bool supportsOperation(llvm::StringRef name, size_t arity, std::optional numParameters, llvm::ArrayRef sites) const; + /// Check parameter values, optionally on ordered sites. + /// Unknown values require unrestricted support. + [[nodiscard]] bool + supportsOperation(llvm::StringRef name, size_t arity, + std::optional numParameters, + std::optional> sites, + llvm::ArrayRef> parameters) const; + /// Return whether a QCO operation is supported. [[nodiscard]] bool supports(::mlir::Operation* operation) const; diff --git a/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td index 693b56755c..f749e65f96 100644 --- a/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td @@ -183,7 +183,9 @@ def NativeOperationAttr : MQTAttr<"NativeOperation", "native_operation"> { The operation records its spelling, arity, parameter count, and optional global or site-specific calibration data. An empty site-tuple list means general applicability; a nonempty list gives all supported ordered - placements. The following example records a directional controlled-X operation: + placements. Optional `fixed_parameters` contains one entry per parameter: + a finite f64 value for a fixed parameter, or `unit` for an unrestricted one. + The following example records a directional controlled-X operation: ```mlir #mqt.native_operation { "uint64_t":$num_parameters, MQTArrayRefParameter<"SiteTupleAttr">:$site_tuples, MQTOptionalUInt64Parameter<>:$duration, - OptionalParameter<"FloatAttr">:$fidelity); + OptionalParameter<"FloatAttr">:$fidelity, + OptionalParameter<"ArrayAttr">:$fixed_parameters); let assemblyFormat = "`<` struct(params) `>`"; let genVerifyDecl = 1; } diff --git a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h index d1a62dd328..22bfb189d2 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -60,8 +60,10 @@ struct SynthesizedUnitary1Q { }; /// Returns whether @p op belongs to @p basis. -[[nodiscard]] bool isSingleQubitBasisGate(Operation* op, - SingleQubitBasis basis); +/// Fixed-pulse bases require their target's @p fixedRotation descriptor. +[[nodiscard]] bool isSingleQubitBasisGate( + Operation* op, SingleQubitBasis basis, + const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr); /// Extracts `(theta, phi, lambda, phase)` of @p matrix in @p basis. /// @@ -86,10 +88,10 @@ struct SynthesizedUnitary1Q { /// @param basis The single-qubit synthesis basis. /// @return The synthesized qubit and correction, or `std::nullopt` if synthesis /// is skipped. -[[nodiscard]] std::optional -synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, - const Matrix2x2& composed, std::size_t runSize, - bool hasNonBasisGate, SingleQubitBasis basis); +[[nodiscard]] std::optional synthesizeUnitary1QEuler( + OpBuilder& builder, Location loc, Value qubit, const Matrix2x2& composed, + std::size_t runSize, bool hasNonBasisGate, SingleQubitBasis basis, + const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr); /// Materializes one accumulated phase correction when needed. /// @@ -104,10 +106,12 @@ void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase); /// Synthesizes one supported runtime-parameterized operation in @p basis. /// /// Leaves operations that already belong to @p basis unchanged. +/// Fixed-pulse bases require their target's @p fixedRotation descriptor. /// /// @pre `canSynthesizeParameterizedUnitary1Q(op)` is true. -void synthesizeParameterizedUnitary1Q(RewriterBase& rewriter, Operation* op, - SingleQubitBasis basis); +void synthesizeParameterizedUnitary1Q( + RewriterBase& rewriter, Operation* op, SingleQubitBasis basis, + const CompilerTarget::FixedRotationBasis* fixedRotation = nullptr); /// Populates @p patterns with the single-qubit run fusion rewrite for /// @p basis (the reusable core of `fuse-single-qubit-unitary-runs`). diff --git a/mlir/lib/Compiler/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/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 1e0fb0aeaf..5bf6388a96 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" @@ -160,6 +161,79 @@ 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; + } + 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 = (!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); + 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; + 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; + } + result.quarterTurnAngles = std::move(zAngles); + 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") { @@ -377,21 +451,22 @@ CompilerTarget::OperationCapability::Arity::Arity(Kind kind, : kind_(kind), value_(value) {} llvm::Expected -CompilerTarget::OperationCapability::create(std::string name, size_t arity, - size_t numParameters, - std::vector siteTuples, - std::optional duration, - std::optional fidelity) { +CompilerTarget::OperationCapability::create( + std::string name, size_t arity, size_t numParameters, + std::vector siteTuples, std::optional duration, + std::optional fidelity, + std::vector> fixedParameters) { return create(std::move(name), Arity::fixed(arity), numParameters, - std::move(siteTuples), duration, fidelity); + std::move(siteTuples), duration, fidelity, + std::move(fixedParameters)); } llvm::Expected -CompilerTarget::OperationCapability::create(std::string name, Arity arity, - size_t numParameters, - std::vector siteTuples, - std::optional duration, - std::optional fidelity) { +CompilerTarget::OperationCapability::create( + std::string name, Arity arity, size_t numParameters, + std::vector siteTuples, std::optional duration, + std::optional fidelity, + std::vector> fixedParameters) { auto canonicalName = canonicalOperationName(name); if (canonicalName.empty()) { return invalidTarget("Compiler target operation name must not be empty"); @@ -414,6 +489,20 @@ CompilerTarget::OperationCapability::create(std::string name, Arity arity, "Compiler target zero-arity operation cannot contain site tuples"); } + if (!fixedParameters.empty() && fixedParameters.size() != numParameters) { + return invalidTarget( + "Compiler target fixed parameters must match its parameter count"); + } + if (llvm::any_of(fixedParameters, [](const auto value) { + return value && !std::isfinite(*value); + })) { + return invalidTarget("Compiler target fixed parameters must be finite"); + } + if (llvm::none_of(fixedParameters, + [](const auto value) { return value.has_value(); })) { + fixedParameters.clear(); + } + llvm::SmallDenseSet> uniqueSiteCombinations; for (const auto& siteTuple : siteTuples) { if (!arity.accepts(siteTuple.sites().size())) { @@ -428,15 +517,17 @@ CompilerTarget::OperationCapability::create(std::string name, Arity arity, return OperationCapability(std::move(name), std::move(canonicalName), arity, numParameters, std::move(siteTuples), duration, - fidelity); + fidelity, std::move(fixedParameters)); } CompilerTarget::OperationCapability::OperationCapability( std::string name, std::string canonicalName, Arity arity, size_t numParameters, std::vector siteTuples, - std::optional duration, std::optional fidelity) + std::optional duration, std::optional fidelity, + std::vector> fixedParameters) : name_(std::move(name)), canonicalName_(std::move(canonicalName)), arity_(arity), numParameters_(numParameters), + fixedParameters_(std::move(fixedParameters)), siteTuples_(std::move(siteTuples)), duration_(duration), fidelity_(fidelity) {} @@ -457,6 +548,11 @@ size_t CompilerTarget::OperationCapability::numParameters() const noexcept { return numParameters_; } +ArrayRef> +CompilerTarget::OperationCapability::fixedParameters() const noexcept { + return fixedParameters_; +} + ArrayRef CompilerTarget::OperationCapability::siteTuples() const noexcept { return siteTuples_; @@ -508,11 +604,11 @@ struct CompilerTarget::Storage { [[nodiscard]] llvm::Error initialize(); void computeDistances(size_t source, MutableArrayRef row) const; - [[nodiscard]] bool - supportsOperation(StringRef name, size_t arity, - std::optional numParameters, - std::optional> orderedSites = std::nullopt, - bool variadicOnly = false) const; + [[nodiscard]] bool supportsOperation( + StringRef name, size_t arity, std::optional numParameters, + std::optional> orderedSites = std::nullopt, + bool variadicOnly = false, + function_ref(size_t)> parameterAt = nullptr) const; [[nodiscard]] bool supportsGate( GateKind gate, std::optional> orderedSites = std::nullopt) const; @@ -681,7 +777,8 @@ llvm::Error CompilerTarget::Storage::initialize() { bool CompilerTarget::Storage::supportsOperation( StringRef operationName, size_t arity, std::optional numParameters, - std::optional> orderedSites, bool variadicOnly) const { + std::optional> orderedSites, bool variadicOnly, + function_ref(size_t)> parameterAt) const { const auto canonical = canonicalOperationName(operationName); if (canonical.empty() || arity > sites.size() || (orderedSites && orderedSites->size() != arity)) { @@ -709,7 +806,20 @@ bool CompilerTarget::Storage::supportsOperation( operation.arity().accepts(arity) && (!numParameters || operation.numParameters() == *numParameters) && (!orderedSites || operation.siteTuples().empty() || - operationSites[index].contains(*orderedSites)); + operationSites[index].contains(*orderedSites)) && + llvm::all_of(llvm::enumerate(operation.fixedParameters()), + [&](const auto entry) { + const auto expected = entry.value(); + if (!expected) { + return true; + } + const auto actual = parameterAt + ? parameterAt(entry.index()) + : std::nullopt; + return actual && + std::abs(*actual - *expected) <= + mqt::PARAMETER_COMPARISON_TOLERANCE; + }); }); } @@ -749,6 +859,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { OperationCapability::Arity::Kind::Variadic) && operation.arity().accepts(arity) && operation.numParameters() == numParameters && + operation.fixedParameters().empty() && operation.siteTuples().empty(); }); }; @@ -758,6 +869,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { }); }; std::optional singleQubit; + std::optional fixedRotation; if (supportsOnEverySite(GateKind::U)) { singleQubit = SingleQubitBasis::U; } else if (supportsOnEverySite(GateKind::X) && @@ -775,6 +887,53 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { } else if (supportsOnEverySite(GateKind::RY) && supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZYZ; + } else { + 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 (GateKind freeGate : {GateKind::RZ, GateKind::RX, GateKind::RY}) { + if (!supportsOnEverySite(freeGate)) { + 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; + } + 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; + } } const auto supportsOnEveryCoupling = [&](GateKind gate) { @@ -830,6 +989,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { .entangler = entangler == entanglerPreference.end() ? std::nullopt : std::optional{*entangler}, + .fixedRotation = fixedRotation, }; } @@ -975,10 +1135,24 @@ CompilerTarget::create(const mqt::CompilationTargetAttr attribute) { static_cast(operationAttr.getArity().getValue())) : OperationCapability::Arity::variadic( static_cast(operationAttr.getArity().getValue())); + std::vector> fixedParameters; + if (auto parameters = operationAttr.getFixedParameters()) { + for (Attribute parameter : parameters) { + auto value = dyn_cast(parameter); + if ((!value && !isa(parameter)) || + (value && !value.getType().isF64())) { + return invalidTarget("Compiler target fixed parameters must be " + "finite f64 values or unit"); + } + fixedParameters.emplace_back( + value ? std::optional{value.getValueAsDouble()} : std::nullopt); + } + } auto operation = OperationCapability::create( operationAttr.getName().getValue().str(), arity, static_cast(operationAttr.getNumParameters()), - std::move(siteTuples), operationAttr.getDuration(), fidelity); + std::move(siteTuples), operationAttr.getDuration(), fidelity, + std::move(fixedParameters)); if (!operation) { return operation.takeError(); } @@ -1126,6 +1300,22 @@ bool CompilerTarget::supportsOperation(StringRef operationName, size_t arity, sites); } +bool CompilerTarget::supportsOperation( + StringRef operationName, size_t arity, std::optional numParameters, + std::optional> sites, + ArrayRef> parameters) const { + if (!parameters.empty()) { + if (numParameters && *numParameters != parameters.size()) { + return false; + } + numParameters = parameters.size(); + } + return storage_->supportsOperation( + operationName, arity, numParameters, sites, false, [&](size_t index) { + return parameters.empty() ? std::nullopt : parameters[index]; + }); +} + bool CompilerTarget::supports(::mlir::Operation* operation) const { return supportsImpl(operation, std::nullopt); } @@ -1154,10 +1344,12 @@ bool CompilerTarget::supportsImpl(::mlir::Operation* operation, if (body.getNumQubits() != controlled.getNumTargets()) { return false; } - if (storage_->supportsOperation(body.getBaseSymbol(), - controlled.getNumQubits(), - body.getNumParams(), sites, - /*variadicOnly=*/true)) { + if (storage_->supportsOperation( + body.getBaseSymbol(), controlled.getNumQubits(), + body.getNumParams(), sites, + /*variadicOnly=*/true, [&](size_t index) { + return mqt::valueToDouble(body.getParameter(index)); + })) { return true; } if (controlled.getNumControls() != 1 || controlled.getNumTargets() != 1) { @@ -1182,9 +1374,11 @@ bool CompilerTarget::supportsImpl(::mlir::Operation* operation, return true; } } - return storage_->supportsOperation(unitary.getBaseSymbol(), - unitary.getNumQubits(), - unitary.getNumParams(), sites); + return storage_->supportsOperation( + unitary.getBaseSymbol(), unitary.getNumQubits(), unitary.getNumParams(), + sites, false, [&](size_t index) { + return mqt::valueToDouble(unitary.getParameter(index)); + }); } if (isa(operation)) { return storage_->supportsOperation("measure", 1, 0, sites); @@ -1270,10 +1464,20 @@ CompilerTarget::materialize(MLIRContext& context) const { : mqt::OperationArityKind::Variadic; const auto arityAttr = mqt::OperationArityAttr::get( &context, arityKind, operation.arity().value()); + ArrayAttr fixedParameters; + if (!operation.fixedParameters().empty()) { + SmallVector values; + for (const auto parameter : operation.fixedParameters()) { + values.push_back(parameter + ? Attribute(builder.getF64FloatAttr(*parameter)) + : Attribute(builder.getUnitAttr())); + } + fixedParameters = builder.getArrayAttr(values); + } operationAttrs.emplace_back(mqt::NativeOperationAttr::get( &context, builder.getStringAttr(operation.name()), arityAttr, operation.numParameters(), siteTupleAttrs, operation.duration(), - fidelityAttr)); + fidelityAttr, fixedParameters)); } const auto connectivity = connectivityKind() == Connectivity::Kind::AllToAll diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 91d0af2d05..92381fe59e 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -248,9 +248,10 @@ OperationArityAttr::verify(const function_ref emitError, LogicalResult NativeOperationAttr::verify( const function_ref emitError, const StringAttr name, - const OperationArityAttr arity, const uint64_t /*numParameters*/, + const OperationArityAttr arity, const uint64_t numParameters, const ArrayRef siteTuples, - const std::optional /*duration*/, const FloatAttr fidelity) { + const std::optional /*duration*/, const FloatAttr fidelity, + const ArrayAttr fixedParameters) { if (name.getValue().trim().empty()) { return emitError() << "compiler target operation name must not be empty"; } @@ -259,6 +260,23 @@ LogicalResult NativeOperationAttr::verify( return failure(); } + if (fixedParameters) { + if (fixedParameters.size() != numParameters) { + return emitError() << "compiler target fixed parameters must match its " + "parameter count"; + } + for (Attribute parameter : fixedParameters) { + if (isa(parameter)) { + continue; + } + auto value = dyn_cast(parameter); + if (!value || !value.getType().isF64() || !value.getValue().isFinite()) { + return emitError() << "compiler target fixed parameters must be finite " + "f64 values or unit"; + } + } + } + if (!siteTuples.empty() && arity.getKind() == OperationArityKind::Variadic) { return emitError() << "compiler target variadic operation cannot contain site tuples"; diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 5b5ab3f7e4..f173e56fbe 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -30,6 +30,7 @@ add_mlir_library( MLIRQCOTransformsIncGen) mqt_mlir_target_use_project_options(MLIRQCOTransforms) +target_include_directories(MLIRQCOTransforms PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) file(GLOB_RECURSE PASSES_HEADERS_SOURCE ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mqt/Dialect/QCO/Transforms/*.h) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 1fb1069db9..d593c4a8a2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -14,6 +14,8 @@ #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" @@ -23,6 +25,7 @@ #include "llvm/ADT/TypeSwitch.h" #include "llvm/Support/ErrorHandling.h" +#include #include #include #include @@ -32,7 +35,31 @@ namespace mlir::qco::decomposition { -bool isSingleQubitBasisGate(Operation* op, SingleQubitBasis basis) { +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 || @@ -40,12 +67,12 @@ bool isSingleQubitBasisGate(Operation* op, SingleQubitBasis basis) { basis == SingleQubitBasis::XZX || basis == SingleQubitBasis::ZSXX; }) - .Case([&](RYOp) { - return basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::XYX; - }) - .Case([&](RXOp) { - return basis == SingleQubitBasis::ZXZ || - basis == SingleQubitBasis::XZX || basis == SingleQubitBasis::XYX; + .Case([&](auto rotation) { + 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; }) @@ -193,6 +220,7 @@ 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); @@ -240,9 +268,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. @@ -260,8 +297,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)) { @@ -274,6 +312,7 @@ struct Unitary1QEulerPlan { case SingleQubitBasis::ZYZ: case SingleQubitBasis::ZXZ: case SingleQubitBasis::ZSXX: + case SingleQubitBasis::FixedRotation: appendRotation(SynthesisStep::Kind::RZ, angles.phi + angles.lambda); break; @@ -330,6 +369,22 @@ struct Unitary1QEulerPlan { angles.lambda); phase = angles.phase; break; + 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 + + 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: { constexpr double pi = std::numbers::pi; constexpr double halfPi = std::numbers::pi / 2.0; @@ -368,15 +423,47 @@ 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); + 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; } @@ -432,12 +519,13 @@ 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 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/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/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 330c2f3ba4..cf4ff20cb0 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -926,7 +926,8 @@ emitUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value qubit1, const auto emitFactor = [&](Value& wire, std::size_t index) { const auto synthesized = synthesizeUnitary1QEuler( builder, loc, wire, factors[index], /*runSize=*/0, - /*hasNonBasisGate=*/true, basis.singleQubit); + /*hasNonBasisGate=*/true, basis.singleQubit, + basis.fixedRotation ? &*basis.fixedRotation : nullptr); wire = synthesized->qubit; globalPhase += synthesized->globalPhase; }; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 281e920fab..4866e27aee 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -528,13 +528,15 @@ static LogicalResult synthesizeTargetOperation( return unsupported( "its unitary matrix is not available at compile time"); } - decomposition::synthesizeParameterizedUnitary1Q(rewriter, operation, - basis->singleQubit); + decomposition::synthesizeParameterizedUnitary1Q( + rewriter, operation, basis->singleQubit, + basis->fixedRotation ? &*basis->fixedRotation : nullptr); return success(); } const auto synthesized = decomposition::synthesizeUnitary1QEuler( rewriter, operation->getLoc(), op.getInputQubit(0), matrix, - /*runSize=*/1, /*hasNonBasisGate=*/true, basis->singleQubit); + /*runSize=*/1, /*hasNonBasisGate=*/true, basis->singleQubit, + basis->fixedRotation ? &*basis->fixedRotation : nullptr); if (!synthesized) { llvm::reportFatalInternalError( "target single-qubit basis failed to synthesize a unitary matrix"); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index 077e3aefc7..763c985afa 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" @@ -731,10 +733,11 @@ 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 || @@ -775,6 +778,39 @@ static Value emitRuntimeEulerAngles(RewriterBase& rewriter, Location loc, 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"); + 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::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; + } case decomposition::SingleQubitBasis::ZSXX: if (isConstantAngle(theta, std::numbers::pi / 2.0)) { const auto halfPi = @@ -997,6 +1033,7 @@ 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: @@ -1188,12 +1225,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; } @@ -1208,16 +1245,54 @@ void decomposition::synthesizeParameterizedUnitary1Q(RewriterBase& rewriter, } const bool usesDirectZYZAngles = basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::ZXZ || - basis == SingleQubitBasis::ZSXX; + basis == SingleQubitBasis::ZSXX || + 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); + 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); + } } rewriter.replaceOp(op, qubit); return; diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 82c48e9f1d..ce8a3bfddc 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -24,7 +24,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -410,6 +412,36 @@ TEST(CompilerQDMIAdapterTest, original, mlir::TargetEnvironment(original.target(), constrained)))); } +TEST(CompilerQDMIAdapterTest, CompatibilityPreservesFixedParameters) { + const auto makeEnvironment = + [](std::vector> parameters) { + const auto operation = + llvm::cantFail(CompilerTarget::OperationCapability::create( + "rx", 1, 1, {}, std::nullopt, std::nullopt, + std::move(parameters))); + return mlir::TargetEnvironment( + llvm::cantFail(CompilerTarget::create( + 1, CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::fromOperations({operation}))), + llvm::cantFail(mlir::payloadSpecificationForProgramFormat( + QDMI_PROGRAM_FORMAT_QASM3))); + }; + const auto fixed = makeEnvironment({std::numbers::pi / 2.}); + EXPECT_FALSE(llvm::errorToBool(mlir::validateTargetCompatibility( + fixed, makeEnvironment({std::numbers::pi / 2.})))); + for (const auto& changed : { + makeEnvironment({std::numbers::pi / 4.}), + makeEnvironment({std::nullopt}), + }) { + EXPECT_TRUE( + llvm::errorToBool(mlir::validateTargetCompatibility(fixed, changed))); + EXPECT_TRUE( + llvm::errorToBool(mlir::validateTargetCompatibility(changed, fixed))); + } + EXPECT_FALSE(llvm::errorToBool(mlir::validateTargetCompatibility( + makeEnvironment({}), makeEnvironment({std::nullopt})))); +} + TEST(CompilerQDMIAdapterTest, CompilationCreatesNoJobAndSubmissionChecksContract) { auto library = std::make_shared( diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 2afaa37c86..cb96737d6e 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -24,6 +24,7 @@ #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Diagnostics.h" #include "mlir/IR/DialectRegistry.h" #include "mlir/IR/Location.h" #include "mlir/IR/MLIRContext.h" @@ -40,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -804,6 +806,144 @@ 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::FixedRotation); + 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 a3979af1da..4db24471dc 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" @@ -29,6 +30,7 @@ #include "mlir/Dialect/Math/IR/Math.h" #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/IR/Block.h" +#include "mlir/IR/Builders.h" #include "mlir/IR/BuiltinAttributes.h" #include "mlir/IR/BuiltinOps.h" #include "mlir/IR/BuiltinTypes.h" @@ -44,6 +46,7 @@ #include "mlir/Pass/PassManager.h" #include "mlir/Support/LLVM.h" #include "mlir/Support/LogicalResult.h" +#include "mlir/Transforms/Passes.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringRef.h" @@ -320,6 +323,163 @@ TEST_F(TargetSynthesisTest, TargetPassesRequireTypedEnvironment) { << diagnostics; } +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., + 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)), + }; + 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 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); + } + } + } + } + } + } +} + +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, FixedParametersRejectNonNativeValues) { + const auto target = valid(Target::create( + 1, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(OperationCapability::create("rz", 1, 1, {}, std::nullopt, + std::nullopt, {0.25})), + }))); + for (double angle : {0.25, 0.5}) { + auto program = build([&](QCOProgramBuilder& builder) { + auto qubit = builder.rz(angle, builder.staticQubit(0)); + builder.sink(qubit); + return builder.intConstant(0); + }); + if (angle == 0.25) { + EXPECT_TRUE(mlir::succeeded(runTargetPass( + *program, target, mlir::qco::createVerifyTargetConformance()))); + } else { + EXPECT_FALSE( + expectTargetFailure(*program, target, + mlir::qco::createVerifyTargetConformance()) + .empty()); + EXPECT_NE(expectTargetFailure(*program, target, + mlir::qco::createTargetNativeSynthesis()) + .find("no usable synthesis basis"), + std::string::npos); + } + } +} + TEST_F(TargetSynthesisTest, TwoQubitGateFusionRequiresStrictImprovement) { const auto adjacentCx = [](QCOProgramBuilder& builder) { const auto q0Input = builder.staticQubit(0); diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 612f68d4a1..8936f98561 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -293,6 +293,8 @@ class CompilerTarget: site_tuples: Sequence[CompilerTarget.SiteTuple | Sequence[int]] | None = None, duration: int | None = None, fidelity: float | None = None, + *, + fixed_parameters: Sequence[float | None] = (), ) -> None: ... @property def name(self) -> str: @@ -314,6 +316,10 @@ class CompilerTarget: def site_tuples(self) -> list[CompilerTarget.SiteTuple]: """Supported ordered placements with optional calibration; empty means general applicability.""" + @property + def fixed_parameters(self) -> list[float | None]: + """Fixed values or None per parameter; empty means unrestricted. Constants use absolute tolerance 1e-15 without angle wrapping.""" + @property def duration(self) -> int | None: """The raw default duration, if available.""" @@ -374,6 +380,8 @@ class CompilerTarget: ZXZ = 6 + FixedRotation = 7 + class SynthesisBasis: """One synthesis basis usable across the complete target.""" @@ -484,9 +492,15 @@ class CompilerTarget: """A target-wide single-qubit basis with an optional entangler, or None when no single-qubit basis is usable.""" def supports_operation( - self, name: str, arity: int, num_parameters: int | None = None, sites: Sequence[int] | None = None + self, + name: str, + arity: int, + num_parameters: int | None = None, + sites: Sequence[int] | None = None, + *, + parameters: Sequence[float | None] = [], ) -> bool: - """Whether the target supports an operation.""" + """Whether the target supports an operation. Omitted or None parameter values require unrestricted support.""" class TargetEnvironment: """A compiler target and its selected payload specification.""" diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index bbd968c101..79aa2c55ec 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -655,6 +655,77 @@ 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.FixedRotation + 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"]) +@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.""" + 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("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()) <= {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 {} + assert np.allclose( + Operator(result.assign_parameters(bindings)).data, + Operator(source.assign_parameters(bindings)).data, + ) + + @requires_qiskit_translation def test_target_compilation_exports_canonical_physical_qiskit_circuit() -> None: """Export a mapped program with the complete compiler target."""