From 781bf6055ed269482686aff718e3ccb29232c093 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sun, 23 Aug 2026 17:31:56 +0000 Subject: [PATCH 01/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Generalize=20compile?= =?UTF-8?q?r=20target=20facts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Represent unknown, unrestricted, and explicit connectivity and native-operation facts separately. Use site and arity vocabulary, and make target passes request facts only when needed. Assisted-by: OpenAI Codex --- .agent/plans/generalize-compiler-target.md | 150 ++++++++ bindings/mlir/qiskit/QiskitExport.cpp | 2 +- bindings/mlir/register_mlir.cpp | 176 ++++++--- bindings/patterns.txt | 20 +- docs/mlir/target_compilation.md | 18 +- mlir/include/mlir/Compiler/Target.h | 137 +++++-- mlir/lib/Compiler/QDMIAdapter.cpp | 54 ++- mlir/lib/Compiler/Target.cpp | 335 +++++++++++------- .../QCO/Transforms/Mapping/Mapping.cpp | 19 +- .../NativeSynthesis/TargetSynthesis.cpp | 20 +- .../Compiler/test_compiler_pipeline.cpp | 4 +- .../Compiler/test_compiler_qdmi_adapter.cpp | 30 +- .../Compiler/test_compiler_target.cpp | 147 ++++---- .../QCO/Transforms/Mapping/test_mapping.cpp | 60 +++- .../NativeSynthesis/test_target_synthesis.cpp | 103 ++++-- python/mqt/core/mlir.pyi | 106 ++++-- test/python/test_mlir.py | 43 ++- test/python/test_mlir_qiskit_translation.py | 11 +- 18 files changed, 994 insertions(+), 441 deletions(-) create mode 100644 .agent/plans/generalize-compiler-target.md diff --git a/.agent/plans/generalize-compiler-target.md b/.agent/plans/generalize-compiler-target.md new file mode 100644 index 0000000000..ef1fa5f603 --- /dev/null +++ b/.agent/plans/generalize-compiler-target.md @@ -0,0 +1,150 @@ +# Generalize compiler target facts + +This ExecPlan is a living document. The sections `Progress`, +`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must +be kept up to date as work proceeds. + +This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the +repository root. + +## Purpose / Big Picture + +The compiler target currently treats missing topology as all-to-all connectivity, +missing native operations as unrestricted support, and names every quantum +resource a qubit. After this change, target descriptions can represent neutral +atoms, trapped ions, photonic modes, spin qubits, and other site-based systems +without claiming facts that a provider did not report. A focused compiler target +test demonstrates the three knowledge states: unknown, unrestricted, and an +explicit list. + +## Progress + +- [x] (2026-08-23 15:50Z) Inspected the public target API, storage validation, + mapping, synthesis, QDMI adapter, bindings, and tests. +- [x] (2026-08-23 17:08Z) Added explicit connectivity and native-operation + knowledge states. +- [x] (2026-08-23 17:08Z) Replaced target qubit-count vocabulary with site + count and operation arity. +- [x] (2026-08-23 17:12Z) Updated compiler consumers, QDMI construction, + public bindings, Python tests, and documentation. +- [x] (2026-08-23 17:20Z) Made passes request target facts only when the + residual program needs them and made QDMI operation applicability fail + closed when the provider does not report it. +- [x] (2026-08-23 17:25Z) Regenerated bindings and ran focused clang-tidy on + every changed C++ source and test file. +- [ ] Add the pull request reference to the launch changelog entry. +- [x] (2026-08-23 17:31Z) Ran the compiler, mapping, synthesis, and Python + tests; regenerated stubs; ran focused clang-tidy, full lint, and the final + diff checks. +- [ ] Publish the signed pull request. + +## Surprises & Discoveries + +- Observation: The existing `std::optional>` parameters use + absence to mean unrestricted support, so they cannot represent unknown + metadata. Evidence: the class comment and `Storage::supportsOperation` in + `mlir/lib/Compiler/Target.cpp`. +- Observation: Tests must compare `std::optional` with `true`, `false`, + or `std::nullopt`; `EXPECT_TRUE` and `EXPECT_FALSE` inspect only whether the + optional has a value. Evidence: the first focused compiler test run exposed + this test-only error. +- Observation: QDMI operation site applicability is optional. Treating an + unavailable site list as global support promoted missing metadata to a native + operation claim. Evidence: `QDMI_OPERATION_PROPERTY_SITES` defines the valid + site tuples, while `Operation::getSites()` returns `std::nullopt` when the + provider does not report the property. + +## Decision Log + +- Decision: Describe hardware through facts rather than a modality enum. + Rationale: sites, connectivity, operations, and optional calibration data are + useful across hardware modalities, while an enum would force technology + switches into compiler passes. Date/Author: 2026-08-23, Codex. +- Decision: Use explicit unknown, unrestricted, and explicit states for both + connectivity and native operations. Rationale: missing provider metadata must + not grant support. Date/Author: 2026-08-23, Codex. +- Decision: Keep this prerequisite free of MLIR target attributes and QDMI + program features. Rationale: the following target-environment change will + serialize this validated contract. Date/Author: 2026-08-23, Codex. +- Decision: A pass diagnoses unknown metadata only when a surviving operation + needs that fact. Rationale: program requirements are stage-relative; a + classical or single-site program does not need native-operation or topology + claims. Date/Author: 2026-08-23, Codex. + +## Outcomes & Retrospective + +The context-free target contract is implemented. The compiler, mapping, +synthesis, and focused Python suites pass. Generated stubs are current. Focused +clang-tidy, full lint, and final diff checks pass. The changelog reference and +publication remain. + +## Context and Orientation + +`mlir/include/mlir/Compiler/Target.h` defines the public immutable +`CompilerTarget`. `mlir/lib/Compiler/Target.cpp` validates it and caches routing +and synthesis facts. Mapping and synthesis passes under +`mlir/lib/Dialect/QCO/Transforms/` consume those facts. The QDMI adapter in +`mlir/lib/Compiler/QDMIAdapter.cpp` constructs a target from device metadata. +Tests live in `mlir/unittests/Compiler/test_compiler_target.cpp` and adjacent +mapping and synthesis test directories. + +Unknown means the provider did not report enough information. Unrestricted +means every site pair or operation is accepted. Explicit means the target lists +the accepted couplings or operations. A pass that requires unknown information +must emit a diagnostic instead of assuming support. + +## Plan of Work + +Add small value types to `CompilerTarget` for connectivity and native-operation +support. Each type carries a three-way kind and, for the explicit kind, the +existing vector. Make target construction accept these values and default them +to unknown. Rename target `numQubits()` to `numSites()` and operation +`numQubits()` to `arity()`. Update mapping, synthesis, the QDMI adapter, bindings, +and tests to use the new vocabulary and to handle unknown facts before querying +routes or operation support. + +Keep site identifiers, ordered operation site tuples, timing units, T1/T2 data, +and fidelity values unchanged. Add no technology enum and no generic property +container. Add the pull request reference to the existing general Compiler +Collection changelog entry. + +## Concrete Steps + +From the repository root, edit the target header and implementation, then use +`rg` to update every caller. Build and run: + + cmake --preset release + cmake --build --preset release --target mqt-core-mlir-unittests-compiler + ./build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler + uvx nox -s lint + +Run the focused mapping and synthesis binaries discovered from their CMake +targets when those sources change. All commands are repeatable. + +## Validation and Acceptance + +Target tests must prove that unknown topology is distinct from all-to-all and +explicit topology, and that unknown native operations are distinct from all and +an explicit list. Existing explicit target mapping and synthesis tests must +still pass. The build must contain no old public `numQubits()` or operation +qubit-count references. `uvx nox -s lint` and `git diff --check` must pass. + +## Idempotence and Recovery + +Builds and tests are safe to repeat. Preserve unrelated worktree changes. Before +rewriting a published branch, record the remote head and create a backup ref. +Use an exact force-with-lease and verify every signed commit before pushing. + +## Artifacts and Notes + +The current behavior to replace is summarized by the existing class comment: + + An absent topology means all-to-all connectivity. An absent operation set + means that every operation is native. + +## Interfaces and Dependencies + +Use LLVM containers already linked by the compiler. Do not add dependencies. +The public target keeps shared immutable storage. Connectivity and native +operation state are context-free C++ values so the later MLIR attribute layer +can materialize them without making `CompilerTarget` depend on an MLIR context. diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index 11ed4805bb..4c2aacd1d2 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -2109,7 +2109,7 @@ nb::object exportCircuit(const mlir::QCProgram& program, ExportState state; collectParameters(function, state); if (target != nullptr) { - state.numQubits = checkedIndex(static_cast(target->numQubits()), + state.numQubits = checkedIndex(static_cast(target->numSites()), "target qubit count"); } collectResources(function, state, target); diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 27e0382eea..e8ae5c1075 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -356,8 +356,8 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { auto compilerTarget = nb::class_( m, "CompilerTarget", R"pb(Immutable MLIR compiler target. -An absent topology means all-to-all connectivity. An absent operation set -means every operation is native.)pb"); +Connectivity and native-operation metadata distinguish unknown, +unrestricted, and explicitly enumerated support.)pb"); auto durationUnit = nb::class_( compilerTarget, "DurationUnit", "Unit for raw target timing metadata."); @@ -444,7 +444,7 @@ means every operation is native.)pb"); .def( "__init__", [](mlir::CompilerTarget::Operation& self, std::string name, - const size_t numQubits, const size_t numParameters, + const size_t arity, const size_t numParameters, std::optional> siteTuples, const std::optional duration, @@ -452,15 +452,14 @@ means every operation is native.)pb"); constructFromExpected( self, mlir::CompilerTarget::Operation::create( - std::move(name), numQubits, numParameters, + std::move(name), arity, numParameters, std::move(siteTuples) .value_or( std::vector{}), duration, fidelity)); }, - "name"_a, "num_qubits"_a, "num_parameters"_a, - "site_tuples"_a = nb::none(), "duration"_a = nb::none(), - "fidelity"_a = nb::none()) + "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), + "duration"_a = nb::none(), "fidelity"_a = nb::none()) .def_prop_ro( "name", [](const mlir::CompilerTarget::Operation& operation) { @@ -473,7 +472,7 @@ means every operation is native.)pb"); return operation.canonicalName().str(); }, "The normalized compiler operation name.") - .def_prop_ro("num_qubits", &mlir::CompilerTarget::Operation::numQubits, + .def_prop_ro("arity", &mlir::CompilerTarget::Operation::arity, "The fixed operation arity.") .def_prop_ro("num_parameters", &mlir::CompilerTarget::Operation::numParameters, @@ -529,71 +528,138 @@ means every operation is native.)pb"); .def_ro("entangler", &mlir::CompilerTarget::SynthesisBasis::entangler, "The two-qubit entangler."); + nb::enum_( + compilerTarget, "ConnectivityKind", "How target connectivity is known.") + .value("UNKNOWN", mlir::CompilerTarget::Connectivity::Kind::Unknown) + .value("ALL_TO_ALL", mlir::CompilerTarget::Connectivity::Kind::AllToAll) + .value("EXPLICIT", mlir::CompilerTarget::Connectivity::Kind::Explicit); + + auto connectivity = nb::class_( + compilerTarget, "Connectivity", "A target connectivity claim."); + connectivity.def(nb::init<>(), "Create an unknown connectivity claim.") + .def( + "__init__", + [](mlir::CompilerTarget::Connectivity& self, + std::vector couplings) { + new (&self) mlir::CompilerTarget::Connectivity( + mlir::CompilerTarget::Connectivity::fromCouplings( + std::move(couplings))); + }, + "couplings"_a, "Create an explicit connectivity claim.") + .def_static("all_to_all", &mlir::CompilerTarget::Connectivity::allToAll, + "Create an all-to-all connectivity claim.") + .def_prop_ro("kind", &mlir::CompilerTarget::Connectivity::kind, + "How the connectivity is known.") + .def_prop_ro( + "couplings", + [](const mlir::CompilerTarget::Connectivity& value) { + return std::vector( + value.couplings().begin(), value.couplings().end()); + }, + "The explicit couplings, if present."); + + nb::enum_( + compilerTarget, "NativeOperationsKind", + "How native target operations are known.") + .value("UNKNOWN", mlir::CompilerTarget::NativeOperations::Kind::Unknown) + .value("UNRESTRICTED", + mlir::CompilerTarget::NativeOperations::Kind::Unrestricted) + .value("EXPLICIT", + mlir::CompilerTarget::NativeOperations::Kind::Explicit); + + auto nativeOperations = nb::class_( + compilerTarget, "NativeOperations", "A native-operation claim."); + nativeOperations + .def(nb::init<>(), "Create an unknown native-operation claim.") + .def( + "__init__", + [](mlir::CompilerTarget::NativeOperations& self, + std::vector operations) { + new (&self) mlir::CompilerTarget::NativeOperations( + mlir::CompilerTarget::NativeOperations::fromOperations( + std::move(operations))); + }, + "operations"_a, "Create an explicit native-operation claim.") + .def_static("unrestricted", + &mlir::CompilerTarget::NativeOperations::unrestricted, + "Create an unrestricted native-operation claim.") + .def_prop_ro("kind", &mlir::CompilerTarget::NativeOperations::kind, + "How the native operations are known.") + .def_prop_ro( + "operations", + [](const mlir::CompilerTarget::NativeOperations& value) { + return std::vector( + value.operations().begin(), value.operations().end()); + }, + "The explicit operations, if present."); + compilerTarget .def( "__init__", - [](mlir::CompilerTarget& self, const size_t numQubits, - std::optional> - couplings, - std::optional> - operations, + [](mlir::CompilerTarget& self, const size_t numSites, + mlir::CompilerTarget::Connectivity connectivity, + mlir::CompilerTarget::NativeOperations nativeOperations, std::optional durationUnit) { constructFromExpected(self, mlir::CompilerTarget::create( - numQubits, std::move(couplings), - std::move(operations), + numSites, std::move(connectivity), + std::move(nativeOperations), std::move(durationUnit))); }, - "num_qubits"_a, nb::kw_only(), "couplings"_a = nb::none(), - "operations"_a = nb::none(), "duration_unit"_a = nb::none()) + "num_sites"_a, nb::kw_only(), + "connectivity"_a = mlir::CompilerTarget::Connectivity{}, + "native_operations"_a = mlir::CompilerTarget::NativeOperations{}, + "duration_unit"_a = nb::none()) .def( "__init__", [](mlir::CompilerTarget& self, std::string name, - const size_t numQubits, - std::optional> - couplings, - std::optional> - operations, + const size_t numSites, + mlir::CompilerTarget::Connectivity connectivity, + mlir::CompilerTarget::NativeOperations nativeOperations, std::optional durationUnit) { constructFromExpected( - self, mlir::CompilerTarget::create( - std::move(name), numQubits, std::move(couplings), - std::move(operations), std::move(durationUnit))); + self, mlir::CompilerTarget::create(std::move(name), numSites, + std::move(connectivity), + std::move(nativeOperations), + std::move(durationUnit))); }, - "name"_a, "num_qubits"_a, nb::kw_only(), "couplings"_a = nb::none(), - "operations"_a = nb::none(), "duration_unit"_a = nb::none()) + "name"_a, "num_sites"_a, nb::kw_only(), + "connectivity"_a = mlir::CompilerTarget::Connectivity{}, + "native_operations"_a = mlir::CompilerTarget::NativeOperations{}, + "duration_unit"_a = nb::none()) .def( "__init__", [](mlir::CompilerTarget& self, std::vector sites, - std::optional> - couplings, - std::optional> - operations, + mlir::CompilerTarget::Connectivity connectivity, + mlir::CompilerTarget::NativeOperations nativeOperations, std::optional durationUnit) { constructFromExpected( - self, mlir::CompilerTarget::create( - std::move(sites), std::move(couplings), - std::move(operations), std::move(durationUnit))); + self, mlir::CompilerTarget::create(std::move(sites), + std::move(connectivity), + std::move(nativeOperations), + std::move(durationUnit))); }, - "sites"_a, nb::kw_only(), "couplings"_a = nb::none(), - "operations"_a = nb::none(), "duration_unit"_a = nb::none()) + "sites"_a, nb::kw_only(), + "connectivity"_a = mlir::CompilerTarget::Connectivity{}, + "native_operations"_a = mlir::CompilerTarget::NativeOperations{}, + "duration_unit"_a = nb::none()) .def( "__init__", [](mlir::CompilerTarget& self, std::string name, std::vector sites, - std::optional> - couplings, - std::optional> - operations, + mlir::CompilerTarget::Connectivity connectivity, + mlir::CompilerTarget::NativeOperations nativeOperations, std::optional durationUnit) { constructFromExpected(self, mlir::CompilerTarget::create( std::move(name), std::move(sites), - std::move(couplings), - std::move(operations), + std::move(connectivity), + std::move(nativeOperations), std::move(durationUnit))); }, - "name"_a, "sites"_a, nb::kw_only(), "couplings"_a = nb::none(), - "operations"_a = nb::none(), "duration_unit"_a = nb::none()) + "name"_a, "sites"_a, nb::kw_only(), + "connectivity"_a = mlir::CompilerTarget::Connectivity{}, + "native_operations"_a = mlir::CompilerTarget::NativeOperations{}, + "duration_unit"_a = nb::none()) .def_static( "from_device", [](const qdmi::Device& device) { @@ -650,7 +716,7 @@ means every operation is native.)pb"); "The target name, if available.") .def_prop_ro("duration_unit", &mlir::CompilerTarget::durationUnit, "The target timing unit, if available.") - .def_prop_ro("num_qubits", &mlir::CompilerTarget::numQubits, + .def_prop_ro("num_sites", &mlir::CompilerTarget::numSites, "The number of target sites.") .def_prop_ro( "sites", @@ -659,9 +725,8 @@ means every operation is native.)pb"); target.sites().begin(), target.sites().end()); }, "Detailed sites in compiler-vertex order.") - .def_prop_ro("has_explicit_topology", - &mlir::CompilerTarget::hasExplicitTopology, - "Whether the target defines a coupling topology.") + .def_prop_ro("connectivity_kind", &mlir::CompilerTarget::connectivityKind, + "How the target connectivity is known.") .def_prop_ro( "couplings", [](const mlir::CompilerTarget& target) { @@ -669,9 +734,9 @@ means every operation is native.)pb"); target.couplings().begin(), target.couplings().end()); }, "Canonical undirected couplings in target site IDs.") - .def_prop_ro("has_explicit_operations", - &mlir::CompilerTarget::hasExplicitOperations, - "Whether the target defines an operation set.") + .def_prop_ro("native_operations_kind", + &mlir::CompilerTarget::nativeOperationsKind, + "How the target native operations are known.") .def_prop_ro( "operations", [](const mlir::CompilerTarget& target) { @@ -691,12 +756,11 @@ means every operation is native.)pb"); .def( "supports_operation", [](const mlir::CompilerTarget& target, const std::string_view name, - const size_t numQubits, - const std::optional numParameters) { - return target.supportsOperation(name, numQubits, numParameters); + const size_t arity, const std::optional numParameters) { + return target.supportsOperation(name, arity, numParameters); }, - "name"_a, "num_qubits"_a, "num_parameters"_a = nb::none(), - "Whether the target supports an operation capability."); + "name"_a, "arity"_a, "num_parameters"_a = nb::none(), + "Whether the target supports an operation, or None if unknown."); auto program = nb::class_( m, "Program", R"pb(Base class for a typed MLIR compiler program. diff --git a/bindings/patterns.txt b/bindings/patterns.txt index 204d1f094e..1420d34e1e 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -92,20 +92,20 @@ mqt\.core\.mlir\.CompilerTarget\.__init__$: @overload def __init__( self, - num_qubits: int, + num_sites: int, *, - couplings: Sequence[tuple[int, int]] | None = None, - operations: Sequence[CompilerTarget.Operation] | None = None, + connectivity: CompilerTarget.Connectivity = ..., + native_operations: CompilerTarget.NativeOperations = ..., duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload def __init__( self, name: str, - num_qubits: int, + num_sites: int, *, - couplings: Sequence[tuple[int, int]] | None = None, - operations: Sequence[CompilerTarget.Operation] | None = None, + connectivity: CompilerTarget.Connectivity = ..., + native_operations: CompilerTarget.NativeOperations = ..., duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload @@ -113,8 +113,8 @@ mqt\.core\.mlir\.CompilerTarget\.__init__$: self, sites: Sequence[CompilerTarget.Site], *, - couplings: Sequence[tuple[int, int]] | None = None, - operations: Sequence[CompilerTarget.Operation] | None = None, + connectivity: CompilerTarget.Connectivity = ..., + native_operations: CompilerTarget.NativeOperations = ..., duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload @@ -123,8 +123,8 @@ mqt\.core\.mlir\.CompilerTarget\.__init__$: name: str, sites: Sequence[CompilerTarget.Site], *, - couplings: Sequence[tuple[int, int]] | None = None, - operations: Sequence[CompilerTarget.Operation] | None = None, + connectivity: CompilerTarget.Connectivity = ..., + native_operations: CompilerTarget.NativeOperations = ..., duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index f53ed0d6e2..7c187ccc0f 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -27,14 +27,22 @@ compiled = compile_program( Target compilation accepts optimized QCO, QC, or QIR output and uses the canonical QCO pipeline; it cannot be combined with a custom `qco_pipeline`. -The target can also be constructed directly. Omitting `couplings` selects -all-to-all connectivity; omitting `operations` means that every operation is -native: +The target can also be constructed directly. Connectivity and native-operation +metadata are unknown unless the caller states them: ```python -target = CompilerTarget(3, couplings=[(0, 1), (1, 2)]) +target = CompilerTarget( + 3, + connectivity=CompilerTarget.Connectivity([(0, 1), (1, 2)]), + native_operations=CompilerTarget.NativeOperations.unrestricted(), +) ``` +Use `CompilerTarget.Connectivity.all_to_all()` for an all-to-all target. An +empty `CompilerTarget.NativeOperations([])` means that no operation is native. +The default-constructed metadata objects mean that the corresponding support is +unknown; target compilation rejects an unknown property when a pass needs it. + Use {py:meth}`~mqt.core.mlir.QCOProgram.compile_for_target` to apply target compilation to an existing QCO program. Compilation runs in place. If a pass fails, earlier passes may already have changed the program. Copy the program @@ -125,6 +133,6 @@ When exporting a program that has already been mapped to a {py:meth}`~mqt.core.mlir.QCProgram.to_qiskit`. The exporter maps each static target site ID to its index in {py:attr}`~mqt.core.mlir.CompilerTarget.sites` and creates a canonical physical Qiskit circuit. The circuit has one register -named {code}`q` with {py:attr}`~mqt.core.mlir.CompilerTarget.num_qubits` qubits. +named {code}`q` with {py:attr}`~mqt.core.mlir.CompilerTarget.num_sites` qubits. This option does not run target compilation or emit Qiskit layout metadata. Target-aware export requires static qubits whose site IDs belong to that target. diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index 94bcdb7d36..8ff5d3caf8 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -32,9 +32,8 @@ class Operation; * * @details Hardware sites retain their target-defined nonnegative i64 * identifiers. Routing algorithms use dense zero-based vertices in site order. - * An absent topology means all-to-all connectivity. An absent operation set - * means that every operation is native; a present empty set means that no - * hardware operation is native. + * Connectivity and native-operation metadata distinguish unknown, + * unrestricted, and explicitly enumerated support. * * Compiler targets have shared immutable storage, making copies cheap while * preserving validated topology and capability caches. @@ -44,6 +43,36 @@ class CompilerTarget { using SiteId = int64_t; using Coupling = std::pair; + /// Target connectivity knowledge. + class Connectivity { + public: + enum class Kind : uint8_t { Unknown, AllToAll, Explicit }; + + /// Create unknown connectivity. + Connectivity() noexcept; + + /// Create unrestricted all-to-all connectivity. + [[nodiscard]] static Connectivity allToAll(); + + /// Create explicitly enumerated connectivity. + [[nodiscard]] static Connectivity + fromCouplings(std::vector couplings); + + /// Return the connectivity knowledge kind. + [[nodiscard]] Kind kind() const noexcept; + + /// Return explicitly enumerated couplings, if any. + [[nodiscard]] llvm::ArrayRef couplings() const noexcept; + + private: + friend class CompilerTarget; + + Connectivity(Kind kind, std::vector couplings); + + Kind kind_; + std::vector couplings_; + }; + /** * @brief Unit shared by all raw timing metadata on a target. * @@ -150,7 +179,7 @@ class CompilerTarget { * @brief Create a validated operation capability. */ [[nodiscard]] static llvm::Expected - create(std::string name, size_t numQubits, size_t numParameters, + create(std::string name, size_t arity, size_t numParameters, std::vector siteTuples = {}, std::optional duration = std::nullopt, std::optional fidelity = std::nullopt); @@ -162,7 +191,7 @@ class CompilerTarget { [[nodiscard]] llvm::StringRef canonicalName() const noexcept; /// Return the positive fixed operation arity. - [[nodiscard]] size_t numQubits() const noexcept; + [[nodiscard]] size_t arity() const noexcept; /// Return the number of real-valued operation parameters. [[nodiscard]] size_t numParameters() const noexcept; @@ -177,19 +206,49 @@ class CompilerTarget { [[nodiscard]] std::optional fidelity() const noexcept; private: - Operation(std::string name, std::string canonicalName, size_t numQubits, + Operation(std::string name, std::string canonicalName, size_t arity, size_t numParameters, std::vector siteTuples, std::optional duration, std::optional fidelity); std::string name_; std::string canonicalName_; - size_t numQubits_; + size_t arity_; size_t numParameters_; std::vector siteTuples_; std::optional duration_; std::optional fidelity_; }; + /// Native-operation knowledge. + class NativeOperations { + public: + enum class Kind : uint8_t { Unknown, Unrestricted, Explicit }; + + /// Create unknown native-operation support. + NativeOperations() noexcept; + + /// Create unrestricted native-operation support. + [[nodiscard]] static NativeOperations unrestricted(); + + /// Create explicitly enumerated native-operation support. + [[nodiscard]] static NativeOperations + fromOperations(std::vector operations); + + /// Return the native-operation knowledge kind. + [[nodiscard]] Kind kind() const noexcept; + + /// Return explicitly enumerated operations, if any. + [[nodiscard]] llvm::ArrayRef operations() const noexcept; + + private: + friend class CompilerTarget; + + NativeOperations(Kind kind, std::vector operations); + + Kind kind_; + std::vector operations_; + }; + /** * @brief Recognized native gate capability independent of synthesis code. */ @@ -236,30 +295,27 @@ class CompilerTarget { }; /** - * @brief Create an unnamed target with dense site IDs `0..numQubits-1`. + * @brief Create an unnamed target with dense site IDs `0..numSites-1`. */ [[nodiscard]] static llvm::Expected - create(size_t numQubits, - std::optional> couplings = std::nullopt, - std::optional> operations = std::nullopt, + create(size_t numSites, Connectivity connectivity = {}, + NativeOperations nativeOperations = {}, std::optional durationUnit = std::nullopt); /** - * @brief Create a named target with dense site IDs `0..numQubits-1`. + * @brief Create a named target with dense site IDs `0..numSites-1`. */ [[nodiscard]] static llvm::Expected - create(std::string name, size_t numQubits, - std::optional> couplings = std::nullopt, - std::optional> operations = std::nullopt, + create(std::string name, size_t numSites, Connectivity connectivity = {}, + NativeOperations nativeOperations = {}, std::optional durationUnit = std::nullopt); /** * @brief Create an unnamed target from detailed sites. */ [[nodiscard]] static llvm::Expected - create(std::vector sites, - std::optional> couplings = std::nullopt, - std::optional> operations = std::nullopt, + create(std::vector sites, Connectivity connectivity = {}, + NativeOperations nativeOperations = {}, std::optional durationUnit = std::nullopt); /** @@ -267,8 +323,7 @@ class CompilerTarget { */ [[nodiscard]] static llvm::Expected create(std::string name, std::vector sites, - std::optional> couplings = std::nullopt, - std::optional> operations = std::nullopt, + Connectivity connectivity = {}, NativeOperations nativeOperations = {}, std::optional durationUnit = std::nullopt); /// Copying shares immutable storage; rvalues copy and keep the source valid. @@ -284,7 +339,7 @@ class CompilerTarget { durationUnit() const noexcept; /// Return the number of compiler vertices and hardware sites. - [[nodiscard]] size_t numQubits() const noexcept; + [[nodiscard]] size_t numSites() const noexcept; /// Return detailed sites in dense compiler-vertex order. [[nodiscard]] llvm::ArrayRef sites() const noexcept; @@ -298,49 +353,60 @@ class CompilerTarget { /// Return the target site identifier for a valid dense compiler vertex. [[nodiscard]] SiteId siteForVertex(size_t vertex) const; - /// Return whether the target contains an explicit coupling topology. - [[nodiscard]] bool hasExplicitTopology() const noexcept; + /// Return the connectivity knowledge kind. + [[nodiscard]] Connectivity::Kind connectivityKind() const noexcept; /** * @brief Return sorted canonical undirected couplings in target site IDs. */ [[nodiscard]] llvm::ArrayRef couplings() const noexcept; - /// Return whether two valid dense compiler vertices are adjacent. + /** + * @brief Return whether two valid dense compiler vertices are adjacent. + * @pre Connectivity must be known. + */ [[nodiscard]] bool areAdjacent(size_t source, size_t target) const; /** * @brief Return the cached shortest-path distance between valid vertices. + * @pre Connectivity must be known. */ [[nodiscard]] size_t distanceBetween(size_t source, size_t target) const; /** * @brief Invoke @p callback for every neighbour of a valid dense vertex. + * @pre Connectivity must be known. */ void forEachNeighbour(size_t vertex, llvm::function_ref callback) const; - /// Return the maximum degree of the target's routing topology. + /** + * @brief Return the maximum degree of the target's routing topology. + * @pre Connectivity must be known. + */ [[nodiscard]] size_t maxDegree() const noexcept; - /// Return whether the target contains an explicit operation set. - [[nodiscard]] bool hasExplicitOperations() const noexcept; + /// Return the native-operation knowledge kind. + [[nodiscard]] NativeOperations::Kind nativeOperationsKind() const noexcept; /// Return operation capabilities in reported order. [[nodiscard]] llvm::ArrayRef operations() const noexcept; /** - * @brief Return whether an operation capability is supported by the target. + * @brief Return whether an operation capability is supported by the target, + * or `std::nullopt` if native-operation support is unknown. */ - [[nodiscard]] bool - supportsOperation(llvm::StringRef name, size_t numQubits, + [[nodiscard]] std::optional + supportsOperation(llvm::StringRef name, size_t arity, std::optional numParameters = std::nullopt) const; - /// Return whether a QCO operation is supported by the target. - [[nodiscard]] bool supports(::mlir::Operation* operation) const; + /// Return whether a QCO operation is supported, or `std::nullopt` if unknown. + [[nodiscard]] std::optional + supports(::mlir::Operation* operation) const; - /// Return whether a recognized gate is supported by the target. - [[nodiscard]] bool supports(GateKind gate) const; + /// Return whether a recognized gate is supported, or `std::nullopt` if + /// unknown. + [[nodiscard]] std::optional supports(GateKind gate) const; /// Return the recognized gates supported by the target. [[nodiscard]] llvm::ArrayRef supportedGates() const noexcept; @@ -355,8 +421,7 @@ class CompilerTarget { [[nodiscard]] static llvm::Expected createImpl(std::optional name, std::vector sites, - std::optional> couplings, - std::optional> operations, + Connectivity connectivity, NativeOperations nativeOperations, std::optional durationUnit); [[nodiscard]] llvm::ArrayRef explicitNeighbours(size_t vertex) const; diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index f177a782b2..c5354af54c 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -107,19 +107,13 @@ isSwapInvariantOperation(const llvm::StringRef operationName) { [[nodiscard]] static llvm::Error validateHomogeneousSupport( const qdmi::Operation& operation, const size_t arity, - const std::optional>& flattenedSites, + const std::vector& flattenedSites, const std::vector& deviceSites, const std::optional>& couplings, const llvm::StringRef deviceName) { - if (!flattenedSites) { - return requireHomogeneousOperation( - arity != 2 || !couplings, deviceName, operation.getName(), - "the device reports an explicit topology but no ordered two-qubit " - "site support"); - } const auto operationName = operation.getName(); if (auto error = requireHomogeneousOperation( - flattenedSites->size() % arity == 0, deviceName, operationName, + flattenedSites.size() % arity == 0, deviceName, operationName, "the reported site list is not divisible by the fixed arity")) { return error; } @@ -138,8 +132,8 @@ isSwapInvariantOperation(const llvm::StringRef operationName) { if (arity == 1) { llvm::DenseSet supportedSites; - supportedSites.reserve(flattenedSites->size()); - for (const auto& site : *flattenedSites) { + supportedSites.reserve(flattenedSites.size()); + for (const auto& site : flattenedSites) { auto siteId = checkedSiteId(site.getIndex()); if (!siteId) { return siteId.takeError(); @@ -159,14 +153,14 @@ isSwapInvariantOperation(const llvm::StringRef operationName) { llvm::DenseSet reportedTuples; llvm::DenseSet supportedCouplings; - reportedTuples.reserve(flattenedSites->size() / arity); - supportedCouplings.reserve(flattenedSites->size() / arity); - for (size_t offset = 0; offset < flattenedSites->size(); offset += arity) { - auto first = checkedSiteId((*flattenedSites)[offset].getIndex()); + reportedTuples.reserve(flattenedSites.size() / arity); + supportedCouplings.reserve(flattenedSites.size() / arity); + for (size_t offset = 0; offset < flattenedSites.size(); offset += arity) { + auto first = checkedSiteId(flattenedSites[offset].getIndex()); if (!first) { return first.takeError(); } - auto second = checkedSiteId((*flattenedSites)[offset + 1].getIndex()); + auto second = checkedSiteId(flattenedSites[offset + 1].getIndex()); if (!second) { return second.takeError(); } @@ -241,22 +235,18 @@ snapshotDurationUnit(const qdmi::Device& device) { [[nodiscard]] static llvm::Expected> snapshotSiteTuples(const qdmi::Operation& operation, const size_t arity, - const std::optional>& flattenedSites, + const std::vector& flattenedSites, const std::optional defaultDuration, const std::optional defaultFidelity) { - if (!flattenedSites) { - return std::vector{}; - } - std::vector siteTuples; - siteTuples.reserve(flattenedSites->size() / arity); - for (size_t offset = 0; offset < flattenedSites->size(); offset += arity) { + siteTuples.reserve(flattenedSites.size() / arity); + for (size_t offset = 0; offset < flattenedSites.size(); offset += arity) { std::vector sites; std::vector siteIds; sites.reserve(arity); siteIds.reserve(arity); for (size_t index = 0; index < arity; ++index) { - const auto& site = (*flattenedSites)[offset + index]; + const auto& site = flattenedSites[offset + index]; sites.emplace_back(site); auto siteId = checkedSiteId(site.getIndex()); if (!siteId) { @@ -279,7 +269,7 @@ snapshotSiteTuples(const qdmi::Operation& operation, const size_t arity, return siteTuples; } -[[nodiscard]] static llvm::Expected> +[[nodiscard]] static llvm::Expected snapshotOperations( const std::vector& operations, const std::vector& deviceSites, @@ -298,14 +288,17 @@ snapshotOperations( continue; } const auto flattenedSites = operation.getSites(); + if (!flattenedSites) { + return CompilerTarget::NativeOperations{}; + } if (auto error = - validateHomogeneousSupport(operation, *arity, flattenedSites, + validateHomogeneousSupport(operation, *arity, *flattenedSites, deviceSites, couplings, deviceName)) { return error; } const auto duration = operation.getDuration(); const auto fidelity = operation.getFidelity(); - auto siteTuples = snapshotSiteTuples(operation, *arity, flattenedSites, + auto siteTuples = snapshotSiteTuples(operation, *arity, *flattenedSites, duration, fidelity); if (!siteTuples) { return siteTuples.takeError(); @@ -318,7 +311,8 @@ snapshotOperations( } targetOperations.emplace_back(std::move(*targetOperation)); } - return targetOperations; + return CompilerTarget::NativeOperations::fromOperations( + std::move(targetOperations)); } [[nodiscard]] static llvm::Expected @@ -378,8 +372,12 @@ snapshotCompilerTarget(const qdmi::Device& device) { if (!durationUnit) { return durationUnit.takeError(); } + auto connectivity = + couplings + ? CompilerTarget::Connectivity::fromCouplings(std::move(*couplings)) + : CompilerTarget::Connectivity{}; return CompilerTarget::create(std::move(deviceName), std::move(sites), - std::move(couplings), std::move(*operations), + std::move(connectivity), std::move(*operations), std::move(*durationUnit)); } diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 8f0b32a22a..bab2fcf1f0 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -47,53 +48,43 @@ using SiteId = CompilerTarget::SiteId; struct GateSpecification { GateKind kind{}; llvm::StringLiteral name; - size_t numQubits{}; + size_t arity{}; size_t numParameters{}; }; constexpr std::array GATE_SPECIFICATIONS{ GateSpecification{ - .kind = GateKind::U, .name = "u", .numQubits = 1, .numParameters = 3}, + .kind = GateKind::U, .name = "u", .arity = 1, .numParameters = 3}, GateSpecification{ - .kind = GateKind::X, .name = "x", .numQubits = 1, .numParameters = 0}, + .kind = GateKind::X, .name = "x", .arity = 1, .numParameters = 0}, GateSpecification{ - .kind = GateKind::SX, .name = "sx", .numQubits = 1, .numParameters = 0}, + .kind = GateKind::SX, .name = "sx", .arity = 1, .numParameters = 0}, GateSpecification{ - .kind = GateKind::RZ, .name = "rz", .numQubits = 1, .numParameters = 1}, + .kind = GateKind::RZ, .name = "rz", .arity = 1, .numParameters = 1}, GateSpecification{ - .kind = GateKind::RX, .name = "rx", .numQubits = 1, .numParameters = 1}, + .kind = GateKind::RX, .name = "rx", .arity = 1, .numParameters = 1}, GateSpecification{ - .kind = GateKind::RY, .name = "ry", .numQubits = 1, .numParameters = 1}, + .kind = GateKind::RY, .name = "ry", .arity = 1, .numParameters = 1}, GateSpecification{ - .kind = GateKind::R, .name = "r", .numQubits = 1, .numParameters = 2}, - GateSpecification{.kind = GateKind::RXX, - .name = "rxx", - .numQubits = 2, - .numParameters = 1}, - GateSpecification{.kind = GateKind::RYY, - .name = "ryy", - .numQubits = 2, - .numParameters = 1}, - GateSpecification{.kind = GateKind::RZX, - .name = "rzx", - .numQubits = 2, - .numParameters = 1}, - GateSpecification{.kind = GateKind::RZZ, - .name = "rzz", - .numQubits = 2, - .numParameters = 1}, + .kind = GateKind::R, .name = "r", .arity = 1, .numParameters = 2}, + GateSpecification{ + .kind = GateKind::RXX, .name = "rxx", .arity = 2, .numParameters = 1}, + GateSpecification{ + .kind = GateKind::RYY, .name = "ryy", .arity = 2, .numParameters = 1}, + GateSpecification{ + .kind = GateKind::RZX, .name = "rzx", .arity = 2, .numParameters = 1}, + GateSpecification{ + .kind = GateKind::RZZ, .name = "rzz", .arity = 2, .numParameters = 1}, GateSpecification{.kind = GateKind::ISWAP, .name = "iswap", - .numQubits = 2, + .arity = 2, .numParameters = 0}, GateSpecification{ - .kind = GateKind::CZ, .name = "cz", .numQubits = 2, .numParameters = 0}, + .kind = GateKind::CZ, .name = "cz", .arity = 2, .numParameters = 0}, GateSpecification{ - .kind = GateKind::CX, .name = "cx", .numQubits = 2, .numParameters = 0}, - GateSpecification{.kind = GateKind::ECR, - .name = "ecr", - .numQubits = 2, - .numParameters = 0}, + .kind = GateKind::CX, .name = "cx", .arity = 2, .numParameters = 0}, + GateSpecification{ + .kind = GateKind::ECR, .name = "ecr", .arity = 2, .numParameters = 0}, }; } // namespace @@ -134,21 +125,46 @@ validateFidelity(const std::optional fidelity, return llvm::Error::success(); } +CompilerTarget::Connectivity CompilerTarget::Connectivity::allToAll() { + return {Kind::AllToAll, {}}; +} + +CompilerTarget::Connectivity::Connectivity() noexcept : kind_(Kind::Unknown) {} + +CompilerTarget::Connectivity +CompilerTarget::Connectivity::fromCouplings(std::vector couplings) { + return {Kind::Explicit, std::move(couplings)}; +} + +CompilerTarget::Connectivity::Kind +CompilerTarget::Connectivity::kind() const noexcept { + return kind_; +} + +ArrayRef +CompilerTarget::Connectivity::couplings() const noexcept { + return couplings_; +} + +CompilerTarget::Connectivity::Connectivity(const Kind kind, + std::vector couplings) + : kind_(kind), couplings_(std::move(couplings)) {} + [[nodiscard]] static llvm::Expected> -makeDenseSites(const size_t numQubits) { - if (numQubits == 0) { +makeDenseSites(const size_t numSites) { + if (numSites == 0) { return invalidTarget("Compiler target must contain at least one site"); } constexpr auto maxNumSites = static_cast(std::numeric_limits::max()) + 1; - if (static_cast(numQubits) > maxNumSites) { + if (static_cast(numSites) > maxNumSites) { return invalidTarget( - "Compiler target qubit count exceeds the nonnegative i64 site domain"); + "Compiler target site count exceeds the nonnegative i64 site domain"); } std::vector sites; - sites.reserve(numQubits); - for (size_t id = 0; id < numQubits; ++id) { + sites.reserve(numSites); + for (size_t id = 0; id < numSites; ++id) { auto site = CompilerTarget::Site::create(static_cast(id)); if (!site) { return site.takeError(); @@ -265,16 +281,15 @@ std::optional CompilerTarget::SiteTuple::fidelity() const noexcept { } llvm::Expected CompilerTarget::Operation::create( - std::string name, const size_t numQubits, const size_t numParameters, + std::string name, const size_t arity, const size_t numParameters, std::vector siteTuples, const std::optional duration, const std::optional fidelity) { auto canonicalName = canonicalOperationName(name); if (canonicalName.empty()) { return invalidTarget("Compiler target operation name must not be empty"); } - if (numQubits == 0) { - return invalidTarget( - "Compiler target operation qubit count must be positive"); + if (arity == 0) { + return invalidTarget("Compiler target operation arity must be positive"); } if (auto error = validateFidelity(fidelity, "Compiler target operation fidelity")) { @@ -283,7 +298,7 @@ llvm::Expected CompilerTarget::Operation::create( std::set> uniqueSiteCombinations; for (const auto& siteTuple : siteTuples) { - if (siteTuple.sites().size() != numQubits) { + if (siteTuple.sites().size() != arity) { return invalidTarget( "Compiler target operation site tuple does not match its arity"); } @@ -294,19 +309,19 @@ llvm::Expected CompilerTarget::Operation::create( "Compiler target operation contains a duplicate site tuple"); } } - return Operation(std::move(name), std::move(canonicalName), numQubits, + return Operation(std::move(name), std::move(canonicalName), arity, numParameters, std::move(siteTuples), duration, fidelity); } CompilerTarget::Operation::Operation(std::string name, std::string canonicalName, - const size_t numQubits, + const size_t arity, const size_t numParameters, std::vector siteTuples, const std::optional duration, const std::optional fidelity) : name_(std::move(name)), canonicalName_(std::move(canonicalName)), - numQubits_(numQubits), numParameters_(numParameters), + arity_(arity), numParameters_(numParameters), siteTuples_(std::move(siteTuples)), duration_(duration), fidelity_(fidelity) {} @@ -316,9 +331,7 @@ StringRef CompilerTarget::Operation::canonicalName() const noexcept { return canonicalName_; } -size_t CompilerTarget::Operation::numQubits() const noexcept { - return numQubits_; -} +size_t CompilerTarget::Operation::arity() const noexcept { return arity_; } size_t CompilerTarget::Operation::numParameters() const noexcept { return numParameters_; @@ -337,22 +350,54 @@ std::optional CompilerTarget::Operation::fidelity() const noexcept { return fidelity_; } +CompilerTarget::NativeOperations +CompilerTarget::NativeOperations::unrestricted() { + return {Kind::Unrestricted, {}}; +} + +CompilerTarget::NativeOperations::NativeOperations() noexcept + : kind_(Kind::Unknown) {} + +CompilerTarget::NativeOperations +CompilerTarget::NativeOperations::fromOperations( + std::vector operations) { + return {Kind::Explicit, std::move(operations)}; +} + +CompilerTarget::NativeOperations::Kind +CompilerTarget::NativeOperations::kind() const noexcept { + return kind_; +} + +ArrayRef +CompilerTarget::NativeOperations::operations() const noexcept { + return operations_; +} + +CompilerTarget::NativeOperations::NativeOperations( + const Kind kind, std::vector operations) + : kind_(kind), operations_(std::move(operations)) {} + struct CompilerTarget::Storage { Storage(std::optional targetName, std::vector targetSites, - std::optional> targetCouplings, - std::optional> targetOperations, + Connectivity::Kind targetConnectivityKind, + std::vector targetCouplings, + NativeOperations::Kind targetNativeOperationsKind, + std::vector targetOperations, std::optional targetDurationUnit); [[nodiscard]] static llvm::Expected> create(std::optional targetName, std::vector targetSites, - std::optional> targetCouplings, - std::optional> targetOperations, + Connectivity::Kind targetConnectivityKind, + std::vector targetCouplings, + NativeOperations::Kind targetNativeOperationsKind, + std::vector targetOperations, std::optional targetDurationUnit); [[nodiscard]] llvm::Error initialize(); - [[nodiscard]] bool - supportsOperation(StringRef name, size_t numQubits, + [[nodiscard]] std::optional + supportsOperation(StringRef name, size_t arity, std::optional numParameters) const; [[nodiscard]] std::optional resolveSynthesisBasis() const; @@ -361,11 +406,13 @@ struct CompilerTarget::Storage { std::vector sites; SmallVector siteIds; DenseMap siteToVertex; - std::optional> couplings; + Connectivity::Kind connectivityKind; + std::vector couplings; SmallVector> adjacency; SmallVector distances; size_t maximumDegree = 0; - std::optional> operations; + NativeOperations::Kind nativeOperationsKind; + std::vector operations; llvm::StringMap> capabilities; SmallVector supportedGates; std::optional basis; @@ -373,21 +420,28 @@ struct CompilerTarget::Storage { CompilerTarget::Storage::Storage( std::optional targetName, std::vector targetSites, - std::optional> targetCouplings, - std::optional> targetOperations, + const Connectivity::Kind targetConnectivityKind, + std::vector targetCouplings, + const NativeOperations::Kind targetNativeOperationsKind, + std::vector targetOperations, std::optional targetDurationUnit) : name(std::move(targetName)), durationUnit(std::move(targetDurationUnit)), - sites(std::move(targetSites)), couplings(std::move(targetCouplings)), + sites(std::move(targetSites)), connectivityKind(targetConnectivityKind), + couplings(std::move(targetCouplings)), + nativeOperationsKind(targetNativeOperationsKind), operations(std::move(targetOperations)) {} llvm::Expected> CompilerTarget::Storage::create( std::optional targetName, std::vector targetSites, - std::optional> targetCouplings, - std::optional> targetOperations, + const Connectivity::Kind targetConnectivityKind, + std::vector targetCouplings, + const NativeOperations::Kind targetNativeOperationsKind, + std::vector targetOperations, std::optional targetDurationUnit) { auto storage = std::make_shared( - std::move(targetName), std::move(targetSites), std::move(targetCouplings), + std::move(targetName), std::move(targetSites), targetConnectivityKind, + std::move(targetCouplings), targetNativeOperationsKind, std::move(targetOperations), std::move(targetDurationUnit)); if (auto error = storage->initialize()) { return std::move(error); @@ -412,9 +466,9 @@ llvm::Error CompilerTarget::Storage::initialize() { siteIds.emplace_back(site.id()); } - if (couplings) { + if (connectivityKind == Connectivity::Kind::Explicit) { std::set canonicalCouplings; - for (auto [source, target] : *couplings) { + for (auto [source, target] : couplings) { if (!siteToVertex.contains(source) || !siteToVertex.contains(target)) { return invalidTarget( "Compiler target topology references an unknown site"); @@ -428,10 +482,10 @@ llvm::Error CompilerTarget::Storage::initialize() { } canonicalCouplings.emplace(source, target); } - couplings->assign(canonicalCouplings.begin(), canonicalCouplings.end()); + couplings.assign(canonicalCouplings.begin(), canonicalCouplings.end()); adjacency.resize(sites.size()); - for (const auto& [source, target] : *couplings) { + for (const auto& [source, target] : couplings) { const auto sourceVertex = siteToVertex.at(source); const auto targetVertex = siteToVertex.at(target); adjacency[sourceVertex].emplace_back(targetVertex); @@ -469,13 +523,13 @@ llvm::Error CompilerTarget::Storage::initialize() { return invalidTarget("Compiler target topology must be connected"); } } - } else { + } else if (connectivityKind == Connectivity::Kind::AllToAll) { maximumDegree = sites.size() - 1; } - if (operations) { - for (const auto [index, operation] : llvm::enumerate(*operations)) { - if (operation.numQubits() > sites.size()) { + if (nativeOperationsKind == NativeOperations::Kind::Explicit) { + for (const auto [index, operation] : llvm::enumerate(operations)) { + if (operation.arity() > sites.size()) { return invalidTarget( "Compiler target operation arity exceeds its site count"); } @@ -495,7 +549,7 @@ llvm::Error CompilerTarget::Storage::initialize() { return site.t1().has_value() || site.t2().has_value(); }); const auto hasOperationTiming = - operations && llvm::any_of(*operations, [](const auto& operation) { + llvm::any_of(operations, [](const auto& operation) { return operation.duration().has_value() || llvm::any_of(operation.siteTuples(), [](const auto& siteTuple) { return siteTuple.duration().has_value(); @@ -507,8 +561,8 @@ llvm::Error CompilerTarget::Storage::initialize() { } for (const auto& specification : GATE_SPECIFICATIONS) { - if (supportsOperation(specification.name, specification.numQubits, - specification.numParameters)) { + if (supportsOperation(specification.name, specification.arity, + specification.numParameters) == true) { supportedGates.emplace_back(specification.kind); } } @@ -516,14 +570,17 @@ llvm::Error CompilerTarget::Storage::initialize() { return llvm::Error::success(); } -bool CompilerTarget::Storage::supportsOperation( - const StringRef operationName, const size_t numQubits, +std::optional CompilerTarget::Storage::supportsOperation( + const StringRef operationName, const size_t arity, const std::optional numParameters) const { const auto canonical = canonicalOperationName(operationName); - if (canonical.empty() || numQubits == 0 || numQubits > sites.size()) { + if (canonical.empty() || arity == 0 || arity > sites.size()) { return false; } - if (!operations) { + if (nativeOperationsKind == NativeOperations::Kind::Unknown) { + return std::nullopt; + } + if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { return true; } const auto found = capabilities.find(canonical); @@ -531,8 +588,8 @@ bool CompilerTarget::Storage::supportsOperation( return false; } return llvm::any_of(found->second, [&](const auto index) { - const auto& operation = (*operations)[index]; - return operation.numQubits() == numQubits && + const auto& operation = operations[index]; + return operation.arity() == arity && (!numParameters || operation.numParameters() == *numParameters); }); } @@ -574,60 +631,58 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { } llvm::Expected -CompilerTarget::create(const size_t numQubits, - std::optional> couplings, - std::optional> operations, +CompilerTarget::create(const size_t numSites, Connectivity connectivity, + NativeOperations nativeOperations, std::optional durationUnit) { - auto sites = makeDenseSites(numQubits); + auto sites = makeDenseSites(numSites); if (!sites) { return sites.takeError(); } - return createImpl(std::nullopt, std::move(*sites), std::move(couplings), - std::move(operations), std::move(durationUnit)); + return createImpl(std::nullopt, std::move(*sites), std::move(connectivity), + std::move(nativeOperations), std::move(durationUnit)); } llvm::Expected -CompilerTarget::create(std::string name, const size_t numQubits, - std::optional> couplings, - std::optional> operations, +CompilerTarget::create(std::string name, const size_t numSites, + Connectivity connectivity, + NativeOperations nativeOperations, std::optional durationUnit) { - auto sites = makeDenseSites(numQubits); + auto sites = makeDenseSites(numSites); if (!sites) { return sites.takeError(); } return createImpl(std::optional(std::move(name)), - std::move(*sites), std::move(couplings), - std::move(operations), std::move(durationUnit)); + std::move(*sites), std::move(connectivity), + std::move(nativeOperations), std::move(durationUnit)); } llvm::Expected -CompilerTarget::create(std::vector sites, - std::optional> couplings, - std::optional> operations, +CompilerTarget::create(std::vector sites, Connectivity connectivity, + NativeOperations nativeOperations, std::optional durationUnit) { - return createImpl(std::nullopt, std::move(sites), std::move(couplings), - std::move(operations), std::move(durationUnit)); + return createImpl(std::nullopt, std::move(sites), std::move(connectivity), + std::move(nativeOperations), std::move(durationUnit)); } llvm::Expected CompilerTarget::create(std::string name, std::vector sites, - std::optional> couplings, - std::optional> operations, + Connectivity connectivity, + NativeOperations nativeOperations, std::optional durationUnit) { return createImpl(std::optional(std::move(name)), - std::move(sites), std::move(couplings), - std::move(operations), std::move(durationUnit)); + std::move(sites), std::move(connectivity), + std::move(nativeOperations), std::move(durationUnit)); } llvm::Expected CompilerTarget::createImpl(std::optional name, - std::vector sites, - std::optional> couplings, - std::optional> operations, + std::vector sites, Connectivity connectivity, + NativeOperations nativeOperations, std::optional durationUnit) { - auto storage = - Storage::create(std::move(name), std::move(sites), std::move(couplings), - std::move(operations), std::move(durationUnit)); + auto storage = Storage::create( + std::move(name), std::move(sites), connectivity.kind_, + std::move(connectivity.couplings_), nativeOperations.kind_, + std::move(nativeOperations.operations_), std::move(durationUnit)); if (!storage) { return storage.takeError(); } @@ -649,7 +704,7 @@ CompilerTarget::durationUnit() const noexcept { return storage_->durationUnit; } -size_t CompilerTarget::numQubits() const noexcept { +size_t CompilerTarget::numSites() const noexcept { return storage_->sites.size(); } @@ -671,26 +726,27 @@ CompilerTarget::vertexForSite(const SiteId site) const noexcept { } SiteId CompilerTarget::siteForVertex(const size_t vertex) const { - assert(vertex < numQubits() && "Compiler target vertex is out of range"); + assert(vertex < numSites() && "Compiler target vertex is out of range"); return storage_->siteIds[vertex]; } -bool CompilerTarget::hasExplicitTopology() const noexcept { - return storage_->couplings.has_value(); +CompilerTarget::Connectivity::Kind +CompilerTarget::connectivityKind() const noexcept { + return storage_->connectivityKind; } ArrayRef CompilerTarget::couplings() const noexcept { - if (!storage_->couplings) { - return {}; - } - return *storage_->couplings; + return storage_->couplings; } bool CompilerTarget::areAdjacent(const size_t source, const size_t target) const { - assert(source < numQubits() && target < numQubits() && + assert(source < numSites() && target < numSites() && "Compiler target vertex is out of range"); - if (!hasExplicitTopology()) { + if (connectivityKind() == Connectivity::Kind::Unknown) { + llvm::report_fatal_error("Compiler target connectivity is unknown"); + } + if (connectivityKind() == Connectivity::Kind::AllToAll) { return source != target; } return llvm::is_contained(storage_->adjacency[source], target); @@ -699,9 +755,12 @@ bool CompilerTarget::areAdjacent(const size_t source, void CompilerTarget::forEachNeighbour( const size_t vertex, const llvm::function_ref callback) const { - if (!hasExplicitTopology()) { - assert(vertex < numQubits() && "Compiler target vertex is out of range"); - for (size_t neighbour = 0; neighbour < numQubits(); ++neighbour) { + if (connectivityKind() == Connectivity::Kind::Unknown) { + llvm::report_fatal_error("Compiler target connectivity is unknown"); + } + if (connectivityKind() == Connectivity::Kind::AllToAll) { + assert(vertex < numSites() && "Compiler target vertex is out of range"); + for (size_t neighbour = 0; neighbour < numSites(); ++neighbour) { if (neighbour != vertex) { callback(neighbour); } @@ -715,42 +774,47 @@ void CompilerTarget::forEachNeighbour( size_t CompilerTarget::distanceBetween(const size_t source, const size_t target) const { - assert(source < numQubits() && target < numQubits() && + assert(source < numSites() && target < numSites() && "Compiler target vertex is out of range"); - if (!hasExplicitTopology()) { + if (connectivityKind() == Connectivity::Kind::Unknown) { + llvm::report_fatal_error("Compiler target connectivity is unknown"); + } + if (connectivityKind() == Connectivity::Kind::AllToAll) { return source == target ? 0 : 1; } - return storage_->distances[(source * numQubits()) + target]; + return storage_->distances[(source * numSites()) + target]; } ArrayRef CompilerTarget::explicitNeighbours(const size_t vertex) const { - assert(vertex < numQubits() && "Compiler target vertex is out of range"); + assert(vertex < numSites() && "Compiler target vertex is out of range"); return storage_->adjacency[vertex]; } size_t CompilerTarget::maxDegree() const noexcept { + if (connectivityKind() == Connectivity::Kind::Unknown) { + llvm::report_fatal_error("Compiler target connectivity is unknown"); + } return storage_->maximumDegree; } -bool CompilerTarget::hasExplicitOperations() const noexcept { - return storage_->operations.has_value(); +CompilerTarget::NativeOperations::Kind +CompilerTarget::nativeOperationsKind() const noexcept { + return storage_->nativeOperationsKind; } ArrayRef CompilerTarget::operations() const noexcept { - if (!storage_->operations) { - return {}; - } - return *storage_->operations; + return storage_->operations; } -bool CompilerTarget::supportsOperation( - const StringRef operationName, const size_t numQubits, +std::optional CompilerTarget::supportsOperation( + const StringRef operationName, const size_t arity, const std::optional numParameters) const { - return storage_->supportsOperation(operationName, numQubits, numParameters); + return storage_->supportsOperation(operationName, arity, numParameters); } -bool CompilerTarget::supports(::mlir::Operation* operation) const { +std::optional +CompilerTarget::supports(::mlir::Operation* operation) const { if (operation == nullptr) { return false; } @@ -784,7 +848,10 @@ bool CompilerTarget::supports(::mlir::Operation* operation) const { return false; } -bool CompilerTarget::supports(const GateKind gate) const { +std::optional CompilerTarget::supports(const GateKind gate) const { + if (nativeOperationsKind() == NativeOperations::Kind::Unknown) { + return std::nullopt; + } return llvm::is_contained(storage_->supportedGates, gate); } diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index ba2493d715..c3a697a982 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -236,12 +236,12 @@ static FailureOr discoverComputation(func::FuncOp func) { static LogicalResult checkCapacity(func::FuncOp func, const CompilerTarget& target, const Computation& computation) { - if (computation.wires.size() <= target.numQubits()) { + if (computation.wires.size() <= target.numSites()) { return success(); } return func.emitError() << "requires " << computation.wires.size() << " qubits, but the target supports " - << target.numQubits(); + << target.numSites(); } /// Replace dynamic qubit roots with the target sites selected by `layout`. @@ -484,14 +484,14 @@ struct MappingPass : impl::MappingPassBase { /// Describes the graph F of arXiv:1602.05150v3. struct FGraph { explicit FGraph(const CompilerTarget& target) - : f_(llvm::to_vector(llvm::seq(target.numQubits()))), + : f_(llvm::to_vector(llvm::seq(target.numSites()))), target_(&target) {}; /// Build F-graph: Add edges to F for each edge in the coupling graph. /// Note that this assumes that the coupling graph is directed, but /// symmetric (essentially: undirected). void construct(const Layout& from, const Layout& to) { - for (size_t u = 0; u < target_->numQubits(); ++u) { + for (size_t u = 0; u < target_->numSites(); ++u) { target_->forEachNeighbour(u, [&](const auto v) { if (shouldAddEdge(u, v, from, to)) { f_.addEdge(u, v); @@ -577,7 +577,8 @@ struct MappingPass : impl::MappingPassBase { } auto moduleOp = getOperation(); - if (!target->hasExplicitTopology()) { + if (target->connectivityKind() != + CompilerTarget::Connectivity::Kind::Explicit) { moduleOp.emitError() << "place-and-route requires an explicit target topology"; signalPassFailure(); @@ -828,8 +829,8 @@ struct MappingPass : impl::MappingPassBase { trials.emplace_back( RoutingBundle{.wires = wires, .infos = infos, - .layout = Layout::random(target->numQubits(), - target->numQubits(), rng())}); + .layout = Layout::random(target->numSites(), + target->numSites(), rng())}); } parallelForEach(&getContext(), trials, [&, this](Trial& t) { @@ -892,7 +893,7 @@ struct MappingPass : impl::MappingPassBase { const Layout& layout) const { constexpr size_t cap = 25'000'000UL; - const size_t b = target->maxDegree() * ((target->numQubits() + 1) / 2); + const size_t b = target->maxDegree() * ((target->numSites() + 1) / 2); const size_t budget = std::min(b * b * b, cap); const Parameters params{.alpha = alpha, .lambda = lambda}; @@ -1289,7 +1290,7 @@ struct MappingPass : impl::MappingPassBase { included.insert(index); } - const auto allIndices = to_vector(llvm::seq(target->numQubits())); + const auto allIndices = to_vector(llvm::seq(target->numSites())); const SmallVector excluded(llvm::make_filter_range( allIndices, [&](const size_t i) { return !included.contains(i); })); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 0503a95462..7c01d76894 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -296,7 +296,7 @@ static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, static bool requiresTargetSynthesis(Operation* operation, const CompilerTarget& target) { - return !target.supports(operation); + return target.supports(operation) != true; } namespace { @@ -451,7 +451,8 @@ struct TargetNativeSynthesisPass final protected: void runOnOperation() override { - if (!target.hasExplicitOperations()) { + if (target.nativeOperationsKind() == + CompilerTarget::NativeOperations::Kind::Unrestricted) { return; } ModuleOp moduleOp = getOperation(); @@ -459,6 +460,13 @@ struct TargetNativeSynthesisPass final if (plan.firstNeed == nullptr) { return; } + if (target.nativeOperationsKind() == + CompilerTarget::NativeOperations::Kind::Unknown) { + plan.firstNeed->emitError() + << "target-native synthesis requires known native operations"; + signalPassFailure(); + return; + } const auto targetBasis = target.synthesisBasis(); if (!targetBasis) { @@ -543,7 +551,13 @@ struct VerifyTargetConformancePass final return WalkResult::advance(); } - if (target.supports(operation)) { + const auto support = target.supports(operation); + if (!support) { + operation->emitError() + << "target conformance requires known native operations"; + return WalkResult::interrupt(); + } + if (*support) { return WalkResult::advance(); } diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 1f8325b39d..41da23c550 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -1588,7 +1588,9 @@ c = measure q; std::vector sites{llvm::cantFail(CompilerTarget::Site::create(2472)), llvm::cantFail(CompilerTarget::Site::create(18449)), llvm::cantFail(CompilerTarget::Site::create(65535))}; - const auto target = llvm::cantFail(CompilerTarget::create(std::move(sites))); + const auto target = llvm::cantFail(CompilerTarget::create( + std::move(sites), CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::unrestricted())); ASSERT_TRUE(qco->compileForTarget(target)); auto compiled = parseRecordedModule(qco->str()); diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 16cebd2727..8108ea191e 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -20,6 +20,7 @@ #include #include +#include #include using mlir::CompilerTarget; @@ -41,8 +42,9 @@ TEST(CompilerQDMIAdapterTest, SnapshotsIQMCalibrationAndLifetime) { ASSERT_TRUE(target.name()); EXPECT_EQ(*target.name(), "IQM Garnet"); - EXPECT_EQ(target.numQubits(), 20); - EXPECT_TRUE(target.hasExplicitTopology()); + EXPECT_EQ(target.numSites(), 20); + EXPECT_EQ(target.connectivityKind(), + CompilerTarget::Connectivity::Kind::Explicit); EXPECT_EQ(target.couplings().size(), 30); ASSERT_TRUE(target.durationUnit()); @@ -70,26 +72,28 @@ TEST(CompilerQDMIAdapterTest, SnapshotsIQMCalibrationAndLifetime) { } } - EXPECT_TRUE(target.supportsOperation("r", 1, 2)); - EXPECT_TRUE(target.supportsOperation("cz", 2, 0)); - EXPECT_TRUE(target.supportsOperation("measure", 1, 0)); - EXPECT_FALSE(target.supportsOperation("rx", 1, 1)); + EXPECT_EQ(target.supportsOperation("r", 1, 2), true); + EXPECT_EQ(target.supportsOperation("cz", 2, 0), true); + EXPECT_EQ(target.supportsOperation("measure", 1, 0), true); + EXPECT_EQ(target.supportsOperation("rx", 1, 1), false); ASSERT_TRUE(target.synthesisBasis()); EXPECT_EQ(target.synthesisBasis()->singleQubit, CompilerTarget::SingleQubitBasis::R); EXPECT_EQ(target.synthesisBasis()->entangler, CompilerTarget::GateKind::CZ); } -TEST(CompilerQDMIAdapterTest, PreservesMissingTopologyAsAllToAll) { +TEST(CompilerQDMIAdapterTest, PreservesMissingTargetFactsAsUnknown) { const auto device = qdmi::Session::openDevice("mqt.ddsim.default"); const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); - EXPECT_EQ(target.numQubits(), 65535); - EXPECT_FALSE(target.hasExplicitTopology()); - EXPECT_TRUE(target.areAdjacent(0, target.numQubits() - 1)); - EXPECT_TRUE(target.supportsOperation("h", 1, 0)); - EXPECT_TRUE(target.supportsOperation("cx", 2, 0)); - EXPECT_TRUE(target.supportsOperation("measure", 1, 0)); + EXPECT_EQ(target.numSites(), 65535); + EXPECT_EQ(target.connectivityKind(), + CompilerTarget::Connectivity::Kind::Unknown); + EXPECT_EQ(target.nativeOperationsKind(), + CompilerTarget::NativeOperations::Kind::Unknown); + EXPECT_EQ(target.supportsOperation("h", 1, 0), std::nullopt); + EXPECT_EQ(target.supportsOperation("cx", 2, 0), std::nullopt); + EXPECT_EQ(target.supportsOperation("measure", 1, 0), std::nullopt); } TEST(CompilerQDMIAdapterTest, ListsRegisteredDeviceIds) { diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 6a32cda8c1..e3e1f3f8c5 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -50,10 +50,12 @@ static void expectInvalid(llvm::Expected value, namespace { using Target = mlir::CompilerTarget; +using Connectivity = Target::Connectivity; using Coupling = Target::Coupling; using DurationUnit = Target::DurationUnit; using GateKind = Target::GateKind; using Operation = Target::Operation; +using NativeOperations = Target::NativeOperations; using Site = Target::Site; using SiteId = Target::SiteId; using SiteTuple = Target::SiteTuple; @@ -70,10 +72,11 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { operations.emplace_back( valid(Operation::create(" PRX ", 1, 2, std::move(siteTuples), 0, 0.97))); - const auto target = valid(Target::create( - "device", std::move(sites), - std::vector{{11, 2}, {2, 11}, {7, 2}}, std::move(operations), - valid(DurationUnit::create("ns", 0.5)))); + const auto target = valid( + Target::create("device", std::move(sites), + Connectivity::fromCouplings({{11, 2}, {2, 11}, {7, 2}}), + NativeOperations::fromOperations(std::move(operations)), + valid(DurationUnit::create("ns", 0.5)))); // The copy itself is the behavior under test: both objects must share the // immutable backing storage. // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) @@ -92,7 +95,7 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { EXPECT_EQ(target.sites()[0].t2(), 80); EXPECT_EQ(target.operations()[0].name(), " PRX "); EXPECT_EQ(target.operations()[0].canonicalName(), "r"); - EXPECT_EQ(target.operations()[0].numQubits(), 1); + EXPECT_EQ(target.operations()[0].arity(), 1); EXPECT_EQ(target.operations()[0].numParameters(), 2); EXPECT_EQ(target.operations()[0].duration(), 0); EXPECT_EQ(target.operations()[0].fidelity(), 0.97); @@ -106,8 +109,9 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { } TEST(CompilerTargetTest, ConstructsDenseUnnamedAllToAllTarget) { - const auto target = valid(Target::create(3)); - const auto named = valid(Target::create("simulator", 2)); + const auto target = valid(Target::create(3, Connectivity::allToAll())); + const auto named = + valid(Target::create("simulator", 2, Connectivity::allToAll())); EXPECT_FALSE(target.name()); ASSERT_TRUE(named.name()); @@ -119,7 +123,7 @@ TEST(CompilerTargetTest, ConstructsDenseUnnamedAllToAllTarget) { EXPECT_EQ(target.vertexForSite(2), 2); EXPECT_FALSE(target.vertexForSite(3)); EXPECT_EQ(target.siteForVertex(1), 1); - EXPECT_FALSE(target.hasExplicitTopology()); + EXPECT_EQ(target.connectivityKind(), Connectivity::Kind::AllToAll); EXPECT_TRUE(target.couplings().empty()); EXPECT_TRUE(target.areAdjacent(0, 2)); EXPECT_FALSE(target.areAdjacent(1, 1)); @@ -136,11 +140,11 @@ TEST(CompilerTargetTest, ConstructsDenseUnnamedAllToAllTarget) { TEST(CompilerTargetTest, CanonicalizesConnectedTopologyAndCachesDistances) { std::vector sites{valid(Site::create(7)), valid(Site::create(2)), valid(Site::create(11))}; - const auto target = valid( - Target::create(std::move(sites), - std::vector{{11, 2}, {2, 11}, {7, 2}, {2, 7}})); + const auto target = valid(Target::create( + std::move(sites), + Connectivity::fromCouplings({{11, 2}, {2, 11}, {7, 2}, {2, 7}}))); - EXPECT_TRUE(target.hasExplicitTopology()); + EXPECT_EQ(target.connectivityKind(), Connectivity::Kind::Explicit); EXPECT_EQ(target.couplings(), (llvm::ArrayRef{{2, 7}, {2, 11}})); EXPECT_EQ(target.vertexForSite(7), 0); EXPECT_EQ(target.vertexForSite(2), 1); @@ -164,7 +168,7 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { if constexpr (sizeof(size_t) >= sizeof(uint64_t)) { expectInvalid( Target::create(std::numeric_limits::max()), - "Compiler target qubit count exceeds the nonnegative i64 site domain"); + "Compiler target site count exceeds the nonnegative i64 site domain"); } expectInvalid(Site::create(-1), "Compiler target site ID must be nonnegative"); @@ -192,7 +196,7 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { expectInvalid(Operation::create("", 1, 0), "Compiler target operation name must not be empty"); expectInvalid(Operation::create("x", 0, 0), - "Compiler target operation qubit count must be positive"); + "Compiler target operation arity must be positive"); expectInvalid( Operation::create("x", 1, 0, std::vector{valid(SiteTuple::create({0, 1}))}), @@ -218,52 +222,58 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { expectInvalid( Target::create(std::vector{valid(Site::create(0, std::nullopt, 1))}), "Compiler target timing metadata requires a duration unit"); - expectInvalid( - Target::create(1, std::nullopt, - std::vector{valid(Operation::create("x", 1, 0, {}, 1))}), - "Compiler target timing metadata requires a duration unit"); + expectInvalid(Target::create(1, {}, + NativeOperations::fromOperations({valid( + Operation::create("x", 1, 0, {}, 1))})), + "Compiler target timing metadata requires a duration unit"); expectInvalid( Target::create( - 1, std::nullopt, - std::vector{valid(Operation::create( - "x", 1, 0, std::vector{valid(SiteTuple::create({0}, 1))}))}), + 1, {}, + NativeOperations::fromOperations({valid(Operation::create( + "x", 1, 0, std::vector{valid(SiteTuple::create({0}, 1))}))})), "Compiler target timing metadata requires a duration unit"); - expectInvalid(Target::create(2, std::vector{{0, 0}}), + expectInvalid(Target::create(2, Connectivity::fromCouplings({{0, 0}})), "Compiler target topology contains a self-coupling"); - expectInvalid(Target::create(2, std::vector{{0, 2}}), + expectInvalid(Target::create(2, Connectivity::fromCouplings({{0, 2}})), "Compiler target topology references an unknown site"); - expectInvalid(Target::create(3, std::vector{{0, 1}}), + expectInvalid(Target::create(3, Connectivity::fromCouplings({{0, 1}})), "Compiler target topology must be connected"); expectInvalid( Target::create( - 2, std::nullopt, - std::vector{valid(Operation::create( - "x", 1, 0, std::vector{valid(SiteTuple::create({2}))}))}), + 2, {}, + NativeOperations::fromOperations({valid(Operation::create( + "x", 1, 0, std::vector{valid(SiteTuple::create({2}))}))})), "Compiler target operation site tuple references an unknown site"); - expectInvalid( - Target::create(1, std::nullopt, - std::vector{valid(Operation::create("cx", 2, 0))}), - "Compiler target operation arity exceeds its site count"); + expectInvalid(Target::create(1, {}, + NativeOperations::fromOperations( + {valid(Operation::create("cx", 2, 0))})), + "Compiler target operation arity exceeds its site count"); } -TEST(CompilerTargetTest, DistinguishesAbsentAndEmptyOperationSets) { - const auto permissive = valid(Target::create(2)); +TEST(CompilerTargetTest, DistinguishesOperationKnowledge) { + const auto unknown = valid(Target::create(2)); + const auto unrestricted = + valid(Target::create(2, {}, NativeOperations::unrestricted())); const auto closed = - valid(Target::create(2, std::nullopt, std::vector{})); - - EXPECT_FALSE(permissive.hasExplicitOperations()); - EXPECT_TRUE(permissive.operations().empty()); - EXPECT_TRUE(permissive.supportsOperation("device.operation", 1)); - EXPECT_TRUE(permissive.supports(GateKind::CX)); - EXPECT_FALSE(permissive.supportsOperation("", 1)); - EXPECT_FALSE(permissive.supportsOperation(" ", 1)); - EXPECT_FALSE(permissive.supportsOperation("x", 0)); - EXPECT_FALSE(permissive.supportsOperation("x", 3)); - - EXPECT_TRUE(closed.hasExplicitOperations()); + valid(Target::create(2, {}, NativeOperations::fromOperations({}))); + + EXPECT_EQ(unknown.nativeOperationsKind(), NativeOperations::Kind::Unknown); + EXPECT_EQ(unknown.supportsOperation("x", 1), std::nullopt); + EXPECT_EQ(unknown.supports(GateKind::CX), std::nullopt); + + EXPECT_EQ(unrestricted.nativeOperationsKind(), + NativeOperations::Kind::Unrestricted); + EXPECT_EQ(unrestricted.supportsOperation("device.operation", 1), true); + EXPECT_EQ(unrestricted.supports(GateKind::CX), true); + EXPECT_EQ(unrestricted.supportsOperation("", 1), false); + EXPECT_EQ(unrestricted.supportsOperation(" ", 1), false); + EXPECT_EQ(unrestricted.supportsOperation("x", 0), false); + EXPECT_EQ(unrestricted.supportsOperation("x", 3), false); + + EXPECT_EQ(closed.nativeOperationsKind(), NativeOperations::Kind::Explicit); EXPECT_TRUE(closed.operations().empty()); - EXPECT_FALSE(closed.supportsOperation("x", 1)); - EXPECT_FALSE(closed.supports(GateKind::CX)); + EXPECT_EQ(closed.supportsOperation("x", 1), false); + EXPECT_EQ(closed.supports(GateKind::CX), false); EXPECT_TRUE(closed.supportedGates().empty()); EXPECT_FALSE(closed.synthesisBasis()); } @@ -274,12 +284,13 @@ TEST(CompilerTargetTest, PreservesCalibrationAndResolvesHomogeneousBasis) { const auto cz = valid(Operation::create( "cz", 2, 0, std::vector{valid(SiteTuple::create({1, 0}, 5, 0.99))})); const auto target = - valid(Target::create(3, chain, std::vector{globalU, cz}, + valid(Target::create(3, Connectivity::fromCouplings(chain), + NativeOperations::fromOperations({globalU, cz}), valid(DurationUnit::create("ns", 1.)))); - EXPECT_TRUE(target.supportsOperation("u", 1, 3)); - EXPECT_TRUE(target.supportsOperation(" U3 ", 1, 3)); - EXPECT_TRUE(target.supports(GateKind::CZ)); + EXPECT_EQ(target.supportsOperation("u", 1, 3), true); + EXPECT_EQ(target.supportsOperation(" U3 ", 1, 3), true); + EXPECT_EQ(target.supports(GateKind::CZ), true); EXPECT_TRUE(llvm::is_contained(target.supportedGates(), GateKind::CZ)); ASSERT_EQ(target.operations().size(), 2U); ASSERT_EQ(target.operations()[1].siteTuples().size(), 1U); @@ -309,10 +320,11 @@ TEST(CompilerTargetTest, ClassifiesEveryEntangler) { SCOPED_TRACE(name); const auto operation = valid(Operation::create(std::string{name}, 2, numParameters)); - const auto target = - valid(Target::create(3, chain, std::vector{globalU, operation})); + const auto target = valid( + Target::create(3, Connectivity::fromCouplings(chain), + NativeOperations::fromOperations({globalU, operation}))); EXPECT_TRUE(llvm::is_contained(target.supportedGates(), gate)); - EXPECT_TRUE(target.supports(gate)); + EXPECT_EQ(target.supports(gate), true); ASSERT_TRUE(target.synthesisBasis()); EXPECT_EQ(target.synthesisBasis()->entangler, gate); } @@ -382,21 +394,22 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { valid(Operation::create("reset", 1, 0)), valid(Operation::create("cnot", 2, 0, std::move(directionalTuples)))}; const auto target = valid( - Target::create(std::move(sites), std::nullopt, std::move(operations))); - EXPECT_TRUE(target.supports(x)); - EXPECT_TRUE(target.supports(cx)); - EXPECT_TRUE(target.supports(measure)); - EXPECT_TRUE(target.supports(reset)); - EXPECT_TRUE(target.supports(barrier)); - EXPECT_TRUE(target.supports(gphase)); - EXPECT_FALSE(target.supports(nullptr)); + Target::create(std::move(sites), {}, + NativeOperations::fromOperations(std::move(operations)))); + EXPECT_EQ(target.supports(x), true); + EXPECT_EQ(target.supports(cx), true); + EXPECT_EQ(target.supports(measure), true); + EXPECT_EQ(target.supports(reset), true); + EXPECT_EQ(target.supports(barrier), true); + EXPECT_EQ(target.supports(gphase), true); + EXPECT_EQ(target.supports(nullptr), false); const auto closed = - valid(Target::create(2, std::nullopt, std::vector{})); - EXPECT_TRUE(closed.supports(barrier)); - EXPECT_TRUE(closed.supports(gphase)); - EXPECT_FALSE(closed.supports(x)); - EXPECT_FALSE(closed.supports(measure)); + valid(Target::create(2, {}, NativeOperations::fromOperations({}))); + EXPECT_EQ(closed.supports(barrier), true); + EXPECT_EQ(closed.supports(gphase), true); + EXPECT_EQ(closed.supports(x), false); + EXPECT_EQ(closed.supports(measure), false); } } // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index c80a397e5f..98e26283f0 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -62,6 +62,8 @@ using namespace mlir; using namespace mlir::qco; using mlir::mqt::getEntryPoint; +using Connectivity = CompilerTarget::Connectivity; +using NativeOperations = CompilerTarget::NativeOperations; static std::string printModule(ModuleOp moduleOp) { std::string result; @@ -243,8 +245,8 @@ static CompilerTarget getSquareGridTarget(const size_t n) { } } - return llvm::cantFail( - CompilerTarget::create(numTarget, std::move(couplings))); + return llvm::cantFail(CompilerTarget::create( + numTarget, Connectivity::fromCouplings(std::move(couplings)))); } /// Creates an N-qubit GHZ state, where N = `qubits.size()` using @@ -342,9 +344,9 @@ class MappingPassTest : public MappingPassFixture, TEST_F(MappingPassFixture, MapTopologyOnlyWithEmptyOperationSet) { constexpr int64_t size = 3; - const auto target = llvm::cantFail(CompilerTarget::create( - 3, std::vector{{0, 1}, {1, 2}}, - std::vector{})); + const auto target = llvm::cantFail( + CompilerTarget::create(3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), + NativeOperations::fromOperations({}))); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(size, builder.getI1Type())); @@ -439,9 +441,8 @@ TEST_F(MappingPassFixture, PreserveNoncontiguousTargetSiteIds) { sites.emplace_back(llvm::cantFail(CompilerTarget::Site::create(42))); const auto target = llvm::cantFail(CompilerTarget::create( - std::move(sites), - std::vector{{7, 19}, {19, 42}}, - std::vector{})); + std::move(sites), Connectivity::fromCouplings({{7, 19}, {19, 42}}), + NativeOperations::fromOperations({}))); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(size, builder.getI1Type())); @@ -481,7 +482,8 @@ TEST_F(MappingPassFixture, PlaceNoncontiguousTargetCompactly) { sites.emplace_back(llvm::cantFail(CompilerTarget::Site::create(7))); sites.emplace_back(llvm::cantFail(CompilerTarget::Site::create(19))); sites.emplace_back(llvm::cantFail(CompilerTarget::Site::create(42))); - const auto target = llvm::cantFail(CompilerTarget::create(std::move(sites))); + const auto target = llvm::cantFail( + CompilerTarget::create(std::move(sites), Connectivity::allToAll())); QCOProgramBuilder builder(context.get()); builder.initialize({builder.getI1Type()}); @@ -601,8 +603,8 @@ TEST_F(MappingPassFixture, KeepWorkspaceSparseOnLargeTarget) { couplings.emplace_back(0, static_cast(site)); } - const auto target = llvm::cantFail( - CompilerTarget::create(numTargetQubits, std::move(couplings))); + const auto target = llvm::cantFail(CompilerTarget::create( + numTargetQubits, Connectivity::fromCouplings(std::move(couplings)))); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(2, builder.getI1Type())); @@ -633,6 +635,36 @@ TEST_F(MappingPassFixture, KeepWorkspaceSparseOnLargeTarget) { EXPECT_EQ(numSinks, numStatics); } +TEST_F(MappingPassFixture, UnknownConnectivityIsNeededOnlyForTwoQubitOps) { + QCOProgramBuilder builder(context.get()); + builder.initialize(); + builder.sink(builder.allocQubit()); + auto moduleOp = builder.finalize(); + const auto target = llvm::cantFail(CompilerTarget::create(2)); + + EXPECT_TRUE(succeeded(runPass(moduleOp.get(), target, MappingPassOptions{}))); + + QCOProgramBuilder twoQubitBuilder(context.get()); + twoQubitBuilder.initialize(); + auto first = twoQubitBuilder.allocQubit(); + auto second = twoQubitBuilder.allocQubit(); + std::tie(first, second) = twoQubitBuilder.cx(first, second); + twoQubitBuilder.sink(first); + twoQubitBuilder.sink(second); + auto twoQubitModule = twoQubitBuilder.finalize(); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE( + failed(runPass(twoQubitModule.get(), target, MappingPassOptions{}))); + EXPECT_TRUE( + StringRef(diagnostics) + .contains("place-and-route requires known target connectivity")); +} + TEST_P(MappingPassTest, FailNoEntryPoint) { const auto& target = GetParam(); @@ -918,7 +950,7 @@ TEST_P(MappingPassTest, FailNoExtractAfterInsert) { TEST_P(MappingPassTest, FailTooManyQubitsForArch) { const auto& target = GetParam(); - const auto size = static_cast(target.numQubits()) + 1; + const auto size = static_cast(target.numSites()) + 1; SmallVector bits(size); SmallVector qubits(size); @@ -984,7 +1016,7 @@ TEST_P(MappingPassTest, MapFlatGHZ) { TEST_P(MappingPassTest, MapLoopBasedGHZByUnrolling) { const auto& target = GetParam(); - const auto size = static_cast(target.numQubits()); + const auto size = static_cast(target.numSites()); SmallVector qubits(size); SmallVector bits(size); @@ -1846,7 +1878,7 @@ TEST_P(MappingPassTest, MapNestedForSwitch) { TEST_P(MappingPassTest, MapPaddedCXCZGrid) { const auto& target = GetParam(); - const auto size = (target.numQubits() + 1) / 2; + const auto size = (target.numSites() + 1) / 2; SmallVector qubits(size); SmallVector bits(size); 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 0ec0326559..8ac7aa55f6 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -56,6 +56,7 @@ namespace mqt::test::qco { using Target = mlir::CompilerTarget; +using NativeOperations = Target::NativeOperations; using Operation = Target::Operation; using Site = Target::Site; using mlir::ModuleOp; @@ -156,7 +157,8 @@ makeUCxTarget(std::optional> sites = std::nullopt) { std::vector operations{valid(Operation::create("u", 1, 3)), valid(Operation::create("cx", 2, 0))}; return valid( - Target::create(std::move(*sites), std::nullopt, std::move(operations))); + Target::create(std::move(*sites), {}, + NativeOperations::fromOperations(std::move(operations)))); } [[nodiscard]] static mlir::DenseElementsAttr @@ -231,7 +233,8 @@ class TargetSynthesisTest : public testing::Test { } // namespace TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { - const auto target = valid(Target::create(2)); + const auto target = + valid(Target::create(2, {}, NativeOperations::unrestricted())); auto fusion = mlir::qco::createFuseTwoQubitGates(); auto synthesis = mlir::qco::createTargetNativeSynthesis(target); auto conformance = mlir::qco::createVerifyTargetConformance(target); @@ -500,8 +503,10 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisPreservesNativeSwap) { std::tie(q0, q1) = builder.swap(q0, q1); return builder.intConstant(0); }); - const auto swapTarget = valid(Target::create( - 2, std::nullopt, std::vector{valid(Operation::create("swap", 2, 0))})); + const auto swapTarget = + valid(Target::create(2, {}, + NativeOperations::fromOperations( + {valid(Operation::create("swap", 2, 0))}))); ASSERT_FALSE(swapTarget.synthesisBasis()); const auto before = printModule(*module); @@ -523,9 +528,10 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisUsesHomogeneousCapability) { auto expected = build(swap); auto synthesized = build(swap); const auto target = - valid(Target::create(2, std::nullopt, - std::vector{valid(Operation::create("u", 1, 3)), - valid(Operation::create("cz", 2, 0))})); + valid(Target::create(2, {}, + NativeOperations::fromOperations( + {valid(Operation::create("u", 1, 3)), + valid(Operation::create("cz", 2, 0))}))); ASSERT_TRUE(target.synthesisBasis()); ASSERT_EQ(target.synthesisBasis()->entangler, Target::GateKind::CZ); @@ -539,13 +545,15 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisUsesHomogeneousCapability) { expectEquivalent(expected, synthesized); } -TEST_F(TargetSynthesisTest, AbsentOperationSetTreatsEveryOperationAsNative) { +TEST_F(TargetSynthesisTest, + UnrestrictedOperationSetTreatsEveryOperationAsNative) { auto module = build([](QCOProgramBuilder& builder) { auto qubit = builder.staticQubit(0); qubit = builder.h(qubit); return builder.intConstant(0); }); - const auto permissive = valid(Target::create(1)); + const auto permissive = + valid(Target::create(1, {}, NativeOperations::unrestricted())); const auto before = printModule(*module); ASSERT_TRUE(mlir::succeeded( @@ -555,6 +563,38 @@ TEST_F(TargetSynthesisTest, AbsentOperationSetTreatsEveryOperationAsNative) { EXPECT_EQ(printModule(*module), before); } +TEST_F(TargetSynthesisTest, UnknownOperationSetIsNeededOnlyForQuantumOps) { + const auto target = valid(Target::create(1)); + auto classical = + build([](QCOProgramBuilder& builder) { return builder.intConstant(0); }); + ASSERT_TRUE(mlir::succeeded( + runPass(*classical, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*classical, mlir::qco::createVerifyTargetConformance(target)))); + + const auto buildQuantum = [&] { + return build([](QCOProgramBuilder& builder) { + builder.sink(builder.h(builder.staticQubit(0))); + return builder.intConstant(0); + }); + }; + auto synthesisModule = buildQuantum(); + auto diagnostics = expectFailure( + *synthesisModule, mlir::qco::createTargetNativeSynthesis(target)); + EXPECT_NE(diagnostics.find( + "target-native synthesis requires known native operations"), + std::string::npos) + << diagnostics; + + auto conformanceModule = buildQuantum(); + diagnostics = expectFailure(*conformanceModule, + mlir::qco::createVerifyTargetConformance(target)); + EXPECT_NE( + diagnostics.find("target conformance requires known native operations"), + std::string::npos) + << diagnostics; +} + TEST_F(TargetSynthesisTest, NativePowShellHidesItsImplementationBody) { auto module = build([](QCOProgramBuilder& builder) { auto qubit = builder.staticQubit(0); @@ -562,8 +602,10 @@ TEST_F(TargetSynthesisTest, NativePowShellHidesItsImplementationBody) { [&](Value argument) { return builder.h(argument); }); return builder.intConstant(0); }); - const auto powOnly = valid(Target::create( - 1, std::nullopt, std::vector{valid(Operation::create("pow", 1, 1))})); + const auto powOnly = + valid(Target::create(1, {}, + NativeOperations::fromOperations( + {valid(Operation::create("pow", 1, 1))}))); ASSERT_FALSE(powOnly.synthesisBasis()); const auto before = printModule(*module); @@ -576,7 +618,8 @@ TEST_F(TargetSynthesisTest, NativePowShellHidesItsImplementationBody) { TEST_F(TargetSynthesisTest, MissingBasisIsDiagnosedOnlyWhenLoweringIsNeeded) { const auto hOnly = valid(Target::create( - 1, std::nullopt, std::vector{valid(Operation::create("h", 1, 0))})); + 1, {}, + NativeOperations::fromOperations({valid(Operation::create("h", 1, 0))}))); ASSERT_FALSE(hOnly.synthesisBasis()); auto supported = build([](QCOProgramBuilder& builder) { @@ -620,9 +663,10 @@ TEST_F(TargetSynthesisTest, SupportedRuntimeParameterizedGateStaysUntouched) { context.get()); ASSERT_TRUE(module); const auto target = - valid(Target::create(2, std::nullopt, - std::vector{valid(Operation::create("u", 1, 3)), - valid(Operation::create("rxx", 2, 1))})); + valid(Target::create(2, {}, + NativeOperations::fromOperations( + {valid(Operation::create("u", 1, 3)), + valid(Operation::create("rxx", 2, 1))}))); const auto before = printModule(*module); ASSERT_TRUE(mlir::succeeded( @@ -682,8 +726,9 @@ TEST_F(TargetSynthesisTest, TEST_F(TargetSynthesisTest, ConformanceUsesHomogeneousCapabilitiesAndValidatesSites) { const auto target = valid(Target::create( - std::vector{valid(Site::create(10)), valid(Site::create(20))}, - std::nullopt, std::vector{valid(Operation::create("cx", 2, 0))})); + std::vector{valid(Site::create(10)), valid(Site::create(20))}, {}, + NativeOperations::fromOperations( + {valid(Operation::create("cx", 2, 0))}))); ASSERT_FALSE(target.synthesisBasis()); auto reversed = build([](QCOProgramBuilder& builder) { @@ -712,7 +757,8 @@ TEST_F(TargetSynthesisTest, TEST_F(TargetSynthesisTest, ConformanceRejectsDynamicAllocations) { const auto target = valid(Target::create( - 1, std::nullopt, std::vector{valid(Operation::create("x", 1, 0))})); + 1, {}, + NativeOperations::fromOperations({valid(Operation::create("x", 1, 0))}))); const auto expectDynamicAllocationFailure = [&](OwningOpRef module) { const auto diagnostics = expectFailure( @@ -747,7 +793,8 @@ TEST_F(TargetSynthesisTest, ConformanceRejectsQuantumFunctionInputs) { context.get()); ASSERT_TRUE(module); const auto target = valid(Target::create( - 1, std::nullopt, std::vector{valid(Operation::create("x", 1, 0))})); + 1, {}, + NativeOperations::fromOperations({valid(Operation::create("x", 1, 0))}))); const auto diagnostics = expectFailure(*module, mlir::qco::createVerifyTargetConformance(target)); @@ -769,8 +816,9 @@ TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityAndParameters) { }; expectUnsupported( - valid(Target::create(std::vector{valid(Site::create(10))}, std::nullopt, - std::vector{valid(Operation::create("x", 1, 0))})), + valid(Target::create(std::vector{valid(Site::create(10))}, {}, + NativeOperations::fromOperations( + {valid(Operation::create("x", 1, 0))}))), build([](QCOProgramBuilder& builder) { auto qubit = builder.staticQubit(10); qubit = builder.h(qubit); @@ -780,8 +828,9 @@ TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityAndParameters) { expectUnsupported( valid(Target::create( - std::vector{valid(Site::create(10)), valid(Site::create(20))}, - std::nullopt, std::vector{valid(Operation::create("x", 2, 0))})), + std::vector{valid(Site::create(10)), valid(Site::create(20))}, {}, + NativeOperations::fromOperations( + {valid(Operation::create("x", 2, 0))}))), build([](QCOProgramBuilder& builder) { auto qubit = builder.staticQubit(10); qubit = builder.x(qubit); @@ -790,8 +839,9 @@ TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityAndParameters) { "'qco.x'", "arity 1 and 0 parameter(s)"); expectUnsupported( - valid(Target::create(std::vector{valid(Site::create(10))}, std::nullopt, - std::vector{valid(Operation::create("rz", 1, 0))})), + valid(Target::create(std::vector{valid(Site::create(10))}, {}, + NativeOperations::fromOperations( + {valid(Operation::create("rz", 1, 0))}))), build([](QCOProgramBuilder& builder) { auto qubit = builder.staticQubit(10); qubit = builder.rz(0.25, qubit); @@ -809,7 +859,8 @@ TEST_F(TargetSynthesisTest, ConformanceChecksNonUnitaryCapabilities) { return builder.intConstant(0); }); const auto xOnly = valid(Target::create( - 1, std::nullopt, std::vector{valid(Operation::create("x", 1, 0))})); + 1, {}, + NativeOperations::fromOperations({valid(Operation::create("x", 1, 0))}))); const auto diagnostics = expectFailure(*module, mlir::qco::createVerifyTargetConformance(xOnly)); EXPECT_NE(diagnostics.find("'qco.measure' with arity 1 and 0 parameter(s)"), diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 66f4cdc369..d85e6c29d1 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -61,27 +61,27 @@ class OutputFormat(enum.Enum): class CompilerTarget: """Immutable MLIR compiler target. - An absent topology means all-to-all connectivity. An absent operation set - means every operation is native. + Connectivity and native-operation metadata distinguish unknown, + unrestricted, and explicitly enumerated support. """ @overload def __init__( self, - num_qubits: int, + num_sites: int, *, - couplings: Sequence[tuple[int, int]] | None = None, - operations: Sequence[CompilerTarget.Operation] | None = None, + connectivity: CompilerTarget.Connectivity = ..., + native_operations: CompilerTarget.NativeOperations = ..., duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload def __init__( self, name: str, - num_qubits: int, + num_sites: int, *, - couplings: Sequence[tuple[int, int]] | None = None, - operations: Sequence[CompilerTarget.Operation] | None = None, + connectivity: CompilerTarget.Connectivity = ..., + native_operations: CompilerTarget.NativeOperations = ..., duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload @@ -89,8 +89,8 @@ class CompilerTarget: self, sites: Sequence[CompilerTarget.Site], *, - couplings: Sequence[tuple[int, int]] | None = None, - operations: Sequence[CompilerTarget.Operation] | None = None, + connectivity: CompilerTarget.Connectivity = ..., + native_operations: CompilerTarget.NativeOperations = ..., duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload @@ -99,8 +99,8 @@ class CompilerTarget: name: str, sites: Sequence[CompilerTarget.Site], *, - couplings: Sequence[tuple[int, int]] | None = None, - operations: Sequence[CompilerTarget.Operation] | None = None, + connectivity: CompilerTarget.Connectivity = ..., + native_operations: CompilerTarget.NativeOperations = ..., duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @@ -162,7 +162,7 @@ class CompilerTarget: def __init__( self, name: str, - num_qubits: int, + arity: int, num_parameters: int, site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, duration: int | None = None, @@ -177,7 +177,7 @@ class CompilerTarget: """The normalized compiler operation name.""" @property - def num_qubits(self) -> int: + def arity(self) -> int: """The fixed operation arity.""" @property @@ -257,6 +257,70 @@ class CompilerTarget: def entangler(self) -> CompilerTarget.GateKind: """The two-qubit entangler.""" + class ConnectivityKind(enum.Enum): + """How target connectivity is known.""" + + UNKNOWN = 0 + + ALL_TO_ALL = 1 + + EXPLICIT = 2 + + class Connectivity: + """A target connectivity claim.""" + + @overload + def __init__(self) -> None: + """Create an unknown connectivity claim.""" + + @overload + def __init__(self, couplings: Sequence[tuple[int, int]]) -> None: + """Create an explicit connectivity claim.""" + + @staticmethod + def all_to_all() -> CompilerTarget.Connectivity: + """Create an all-to-all connectivity claim.""" + + @property + def kind(self) -> CompilerTarget.ConnectivityKind: + """How the connectivity is known.""" + + @property + def couplings(self) -> list[tuple[int, int]]: + """The explicit couplings, if present.""" + + class NativeOperationsKind(enum.Enum): + """How native target operations are known.""" + + UNKNOWN = 0 + + UNRESTRICTED = 1 + + EXPLICIT = 2 + + class NativeOperations: + """A native-operation claim.""" + + @overload + def __init__(self) -> None: + """Create an unknown native-operation claim.""" + + @overload + def __init__(self, operations: Sequence[CompilerTarget.Operation]) -> None: + """Create an explicit native-operation claim.""" + + @staticmethod + def unrestricted() -> CompilerTarget.NativeOperations: + """Create an unrestricted native-operation claim.""" + + @property + def kind(self) -> CompilerTarget.NativeOperationsKind: + """How the native operations are known.""" + + @property + def operations(self) -> list[CompilerTarget.Operation]: + """The explicit operations, if present.""" + @staticmethod def from_device(device: Device) -> CompilerTarget: """Snapshot a circuit-model QDMI device.""" @@ -274,7 +338,7 @@ class CompilerTarget: """The target timing unit, if available.""" @property - def num_qubits(self) -> int: + def num_sites(self) -> int: """The number of target sites.""" @property @@ -282,16 +346,16 @@ class CompilerTarget: """Detailed sites in compiler-vertex order.""" @property - def has_explicit_topology(self) -> bool: - """Whether the target defines a coupling topology.""" + def connectivity_kind(self) -> CompilerTarget.ConnectivityKind: + """How the target connectivity is known.""" @property def couplings(self) -> list[tuple[int, int]]: """Canonical undirected couplings in target site IDs.""" @property - def has_explicit_operations(self) -> bool: - """Whether the target defines an operation set.""" + def native_operations_kind(self) -> CompilerTarget.NativeOperationsKind: + """How the target native operations are known.""" @property def operations(self) -> list[CompilerTarget.Operation]: @@ -305,8 +369,8 @@ class CompilerTarget: def synthesis_basis(self) -> CompilerTarget.SynthesisBasis | None: """A complete target-wide synthesis basis, if available.""" - def supports_operation(self, name: str, num_qubits: int, num_parameters: int | None = None) -> bool: - """Whether the target supports an operation capability.""" + def supports_operation(self, name: str, arity: int, num_parameters: int | None = None) -> bool | None: + """Whether the target supports an operation, or None if unknown.""" class Program: """Base class for a typed MLIR compiler program. diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index e65e20e0a6..aac5773d5c 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -397,12 +397,12 @@ def test_qco_program_compiles_for_direct_sparse_target() -> None: target = CompilerTarget( "sparse target", [CompilerTarget.Site(10), CompilerTarget.Site(20)], - couplings=[(10, 20)], - operations=[ + connectivity=CompilerTarget.Connectivity([(10, 20)]), + native_operations=CompilerTarget.NativeOperations([ CompilerTarget.Operation("u", 1, 3), CompilerTarget.Operation("cz", 2, 0), CompilerTarget.Operation("measure", 1, 0), - ], + ]), ) assert target.name == "sparse target" assert [site.id for site in target.sites] == [10, 20] @@ -426,14 +426,18 @@ def test_qco_program_compiles_for_direct_sparse_target() -> None: @requires_qiskit_translation def test_target_compilation_exports_canonical_physical_qiskit_circuit() -> None: """Export a mapped program with the complete compiler target.""" - target = CompilerTarget(5) + target = CompilerTarget( + 5, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations.unrestricted(), + ) mapped = compile_program( QASM_STRING, output=OutputFormat.QCO_OPTIMIZED, target=target, ) assert isinstance(mapped, QCOProgram) - assert 0 < mapped.ir.count("qco.static") < target.num_qubits + assert 0 < mapped.ir.count("qco.static") < target.num_sites qc = mapped.to_qc(copy=True) restored = qc.to_qiskit(target=target) @@ -457,11 +461,20 @@ def test_compiler_target_constructors_preserve_python_api() -> None: targets = [ CompilerTarget(2, duration_unit=duration_unit), CompilerTarget("dense", 2, duration_unit=duration_unit), - CompilerTarget(sites, operations=[operation], duration_unit=duration_unit), - CompilerTarget("sparse", sites, operations=[operation], duration_unit=duration_unit), + CompilerTarget( + sites, + native_operations=CompilerTarget.NativeOperations([operation]), + duration_unit=duration_unit, + ), + CompilerTarget( + "sparse", + sites, + native_operations=CompilerTarget.NativeOperations([operation]), + duration_unit=duration_unit, + ), ] - assert [target.num_qubits for target in targets] == [2, 2, 2, 2] + assert [target.num_sites for target in targets] == [2, 2, 2, 2] assert targets[1].name == "dense" assert targets[3].name == "sparse" assert sites[0].name == "q0" @@ -484,7 +497,7 @@ def test_compiler_target_construction_preserves_validation_errors() -> None: CompilerTarget.SiteTuple([0, 0]) with pytest.raises(ValueError, match="duration unit must not be empty"): CompilerTarget.DurationUnit("", 1.0) - with pytest.raises(ValueError, match="operation qubit count must be positive"): + with pytest.raises(ValueError, match="operation arity must be positive"): CompilerTarget.Operation("x", 0, 0) @@ -493,7 +506,9 @@ def test_compiler_target_snapshots_qdmi_device(garnet_target: CompilerTarget) -> target = garnet_target assert target.name == "IQM Garnet" - assert target.num_qubits == 20 + assert target.num_sites == 20 + assert target.connectivity_kind == CompilerTarget.ConnectivityKind.EXPLICIT + assert target.native_operations_kind == CompilerTarget.NativeOperationsKind.EXPLICIT assert len(target.couplings) == 30 assert target.sites[0].name == "QB1" assert target.sites[0].t1 == 26626 @@ -523,16 +538,16 @@ def _compiler_target_metadata(target: CompilerTarget) -> dict[str, object]: return { "name": target.name, "duration_unit": None if duration_unit is None else (duration_unit.unit, duration_unit.scale_factor), - "num_qubits": target.num_qubits, + "num_sites": target.num_sites, "sites": [(site.id, site.name, site.t1, site.t2) for site in target.sites], - "has_explicit_topology": target.has_explicit_topology, + "connectivity_kind": target.connectivity_kind, "couplings": target.couplings, - "has_explicit_operations": target.has_explicit_operations, + "native_operations_kind": target.native_operations_kind, "operations": [ ( operation.name, operation.canonical_name, - operation.num_qubits, + operation.arity, operation.num_parameters, operation.duration, operation.fidelity, diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 7bdee690b1..998bdebaa2 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -226,10 +226,11 @@ def test_two_qubit_dense_unitary_compiles_to_target_basis() -> None: circuit.append(library.UnitaryGate(random_unitary(4, seed=2136)), [0, 1]) target = CompilerTarget( 2, - operations=[ + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations([ CompilerTarget.Operation("u", 1, 3), CompilerTarget.Operation("cx", 2, 0), - ], + ]), ) program = QCProgram.from_qiskit(circuit).to_qco(copy=True) @@ -527,7 +528,11 @@ def test_flat_export_rejects_classical_store_after_quantum_work(late_value: str) def test_target_compiled_openqasm2_measurements_export() -> None: """Export initialized result registers after target compilation.""" - target = CompilerTarget(5) + target = CompilerTarget( + 5, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations.unrestricted(), + ) program = QCProgram.from_qasm_str( """OPENQASM 2.0; include "qelib1.inc"; From 7bef0325ce306527079c5221ab7f61bb92d4f4c9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 23 Aug 2026 17:33:29 +0000 Subject: [PATCH 02/20] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agent/plans/generalize-compiler-target.md | 48 +++++++++++----------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/.agent/plans/generalize-compiler-target.md b/.agent/plans/generalize-compiler-target.md index ef1fa5f603..2251309c95 100644 --- a/.agent/plans/generalize-compiler-target.md +++ b/.agent/plans/generalize-compiler-target.md @@ -9,13 +9,13 @@ repository root. ## Purpose / Big Picture -The compiler target currently treats missing topology as all-to-all connectivity, -missing native operations as unrestricted support, and names every quantum -resource a qubit. After this change, target descriptions can represent neutral -atoms, trapped ions, photonic modes, spin qubits, and other site-based systems -without claiming facts that a provider did not report. A focused compiler target -test demonstrates the three knowledge states: unknown, unrestricted, and an -explicit list. +The compiler target currently treats missing topology as all-to-all +connectivity, missing native operations as unrestricted support, and names every +quantum resource a qubit. After this change, target descriptions can represent +neutral atoms, trapped ions, photonic modes, spin qubits, and other site-based +systems without claiming facts that a provider did not report. A focused +compiler target test demonstrates the three knowledge states: unknown, +unrestricted, and an explicit list. ## Progress @@ -23,10 +23,10 @@ explicit list. mapping, synthesis, QDMI adapter, bindings, and tests. - [x] (2026-08-23 17:08Z) Added explicit connectivity and native-operation knowledge states. -- [x] (2026-08-23 17:08Z) Replaced target qubit-count vocabulary with site - count and operation arity. -- [x] (2026-08-23 17:12Z) Updated compiler consumers, QDMI construction, - public bindings, Python tests, and documentation. +- [x] (2026-08-23 17:08Z) Replaced target qubit-count vocabulary with site count + and operation arity. +- [x] (2026-08-23 17:12Z) Updated compiler consumers, QDMI construction, public + bindings, Python tests, and documentation. - [x] (2026-08-23 17:20Z) Made passes request target facts only when the residual program needs them and made QDMI operation applicability fail closed when the provider does not report it. @@ -44,8 +44,8 @@ explicit list. absence to mean unrestricted support, so they cannot represent unknown metadata. Evidence: the class comment and `Storage::supportsOperation` in `mlir/lib/Compiler/Target.cpp`. -- Observation: Tests must compare `std::optional` with `true`, `false`, - or `std::nullopt`; `EXPECT_TRUE` and `EXPECT_FALSE` inspect only whether the +- Observation: Tests must compare `std::optional` with `true`, `false`, or + `std::nullopt`; `EXPECT_TRUE` and `EXPECT_FALSE` inspect only whether the optional has a value. Evidence: the first focused compiler test run exposed this test-only error. - Observation: QDMI operation site applicability is optional. Treating an @@ -88,10 +88,10 @@ and synthesis facts. Mapping and synthesis passes under Tests live in `mlir/unittests/Compiler/test_compiler_target.cpp` and adjacent mapping and synthesis test directories. -Unknown means the provider did not report enough information. Unrestricted -means every site pair or operation is accepted. Explicit means the target lists -the accepted couplings or operations. A pass that requires unknown information -must emit a diagnostic instead of assuming support. +Unknown means the provider did not report enough information. Unrestricted means +every site pair or operation is accepted. Explicit means the target lists the +accepted couplings or operations. A pass that requires unknown information must +emit a diagnostic instead of assuming support. ## Plan of Work @@ -99,9 +99,9 @@ Add small value types to `CompilerTarget` for connectivity and native-operation support. Each type carries a three-way kind and, for the explicit kind, the existing vector. Make target construction accept these values and default them to unknown. Rename target `numQubits()` to `numSites()` and operation -`numQubits()` to `arity()`. Update mapping, synthesis, the QDMI adapter, bindings, -and tests to use the new vocabulary and to handle unknown facts before querying -routes or operation support. +`numQubits()` to `arity()`. Update mapping, synthesis, the QDMI adapter, +bindings, and tests to use the new vocabulary and to handle unknown facts before +querying routes or operation support. Keep site identifiers, ordered operation site tuples, timing units, T1/T2 data, and fidelity values unchanged. Add no technology enum and no generic property @@ -144,7 +144,7 @@ The current behavior to replace is summarized by the existing class comment: ## Interfaces and Dependencies -Use LLVM containers already linked by the compiler. Do not add dependencies. -The public target keeps shared immutable storage. Connectivity and native -operation state are context-free C++ values so the later MLIR attribute layer -can materialize them without making `CompilerTarget` depend on an MLIR context. +Use LLVM containers already linked by the compiler. Do not add dependencies. The +public target keeps shared immutable storage. Connectivity and native operation +state are context-free C++ values so the later MLIR attribute layer can +materialize them without making `CompilerTarget` depend on an MLIR context. From 9308dc3fac3ee92439f19adeb794a05c4d6106dd Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sun, 23 Aug 2026 17:34:11 +0000 Subject: [PATCH 03/20] =?UTF-8?q?=F0=9F=93=9D=20Fold=20compiler=20target?= =?UTF-8?q?=20work=20into=20launch=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: OpenAI Codex --- .agent/plans/generalize-compiler-target.md | 12 ++++++------ CHANGELOG.md | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.agent/plans/generalize-compiler-target.md b/.agent/plans/generalize-compiler-target.md index 2251309c95..920497dd4f 100644 --- a/.agent/plans/generalize-compiler-target.md +++ b/.agent/plans/generalize-compiler-target.md @@ -32,11 +32,12 @@ unrestricted, and an explicit list. closed when the provider does not report it. - [x] (2026-08-23 17:25Z) Regenerated bindings and ran focused clang-tidy on every changed C++ source and test file. -- [ ] Add the pull request reference to the launch changelog entry. +- [x] (2026-08-23 17:33Z) Added the pull request reference to the existing + Compiler Collection launch changelog entry. - [x] (2026-08-23 17:31Z) Ran the compiler, mapping, synthesis, and Python tests; regenerated stubs; ran focused clang-tidy, full lint, and the final diff checks. -- [ ] Publish the signed pull request. +- [x] (2026-08-23 17:33Z) Published the signed change as pull request #2218. ## Surprises & Discoveries @@ -73,10 +74,9 @@ unrestricted, and an explicit list. ## Outcomes & Retrospective -The context-free target contract is implemented. The compiler, mapping, -synthesis, and focused Python suites pass. Generated stubs are current. Focused -clang-tidy, full lint, and final diff checks pass. The changelog reference and -publication remain. +The context-free target contract is implemented in pull request #2218. The +compiler, mapping, synthesis, and focused Python suites pass. Generated stubs +are current. Focused clang-tidy, full lint, and final diff checks pass. ## Context and Orientation diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1336a9d6..dad4654bfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ releases may include breaking changes. [#1807], [#1808], [#1815], [#1824], [#1869], [#1872], [#1914], [#1925], [#1927], [#1935], [#1936], [#1938], [#1975], [#1976], [#2006], [#2014], [#2015], [#2017], [#2026], [#2028], [#2054], [#2058], [#2125], [#2136], - [#2149], [#2150], [#2158], [#2194], [#2210], [#2211], [#2220]) + [#2149], [#2150], [#2158], [#2194], [#2210], [#2211], [#2220], [#2218]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**], [**@J4MMlE**]) @@ -868,6 +868,7 @@ for previous changelogs._ [#2315]: https://github.com/munich-quantum-toolkit/core/pull/2315 +[#2218]: https://github.com/munich-quantum-toolkit/core/pull/2218 [#2298]: https://github.com/munich-quantum-toolkit/core/pull/2298 [#2284]: https://github.com/munich-quantum-toolkit/core/pull/2284 [#2283]: https://github.com/munich-quantum-toolkit/core/pull/2283 From 4ca21783adc7d85b48c9c5c407e4f168767d4438 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sun, 23 Aug 2026 17:55:09 +0000 Subject: [PATCH 04/20] =?UTF-8?q?=F0=9F=A7=AA=20State=20DDSIM=20target=20f?= =?UTF-8?q?acts=20explicitly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep unavailable QDMI v1.3 properties unknown while preserving the compile-and-execute integration with explicit simulator capabilities. Assisted-by: OpenAI Codex --- .agent/plans/generalize-compiler-target.md | 9 +++++++++ docs/qdmi/ddsim_device.md | 11 ++++++++--- test/python/qdmi/test_qdmi.py | 6 +++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.agent/plans/generalize-compiler-target.md b/.agent/plans/generalize-compiler-target.md index 920497dd4f..ee4c59827d 100644 --- a/.agent/plans/generalize-compiler-target.md +++ b/.agent/plans/generalize-compiler-target.md @@ -38,6 +38,8 @@ unrestricted, and an explicit list. tests; regenerated stubs; ran focused clang-tidy, full lint, and the final diff checks. - [x] (2026-08-23 17:33Z) Published the signed change as pull request #2218. +- [x] (2026-08-23 17:55Z) Kept the DDSIM QDMI snapshot fail-closed and made its + QIR example state the simulator's unrestricted facts explicitly. ## Surprises & Discoveries @@ -54,6 +56,9 @@ unrestricted, and an explicit list. operation claim. Evidence: `QDMI_OPERATION_PROPERTY_SITES` defines the valid site tuples, while `Operation::getSites()` returns `std::nullopt` when the provider does not report the property. +- Observation: QDMI v1.3 has no compact representation for all-to-all + connectivity or unrestricted operations. Evidence: the DDSIM device omits both + optional lists because enumerating all pairs of 65,535 sites is not practical. ## Decision Log @@ -71,6 +76,10 @@ unrestricted, and an explicit list. needs that fact. Rationale: program requirements are stage-relative; a classical or single-site program does not need native-operation or topology claims. Date/Author: 2026-08-23, Codex. +- Decision: Keep the DDSIM snapshot unknown and state its known simulator facts + at the compiler call site. Rationale: interpreting unavailable QDMI v1.3 + properties as unrestricted would weaken the target contract for every + provider. Date/Author: 2026-08-23, Codex. ## Outcomes & Retrospective diff --git a/docs/qdmi/ddsim_device.md b/docs/qdmi/ddsim_device.md index 2817165a97..53cca0b385 100644 --- a/docs/qdmi/ddsim_device.md +++ b/docs/qdmi/ddsim_device.md @@ -43,8 +43,9 @@ The device implements the full QDMI job interface (except for the ## Compile and execute QIR -The compiler can snapshot the DDSIM device as an all-to-all target, compile a -program to QIR, and submit the resulting bitcode to the same device: +QDMI v1.3 cannot report unrestricted topology and operation support. State these +known DDSIM properties when compiling a program to QIR, then submit the +resulting bitcode to the same device: ```python from mqt.core.mlir import CompilerTarget, OutputFormat, compile_program @@ -52,7 +53,11 @@ from mqt.core.qdmi import ProgramFormat from mqt.core.qdmi.driver import open_device device = open_device("mqt.ddsim.default") -target = CompilerTarget.from_device(device) +target = CompilerTarget( + device.qubits_num(), + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations.unrestricted(), +) program = compile_program( "bell.qasm", target=target, diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index 5ac24a7275..529e2a4b64 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -552,7 +552,11 @@ def test_device_executes_qir_program(ddsim_device: Device) -> None: cx q[0], q[1]; c = measure q; """ - target = CompilerTarget.from_device(ddsim_device) + target = CompilerTarget( + ddsim_device.qubits_num(), + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations.unrestricted(), + ) program = compile_program(qasm3_program, output=OutputFormat.QIR_BASE, target=target) assert ProgramFormat.QIR_BASE_STRING in ddsim_device.supported_program_formats() From 73c50088f759ceee1bff10ed2dc5c94c95a7512b Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sun, 23 Aug 2026 18:02:59 +0000 Subject: [PATCH 05/20] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Use=20compact=20LLVM?= =?UTF-8?q?=20target=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace ordered sets with the project's LLVM containers and in-place canonicalization. Assisted-by: OpenAI Codex --- mlir/lib/Compiler/Target.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index bab2fcf1f0..54ec2a5815 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include +#include #include #include #include @@ -33,7 +34,6 @@ #include #include #include -#include #include #include #include @@ -245,7 +245,7 @@ llvm::Expected CompilerTarget::SiteTuple::create(std::vector sites, const std::optional duration, const std::optional fidelity) { - std::set uniqueSites; + llvm::SmallDenseSet uniqueSites; for (const auto site : sites) { if (site < 0) { return invalidTarget( @@ -296,18 +296,17 @@ llvm::Expected CompilerTarget::Operation::create( return std::move(error); } - std::set> uniqueSiteCombinations; + SmallVector> uniqueSiteCombinations; for (const auto& siteTuple : siteTuples) { if (siteTuple.sites().size() != arity) { return invalidTarget( "Compiler target operation site tuple does not match its arity"); } - if (!uniqueSiteCombinations - .emplace(siteTuple.sites().begin(), siteTuple.sites().end()) - .second) { + if (llvm::is_contained(uniqueSiteCombinations, siteTuple.sites())) { return invalidTarget( "Compiler target operation contains a duplicate site tuple"); } + uniqueSiteCombinations.emplace_back(siteTuple.sites()); } return Operation(std::move(name), std::move(canonicalName), arity, numParameters, std::move(siteTuples), duration, fidelity); @@ -467,8 +466,7 @@ llvm::Error CompilerTarget::Storage::initialize() { } if (connectivityKind == Connectivity::Kind::Explicit) { - std::set canonicalCouplings; - for (auto [source, target] : couplings) { + for (auto& [source, target] : couplings) { if (!siteToVertex.contains(source) || !siteToVertex.contains(target)) { return invalidTarget( "Compiler target topology references an unknown site"); @@ -480,9 +478,9 @@ llvm::Error CompilerTarget::Storage::initialize() { if (target < source) { std::swap(source, target); } - canonicalCouplings.emplace(source, target); } - couplings.assign(canonicalCouplings.begin(), canonicalCouplings.end()); + std::ranges::sort(couplings); + couplings.erase(std::ranges::unique(couplings).begin(), couplings.end()); adjacency.resize(sites.size()); for (const auto& [source, target] : couplings) { From c18c96442d8c1439387c960ae587100e6c212475 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 24 Aug 2026 12:38:51 +0000 Subject: [PATCH 06/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20LLVM=20views?= =?UTF-8?q?=20for=20target=20collections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy public collection views into compact LLVM storage and document the explicit DDSIM synthesis basis used until QDMI v1.4 reports all-to-all connectivity. Assisted-by: GPT-5.6 Sol via Codex --- bindings/mlir/register_mlir.cpp | 9 ++--- docs/qdmi/ddsim_device.md | 14 +++++-- mlir/include/mlir/Compiler/Target.h | 13 ++++--- mlir/lib/Compiler/QDMIAdapter.cpp | 8 ++-- mlir/lib/Compiler/Target.cpp | 38 +++++++++---------- .../Compiler/test_compiler_pipeline.cpp | 4 +- .../Compiler/test_compiler_target.cpp | 7 ++-- .../QCO/Transforms/Mapping/test_mapping.cpp | 4 +- .../NativeSynthesis/test_target_synthesis.cpp | 5 +-- 9 files changed, 52 insertions(+), 50 deletions(-) diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index e8ae5c1075..7bbce98d6d 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -540,10 +540,9 @@ unrestricted, and explicitly enumerated support.)pb"); .def( "__init__", [](mlir::CompilerTarget::Connectivity& self, - std::vector couplings) { + const std::vector& couplings) { new (&self) mlir::CompilerTarget::Connectivity( - mlir::CompilerTarget::Connectivity::fromCouplings( - std::move(couplings))); + mlir::CompilerTarget::Connectivity::fromCouplings(couplings)); }, "couplings"_a, "Create an explicit connectivity claim.") .def_static("all_to_all", &mlir::CompilerTarget::Connectivity::allToAll, @@ -574,10 +573,10 @@ unrestricted, and explicitly enumerated support.)pb"); .def( "__init__", [](mlir::CompilerTarget::NativeOperations& self, - std::vector operations) { + const std::vector& operations) { new (&self) mlir::CompilerTarget::NativeOperations( mlir::CompilerTarget::NativeOperations::fromOperations( - std::move(operations))); + operations)); }, "operations"_a, "Create an explicit native-operation claim.") .def_static("unrestricted", diff --git a/docs/qdmi/ddsim_device.md b/docs/qdmi/ddsim_device.md index 53cca0b385..7980f5e1c4 100644 --- a/docs/qdmi/ddsim_device.md +++ b/docs/qdmi/ddsim_device.md @@ -43,9 +43,10 @@ The device implements the full QDMI job interface (except for the ## Compile and execute QIR -QDMI v1.3 cannot report unrestricted topology and operation support. State these -known DDSIM properties when compiling a program to QIR, then submit the -resulting bitcode to the same device: +QDMI v1.3 cannot report the DDSIM device's all-to-all topology. State this +temporary topology workaround and an explicit DDSIM synthesis basis when +compiling a program to QIR. QDMI v1.4 will remove the topology workaround. +Submit the resulting bitcode to the same device: ```python from mqt.core.mlir import CompilerTarget, OutputFormat, compile_program @@ -56,7 +57,12 @@ device = open_device("mqt.ddsim.default") target = CompilerTarget( device.qubits_num(), connectivity=CompilerTarget.Connectivity.all_to_all(), - native_operations=CompilerTarget.NativeOperations.unrestricted(), + native_operations=CompilerTarget.NativeOperations([ + CompilerTarget.Operation("u", 1, 3), + CompilerTarget.Operation("cx", 2, 0), + CompilerTarget.Operation("measure", 1, 0), + CompilerTarget.Operation("reset", 1, 0), + ]), ) program = compile_program( "bell.qasm", diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index 8ff5d3caf8..f7a41a17c9 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -56,7 +57,7 @@ class CompilerTarget { /// Create explicitly enumerated connectivity. [[nodiscard]] static Connectivity - fromCouplings(std::vector couplings); + fromCouplings(llvm::ArrayRef couplings); /// Return the connectivity knowledge kind. [[nodiscard]] Kind kind() const noexcept; @@ -67,10 +68,10 @@ class CompilerTarget { private: friend class CompilerTarget; - Connectivity(Kind kind, std::vector couplings); + Connectivity(Kind kind, llvm::ArrayRef couplings); Kind kind_; - std::vector couplings_; + llvm::SmallVector couplings_; }; /** @@ -232,7 +233,7 @@ class CompilerTarget { /// Create explicitly enumerated native-operation support. [[nodiscard]] static NativeOperations - fromOperations(std::vector operations); + fromOperations(llvm::ArrayRef operations); /// Return the native-operation knowledge kind. [[nodiscard]] Kind kind() const noexcept; @@ -243,10 +244,10 @@ class CompilerTarget { private: friend class CompilerTarget; - NativeOperations(Kind kind, std::vector operations); + NativeOperations(Kind kind, llvm::ArrayRef operations); Kind kind_; - std::vector operations_; + llvm::SmallVector operations_; }; /** diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index c5354af54c..26cd1097b6 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -311,8 +311,7 @@ snapshotOperations( } targetOperations.emplace_back(std::move(*targetOperation)); } - return CompilerTarget::NativeOperations::fromOperations( - std::move(targetOperations)); + return CompilerTarget::NativeOperations::fromOperations(targetOperations); } [[nodiscard]] static llvm::Expected @@ -373,9 +372,8 @@ snapshotCompilerTarget(const qdmi::Device& device) { return durationUnit.takeError(); } auto connectivity = - couplings - ? CompilerTarget::Connectivity::fromCouplings(std::move(*couplings)) - : CompilerTarget::Connectivity{}; + couplings ? CompilerTarget::Connectivity::fromCouplings(*couplings) + : CompilerTarget::Connectivity{}; return CompilerTarget::create(std::move(deviceName), std::move(sites), std::move(connectivity), std::move(*operations), std::move(*durationUnit)); diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 54ec2a5815..522ebef753 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -131,9 +131,9 @@ CompilerTarget::Connectivity CompilerTarget::Connectivity::allToAll() { CompilerTarget::Connectivity::Connectivity() noexcept : kind_(Kind::Unknown) {} -CompilerTarget::Connectivity -CompilerTarget::Connectivity::fromCouplings(std::vector couplings) { - return {Kind::Explicit, std::move(couplings)}; +CompilerTarget::Connectivity CompilerTarget::Connectivity::fromCouplings( + const ArrayRef couplings) { + return {Kind::Explicit, couplings}; } CompilerTarget::Connectivity::Kind @@ -147,8 +147,8 @@ CompilerTarget::Connectivity::couplings() const noexcept { } CompilerTarget::Connectivity::Connectivity(const Kind kind, - std::vector couplings) - : kind_(kind), couplings_(std::move(couplings)) {} + const ArrayRef couplings) + : kind_(kind), couplings_(couplings) {} [[nodiscard]] static llvm::Expected> makeDenseSites(const size_t numSites) { @@ -359,8 +359,8 @@ CompilerTarget::NativeOperations::NativeOperations() noexcept CompilerTarget::NativeOperations CompilerTarget::NativeOperations::fromOperations( - std::vector operations) { - return {Kind::Explicit, std::move(operations)}; + const ArrayRef operations) { + return {Kind::Explicit, operations}; } CompilerTarget::NativeOperations::Kind @@ -374,23 +374,23 @@ CompilerTarget::NativeOperations::operations() const noexcept { } CompilerTarget::NativeOperations::NativeOperations( - const Kind kind, std::vector operations) - : kind_(kind), operations_(std::move(operations)) {} + const Kind kind, const ArrayRef operations) + : kind_(kind), operations_(operations) {} struct CompilerTarget::Storage { Storage(std::optional targetName, std::vector targetSites, Connectivity::Kind targetConnectivityKind, - std::vector targetCouplings, + SmallVector targetCouplings, NativeOperations::Kind targetNativeOperationsKind, - std::vector targetOperations, + SmallVector targetOperations, std::optional targetDurationUnit); [[nodiscard]] static llvm::Expected> create(std::optional targetName, std::vector targetSites, Connectivity::Kind targetConnectivityKind, - std::vector targetCouplings, + SmallVector targetCouplings, NativeOperations::Kind targetNativeOperationsKind, - std::vector targetOperations, + SmallVector targetOperations, std::optional targetDurationUnit); [[nodiscard]] llvm::Error initialize(); @@ -406,12 +406,12 @@ struct CompilerTarget::Storage { SmallVector siteIds; DenseMap siteToVertex; Connectivity::Kind connectivityKind; - std::vector couplings; + SmallVector couplings; SmallVector> adjacency; SmallVector distances; size_t maximumDegree = 0; NativeOperations::Kind nativeOperationsKind; - std::vector operations; + SmallVector operations; llvm::StringMap> capabilities; SmallVector supportedGates; std::optional basis; @@ -420,9 +420,9 @@ struct CompilerTarget::Storage { CompilerTarget::Storage::Storage( std::optional targetName, std::vector targetSites, const Connectivity::Kind targetConnectivityKind, - std::vector targetCouplings, + SmallVector targetCouplings, const NativeOperations::Kind targetNativeOperationsKind, - std::vector targetOperations, + SmallVector targetOperations, std::optional targetDurationUnit) : name(std::move(targetName)), durationUnit(std::move(targetDurationUnit)), sites(std::move(targetSites)), connectivityKind(targetConnectivityKind), @@ -434,9 +434,9 @@ llvm::Expected> CompilerTarget::Storage::create( std::optional targetName, std::vector targetSites, const Connectivity::Kind targetConnectivityKind, - std::vector targetCouplings, + SmallVector targetCouplings, const NativeOperations::Kind targetNativeOperationsKind, - std::vector targetOperations, + SmallVector targetOperations, std::optional targetDurationUnit) { auto storage = std::make_shared( std::move(targetName), std::move(targetSites), targetConnectivityKind, diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 41da23c550..bb67ae88f8 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -205,8 +205,8 @@ makeSparseUCZTarget(const bool includeMeasure) { llvm::cantFail(Site::create(17))}; return llvm::cantFail(CompilerTarget::create( "sparse-line", std::move(sites), - std::vector{{5, 9}, {9, 17}}, - std::move(operations))); + CompilerTarget::Connectivity::fromCouplings({{5, 9}, {9, 17}}), + CompilerTarget::NativeOperations::fromOperations(operations))); } using NameAndCount = std::pair; diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index e3e1f3f8c5..074091898c 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -75,7 +75,7 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { const auto target = valid( Target::create("device", std::move(sites), Connectivity::fromCouplings({{11, 2}, {2, 11}, {7, 2}}), - NativeOperations::fromOperations(std::move(operations)), + NativeOperations::fromOperations(operations), valid(DurationUnit::create("ns", 0.5)))); // The copy itself is the behavior under test: both objects must share the // immutable backing storage. @@ -393,9 +393,8 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { valid(Operation::create("measure", 1, 0)), valid(Operation::create("reset", 1, 0)), valid(Operation::create("cnot", 2, 0, std::move(directionalTuples)))}; - const auto target = valid( - Target::create(std::move(sites), {}, - NativeOperations::fromOperations(std::move(operations)))); + const auto target = valid(Target::create( + std::move(sites), {}, NativeOperations::fromOperations(operations))); EXPECT_EQ(target.supports(x), true); EXPECT_EQ(target.supports(cx), true); EXPECT_EQ(target.supports(measure), true); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 98e26283f0..eefa9a35c7 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -246,7 +246,7 @@ static CompilerTarget getSquareGridTarget(const size_t n) { } return llvm::cantFail(CompilerTarget::create( - numTarget, Connectivity::fromCouplings(std::move(couplings)))); + numTarget, Connectivity::fromCouplings(couplings))); } /// Creates an N-qubit GHZ state, where N = `qubits.size()` using @@ -604,7 +604,7 @@ TEST_F(MappingPassFixture, KeepWorkspaceSparseOnLargeTarget) { } const auto target = llvm::cantFail(CompilerTarget::create( - numTargetQubits, Connectivity::fromCouplings(std::move(couplings)))); + numTargetQubits, Connectivity::fromCouplings(couplings))); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(2, builder.getI1Type())); 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 8ac7aa55f6..596cd27490 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -156,9 +156,8 @@ makeUCxTarget(std::optional> sites = std::nullopt) { } std::vector operations{valid(Operation::create("u", 1, 3)), valid(Operation::create("cx", 2, 0))}; - return valid( - Target::create(std::move(*sites), {}, - NativeOperations::fromOperations(std::move(operations)))); + return valid(Target::create(std::move(*sites), {}, + NativeOperations::fromOperations(operations))); } [[nodiscard]] static mlir::DenseElementsAttr From d860d6558e34a2bed7c968dcd75138cb0a340ae1 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 1 Sep 2026 06:13:04 +0000 Subject: [PATCH 07/20] =?UTF-8?q?=F0=9F=90=9B=20Preserve=20compiler=20targ?= =?UTF-8?q?et=20fact=20semantics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid topology reconciliation when a single-site program does not need unknown connectivity, preserve the full nonnegative site-ID domain, and document explicit DDSIM capabilities. Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/generalize-compiler-target.md | 34 +++++++++- docs/mlir/target_compilation.md | 17 +++-- docs/qdmi/ddsim_device.md | 28 +++++--- mlir/lib/Compiler/QDMIAdapter.cpp | 16 ++--- mlir/lib/Compiler/Target.cpp | 8 +-- .../QCO/Transforms/Mapping/Mapping.cpp | 68 ++++++++++--------- .../Compiler/test_compiler_pipeline.cpp | 10 +-- .../Compiler/test_compiler_target.cpp | 20 ++++++ .../QCO/Transforms/Mapping/test_mapping.cpp | 21 ++++-- test/python/qdmi/test_qdmi.py | 14 +++- 10 files changed, 167 insertions(+), 69 deletions(-) diff --git a/.agent/plans/generalize-compiler-target.md b/.agent/plans/generalize-compiler-target.md index ee4c59827d..3106e8ed11 100644 --- a/.agent/plans/generalize-compiler-target.md +++ b/.agent/plans/generalize-compiler-target.md @@ -40,6 +40,19 @@ unrestricted, and an explicit list. - [x] (2026-08-23 17:33Z) Published the signed change as pull request #2218. - [x] (2026-08-23 17:55Z) Kept the DDSIM QDMI snapshot fail-closed and made its QIR example state the simulator's unrestricted facts explicitly. +- [x] (2026-09-01 05:42Z) Archived the exact published head, rebased the signed + commits onto current `main`, and reconciled post-branch compiler tests + with the explicit connectivity and operation states. +- [x] (2026-09-01 05:50Z) Addressed the substantive review findings: preserved + the full nonnegative site-ID domain, avoided topology reconciliation for + single-site control flow with unknown connectivity, and documented a + truthful explicit DDSIM operation set. +- [x] (2026-09-01 06:11Z) Regenerated bindings and ran 139 compiler, 83 mapping, + 25 target-synthesis, and focused Python QIR tests; built the + documentation; and ran full repository lint plus changed-file C++ lint. +- [x] (2026-09-01 08:41Z) Archived the pre-refresh heads, rebased onto `main` at + `30bb9d1f8`, adapted the newly merged mapping regression test to the + three-state target API, and reran the affected builds and tests. ## Surprises & Discoveries @@ -59,6 +72,14 @@ unrestricted, and an explicit list. - Observation: QDMI v1.3 has no compact representation for all-to-all connectivity or unrestricted operations. Evidence: the DDSIM device omits both optional lists because enumerating all pairs of 65,535 sites is not practical. +- Observation: LLVM dense containers reserve sentinel values inside the key + domain. Evidence: `DenseMapInfo` reserves the two largest signed + values, while `CompilerTarget::SiteId` intentionally accepts every nonnegative + `int64_t` value. +- Observation: Unknown connectivity is sufficient for a program containing no + multi-site operation, including structured control flow. Evidence: such a + program cannot change its layout, so branch reconciliation would only query + topology unnecessarily. ## Decision Log @@ -80,12 +101,23 @@ unrestricted, and an explicit list. at the compiler call site. Rationale: interpreting unavailable QDMI v1.3 properties as unrestricted would weaken the target contract for every provider. Date/Author: 2026-08-23, Codex. +- Decision: Use sentinel-free standard containers for site IDs. Rationale: this + preserves the documented public domain instead of introducing an arbitrary + range restriction to accommodate an implementation detail. Date/Author: + 2026-09-01, Lukas Burgholzer with Codex assistance. +- Decision: Skip structured-control-flow topology reconciliation only for + unknown connectivity after the pass has established that no multi-site + operation remains. Rationale: the layouts cannot diverge in that case, while + all-to-all and explicit topologies retain their existing reconciliation. + Date/Author: 2026-09-01, Lukas Burgholzer with Codex assistance. ## Outcomes & Retrospective The context-free target contract is implemented in pull request #2218. The compiler, mapping, synthesis, and focused Python suites pass. Generated stubs -are current. Focused clang-tidy, full lint, and final diff checks pass. +are current. Documentation, changed-file C++ lint, full repository lint, and +final diff checks pass. Durable archive branches preserve both published heads +from before the rescope and from before the latest `main` refresh. ## Context and Orientation diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 7c187ccc0f..4d01a7375f 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -34,14 +34,23 @@ metadata are unknown unless the caller states them: target = CompilerTarget( 3, connectivity=CompilerTarget.Connectivity([(0, 1), (1, 2)]), - native_operations=CompilerTarget.NativeOperations.unrestricted(), + native_operations=CompilerTarget.NativeOperations([ + CompilerTarget.Operation("u", arity=1, num_parameters=3), + CompilerTarget.Operation("cx", arity=2, num_parameters=0), + CompilerTarget.Operation("measure", arity=1, num_parameters=0), + CompilerTarget.Operation("reset", arity=1, num_parameters=0), + ]), ) ``` Use `CompilerTarget.Connectivity.all_to_all()` for an all-to-all target. An -empty `CompilerTarget.NativeOperations([])` means that no operation is native. -The default-constructed metadata objects mean that the corresponding support is -unknown; target compilation rejects an unknown property when a pass needs it. +empty `CompilerTarget.NativeOperations([])` reports that no quantum operation is +native. It can be used with passes that need only topology, but target +compilation cannot lower quantum operations without a synthesis basis. Use +`CompilerTarget.NativeOperations.unrestricted()` only when the target accepts +every operation. The default-constructed metadata objects mean that the +corresponding support is unknown; target compilation rejects an unknown property +when a pass needs it. Use {py:meth}`~mqt.core.mlir.QCOProgram.compile_for_target` to apply target compilation to an existing QCO program. Compilation runs in place. If a pass diff --git a/docs/qdmi/ddsim_device.md b/docs/qdmi/ddsim_device.md index 7980f5e1c4..eb056c3c19 100644 --- a/docs/qdmi/ddsim_device.md +++ b/docs/qdmi/ddsim_device.md @@ -43,10 +43,10 @@ The device implements the full QDMI job interface (except for the ## Compile and execute QIR -QDMI v1.3 cannot report the DDSIM device's all-to-all topology. State this -temporary topology workaround and an explicit DDSIM synthesis basis when -compiling a program to QIR. QDMI v1.4 will remove the topology workaround. -Submit the resulting bitcode to the same device: +QDMI v1.3 cannot compactly report the DDSIM device's all-to-all topology or the +site applicability of every operation. State the topology and reconstruct the +fixed-arity operation list from the device metadata when compiling a program to +QIR. Submit the resulting bitcode to the same device: ```python from mqt.core.mlir import CompilerTarget, OutputFormat, compile_program @@ -54,15 +54,23 @@ from mqt.core.qdmi import ProgramFormat from mqt.core.qdmi.driver import open_device device = open_device("mqt.ddsim.default") +operations = [] +for operation in device.operations(): + arity = operation.qubits_num() + if arity is None or arity == 0: + continue + operations.append( + CompilerTarget.Operation( + name=operation.name(), + arity=arity, + num_parameters=operation.parameters_num(), + ) + ) + target = CompilerTarget( device.qubits_num(), connectivity=CompilerTarget.Connectivity.all_to_all(), - native_operations=CompilerTarget.NativeOperations([ - CompilerTarget.Operation("u", 1, 3), - CompilerTarget.Operation("cx", 2, 0), - CompilerTarget.Operation("measure", 1, 0), - CompilerTarget.Operation("reset", 1, 0), - ]), + native_operations=CompilerTarget.NativeOperations(operations), ) program = compile_program( "bell.qasm", diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index 26cd1097b6..0885b4f146 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -14,7 +14,6 @@ #include "qdmi/Client.hpp" #include "qdmi/driver/Driver.hpp" -#include #include #include #include @@ -27,9 +26,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -124,14 +125,14 @@ isSwapInvariantOperation(const llvm::StringRef operationName) { return error; } - llvm::DenseSet knownSites; + std::unordered_set knownSites; knownSites.reserve(deviceSites.size()); for (const auto& site : deviceSites) { knownSites.insert(site.id()); } if (arity == 1) { - llvm::DenseSet supportedSites; + std::unordered_set supportedSites; supportedSites.reserve(flattenedSites.size()); for (const auto& site : flattenedSites) { auto siteId = checkedSiteId(site.getIndex()); @@ -151,10 +152,8 @@ isSwapInvariantOperation(const llvm::StringRef operationName) { "the operation is not available on every device site"); } - llvm::DenseSet reportedTuples; - llvm::DenseSet supportedCouplings; - reportedTuples.reserve(flattenedSites.size() / arity); - supportedCouplings.reserve(flattenedSites.size() / arity); + std::set reportedTuples; + std::set supportedCouplings; for (size_t offset = 0; offset < flattenedSites.size(); offset += arity) { auto first = checkedSiteId(flattenedSites[offset].getIndex()); if (!first) { @@ -181,8 +180,7 @@ isSwapInvariantOperation(const llvm::StringRef operationName) { const auto expected = allToAllCouplingCount(knownSites.size()); coversTarget = expected && supportedCouplings.size() == *expected; } else { - llvm::DenseSet expectedCouplings; - expectedCouplings.reserve(couplings->size()); + std::set expectedCouplings; for (const auto& [first, second] : *couplings) { expectedCouplings.insert(canonicalCoupling(first, second)); } diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 522ebef753..60bb61983e 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -13,8 +13,6 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include -#include #include #include #include @@ -36,6 +34,8 @@ #include #include #include +#include +#include #include #include @@ -245,7 +245,7 @@ llvm::Expected CompilerTarget::SiteTuple::create(std::vector sites, const std::optional duration, const std::optional fidelity) { - llvm::SmallDenseSet uniqueSites; + std::unordered_set uniqueSites; for (const auto site : sites) { if (site < 0) { return invalidTarget( @@ -404,7 +404,7 @@ struct CompilerTarget::Storage { std::optional durationUnit; std::vector sites; SmallVector siteIds; - DenseMap siteToVertex; + std::unordered_map siteToVertex; Connectivity::Kind connectivityKind; SmallVector couplings; SmallVector> adjacency; diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index c3a697a982..00f279e454 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -1517,38 +1517,42 @@ struct MappingPass : impl::MappingPassBase { // using the restore (scf::ForOp, scf::While), converge (IfOp), and vote // and restore (IndexSwitchOp) strategies. - Layout exit = - TypeSwitch(op) - .Case([&](scf::ForOp) { - const auto swaps = restore(children[0].layout, parent.layout); - insertSWAPs(swaps, children[0], totalStats, rewriter); - return parent.layout; - }) - .template Case([&](scf::WhileOp) { - const auto swaps = restore(children[1].layout, parent.layout); - insertSWAPs(swaps, children[1], totalStats, rewriter); - // The scf::YieldOp is the terminator in the before region and - // thus determines the final output layout. - return children[0].layout; - }) - .template Case([&](IfOp) { - const auto [convergedLayout, fst, snd] = - converge(children[0].layout, children[1].layout); - insertSWAPs(fst, children[0], totalStats, rewriter); - insertSWAPs(snd, children[1], totalStats, rewriter); - return convergedLayout; - }) - .template Case([&](IndexSwitchOp) { - auto compromise = driveby(map_range( - children, [](const RoutingBundle& b) -> const Layout& { - return b.layout; - })); - for (RoutingBundle& child : children) { - const auto swaps = restore(child.layout, compromise); - insertSWAPs(swaps, child, totalStats, rewriter); - } - return compromise; - }); + Layout exit = parent.layout; + if (target->connectivityKind() != + CompilerTarget::Connectivity::Kind::Unknown) { + exit = + TypeSwitch(op) + .Case([&](scf::ForOp) { + const auto swaps = restore(children[0].layout, parent.layout); + insertSWAPs(swaps, children[0], totalStats, rewriter); + return parent.layout; + }) + .template Case([&](scf::WhileOp) { + const auto swaps = restore(children[1].layout, parent.layout); + insertSWAPs(swaps, children[1], totalStats, rewriter); + // The scf::YieldOp is the terminator in the before region and + // thus determines the final output layout. + return children[0].layout; + }) + .template Case([&](IfOp) { + const auto [convergedLayout, fst, snd] = + converge(children[0].layout, children[1].layout); + insertSWAPs(fst, children[0], totalStats, rewriter); + insertSWAPs(snd, children[1], totalStats, rewriter); + return convergedLayout; + }) + .template Case([&](IndexSwitchOp) { + auto compromise = driveby(map_range( + children, [](const RoutingBundle& b) -> const Layout& { + return b.layout; + })); + for (RoutingBundle& child : children) { + const auto swaps = restore(child.layout, compromise); + insertSWAPs(swaps, child, totalStats, rewriter); + } + return compromise; + }); + } if constexpr (Mode == RoutingMode::Hot) { // Realign terminator values to ensure that i-th input qubit and the diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index bb67ae88f8..3edd16fe08 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -221,8 +221,9 @@ makeCZTarget(std::initializer_list singleQubitGates) { llvm::cantFail(Operation::create(name.str(), 1, numParameters))); } operations.emplace_back(llvm::cantFail(Operation::create("cz", 2, 0))); - return llvm::cantFail( - CompilerTarget::create(2, std::nullopt, std::move(operations))); + return llvm::cantFail(CompilerTarget::create( + 2, CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::fromOperations(operations))); } TEST_P(CompilerPipelineTest, EndToEndPipeline) { @@ -1542,8 +1543,9 @@ TEST_F(CompilerPipelineTest, QCOProgramMergesDynamicRunInNativeCtrlBody) { llvm::cantFail(Operation::create("cz", 2, 0)), llvm::cantFail(Operation::create("ctrl", 2, 0)), }; - const auto target = llvm::cantFail( - CompilerTarget::create(2, std::nullopt, std::move(operations))); + const auto target = llvm::cantFail(CompilerTarget::create( + 2, CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::fromOperations(operations))); ASSERT_TRUE(target.synthesisBasis()); ASSERT_EQ(target.synthesisBasis()->singleQubit, CompilerTarget::SingleQubitBasis::ZSXX); diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 074091898c..f11c0cf2b9 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -137,6 +137,26 @@ TEST(CompilerTargetTest, ConstructsDenseUnnamedAllToAllTarget) { EXPECT_EQ(neighbours, (std::vector{0, 2})); } +TEST(CompilerTargetTest, PreservesFullNonnegativeSiteIdDomain) { + constexpr auto maxSite = std::numeric_limits::max(); + constexpr auto nextSite = maxSite - 1; + auto siteTuple = valid(SiteTuple::create({maxSite, nextSite})); + std::vector sites{valid(Site::create(maxSite)), + valid(Site::create(nextSite))}; + const auto target = valid(Target::create( + std::move(sites), Connectivity::fromCouplings({{maxSite, nextSite}}), + NativeOperations::fromOperations( + {valid(Operation::create("cx", 2, 0, {std::move(siteTuple)}))}))); + + EXPECT_EQ(target.siteIds(), (llvm::ArrayRef{maxSite, nextSite})); + EXPECT_EQ(target.vertexForSite(maxSite), 0); + EXPECT_EQ(target.vertexForSite(nextSite), 1); + EXPECT_EQ(target.couplings(), + (llvm::ArrayRef{{nextSite, maxSite}})); + EXPECT_EQ(target.operations().front().siteTuples().front().sites(), + (llvm::ArrayRef{maxSite, nextSite})); +} + TEST(CompilerTargetTest, CanonicalizesConnectedTopologyAndCachesDistances) { std::vector sites{valid(Site::create(7)), valid(Site::create(2)), valid(Site::create(11))}; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index eefa9a35c7..77fa66c0c7 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -397,9 +397,9 @@ TEST_F(MappingPassFixture, MapTopologyOnlyWithEmptyOperationSet) { TEST_F(MappingPassFixture, KeepClassicallyDependentMeasurementBeforeRoutingSwaps) { - const auto target = llvm::cantFail(CompilerTarget::create( - 3, std::vector{{0, 1}, {1, 2}}, - std::vector{})); + const auto target = llvm::cantFail( + CompilerTarget::create(3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), + NativeOperations::fromOperations({}))); QCOProgramBuilder builder(context.get()); builder.initialize(); @@ -638,11 +638,24 @@ TEST_F(MappingPassFixture, KeepWorkspaceSparseOnLargeTarget) { TEST_F(MappingPassFixture, UnknownConnectivityIsNeededOnlyForTwoQubitOps) { QCOProgramBuilder builder(context.get()); builder.initialize(); - builder.sink(builder.allocQubit()); + auto qubit = builder.allocQubit(); + Value condition; + std::tie(qubit, condition) = builder.measure(qubit); + SmallVector qubits{qubit}; + qubits = builder.qcoIf( + condition, qubits, + [&](ValueRange args) { + return SmallVector{builder.x(args.front())}; + }, + [&](ValueRange args) { + return SmallVector{builder.h(args.front())}; + }); + builder.sink(qubits.front()); auto moduleOp = builder.finalize(); const auto target = llvm::cantFail(CompilerTarget::create(2)); EXPECT_TRUE(succeeded(runPass(moduleOp.get(), target, MappingPassOptions{}))); + EXPECT_TRUE(succeeded(verify(*moduleOp))); QCOProgramBuilder twoQubitBuilder(context.get()); twoQubitBuilder.initialize(); diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index 529e2a4b64..baa25a9319 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -552,10 +552,22 @@ def test_device_executes_qir_program(ddsim_device: Device) -> None: cx q[0], q[1]; c = measure q; """ + operations = [] + for operation in ddsim_device.operations(): + arity = operation.qubits_num() + if arity is None or arity == 0: + continue + operations.append( + CompilerTarget.Operation( + name=operation.name(), + arity=arity, + num_parameters=operation.parameters_num(), + ) + ) target = CompilerTarget( ddsim_device.qubits_num(), connectivity=CompilerTarget.Connectivity.all_to_all(), - native_operations=CompilerTarget.NativeOperations.unrestricted(), + native_operations=CompilerTarget.NativeOperations(operations), ) program = compile_program(qasm3_program, output=OutputFormat.QIR_BASE, target=target) assert ProgramFormat.QIR_BASE_STRING in ddsim_device.supported_program_formats() From cda61826c8e0bba67e9c96bea117fe62efd657a4 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 1 Sep 2026 11:23:48 +0000 Subject: [PATCH 08/20] =?UTF-8?q?=F0=9F=90=9B=20Describe=20DDSIM=20compile?= =?UTF-8?q?r=20target=20facts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QDMI 1.3 cannot enumerate the DDSIM all-to-all topology or homogeneous operation loci compactly. Advertise an exact custom marker and let the compiler adapter consume it without changing the fail-closed default for other devices. Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/generalize-compiler-target.md | 38 +++++++++++-- docs/qdmi/ddsim_device.md | 25 +------- mlir/lib/Compiler/QDMIAdapter.cpp | 57 +++++++++++++------ .../Compiler/test_compiler_qdmi_adapter.cpp | 16 +++--- src/qdmi/devices/dd/Device.cpp | 4 ++ test/python/qdmi/test_qdmi.py | 18 +----- test/qdmi/devices/dd/error_handling_test.cpp | 2 +- test/qdmi/test_client.cpp | 7 ++- 8 files changed, 98 insertions(+), 69 deletions(-) diff --git a/.agent/plans/generalize-compiler-target.md b/.agent/plans/generalize-compiler-target.md index 3106e8ed11..3afc91cc89 100644 --- a/.agent/plans/generalize-compiler-target.md +++ b/.agent/plans/generalize-compiler-target.md @@ -53,6 +53,15 @@ unrestricted, and an explicit list. - [x] (2026-09-01 08:41Z) Archived the pre-refresh heads, rebased onto `main` at `30bb9d1f8`, adapted the newly merged mapping regression test to the three-state target API, and reran the affected builds and tests. +- [x] (2026-09-01 11:23Z) Added an exact versioned DDSIM device marker for the + all-to-all topology and homogeneous fixed-arity operations that QDMI 1.3 + cannot encode compactly. Restored the direct `CompilerTarget.from_device` + documentation and end-to-end QIR test. +- [x] (2026-09-01 11:23Z) Diagnosed the Windows ARM failure as two parallel + `mqt-cc` tests sharing one scratch directory. Isolated the invalid-input + test and repeated both tests concurrently 100 times. +- [ ] Rebase this pull request onto the independent placement-pass change after + that change merges, then rerun final validation. ## Surprises & Discoveries @@ -80,6 +89,13 @@ unrestricted, and an explicit list. multi-site operation, including structured control flow. Evidence: such a program cannot change its layout, so branch reconciliation would only query topology unnecessarily. +- Observation: The mapping pass owns both static placement and topology-aware + routing. Skipping it for unknown connectivity leaves dynamic allocations that + the target-conformance pass rejects. Evidence: `VerifyTargetConformancePass` + rejects `qco.alloc` and `qtensor.alloc`. +- Observation: The Windows ARM failure was a test-data race, not a compiler + regression. Evidence: the QIR and invalid-input CTest scripts used the same + output directory while the QIR script removed that directory at startup. ## Decision Log @@ -110,14 +126,26 @@ unrestricted, and an explicit list. operation remains. Rationale: the layouts cannot diverge in that case, while all-to-all and explicit topologies retain their existing reconciliation. Date/Author: 2026-09-01, Lukas Burgholzer with Codex assistance. +- Decision: Supersede the call-site DDSIM workaround with an exact namespaced + marker in the bundled device's first custom property. Rationale: QDMI 1.3 + cannot enumerate the simulator's all-to-all topology or homogeneous operation + support compactly, while an exact marker lets only an explicit provider claim + those facts. Date/Author: 2026-09-01, Lukas Burgholzer with Codex assistance. +- Decision: Split topology-free placement from routing in an independent pull + request, then rebase this pull request onto it. Rationale: target compilation + must still assign static sites when topology is unknown, but the placement + stage does not need a coupling graph. The separate base keeps this pull + request and its history reviewable. Date/Author: 2026-09-01, Lukas Burgholzer + with Codex assistance. ## Outcomes & Retrospective -The context-free target contract is implemented in pull request #2218. The -compiler, mapping, synthesis, and focused Python suites pass. Generated stubs -are current. Documentation, changed-file C++ lint, full repository lint, and -final diff checks pass. Durable archive branches preserve both published heads -from before the rescope and from before the latest `main` refresh. +The context-free target contract is implemented in pull request #2218. DDSIM now +exposes enough exact metadata for `CompilerTarget.from_device` under QDMI 1.3, +and the direct path compiles and executes QIR. The compiler and focused Python +tests pass. The placement and routing split remains in an independent +prerequisite pull request. Durable archive branches preserve both published +heads from before the rescope and from before the latest `main` refresh. ## Context and Orientation diff --git a/docs/qdmi/ddsim_device.md b/docs/qdmi/ddsim_device.md index eb056c3c19..2817165a97 100644 --- a/docs/qdmi/ddsim_device.md +++ b/docs/qdmi/ddsim_device.md @@ -43,10 +43,8 @@ The device implements the full QDMI job interface (except for the ## Compile and execute QIR -QDMI v1.3 cannot compactly report the DDSIM device's all-to-all topology or the -site applicability of every operation. State the topology and reconstruct the -fixed-arity operation list from the device metadata when compiling a program to -QIR. Submit the resulting bitcode to the same device: +The compiler can snapshot the DDSIM device as an all-to-all target, compile a +program to QIR, and submit the resulting bitcode to the same device: ```python from mqt.core.mlir import CompilerTarget, OutputFormat, compile_program @@ -54,24 +52,7 @@ from mqt.core.qdmi import ProgramFormat from mqt.core.qdmi.driver import open_device device = open_device("mqt.ddsim.default") -operations = [] -for operation in device.operations(): - arity = operation.qubits_num() - if arity is None or arity == 0: - continue - operations.append( - CompilerTarget.Operation( - name=operation.name(), - arity=arity, - num_parameters=operation.parameters_num(), - ) - ) - -target = CompilerTarget( - device.qubits_num(), - connectivity=CompilerTarget.Connectivity.all_to_all(), - native_operations=CompilerTarget.NativeOperations(operations), -) +target = CompilerTarget.from_device(device) program = compile_program( "bell.qasm", target=target, diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index 0885b4f146..61f9d5f180 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,20 @@ namespace mlir { +/// Target facts that QDMI v1.3 cannot encode compactly. +constexpr std::string_view ALL_TO_ALL_HOMOGENEOUS_METADATA = + "mqt.compiler-target.v1:all-to-all-homogeneous"; + +[[nodiscard]] static bool +hasAllToAllHomogeneousMetadata(const qdmi::Device& device) { + const auto metadata = device.queryCustomProperty>( + qdmi::CustomProperty::Custom1); + const auto expected = + std::as_bytes(std::span{ALL_TO_ALL_HOMOGENEOUS_METADATA.data(), + ALL_TO_ALL_HOMOGENEOUS_METADATA.size() + 1}); + return metadata && std::ranges::equal(*metadata, expected); +} + [[nodiscard]] static llvm::Error requireAdapterInput(const bool condition, const llvm::Twine& message) { if (!condition) { @@ -272,7 +287,7 @@ snapshotOperations( const std::vector& operations, const std::vector& deviceSites, const std::optional>& couplings, - const llvm::StringRef deviceName) { + const llvm::StringRef deviceName, const bool homogeneousOperationSupport) { std::vector targetOperations; targetOperations.reserve(operations.size()); for (const auto& operation : operations) { @@ -286,24 +301,28 @@ snapshotOperations( continue; } const auto flattenedSites = operation.getSites(); - if (!flattenedSites) { + if (!flattenedSites && !homogeneousOperationSupport) { return CompilerTarget::NativeOperations{}; } - if (auto error = - validateHomogeneousSupport(operation, *arity, *flattenedSites, - deviceSites, couplings, deviceName)) { - return error; - } const auto duration = operation.getDuration(); const auto fidelity = operation.getFidelity(); - auto siteTuples = snapshotSiteTuples(operation, *arity, *flattenedSites, - duration, fidelity); - if (!siteTuples) { - return siteTuples.takeError(); + std::vector siteTuples; + if (flattenedSites) { + if (auto error = + validateHomogeneousSupport(operation, *arity, *flattenedSites, + deviceSites, couplings, deviceName)) { + return error; + } + auto tuples = snapshotSiteTuples(operation, *arity, *flattenedSites, + duration, fidelity); + if (!tuples) { + return tuples.takeError(); + } + siteTuples = std::move(*tuples); } auto targetOperation = CompilerTarget::Operation::create( operation.getName(), *arity, operation.getParametersNum(), - std::move(*siteTuples), duration, fidelity); + std::move(siteTuples), duration, fidelity); if (!targetOperation) { return targetOperation.takeError(); } @@ -315,6 +334,8 @@ snapshotOperations( [[nodiscard]] static llvm::Expected snapshotCompilerTarget(const qdmi::Device& device) { auto deviceName = device.getName(); + const auto hasHomogeneousAllToAllMetadata = + hasAllToAllHomogeneousMetadata(device); const auto deviceSites = device.getSites(); if (auto error = requireCircuitDevice( std::ranges::none_of(deviceSites, @@ -361,7 +382,8 @@ snapshotCompilerTarget(const qdmi::Device& device) { } auto operations = - snapshotOperations(device.getOperations(), sites, couplings, deviceName); + snapshotOperations(device.getOperations(), sites, couplings, deviceName, + hasHomogeneousAllToAllMetadata); if (!operations) { return operations.takeError(); } @@ -369,9 +391,12 @@ snapshotCompilerTarget(const qdmi::Device& device) { if (!durationUnit) { return durationUnit.takeError(); } - auto connectivity = - couplings ? CompilerTarget::Connectivity::fromCouplings(*couplings) - : CompilerTarget::Connectivity{}; + CompilerTarget::Connectivity connectivity; + if (couplings) { + connectivity = CompilerTarget::Connectivity::fromCouplings(*couplings); + } else if (hasHomogeneousAllToAllMetadata) { + connectivity = CompilerTarget::Connectivity::allToAll(); + } return CompilerTarget::create(std::move(deviceName), std::move(sites), std::move(connectivity), std::move(*operations), std::move(*durationUnit)); diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 8108ea191e..8a30417f42 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -20,7 +20,6 @@ #include #include -#include #include using mlir::CompilerTarget; @@ -82,18 +81,21 @@ TEST(CompilerQDMIAdapterTest, SnapshotsIQMCalibrationAndLifetime) { EXPECT_EQ(target.synthesisBasis()->entangler, CompilerTarget::GateKind::CZ); } -TEST(CompilerQDMIAdapterTest, PreservesMissingTargetFactsAsUnknown) { +TEST(CompilerQDMIAdapterTest, InfersDDSIMTargetFacts) { const auto device = qdmi::Session::openDevice("mqt.ddsim.default"); const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); EXPECT_EQ(target.numSites(), 65535); EXPECT_EQ(target.connectivityKind(), - CompilerTarget::Connectivity::Kind::Unknown); + CompilerTarget::Connectivity::Kind::AllToAll); EXPECT_EQ(target.nativeOperationsKind(), - CompilerTarget::NativeOperations::Kind::Unknown); - EXPECT_EQ(target.supportsOperation("h", 1, 0), std::nullopt); - EXPECT_EQ(target.supportsOperation("cx", 2, 0), std::nullopt); - EXPECT_EQ(target.supportsOperation("measure", 1, 0), std::nullopt); + CompilerTarget::NativeOperations::Kind::Explicit); + EXPECT_EQ(target.supportsOperation("h", 1, 0), true); + EXPECT_EQ(target.supportsOperation("cx", 2, 0), true); + EXPECT_EQ(target.supportsOperation("cswap", 3, 0), true); + EXPECT_EQ(target.supportsOperation("measure", 1, 0), true); + EXPECT_EQ(target.supportsOperation("reset", 1, 0), true); + EXPECT_EQ(target.supportsOperation("barrier", 0, 0), false); } TEST(CompilerQDMIAdapterTest, ListsRegisteredDeviceIds) { diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index ea43440489..2491d4d511 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -223,6 +223,10 @@ auto Device::queryProperty(const QDMI_Device_Property prop, const size_t size, prop, size, value, sizeRet) ADD_LIST_PROPERTY(QDMI_DEVICE_PROPERTY_OPERATIONS, MQT_DDSIM_QDMI_Operation, OPERATION_ADDRESSES, prop, size, value, sizeRet) + /// Advertise compiler-target facts that QDMI v1.3 cannot encode compactly. + ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_CUSTOM1, + "mqt.compiler-target.v1:all-to-all-homogeneous", prop, + size, value, sizeRet) ADD_LIST_PROPERTY(QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS, QDMI_Program_Format, SUPPORTED_PROGRAM_FORMATS, prop, size, value, sizeRet) diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index baa25a9319..5ac24a7275 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -552,23 +552,7 @@ def test_device_executes_qir_program(ddsim_device: Device) -> None: cx q[0], q[1]; c = measure q; """ - operations = [] - for operation in ddsim_device.operations(): - arity = operation.qubits_num() - if arity is None or arity == 0: - continue - operations.append( - CompilerTarget.Operation( - name=operation.name(), - arity=arity, - num_parameters=operation.parameters_num(), - ) - ) - target = CompilerTarget( - ddsim_device.qubits_num(), - connectivity=CompilerTarget.Connectivity.all_to_all(), - native_operations=CompilerTarget.NativeOperations(operations), - ) + target = CompilerTarget.from_device(ddsim_device) program = compile_program(qasm3_program, output=OutputFormat.QIR_BASE, target=target) assert ProgramFormat.QIR_BASE_STRING in ddsim_device.supported_program_formats() diff --git a/test/qdmi/devices/dd/error_handling_test.cpp b/test/qdmi/devices/dd/error_handling_test.cpp index 17e3d03886..d3627ffdc5 100644 --- a/test/qdmi/devices/dd/error_handling_test.cpp +++ b/test/qdmi/devices/dd/error_handling_test.cpp @@ -161,7 +161,7 @@ TEST_F(ErrorHandling, CustomEnums) { EXPECT_EQ(MQT_DDSIM_QDMI_device_session_query_device_property( s.session, QDMI_DEVICE_PROPERTY_CUSTOM1, 0, nullptr, nullptr), - QDMI_ERROR_NOTSUPPORTED); + QDMI_SUCCESS); EXPECT_EQ(MQT_DDSIM_QDMI_device_session_query_device_property( s.session, QDMI_DEVICE_PROPERTY_CUSTOM2, 0, nullptr, nullptr), QDMI_ERROR_NOTSUPPORTED); diff --git a/test/qdmi/test_client.cpp b/test/qdmi/test_client.cpp index d388c0d2a1..65b4f82a42 100644 --- a/test/qdmi/test_client.cpp +++ b/test/qdmi/test_client.cpp @@ -527,10 +527,15 @@ TEST_P(DeviceTest, ChildDevices) { TEST_P(DeviceTest, UnsupportedCustomPropertyReturnsNullopt) { EXPECT_EQ(device.queryCustomProperty>( - CustomProperty::Custom1), + CustomProperty::Custom2), std::nullopt); } +TEST_F(DDSimulatorDeviceTest, ReportsCompilerTargetMetadata) { + EXPECT_EQ(device.queryCustomProperty(CustomProperty::Custom1), + "mqt.compiler-target.v1:all-to-all-homogeneous"); +} + TEST_P(SiteTest, Index) { for (const auto& site : sites) { EXPECT_NO_THROW(std::ignore = site.getIndex()); From 5ed0f5c983c458072cc21eb09cf609f72933300f Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 1 Sep 2026 11:24:07 +0000 Subject: [PATCH 09/20] =?UTF-8?q?=F0=9F=A7=AA=20Isolate=20mqt-cc=20test=20?= =?UTF-8?q?scratch=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parallel Windows CTest workers could delete each other's inputs because the QIR and invalid-input scripts shared one output directory. Give the invalid-input test its own scratch path. Assisted-by: GPT-5.6 Sol via Codex --- mlir/unittests/Compiler/mqt-cc/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/unittests/Compiler/mqt-cc/CMakeLists.txt b/mlir/unittests/Compiler/mqt-cc/CMakeLists.txt index 6777382400..6e5552e541 100644 --- a/mlir/unittests/Compiler/mqt-cc/CMakeLists.txt +++ b/mlir/unittests/Compiler/mqt-cc/CMakeLists.txt @@ -19,5 +19,5 @@ add_test( COMMAND ${CMAKE_COMMAND} "-DMQT_CC=$" "-DNONLINEAR_QCO_INPUT=${CMAKE_CURRENT_SOURCE_DIR}/nonlinear.qco.mlir" - "-DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/output-$" -P + "-DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/invalid-output-$" -P "${CMAKE_CURRENT_SOURCE_DIR}/verify_invalid_mlir.cmake") From 56ecd6212e8c11215fd75d11ba9e6b8c589757e4 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 1 Sep 2026 12:02:36 +0000 Subject: [PATCH 10/20] =?UTF-8?q?=F0=9F=A7=AA=20Test=20an=20unused=20custo?= =?UTF-8?q?m=20property=20slot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the generic Python overload checks independent of DDSIM metadata now that CUSTOM1 carries compiler-target facts. Assisted-by: GPT-5.6 Sol via Codex --- test/python/qdmi/test_qdmi.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index 5ac24a7275..337bb504d8 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -220,16 +220,16 @@ def test_device_min_atom_distance(device: Device) -> None: @pytest.mark.parametrize("value_type", [str, bool, int, float, bytes]) def test_device_custom_property_unsupported(device: Device, value_type: CustomValueType) -> None: """Test typed custom device queries for unsupported slots.""" - assert device.query_custom_property(CustomProperty.CUSTOM1, value_type) is None + assert device.query_custom_property(CustomProperty.CUSTOM2, value_type) is None def test_device_custom_property_type_overloads(device: Device) -> None: """Test that each explicit value type produces a correspondingly typed result.""" - string_value: str | None = device.query_custom_property(CustomProperty.CUSTOM1, str) - bool_value: bool | None = device.query_custom_property(CustomProperty.CUSTOM1, bool) - int_value: int | None = device.query_custom_property(CustomProperty.CUSTOM1, int) - float_value: float | None = device.query_custom_property(CustomProperty.CUSTOM1, float) - bytes_value: bytes | None = device.query_custom_property(CustomProperty.CUSTOM1, bytes) + string_value: str | None = device.query_custom_property(CustomProperty.CUSTOM2, str) + bool_value: bool | None = device.query_custom_property(CustomProperty.CUSTOM2, bool) + int_value: int | None = device.query_custom_property(CustomProperty.CUSTOM2, int) + float_value: float | None = device.query_custom_property(CustomProperty.CUSTOM2, float) + bytes_value: bytes | None = device.query_custom_property(CustomProperty.CUSTOM2, bytes) assert all(value is None for value in (string_value, bool_value, int_value, float_value, bytes_value)) From 3f7171347a1c8b174e29fc3d836562f74a1b792c Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 1 Sep 2026 15:08:36 +0000 Subject: [PATCH 11/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Integrate=20topology?= =?UTF-8?q?-free=20placement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dispatch explicit connectivity to mapping and use deterministic placement for all-to-all or safe unknown-connectivity programs. Reject unknown multi-site placement before mutation. Keep the QDMI 1.3 DDSIM metadata marker explicitly temporary until QDMI standardizes the equivalent facts. Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/generalize-compiler-target.md | 72 ++++++++------- docs/mlir/target_compilation.md | 3 +- .../Dialect/QCO/Transforms/Mapping/Mapping.h | 2 + mlir/lib/Compiler/QDMIAdapter.cpp | 2 + mlir/lib/Compiler/TargetCompilation.cpp | 8 +- .../QCO/Transforms/Mapping/Mapping.cpp | 87 +++++++++++-------- .../Compiler/test_compiler_pipeline.cpp | 4 +- .../QCO/Transforms/Mapping/test_mapping.cpp | 40 +++++---- src/qdmi/devices/dd/Device.cpp | 4 +- 9 files changed, 135 insertions(+), 87 deletions(-) diff --git a/.agent/plans/generalize-compiler-target.md b/.agent/plans/generalize-compiler-target.md index 3afc91cc89..d68149ba78 100644 --- a/.agent/plans/generalize-compiler-target.md +++ b/.agent/plans/generalize-compiler-target.md @@ -60,8 +60,13 @@ unrestricted, and an explicit list. - [x] (2026-09-01 11:23Z) Diagnosed the Windows ARM failure as two parallel `mqt-cc` tests sharing one scratch directory. Isolated the invalid-input test and repeated both tests concurrently 100 times. -- [ ] Rebase this pull request onto the independent placement-pass change after - that change merges, then rerun final validation. +- [x] (2026-09-01 14:58Z) Archived the pre-integration head and rebased onto the + merged placement pass. Target compilation now maps explicit connectivity, + places all-to-all targets, and places unknown connectivity only when no + non-barrier multi-site operation remains. +- [x] (2026-09-01 15:07Z) Ran the final native, Python, documentation, stub, + lint, and C++ lint validation and prepared the signed rebased head for + publication. ## Surprises & Discoveries @@ -86,13 +91,12 @@ unrestricted, and an explicit list. values, while `CompilerTarget::SiteId` intentionally accepts every nonnegative `int64_t` value. - Observation: Unknown connectivity is sufficient for a program containing no - multi-site operation, including structured control flow. Evidence: such a - program cannot change its layout, so branch reconciliation would only query - topology unnecessarily. -- Observation: The mapping pass owns both static placement and topology-aware - routing. Skipping it for unknown connectivity leaves dynamic allocations that - the target-conformance pass rejects. Evidence: `VerifyTargetConformancePass` - rejects `qco.alloc` and `qtensor.alloc`. + non-barrier multi-site operation, including structured control flow. Evidence: + such a program cannot change its layout, so branch reconciliation would only + query topology unnecessarily. +- Observation: Placement does not need a coupling graph. Evidence: the merged + placement pass replaces dynamic allocations with target sites without using + routing data, while the mapping pass requires explicit connectivity. - Observation: The Windows ARM failure was a test-data race, not a compiler regression. Evidence: the QIR and invalid-input CTest scripts used the same output directory while the QIR script removed that directory at startup. @@ -113,39 +117,37 @@ unrestricted, and an explicit list. needs that fact. Rationale: program requirements are stage-relative; a classical or single-site program does not need native-operation or topology claims. Date/Author: 2026-08-23, Codex. -- Decision: Keep the DDSIM snapshot unknown and state its known simulator facts - at the compiler call site. Rationale: interpreting unavailable QDMI v1.3 - properties as unrestricted would weaken the target contract for every - provider. Date/Author: 2026-08-23, Codex. - Decision: Use sentinel-free standard containers for site IDs. Rationale: this preserves the documented public domain instead of introducing an arbitrary range restriction to accommodate an implementation detail. Date/Author: 2026-09-01, Lukas Burgholzer with Codex assistance. -- Decision: Skip structured-control-flow topology reconciliation only for - unknown connectivity after the pass has established that no multi-site - operation remains. Rationale: the layouts cannot diverge in that case, while - all-to-all and explicit topologies retain their existing reconciliation. - Date/Author: 2026-09-01, Lukas Burgholzer with Codex assistance. +- Decision: Route explicit connectivity through the mapping pass and route + all-to-all or unknown connectivity through the placement pass. The placement + pass rejects unknown connectivity before mutation when a non-barrier + multi-site operation remains. Rationale: placement needs only target sites, + while routing needs a coupling graph. Keeping the guard in the placement pass + also protects direct pass users. Date/Author: 2026-09-01, Lukas Burgholzer + with Codex assistance. - Decision: Supersede the call-site DDSIM workaround with an exact namespaced marker in the bundled device's first custom property. Rationale: QDMI 1.3 cannot enumerate the simulator's all-to-all topology or homogeneous operation support compactly, while an exact marker lets only an explicit provider claim those facts. Date/Author: 2026-09-01, Lukas Burgholzer with Codex assistance. -- Decision: Split topology-free placement from routing in an independent pull - request, then rebase this pull request onto it. Rationale: target compilation - must still assign static sites when topology is unknown, but the placement - stage does not need a coupling graph. The separate base keeps this pull - request and its history reviewable. Date/Author: 2026-09-01, Lukas Burgholzer - with Codex assistance. +- Decision: Build on the independently merged topology-free placement pass. + Rationale: target compilation must still assign static sites when topology is + unknown, but the placement stage does not need a coupling graph. The separate + prerequisite kept each change reviewable. Date/Author: 2026-09-01, Lukas + Burgholzer with Codex assistance. ## Outcomes & Retrospective The context-free target contract is implemented in pull request #2218. DDSIM now exposes enough exact metadata for `CompilerTarget.from_device` under QDMI 1.3, and the direct path compiles and executes QIR. The compiler and focused Python -tests pass. The placement and routing split remains in an independent -prerequisite pull request. Durable archive branches preserve both published -heads from before the rescope and from before the latest `main` refresh. +tests pass. Target compilation uses the merged placement pass for all-to-all and +safe unknown-connectivity programs, while explicit coupling graphs use the +mapping pass. Durable archive branches preserve the published heads from before +the rescope, `main` refresh, and placement integration. ## Context and Orientation @@ -172,6 +174,11 @@ to unknown. Rename target `numQubits()` to `numSites()` and operation bindings, and tests to use the new vocabulary and to handle unknown facts before querying routes or operation support. +Use deterministic placement for all-to-all connectivity and for unknown +connectivity when no non-barrier multi-site operation remains. Use the mapping +pass only for an explicit coupling graph. Validate unknown connectivity in the +placement pass before it changes the program. + Keep site identifiers, ordered operation site tuples, timing units, T1/T2 data, and fidelity values unchanged. Add no technology enum and no generic property container. Add the pull request reference to the existing general Compiler @@ -195,8 +202,11 @@ targets when those sources change. All commands are repeatable. Target tests must prove that unknown topology is distinct from all-to-all and explicit topology, and that unknown native operations are distinct from all and an explicit list. Existing explicit target mapping and synthesis tests must -still pass. The build must contain no old public `numQubits()` or operation -qubit-count references. `uvx nox -s lint` and `git diff --check` must pass. +still pass. Placement must accept barriers and single-site operations with +unknown connectivity, reject a multi-site unitary without changing the input, +and place all-to-all programs compactly. The build must contain no old public +`numQubits()` or operation qubit-count references. `uvx nox -s lint` and +`git diff --check` must pass. ## Idempotence and Recovery @@ -217,3 +227,7 @@ Use LLVM containers already linked by the compiler. Do not add dependencies. The public target keeps shared immutable storage. Connectivity and native operation state are context-free C++ values so the later MLIR attribute layer can materialize them without making `CompilerTarget` depend on an MLIR context. + +Plan revision note (2026-09-01): Updated the plan after the independent +placement pass merged. The final design delegates topology-free work to that +pass and keeps routing confined to explicit connectivity. diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 4d01a7375f..1b0adcf777 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -60,7 +60,8 @@ benchmarking, the C++ API exposes separate factories for pre-routing optimization, deterministic placement, topology-aware mapping, native synthesis, and conformance verification. Target compilation uses compact placement on all-to-all targets and the mapper only when the target has an explicit coupling -graph. +graph. A target with unknown connectivity can use compact placement only when no +non-barrier multi-site operation remains. Target compilation preserves quantum operations even when their final qubit values are not measured or returned. This supports measurement-free programs, diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h index 6ef068926a..942c67bfbb 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h @@ -23,6 +23,8 @@ class CompilerTarget; namespace qco { /// Create a deterministic placement pass for a compiler target. +/// Unknown connectivity requires a program without non-barrier multi-site +/// operations. std::unique_ptr createPlacementPass(const CompilerTarget& target); /// Create a mapping pass for a compiler target with explicit topology. diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index 61f9d5f180..05d15a4d40 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -38,6 +38,8 @@ namespace mlir { /// Target facts that QDMI v1.3 cannot encode compactly. +/// TODO(#2093): Remove this compatibility marker when QDMI standardizes +/// explicit unrestricted connectivity and operation applicability. constexpr std::string_view ALL_TO_ALL_HOMOGENEOUS_METADATA = "mqt.compiler-target.v1:all-to-all-homogeneous"; diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index 617383db11..bb329a1b2e 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -26,10 +26,14 @@ void populateTargetCompilationPipeline(OpPassManager& pm, populateDecomposeMultiControlledPipeline(pm, 3); populateDefaultQCOOptimizationPipeline(pm); pm.addPass(qco::createFuseTwoQubitGates()); - if (target.hasExplicitTopology()) { + switch (target.connectivityKind()) { + case CompilerTarget::Connectivity::Kind::Explicit: pm.addPass(qco::createMappingPass(target, qco::MappingPassOptions{})); - } else { + break; + case CompilerTarget::Connectivity::Kind::AllToAll: + case CompilerTarget::Connectivity::Kind::Unknown: pm.addPass(qco::createPlacementPass(target)); + break; } populateQCOCleanupPipeline(pm); pm.addPass(qco::createTargetNativeSynthesis(target)); diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 00f279e454..1e20b24da8 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -240,7 +240,7 @@ static LogicalResult checkCapacity(func::FuncOp func, return success(); } return func.emitError() << "requires " << computation.wires.size() - << " qubits, but the target supports " + << " program qubits, but the target site count is " << target.numSites(); } @@ -339,6 +339,23 @@ struct PlacementPass final return; } + if (target.connectivityKind() == + CompilerTarget::Connectivity::Kind::Unknown) { + const auto result = func.walk([](UnitaryOpInterface unitary) { + if (isa(unitary) || unitary.getNumQubits() <= 1) { + return WalkResult::advance(); + } + unitary.emitError() << "target placement requires known connectivity " + "for an operation with arity " + << unitary.getNumQubits(); + return WalkResult::interrupt(); + }); + if (result.wasInterrupted()) { + signalPassFailure(); + return; + } + } + auto computation = discoverComputation(func); if (failed(computation) || failed(checkCapacity(func, target, *computation))) { @@ -1517,42 +1534,38 @@ struct MappingPass : impl::MappingPassBase { // using the restore (scf::ForOp, scf::While), converge (IfOp), and vote // and restore (IndexSwitchOp) strategies. - Layout exit = parent.layout; - if (target->connectivityKind() != - CompilerTarget::Connectivity::Kind::Unknown) { - exit = - TypeSwitch(op) - .Case([&](scf::ForOp) { - const auto swaps = restore(children[0].layout, parent.layout); - insertSWAPs(swaps, children[0], totalStats, rewriter); - return parent.layout; - }) - .template Case([&](scf::WhileOp) { - const auto swaps = restore(children[1].layout, parent.layout); - insertSWAPs(swaps, children[1], totalStats, rewriter); - // The scf::YieldOp is the terminator in the before region and - // thus determines the final output layout. - return children[0].layout; - }) - .template Case([&](IfOp) { - const auto [convergedLayout, fst, snd] = - converge(children[0].layout, children[1].layout); - insertSWAPs(fst, children[0], totalStats, rewriter); - insertSWAPs(snd, children[1], totalStats, rewriter); - return convergedLayout; - }) - .template Case([&](IndexSwitchOp) { - auto compromise = driveby(map_range( - children, [](const RoutingBundle& b) -> const Layout& { - return b.layout; - })); - for (RoutingBundle& child : children) { - const auto swaps = restore(child.layout, compromise); - insertSWAPs(swaps, child, totalStats, rewriter); - } - return compromise; - }); - } + Layout exit = + TypeSwitch(op) + .Case([&](scf::ForOp) { + const auto swaps = restore(children[0].layout, parent.layout); + insertSWAPs(swaps, children[0], totalStats, rewriter); + return parent.layout; + }) + .template Case([&](scf::WhileOp) { + const auto swaps = restore(children[1].layout, parent.layout); + insertSWAPs(swaps, children[1], totalStats, rewriter); + // The scf::YieldOp is the terminator in the before region and + // thus determines the final output layout. + return children[0].layout; + }) + .template Case([&](IfOp) { + const auto [convergedLayout, fst, snd] = + converge(children[0].layout, children[1].layout); + insertSWAPs(fst, children[0], totalStats, rewriter); + insertSWAPs(snd, children[1], totalStats, rewriter); + return convergedLayout; + }) + .template Case([&](IndexSwitchOp) { + auto compromise = driveby(map_range( + children, [](const RoutingBundle& b) -> const Layout& { + return b.layout; + })); + for (RoutingBundle& child : children) { + const auto swaps = restore(child.layout, compromise); + insertSWAPs(swaps, child, totalStats, rewriter); + } + return compromise; + }); if constexpr (Mode == RoutingMode::Hot) { // Realign terminator values to ensure that i-th input qubit and the diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 3edd16fe08..a74cc54fe1 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -1625,7 +1625,9 @@ h q[0]; reset q[0]; h q[1]; )"; - const auto target = llvm::cantFail(CompilerTarget::create(3)); + const auto target = llvm::cantFail( + CompilerTarget::create(3, CompilerTarget::Connectivity{}, + CompilerTarget::NativeOperations::unrestricted())); auto qc = QCProgram::fromQASMString(source); ASSERT_TRUE(qc); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 77fa66c0c7..d94492b724 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -513,7 +513,8 @@ TEST_F(MappingPassFixture, PlaceTensorOnFirstTargetSites) { llvm::cantFail(CompilerTarget::Site::create(19)), llvm::cantFail(CompilerTarget::Site::create(42)), llvm::cantFail(CompilerTarget::Site::create(81))}; - const auto target = llvm::cantFail(CompilerTarget::create(std::move(sites))); + const auto target = llvm::cantFail(CompilerTarget::create( + std::move(sites), CompilerTarget::Connectivity::allToAll())); QCOProgramBuilder builder(context.get()); builder.initialize({builder.getI1Type(), builder.getI1Type()}); @@ -591,8 +592,10 @@ TEST_F(MappingPassFixture, RejectOversizedPlacementBeforeMutation) { }); EXPECT_TRUE(failed(runPlacement(moduleOp.get(), target))); EXPECT_EQ(printModule(moduleOp.get()), before); - EXPECT_TRUE(StringRef(diagnostics) - .contains("requires 2 qubits, but the target supports 1")); + EXPECT_TRUE( + StringRef(diagnostics) + .contains( + "requires 2 program qubits, but the target site count is 1")); } TEST_F(MappingPassFixture, KeepWorkspaceSparseOnLargeTarget) { @@ -635,26 +638,29 @@ TEST_F(MappingPassFixture, KeepWorkspaceSparseOnLargeTarget) { EXPECT_EQ(numSinks, numStatics); } -TEST_F(MappingPassFixture, UnknownConnectivityIsNeededOnlyForTwoQubitOps) { +TEST_F(MappingPassFixture, + UnknownConnectivityRejectsMultiSiteUnitaryBeforeMutation) { QCOProgramBuilder builder(context.get()); builder.initialize(); - auto qubit = builder.allocQubit(); + SmallVector qubits{builder.allocQubit(), builder.allocQubit()}; + qubits = builder.barrier(qubits); Value condition; - std::tie(qubit, condition) = builder.measure(qubit); - SmallVector qubits{qubit}; - qubits = builder.qcoIf( - condition, qubits, + std::tie(qubits[0], condition) = builder.measure(qubits[0]); + SmallVector controlled{qubits[0]}; + controlled = builder.qcoIf( + condition, controlled, [&](ValueRange args) { return SmallVector{builder.x(args.front())}; }, [&](ValueRange args) { return SmallVector{builder.h(args.front())}; }); - builder.sink(qubits.front()); + builder.sink(controlled.front()); + builder.sink(qubits[1]); auto moduleOp = builder.finalize(); const auto target = llvm::cantFail(CompilerTarget::create(2)); - EXPECT_TRUE(succeeded(runPass(moduleOp.get(), target, MappingPassOptions{}))); + EXPECT_TRUE(succeeded(runPlacement(moduleOp.get(), target))); EXPECT_TRUE(succeeded(verify(*moduleOp))); QCOProgramBuilder twoQubitBuilder(context.get()); @@ -665,17 +671,19 @@ TEST_F(MappingPassFixture, UnknownConnectivityIsNeededOnlyForTwoQubitOps) { twoQubitBuilder.sink(first); twoQubitBuilder.sink(second); auto twoQubitModule = twoQubitBuilder.finalize(); + const auto before = printModule(twoQubitModule.get()); std::string diagnostics; ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { diagnostics += diagnostic.str(); return success(); }); - EXPECT_TRUE( - failed(runPass(twoQubitModule.get(), target, MappingPassOptions{}))); - EXPECT_TRUE( - StringRef(diagnostics) - .contains("place-and-route requires known target connectivity")); + EXPECT_TRUE(failed(runPlacement(twoQubitModule.get(), target))); + EXPECT_EQ(printModule(twoQubitModule.get()), before); + EXPECT_TRUE(StringRef(diagnostics) + .contains("target placement requires known connectivity for " + "an operation with " + "arity 2")); } TEST_P(MappingPassTest, FailNoEntryPoint) { diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 2491d4d511..da617e5033 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -223,7 +223,9 @@ auto Device::queryProperty(const QDMI_Device_Property prop, const size_t size, prop, size, value, sizeRet) ADD_LIST_PROPERTY(QDMI_DEVICE_PROPERTY_OPERATIONS, MQT_DDSIM_QDMI_Operation, OPERATION_ADDRESSES, prop, size, value, sizeRet) - /// Advertise compiler-target facts that QDMI v1.3 cannot encode compactly. + /// Target facts that QDMI v1.3 cannot encode compactly. + /// TODO(#2093): Remove this compatibility marker when QDMI standardizes + /// explicit unrestricted connectivity and operation applicability. ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_CUSTOM1, "mqt.compiler-target.v1:all-to-all-homogeneous", prop, size, value, sizeRet) From ef17752b7f040749e58105ee5bf705c12c500e73 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 1 Sep 2026 18:52:30 +0000 Subject: [PATCH 12/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Complete=20compiler?= =?UTF-8?q?=20target=20capabilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Require complete topology and native-operation facts at target construction instead of propagating unusable unknown states. Model global phase as fixed zero arity and simulator gate families as positive variadic capabilities. DDSIM reports arbitrary controls through one exact QDMI 1.3 compatibility marker, so compiler support covers every canonical base gate without enumerating controlled aliases. Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/generalize-compiler-target.md | 174 +++++++----- bindings/mlir/register_mlir.cpp | 100 ++++--- bindings/patterns.txt | 29 +- docs/mlir/target_compilation.md | 38 ++- mlir/include/mlir/Compiler/Target.h | 112 +++++--- .../Dialect/QCO/Transforms/Mapping/Mapping.h | 2 - mlir/lib/Compiler/QDMIAdapter.cpp | 202 ++++++++++---- mlir/lib/Compiler/Target.cpp | 261 +++++++++++------- mlir/lib/Compiler/TargetCompilation.cpp | 1 - .../QCO/Transforms/Mapping/Mapping.cpp | 17 -- .../NativeSynthesis/TargetSynthesis.cpp | 57 ++-- mlir/unittests/Compiler/CMakeLists.txt | 1 + .../Compiler/Inputs/higher-arity-sc.json | 32 +++ .../Compiler/test_compiler_pipeline.cpp | 10 +- .../Compiler/test_compiler_qdmi_adapter.cpp | 38 ++- .../Compiler/test_compiler_target.cpp | 242 +++++++++++++--- .../QCO/Transforms/Mapping/test_mapping.cpp | 68 +---- .../NativeSynthesis/test_target_synthesis.cpp | 159 +++++++---- python/mqt/core/mlir.pyi | 105 ++++--- src/qdmi/devices/dd/Device.cpp | 78 ++++-- test/python/test_mlir.py | 53 +++- test/python/test_mlir_qiskit_translation.py | 10 +- .../devices/dd/device_properties_test.cpp | 63 +++++ 23 files changed, 1260 insertions(+), 592 deletions(-) create mode 100644 mlir/unittests/Compiler/Inputs/higher-arity-sc.json diff --git a/.agent/plans/generalize-compiler-target.md b/.agent/plans/generalize-compiler-target.md index d68149ba78..d468caac18 100644 --- a/.agent/plans/generalize-compiler-target.md +++ b/.agent/plans/generalize-compiler-target.md @@ -13,9 +13,13 @@ The compiler target currently treats missing topology as all-to-all connectivity, missing native operations as unrestricted support, and names every quantum resource a qubit. After this change, target descriptions can represent neutral atoms, trapped ions, photonic modes, spin qubits, and other site-based -systems without claiming facts that a provider did not report. A focused -compiler target test demonstrates the three knowledge states: unknown, -unrestricted, and an explicit list. +systems without silently inventing facts. Every compiler target is complete: +connectivity is all-to-all or explicit, and native-operation support is +unrestricted or explicit. Incomplete QDMI metadata fails during target +inference, before a compiler pass runs. Operation capabilities also distinguish +fixed arity from a variadic total width with a minimum, so simulator targets can +retain zero-site global phases and arbitrary controlled forms of their standard +gates. ## Progress @@ -67,17 +71,28 @@ unrestricted, and an explicit list. - [x] (2026-09-01 15:07Z) Ran the final native, Python, documentation, stub, lint, and C++ lint validation and prepared the signed rebased head for publication. +- [x] (2026-09-01 15:39Z) Removed the intermediate unknown target states, + required complete connectivity and native-operation facts at construction, + and moved incomplete QDMI metadata failures to target inference. +- [x] (2026-09-01 15:56Z) Generalized homogeneous fixed-arity QDMI operation + validation beyond two sites so that the SV1 three-site operations remain + representable, while retaining format-level constructs outside the + compiler target. +- [x] (2026-09-01 18:44Z) Represented zero-site global phase and DDSIM's + arbitrary positive controls without enumerating controlled-gate aliases. +- [x] (2026-09-01 18:44Z) Taught target support checks and the QDMI adapter + about variadic operation widths, then updated bindings, documentation, and + focused tests. +- [x] (2026-09-01 18:44Z) Prepared the revised #2218 head and split typed target + serialization and target-aware controlled-operation decomposition into + direct dependent work without new archive branches. ## Surprises & Discoveries - Observation: The existing `std::optional>` parameters use - absence to mean unrestricted support, so they cannot represent unknown - metadata. Evidence: the class comment and `Storage::supportsOperation` in - `mlir/lib/Compiler/Target.cpp`. -- Observation: Tests must compare `std::optional` with `true`, `false`, or - `std::nullopt`; `EXPECT_TRUE` and `EXPECT_FALSE` inspect only whether the - optional has a value. Evidence: the first focused compiler test run exposed - this test-only error. + absence to mean unrestricted support, so omission cannot safely represent a + complete target fact. Evidence: the class comment and + `Storage::supportsOperation` in `mlir/lib/Compiler/Target.cpp`. - Observation: QDMI operation site applicability is optional. Treating an unavailable site list as global support promoted missing metadata to a native operation claim. Evidence: `QDMI_OPERATION_PROPERTY_SITES` defines the valid @@ -90,13 +105,21 @@ unrestricted, and an explicit list. domain. Evidence: `DenseMapInfo` reserves the two largest signed values, while `CompilerTarget::SiteId` intentionally accepts every nonnegative `int64_t` value. -- Observation: Unknown connectivity is sufficient for a program containing no - non-barrier multi-site operation, including structured control flow. Evidence: - such a program cannot change its layout, so branch reconciliation would only - query topology unnecessarily. - Observation: Placement does not need a coupling graph. Evidence: the merged placement pass replaces dynamic allocations with target sites without using routing data, while the mapping pass requires explicit connectivity. +- Observation: A compiler target is only useful when both its connectivity and + native-operation support are known. Evidence: every planned real target can + report an explicit set or an unrestricted claim, while deferring missing facts + to individual passes adds branches without producing a usable target. +- Observation: QDMI 1.3 operation metadata describes one fixed qubit and + parameter count. Evidence: finite higher-arity operations such as SV1 CCNOT + are representable, while generic `unitary`, barriers, and control-flow + constructs do not have a truthful fixed positive compiler-target signature. +- Observation: DDSIM's controlled-gate surface is not limited to `mcx` and + `mcp`. Evidence: every canonical QCO gate in `GateTable.def`, including gates + with two or three targets, has generic QIR controlled specializations and the + DDSIM construction accepts an arbitrary positive control set. - Observation: The Windows ARM failure was a test-data race, not a compiler regression. Evidence: the QIR and invalid-input CTest scripts used the same output directory while the QIR script removed that directory at startup. @@ -107,47 +130,57 @@ unrestricted, and an explicit list. Rationale: sites, connectivity, operations, and optional calibration data are useful across hardware modalities, while an enum would force technology switches into compiler passes. Date/Author: 2026-08-23, Codex. -- Decision: Use explicit unknown, unrestricted, and explicit states for both - connectivity and native operations. Rationale: missing provider metadata must - not grant support. Date/Author: 2026-08-23, Codex. +- Decision: The initial three-state design was superseded by a complete-target + contract. Connectivity is all-to-all or explicit; native-operation support is + unrestricted or explicit. Missing provider metadata is an inference error. + Rationale: incomplete targets are not meaningful for the supported compiler + workflows, and rejecting them once removes defensive branches from every pass. + Date/Author: 2026-09-01, Lukas Burgholzer with Codex assistance. +- Decision: Represent an operation arity as either a fixed width or a variadic + total width with a minimum. Fixed zero represents `gphase`; variadic `n` + represents a base operation on `n` targets with any number of additional + positive controls. Rationale: this one capability describes DDSIM's complete + controlled standard-gate surface, including controlled multi-target gates, + without inventing `mc*` aliases or a maximum derived from the device size. + Date/Author: 2026-09-01, Lukas Burgholzer with Codex assistance. +- Decision: Bridge DDSIM's control capability through one exact versioned + operation custom-property marker on canonical uncontrolled base gates. + Rationale: QDMI 1.3 has no standard operation-arity range or modifier + capability; keeping the workaround narrow and exact makes its QDMI 1.4 + replacement explicit. Date/Author: 2026-09-01, Lukas Burgholzer with Codex + assistance. - Decision: Keep this prerequisite free of MLIR target attributes and QDMI program features. Rationale: the following target-environment change will serialize this validated contract. Date/Author: 2026-08-23, Codex. -- Decision: A pass diagnoses unknown metadata only when a surviving operation - needs that fact. Rationale: program requirements are stage-relative; a - classical or single-site program does not need native-operation or topology - claims. Date/Author: 2026-08-23, Codex. - Decision: Use sentinel-free standard containers for site IDs. Rationale: this preserves the documented public domain instead of introducing an arbitrary range restriction to accommodate an implementation detail. Date/Author: 2026-09-01, Lukas Burgholzer with Codex assistance. -- Decision: Route explicit connectivity through the mapping pass and route - all-to-all or unknown connectivity through the placement pass. The placement - pass rejects unknown connectivity before mutation when a non-barrier - multi-site operation remains. Rationale: placement needs only target sites, - while routing needs a coupling graph. Keeping the guard in the placement pass - also protects direct pass users. Date/Author: 2026-09-01, Lukas Burgholzer - with Codex assistance. +- Decision: Route explicit connectivity through the mapping pass and all-to-all + connectivity through the placement pass. Rationale: placement needs only + target sites, while routing needs a coupling graph. Date/Author: 2026-09-01, + Lukas Burgholzer with Codex assistance. - Decision: Supersede the call-site DDSIM workaround with an exact namespaced marker in the bundled device's first custom property. Rationale: QDMI 1.3 cannot enumerate the simulator's all-to-all topology or homogeneous operation support compactly, while an exact marker lets only an explicit provider claim those facts. Date/Author: 2026-09-01, Lukas Burgholzer with Codex assistance. - Decision: Build on the independently merged topology-free placement pass. - Rationale: target compilation must still assign static sites when topology is - unknown, but the placement stage does not need a coupling graph. The separate - prerequisite kept each change reviewable. Date/Author: 2026-09-01, Lukas - Burgholzer with Codex assistance. + Rationale: all-to-all targets still need static site assignment, but no + routing graph. The separate prerequisite kept each change reviewable. + Date/Author: 2026-09-01, Lukas Burgholzer with Codex assistance. ## Outcomes & Retrospective The context-free target contract is implemented in pull request #2218. DDSIM now exposes enough exact metadata for `CompilerTarget.from_device` under QDMI 1.3, and the direct path compiles and executes QIR. The compiler and focused Python -tests pass. Target compilation uses the merged placement pass for all-to-all and -safe unknown-connectivity programs, while explicit coupling graphs use the -mapping pass. Durable archive branches preserve the published heads from before -the rescope, `main` refresh, and placement integration. +tests pass. Target compilation uses the merged placement pass for all-to-all +targets, while explicit coupling graphs use the mapping pass. Incomplete target +metadata fails at construction or QDMI inference instead of being carried into +passes. Historical pre-rescope heads remain available in the existing one-off +archive branches; no new archive branches are part of this or future branch +updates. ## Context and Orientation @@ -159,30 +192,32 @@ and synthesis facts. Mapping and synthesis passes under Tests live in `mlir/unittests/Compiler/test_compiler_target.cpp` and adjacent mapping and synthesis test directories. -Unknown means the provider did not report enough information. Unrestricted means -every site pair or operation is accepted. Explicit means the target lists the -accepted couplings or operations. A pass that requires unknown information must -emit a diagnostic instead of assuming support. +All-to-all means every distinct site pair is connected. Unrestricted means every +representable operation is accepted. Explicit means the target lists the +accepted couplings or operations. QDMI inference rejects absent connectivity or +operation applicability instead of constructing a partial target. Fixed arity +accepts one exact total width. Variadic arity accepts every total width from its +minimum through the target's site count; for the DDSIM compatibility marker, the +additional sites are positive controls around the named base gate. ## Plan of Work Add small value types to `CompilerTarget` for connectivity and native-operation -support. Each type carries a three-way kind and, for the explicit kind, the -existing vector. Make target construction accept these values and default them -to unknown. Rename target `numQubits()` to `numSites()` and operation -`numQubits()` to `arity()`. Update mapping, synthesis, the QDMI adapter, -bindings, and tests to use the new vocabulary and to handle unknown facts before -querying routes or operation support. - -Use deterministic placement for all-to-all connectivity and for unknown -connectivity when no non-barrier multi-site operation remains. Use the mapping -pass only for an explicit coupling graph. Validate unknown connectivity in the -placement pass before it changes the program. +support. Connectivity is either all-to-all or carries the existing explicit +coupling vector. Native-operation support is either unrestricted or carries the +existing explicit operation vector. Require both at target construction. Rename +target `numQubits()` to `numSites()` and operation `numQubits()` to `arity()`. +Update mapping, synthesis, the QDMI adapter, bindings, and tests to use the new +vocabulary. Reject missing QDMI facts while constructing the target. + +Use deterministic placement for all-to-all connectivity. Use the mapping pass +only for an explicit coupling graph. Keep site identifiers, ordered operation site tuples, timing units, T1/T2 data, -and fidelity values unchanged. Add no technology enum and no generic property -container. Add the pull request reference to the existing general Compiler -Collection changelog entry. +and fidelity values unchanged. Represent fixed and variadic operation widths +directly; do not add a technology enum, generic property container, or +gate-family-specific controlled aliases. Add the pull request reference to the +existing general Compiler Collection changelog entry. ## Concrete Steps @@ -199,24 +234,22 @@ targets when those sources change. All commands are repeatable. ## Validation and Acceptance -Target tests must prove that unknown topology is distinct from all-to-all and -explicit topology, and that unknown native operations are distinct from all and -an explicit list. Existing explicit target mapping and synthesis tests must -still pass. Placement must accept barriers and single-site operations with -unknown connectivity, reject a multi-site unitary without changing the input, -and place all-to-all programs compactly. The build must contain no old public -`numQubits()` or operation qubit-count references. `uvx nox -s lint` and -`git diff --check` must pass. +Target tests must cover all-to-all and explicit connectivity, plus unrestricted, +explicit-empty, and explicit native-operation support. C++ and Python target +construction must require both facts. Existing explicit target mapping and +synthesis tests must still pass, and placement must place all-to-all programs +compactly. The build must contain no old public `numQubits()` or operation +qubit-count references. `uvx nox -s lint` and `git diff --check` must pass. ## Idempotence and Recovery -Builds and tests are safe to repeat. Preserve unrelated worktree changes. Before -rewriting a published branch, record the remote head and create a backup ref. -Use an exact force-with-lease and verify every signed commit before pushing. +Builds and tests are safe to repeat. Preserve unrelated worktree changes. Use an +exact force-with-lease and verify every signed commit before pushing. Do not +create archive branches for branch rewrites. ## Artifacts and Notes -The current behavior to replace is summarized by the existing class comment: +The original behavior replaced by this work was summarized by the class comment: An absent topology means all-to-all connectivity. An absent operation set means that every operation is native. @@ -229,5 +262,10 @@ state are context-free C++ values so the later MLIR attribute layer can materialize them without making `CompilerTarget` depend on an MLIR context. Plan revision note (2026-09-01): Updated the plan after the independent -placement pass merged. The final design delegates topology-free work to that -pass and keeps routing confined to explicit connectivity. +placement pass merged and after the complete-target contract replaced the +intermediate three-state design. All-to-all placement remains separate from +explicit-topology routing; incomplete metadata now fails at inference. + +Plan revision note (2026-09-01): Added zero-arity global phase and variadic +controlled standard-gate capabilities for DDSIM. The temporary QDMI v1.3 bridge +is operation-local and does not enumerate `mcx`, `mcp`, or other aliases. diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 7bbce98d6d..be157f5a09 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -356,8 +356,8 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { auto compilerTarget = nb::class_( m, "CompilerTarget", R"pb(Immutable MLIR compiler target. -Connectivity and native-operation metadata distinguish unknown, -unrestricted, and explicitly enumerated support.)pb"); +Every target has either all-to-all or explicitly enumerated connectivity and +either unrestricted or explicitly enumerated native-operation support.)pb"); auto durationUnit = nb::class_( compilerTarget, "DurationUnit", "Unit for raw target timing metadata."); @@ -437,10 +437,53 @@ unrestricted, and explicitly enumerated support.)pb"); .def_prop_ro("fidelity", &mlir::CompilerTarget::SiteTuple::fidelity, "The operation fidelity, if available."); + nb::enum_( + compilerTarget, "OperationArityKind", + "How an operation capability accepts qubit widths.") + .value("FIXED", mlir::CompilerTarget::Operation::Arity::Kind::Fixed) + .value("VARIADIC", + mlir::CompilerTarget::Operation::Arity::Kind::Variadic); + + auto operationArity = nb::class_( + compilerTarget, "OperationArity", "Accepted operation qubit widths."); + operationArity + .def_static("fixed", &mlir::CompilerTarget::Operation::Arity::fixed, + "value"_a, "Create an exact operation arity.") + .def_static("variadic", &mlir::CompilerTarget::Operation::Arity::variadic, + "minimum"_a, + "Create an operation arity with an inclusive minimum. " + "Operation construction requires a positive minimum.") + .def_prop_ro("kind", &mlir::CompilerTarget::Operation::Arity::kind, + "The arity kind.") + .def_prop_ro("value", &mlir::CompilerTarget::Operation::Arity::value, + "The exact arity or inclusive variadic minimum.") + .def("accepts", &mlir::CompilerTarget::Operation::Arity::accepts, + "width"_a, "Whether this arity accepts a concrete width."); + auto targetOperation = nb::class_( compilerTarget, "Operation", "A homogeneous target-wide operation capability and its calibration."); targetOperation + .def( + "__init__", + [](mlir::CompilerTarget::Operation& self, std::string name, + const mlir::CompilerTarget::Operation::Arity arity, + const size_t numParameters, + std::optional> + siteTuples, + const std::optional duration, + const std::optional fidelity) { + constructFromExpected( + self, + mlir::CompilerTarget::Operation::create( + std::move(name), arity, numParameters, + std::move(siteTuples) + .value_or( + std::vector{}), + duration, fidelity)); + }, + "name"_a, "arity"_a, "num_parameters"_a, "site_tuples"_a = nb::none(), + "duration"_a = nb::none(), "fidelity"_a = nb::none()) .def( "__init__", [](mlir::CompilerTarget::Operation& self, std::string name, @@ -473,7 +516,7 @@ unrestricted, and explicitly enumerated support.)pb"); }, "The normalized compiler operation name.") .def_prop_ro("arity", &mlir::CompilerTarget::Operation::arity, - "The fixed operation arity.") + "The accepted operation arity.") .def_prop_ro("num_parameters", &mlir::CompilerTarget::Operation::numParameters, "The number of real-valued parameters.") @@ -529,14 +572,13 @@ unrestricted, and explicitly enumerated support.)pb"); "The two-qubit entangler."); nb::enum_( - compilerTarget, "ConnectivityKind", "How target connectivity is known.") - .value("UNKNOWN", mlir::CompilerTarget::Connectivity::Kind::Unknown) + compilerTarget, "ConnectivityKind", "The target connectivity model.") .value("ALL_TO_ALL", mlir::CompilerTarget::Connectivity::Kind::AllToAll) .value("EXPLICIT", mlir::CompilerTarget::Connectivity::Kind::Explicit); auto connectivity = nb::class_( - compilerTarget, "Connectivity", "A target connectivity claim."); - connectivity.def(nb::init<>(), "Create an unknown connectivity claim.") + compilerTarget, "Connectivity", "A target connectivity model."); + connectivity .def( "__init__", [](mlir::CompilerTarget::Connectivity& self, @@ -544,11 +586,11 @@ unrestricted, and explicitly enumerated support.)pb"); new (&self) mlir::CompilerTarget::Connectivity( mlir::CompilerTarget::Connectivity::fromCouplings(couplings)); }, - "couplings"_a, "Create an explicit connectivity claim.") + "couplings"_a, "Create an explicit connectivity model.") .def_static("all_to_all", &mlir::CompilerTarget::Connectivity::allToAll, - "Create an all-to-all connectivity claim.") + "Create an all-to-all connectivity model.") .def_prop_ro("kind", &mlir::CompilerTarget::Connectivity::kind, - "How the connectivity is known.") + "The connectivity model.") .def_prop_ro( "couplings", [](const mlir::CompilerTarget::Connectivity& value) { @@ -559,17 +601,15 @@ unrestricted, and explicitly enumerated support.)pb"); nb::enum_( compilerTarget, "NativeOperationsKind", - "How native target operations are known.") - .value("UNKNOWN", mlir::CompilerTarget::NativeOperations::Kind::Unknown) + "The native-operation support model.") .value("UNRESTRICTED", mlir::CompilerTarget::NativeOperations::Kind::Unrestricted) .value("EXPLICIT", mlir::CompilerTarget::NativeOperations::Kind::Explicit); auto nativeOperations = nb::class_( - compilerTarget, "NativeOperations", "A native-operation claim."); + compilerTarget, "NativeOperations", "Native-operation support."); nativeOperations - .def(nb::init<>(), "Create an unknown native-operation claim.") .def( "__init__", [](mlir::CompilerTarget::NativeOperations& self, @@ -578,12 +618,12 @@ unrestricted, and explicitly enumerated support.)pb"); mlir::CompilerTarget::NativeOperations::fromOperations( operations)); }, - "operations"_a, "Create an explicit native-operation claim.") + "operations"_a, "Create explicit native-operation support.") .def_static("unrestricted", &mlir::CompilerTarget::NativeOperations::unrestricted, - "Create an unrestricted native-operation claim.") + "Create unrestricted native-operation support.") .def_prop_ro("kind", &mlir::CompilerTarget::NativeOperations::kind, - "How the native operations are known.") + "The native-operation support model.") .def_prop_ro( "operations", [](const mlir::CompilerTarget::NativeOperations& value) { @@ -604,9 +644,7 @@ unrestricted, and explicitly enumerated support.)pb"); std::move(nativeOperations), std::move(durationUnit))); }, - "num_sites"_a, nb::kw_only(), - "connectivity"_a = mlir::CompilerTarget::Connectivity{}, - "native_operations"_a = mlir::CompilerTarget::NativeOperations{}, + "num_sites"_a, nb::kw_only(), "connectivity"_a, "native_operations"_a, "duration_unit"_a = nb::none()) .def( "__init__", @@ -621,10 +659,8 @@ unrestricted, and explicitly enumerated support.)pb"); std::move(nativeOperations), std::move(durationUnit))); }, - "name"_a, "num_sites"_a, nb::kw_only(), - "connectivity"_a = mlir::CompilerTarget::Connectivity{}, - "native_operations"_a = mlir::CompilerTarget::NativeOperations{}, - "duration_unit"_a = nb::none()) + "name"_a, "num_sites"_a, nb::kw_only(), "connectivity"_a, + "native_operations"_a, "duration_unit"_a = nb::none()) .def( "__init__", [](mlir::CompilerTarget& self, @@ -638,9 +674,7 @@ unrestricted, and explicitly enumerated support.)pb"); std::move(nativeOperations), std::move(durationUnit))); }, - "sites"_a, nb::kw_only(), - "connectivity"_a = mlir::CompilerTarget::Connectivity{}, - "native_operations"_a = mlir::CompilerTarget::NativeOperations{}, + "sites"_a, nb::kw_only(), "connectivity"_a, "native_operations"_a, "duration_unit"_a = nb::none()) .def( "__init__", @@ -655,10 +689,8 @@ unrestricted, and explicitly enumerated support.)pb"); std::move(nativeOperations), std::move(durationUnit))); }, - "name"_a, "sites"_a, nb::kw_only(), - "connectivity"_a = mlir::CompilerTarget::Connectivity{}, - "native_operations"_a = mlir::CompilerTarget::NativeOperations{}, - "duration_unit"_a = nb::none()) + "name"_a, "sites"_a, nb::kw_only(), "connectivity"_a, + "native_operations"_a, "duration_unit"_a = nb::none()) .def_static( "from_device", [](const qdmi::Device& device) { @@ -725,7 +757,7 @@ unrestricted, and explicitly enumerated support.)pb"); }, "Detailed sites in compiler-vertex order.") .def_prop_ro("connectivity_kind", &mlir::CompilerTarget::connectivityKind, - "How the target connectivity is known.") + "The target connectivity model.") .def_prop_ro( "couplings", [](const mlir::CompilerTarget& target) { @@ -735,7 +767,7 @@ unrestricted, and explicitly enumerated support.)pb"); "Canonical undirected couplings in target site IDs.") .def_prop_ro("native_operations_kind", &mlir::CompilerTarget::nativeOperationsKind, - "How the target native operations are known.") + "The target native-operation support model.") .def_prop_ro( "operations", [](const mlir::CompilerTarget& target) { @@ -759,7 +791,7 @@ unrestricted, and explicitly enumerated support.)pb"); return target.supportsOperation(name, arity, numParameters); }, "name"_a, "arity"_a, "num_parameters"_a = nb::none(), - "Whether the target supports an operation, or None if unknown."); + "Whether the target supports an operation."); auto program = nb::class_( m, "Program", R"pb(Base class for a typed MLIR compiler program. diff --git a/bindings/patterns.txt b/bindings/patterns.txt index 1420d34e1e..d2a24897c9 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -94,8 +94,8 @@ mqt\.core\.mlir\.CompilerTarget\.__init__$: self, num_sites: int, *, - connectivity: CompilerTarget.Connectivity = ..., - native_operations: CompilerTarget.NativeOperations = ..., + connectivity: CompilerTarget.Connectivity, + native_operations: CompilerTarget.NativeOperations, duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload @@ -104,8 +104,8 @@ mqt\.core\.mlir\.CompilerTarget\.__init__$: name: str, num_sites: int, *, - connectivity: CompilerTarget.Connectivity = ..., - native_operations: CompilerTarget.NativeOperations = ..., + connectivity: CompilerTarget.Connectivity, + native_operations: CompilerTarget.NativeOperations, duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload @@ -113,8 +113,8 @@ mqt\.core\.mlir\.CompilerTarget\.__init__$: self, sites: Sequence[CompilerTarget.Site], *, - connectivity: CompilerTarget.Connectivity = ..., - native_operations: CompilerTarget.NativeOperations = ..., + connectivity: CompilerTarget.Connectivity, + native_operations: CompilerTarget.NativeOperations, duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload @@ -123,11 +123,24 @@ mqt\.core\.mlir\.CompilerTarget\.__init__$: name: str, sites: Sequence[CompilerTarget.Site], *, - connectivity: CompilerTarget.Connectivity = ..., - native_operations: CompilerTarget.NativeOperations = ..., + connectivity: CompilerTarget.Connectivity, + native_operations: CompilerTarget.NativeOperations, duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... +mqt\.core\.mlir\.CompilerTarget\.Operation\.__init__$: + \from collections.abc import Sequence + def __init__( + self, + name: str, + arity: int | CompilerTarget.OperationArity, + num_parameters: int, + site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, + duration: int | None = None, + fidelity: float | None = None, + ) -> None: + \doc + mqt\.core\.mlir\.CompilerTarget\.from_device$: \from mqt.core.qdmi import Device @staticmethod diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 1b0adcf777..6a096e949b 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -28,13 +28,18 @@ Target compilation accepts optimized QCO, QC, or QIR output and uses the canonical QCO pipeline; it cannot be combined with a custom `qco_pipeline`. The target can also be constructed directly. Connectivity and native-operation -metadata are unknown unless the caller states them: +support are required: ```python target = CompilerTarget( 3, connectivity=CompilerTarget.Connectivity([(0, 1), (1, 2)]), native_operations=CompilerTarget.NativeOperations([ + CompilerTarget.Operation( + "gphase", + arity=CompilerTarget.OperationArity.fixed(0), + num_parameters=1, + ), CompilerTarget.Operation("u", arity=1, num_parameters=3), CompilerTarget.Operation("cx", arity=2, num_parameters=0), CompilerTarget.Operation("measure", arity=1, num_parameters=0), @@ -48,9 +53,18 @@ empty `CompilerTarget.NativeOperations([])` reports that no quantum operation is native. It can be used with passes that need only topology, but target compilation cannot lower quantum operations without a synthesis basis. Use `CompilerTarget.NativeOperations.unrestricted()` only when the target accepts -every operation. The default-constructed metadata objects mean that the -corresponding support is unknown; target compilation rejects an unknown property -when a pass needs it. +every operation. Creating a target from a QDMI device fails if the device does +not provide a complete connectivity model and a representable native-operation +set. An explicit operation arity is either fixed or variadic with a positive, +inclusive minimum. Fixed zero represents a global-phase operation. A variadic +capability accepts every total width from its minimum through the target's site +count; site-specific calibration tuples are therefore available only for fixed, +positive arities. Structural and program-format constructs are not +compiler-target operations. + +Target synthesis preserves a native `gphase`. If the target does not support +`gphase`, target synthesis preserves relative phase effects and removes only the +unobservable global phase of the entry point. Use {py:meth}`~mqt.core.mlir.QCOProgram.compile_for_target` to apply target compilation to an existing QCO program. Compilation runs in place. If a pass @@ -60,8 +74,7 @@ benchmarking, the C++ API exposes separate factories for pre-routing optimization, deterministic placement, topology-aware mapping, native synthesis, and conformance verification. Target compilation uses compact placement on all-to-all targets and the mapper only when the target has an explicit coupling -graph. A target with unknown connectivity can use compact placement only when no -non-barrier multi-site operation remains. +graph. Target compilation preserves quantum operations even when their final qubit values are not measured or returned. This supports measurement-free programs, @@ -125,9 +138,20 @@ if (!qco || !qco->compileForTarget(*target)) { The adapter accepts circuit-model devices whose operations are available throughout the topology in both operand orientations. Operand-symmetric gates, -such as CZ, may report each edge once. Neutral-atom zone models require a +such as CZ, may report each edge once. Operations with arity above two must +report every ordered tuple of distinct sites. Neutral-atom zone models require a different compilation model and are rejected with a diagnostic. +QDMI 1.3 cannot report an operation-arity range. The bundled DDSIM device uses +an exact, versioned custom-operation marker to state that each canonical +standard gate with one or more targets accepts arbitrary positive controls. The +adapter turns such a base gate into a variadic capability whose minimum is the +base gate's target count. For example, DDSIM reports `h` with minimum one, `rxx` +with minimum two, and `rccx` with minimum three; each also accepts any +additional number of controls up to the simulator's site count. Controlled +aliases such as `mcx` and `mcp` are not enumerated as compiler capabilities. +This private bridge can be removed when QDMI standardizes equivalent metadata. + The bundled Garnet and Emerald snapshots contain available T1, T2, and fidelity data. Operation durations are absent because they were unavailable. See {doc}`../qdmi/sc_device` for their stable IDs and {doc}`../qdmi/configuration` diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index f7a41a17c9..4aaad1ecf1 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -33,8 +33,8 @@ class Operation; * * @details Hardware sites retain their target-defined nonnegative i64 * identifiers. Routing algorithms use dense zero-based vertices in site order. - * Connectivity and native-operation metadata distinguish unknown, - * unrestricted, and explicitly enumerated support. + * Connectivity is either all-to-all or explicitly enumerated. Native-operation + * support is either unrestricted or explicitly enumerated. * * Compiler targets have shared immutable storage, making copies cheap while * preserving validated topology and capability caches. @@ -44,13 +44,10 @@ class CompilerTarget { using SiteId = int64_t; using Coupling = std::pair; - /// Target connectivity knowledge. + /// Target connectivity. class Connectivity { public: - enum class Kind : uint8_t { Unknown, AllToAll, Explicit }; - - /// Create unknown connectivity. - Connectivity() noexcept; + enum class Kind : uint8_t { AllToAll, Explicit }; /// Create unrestricted all-to-all connectivity. [[nodiscard]] static Connectivity allToAll(); @@ -59,7 +56,7 @@ class CompilerTarget { [[nodiscard]] static Connectivity fromCouplings(llvm::ArrayRef couplings); - /// Return the connectivity knowledge kind. + /// Return the connectivity kind. [[nodiscard]] Kind kind() const noexcept; /// Return explicitly enumerated couplings, if any. @@ -176,6 +173,38 @@ class CompilerTarget { */ class Operation { public: + /** + * @brief The accepted number of qubits for an operation capability. + */ + class Arity { + public: + enum class Kind : uint8_t { Fixed, Variadic }; + + /// Create an exact operation arity. + [[nodiscard]] static Arity fixed(size_t value) noexcept; + + /// Create an operation arity with the given inclusive minimum. + /// Operation construction requires a positive minimum. + [[nodiscard]] static Arity variadic(size_t minimum) noexcept; + + /// Return the arity kind. + [[nodiscard]] Kind kind() const noexcept; + + /// Return the exact arity or inclusive variadic minimum. + [[nodiscard]] size_t value() const noexcept; + + /// Return whether this arity accepts a concrete operation width. + [[nodiscard]] bool accepts(size_t width) const noexcept; + + friend bool operator==(const Arity&, const Arity&) = default; + + private: + Arity(Kind kind, size_t value) noexcept; + + Kind kind_; + size_t value_; + }; + /** * @brief Create a validated operation capability. */ @@ -185,14 +214,23 @@ class CompilerTarget { std::optional duration = std::nullopt, std::optional fidelity = std::nullopt); + /** + * @brief 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); + /// Return the exact reported operation name. [[nodiscard]] llvm::StringRef name() const noexcept; /// Return the canonical lower-case compiler operation name. [[nodiscard]] llvm::StringRef canonicalName() const noexcept; - /// Return the positive fixed operation arity. - [[nodiscard]] size_t arity() const noexcept; + /// Return the accepted operation arity. + [[nodiscard]] Arity arity() const noexcept; /// Return the number of real-valued operation parameters. [[nodiscard]] size_t numParameters() const noexcept; @@ -207,26 +245,23 @@ class CompilerTarget { [[nodiscard]] std::optional fidelity() const noexcept; private: - Operation(std::string name, std::string canonicalName, size_t arity, + Operation(std::string name, std::string canonicalName, Arity arity, size_t numParameters, std::vector siteTuples, std::optional duration, std::optional fidelity); std::string name_; std::string canonicalName_; - size_t arity_; + Arity arity_; size_t numParameters_; std::vector siteTuples_; std::optional duration_; std::optional fidelity_; }; - /// Native-operation knowledge. + /// Native-operation support. class NativeOperations { public: - enum class Kind : uint8_t { Unknown, Unrestricted, Explicit }; - - /// Create unknown native-operation support. - NativeOperations() noexcept; + enum class Kind : uint8_t { Unrestricted, Explicit }; /// Create unrestricted native-operation support. [[nodiscard]] static NativeOperations unrestricted(); @@ -235,7 +270,7 @@ class CompilerTarget { [[nodiscard]] static NativeOperations fromOperations(llvm::ArrayRef operations); - /// Return the native-operation knowledge kind. + /// Return the native-operation support kind. [[nodiscard]] Kind kind() const noexcept; /// Return explicitly enumerated operations, if any. @@ -299,32 +334,32 @@ class CompilerTarget { * @brief Create an unnamed target with dense site IDs `0..numSites-1`. */ [[nodiscard]] static llvm::Expected - create(size_t numSites, Connectivity connectivity = {}, - NativeOperations nativeOperations = {}, + create(size_t numSites, Connectivity connectivity, + NativeOperations nativeOperations, std::optional durationUnit = std::nullopt); /** * @brief Create a named target with dense site IDs `0..numSites-1`. */ [[nodiscard]] static llvm::Expected - create(std::string name, size_t numSites, Connectivity connectivity = {}, - NativeOperations nativeOperations = {}, + create(std::string name, size_t numSites, Connectivity connectivity, + NativeOperations nativeOperations, std::optional durationUnit = std::nullopt); /** * @brief Create an unnamed target from detailed sites. */ [[nodiscard]] static llvm::Expected - create(std::vector sites, Connectivity connectivity = {}, - NativeOperations nativeOperations = {}, + create(std::vector sites, Connectivity connectivity, + NativeOperations nativeOperations, std::optional durationUnit = std::nullopt); /** * @brief Create a named target from detailed sites. */ [[nodiscard]] static llvm::Expected - create(std::string name, std::vector sites, - Connectivity connectivity = {}, NativeOperations nativeOperations = {}, + create(std::string name, std::vector sites, Connectivity connectivity, + NativeOperations nativeOperations, std::optional durationUnit = std::nullopt); /// Copying shares immutable storage; rvalues copy and keep the source valid. @@ -354,7 +389,7 @@ class CompilerTarget { /// Return the target site identifier for a valid dense compiler vertex. [[nodiscard]] SiteId siteForVertex(size_t vertex) const; - /// Return the connectivity knowledge kind. + /// Return the connectivity kind. [[nodiscard]] Connectivity::Kind connectivityKind() const noexcept; /** @@ -364,50 +399,41 @@ class CompilerTarget { /** * @brief Return whether two valid dense compiler vertices are adjacent. - * @pre Connectivity must be known. */ [[nodiscard]] bool areAdjacent(size_t source, size_t target) const; /** * @brief Return the cached shortest-path distance between valid vertices. - * @pre Connectivity must be known. */ [[nodiscard]] size_t distanceBetween(size_t source, size_t target) const; /** * @brief Invoke @p callback for every neighbour of a valid dense vertex. - * @pre Connectivity must be known. */ void forEachNeighbour(size_t vertex, llvm::function_ref callback) const; /** * @brief Return the maximum degree of the target's routing topology. - * @pre Connectivity must be known. */ [[nodiscard]] size_t maxDegree() const noexcept; - /// Return the native-operation knowledge kind. + /// Return the native-operation support kind. [[nodiscard]] NativeOperations::Kind nativeOperationsKind() const noexcept; /// Return operation capabilities in reported order. [[nodiscard]] llvm::ArrayRef operations() const noexcept; - /** - * @brief Return whether an operation capability is supported by the target, - * or `std::nullopt` if native-operation support is unknown. - */ - [[nodiscard]] std::optional + /// Return whether an operation capability is supported by the target. + [[nodiscard]] bool supportsOperation(llvm::StringRef name, size_t arity, std::optional numParameters = std::nullopt) const; - /// Return whether a QCO operation is supported, or `std::nullopt` if unknown. - [[nodiscard]] std::optional - supports(::mlir::Operation* operation) const; + /// Return whether a QCO operation is supported. + [[nodiscard]] bool supports(::mlir::Operation* operation) const; - /// Return whether a recognized gate is supported, or `std::nullopt` if - /// unknown. - [[nodiscard]] std::optional supports(GateKind gate) const; + /// Return whether a recognized gate is supported. + [[nodiscard]] bool supports(GateKind gate) const; /// Return the recognized gates supported by the target. [[nodiscard]] llvm::ArrayRef supportedGates() const noexcept; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h index 942c67bfbb..6ef068926a 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h @@ -23,8 +23,6 @@ class CompilerTarget; namespace qco { /// Create a deterministic placement pass for a compiler target. -/// Unknown connectivity requires a program without non-barrier multi-site -/// operations. std::unique_ptr createPlacementPass(const CompilerTarget& target); /// Create a mapping pass for a compiler target with explicit topology. diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index 05d15a4d40..ad215d07e7 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -38,23 +38,38 @@ namespace mlir { /// Target facts that QDMI v1.3 cannot encode compactly. -/// TODO(#2093): Remove this compatibility marker when QDMI standardizes -/// explicit unrestricted connectivity and operation applicability. +/// TODO(#2093): Remove these compatibility markers when QDMI standardizes +/// explicit unrestricted connectivity, operation applicability, and operation +/// arity ranges. constexpr std::string_view ALL_TO_ALL_HOMOGENEOUS_METADATA = "mqt.compiler-target.v1:all-to-all-homogeneous"; +constexpr std::string_view ARBITRARY_POSITIVE_CONTROLS_METADATA = + "mqt.compiler-target.v1:arbitrary-positive-controls"; + +[[nodiscard]] static bool +matchesMetadata(const std::optional>& metadata, + const std::string_view expected) { + const auto expectedBytes = + std::as_bytes(std::span{expected.data(), expected.size() + 1}); + return metadata && std::ranges::equal(*metadata, expectedBytes); +} [[nodiscard]] static bool hasAllToAllHomogeneousMetadata(const qdmi::Device& device) { - const auto metadata = device.queryCustomProperty>( - qdmi::CustomProperty::Custom1); - const auto expected = - std::as_bytes(std::span{ALL_TO_ALL_HOMOGENEOUS_METADATA.data(), - ALL_TO_ALL_HOMOGENEOUS_METADATA.size() + 1}); - return metadata && std::ranges::equal(*metadata, expected); + return matchesMetadata(device.queryCustomProperty>( + qdmi::CustomProperty::Custom1), + ALL_TO_ALL_HOMOGENEOUS_METADATA); +} + +[[nodiscard]] static bool +hasArbitraryPositiveControlsMetadata(const qdmi::Operation& operation) { + return matchesMetadata(operation.queryCustomProperty>( + qdmi::CustomProperty::Custom1), + ARBITRARY_POSITIVE_CONTROLS_METADATA); } [[nodiscard]] static llvm::Error -requireAdapterInput(const bool condition, const llvm::Twine& message) { +requireAdapterInput(bool condition, const llvm::Twine& message) { if (!condition) { return llvm::createStringError( std::make_error_code(std::errc::invalid_argument), message); @@ -63,8 +78,8 @@ requireAdapterInput(const bool condition, const llvm::Twine& message) { } [[nodiscard]] static llvm::Error -requireCircuitDevice(const bool condition, const llvm::StringRef deviceName, - const llvm::StringRef detail) { +requireCircuitDevice(bool condition, llvm::StringRef deviceName, + llvm::StringRef detail) { return requireAdapterInput( condition, llvm::Twine("QDMI device '") + deviceName + "' cannot be used as an MLIR compiler target: only " @@ -73,20 +88,19 @@ requireCircuitDevice(const bool condition, const llvm::StringRef deviceName, detail + ")"); } -[[nodiscard]] static llvm::Error requireHomogeneousOperation( - const bool condition, const llvm::StringRef deviceName, - const llvm::StringRef operationName, const llvm::StringRef detail) { +[[nodiscard]] static llvm::Error +requireRepresentableOperation(bool condition, llvm::StringRef deviceName, + llvm::StringRef operationName, + llvm::StringRef detail) { return requireAdapterInput( condition, llvm::Twine("QDMI device '") + deviceName + "' operation '" + operationName + - "' cannot be represented by the MLIR compiler target: " - "operation support must be homogeneous across the device " - "(" + + "' cannot be represented by the MLIR compiler target (" + detail + ")"); } [[nodiscard]] static llvm::Expected -checkedSiteId(const size_t index) { +checkedSiteId(size_t index) { if (auto error = requireAdapterInput( index <= static_cast( std::numeric_limits::max()), @@ -98,14 +112,13 @@ checkedSiteId(const size_t index) { } [[nodiscard]] static CompilerTarget::Coupling -canonicalCoupling(const CompilerTarget::SiteId first, - const CompilerTarget::SiteId second) { +canonicalCoupling(CompilerTarget::SiteId first, CompilerTarget::SiteId second) { return first < second ? CompilerTarget::Coupling{first, second} : CompilerTarget::Coupling{second, first}; } [[nodiscard]] static std::optional -allToAllCouplingCount(const size_t numSites) { +allToAllCouplingCount(size_t numSites) { if (numSites < 2) { return 0; } @@ -115,7 +128,7 @@ allToAllCouplingCount(const size_t numSites) { } [[nodiscard]] static bool -isSwapInvariantOperation(const llvm::StringRef operationName) { +isSwapInvariantOperation(llvm::StringRef operationName) { const auto canonicalName = operationName.trim().lower(); return llvm::StringSwitch(canonicalName) .Cases({"cz", "swap", "iswap"}, true) @@ -124,30 +137,67 @@ isSwapInvariantOperation(const llvm::StringRef operationName) { } [[nodiscard]] static llvm::Error validateHomogeneousSupport( - const qdmi::Operation& operation, const size_t arity, + const qdmi::Operation& operation, size_t arity, const std::vector& flattenedSites, const std::vector& deviceSites, const std::optional>& couplings, - const llvm::StringRef deviceName) { + llvm::StringRef deviceName) { const auto operationName = operation.getName(); - if (auto error = requireHomogeneousOperation( + if (auto error = requireRepresentableOperation( flattenedSites.size() % arity == 0, deviceName, operationName, "the reported site list is not divisible by the fixed arity")) { return error; } - if (auto error = requireHomogeneousOperation( - arity <= 2, deviceName, operationName, - "explicit site lists are supported only for one- and two-qubit " - "operations")) { - return error; - } - std::unordered_set knownSites; knownSites.reserve(deviceSites.size()); for (const auto& site : deviceSites) { knownSites.insert(site.id()); } + if (arity > 2) { + std::set> supportedTuples; + for (size_t offset = 0; offset < flattenedSites.size(); offset += arity) { + std::vector tuple; + tuple.reserve(arity); + for (size_t index = 0; index < arity; ++index) { + auto siteId = checkedSiteId(flattenedSites[offset + index].getIndex()); + if (!siteId) { + return siteId.takeError(); + } + if (auto error = requireRepresentableOperation( + knownSites.contains(*siteId) && + !llvm::is_contained(tuple, *siteId), + deviceName, operationName, + "each higher-arity site tuple must contain distinct device " + "sites")) { + return error; + } + tuple.emplace_back(*siteId); + } + if (auto error = requireRepresentableOperation( + supportedTuples.emplace(std::move(tuple)).second, deviceName, + operationName, + "the reported higher-arity site tuples must be unique")) { + return error; + } + } + + auto expectedTuples = std::optional{1}; + for (size_t index = 0; index < arity && expectedTuples; ++index) { + if (index >= knownSites.size()) { + expectedTuples = 0; + break; + } + expectedTuples = + llvm::checkedMulUnsigned(*expectedTuples, knownSites.size() - index); + } + return requireRepresentableOperation( + expectedTuples && supportedTuples.size() == *expectedTuples, deviceName, + operationName, + "support is not homogeneous across all ordered tuples of distinct " + "device sites"); + } + if (arity == 1) { std::unordered_set supportedSites; supportedSites.reserve(flattenedSites.size()); @@ -157,16 +207,16 @@ isSwapInvariantOperation(const llvm::StringRef operationName) { return siteId.takeError(); } const auto inserted = supportedSites.insert(*siteId).second; - if (auto error = requireHomogeneousOperation( + if (auto error = requireRepresentableOperation( knownSites.contains(*siteId) && inserted, deviceName, operationName, "the reported one-qubit sites must be unique device sites")) { return error; } } - return requireHomogeneousOperation( + return requireRepresentableOperation( supportedSites.size() == knownSites.size(), deviceName, operationName, - "the operation is not available on every device site"); + "support is not homogeneous across all device sites"); } std::set reportedTuples; @@ -183,10 +233,10 @@ isSwapInvariantOperation(const llvm::StringRef operationName) { const auto inserted = reportedTuples.insert({*first, *second}).second; const auto validTuple = *first != *second && knownSites.contains(*first) && knownSites.contains(*second) && inserted; - if (auto error = - requireHomogeneousOperation(validTuple, deviceName, operationName, - "the reported two-qubit sites must be " - "unique pairs of device sites")) { + if (auto error = requireRepresentableOperation( + validTuple, deviceName, operationName, + "the reported two-qubit sites must be " + "unique pairs of device sites")) { return error; } supportedCouplings.insert(canonicalCoupling(*first, *second)); @@ -207,15 +257,15 @@ isSwapInvariantOperation(const llvm::StringRef operationName) { return supportedCouplings.contains(coupling); }); } - if (auto error = requireHomogeneousOperation( + if (auto error = requireRepresentableOperation( coversTarget, deviceName, operationName, - couplings ? "the operation is not available on every topology edge" - : "the operation is not available on every all-to-all site " - "pair")) { + couplings ? "support is not homogeneous across all topology edges" + : "support is not homogeneous across all-to-all site " + "pairs")) { return error; } - return requireHomogeneousOperation( + return requireRepresentableOperation( isSwapInvariantOperation(operationName) || std::ranges::all_of( supportedCouplings, @@ -249,10 +299,10 @@ snapshotDurationUnit(const qdmi::Device& device) { } [[nodiscard]] static llvm::Expected> -snapshotSiteTuples(const qdmi::Operation& operation, const size_t arity, +snapshotSiteTuples(const qdmi::Operation& operation, size_t arity, const std::vector& flattenedSites, - const std::optional defaultDuration, - const std::optional defaultFidelity) { + std::optional defaultDuration, + std::optional defaultFidelity) { std::vector siteTuples; siteTuples.reserve(flattenedSites.size() / arity); for (size_t offset = 0; offset < flattenedSites.size(); offset += arity) { @@ -289,7 +339,7 @@ snapshotOperations( const std::vector& operations, const std::vector& deviceSites, const std::optional>& couplings, - const llvm::StringRef deviceName, const bool homogeneousOperationSupport) { + llvm::StringRef deviceName, bool homogeneousOperationSupport) { std::vector targetOperations; targetOperations.reserve(operations.size()); for (const auto& operation : operations) { @@ -299,17 +349,37 @@ snapshotOperations( return error; } const auto arity = operation.getQubitsNum(); - if (!arity || *arity == 0) { + if (!arity) { continue; } + const auto hasArbitraryPositiveControls = + hasArbitraryPositiveControlsMetadata(operation); + if (auto error = requireRepresentableOperation( + !hasArbitraryPositiveControls || + (*arity > 0 && homogeneousOperationSupport), + deviceName, operation.getName(), + "arbitrary positive controls require a positive base arity and " + "homogeneous operation support")) { + return error; + } const auto flattenedSites = operation.getSites(); - if (!flattenedSites && !homogeneousOperationSupport) { - return CompilerTarget::NativeOperations{}; + if (auto error = requireRepresentableOperation( + *arity == 0 || flattenedSites || homogeneousOperationSupport, + deviceName, operation.getName(), + "the supported sites are not reported")) { + return error; } const auto duration = operation.getDuration(); const auto fidelity = operation.getFidelity(); std::vector siteTuples; - if (flattenedSites) { + if (*arity == 0) { + if (auto error = requireRepresentableOperation( + !flattenedSites || flattenedSites->empty(), deviceName, + operation.getName(), + "a zero-arity operation cannot report supported sites")) { + return error; + } + } else if (flattenedSites) { if (auto error = validateHomogeneousSupport(operation, *arity, *flattenedSites, deviceSites, couplings, deviceName)) { @@ -322,8 +392,18 @@ snapshotOperations( } siteTuples = std::move(*tuples); } + if (auto error = requireRepresentableOperation( + !hasArbitraryPositiveControls || siteTuples.empty(), deviceName, + operation.getName(), + "a variadic operation cannot retain site-specific calibration")) { + return error; + } + const auto targetArity = + hasArbitraryPositiveControls + ? CompilerTarget::Operation::Arity::variadic(*arity) + : CompilerTarget::Operation::Arity::fixed(*arity); auto targetOperation = CompilerTarget::Operation::create( - operation.getName(), *arity, operation.getParametersNum(), + operation.getName(), targetArity, operation.getParametersNum(), std::move(siteTuples), duration, fidelity); if (!targetOperation) { return targetOperation.takeError(); @@ -382,6 +462,13 @@ snapshotCompilerTarget(const qdmi::Device& device) { couplings->emplace_back(*sourceId, *targetId); } } + if (auto error = requireAdapterInput( + couplings || hasHomogeneousAllToAllMetadata || sites.size() == 1, + llvm::Twine("QDMI device '") + deviceName + + "' cannot be used as an MLIR compiler target: connectivity is " + "not reported")) { + return error; + } auto operations = snapshotOperations(device.getOperations(), sites, couplings, deviceName, @@ -393,12 +480,9 @@ snapshotCompilerTarget(const qdmi::Device& device) { if (!durationUnit) { return durationUnit.takeError(); } - CompilerTarget::Connectivity connectivity; - if (couplings) { - connectivity = CompilerTarget::Connectivity::fromCouplings(*couplings); - } else if (hasHomogeneousAllToAllMetadata) { - connectivity = CompilerTarget::Connectivity::allToAll(); - } + auto connectivity = + couplings ? CompilerTarget::Connectivity::fromCouplings(*couplings) + : CompilerTarget::Connectivity::allToAll(); return CompilerTarget::create(std::move(deviceName), std::move(sites), std::move(connectivity), std::move(*operations), std::move(*durationUnit)); diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 60bb61983e..c9e4895444 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include @@ -89,10 +88,12 @@ constexpr std::array GATE_SPECIFICATIONS{ } // namespace -[[nodiscard]] static std::string canonicalOperationName(const StringRef name) { +[[nodiscard]] static std::string canonicalOperationName(StringRef name) { auto canonical = name.trim().lower(); if (canonical == "prx") { canonical = "r"; + } else if (canonical == "i") { + canonical = "id"; } else if (canonical == "u3") { canonical = "u"; } else if (canonical == "cnot") { @@ -107,8 +108,8 @@ constexpr std::array GATE_SPECIFICATIONS{ } [[nodiscard]] static llvm::Error -validatePositiveCoherenceTime(const std::optional time, - const StringRef description) { +validatePositiveCoherenceTime(std::optional time, + StringRef description) { if (time && *time == 0) { return invalidTarget(description + " must be positive"); } @@ -116,8 +117,7 @@ validatePositiveCoherenceTime(const std::optional time, } [[nodiscard]] static llvm::Error -validateFidelity(const std::optional fidelity, - const StringRef description) { +validateFidelity(std::optional fidelity, StringRef description) { if (fidelity && (!std::isfinite(*fidelity) || *fidelity < 0. || *fidelity > 1.)) { return invalidTarget(description + " must be finite and in [0, 1]"); @@ -129,10 +129,8 @@ CompilerTarget::Connectivity CompilerTarget::Connectivity::allToAll() { return {Kind::AllToAll, {}}; } -CompilerTarget::Connectivity::Connectivity() noexcept : kind_(Kind::Unknown) {} - -CompilerTarget::Connectivity CompilerTarget::Connectivity::fromCouplings( - const ArrayRef couplings) { +CompilerTarget::Connectivity +CompilerTarget::Connectivity::fromCouplings(ArrayRef couplings) { return {Kind::Explicit, couplings}; } @@ -146,12 +144,12 @@ CompilerTarget::Connectivity::couplings() const noexcept { return couplings_; } -CompilerTarget::Connectivity::Connectivity(const Kind kind, - const ArrayRef couplings) +CompilerTarget::Connectivity::Connectivity(Kind kind, + ArrayRef couplings) : kind_(kind), couplings_(couplings) {} [[nodiscard]] static llvm::Expected> -makeDenseSites(const size_t numSites) { +makeDenseSites(size_t numSites) { if (numSites == 0) { return invalidTarget("Compiler target must contain at least one site"); } @@ -175,8 +173,7 @@ makeDenseSites(const size_t numSites) { } llvm::Expected -CompilerTarget::DurationUnit::create(std::string unit, - const double scaleFactor) { +CompilerTarget::DurationUnit::create(std::string unit, double scaleFactor) { if (StringRef(unit).trim().empty()) { return invalidTarget("Compiler target duration unit must not be empty"); } @@ -187,8 +184,7 @@ CompilerTarget::DurationUnit::create(std::string unit, return DurationUnit(std::move(unit), scaleFactor); } -CompilerTarget::DurationUnit::DurationUnit(std::string unit, - const double scaleFactor) +CompilerTarget::DurationUnit::DurationUnit(std::string unit, double scaleFactor) : unit_(std::move(unit)), scaleFactor_(scaleFactor) {} StringRef CompilerTarget::DurationUnit::unit() const noexcept { return unit_; } @@ -198,9 +194,9 @@ double CompilerTarget::DurationUnit::scaleFactor() const noexcept { } llvm::Expected -CompilerTarget::Site::create(const SiteId id, std::optional name, - const std::optional t1, - const std::optional t2) { +CompilerTarget::Site::create(SiteId id, std::optional name, + std::optional t1, + std::optional t2) { if (id < 0) { return invalidTarget("Compiler target site ID must be nonnegative"); } @@ -219,9 +215,9 @@ CompilerTarget::Site::create(const SiteId id, std::optional name, return Site(id, std::move(name), t1, t2); } -CompilerTarget::Site::Site(const SiteId id, std::optional name, - const std::optional t1, - const std::optional t2) +CompilerTarget::Site::Site(SiteId id, std::optional name, + std::optional t1, + std::optional t2) : id_(id), name_(std::move(name)), t1_(t1), t2_(t2) {} CompilerTarget::SiteId CompilerTarget::Site::id() const noexcept { return id_; } @@ -243,8 +239,8 @@ std::optional CompilerTarget::Site::t2() const noexcept { llvm::Expected CompilerTarget::SiteTuple::create(std::vector sites, - const std::optional duration, - const std::optional fidelity) { + std::optional duration, + std::optional fidelity) { std::unordered_set uniqueSites; for (const auto site : sites) { if (site < 0) { @@ -264,8 +260,8 @@ CompilerTarget::SiteTuple::create(std::vector sites, } CompilerTarget::SiteTuple::SiteTuple(std::vector sites, - const std::optional duration, - const std::optional fidelity) + std::optional duration, + std::optional fidelity) : sites_(std::move(sites)), duration_(duration), fidelity_(fidelity) {} ArrayRef CompilerTarget::SiteTuple::sites() const noexcept { @@ -280,25 +276,69 @@ std::optional CompilerTarget::SiteTuple::fidelity() const noexcept { return fidelity_; } +CompilerTarget::Operation::Arity +CompilerTarget::Operation::Arity::fixed(size_t value) noexcept { + return {Kind::Fixed, value}; +} + +CompilerTarget::Operation::Arity +CompilerTarget::Operation::Arity::variadic(size_t minimum) noexcept { + return {Kind::Variadic, minimum}; +} + +CompilerTarget::Operation::Arity::Kind +CompilerTarget::Operation::Arity::kind() const noexcept { + return kind_; +} + +size_t CompilerTarget::Operation::Arity::value() const noexcept { + return value_; +} + +bool CompilerTarget::Operation::Arity::accepts(size_t width) const noexcept { + return kind_ == Kind::Variadic ? width >= value_ : width == value_; +} + +CompilerTarget::Operation::Arity::Arity(Kind kind, size_t value) noexcept + : kind_(kind), value_(value) {} + +llvm::Expected CompilerTarget::Operation::create( + std::string name, size_t arity, size_t numParameters, + std::vector siteTuples, std::optional duration, + std::optional fidelity) { + return create(std::move(name), Arity::fixed(arity), numParameters, + std::move(siteTuples), duration, fidelity); +} + llvm::Expected CompilerTarget::Operation::create( - std::string name, const size_t arity, const size_t numParameters, - std::vector siteTuples, const std::optional duration, - const std::optional fidelity) { + std::string name, Arity arity, size_t numParameters, + std::vector siteTuples, std::optional duration, + std::optional fidelity) { auto canonicalName = canonicalOperationName(name); if (canonicalName.empty()) { return invalidTarget("Compiler target operation name must not be empty"); } - if (arity == 0) { - return invalidTarget("Compiler target operation arity must be positive"); - } if (auto error = validateFidelity(fidelity, "Compiler target operation fidelity")) { return std::move(error); } + if (arity.kind() == Arity::Kind::Variadic && arity.value() == 0) { + return invalidTarget( + "Compiler target operation variadic minimum must be positive"); + } + if (arity.kind() == Arity::Kind::Variadic && !siteTuples.empty()) { + return invalidTarget( + "Compiler target variadic operation cannot contain site tuples"); + } + if (arity.kind() == Arity::Kind::Fixed && arity.value() == 0 && + !siteTuples.empty()) { + return invalidTarget( + "Compiler target zero-arity operation cannot contain site tuples"); + } SmallVector> uniqueSiteCombinations; for (const auto& siteTuple : siteTuples) { - if (siteTuple.sites().size() != arity) { + if (!arity.accepts(siteTuple.sites().size())) { return invalidTarget( "Compiler target operation site tuple does not match its arity"); } @@ -313,12 +353,11 @@ llvm::Expected CompilerTarget::Operation::create( } CompilerTarget::Operation::Operation(std::string name, - std::string canonicalName, - const size_t arity, - const size_t numParameters, + std::string canonicalName, Arity arity, + size_t numParameters, std::vector siteTuples, - const std::optional duration, - const std::optional fidelity) + std::optional duration, + std::optional fidelity) : name_(std::move(name)), canonicalName_(std::move(canonicalName)), arity_(arity), numParameters_(numParameters), siteTuples_(std::move(siteTuples)), duration_(duration), @@ -330,7 +369,10 @@ StringRef CompilerTarget::Operation::canonicalName() const noexcept { return canonicalName_; } -size_t CompilerTarget::Operation::arity() const noexcept { return arity_; } +CompilerTarget::Operation::Arity +CompilerTarget::Operation::arity() const noexcept { + return arity_; +} size_t CompilerTarget::Operation::numParameters() const noexcept { return numParameters_; @@ -354,12 +396,9 @@ CompilerTarget::NativeOperations::unrestricted() { return {Kind::Unrestricted, {}}; } -CompilerTarget::NativeOperations::NativeOperations() noexcept - : kind_(Kind::Unknown) {} - CompilerTarget::NativeOperations CompilerTarget::NativeOperations::fromOperations( - const ArrayRef operations) { + ArrayRef operations) { return {Kind::Explicit, operations}; } @@ -374,7 +413,7 @@ CompilerTarget::NativeOperations::operations() const noexcept { } CompilerTarget::NativeOperations::NativeOperations( - const Kind kind, const ArrayRef operations) + Kind kind, ArrayRef operations) : kind_(kind), operations_(operations) {} struct CompilerTarget::Storage { @@ -395,9 +434,12 @@ struct CompilerTarget::Storage { [[nodiscard]] llvm::Error initialize(); - [[nodiscard]] std::optional + [[nodiscard]] bool supportsOperation(StringRef name, size_t arity, std::optional numParameters) const; + [[nodiscard]] bool + supportsVariadicOperation(StringRef name, size_t arity, + std::optional numParameters) const; [[nodiscard]] std::optional resolveSynthesisBasis() const; std::optional name; @@ -419,9 +461,9 @@ struct CompilerTarget::Storage { CompilerTarget::Storage::Storage( std::optional targetName, std::vector targetSites, - const Connectivity::Kind targetConnectivityKind, + Connectivity::Kind targetConnectivityKind, SmallVector targetCouplings, - const NativeOperations::Kind targetNativeOperationsKind, + NativeOperations::Kind targetNativeOperationsKind, SmallVector targetOperations, std::optional targetDurationUnit) : name(std::move(targetName)), durationUnit(std::move(targetDurationUnit)), @@ -433,9 +475,9 @@ CompilerTarget::Storage::Storage( llvm::Expected> CompilerTarget::Storage::create( std::optional targetName, std::vector targetSites, - const Connectivity::Kind targetConnectivityKind, + Connectivity::Kind targetConnectivityKind, SmallVector targetCouplings, - const NativeOperations::Kind targetNativeOperationsKind, + NativeOperations::Kind targetNativeOperationsKind, SmallVector targetOperations, std::optional targetDurationUnit) { auto storage = std::make_shared( @@ -521,13 +563,17 @@ llvm::Error CompilerTarget::Storage::initialize() { return invalidTarget("Compiler target topology must be connected"); } } - } else if (connectivityKind == Connectivity::Kind::AllToAll) { + } else { maximumDegree = sites.size() - 1; } if (nativeOperationsKind == NativeOperations::Kind::Explicit) { for (const auto [index, operation] : llvm::enumerate(operations)) { - if (operation.arity() > sites.size()) { + if (operation.arity().value() > sites.size()) { + if (operation.arity().kind() == Operation::Arity::Kind::Variadic) { + return invalidTarget("Compiler target operation variadic minimum " + "exceeds its site count"); + } return invalidTarget( "Compiler target operation arity exceeds its site count"); } @@ -560,7 +606,7 @@ llvm::Error CompilerTarget::Storage::initialize() { for (const auto& specification : GATE_SPECIFICATIONS) { if (supportsOperation(specification.name, specification.arity, - specification.numParameters) == true) { + specification.numParameters)) { supportedGates.emplace_back(specification.kind); } } @@ -568,15 +614,33 @@ llvm::Error CompilerTarget::Storage::initialize() { return llvm::Error::success(); } -std::optional CompilerTarget::Storage::supportsOperation( - const StringRef operationName, const size_t arity, - const std::optional numParameters) const { +bool CompilerTarget::Storage::supportsOperation( + StringRef operationName, size_t arity, + std::optional numParameters) const { const auto canonical = canonicalOperationName(operationName); - if (canonical.empty() || arity == 0 || arity > sites.size()) { + if (canonical.empty() || arity > sites.size()) { return false; } - if (nativeOperationsKind == NativeOperations::Kind::Unknown) { - return std::nullopt; + if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { + return true; + } + const auto found = capabilities.find(canonical); + if (found == capabilities.end()) { + return false; + } + return llvm::any_of(found->second, [&](const auto index) { + const auto& operation = operations[index]; + return operation.arity().accepts(arity) && + (!numParameters || operation.numParameters() == *numParameters); + }); +} + +bool CompilerTarget::Storage::supportsVariadicOperation( + StringRef operationName, size_t arity, + std::optional numParameters) const { + const auto canonical = canonicalOperationName(operationName); + if (canonical.empty() || arity > sites.size()) { + return false; } if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { return true; @@ -587,14 +651,15 @@ std::optional CompilerTarget::Storage::supportsOperation( } return llvm::any_of(found->second, [&](const auto index) { const auto& operation = operations[index]; - return operation.arity() == arity && + return operation.arity().kind() == Operation::Arity::Kind::Variadic && + operation.arity().accepts(arity) && (!numParameters || operation.numParameters() == *numParameters); }); } std::optional CompilerTarget::Storage::resolveSynthesisBasis() const { - const auto supports = [&](const GateKind gate) { + const auto supports = [&](GateKind gate) { return llvm::is_contained(supportedGates, gate); }; std::optional singleQubit; @@ -629,7 +694,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { } llvm::Expected -CompilerTarget::create(const size_t numSites, Connectivity connectivity, +CompilerTarget::create(size_t numSites, Connectivity connectivity, NativeOperations nativeOperations, std::optional durationUnit) { auto sites = makeDenseSites(numSites); @@ -641,7 +706,7 @@ CompilerTarget::create(const size_t numSites, Connectivity connectivity, } llvm::Expected -CompilerTarget::create(std::string name, const size_t numSites, +CompilerTarget::create(std::string name, size_t numSites, Connectivity connectivity, NativeOperations nativeOperations, std::optional durationUnit) { @@ -715,7 +780,7 @@ ArrayRef CompilerTarget::siteIds() const noexcept { } std::optional -CompilerTarget::vertexForSite(const SiteId site) const noexcept { +CompilerTarget::vertexForSite(SiteId site) const noexcept { const auto found = storage_->siteToVertex.find(site); if (found == storage_->siteToVertex.end()) { return std::nullopt; @@ -723,7 +788,7 @@ CompilerTarget::vertexForSite(const SiteId site) const noexcept { return found->second; } -SiteId CompilerTarget::siteForVertex(const size_t vertex) const { +SiteId CompilerTarget::siteForVertex(size_t vertex) const { assert(vertex < numSites() && "Compiler target vertex is out of range"); return storage_->siteIds[vertex]; } @@ -737,13 +802,9 @@ ArrayRef CompilerTarget::couplings() const noexcept { return storage_->couplings; } -bool CompilerTarget::areAdjacent(const size_t source, - const size_t target) const { +bool CompilerTarget::areAdjacent(size_t source, size_t target) const { assert(source < numSites() && target < numSites() && "Compiler target vertex is out of range"); - if (connectivityKind() == Connectivity::Kind::Unknown) { - llvm::report_fatal_error("Compiler target connectivity is unknown"); - } if (connectivityKind() == Connectivity::Kind::AllToAll) { return source != target; } @@ -751,11 +812,7 @@ bool CompilerTarget::areAdjacent(const size_t source, } void CompilerTarget::forEachNeighbour( - const size_t vertex, - const llvm::function_ref callback) const { - if (connectivityKind() == Connectivity::Kind::Unknown) { - llvm::report_fatal_error("Compiler target connectivity is unknown"); - } + size_t vertex, llvm::function_ref callback) const { if (connectivityKind() == Connectivity::Kind::AllToAll) { assert(vertex < numSites() && "Compiler target vertex is out of range"); for (size_t neighbour = 0; neighbour < numSites(); ++neighbour) { @@ -770,28 +827,21 @@ void CompilerTarget::forEachNeighbour( } } -size_t CompilerTarget::distanceBetween(const size_t source, - const size_t target) const { +size_t CompilerTarget::distanceBetween(size_t source, size_t target) const { assert(source < numSites() && target < numSites() && "Compiler target vertex is out of range"); - if (connectivityKind() == Connectivity::Kind::Unknown) { - llvm::report_fatal_error("Compiler target connectivity is unknown"); - } if (connectivityKind() == Connectivity::Kind::AllToAll) { return source == target ? 0 : 1; } return storage_->distances[(source * numSites()) + target]; } -ArrayRef CompilerTarget::explicitNeighbours(const size_t vertex) const { +ArrayRef CompilerTarget::explicitNeighbours(size_t vertex) const { assert(vertex < numSites() && "Compiler target vertex is out of range"); return storage_->adjacency[vertex]; } size_t CompilerTarget::maxDegree() const noexcept { - if (connectivityKind() == Connectivity::Kind::Unknown) { - llvm::report_fatal_error("Compiler target connectivity is unknown"); - } return storage_->maximumDegree; } @@ -805,33 +855,45 @@ CompilerTarget::operations() const noexcept { return storage_->operations; } -std::optional CompilerTarget::supportsOperation( - const StringRef operationName, const size_t arity, - const std::optional numParameters) const { +bool CompilerTarget::supportsOperation( + StringRef operationName, size_t arity, + std::optional numParameters) const { return storage_->supportsOperation(operationName, arity, numParameters); } -std::optional -CompilerTarget::supports(::mlir::Operation* operation) const { +bool CompilerTarget::supports(::mlir::Operation* operation) const { if (operation == nullptr) { return false; } if (auto unitary = dyn_cast(operation)) { - if (isa(operation)) { + if (isa(operation)) { return true; } - if (auto controlled = dyn_cast(operation); - controlled && controlled.getNumControls() == 1 && - controlled.getNumTargets() == 1 && - controlled.getNumBodyUnitaries() == 1) { - auto* const body = controlled.getBodyUnitary(0).getOperation(); - if (isa(body)) { + if (auto controlled = dyn_cast(operation)) { + if (controlled.getNumControls() == 0 || + controlled.getNumBodyUnitaries() != 1) { + return false; + } + auto body = controlled.getBodyUnitary(0); + if (body.getNumQubits() != controlled.getNumTargets()) { + return false; + } + if (storage_->supportsVariadicOperation(body.getBaseSymbol(), + controlled.getNumQubits(), + body.getNumParams())) { + return true; + } + if (controlled.getNumControls() != 1 || controlled.getNumTargets() != 1) { + return false; + } + if (isa(body.getOperation())) { return storage_->supportsOperation("cx", 2, 0); } - if (isa(body)) { + if (isa(body.getOperation())) { return storage_->supportsOperation("cz", 2, 0); } + return false; } return storage_->supportsOperation(unitary.getBaseSymbol(), unitary.getNumQubits(), @@ -846,10 +908,7 @@ CompilerTarget::supports(::mlir::Operation* operation) const { return false; } -std::optional CompilerTarget::supports(const GateKind gate) const { - if (nativeOperationsKind() == NativeOperations::Kind::Unknown) { - return std::nullopt; - } +bool CompilerTarget::supports(GateKind gate) const { return llvm::is_contained(storage_->supportedGates, gate); } diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index bb329a1b2e..6be7b2c258 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -31,7 +31,6 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(qco::createMappingPass(target, qco::MappingPassOptions{})); break; case CompilerTarget::Connectivity::Kind::AllToAll: - case CompilerTarget::Connectivity::Kind::Unknown: pm.addPass(qco::createPlacementPass(target)); break; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 1e20b24da8..096d75eece 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -339,23 +339,6 @@ struct PlacementPass final return; } - if (target.connectivityKind() == - CompilerTarget::Connectivity::Kind::Unknown) { - const auto result = func.walk([](UnitaryOpInterface unitary) { - if (isa(unitary) || unitary.getNumQubits() <= 1) { - return WalkResult::advance(); - } - unitary.emitError() << "target placement requires known connectivity " - "for an operation with arity " - << unitary.getNumQubits(); - return WalkResult::interrupt(); - }); - if (result.wasInterrupted()) { - signalPassFailure(); - return; - } - } - auto computation = discoverComputation(func); if (failed(computation) || failed(checkCapacity(func, target, *computation))) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 7c01d76894..b8494834f9 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -9,6 +9,7 @@ */ #include "mlir/Compiler/Target.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" @@ -296,7 +297,39 @@ static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, static bool requiresTargetSynthesis(Operation* operation, const CompilerTarget& target) { - return target.supports(operation) != true; + return !target.supports(operation); +} + +/// Normalize relative phase effects and discard only the unobservable global +/// phase of an entry point when the target cannot represent it. +static LogicalResult prepareGlobalPhases(ModuleOp moduleOp, + const CompilerTarget& target) { + if (failed(mqt::normalizeGlobalPhases(moduleOp))) { + return failure(); + } + SmallVector emptyControls; + moduleOp.walk([&](CtrlOp op) { + if (llvm::hasSingleElement(*op.getBody())) { + emptyControls.push_back(op); + } + }); + IRRewriter rewriter(moduleOp.getContext()); + for (auto op : llvm::reverse(emptyControls)) { + rewriter.replaceOp(op, op.getOperands()); + } + if (target.supportsOperation("gphase", 0, 1)) { + return success(); + } + auto entryPoint = mqt::getEntryPoint(moduleOp); + if (!entryPoint) { + return success(); + } + for (auto& block : entryPoint.getBody()) { + for (auto phase : llvm::make_early_inc_range(block.getOps())) { + phase.erase(); + } + } + return success(); } namespace { @@ -456,18 +489,14 @@ struct TargetNativeSynthesisPass final return; } ModuleOp moduleOp = getOperation(); - const auto plan = planTargetSynthesis(moduleOp, target); - if (plan.firstNeed == nullptr) { + if (failed(prepareGlobalPhases(moduleOp, target))) { + signalPassFailure(); return; } - if (target.nativeOperationsKind() == - CompilerTarget::NativeOperations::Kind::Unknown) { - plan.firstNeed->emitError() - << "target-native synthesis requires known native operations"; - signalPassFailure(); + const auto plan = planTargetSynthesis(moduleOp, target); + if (plan.firstNeed == nullptr) { return; } - const auto targetBasis = target.synthesisBasis(); if (!targetBasis) { plan.firstNeed->emitError() @@ -491,7 +520,7 @@ struct TargetNativeSynthesisPass final lowerTargetOperation(rewriter, cast(operation), *targetBasis); } - if (failed(mlir::mqt::normalizeGlobalPhases(moduleOp))) { + if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); } } @@ -551,13 +580,7 @@ struct VerifyTargetConformancePass final return WalkResult::advance(); } - const auto support = target.supports(operation); - if (!support) { - operation->emitError() - << "target conformance requires known native operations"; - return WalkResult::interrupt(); - } - if (*support) { + if (target.supports(operation)) { return WalkResult::advance(); } diff --git a/mlir/unittests/Compiler/CMakeLists.txt b/mlir/unittests/Compiler/CMakeLists.txt index 122f4ff745..398f6489b3 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -32,6 +32,7 @@ target_compile_definitions( mqt-core-mlir-unittests-compiler PRIVATE MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/heterogeneous-sc.json" + MQT_CORE_MLIR_HIGHER_ARITY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/higher-arity-sc.json" MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-one-way-sc.json" MQT_CORE_MLIR_DIRECTIONAL_TWO_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-two-way-sc.json" ) diff --git a/mlir/unittests/Compiler/Inputs/higher-arity-sc.json b/mlir/unittests/Compiler/Inputs/higher-arity-sc.json new file mode 100644 index 0000000000..0a47d7dd10 --- /dev/null +++ b/mlir/unittests/Compiler/Inputs/higher-arity-sc.json @@ -0,0 +1,32 @@ +{ + "schema-version": 1, + "name": "Higher-arity SC Test Device", + "numQubits": 3, + "durationUnit": { + "unit": "ns", + "scaleFactor": 1.0 + }, + "qubitProperties": { + "defaults": {}, + "overrides": [] + }, + "couplings": [ + [0, 1], + [1, 2] + ], + "operations": [ + { + "name": "ccnot", + "numQubits": 3, + "numParameters": 0, + "sites": [ + [0, 1, 2], + [0, 2, 1], + [1, 0, 2], + [1, 2, 0], + [2, 0, 1], + [2, 1, 0] + ] + } + ] +} diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index a74cc54fe1..f6efd603a2 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -1541,14 +1541,14 @@ TEST_F(CompilerPipelineTest, QCOProgramMergesDynamicRunInNativeCtrlBody) { llvm::cantFail(Operation::create("sx", 1, 0)), llvm::cantFail(Operation::create("rz", 1, 1)), llvm::cantFail(Operation::create("cz", 2, 0)), - llvm::cantFail(Operation::create("ctrl", 2, 0)), + llvm::cantFail(Operation::create("u", Operation::Arity::variadic(1), 3)), }; const auto target = llvm::cantFail(CompilerTarget::create( 2, CompilerTarget::Connectivity::allToAll(), CompilerTarget::NativeOperations::fromOperations(operations))); ASSERT_TRUE(target.synthesisBasis()); ASSERT_EQ(target.synthesisBasis()->singleQubit, - CompilerTarget::SingleQubitBasis::ZSXX); + CompilerTarget::SingleQubitBasis::U); auto program = QCOProgram::fromMLIRString(source); ASSERT_TRUE(program); @@ -1626,7 +1626,7 @@ reset q[0]; h q[1]; )"; const auto target = llvm::cantFail( - CompilerTarget::create(3, CompilerTarget::Connectivity{}, + CompilerTarget::create(3, CompilerTarget::Connectivity::allToAll(), CompilerTarget::NativeOperations::unrestricted())); auto qc = QCProgram::fromQASMString(source); @@ -1770,7 +1770,9 @@ h q; CompilerInput{std::move(*customPipelineInput)}, ProgramFormat::QCO, nullptr, "builtin.module(merge-single-qubit-rotation-gates)")); - const auto target = llvm::cantFail(CompilerTarget::create(1)); + const auto target = llvm::cantFail( + CompilerTarget::create(1, CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::unrestricted())); auto targetedImport = QCProgram::fromQASMString(qasm); auto targetedRawQCO = QCProgram::fromQASMString(qasm); auto targetedJeff = QCProgram::fromQASMString(qasm); diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 8a30417f42..307ab095fe 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -20,7 +20,9 @@ #include #include +#include #include +#include using mlir::CompilerTarget; @@ -90,6 +92,30 @@ TEST(CompilerQDMIAdapterTest, InfersDDSIMTargetFacts) { CompilerTarget::Connectivity::Kind::AllToAll); EXPECT_EQ(target.nativeOperationsKind(), CompilerTarget::NativeOperations::Kind::Explicit); + const auto& gphase = findOperation(target, "gphase"); + EXPECT_EQ(gphase.arity().kind(), + CompilerTarget::Operation::Arity::Kind::Fixed); + EXPECT_EQ(gphase.arity().value(), 0); + for (const auto [name, minimum] : + std::initializer_list>{{"id", 1}, + {"h", 1}, + {"rx", 1}, + {"swap", 2}, + {"rxx", 2}, + {"rccx", 3}}) { + const auto& operation = findOperation(target, name); + EXPECT_EQ(operation.arity().kind(), + CompilerTarget::Operation::Arity::Kind::Variadic) + << name.str(); + EXPECT_EQ(operation.arity().value(), minimum) << name.str(); + EXPECT_TRUE( + target.supportsOperation(name, minimum, operation.numParameters())) + << name.str(); + EXPECT_TRUE( + target.supportsOperation(name, minimum + 4, operation.numParameters())) + << name.str(); + } + EXPECT_TRUE(target.supportsOperation("gphase", 0, 1)); EXPECT_EQ(target.supportsOperation("h", 1, 0), true); EXPECT_EQ(target.supportsOperation("cx", 2, 0), true); EXPECT_EQ(target.supportsOperation("cswap", 3, 0), true); @@ -120,7 +146,17 @@ TEST(CompilerQDMIAdapterTest, RejectsNonhomogeneousOperationSupport) { ASSERT_FALSE(target); const auto message = llvm::toString(target.takeError()); EXPECT_NE(message.find("homogeneous"), std::string::npos); - EXPECT_NE(message.find("every topology edge"), std::string::npos); + EXPECT_NE(message.find("all topology edges"), std::string::npos); +} + +TEST(CompilerQDMIAdapterTest, SnapshotsHomogeneousHigherArityOperation) { + qdmi::DeviceSessionConfig overrides; + overrides.deviceConfiguration = + qdmi::FileDeviceConfiguration{MQT_CORE_MLIR_HIGHER_ARITY_SC_CONFIG}; + const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); + const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); + + EXPECT_TRUE(target.supportsOperation("ccnot", 3, 0)); } TEST(CompilerQDMIAdapterTest, RejectsDirectionalOperationWithoutReverseSites) { diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index f11c0cf2b9..96ce991856 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -55,6 +55,7 @@ using Coupling = Target::Coupling; using DurationUnit = Target::DurationUnit; using GateKind = Target::GateKind; using Operation = Target::Operation; +using Arity = Operation::Arity; using NativeOperations = Target::NativeOperations; using Site = Target::Site; using SiteId = Target::SiteId; @@ -95,7 +96,7 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { EXPECT_EQ(target.sites()[0].t2(), 80); EXPECT_EQ(target.operations()[0].name(), " PRX "); EXPECT_EQ(target.operations()[0].canonicalName(), "r"); - EXPECT_EQ(target.operations()[0].arity(), 1); + EXPECT_EQ(target.operations()[0].arity(), Arity::fixed(1)); EXPECT_EQ(target.operations()[0].numParameters(), 2); EXPECT_EQ(target.operations()[0].duration(), 0); EXPECT_EQ(target.operations()[0].fidelity(), 0.97); @@ -109,9 +110,11 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { } TEST(CompilerTargetTest, ConstructsDenseUnnamedAllToAllTarget) { - const auto target = valid(Target::create(3, Connectivity::allToAll())); + const auto target = valid(Target::create(3, Connectivity::allToAll(), + NativeOperations::unrestricted())); const auto named = - valid(Target::create("simulator", 2, Connectivity::allToAll())); + valid(Target::create("simulator", 2, Connectivity::allToAll(), + NativeOperations::unrestricted())); EXPECT_FALSE(target.name()); ASSERT_TRUE(named.name()); @@ -137,6 +140,26 @@ TEST(CompilerTargetTest, ConstructsDenseUnnamedAllToAllTarget) { EXPECT_EQ(neighbours, (std::vector{0, 2})); } +TEST(CompilerTargetTest, ModelsFixedAndVariadicOperationArities) { + const auto zero = Arity::fixed(0); + EXPECT_EQ(zero.kind(), Arity::Kind::Fixed); + EXPECT_EQ(zero.value(), 0U); + EXPECT_TRUE(zero.accepts(0)); + EXPECT_FALSE(zero.accepts(1)); + + const auto fixed = Arity::fixed(2); + EXPECT_FALSE(fixed.accepts(1)); + EXPECT_TRUE(fixed.accepts(2)); + EXPECT_FALSE(fixed.accepts(3)); + + const auto variadic = Arity::variadic(2); + EXPECT_EQ(variadic.kind(), Arity::Kind::Variadic); + EXPECT_EQ(variadic.value(), 2U); + EXPECT_FALSE(variadic.accepts(1)); + EXPECT_TRUE(variadic.accepts(2)); + EXPECT_TRUE(variadic.accepts(7)); +} + TEST(CompilerTargetTest, PreservesFullNonnegativeSiteIdDomain) { constexpr auto maxSite = std::numeric_limits::max(); constexpr auto nextSite = maxSite - 1; @@ -162,7 +185,8 @@ TEST(CompilerTargetTest, CanonicalizesConnectedTopologyAndCachesDistances) { valid(Site::create(11))}; const auto target = valid(Target::create( std::move(sites), - Connectivity::fromCouplings({{11, 2}, {2, 11}, {7, 2}, {2, 7}}))); + Connectivity::fromCouplings({{11, 2}, {2, 11}, {7, 2}, {2, 7}}), + NativeOperations::unrestricted())); EXPECT_EQ(target.connectivityKind(), Connectivity::Kind::Explicit); EXPECT_EQ(target.couplings(), (llvm::ArrayRef{{2, 7}, {2, 11}})); @@ -183,11 +207,14 @@ TEST(CompilerTargetTest, CanonicalizesConnectedTopologyAndCachesDistances) { } TEST(CompilerTargetTest, RejectsInvalidMetadata) { - expectInvalid(Target::create(0), + expectInvalid(Target::create(0, Connectivity::allToAll(), + NativeOperations::unrestricted()), "Compiler target must contain at least one site"); if constexpr (sizeof(size_t) >= sizeof(uint64_t)) { expectInvalid( - Target::create(std::numeric_limits::max()), + Target::create(std::numeric_limits::max(), + Connectivity::allToAll(), + NativeOperations::unrestricted()), "Compiler target site count exceeds the nonnegative i64 site domain"); } expectInvalid(Site::create(-1), @@ -215,8 +242,18 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { "Compiler target site-tuple fidelity must be finite and in [0, 1]"); expectInvalid(Operation::create("", 1, 0), "Compiler target operation name must not be empty"); - expectInvalid(Operation::create("x", 0, 0), - "Compiler target operation arity must be positive"); + expectInvalid(Operation::create("x", Arity::variadic(0), 0), + "Compiler target operation variadic minimum must be positive"); + expectInvalid( + Operation::create( + "gphase", Arity::fixed(0), 1, + std::vector{valid(SiteTuple::create(std::vector{}))}), + "Compiler target zero-arity operation cannot contain site tuples"); + expectInvalid( + Operation::create( + "h", Arity::variadic(1), 0, + std::vector{valid(SiteTuple::create(std::vector{0}))}), + "Compiler target variadic operation cannot contain site tuples"); expectInvalid( Operation::create("x", 1, 0, std::vector{valid(SiteTuple::create({0, 1}))}), @@ -230,56 +267,71 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { std::numeric_limits::quiet_NaN()), "Compiler target operation fidelity must be finite and in [0, 1]"); - expectInvalid(Target::create(std::vector{}), + expectInvalid(Target::create(std::vector{}, Connectivity::allToAll(), + NativeOperations::unrestricted()), "Compiler target must contain at least one site"); - expectInvalid(Target::create("", 1), + expectInvalid(Target::create("", 1, Connectivity::allToAll(), + NativeOperations::unrestricted()), "Compiler target name must not be empty when present"); - expectInvalid(Target::create("invalid", 0), + expectInvalid(Target::create("invalid", 0, Connectivity::allToAll(), + NativeOperations::unrestricted()), "Compiler target must contain at least one site"); - expectInvalid(Target::create(std::vector{valid(Site::create(1)), - valid(Site::create(1))}), + expectInvalid(Target::create( + std::vector{valid(Site::create(1)), valid(Site::create(1))}, + Connectivity::allToAll(), NativeOperations::unrestricted()), "Compiler target contains duplicate site IDs"); - expectInvalid( - Target::create(std::vector{valid(Site::create(0, std::nullopt, 1))}), - "Compiler target timing metadata requires a duration unit"); - expectInvalid(Target::create(1, {}, + expectInvalid(Target::create( + std::vector{valid(Site::create(0, std::nullopt, 1))}, + Connectivity::allToAll(), NativeOperations::unrestricted()), + "Compiler target timing metadata requires a duration unit"); + expectInvalid(Target::create(1, Connectivity::allToAll(), NativeOperations::fromOperations({valid( Operation::create("x", 1, 0, {}, 1))})), "Compiler target timing metadata requires a duration unit"); expectInvalid( Target::create( - 1, {}, + 1, Connectivity::allToAll(), NativeOperations::fromOperations({valid(Operation::create( "x", 1, 0, std::vector{valid(SiteTuple::create({0}, 1))}))})), "Compiler target timing metadata requires a duration unit"); - expectInvalid(Target::create(2, Connectivity::fromCouplings({{0, 0}})), + expectInvalid(Target::create(2, Connectivity::fromCouplings({{0, 0}}), + NativeOperations::unrestricted()), "Compiler target topology contains a self-coupling"); - expectInvalid(Target::create(2, Connectivity::fromCouplings({{0, 2}})), + expectInvalid(Target::create(2, Connectivity::fromCouplings({{0, 2}}), + NativeOperations::unrestricted()), "Compiler target topology references an unknown site"); - expectInvalid(Target::create(3, Connectivity::fromCouplings({{0, 1}})), + expectInvalid(Target::create(3, Connectivity::fromCouplings({{0, 1}}), + NativeOperations::unrestricted()), "Compiler target topology must be connected"); expectInvalid( Target::create( - 2, {}, + 2, Connectivity::allToAll(), NativeOperations::fromOperations({valid(Operation::create( "x", 1, 0, std::vector{valid(SiteTuple::create({2}))}))})), "Compiler target operation site tuple references an unknown site"); - expectInvalid(Target::create(1, {}, + expectInvalid(Target::create(1, Connectivity::allToAll(), NativeOperations::fromOperations( {valid(Operation::create("cx", 2, 0))})), "Compiler target operation arity exceeds its site count"); + expectInvalid( + Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations({valid( + Operation::create("h", Arity::variadic(3), 0))})), + "Compiler target operation variadic minimum exceeds its site count"); } -TEST(CompilerTargetTest, DistinguishesOperationKnowledge) { - const auto unknown = valid(Target::create(2)); - const auto unrestricted = - valid(Target::create(2, {}, NativeOperations::unrestricted())); - const auto closed = - valid(Target::create(2, {}, NativeOperations::fromOperations({}))); - - EXPECT_EQ(unknown.nativeOperationsKind(), NativeOperations::Kind::Unknown); - EXPECT_EQ(unknown.supportsOperation("x", 1), std::nullopt); - EXPECT_EQ(unknown.supports(GateKind::CX), std::nullopt); +TEST(CompilerTargetTest, DistinguishesOperationSupport) { + const auto unrestricted = valid(Target::create( + 2, Connectivity::allToAll(), NativeOperations::unrestricted())); + const auto closed = valid(Target::create( + 2, Connectivity::allToAll(), NativeOperations::fromOperations({}))); + const auto variadic = valid(Target::create( + 4, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("gphase", Arity::fixed(0), 1)), + valid(Operation::create("h", Arity::variadic(1), 0)), + valid(Operation::create("rxx", Arity::variadic(2), 1)), + valid(Operation::create("I", Arity::fixed(1), 0))}))); EXPECT_EQ(unrestricted.nativeOperationsKind(), NativeOperations::Kind::Unrestricted); @@ -287,7 +339,7 @@ TEST(CompilerTargetTest, DistinguishesOperationKnowledge) { EXPECT_EQ(unrestricted.supports(GateKind::CX), true); EXPECT_EQ(unrestricted.supportsOperation("", 1), false); EXPECT_EQ(unrestricted.supportsOperation(" ", 1), false); - EXPECT_EQ(unrestricted.supportsOperation("x", 0), false); + EXPECT_EQ(unrestricted.supportsOperation("gphase", 0), true); EXPECT_EQ(unrestricted.supportsOperation("x", 3), false); EXPECT_EQ(closed.nativeOperationsKind(), NativeOperations::Kind::Explicit); @@ -296,6 +348,19 @@ TEST(CompilerTargetTest, DistinguishesOperationKnowledge) { EXPECT_EQ(closed.supports(GateKind::CX), false); EXPECT_TRUE(closed.supportedGates().empty()); EXPECT_FALSE(closed.synthesisBasis()); + + EXPECT_TRUE(variadic.supportsOperation("gphase", 0, 1)); + EXPECT_FALSE(variadic.supportsOperation("gphase", 1, 1)); + EXPECT_FALSE(variadic.supportsOperation("h", 0, 0)); + EXPECT_TRUE(variadic.supportsOperation("h", 1, 0)); + EXPECT_TRUE(variadic.supportsOperation("h", 4, 0)); + EXPECT_FALSE(variadic.supportsOperation("h", 5, 0)); + EXPECT_FALSE(variadic.supportsOperation("rxx", 1, 1)); + EXPECT_TRUE(variadic.supportsOperation("rxx", 2, 1)); + EXPECT_TRUE(variadic.supportsOperation("rxx", 4, 1)); + EXPECT_FALSE(variadic.supportsOperation("rxx", 4, 0)); + EXPECT_TRUE(variadic.supportsOperation("id", 1, 0)); + EXPECT_TRUE(variadic.supportsOperation("i", 1, 0)); } TEST(CompilerTargetTest, PreservesCalibrationAndResolvesHomogeneousBasis) { @@ -367,6 +432,7 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { auto barrierResults = builder.barrier({q0, q1}); q0 = barrierResults[0]; q1 = barrierResults[1]; + std::tie(q0, q1) = builder.cz(q0, q1); builder.gphase(0.25); auto [measured, result] = builder.measure(q0); static_cast(result); @@ -379,6 +445,7 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { mlir::Operation* x = nullptr; mlir::Operation* cx = nullptr; + mlir::Operation* cz = nullptr; mlir::Operation* measure = nullptr; mlir::Operation* reset = nullptr; mlir::Operation* barrier = nullptr; @@ -386,8 +453,13 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { moduleOp->walk([&](mlir::Operation* operation) { if (mlir::isa(operation) && x == nullptr) { x = operation; - } else if (mlir::isa(operation)) { - cx = operation; + } else if (auto controlled = mlir::dyn_cast(operation)) { + auto* body = controlled.getBodyUnitary(0).getOperation(); + if (mlir::isa(body)) { + cx = operation; + } else if (mlir::isa(body)) { + cz = operation; + } } else if (mlir::isa(operation)) { measure = operation; } else if (mlir::isa(operation)) { @@ -400,6 +472,7 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { }); ASSERT_NE(x, nullptr); ASSERT_NE(cx, nullptr); + ASSERT_NE(cz, nullptr); ASSERT_NE(measure, nullptr); ASSERT_NE(reset, nullptr); ASSERT_NE(barrier, nullptr); @@ -410,26 +483,111 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { valid(SiteTuple::create({20, 10}))}; std::vector operations{ valid(Operation::create("x", 1, 0)), + valid(Operation::create("gphase", 0, 1)), valid(Operation::create("measure", 1, 0)), valid(Operation::create("reset", 1, 0)), - valid(Operation::create("cnot", 2, 0, std::move(directionalTuples)))}; - const auto target = valid(Target::create( - std::move(sites), {}, NativeOperations::fromOperations(operations))); + valid(Operation::create("cnot", 2, 0, std::move(directionalTuples))), + valid(Operation::create("cz", 2, 0))}; + const auto target = + valid(Target::create(std::move(sites), Connectivity::allToAll(), + NativeOperations::fromOperations(operations))); EXPECT_EQ(target.supports(x), true); EXPECT_EQ(target.supports(cx), true); + EXPECT_EQ(target.supports(cz), true); EXPECT_EQ(target.supports(measure), true); EXPECT_EQ(target.supports(reset), true); EXPECT_EQ(target.supports(barrier), true); EXPECT_EQ(target.supports(gphase), true); EXPECT_EQ(target.supports(nullptr), false); - const auto closed = - valid(Target::create(2, {}, NativeOperations::fromOperations({}))); + const auto closed = valid(Target::create( + 2, Connectivity::allToAll(), NativeOperations::fromOperations({}))); EXPECT_EQ(closed.supports(barrier), true); - EXPECT_EQ(closed.supports(gphase), true); + EXPECT_EQ(closed.supports(gphase), false); EXPECT_EQ(closed.supports(x), false); EXPECT_EQ(closed.supports(measure), false); } +TEST(CompilerTargetTest, SupportsArbitrarilyControlledBaseOperations) { + mlir::DialectRegistry registry; + registry.insert(); + mlir::MLIRContext context; + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + auto supportedModule = mlir::qco::QCOProgramBuilder::build( + &context, [](mlir::qco::QCOProgramBuilder& builder) { + static_cast( + builder.mch({builder.staticQubit(0), builder.staticQubit(1)}, + builder.staticQubit(2))); + static_cast( + builder.mcrx(0.25, {builder.staticQubit(3), builder.staticQubit(4)}, + builder.staticQubit(5))); + static_cast( + builder.mcrxx(0.5, {builder.staticQubit(6), builder.staticQubit(7)}, + builder.staticQubit(8), builder.staticQubit(9))); + static_cast( + builder.mcrccx({builder.staticQubit(10), builder.staticQubit(11)}, + builder.staticQubit(12), builder.staticQubit(13), + builder.staticQubit(14))); + return builder.intConstant(0); + }); + ASSERT_TRUE(supportedModule); + + std::vector supportedControls; + supportedModule->walk([&](mlir::qco::CtrlOp controlled) { + supportedControls.emplace_back(controlled.getOperation()); + }); + ASSERT_EQ(supportedControls.size(), 4U); + + const auto target = valid(Target::create( + 5, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("h", Arity::variadic(1), 0)), + valid(Operation::create("rx", Arity::variadic(1), 1)), + valid(Operation::create("rxx", Arity::variadic(2), 1)), + valid(Operation::create("rccx", Arity::variadic(3), 0))}))); + for (auto* controlled : supportedControls) { + EXPECT_TRUE(target.supports(controlled)); + } + + const auto fixedOnly = valid( + Target::create(5, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("h", Arity::fixed(3), 0))}))); + EXPECT_FALSE(fixedOnly.supports(supportedControls.front())); + + auto rejectedModule = mlir::qco::QCOProgramBuilder::build( + &context, [](mlir::qco::QCOProgramBuilder& builder) { + static_cast(builder.mch({}, builder.staticQubit(0))); + static_cast( + builder.ctrl({builder.staticQubit(1)}, + {builder.staticQubit(2), builder.staticQubit(3)}, + [&](mlir::ValueRange targets) { + return llvm::SmallVector{ + builder.h(targets[0]), builder.x(targets[1])}; + })); + static_cast( + builder.ctrl({builder.staticQubit(4)}, + {builder.staticQubit(5), builder.staticQubit(6)}, + [&](mlir::ValueRange targets) { + return llvm::SmallVector{ + builder.h(targets[0]), targets[1]}; + })); + return builder.intConstant(0); + }); + ASSERT_TRUE(rejectedModule); + + std::vector rejectedControls; + rejectedModule->walk([&](mlir::qco::CtrlOp controlled) { + rejectedControls.emplace_back(controlled.getOperation()); + }); + ASSERT_EQ(rejectedControls.size(), 3U); + for (auto* controlled : rejectedControls) { + EXPECT_FALSE(target.supports(controlled)); + } +} + } // namespace } // namespace mqt::test::compiler diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index d94492b724..b58699ed73 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -245,8 +245,9 @@ static CompilerTarget getSquareGridTarget(const size_t n) { } } - return llvm::cantFail(CompilerTarget::create( - numTarget, Connectivity::fromCouplings(couplings))); + return llvm::cantFail( + CompilerTarget::create(numTarget, Connectivity::fromCouplings(couplings), + NativeOperations::unrestricted())); } /// Creates an N-qubit GHZ state, where N = `qubits.size()` using @@ -483,7 +484,8 @@ TEST_F(MappingPassFixture, PlaceNoncontiguousTargetCompactly) { sites.emplace_back(llvm::cantFail(CompilerTarget::Site::create(19))); sites.emplace_back(llvm::cantFail(CompilerTarget::Site::create(42))); const auto target = llvm::cantFail( - CompilerTarget::create(std::move(sites), Connectivity::allToAll())); + CompilerTarget::create(std::move(sites), Connectivity::allToAll(), + NativeOperations::unrestricted())); QCOProgramBuilder builder(context.get()); builder.initialize({builder.getI1Type()}); @@ -514,7 +516,8 @@ TEST_F(MappingPassFixture, PlaceTensorOnFirstTargetSites) { llvm::cantFail(CompilerTarget::Site::create(42)), llvm::cantFail(CompilerTarget::Site::create(81))}; const auto target = llvm::cantFail(CompilerTarget::create( - std::move(sites), CompilerTarget::Connectivity::allToAll())); + std::move(sites), CompilerTarget::Connectivity::allToAll(), + NativeOperations::unrestricted())); QCOProgramBuilder builder(context.get()); builder.initialize({builder.getI1Type(), builder.getI1Type()}); @@ -554,7 +557,8 @@ TEST_F(MappingPassFixture, PlaceTensorOnFirstTargetSites) { } TEST_F(MappingPassFixture, RejectNonExplicitTopologyBeforeMutation) { - const auto target = llvm::cantFail(CompilerTarget::create(2)); + const auto target = llvm::cantFail(CompilerTarget::create( + 2, Connectivity::allToAll(), NativeOperations::unrestricted())); QCOProgramBuilder builder(context.get()); builder.initialize(); auto qubit = builder.h(builder.allocQubit()); @@ -575,7 +579,8 @@ TEST_F(MappingPassFixture, RejectNonExplicitTopologyBeforeMutation) { } TEST_F(MappingPassFixture, RejectOversizedPlacementBeforeMutation) { - const auto target = llvm::cantFail(CompilerTarget::create(1)); + const auto target = llvm::cantFail(CompilerTarget::create( + 1, Connectivity::allToAll(), NativeOperations::unrestricted())); QCOProgramBuilder builder(context.get()); builder.initialize(); auto first = builder.allocQubit(); @@ -607,7 +612,8 @@ TEST_F(MappingPassFixture, KeepWorkspaceSparseOnLargeTarget) { } const auto target = llvm::cantFail(CompilerTarget::create( - numTargetQubits, Connectivity::fromCouplings(couplings))); + numTargetQubits, Connectivity::fromCouplings(couplings), + NativeOperations::unrestricted())); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(2, builder.getI1Type())); @@ -638,54 +644,6 @@ TEST_F(MappingPassFixture, KeepWorkspaceSparseOnLargeTarget) { EXPECT_EQ(numSinks, numStatics); } -TEST_F(MappingPassFixture, - UnknownConnectivityRejectsMultiSiteUnitaryBeforeMutation) { - QCOProgramBuilder builder(context.get()); - builder.initialize(); - SmallVector qubits{builder.allocQubit(), builder.allocQubit()}; - qubits = builder.barrier(qubits); - Value condition; - std::tie(qubits[0], condition) = builder.measure(qubits[0]); - SmallVector controlled{qubits[0]}; - controlled = builder.qcoIf( - condition, controlled, - [&](ValueRange args) { - return SmallVector{builder.x(args.front())}; - }, - [&](ValueRange args) { - return SmallVector{builder.h(args.front())}; - }); - builder.sink(controlled.front()); - builder.sink(qubits[1]); - auto moduleOp = builder.finalize(); - const auto target = llvm::cantFail(CompilerTarget::create(2)); - - EXPECT_TRUE(succeeded(runPlacement(moduleOp.get(), target))); - EXPECT_TRUE(succeeded(verify(*moduleOp))); - - QCOProgramBuilder twoQubitBuilder(context.get()); - twoQubitBuilder.initialize(); - auto first = twoQubitBuilder.allocQubit(); - auto second = twoQubitBuilder.allocQubit(); - std::tie(first, second) = twoQubitBuilder.cx(first, second); - twoQubitBuilder.sink(first); - twoQubitBuilder.sink(second); - auto twoQubitModule = twoQubitBuilder.finalize(); - const auto before = printModule(twoQubitModule.get()); - - std::string diagnostics; - ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { - diagnostics += diagnostic.str(); - return success(); - }); - EXPECT_TRUE(failed(runPlacement(twoQubitModule.get(), target))); - EXPECT_EQ(printModule(twoQubitModule.get()), before); - EXPECT_TRUE(StringRef(diagnostics) - .contains("target placement requires known connectivity for " - "an operation with " - "arity 2")); -} - TEST_P(MappingPassTest, FailNoEntryPoint) { const auto& target = GetParam(); 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 596cd27490..28d3e660a4 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 "dd/DDDefinitions.hpp" #include "dd/Package.hpp" #include "mlir/Compiler/Target.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -56,6 +57,7 @@ namespace mqt::test::qco { using Target = mlir::CompilerTarget; +using Connectivity = Target::Connectivity; using NativeOperations = Target::NativeOperations; using Operation = Target::Operation; using Site = Target::Site; @@ -64,7 +66,9 @@ using mlir::OwningOpRef; using mlir::Value; using mlir::ValueRange; using mlir::qco::CtrlOp; +using mlir::qco::GPhaseOp; using mlir::qco::HOp; +using mlir::qco::POp; using mlir::qco::QCOProgramBuilder; using mlir::qco::RXXOp; using mlir::qco::RYOp; @@ -155,8 +159,9 @@ makeUCxTarget(std::optional> sites = std::nullopt) { sites = std::vector{valid(Site::create(0)), valid(Site::create(1))}; } std::vector operations{valid(Operation::create("u", 1, 3)), - valid(Operation::create("cx", 2, 0))}; - return valid(Target::create(std::move(*sites), {}, + valid(Operation::create("cx", 2, 0)), + valid(Operation::create("gphase", 0, 1))}; + return valid(Target::create(std::move(*sites), Connectivity::allToAll(), NativeOperations::fromOperations(operations))); } @@ -232,8 +237,8 @@ class TargetSynthesisTest : public testing::Test { } // namespace TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { - const auto target = - valid(Target::create(2, {}, NativeOperations::unrestricted())); + const auto target = valid(Target::create(2, Connectivity::allToAll(), + NativeOperations::unrestricted())); auto fusion = mlir::qco::createFuseTwoQubitGates(); auto synthesis = mlir::qco::createTargetNativeSynthesis(target); auto conformance = mlir::qco::createVerifyTargetConformance(target); @@ -503,7 +508,7 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisPreservesNativeSwap) { return builder.intConstant(0); }); const auto swapTarget = - valid(Target::create(2, {}, + valid(Target::create(2, Connectivity::allToAll(), NativeOperations::fromOperations( {valid(Operation::create("swap", 2, 0))}))); ASSERT_FALSE(swapTarget.synthesisBasis()); @@ -517,6 +522,84 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisPreservesNativeSwap) { EXPECT_EQ(printModule(*module), before); } +TEST_F(TargetSynthesisTest, TargetNativeSynthesisPreservesNativeGlobalPhase) { + const auto phasedX = [](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + qubit = builder.x(qubit); + builder.gphase(0.25); + return builder.intConstant(0); + }; + auto expected = build(phasedX); + auto synthesized = build(phasedX); + const auto target = + valid(Target::create(1, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("x", 1, 0)), + valid(Operation::create("gphase", 0, 1))}))); + + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*synthesized), 1U); + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + expectEquivalent(expected, synthesized); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisDropsOnlyEntryPointGlobalPhase) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func private @helper() { + %helper_phase = arith.constant 0.5 : f64 + qco.gphase(%helper_phase) + return + } + func.func @main() { + %entry_phase = arith.constant 0.25 : f64 + qco.gphase(%entry_phase) + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + auto functions = llvm::to_vector(module->getOps()); + ASSERT_EQ(functions.size(), 2U); + mlir::mqt::setEntryPoint(functions[1]); + const auto target = valid(Target::create( + 1, Connectivity::allToAll(), NativeOperations::fromOperations({}))); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*module), 1U); + EXPECT_EQ(llvm::range_size(functions[0].getOps()), 1U); + EXPECT_EQ(llvm::range_size(functions[1].getOps()), 0U); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisPreservesControlledGlobalPhaseSemantics) { + const auto controlledPhase = [](QCOProgramBuilder& builder) { + auto control = builder.staticQubit(0); + control = builder.cgphase(0.25, control); + static_cast(control); + return builder.intConstant(0); + }; + auto expected = build(controlledPhase); + auto synthesized = build(controlledPhase); + const auto target = valid(Target::create( + 1, Connectivity::allToAll(), + NativeOperations::fromOperations({valid(Operation::create("p", 1, 1))}))); + + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*synthesized), 0U); + EXPECT_EQ(countOps(*synthesized), 0U); + EXPECT_EQ(countOps(*synthesized), 1U); + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + expectEquivalent(expected, synthesized); +} + TEST_F(TargetSynthesisTest, TargetNativeSynthesisUsesHomogeneousCapability) { const auto swap = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); @@ -527,10 +610,11 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisUsesHomogeneousCapability) { auto expected = build(swap); auto synthesized = build(swap); const auto target = - valid(Target::create(2, {}, + valid(Target::create(2, Connectivity::allToAll(), NativeOperations::fromOperations( {valid(Operation::create("u", 1, 3)), - valid(Operation::create("cz", 2, 0))}))); + valid(Operation::create("cz", 2, 0)), + valid(Operation::create("gphase", 0, 1))}))); ASSERT_TRUE(target.synthesisBasis()); ASSERT_EQ(target.synthesisBasis()->entangler, Target::GateKind::CZ); @@ -549,10 +633,11 @@ TEST_F(TargetSynthesisTest, auto module = build([](QCOProgramBuilder& builder) { auto qubit = builder.staticQubit(0); qubit = builder.h(qubit); + builder.gphase(0.25); return builder.intConstant(0); }); - const auto permissive = - valid(Target::create(1, {}, NativeOperations::unrestricted())); + const auto permissive = valid(Target::create( + 1, Connectivity::allToAll(), NativeOperations::unrestricted())); const auto before = printModule(*module); ASSERT_TRUE(mlir::succeeded( @@ -562,38 +647,6 @@ TEST_F(TargetSynthesisTest, EXPECT_EQ(printModule(*module), before); } -TEST_F(TargetSynthesisTest, UnknownOperationSetIsNeededOnlyForQuantumOps) { - const auto target = valid(Target::create(1)); - auto classical = - build([](QCOProgramBuilder& builder) { return builder.intConstant(0); }); - ASSERT_TRUE(mlir::succeeded( - runPass(*classical, mlir::qco::createTargetNativeSynthesis(target)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*classical, mlir::qco::createVerifyTargetConformance(target)))); - - const auto buildQuantum = [&] { - return build([](QCOProgramBuilder& builder) { - builder.sink(builder.h(builder.staticQubit(0))); - return builder.intConstant(0); - }); - }; - auto synthesisModule = buildQuantum(); - auto diagnostics = expectFailure( - *synthesisModule, mlir::qco::createTargetNativeSynthesis(target)); - EXPECT_NE(diagnostics.find( - "target-native synthesis requires known native operations"), - std::string::npos) - << diagnostics; - - auto conformanceModule = buildQuantum(); - diagnostics = expectFailure(*conformanceModule, - mlir::qco::createVerifyTargetConformance(target)); - EXPECT_NE( - diagnostics.find("target conformance requires known native operations"), - std::string::npos) - << diagnostics; -} - TEST_F(TargetSynthesisTest, NativePowShellHidesItsImplementationBody) { auto module = build([](QCOProgramBuilder& builder) { auto qubit = builder.staticQubit(0); @@ -602,7 +655,7 @@ TEST_F(TargetSynthesisTest, NativePowShellHidesItsImplementationBody) { return builder.intConstant(0); }); const auto powOnly = - valid(Target::create(1, {}, + valid(Target::create(1, Connectivity::allToAll(), NativeOperations::fromOperations( {valid(Operation::create("pow", 1, 1))}))); ASSERT_FALSE(powOnly.synthesisBasis()); @@ -617,7 +670,7 @@ TEST_F(TargetSynthesisTest, NativePowShellHidesItsImplementationBody) { TEST_F(TargetSynthesisTest, MissingBasisIsDiagnosedOnlyWhenLoweringIsNeeded) { const auto hOnly = valid(Target::create( - 1, {}, + 1, Connectivity::allToAll(), NativeOperations::fromOperations({valid(Operation::create("h", 1, 0))}))); ASSERT_FALSE(hOnly.synthesisBasis()); @@ -662,7 +715,7 @@ TEST_F(TargetSynthesisTest, SupportedRuntimeParameterizedGateStaysUntouched) { context.get()); ASSERT_TRUE(module); const auto target = - valid(Target::create(2, {}, + valid(Target::create(2, Connectivity::allToAll(), NativeOperations::fromOperations( {valid(Operation::create("u", 1, 3)), valid(Operation::create("rxx", 2, 1))}))); @@ -725,7 +778,8 @@ TEST_F(TargetSynthesisTest, TEST_F(TargetSynthesisTest, ConformanceUsesHomogeneousCapabilitiesAndValidatesSites) { const auto target = valid(Target::create( - std::vector{valid(Site::create(10)), valid(Site::create(20))}, {}, + std::vector{valid(Site::create(10)), valid(Site::create(20))}, + Connectivity::allToAll(), NativeOperations::fromOperations( {valid(Operation::create("cx", 2, 0))}))); ASSERT_FALSE(target.synthesisBasis()); @@ -756,7 +810,7 @@ TEST_F(TargetSynthesisTest, TEST_F(TargetSynthesisTest, ConformanceRejectsDynamicAllocations) { const auto target = valid(Target::create( - 1, {}, + 1, Connectivity::allToAll(), NativeOperations::fromOperations({valid(Operation::create("x", 1, 0))}))); const auto expectDynamicAllocationFailure = [&](OwningOpRef module) { @@ -792,7 +846,7 @@ TEST_F(TargetSynthesisTest, ConformanceRejectsQuantumFunctionInputs) { context.get()); ASSERT_TRUE(module); const auto target = valid(Target::create( - 1, {}, + 1, Connectivity::allToAll(), NativeOperations::fromOperations({valid(Operation::create("x", 1, 0))}))); const auto diagnostics = @@ -815,7 +869,8 @@ TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityAndParameters) { }; expectUnsupported( - valid(Target::create(std::vector{valid(Site::create(10))}, {}, + valid(Target::create(std::vector{valid(Site::create(10))}, + Connectivity::allToAll(), NativeOperations::fromOperations( {valid(Operation::create("x", 1, 0))}))), build([](QCOProgramBuilder& builder) { @@ -827,7 +882,8 @@ TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityAndParameters) { expectUnsupported( valid(Target::create( - std::vector{valid(Site::create(10)), valid(Site::create(20))}, {}, + std::vector{valid(Site::create(10)), valid(Site::create(20))}, + Connectivity::allToAll(), NativeOperations::fromOperations( {valid(Operation::create("x", 2, 0))}))), build([](QCOProgramBuilder& builder) { @@ -838,7 +894,8 @@ TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityAndParameters) { "'qco.x'", "arity 1 and 0 parameter(s)"); expectUnsupported( - valid(Target::create(std::vector{valid(Site::create(10))}, {}, + valid(Target::create(std::vector{valid(Site::create(10))}, + Connectivity::allToAll(), NativeOperations::fromOperations( {valid(Operation::create("rz", 1, 0))}))), build([](QCOProgramBuilder& builder) { @@ -858,7 +915,7 @@ TEST_F(TargetSynthesisTest, ConformanceChecksNonUnitaryCapabilities) { return builder.intConstant(0); }); const auto xOnly = valid(Target::create( - 1, {}, + 1, Connectivity::allToAll(), NativeOperations::fromOperations({valid(Operation::create("x", 1, 0))}))); const auto diagnostics = expectFailure(*module, mlir::qco::createVerifyTargetConformance(xOnly)); diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index d85e6c29d1..672ea83e19 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -61,8 +61,8 @@ class OutputFormat(enum.Enum): class CompilerTarget: """Immutable MLIR compiler target. - Connectivity and native-operation metadata distinguish unknown, - unrestricted, and explicitly enumerated support. + Every target has either all-to-all or explicitly enumerated connectivity and + either unrestricted or explicitly enumerated native-operation support. """ @overload @@ -70,8 +70,8 @@ class CompilerTarget: self, num_sites: int, *, - connectivity: CompilerTarget.Connectivity = ..., - native_operations: CompilerTarget.NativeOperations = ..., + connectivity: CompilerTarget.Connectivity, + native_operations: CompilerTarget.NativeOperations, duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload @@ -80,8 +80,8 @@ class CompilerTarget: name: str, num_sites: int, *, - connectivity: CompilerTarget.Connectivity = ..., - native_operations: CompilerTarget.NativeOperations = ..., + connectivity: CompilerTarget.Connectivity, + native_operations: CompilerTarget.NativeOperations, duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload @@ -89,8 +89,8 @@ class CompilerTarget: self, sites: Sequence[CompilerTarget.Site], *, - connectivity: CompilerTarget.Connectivity = ..., - native_operations: CompilerTarget.NativeOperations = ..., + connectivity: CompilerTarget.Connectivity, + native_operations: CompilerTarget.NativeOperations, duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @overload @@ -99,8 +99,8 @@ class CompilerTarget: name: str, sites: Sequence[CompilerTarget.Site], *, - connectivity: CompilerTarget.Connectivity = ..., - native_operations: CompilerTarget.NativeOperations = ..., + connectivity: CompilerTarget.Connectivity, + native_operations: CompilerTarget.NativeOperations, duration_unit: CompilerTarget.DurationUnit | None = None, ) -> None: ... @@ -156,13 +156,42 @@ class CompilerTarget: def fidelity(self) -> float | None: """The operation fidelity, if available.""" + class OperationArityKind(enum.Enum): + """How an operation capability accepts qubit widths.""" + + FIXED = 0 + + VARIADIC = 1 + + class OperationArity: + """Accepted operation qubit widths.""" + + @staticmethod + def fixed(value: int) -> CompilerTarget.OperationArity: + """Create an exact operation arity.""" + + @staticmethod + def variadic(minimum: int) -> CompilerTarget.OperationArity: + """Create an operation arity with an inclusive minimum. Operation construction requires a positive minimum.""" + + @property + def kind(self) -> CompilerTarget.OperationArityKind: + """The arity kind.""" + + @property + def value(self) -> int: + """The exact arity or inclusive variadic minimum.""" + + def accepts(self, width: int) -> bool: + """Whether this arity accepts a concrete width.""" + class Operation: """A homogeneous target-wide operation capability and its calibration.""" def __init__( self, name: str, - arity: int, + arity: int | CompilerTarget.OperationArity, num_parameters: int, site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, duration: int | None = None, @@ -177,8 +206,8 @@ class CompilerTarget: """The normalized compiler operation name.""" @property - def arity(self) -> int: - """The fixed operation arity.""" + def arity(self) -> CompilerTarget.OperationArity: + """The accepted operation arity.""" @property def num_parameters(self) -> int: @@ -258,64 +287,50 @@ class CompilerTarget: """The two-qubit entangler.""" class ConnectivityKind(enum.Enum): - """How target connectivity is known.""" + """The target connectivity model.""" - UNKNOWN = 0 + ALL_TO_ALL = 0 - ALL_TO_ALL = 1 - - EXPLICIT = 2 + EXPLICIT = 1 class Connectivity: - """A target connectivity claim.""" - - @overload - def __init__(self) -> None: - """Create an unknown connectivity claim.""" + """A target connectivity model.""" - @overload def __init__(self, couplings: Sequence[tuple[int, int]]) -> None: - """Create an explicit connectivity claim.""" + """Create an explicit connectivity model.""" @staticmethod def all_to_all() -> CompilerTarget.Connectivity: - """Create an all-to-all connectivity claim.""" + """Create an all-to-all connectivity model.""" @property def kind(self) -> CompilerTarget.ConnectivityKind: - """How the connectivity is known.""" + """The connectivity model.""" @property def couplings(self) -> list[tuple[int, int]]: """The explicit couplings, if present.""" class NativeOperationsKind(enum.Enum): - """How native target operations are known.""" + """The native-operation support model.""" - UNKNOWN = 0 + UNRESTRICTED = 0 - UNRESTRICTED = 1 - - EXPLICIT = 2 + EXPLICIT = 1 class NativeOperations: - """A native-operation claim.""" - - @overload - def __init__(self) -> None: - """Create an unknown native-operation claim.""" + """Native-operation support.""" - @overload def __init__(self, operations: Sequence[CompilerTarget.Operation]) -> None: - """Create an explicit native-operation claim.""" + """Create explicit native-operation support.""" @staticmethod def unrestricted() -> CompilerTarget.NativeOperations: - """Create an unrestricted native-operation claim.""" + """Create unrestricted native-operation support.""" @property def kind(self) -> CompilerTarget.NativeOperationsKind: - """How the native operations are known.""" + """The native-operation support model.""" @property def operations(self) -> list[CompilerTarget.Operation]: @@ -347,7 +362,7 @@ class CompilerTarget: @property def connectivity_kind(self) -> CompilerTarget.ConnectivityKind: - """How the target connectivity is known.""" + """The target connectivity model.""" @property def couplings(self) -> list[tuple[int, int]]: @@ -355,7 +370,7 @@ class CompilerTarget: @property def native_operations_kind(self) -> CompilerTarget.NativeOperationsKind: - """How the target native operations are known.""" + """The target native-operation support model.""" @property def operations(self) -> list[CompilerTarget.Operation]: @@ -369,8 +384,8 @@ class CompilerTarget: def synthesis_basis(self) -> CompilerTarget.SynthesisBasis | None: """A complete target-wide synthesis basis, if available.""" - def supports_operation(self, name: str, arity: int, num_parameters: int | None = None) -> bool | None: - """Whether the target supports an operation, or None if unknown.""" + def supports_operation(self, name: str, arity: int, num_parameters: int | None = None) -> bool: + """Whether the target supports an operation.""" class Program: """Base class for a typed MLIR compiler program. diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index da617e5033..15c309cfdc 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -74,61 +74,73 @@ struct OperationInfo { std::size_t numSites{}; std::size_t numParams{}; bool isVariadic = false; + bool supportsArbitraryPositiveControls = false; }; +constexpr auto ARBITRARY_POSITIVE_CONTROLS_METADATA = + "mqt.compiler-target.v1:arbitrary-positive-controls"; + +constexpr auto controllableOperation(const char* name, const size_t numSites, + const size_t numParams) -> OperationInfo { + return OperationInfo{.name = name, + .numSites = numSites, + .numParams = numParams, + .supportsArbitraryPositiveControls = true}; +} + constexpr std::array OPERATIONS{ OperationInfo{.name = "gphase", .numSites = 0, .numParams = 1}, - OperationInfo{.name = "i", .numSites = 1, .numParams = 0}, - OperationInfo{.name = "x", .numSites = 1, .numParams = 0}, + controllableOperation("i", 1, 0), + controllableOperation("x", 1, 0), OperationInfo{.name = "cx", .numSites = 2, .numParams = 0}, OperationInfo{.name = "ccx", .numSites = 3, .numParams = 0}, OperationInfo{ .name = "mcx", .numSites = 0, .numParams = 0, .isVariadic = true}, - OperationInfo{.name = "y", .numSites = 1, .numParams = 0}, + controllableOperation("y", 1, 0), OperationInfo{.name = "cy", .numSites = 2, .numParams = 0}, - OperationInfo{.name = "z", .numSites = 1, .numParams = 0}, + controllableOperation("z", 1, 0), OperationInfo{.name = "cz", .numSites = 2, .numParams = 0}, OperationInfo{.name = "ccz", .numSites = 3, .numParams = 0}, - OperationInfo{.name = "h", .numSites = 1, .numParams = 0}, + controllableOperation("h", 1, 0), OperationInfo{.name = "ch", .numSites = 2, .numParams = 0}, - OperationInfo{.name = "s", .numSites = 1, .numParams = 0}, + controllableOperation("s", 1, 0), OperationInfo{.name = "cs", .numSites = 2, .numParams = 0}, - OperationInfo{.name = "sdg", .numSites = 1, .numParams = 0}, + controllableOperation("sdg", 1, 0), OperationInfo{.name = "csdg", .numSites = 2, .numParams = 0}, - OperationInfo{.name = "t", .numSites = 1, .numParams = 0}, - OperationInfo{.name = "tdg", .numSites = 1, .numParams = 0}, - OperationInfo{.name = "sx", .numSites = 1, .numParams = 0}, + controllableOperation("t", 1, 0), + controllableOperation("tdg", 1, 0), + controllableOperation("sx", 1, 0), OperationInfo{.name = "csx", .numSites = 2, .numParams = 0}, - OperationInfo{.name = "sxdg", .numSites = 1, .numParams = 0}, - OperationInfo{.name = "r", .numSites = 1, .numParams = 2}, - OperationInfo{.name = "rx", .numSites = 1, .numParams = 1}, + controllableOperation("sxdg", 1, 0), + controllableOperation("r", 1, 2), + controllableOperation("rx", 1, 1), OperationInfo{.name = "crx", .numSites = 2, .numParams = 1}, - OperationInfo{.name = "ry", .numSites = 1, .numParams = 1}, + controllableOperation("ry", 1, 1), OperationInfo{.name = "cry", .numSites = 2, .numParams = 1}, - OperationInfo{.name = "rz", .numSites = 1, .numParams = 1}, + controllableOperation("rz", 1, 1), OperationInfo{.name = "crz", .numSites = 2, .numParams = 1}, - OperationInfo{.name = "p", .numSites = 1, .numParams = 1}, + controllableOperation("p", 1, 1), OperationInfo{.name = "cp", .numSites = 2, .numParams = 1}, OperationInfo{ .name = "mcp", .numSites = 0, .numParams = 1, .isVariadic = true}, OperationInfo{.name = "u1", .numSites = 1, .numParams = 1}, OperationInfo{.name = "cu1", .numSites = 2, .numParams = 1}, - OperationInfo{.name = "u2", .numSites = 1, .numParams = 2}, - OperationInfo{.name = "u", .numSites = 1, .numParams = 3}, + controllableOperation("u2", 1, 2), + controllableOperation("u", 1, 3), OperationInfo{.name = "u3", .numSites = 1, .numParams = 3}, OperationInfo{.name = "cu3", .numSites = 2, .numParams = 3}, - OperationInfo{.name = "swap", .numSites = 2, .numParams = 0}, + controllableOperation("swap", 2, 0), OperationInfo{.name = "cswap", .numSites = 3, .numParams = 0}, - OperationInfo{.name = "iswap", .numSites = 2, .numParams = 0}, - OperationInfo{.name = "dcx", .numSites = 2, .numParams = 0}, - OperationInfo{.name = "ecr", .numSites = 2, .numParams = 0}, - OperationInfo{.name = "rxx", .numSites = 2, .numParams = 1}, - OperationInfo{.name = "ryy", .numSites = 2, .numParams = 1}, - OperationInfo{.name = "rzz", .numSites = 2, .numParams = 1}, - OperationInfo{.name = "rzx", .numSites = 2, .numParams = 1}, - OperationInfo{.name = "xx_minus_yy", .numSites = 2, .numParams = 2}, - OperationInfo{.name = "xx_plus_yy", .numSites = 2, .numParams = 2}, - OperationInfo{.name = "rccx", .numSites = 3, .numParams = 0}, + controllableOperation("iswap", 2, 0), + controllableOperation("dcx", 2, 0), + controllableOperation("ecr", 2, 0), + controllableOperation("rxx", 2, 1), + controllableOperation("ryy", 2, 1), + controllableOperation("rzz", 2, 1), + controllableOperation("rzx", 2, 1), + controllableOperation("xx_minus_yy", 2, 2), + controllableOperation("xx_plus_yy", 2, 2), + controllableOperation("rccx", 3, 0), OperationInfo{.name = "measure", .numSites = 1, .numParams = 0}, OperationInfo{.name = "reset", .numSites = 1, .numParams = 0}, OperationInfo{ @@ -333,7 +345,8 @@ auto MQT_DDSIM_QDMI_Device_Session_impl_d::queryOperationProperty( IS_INVALID_ARGUMENT(prop, QDMI_OPERATION_PROPERTY)) { return QDMI_ERROR_INVALIDARGUMENT; } - const auto& [name_, numSites_, numParams_, isVariadic] = + const auto& [name_, numSites_, numParams_, isVariadic, + supportsArbitraryPositiveControls] = *reinterpret_cast(operation); ADD_STRING_PROPERTY(QDMI_OPERATION_PROPERTY_NAME, name_, prop, size, value, sizeRet) @@ -348,6 +361,11 @@ auto MQT_DDSIM_QDMI_Device_Session_impl_d::queryOperationProperty( numParams_, prop, size, value, sizeRet) ADD_SINGLE_VALUE_PROPERTY(QDMI_OPERATION_PROPERTY_FIDELITY, double, 1.0, prop, size, value, sizeRet) + if (supportsArbitraryPositiveControls) { + ADD_STRING_PROPERTY(QDMI_OPERATION_PROPERTY_CUSTOM1, + ARBITRARY_POSITIVE_CONTROLS_METADATA, prop, size, value, + sizeRet) + } return QDMI_ERROR_NOTSUPPORTED; } auto MQT_DDSIM_QDMI_Device_Job_impl_d::free() -> void { diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index aac5773d5c..970e679afe 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -457,18 +457,28 @@ def test_compiler_target_constructors_preserve_python_api() -> None: ] site_tuple = CompilerTarget.SiteTuple([10, 20], duration=10, fidelity=0.99) operation = CompilerTarget.Operation("cx", 2, 0, site_tuples=[site_tuple], duration=20, fidelity=0.98) + fixed_zero = CompilerTarget.OperationArity.fixed(0) + variadic = CompilerTarget.OperationArity.variadic(2) + global_phase = CompilerTarget.Operation("gphase", fixed_zero, 1) + multi_controlled_x = CompilerTarget.Operation("x", variadic, 0) + connectivity = CompilerTarget.Connectivity.all_to_all() + unrestricted = CompilerTarget.NativeOperations.unrestricted() targets = [ - CompilerTarget(2, duration_unit=duration_unit), - CompilerTarget("dense", 2, duration_unit=duration_unit), + CompilerTarget(2, connectivity=connectivity, native_operations=unrestricted, duration_unit=duration_unit), + CompilerTarget( + "dense", 2, connectivity=connectivity, native_operations=unrestricted, duration_unit=duration_unit + ), CompilerTarget( sites, + connectivity=connectivity, native_operations=CompilerTarget.NativeOperations([operation]), duration_unit=duration_unit, ), CompilerTarget( "sparse", sites, + connectivity=connectivity, native_operations=CompilerTarget.NativeOperations([operation]), duration_unit=duration_unit, ), @@ -483,22 +493,53 @@ def test_compiler_target_constructors_preserve_python_api() -> None: assert site_tuple.sites == [10, 20] assert len(operation.site_tuples) == 1 assert operation.site_tuples[0].sites == [10, 20] + assert operation.arity.kind == CompilerTarget.OperationArityKind.FIXED + assert operation.arity.value == 2 + assert global_phase.arity.kind == CompilerTarget.OperationArityKind.FIXED + assert global_phase.arity.value == 0 + assert fixed_zero.accepts(0) + assert not fixed_zero.accepts(1) + assert multi_controlled_x.arity.kind == CompilerTarget.OperationArityKind.VARIADIC + assert multi_controlled_x.arity.value == 2 + assert not variadic.accepts(1) + assert variadic.accepts(2) + assert variadic.accepts(5) assert duration_unit.unit == "ns" def test_compiler_target_construction_preserves_validation_errors() -> None: """Translate explicit C++ construction errors to Python ``ValueError``.""" + with pytest.raises(TypeError): + CompilerTarget(1) # ty: ignore[no-matching-overload] for _ in range(2): with pytest.raises(ValueError, match="must contain at least one site"): - CompilerTarget(0) + CompilerTarget( + 0, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations.unrestricted(), + ) with pytest.raises(ValueError, match="site ID must be nonnegative"): CompilerTarget.Site(-1) with pytest.raises(ValueError, match="contains a duplicate site"): CompilerTarget.SiteTuple([0, 0]) with pytest.raises(ValueError, match="duration unit must not be empty"): CompilerTarget.DurationUnit("", 1.0) - with pytest.raises(ValueError, match="operation arity must be positive"): - CompilerTarget.Operation("x", 0, 0) + with pytest.raises(ValueError, match="zero-arity operation cannot contain site tuples"): + CompilerTarget.Operation( + "gphase", + CompilerTarget.OperationArity.fixed(0), + 1, + site_tuples=[CompilerTarget.SiteTuple([])], + ) + with pytest.raises(ValueError, match="variadic minimum must be positive"): + CompilerTarget.Operation("x", CompilerTarget.OperationArity.variadic(0), 0) + with pytest.raises(ValueError, match="variadic operation cannot contain site tuples"): + CompilerTarget.Operation( + "x", + CompilerTarget.OperationArity.variadic(2), + 0, + site_tuples=[CompilerTarget.SiteTuple([0, 1])], + ) def test_compiler_target_snapshots_qdmi_device(garnet_target: CompilerTarget) -> None: @@ -547,7 +588,7 @@ def _compiler_target_metadata(target: CompilerTarget) -> dict[str, object]: ( operation.name, operation.canonical_name, - operation.arity, + (operation.arity.kind, operation.arity.value), operation.num_parameters, operation.duration, operation.fidelity, diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 998bdebaa2..1bfb090603 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -2798,6 +2798,8 @@ def test_target_aware_qiskit_export_maps_sparse_site_ids() -> None: target = CompilerTarget( "sparse target", [CompilerTarget.Site(10), CompilerTarget.Site(4294967296)], + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations.unrestricted(), ) program = QCProgram.from_mlir_str( """module { @@ -2824,6 +2826,8 @@ def test_target_aware_qiskit_export_rejects_unknown_site() -> None: target = CompilerTarget( "sparse target", [CompilerTarget.Site(10), CompilerTarget.Site(20)], + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations.unrestricted(), ) program = QCProgram.from_mlir_str( """module { @@ -2856,7 +2860,11 @@ def test_target_aware_qiskit_export_rejects_unknown_site() -> None: ) def test_target_aware_qiskit_export_rejects_dynamic_qubits(allocation: str) -> None: """Require target-aware export inputs to use static qubits.""" - target = CompilerTarget(2) + target = CompilerTarget( + 2, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations.unrestricted(), + ) program = QCProgram.from_mlir_str( f"""module {{ func.func @main() attributes {{mqt.entry_point}} {{ diff --git a/test/qdmi/devices/dd/device_properties_test.cpp b/test/qdmi/devices/dd/device_properties_test.cpp index e922d1ea80..3bb84aa985 100644 --- a/test/qdmi/devices/dd/device_properties_test.cpp +++ b/test/qdmi/devices/dd/device_properties_test.cpp @@ -19,7 +19,9 @@ #include #include +#include #include +#include #include using testing::AnyOf; @@ -246,3 +248,64 @@ TEST(OperationProperties, BasicQueries) { } } } + +TEST(OperationProperties, ArbitraryPositiveControlsMetadata) { + constexpr std::string_view expectedMetadata = + "mqt.compiler-target.v1:arbitrary-positive-controls"; + const std::set expectedMarked{ + "i", "x", "y", "z", "h", "s", "sdg", "t", + "tdg", "sx", "sxdg", "r", "rx", "ry", "rz", "p", + "u2", "u", "swap", "iswap", "dcx", "ecr", "rxx", "ryy", + "rzz", "rzx", "xx_minus_yy", "xx_plus_yy", "rccx"}; + const std::set expectedUnmarked{ + "gphase", "cx", "ccx", "mcx", "cy", "cz", "ccz", + "ch", "cs", "csdg", "csx", "crx", "cry", "crz", + "cp", "mcp", "u1", "cu1", "u3", "cu3", "cswap", + "measure", "reset", "barrier", "if_else"}; + + const qdmi_test::SessionGuard s{}; + std::set marked{}; + std::set unmarked{}; + for (auto* const operation : qdmi_test::queryOperations(s.session)) { + size_t nameSize = 0; + ASSERT_EQ(MQT_DDSIM_QDMI_device_session_query_operation_property( + s.session, operation, 0, nullptr, 0, nullptr, + QDMI_OPERATION_PROPERTY_NAME, 0, nullptr, &nameSize), + QDMI_SUCCESS); + std::vector nameBuffer(nameSize); + ASSERT_EQ(MQT_DDSIM_QDMI_device_session_query_operation_property( + s.session, operation, 0, nullptr, 0, nullptr, + QDMI_OPERATION_PROPERTY_NAME, nameBuffer.size(), + nameBuffer.data(), nullptr), + QDMI_SUCCESS); + ASSERT_EQ(nameBuffer.back(), '\0'); + const std::string name{nameBuffer.data()}; + + size_t metadataSize = 0; + const auto rc = MQT_DDSIM_QDMI_device_session_query_operation_property( + s.session, operation, 0, nullptr, 0, nullptr, + QDMI_OPERATION_PROPERTY_CUSTOM1, 0, nullptr, &metadataSize); + if (rc == QDMI_ERROR_NOTSUPPORTED) { + unmarked.emplace(name); + continue; + } + + ASSERT_EQ(rc, QDMI_SUCCESS) << name; + ASSERT_EQ(metadataSize, expectedMetadata.size() + 1) << name; + std::vector metadata(metadataSize); + ASSERT_EQ(MQT_DDSIM_QDMI_device_session_query_operation_property( + s.session, operation, 0, nullptr, 0, nullptr, + QDMI_OPERATION_PROPERTY_CUSTOM1, metadata.size(), + metadata.data(), nullptr), + QDMI_SUCCESS) + << name; + EXPECT_EQ(metadata.back(), '\0') << name; + EXPECT_EQ(std::string_view(metadata.data(), metadata.size() - 1), + expectedMetadata) + << name; + marked.emplace(name); + } + + EXPECT_EQ(marked, expectedMarked); + EXPECT_EQ(unmarked, expectedUnmarked); +} From 1ad5f7261a8fa14a61ea8359e5366886571c9b24 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 1 Sep 2026 21:08:42 +0000 Subject: [PATCH 13/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20target=20?= =?UTF-8?q?synthesis=20invariants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rely on the QCO cleanup pipeline to canonicalize empty control modifiers before target synthesis. Keep target documentation consistent with the current Doxygen style and query an unused custom-property slot in generic DDSIM tests. Assisted-by: GPT-5.6 Sol via Codex --- CHANGELOG.md | 12 +- bindings/mlir/register_mlir.cpp | 28 ++-- mlir/include/mlir/Compiler/Target.h | 122 ++++++------------ .../NativeSynthesis/TargetSynthesis.cpp | 12 +- .../Compiler/test_compiler_pipeline.cpp | 75 +++-------- .../NativeSynthesis/test_target_synthesis.cpp | 25 ---- src/qdmi/devices/dd/Device.cpp | 5 +- test/qdmi/test_client.cpp | 2 +- 8 files changed, 78 insertions(+), 203 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dad4654bfc..1b2d9b513d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -868,7 +868,6 @@ for previous changelogs._ [#2315]: https://github.com/munich-quantum-toolkit/core/pull/2315 -[#2218]: https://github.com/munich-quantum-toolkit/core/pull/2218 [#2298]: https://github.com/munich-quantum-toolkit/core/pull/2298 [#2284]: https://github.com/munich-quantum-toolkit/core/pull/2284 [#2283]: https://github.com/munich-quantum-toolkit/core/pull/2283 @@ -880,17 +879,18 @@ for previous changelogs._ [#2257]: https://github.com/munich-quantum-toolkit/core/pull/2257 [#2249]: https://github.com/munich-quantum-toolkit/core/pull/2249 [#2246]: https://github.com/munich-quantum-toolkit/core/pull/2246 -[#2220]: https://github.com/munich-quantum-toolkit/core/pull/2220 [#2232]: https://github.com/munich-quantum-toolkit/core/pull/2232 [#2228]: https://github.com/munich-quantum-toolkit/core/pull/2228 [#2224]: https://github.com/munich-quantum-toolkit/core/pull/2224 -[#2209]: https://github.com/munich-quantum-toolkit/core/pull/2209 -[#2211]: https://github.com/munich-quantum-toolkit/core/pull/2211 +[#2220]: https://github.com/munich-quantum-toolkit/core/pull/2220 +[#2218]: https://github.com/munich-quantum-toolkit/core/pull/2218 [#2217]: https://github.com/munich-quantum-toolkit/core/pull/2217 -[#2210]: https://github.com/munich-quantum-toolkit/core/pull/2210 [#2216]: https://github.com/munich-quantum-toolkit/core/pull/2216 -[#2203]: https://github.com/munich-quantum-toolkit/core/pull/2203 [#2214]: https://github.com/munich-quantum-toolkit/core/pull/2214 +[#2211]: https://github.com/munich-quantum-toolkit/core/pull/2211 +[#2210]: https://github.com/munich-quantum-toolkit/core/pull/2210 +[#2209]: https://github.com/munich-quantum-toolkit/core/pull/2209 +[#2203]: https://github.com/munich-quantum-toolkit/core/pull/2203 [#2194]: https://github.com/munich-quantum-toolkit/core/pull/2194 [#2193]: https://github.com/munich-quantum-toolkit/core/pull/2193 [#2185]: https://github.com/munich-quantum-toolkit/core/pull/2185 diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index be157f5a09..5a59908085 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -183,9 +183,7 @@ template return std::move(program); } -/** - * @brief Check whether @p input unambiguously looks like source text. - */ +/// Check whether @p input unambiguously looks like source text. [[nodiscard]] static bool isSourceString(const std::string_view input) { auto source = input; while (!source.empty() && @@ -198,9 +196,7 @@ template std::isspace(static_cast(source[6])) != 0); } -/** - * @brief Construct a frontend program from a file path. - */ +/// Construct a frontend program from a file path. [[nodiscard]] static mlir::CompilerInput programFromPath(const std::filesystem::path& path) { if (path.empty()) { @@ -236,9 +232,7 @@ programFromPath(const std::filesystem::path& path) { "' has unsupported extension '" + extension + "'."); } -/** - * @brief Construct a frontend program from a string containing source or path. - */ +/// Construct a frontend program from a string containing source or path. [[nodiscard]] static mlir::CompilerInput programFromString(const std::string& input) { if (isSourceString(input)) { @@ -250,13 +244,11 @@ programFromString(const std::string& input) { return programFromPath(std::filesystem::path(input)); } -/** - * @brief Convert a Python object to a compiler program. - * - * @details Program objects are copied by default so the high-level entry point - * behaves like a conventional compiler function. Set @p inplace to transfer - * ownership from a program object instead. - */ +/// Convert a Python object to a compiler program. +/// +/// Program objects are copied by default so the high-level entry point +/// behaves like a conventional compiler function. Set @p inplace to transfer +/// ownership from a program object instead. [[nodiscard]] static mlir::CompilerInput programFromInput(const nb::object& program, const bool inplace) { if (nb::isinstance(program)) { @@ -299,9 +291,7 @@ programFromInput(const nb::object& program, const bool inplace) { " is not supported."); } -/** - * @brief Run the coordinated default pipeline and return a typed program. - */ +/// Run the coordinated default pipeline and return a typed program. [[nodiscard]] static mlir::CompilerProgram compileProgram(const nb::object& program, const mlir::ProgramFormat output, const bool inplace, const mlir::CompilerTarget* const target, diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index 4aaad1ecf1..3e5ee9288d 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -28,17 +28,15 @@ namespace mlir { class Operation; -/** - * @brief Immutable description of an MLIR compiler target. - * - * @details Hardware sites retain their target-defined nonnegative i64 - * identifiers. Routing algorithms use dense zero-based vertices in site order. - * Connectivity is either all-to-all or explicitly enumerated. Native-operation - * support is either unrestricted or explicitly enumerated. - * - * Compiler targets have shared immutable storage, making copies cheap while - * preserving validated topology and capability caches. - */ +/// Immutable description of an MLIR compiler target. +/// +/// Hardware sites retain their target-defined nonnegative i64 +/// identifiers. Routing algorithms use dense zero-based vertices in site order. +/// Connectivity is either all-to-all or explicitly enumerated. Native-operation +/// support is either unrestricted or explicitly enumerated. +/// +/// Compiler targets have shared immutable storage, making copies cheap while +/// preserving validated topology and capability caches. class CompilerTarget { public: using SiteId = int64_t; @@ -71,16 +69,12 @@ class CompilerTarget { llvm::SmallVector couplings_; }; - /** - * @brief Unit shared by all raw timing metadata on a target. - * - * @details A raw duration denotes `value * scaleFactor()` units. - */ + /// Unit shared by all raw timing metadata on a target. + /// + /// A raw duration denotes `value * scaleFactor()` units. class DurationUnit { public: - /** - * @brief Create a validated duration unit. - */ + /// Create a validated duration unit. [[nodiscard]] static llvm::Expected create(std::string unit, double scaleFactor); @@ -97,14 +91,10 @@ class CompilerTarget { double scaleFactor_; }; - /** - * @brief A hardware site and its optional target metadata. - */ + /// A hardware site and its optional target metadata. class Site { public: - /** - * @brief Create validated hardware-site metadata. - */ + /// Create validated hardware-site metadata. [[nodiscard]] static llvm::Expected create(SiteId id, std::optional name = std::nullopt, std::optional t1 = std::nullopt, @@ -132,14 +122,10 @@ class CompilerTarget { std::optional t2_; }; - /** - * @brief Calibration data for an ordered tuple of hardware sites. - */ + /// Calibration data for an ordered tuple of hardware sites. class SiteTuple { public: - /** - * @brief Create validated calibration data for a site tuple. - */ + /// Create validated calibration data for a site tuple. [[nodiscard]] static llvm::Expected create(std::vector sites, std::optional duration = std::nullopt, @@ -163,19 +149,15 @@ class CompilerTarget { std::optional fidelity_; }; - /** - * @brief An operation capability described by a target. - * - * @details The reported name is retained verbatim while - * @ref canonicalName contains its normalized compiler spelling. Operations - * are available throughout the target; site tuples carry optional - * site-specific calibration data only. - */ + /// An operation capability described by a target. + /// + /// The reported name is retained verbatim while + /// @ref canonicalName contains its normalized compiler spelling. Operations + /// are available throughout the target; site tuples carry optional + /// site-specific calibration data only. class Operation { public: - /** - * @brief The accepted number of qubits for an operation capability. - */ + /// The accepted number of qubits for an operation capability. class Arity { public: enum class Kind : uint8_t { Fixed, Variadic }; @@ -205,18 +187,14 @@ class CompilerTarget { size_t value_; }; - /** - * @brief Create a validated operation capability. - */ + /// Create a validated operation capability. [[nodiscard]] static llvm::Expected create(std::string name, size_t arity, size_t numParameters, std::vector siteTuples = {}, std::optional duration = std::nullopt, std::optional fidelity = std::nullopt); - /** - * @brief Create a validated operation capability. - */ + /// Create a validated operation capability. [[nodiscard]] static llvm::Expected create(std::string name, Arity arity, size_t numParameters, std::vector siteTuples = {}, @@ -285,9 +263,7 @@ class CompilerTarget { llvm::SmallVector operations_; }; - /** - * @brief Recognized native gate capability independent of synthesis code. - */ + /// Recognized native gate capability independent of synthesis code. enum class GateKind : uint8_t { U, X, @@ -306,9 +282,7 @@ class CompilerTarget { ECR, }; - /** - * @brief Recognized globally usable single-qubit synthesis basis. - */ + /// Recognized globally usable single-qubit synthesis basis. enum class SingleQubitBasis : uint8_t { U, ///< `U(theta, phi, lambda)`. ZSXX, ///< `RZ` / `SX` / `X` synthesis via a ZYZ decomposition. @@ -319,9 +293,7 @@ class CompilerTarget { ZXZ, ///< `RZ(phi) * RX(theta) * RZ(lambda)`. }; - /** - * @brief One single-qubit basis and entangler usable across the target. - */ + /// One single-qubit basis and entangler usable across the target. struct SynthesisBasis { SingleQubitBasis singleQubit; GateKind entangler; @@ -330,33 +302,25 @@ class CompilerTarget { const SynthesisBasis&) = default; }; - /** - * @brief Create an unnamed target with dense site IDs `0..numSites-1`. - */ + /// Create an unnamed target with dense site IDs `0..numSites-1`. [[nodiscard]] static llvm::Expected create(size_t numSites, Connectivity connectivity, NativeOperations nativeOperations, std::optional durationUnit = std::nullopt); - /** - * @brief Create a named target with dense site IDs `0..numSites-1`. - */ + /// Create a named target with dense site IDs `0..numSites-1`. [[nodiscard]] static llvm::Expected create(std::string name, size_t numSites, Connectivity connectivity, NativeOperations nativeOperations, std::optional durationUnit = std::nullopt); - /** - * @brief Create an unnamed target from detailed sites. - */ + /// Create an unnamed target from detailed sites. [[nodiscard]] static llvm::Expected create(std::vector sites, Connectivity connectivity, NativeOperations nativeOperations, std::optional durationUnit = std::nullopt); - /** - * @brief Create a named target from detailed sites. - */ + /// Create a named target from detailed sites. [[nodiscard]] static llvm::Expected create(std::string name, std::vector sites, Connectivity connectivity, NativeOperations nativeOperations, @@ -392,30 +356,20 @@ class CompilerTarget { /// Return the connectivity kind. [[nodiscard]] Connectivity::Kind connectivityKind() const noexcept; - /** - * @brief Return sorted canonical undirected couplings in target site IDs. - */ + /// Return sorted canonical undirected couplings in target site IDs. [[nodiscard]] llvm::ArrayRef couplings() const noexcept; - /** - * @brief Return whether two valid dense compiler vertices are adjacent. - */ + /// Return whether two valid dense compiler vertices are adjacent. [[nodiscard]] bool areAdjacent(size_t source, size_t target) const; - /** - * @brief Return the cached shortest-path distance between valid vertices. - */ + /// Return the cached shortest-path distance between valid vertices. [[nodiscard]] size_t distanceBetween(size_t source, size_t target) const; - /** - * @brief Invoke @p callback for every neighbour of a valid dense vertex. - */ + /// Invoke @p callback for every neighbour of a valid dense vertex. void forEachNeighbour(size_t vertex, llvm::function_ref callback) const; - /** - * @brief Return the maximum degree of the target's routing topology. - */ + /// Return the maximum degree of the target's routing topology. [[nodiscard]] size_t maxDegree() const noexcept; /// Return the native-operation support kind. diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index b8494834f9..2df7dd8655 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -50,7 +50,7 @@ using decomposition::emitUnitary2QWeyl; namespace { -/** Composed unitary and metadata for a fusable two-qubit run. */ +/// Composed unitary and metadata for a fusable two-qubit run. struct FusableTwoQubitRun { SmallVector ops; ///< Members in program order. Matrix4x4 composed = Matrix4x4::identity(); @@ -307,16 +307,6 @@ static LogicalResult prepareGlobalPhases(ModuleOp moduleOp, if (failed(mqt::normalizeGlobalPhases(moduleOp))) { return failure(); } - SmallVector emptyControls; - moduleOp.walk([&](CtrlOp op) { - if (llvm::hasSingleElement(*op.getBody())) { - emptyControls.push_back(op); - } - }); - IRRewriter rewriter(moduleOp.getContext()); - for (auto op : llvm::reverse(emptyControls)) { - rewriter.replaceOp(op, op.getOperands()); - } if (target.supportsOperation("gphase", 0, 1)) { return success(); } diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index f6efd603a2..229201642a 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -418,7 +418,7 @@ TEST(CompilerProgramOwnershipTest, EnforcesQCOLinearityAtPublicBoundaries) { ProgramFormat::QCO)); } -/** @brief Raw QCO stops before the registered default optimization pipeline. */ +/// Raw QCO stops before the registered default optimization pipeline. TEST_F(CompilerPipelineTest, RawAndOptimizedQCOAreDistinctCheckpoints) { const std::string qasm = R"(OPENQASM 3.0; include "stdgates.inc"; @@ -457,9 +457,7 @@ h q; EXPECT_FALSE(std::get(*result).str().empty()); } -/** - * @brief Test: typed programs transfer ownership between compiler dialects - */ +/// Test: typed programs transfer ownership between compiler dialects TEST_F(CompilerPipelineTest, TypedProgramsComposeWithoutImplicitCopies) { const std::string qasm = R"(OPENQASM 3.0; include "stdgates.inc"; @@ -996,9 +994,7 @@ INSTANTIATE_TEST_SUITE_P(OpenQASMPrograms, OpenQASMJeffBoundaryTest, } // namespace -/** - * @brief Test: typed programs import MLIR and OpenQASM from their public APIs - */ +/// Test: typed programs import MLIR and OpenQASM from their public APIs TEST_F(CompilerPipelineTest, TypedProgramImportsAndCopies) { const std::string mlir = R"(module { %0 = qc.alloc : !qc.qubit @@ -1040,9 +1036,7 @@ h q; EXPECT_FALSE(QCOProgram::fromMLIRString(mlir)); } -/** - * @brief Test: QCO imports require each linear value to have one use. - */ +/// Test: QCO imports require each linear value to have one use. TEST_F(CompilerPipelineTest, QCOProgramImportsEnforceLinearity) { const std::string valid = R"mlir(module { func.func @main() { @@ -1089,9 +1083,7 @@ TEST_F(CompilerPipelineTest, QCOProgramImportsEnforceLinearity) { EXPECT_FALSE(QCOProgram::fromMLIRFile(path)); } -/** - * @brief Test: typed programs emit OpenQASM directly and through the pipeline. - */ +/// Test: typed programs emit OpenQASM directly and through the pipeline. TEST_F(CompilerPipelineTest, TypedProgramsEmitOpenQASM) { const std::string qasm = R"(OPENQASM 3.1; include "stdgates.inc"; @@ -1160,9 +1152,7 @@ TEST_F(CompilerPipelineTest, TypedOpenQASMExportReportsUnsupportedQC) { EXPECT_FALSE(program->toOpenQASM3()); } -/** - * @brief Test: typed programs expose idempotent global-phase normalization. - */ +/// Test: typed programs expose idempotent global-phase normalization. TEST_F(CompilerPipelineTest, TypedProgramsNormalizeGlobalPhases) { const std::string qcSource = R"mlir(module { func.func @test(%q: !qc.qubit) { @@ -1204,9 +1194,7 @@ TEST_F(CompilerPipelineTest, TypedProgramsNormalizeGlobalPhases) { EXPECT_EQ(StringRef(textual->str()).count("qco.gphase"), 1); } -/** - * @brief Test: jeff programs round-trip through their binary APIs - */ +/// Test: jeff programs round-trip through their binary APIs TEST_F(CompilerPipelineTest, JeffProgramsRoundTripThroughBytesAndFiles) { const std::string qasm = R"(OPENQASM 3.0; include "stdgates.inc"; @@ -1247,9 +1235,7 @@ x q; EXPECT_FALSE(jeff.write(path.parent_path() / "missing" / "output.jeff")); } -/** - * @brief Test: QCO and QIR typed programs retain their respective semantics - */ +/// Test: QCO and QIR typed programs retain their respective semantics TEST_F(CompilerPipelineTest, QCOAndQIRProgramsImportCopyAndOptimize) { const std::string qasm = R"(OPENQASM 3.0; include "stdgates.inc"; @@ -1303,9 +1289,7 @@ h q; base->writeBitcode(bitcodePath.parent_path() / "missing" / "output.bc")); } -/** - * @brief Test: QCO program APIs configure and execute their associated passes. - */ +/// Test: QCO program APIs configure and execute their associated passes. TEST_F(CompilerPipelineTest, QCOProgramOptimizationAPIs) { const std::string qasm = R"(OPENQASM 3.0; include "stdgates.inc"; @@ -1338,9 +1322,7 @@ cx q[0], q[2]; EXPECT_EQ(loopProgram->str().find("scf.for"), std::string::npos); } -/** - * @brief Test: target compilation decomposes, maps, synthesizes, and verifies. - */ +/// Test: target compilation decomposes, maps, synthesizes, and verifies. TEST_F(CompilerPipelineTest, QCOProgramCompilesForTarget) { auto qc = QCProgram::fromQASMString(qasm::multipleControlledX); ASSERT_TRUE(qc); @@ -1570,9 +1552,7 @@ TEST_F(CompilerPipelineTest, QCOProgramMergesDynamicRunInNativeCtrlBody) { EXPECT_FALSE(main.getArgument(0).use_empty()); } -/** - * @brief Test: all-to-all target compilation uses compact placement. - */ +/// Test: all-to-all target compilation uses compact placement. TEST_F(CompilerPipelineTest, QCOProgramUsesCompactAllToAllPlacement) { const std::string qasm = R"(OPENQASM 3.0; include "stdgates.inc"; @@ -1614,9 +1594,7 @@ c = measure q; EXPECT_EQ(numSwaps, 0); } -/** - * @brief Test: target compilation retains unobserved quantum operations. - */ +/// Test: target compilation retains unobserved quantum operations. TEST_F(CompilerPipelineTest, QCOProgramPreservesUnobservedQuantumOperations) { constexpr llvm::StringLiteral source = R"(OPENQASM 3.0; include "stdgates.inc"; @@ -1651,9 +1629,7 @@ h q[1]; EXPECT_EQ(staticQubits, 2U); } -/** - * @brief Test: the default pipeline accepts an optional compiler target. - */ +/// Test: the default pipeline accepts an optional compiler target. TEST_F(CompilerPipelineTest, DefaultPipelineCompilesForTarget) { auto input = QCProgram::fromQASMString(qasm::multipleControlledX); ASSERT_TRUE(input); @@ -1692,9 +1668,7 @@ TEST_F(CompilerPipelineTest, DefaultPipelineCompilesForTarget) { EXPECT_TRUE(qir.llvmIR()); } -/** - * @brief Test: QCO programs expose the raw and composite qubit-reuse flows. - */ +/// Test: QCO programs expose the raw and composite qubit-reuse flows. TEST_F(CompilerPipelineTest, QCOProgramQubitReuseAPIs) { const auto countAllocations = [](const QCOProgram& program) { const auto ir = program.str(); @@ -1728,9 +1702,7 @@ TEST_F(CompilerPipelineTest, QCOProgramQubitReuseAPIs) { EXPECT_NE(compositeQCO->str().find("qco.reset"), std::string::npos); } -/** - * @brief Test: default compilation returns the requested typed program format - */ +/// Test: default compilation returns the requested typed program format TEST_F(CompilerPipelineTest, DefaultPipelineSelectsRequestedProgramFormats) { const std::string qasm = R"(OPENQASM 3.0; include "stdgates.inc"; @@ -1835,11 +1807,9 @@ h q; EXPECT_TRUE(std::holds_alternative(*fromJeff)); } -/** - * @brief Test: QCOProgram::decomposeMultiControlled runs the pass on MCX. - * - * @details Correctness of the decomposition is tested in a dedicated suite. - */ +/// Test: QCOProgram::decomposeMultiControlled runs the pass on MCX. +/// +/// Correctness of the decomposition is tested in a dedicated suite. TEST_F(CompilerPipelineTest, DecomposeMultiControlledPass) { auto module = mlir::qc::QCProgramBuilder::build( context.get(), mlir::qc::multipleControlledX); @@ -1988,9 +1958,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(mlir::qir::singleControlledXOnIndividualQubits), true, "reuse-qubits,mqt-qco-default"})); -/** - * @brief Test: gate counting respects modifiers and skips barriers. - */ +/// Test: gate counting respects modifiers and skips barriers. TEST_F(CompilerPipelineTest, QCProgramCountGates) { const std::string qasm = R"(OPENQASM 3.0; include "stdgates.inc"; @@ -2026,9 +1994,8 @@ TEST_F(CompilerPipelineTest, QCProgramCountGatesWithoutEntryPoint) { EXPECT_EQ(qc->numTwoQubitGates(), 0); } -/** - * @brief Test: gate counting includes each structured control-flow region once. - */ +/// Test: gate counting includes each structured control-flow region +/// once. TEST_F(CompilerPipelineTest, QCProgramCountGatesInStructuredControlFlow) { const std::string qasm = R"(OPENQASM 3.0; include "stdgates.inc"; 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 28d3e660a4..394011fea6 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -68,7 +68,6 @@ using mlir::ValueRange; using mlir::qco::CtrlOp; using mlir::qco::GPhaseOp; using mlir::qco::HOp; -using mlir::qco::POp; using mlir::qco::QCOProgramBuilder; using mlir::qco::RXXOp; using mlir::qco::RYOp; @@ -576,30 +575,6 @@ TEST_F(TargetSynthesisTest, EXPECT_EQ(llvm::range_size(functions[1].getOps()), 0U); } -TEST_F(TargetSynthesisTest, - TargetNativeSynthesisPreservesControlledGlobalPhaseSemantics) { - const auto controlledPhase = [](QCOProgramBuilder& builder) { - auto control = builder.staticQubit(0); - control = builder.cgphase(0.25, control); - static_cast(control); - return builder.intConstant(0); - }; - auto expected = build(controlledPhase); - auto synthesized = build(controlledPhase); - const auto target = valid(Target::create( - 1, Connectivity::allToAll(), - NativeOperations::fromOperations({valid(Operation::create("p", 1, 1))}))); - - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); - EXPECT_EQ(countOps(*synthesized), 0U); - EXPECT_EQ(countOps(*synthesized), 0U); - EXPECT_EQ(countOps(*synthesized), 1U); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); - expectEquivalent(expected, synthesized); -} - TEST_F(TargetSynthesisTest, TargetNativeSynthesisUsesHomogeneousCapability) { const auto swap = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 15c309cfdc..d453737d6d 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -8,9 +8,8 @@ * Licensed under the MIT License */ -/** @file Device.cpp - * @brief The MQT QDMI device implementation for its DD-based simulator. - */ +/// @file Device.cpp +/// The MQT QDMI device implementation for its DD-based simulator. #include "qdmi/devices/dd/Device.hpp" diff --git a/test/qdmi/test_client.cpp b/test/qdmi/test_client.cpp index 65b4f82a42..1e9d020d26 100644 --- a/test/qdmi/test_client.cpp +++ b/test/qdmi/test_client.cpp @@ -775,7 +775,7 @@ TEST_P(OperationTest, MeanShuttlingSpeed) { TEST_P(OperationTest, UnsupportedCustomPropertyReturnsNullopt) { for (const auto& operation : operations) { EXPECT_EQ(operation.queryCustomProperty>( - CustomProperty::Custom1), + CustomProperty::Custom2), std::nullopt); } } From 14e0132f1e47e55fd2587510d485fbac975162e3 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 2 Sep 2026 12:38:29 +0200 Subject: [PATCH 14/20] =?UTF-8?q?=F0=9F=90=9B=20Derive=20controlled=20gate?= =?UTF-8?q?s=20from=20variadic=20bases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Treat variadic X and Z capabilities as CX and CZ support in the gate cache. This keeps synthesis-basis resolution consistent with controlled-operation support. Assisted-by: GPT-5.6 Sol via Codex --- mlir/lib/Compiler/Target.cpp | 10 +++++++- .../Compiler/test_compiler_target.cpp | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index c9e4895444..2870c0c100 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -605,7 +605,15 @@ llvm::Error CompilerTarget::Storage::initialize() { } for (const auto& specification : GATE_SPECIFICATIONS) { - if (supportsOperation(specification.name, specification.arity, + const bool supportsControlledBase = + (specification.kind == GateKind::CX && + supportsVariadicOperation("x", specification.arity, + specification.numParameters)) || + (specification.kind == GateKind::CZ && + supportsVariadicOperation("z", specification.arity, + specification.numParameters)); + if (supportsControlledBase || + supportsOperation(specification.name, specification.arity, specification.numParameters)) { supportedGates.emplace_back(specification.kind); } diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 96ce991856..03b8d78349 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -415,6 +415,30 @@ TEST(CompilerTargetTest, ClassifiesEveryEntangler) { } } +TEST(CompilerTargetTest, DerivesControlledEntanglersFromVariadicBases) { + constexpr std::array bases{ + std::pair{std::string_view{"x"}, GateKind::CX}, + std::pair{std::string_view{"z"}, GateKind::CZ}, + }; + const auto globalU = valid(Operation::create("u", 1, 3)); + + for (const auto& [base, entangler] : bases) { + SCOPED_TRACE(base); + const auto variadic = + valid(Operation::create(std::string{base}, Arity::variadic(1), 0)); + const auto target = valid( + Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations({globalU, variadic}))); + + EXPECT_TRUE(target.supports(entangler)); + EXPECT_TRUE(llvm::is_contained(target.supportedGates(), entangler)); + ASSERT_TRUE(target.synthesisBasis()); + EXPECT_EQ(target.synthesisBasis()->singleQubit, + Target::SingleQubitBasis::U); + EXPECT_EQ(target.synthesisBasis()->entangler, entangler); + } +} + TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { mlir::DialectRegistry registry; registry.insert Date: Wed, 2 Sep 2026 12:39:14 +0200 Subject: [PATCH 15/20] =?UTF-8?q?=F0=9F=90=9B=20Remove=20empty=20controls?= =?UTF-8?q?=20after=20phase=20normalization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Global-phase normalization can leave an empty control shell beside the lowered phase operation. Remove that shell before target support planning so the standalone synthesis pass accepts P-only targets. Assisted-by: GPT-5.6 Sol via Codex --- .../NativeSynthesis/TargetSynthesis.cpp | 10 ++++++++ .../NativeSynthesis/test_target_synthesis.cpp | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 2df7dd8655..7297b90235 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -307,6 +307,16 @@ static LogicalResult prepareGlobalPhases(ModuleOp moduleOp, if (failed(mqt::normalizeGlobalPhases(moduleOp))) { return failure(); } + SmallVector emptyControls; + moduleOp.walk([&](CtrlOp op) { + if (llvm::hasSingleElement(*op.getBody())) { + emptyControls.push_back(op); + } + }); + IRRewriter rewriter(moduleOp.getContext()); + for (auto op : llvm::reverse(emptyControls)) { + rewriter.replaceOp(op, op.getOperands()); + } if (target.supportsOperation("gphase", 0, 1)) { return success(); } 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 394011fea6..28d3e660a4 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -68,6 +68,7 @@ using mlir::ValueRange; using mlir::qco::CtrlOp; using mlir::qco::GPhaseOp; using mlir::qco::HOp; +using mlir::qco::POp; using mlir::qco::QCOProgramBuilder; using mlir::qco::RXXOp; using mlir::qco::RYOp; @@ -575,6 +576,30 @@ TEST_F(TargetSynthesisTest, EXPECT_EQ(llvm::range_size(functions[1].getOps()), 0U); } +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisPreservesControlledGlobalPhaseSemantics) { + const auto controlledPhase = [](QCOProgramBuilder& builder) { + auto control = builder.staticQubit(0); + control = builder.cgphase(0.25, control); + static_cast(control); + return builder.intConstant(0); + }; + auto expected = build(controlledPhase); + auto synthesized = build(controlledPhase); + const auto target = valid(Target::create( + 1, Connectivity::allToAll(), + NativeOperations::fromOperations({valid(Operation::create("p", 1, 1))}))); + + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*synthesized), 0U); + EXPECT_EQ(countOps(*synthesized), 0U); + EXPECT_EQ(countOps(*synthesized), 1U); + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + expectEquivalent(expected, synthesized); +} + TEST_F(TargetSynthesisTest, TargetNativeSynthesisUsesHomogeneousCapability) { const auto swap = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); From 64bad12e52b376124f40cdaad5d2d7b273d37766 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 2 Sep 2026 13:42:25 +0200 Subject: [PATCH 16/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20empty=20contr?= =?UTF-8?q?ol=20cleanup=20into=20phase=20normalization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MQT/Transforms/NormalizeGlobalPhases.cpp | 118 +++++++++++------- .../NativeSynthesis/TargetSynthesis.cpp | 10 -- .../test_global_phase_normalization.cpp | 63 ++++++++++ 3 files changed, 133 insertions(+), 58 deletions(-) diff --git a/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp b/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp index ac28e2d7d9..886ce9c431 100644 --- a/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp +++ b/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp @@ -13,6 +13,7 @@ #include "mlir/Dialect/MQT/Utils/Angles.h" #include "mlir/Dialect/MQT/Utils/ConstantFolding.h" #include "mlir/Dialect/MQT/Utils/GatePowering.h" +#include "mlir/Dialect/MQT/Utils/Modifiers.h" #include "mlir/Dialect/MQT/Utils/Parameters.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -310,66 +311,87 @@ class GlobalPhaseNormalizer final { return phase; } + void eraseDeadBodyOps(Block& body) { + for (auto* op = body.getTerminator()->getPrevNode(); op != nullptr;) { + auto* previous = op->getPrevNode(); + if (!isa(op) && + isOpTriviallyDead(op)) { + rewriter.eraseOp(op); + } + op = previous; + } + } + [[nodiscard]] std::optional factorControl(qc::CtrlOp op) { auto phase = normalizeBlock(*op.getBody(), op); - if (!phase || op.getNumControls() == 0) { - return phase; + const bool releasePhase = phase.has_value() && op.getNumControls() == 0; + if (phase && !releasePhase && !phase->expression.isZero()) { + rewriter.setInsertionPoint(op); + auto angle = phase->expression.materialize(rewriter, phase->loc); + rewriter.setInsertionPointAfter(op); + if (op.getNumControls() == 1) { + qc::POp::create(rewriter, phase->loc, op.getControl(0), angle); + } else { + auto controls = op.getControls(); + qc::CtrlOp::create(rewriter, phase->loc, controls.drop_back(), + controls.back(), [&](Value target) { + qc::POp::create(rewriter, phase->loc, target, + angle); + }); + } } - if (phase->expression.isZero()) { - return std::nullopt; + if (phase) { + eraseDeadBodyOps(*op.getBody()); } - - rewriter.setInsertionPoint(op); - auto angle = phase->expression.materialize(rewriter, phase->loc); - rewriter.setInsertionPointAfter(op); - if (op.getNumControls() == 1) { - qc::POp::create(rewriter, phase->loc, op.getControl(0), angle); - return std::nullopt; + if (llvm::hasSingleElement(*op.getBody())) { + rewriter.eraseOp(op); } - auto controls = op.getControls(); - qc::CtrlOp::create(rewriter, phase->loc, controls.drop_back(), - controls.back(), [&](Value target) { - qc::POp::create(rewriter, phase->loc, target, angle); - }); - return std::nullopt; + return releasePhase ? phase : std::nullopt; } [[nodiscard]] std::optional factorControl(qco::CtrlOp op) { auto phase = normalizeBlock(*op.getBody(), op); - if (!phase || op.getNumControls() == 0) { - return phase; - } - if (phase->expression.isZero()) { - return std::nullopt; - } + const bool releasePhase = phase.has_value() && op.getNumControls() == 0; + if (phase && !releasePhase && !phase->expression.isZero()) { + rewriter.setInsertionPoint(op); + auto angle = phase->expression.materialize(rewriter, phase->loc); + rewriter.setInsertionPointAfter(op); + SmallVector oldControls(op.getOutputControls()); + SmallVector newControls; + Operation* relativePhase = nullptr; + if (op.getNumControls() == 1) { + auto p = + qco::POp::create(rewriter, phase->loc, oldControls.front(), angle); + newControls.push_back(p.getOutputTarget(0)); + relativePhase = p; + } else { + auto relative = qco::CtrlOp::create( + rewriter, phase->loc, ValueRange(oldControls).drop_back(), + oldControls.back(), [&](Value target) { + return qco::POp::create(rewriter, phase->loc, target, angle) + .getOutputTarget(0); + }); + llvm::append_range(newControls, relative.getOutputQubits()); + relativePhase = relative; + } - rewriter.setInsertionPoint(op); - auto angle = phase->expression.materialize(rewriter, phase->loc); - rewriter.setInsertionPointAfter(op); - SmallVector oldControls(op.getOutputControls()); - SmallVector newControls; - Operation* relativePhase = nullptr; - if (op.getNumControls() == 1) { - auto p = - qco::POp::create(rewriter, phase->loc, oldControls.front(), angle); - newControls.push_back(p.getOutputTarget(0)); - relativePhase = p; - } else { - auto relative = qco::CtrlOp::create( - rewriter, phase->loc, ValueRange(oldControls).drop_back(), - oldControls.back(), [&](Value target) { - return qco::POp::create(rewriter, phase->loc, target, angle) - .getOutputTarget(0); - }); - llvm::append_range(newControls, relative.getOutputQubits()); - relativePhase = relative; + for (auto [oldControl, newControl] : + llvm::zip_equal(oldControls, newControls)) { + rewriter.replaceAllUsesExcept(oldControl, newControl, relativePhase); + } } - - for (auto [oldControl, newControl] : - llvm::zip_equal(oldControls, newControls)) { - rewriter.replaceAllUsesExcept(oldControl, newControl, relativePhase); + if (phase) { + eraseDeadBodyOps(*op.getBody()); } - return std::nullopt; + if (llvm::hasSingleElement(*op.getBody())) { + SmallVector replacements(op.getInputControls()); + for (auto yielded : op.getBody()->getTerminator()->getOperands()) { + replacements.push_back( + getValueFromBlockArgument(yielded, op.getInputTargets())); + } + rewriter.replaceOp(op, replacements); + } + return releasePhase ? phase : std::nullopt; } void normalizeRegion(Region& region) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 7297b90235..2df7dd8655 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -307,16 +307,6 @@ static LogicalResult prepareGlobalPhases(ModuleOp moduleOp, if (failed(mqt::normalizeGlobalPhases(moduleOp))) { return failure(); } - SmallVector emptyControls; - moduleOp.walk([&](CtrlOp op) { - if (llvm::hasSingleElement(*op.getBody())) { - emptyControls.push_back(op); - } - }); - IRRewriter rewriter(moduleOp.getContext()); - for (auto op : llvm::reverse(emptyControls)) { - rewriter.replaceOp(op, op.getOperands()); - } if (target.supportsOperation("gphase", 0, 1)) { return success(); } diff --git a/mlir/unittests/Dialect/MQT/Transforms/test_global_phase_normalization.cpp b/mlir/unittests/Dialect/MQT/Transforms/test_global_phase_normalization.cpp index 255357696f..973f5a69b8 100644 --- a/mlir/unittests/Dialect/MQT/Transforms/test_global_phase_normalization.cpp +++ b/mlir/unittests/Dialect/MQT/Transforms/test_global_phase_normalization.cpp @@ -233,6 +233,33 @@ TEST_F(GlobalPhaseNormalizationTest, expectNormalizedQCUnitary(moduleOp, 3); } +TEST_F(GlobalPhaseNormalizationTest, RemovesQCControlEmptiedByPhaseExtraction) { + auto moduleOp = mlir::qc::QCProgramBuilder::build( + context.get(), [](mlir::qc::QCProgramBuilder& builder) { + builder.cgphase(0.25, builder.staticQubit(0)); + return builder.intConstant(0); + }); + ASSERT_TRUE(moduleOp); + auto cloned = cast((*moduleOp)->clone()); + OwningOpRef expected(cloned); + auto function = *moduleOp->getOps().begin(); + ASSERT_EQ(llvm::range_size(function.getBody().getOps()), + 1U); + + ASSERT_TRUE(mlir::mqt::normalizeGlobalPhases(*moduleOp).succeeded()); + ASSERT_TRUE(verify(*moduleOp).succeeded()); + EXPECT_TRUE(function.getBody().getOps().empty()); + EXPECT_EQ(llvm::range_size(function.getBody().getOps()), 1U); + + for (ModuleOp candidate : {expected.get(), moduleOp.get()}) { + PassManager pm(candidate.getContext()); + pm.addPass(createQCToQCO()); + ASSERT_TRUE(pm.run(candidate).succeeded()); + ASSERT_TRUE(verify(candidate).succeeded()); + } + ::mqt::test::expectFullUnitaryEqual(*expected, *moduleOp, 1); +} + TEST_F(GlobalPhaseNormalizationTest, QCInverseAndIntegralPowerPreserveFullUnitary) { auto moduleOp = mlir::qc::QCProgramBuilder::build( @@ -441,6 +468,42 @@ TEST_F(GlobalPhaseNormalizationTest, FactorsControlledPhaseOntoControl) { EXPECT_EQ(returnOp.getOperand(1), ctrl.getOutputTarget(0)); } +TEST_F(GlobalPhaseNormalizationTest, + RemovesQCOControlEmptiedByPhaseExtraction) { + auto moduleOp = parse(R"mlir( + module { + func.func @test(%control: !qco.qubit, %lhs: !qco.qubit, + %rhs: !qco.qubit) + -> (!qco.qubit, !qco.qubit, !qco.qubit) { + %control_out, %lhs_out, %rhs_out = qco.ctrl(%control) + targets(%lhs_arg = %lhs, %rhs_arg = %rhs) { + %phase = arith.constant 0.25 : f64 + qco.gphase(%phase) + qco.yield %rhs_arg, %lhs_arg : !qco.qubit, !qco.qubit + } : ({!qco.qubit}, {!qco.qubit, !qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit, !qco.qubit}) + return %control_out, %lhs_out, %rhs_out + : !qco.qubit, !qco.qubit, !qco.qubit + } + } + )mlir"); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(verify(*moduleOp).succeeded()); + auto function = *moduleOp->getOps().begin(); + ASSERT_EQ(llvm::range_size(function.getBody().getOps()), 1U); + + ASSERT_TRUE(mlir::mqt::normalizeGlobalPhases(*moduleOp).succeeded()); + ASSERT_TRUE(verify(*moduleOp).succeeded()); + EXPECT_TRUE(function.getBody().getOps().empty()); + EXPECT_EQ(llvm::range_size(function.getBody().getOps()), 1U); + auto returnOp = + cast(function.getBody().front().getTerminator()); + auto p = *function.getBody().getOps().begin(); + EXPECT_EQ(returnOp.getOperand(0), p.getOutputTarget(0)); + EXPECT_EQ(returnOp.getOperand(1), function.getArgument(2)); + EXPECT_EQ(returnOp.getOperand(2), function.getArgument(1)); +} + TEST_F(GlobalPhaseNormalizationTest, ControlledExtractionPreservesFullUnitaryUnderOuterControl) { auto moduleOp = parse(R"mlir( From 2ecba6e5e67389bb73b96eb0324ef9366f2fdd9d Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 2 Sep 2026 13:02:44 +0000 Subject: [PATCH 17/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20reverse?= =?UTF-8?q?=20operation=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use LLVM early-increment iteration to preserve reverse deletion without manual iterator bookkeeping. Assisted-by: GPT-5.6 Sol via Codex --- .../Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp b/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp index 886ce9c431..a5871f0239 100644 --- a/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp +++ b/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp @@ -312,13 +312,12 @@ class GlobalPhaseNormalizer final { } void eraseDeadBodyOps(Block& body) { - for (auto* op = body.getTerminator()->getPrevNode(); op != nullptr;) { - auto* previous = op->getPrevNode(); - if (!isa(op) && - isOpTriviallyDead(op)) { - rewriter.eraseOp(op); + for (auto& op : + llvm::make_early_inc_range(llvm::reverse(body.without_terminator()))) { + if (!isa(&op) && + isOpTriviallyDead(&op)) { + rewriter.eraseOp(&op); } - op = previous; } } From 808be13caaaa3afc239b6513d966c52af6ba14b9 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 2 Sep 2026 16:00:57 +0200 Subject: [PATCH 18/20] =?UTF-8?q?=F0=9F=90=9B=20Preserve=20reverse=20delet?= =?UTF-8?q?ion=20iterators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep an explicit pointer to the previous operation before erasing the current node. Reverse early-increment iteration retains a dangling base iterator after deletion.\n\nAssisted-by: GPT-5.6 Sol via Codex --- .../Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp b/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp index a5871f0239..886ce9c431 100644 --- a/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp +++ b/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp @@ -312,12 +312,13 @@ class GlobalPhaseNormalizer final { } void eraseDeadBodyOps(Block& body) { - for (auto& op : - llvm::make_early_inc_range(llvm::reverse(body.without_terminator()))) { - if (!isa(&op) && - isOpTriviallyDead(&op)) { - rewriter.eraseOp(&op); + for (auto* op = body.getTerminator()->getPrevNode(); op != nullptr;) { + auto* previous = op->getPrevNode(); + if (!isa(op) && + isOpTriviallyDead(op)) { + rewriter.eraseOp(op); } + op = previous; } } From ba81d4b4def2792a0a8ef769ea4ce4302c21a0a4 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 2 Sep 2026 16:17:19 +0200 Subject: [PATCH 19/20] =?UTF-8?q?=F0=9F=A7=AA=20Update=20mapping=20target?= =?UTF-8?q?=20construction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapt the mapping test added on main to the explicit connectivity and native-operation facts required by the generalized compiler target API.\n\nAssisted-by: GPT-5.6 Sol via Codex --- .../Dialect/QCO/Transforms/Mapping/test_mapping.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index b58699ed73..39198207a3 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -701,8 +701,9 @@ TEST_F(MappingPassFixture, ExpandNonAdjacentTwoQubitIfOnLineTarget) { builder.sink(conditionalResults[1]); auto moduleOp = builder.finalize(); - const auto target = llvm::cantFail(CompilerTarget::create( - 3, std::vector{{0, 1}, {1, 2}})); + const auto target = llvm::cantFail( + CompilerTarget::create(3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), + NativeOperations::unrestricted())); ASSERT_TRUE(runPass(moduleOp.get(), target, MappingPassOptions{.ntrials = 1}) .succeeded()); ASSERT_TRUE(succeeded(verify(*moduleOp))); From 1bc668bbb4206859c3b93939372d3095c03e68ab Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 2 Sep 2026 18:23:35 +0200 Subject: [PATCH 20/20] =?UTF-8?q?=F0=9F=A7=AA=20Update=20compiler=20target?= =?UTF-8?q?=20construction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapt the fixed-point test added on main to the explicit connectivity and native-operation facts required by the generalized compiler target API. Assisted-by: GPT-5.6 Sol via Codex --- mlir/unittests/Compiler/test_compiler_pipeline.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 229201642a..1114645e8b 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -1408,7 +1408,8 @@ TEST_F(CompilerPipelineTest, std::vector operations{llvm::cantFail(TargetOperation::create("u", 1, 3)), llvm::cantFail(TargetOperation::create("cz", 2, 0))}; auto target = llvm::cantFail(CompilerTarget::create( - 2, std::vector{{0, 1}}, std::move(operations))); + 2, CompilerTarget::Connectivity::fromCouplings({{0, 1}}), + CompilerTarget::NativeOperations::fromOperations(operations))); ASSERT_TRUE(program->compileForTarget(target)); const std::string before = program->str();