diff --git a/.agent/plans/fixed-parameter-targets.md b/.agent/plans/fixed-parameter-targets.md new file mode 100644 index 0000000000..de87b9ced9 --- /dev/null +++ b/.agent/plans/fixed-parameter-targets.md @@ -0,0 +1,153 @@ +# 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 + +- [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: + +```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 the 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 test/python/test_mlir*.py +uvx nox -s stubs +uvx nox -s cpp-lint +uvx nox -s lint +uvx nox --non-interactive -s docs +``` + +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..b1a26b1d1e 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 874d0c1496..ee5b2277aa 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -861,7 +861,9 @@ 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, + std::optional canonicalName) { constructFromExpected( self, mlir::CompilerTarget::OperationCapability::create( @@ -869,10 +871,13 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity)); + 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()) + "duration"_a = nb::none(), "fidelity"_a = nb::none(), nb::kw_only(), + "fixed_parameters"_a = std::vector>{}, + "canonical_name"_a = nb::none()) .def( "__init__", [](mlir::CompilerTarget::OperationCapability& self, std::string name, @@ -880,7 +885,9 @@ 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, + std::optional canonicalName) { constructFromExpected( self, mlir::CompilerTarget::OperationCapability::create( @@ -888,10 +895,13 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); std::move(siteTuples) .value_or( std::vector{}), - duration, fidelity)); + 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()) + "duration"_a = nb::none(), "fidelity"_a = nb::none(), nb::kw_only(), + "fixed_parameters"_a = std::vector>{}, + "canonical_name"_a = nb::none()) .def_prop_ro( "name", [](const mlir::CompilerTarget::OperationCapability& operation) { @@ -917,6 +927,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.") @@ -1210,6 +1229,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( @@ -1217,22 +1237,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.sig("()") = 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..7eb7ae43b4 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -138,6 +138,9 @@ 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] = (), + canonical_name: str | None = 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/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 59494f41f3..ed68b952d7 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -180,6 +180,36 @@ placements without calibration in this list, and omit operations that are not available anywhere. Structural and program-format constructs are not compiler-target operations. +`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 calibration to a placement; both forms can appear in the same list. @@ -191,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 6381378d53..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. @@ -196,19 +197,23 @@ 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 = {}, + std::optional canonicalName = std::nullopt); /// 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 = {}, + 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. @@ -217,6 +222,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 +243,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_; @@ -293,7 +307,7 @@ 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. + 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(λ)`. @@ -301,10 +315,21 @@ class CompilerTarget { ZXZ, ///< `RZ(φ) * RX(θ) * RZ(λ)`. }; + /// Native RX pulses implementing the X rotations of the ZSXX basis. + struct RXPulses { + double quarterTurnAngle; + std::optional halfTurnAngle; + + 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 rxPulses; + /// Whether the named X shortcut is available when using native SX gates. + bool hasX = true; friend bool operator==(const SynthesisBasis&, const SynthesisBasis&) = default; @@ -400,16 +425,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; @@ -428,7 +461,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/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 bb0d81ebec..999f846b3d 100644 --- a/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mqt/Dialect/MQT/IR/MQTDialect.td @@ -212,7 +212,11 @@ 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. + 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 #mqt.native_operation { "uint64_t":$num_parameters, MQTArrayRefParameter<"SiteTupleAttr">:$site_tuples, MQTOptionalUInt64Parameter<>:$duration, - OptionalParameter<"FloatAttr">:$fidelity); + OptionalParameter<"FloatAttr">:$fidelity, + 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 e639da2daa..ca689e1a6a 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -86,10 +86,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::SynthesisBasis* targetBasis = nullptr); /// Materializes one accumulated phase correction when needed. /// @@ -104,10 +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. +/// The optional target basis selects native pulse forms and shortcuts. /// /// @pre `canSynthesizeParameterizedUnitary1Q(op)` is true. -void synthesizeParameterizedUnitary1Q(RewriterBase& rewriter, Operation* op, - SingleQubitBasis basis); +void synthesizeParameterizedUnitary1Q( + RewriterBase& rewriter, Operation* op, SingleQubitBasis basis, + 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`). @@ -124,6 +126,7 @@ void populateFuseSingleQubitUnitaryRunsPatterns( /// /// The patterns emit @p basis directly. With @p target, preserve native runs /// and only use direct Euler identities, keeping optional fusion exportable. +/// ZSXX targets only merge adjacent RZ operations symbolically. void populateParameterizedSingleQubitRunCompositionPatterns( RewritePatternSet& patterns, SingleQubitBasis basis, const CompilerTarget* target = nullptr); 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/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 f1b6800111..4cc6339415 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,25 +380,33 @@ 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, + std::optional canonicalName) { return create(std::move(name), Arity::fixed(arity), numParameters, - std::move(siteTuples), duration, fidelity); + std::move(siteTuples), duration, fidelity, + std::move(fixedParameters), std::move(canonicalName)); } llvm::Expected -CompilerTarget::OperationCapability::create(std::string name, Arity arity, - size_t numParameters, - std::vector siteTuples, - std::optional duration, - std::optional fidelity) { - auto canonicalName = canonicalOperationName(name); - if (canonicalName.empty()) { +CompilerTarget::OperationCapability::create( + std::string name, Arity arity, size_t numParameters, + std::vector siteTuples, std::optional duration, + std::optional fidelity, + 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); @@ -416,6 +425,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,17 +451,19 @@ CompilerTarget::OperationCapability::create(std::string name, Arity arity, } } - return OperationCapability(std::move(name), std::move(canonicalName), arity, + return OperationCapability(std::move(name), std::move(canonical), 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 +484,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 +540,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 +713,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 +742,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 +795,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { OperationCapability::Arity::Kind::Variadic) && operation.arity().accepts(arity) && operation.numParameters() == numParameters && + operation.fixedParameters().empty() && operation.siteTuples().empty(); }); }; @@ -760,12 +805,14 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { }); }; std::optional singleQubit; + 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) && @@ -777,6 +824,35 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { } else if (supportsOnEverySite(GateKind::RY) && supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZYZ; + } else if (supportsOnEverySite(GateKind::RZ)) { + const auto supportsPulse = [&](double angle) { + return llvm::all_of(siteIds, [&](SiteId site) { + return supportsOperation( + "rx", 1, 1, ArrayRef(&site, 1), false, + [angle](size_t) { return std::optional{angle}; }); + }); + }; + for (double quarter : {std::numbers::pi / 2., -std::numbers::pi / 2.}) { + if (!supportsPulse(quarter)) { + continue; + } + rxPulses = { + .quarterTurnAngle = quarter, + .halfTurnAngle = std::nullopt, + }; + for (double half : {std::numbers::pi, -std::numbers::pi}) { + if (supportsPulse(half)) { + rxPulses->halfTurnAngle = half; + break; + } + } + singleQubit = SingleQubitBasis::ZSXX; + break; + } + } + + if (!singleQubit) { + return std::nullopt; } const auto supportsOnEveryCoupling = [&](GateKind gate) { @@ -824,14 +900,13 @@ 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}, + .rxPulses = rxPulses, + .hasX = hasX, }; } @@ -977,10 +1052,27 @@ 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), + operationAttr.getCanonicalName() + ? std::optional{operationAttr.getCanonicalName().getValue().str()} + : std::nullopt); if (!operation) { return operation.takeError(); } @@ -1148,6 +1240,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 +1284,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 +1314,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); @@ -1229,7 +1341,7 @@ ArrayRef CompilerTarget::supportedGates() const noexcept { return storage_->supportedGates; } -std::optional +const std::optional& CompilerTarget::synthesisBasis() const noexcept { return storage_->basis; } @@ -1292,10 +1404,23 @@ 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, + 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 e9a7155af9..d823b3ffed 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -129,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; diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index f1a19761a1..7d867548b1 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -250,17 +250,39 @@ 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, 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(); } + 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..e99307996c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -261,7 +261,8 @@ 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 SingleQubitBasis basis, + const CompilerTarget::SynthesisBasis* targetBasis) { if (isNearZeroRotationAngle(angles.theta) && isNearZeroRotationAngle(angles.phi) && isNearZeroRotationAngle(angles.lambda)) { @@ -334,26 +335,50 @@ struct Unitary1QEulerPlan { 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; } } @@ -368,15 +393,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::SynthesisBasis* targetBasis) { Unitary1QEulerPlan plan; if (targetMatrix.isApprox(Matrix2x2::identity())) { return plan; } const EulerAngles angles = anglesFromUnitary(targetMatrix, basis); - plan.appendDecomposition(angles, basis); + plan.appendDecomposition(angles, basis, targetBasis); return plan; } @@ -436,8 +461,10 @@ 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); + const SingleQubitBasis basis, + const CompilerTarget::SynthesisBasis* targetBasis) { + const Unitary1QEulerPlan plan = + 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 809a5c167c..9c0ba0611f 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"); @@ -921,7 +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); + /*hasNonBasisGate=*/true, basis.singleQubit, &basis); wire = synthesized->qubit; globalPhase += synthesized->globalPhase; }; 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/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index aa73250a3b..914f8b5bd4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -155,9 +155,12 @@ struct FuseSingleQubitUnitaryRunsPattern final } FusableRunScan run = scanFusableRun(op, *headMatrix, basis, target); + 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); + 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 bd5ca0c8ee..619226d11a 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; } @@ -1091,13 +1091,13 @@ 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); 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); if (!synthesized) { llvm::reportFatalInternalError( "target single-qubit basis failed to synthesize a unitary matrix"); @@ -1182,7 +1182,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) { @@ -1229,7 +1229,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, @@ -1265,12 +1265,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; } @@ -1342,7 +1341,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; @@ -1427,6 +1426,16 @@ struct TargetNativeSynthesisPass final signalPassFailure(); return; } + if (targetBasis && + targetBasis->singleQubit == CompilerTarget::SingleQubitBasis::ZSXX) { + 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 7f74d0879d..40bc8ff945 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 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() || + (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)) { @@ -747,10 +789,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::SynthesisBasis* targetBasis = nullptr) { auto [theta, phi, lambda, phase] = angles; const bool usesZYZAngles = basis == decomposition::SingleQubitBasis::ZYZ || @@ -791,25 +834,59 @@ static Value emitRuntimeEulerAngles(RewriterBase& rewriter, Location loc, qubit = UOp::create(rewriter, loc, qubit, theta.v, phi.v, lambda.v) .getQubitOut(); 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; + 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 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; } - phase = phase + consts.pi / consts.two; - qubit = emitRotationIfNeeded(rewriter, loc, qubit, lambda); - qubit = SXOp::create(rewriter, loc, qubit).getQubitOut(); + 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; + } + 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); @@ -1299,9 +1376,9 @@ 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::SynthesisBasis* targetBasis) { assert(canSynthesizeParameterizedUnitary1Q(op) && "operation must support parameterized one-qubit synthesis"); if (isSingleQubitBasisGate(op, basis)) { @@ -1326,9 +1403,10 @@ void decomposition::synthesizeParameterizedUnitary1Q(RewriterBase& rewriter, 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); + qubit = emitRuntimeEulerAngles(rewriter, op->getLoc(), qubit, angles, + basis, consts, targetBasis); } rewriter.replaceOp(op, qubit); return; @@ -1348,9 +1426,13 @@ namespace mlir::qco::decomposition { void populateParameterizedSingleQubitRunCompositionPatterns( RewritePatternSet& patterns, SingleQubitBasis basis, const CompilerTarget* target) { + RZOp::getCanonicalizationPatterns(patterns, patterns.getContext()); + if (basis == SingleQubitBasis::ZSXX && target != nullptr) { + 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_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index b791c619aa..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,6 +4020,32 @@ TEST_F(CompilerPipelineTest, TargetSynthesisResynthesizesTwoQubitBlocks) { makeSparseUCZTarget(true), makePayloadSpecification()))); } +TEST_F(CompilerPipelineTest, TargetPipelinesRequireSynthesisBasis) { + using Capability = CompilerTarget::OperationCapability; + for (const auto& native : { + llvm::cantFail(Capability::create("s", 1, 0)), + llvm::cantFail(Capability::create("rx", 1, 1, {}, std::nullopt, + std::nullopt, {0.37})), + }) { + 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); + } + } +} + TEST_F(CompilerPipelineTest, TargetCompilationFusesOnlyWithUsableNativeBasis) { using NativeOperations = CompilerTarget::NativeOperations; using OperationCapability = CompilerTarget::OperationCapability; @@ -4093,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_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( diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 1a3d46722f..7f497b04e7 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,169 @@ TEST(CompilerTargetTest, EnforcesExactOrderedOperationApplicability) { target.supportsOperation("device.operation", 3, 0, {30, 20, 10})); } +TEST(CompilerTargetTest, MatchesFixedParametersAndPreservesPlacements) { + const auto rotation = valid(OperationCapability::create( + "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}))); + 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].name(), "prx_90"); + EXPECT_EQ(restored.operations()[0].canonicalName(), "r"); + 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::ZSXX); + EXPECT_FALSE(target.synthesisBasis()->entangler); +} + +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({ + 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, 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 37d021ec88..8248137f44 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,221 @@ TEST_F(TargetSynthesisTest, TargetPassesRequireTypedEnvironment) { << diagnostics; } +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.value_or(0.) << " " << halfTurn.value_or(0.)); + std::vector operations{ + 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( + 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)); + 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, FixedPulseFusionPreservesNativeAngles) { + for (const auto angle : {std::numbers::pi / 2, -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, 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); + } +} + +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); @@ -499,6 +715,47 @@ TEST_F(TargetSynthesisTest, ReadOnlyNativeCostMatchesSynthesis) { } } +TEST_F(TargetSynthesisTest, NativeCostMatchesFixedPulseSynthesis) { + const auto target = valid(Target::create( + 2, Connectivity::allToAll(), + NativeOperations::fromOperations({ + 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)), + }))); + 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(), 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 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 e8a98dc537..2469c558be 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -654,6 +654,105 @@ 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.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]) + 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("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("rx", 1, 1, fixed_parameters=[pulse_angle]), + 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 == [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( + Operator(result.assign_parameters(bindings)).data, + Operator(source.assign_parameters(bindings)).data, + ) + + +@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.""" 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 94f98d7292..31190c374e 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("cz", 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", "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 assert sorted(layout.initial_index_layout()) == list(range(4))