Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ebc5f7e
✨ Support fixed-parameter compiler targets
simon1hofmann Sep 18, 2026
9996221
✨ Derive synthesis from fixed X/Y pulse angles
simon1hofmann Sep 18, 2026
0756b4e
✨ Generalize fixed-pulse synthesis across axes
simon1hofmann Sep 18, 2026
ef290ca
🎨 Apply C++ lint to generic pulse synthesis
simon1hofmann Sep 18, 2026
8dac747
📝 Record generic pulse validation
simon1hofmann Sep 18, 2026
b29be1a
🐛 Compare fixed parameters in target contracts
simon1hofmann Sep 18, 2026
12ced14
🧪 Cover symbolic fixed-pulse synthesis
simon1hofmann Sep 18, 2026
c79d81a
♻️ Share fixed-pulse synthesis recipes
simon1hofmann Sep 21, 2026
a15954a
🔧 Scope pulse synthesis includes to their library
simon1hofmann Sep 21, 2026
7eb869f
🐛 Preserve fixed-pulse lowering during fusion
simon1hofmann Sep 25, 2026
453e825
⚡ Reuse cached fixed-pulse bases during routing
simon1hofmann Sep 29, 2026
6ef8f84
📝 Clarify native-only target compilation limits
simon1hofmann Sep 29, 2026
d129e20
🐛 Preserve native gate sequences during target cleanup
simon1hofmann Sep 29, 2026
a343bff
🎨 Match pass hook visibility
simon1hofmann Sep 29, 2026
2fb7694
♻️ Simplify target synthesis integration
simon1hofmann Sep 29, 2026
ea046ec
♻️ Trim target setup and cleanup branches
simon1hofmann Sep 29, 2026
936976b
♻️ Centralize target cleanup selection
simon1hofmann Sep 29, 2026
efa27fb
🐛 Align fixed-target Python API conventions
simon1hofmann Sep 30, 2026
74593d9
♻️ Limit fixed-pulse synthesis to native RX gates
burgholzer Oct 1, 2026
284d8b7
🧪 Cover fixed-pulse targets with routed layouts
burgholzer Oct 1, 2026
fd433a2
♻️ Reuse ZSXX for constrained target pulses
burgholzer Oct 2, 2026
4c72462
🐛 Rebuild documentation artifacts in nox
burgholzer Oct 2, 2026
d1cb96f
🐛 Pass the optional native name directly
simon1hofmann Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions .agent/plans/fixed-parameter-targets.md
Original file line number Diff line number Diff line change
@@ -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
71 changes: 58 additions & 13 deletions bindings/mlir/qiskit/Qiskit2_5.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -40,6 +41,7 @@
#include <limits>
#include <memory>
#include <optional>
#include <ranges>
#include <set>
#include <span>
#include <stdexcept>
Expand Down Expand Up @@ -2221,17 +2223,29 @@ class NativeCircuitWriter final : public CircuitWriter {
const std::vector<uint32_t>& qubits,
const std::vector<Parameter>& 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, {}, {},
Expand All @@ -2240,6 +2254,7 @@ class NativeCircuitWriter final : public CircuitWriter {
});
})) {
gate = candidate;
nativeName = operation.name().str();
break;
}
}
Expand All @@ -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;
})) {
Expand All @@ -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")
Expand Down Expand Up @@ -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<std::string> names;
for (auto item : nb::borrow<nb::iterable>(operationNames.is_none()
Expand Down Expand Up @@ -2828,21 +2859,34 @@ class NativeTranslation final : public VersionedTranslation {

std::string reason;
size_t numParameters = 0;
std::vector<std::optional<double>> fixedParameters;
if (nb::isinstance(instruction, circuit.attr("ControlledGate")) &&
nb::cast<uint64_t>(instruction.attr("ctrl_state")) !=
closedControlState(
nb::cast<uint64_t>(instruction.attr("num_ctrl_qubits")))) {
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<nb::object>(numParameters, parameter);
bool supported = nb::cast<bool>(target.attr("instruction_supported")(
operationName, nb::arg("parameters") = slots));
bool supported = true;
for (auto value : nb::borrow<nb::iterable>(parameters)) {
if (nb::isinstance(value, circuit.attr("ParameterExpression")) &&
nb::len(nb::borrow<nb::object>(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<bool>(target.attr("gate_has_angle_bounds")(
operationName))) {
for (const auto bound : {
Expand Down Expand Up @@ -2889,7 +2933,8 @@ class NativeTranslation final : public VersionedTranslation {
}
const auto arity = nb::cast<size_t>(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;
Expand Down
Loading
Loading