diff --git a/.agent/plans/generalize-compiler-target.md b/.agent/plans/generalize-compiler-target.md new file mode 100644 index 0000000000..d468caac18 --- /dev/null +++ b/.agent/plans/generalize-compiler-target.md @@ -0,0 +1,271 @@ +# 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 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 + +- [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. +- [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. +- [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. +- [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. +- [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. +- [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 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 + 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. +- 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: 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. + +## 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: 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: 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 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: 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 +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 + +`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. + +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. 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. 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 + +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 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. 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 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. + +## 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. + +Plan revision note (2026-09-01): Updated the plan after the independent +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/CHANGELOG.md b/CHANGELOG.md index 1e1336a9d6..1b2d9b513d 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**]) @@ -879,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/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..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, @@ -356,8 +346,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"); +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,6 +427,29 @@ means every operation is native.)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."); @@ -444,7 +457,27 @@ means every operation is native.)pb"); .def( "__init__", [](mlir::CompilerTarget::Operation& self, std::string name, - const size_t numQubits, const size_t numParameters, + 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, + const size_t arity, const size_t numParameters, std::optional> siteTuples, const std::optional duration, @@ -452,15 +485,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,8 +505,8 @@ means every operation is native.)pb"); return operation.canonicalName().str(); }, "The normalized compiler operation name.") - .def_prop_ro("num_qubits", &mlir::CompilerTarget::Operation::numQubits, - "The fixed operation arity.") + .def_prop_ro("arity", &mlir::CompilerTarget::Operation::arity, + "The accepted operation arity.") .def_prop_ro("num_parameters", &mlir::CompilerTarget::Operation::numParameters, "The number of real-valued parameters.") @@ -529,71 +561,126 @@ means every operation is native.)pb"); .def_ro("entangler", &mlir::CompilerTarget::SynthesisBasis::entangler, "The two-qubit entangler."); + nb::enum_( + 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 model."); + connectivity + .def( + "__init__", + [](mlir::CompilerTarget::Connectivity& self, + const std::vector& couplings) { + new (&self) mlir::CompilerTarget::Connectivity( + mlir::CompilerTarget::Connectivity::fromCouplings(couplings)); + }, + "couplings"_a, "Create an explicit connectivity model.") + .def_static("all_to_all", &mlir::CompilerTarget::Connectivity::allToAll, + "Create an all-to-all connectivity model.") + .def_prop_ro("kind", &mlir::CompilerTarget::Connectivity::kind, + "The connectivity model.") + .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", + "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", "Native-operation support."); + nativeOperations + .def( + "__init__", + [](mlir::CompilerTarget::NativeOperations& self, + const std::vector& operations) { + new (&self) mlir::CompilerTarget::NativeOperations( + mlir::CompilerTarget::NativeOperations::fromOperations( + operations)); + }, + "operations"_a, "Create explicit native-operation support.") + .def_static("unrestricted", + &mlir::CompilerTarget::NativeOperations::unrestricted, + "Create unrestricted native-operation support.") + .def_prop_ro("kind", &mlir::CompilerTarget::NativeOperations::kind, + "The native-operation support model.") + .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, "native_operations"_a, + "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, + "native_operations"_a, "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, "native_operations"_a, + "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, + "native_operations"_a, "duration_unit"_a = nb::none()) .def_static( "from_device", [](const qdmi::Device& device) { @@ -650,7 +737,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 +746,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, + "The target connectivity model.") .def_prop_ro( "couplings", [](const mlir::CompilerTarget& target) { @@ -669,9 +755,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, + "The target native-operation support model.") .def_prop_ro( "operations", [](const mlir::CompilerTarget& target) { @@ -691,12 +777,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."); 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..d2a24897c9 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,11 +123,24 @@ 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: ... +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 f53ed0d6e2..6a096e949b 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -27,14 +27,45 @@ 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 +support are required: ```python -target = CompilerTarget(3, couplings=[(0, 1), (1, 2)]) +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), + CompilerTarget.Operation("reset", arity=1, num_parameters=0), + ]), +) ``` +Use `CompilerTarget.Connectivity.all_to_all()` for an all-to-all target. An +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. 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 fails, earlier passes may already have changed the program. Copy the program @@ -107,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` @@ -125,6 +167,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..3e5ee9288d 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -27,33 +28,53 @@ 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. - * 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. - * - * 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; using Coupling = std::pair; - /** - * @brief Unit shared by all raw timing metadata on a target. - * - * @details A raw duration denotes `value * scaleFactor()` units. - */ + /// Target connectivity. + class Connectivity { + public: + enum class Kind : uint8_t { AllToAll, Explicit }; + + /// Create unrestricted all-to-all connectivity. + [[nodiscard]] static Connectivity allToAll(); + + /// Create explicitly enumerated connectivity. + [[nodiscard]] static Connectivity + fromCouplings(llvm::ArrayRef couplings); + + /// Return the connectivity 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, llvm::ArrayRef couplings); + + Kind kind_; + llvm::SmallVector couplings_; + }; + + /// 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); @@ -70,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, @@ -105,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, @@ -136,21 +149,54 @@ 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 Create a validated operation capability. - */ + /// 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_; + }; + + /// 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); + + /// Create a validated operation capability. [[nodiscard]] static llvm::Expected - create(std::string name, size_t numQubits, size_t numParameters, + create(std::string name, Arity arity, size_t numParameters, std::vector siteTuples = {}, std::optional duration = std::nullopt, std::optional fidelity = std::nullopt); @@ -161,8 +207,8 @@ class CompilerTarget { /// Return the canonical lower-case compiler operation name. [[nodiscard]] llvm::StringRef canonicalName() const noexcept; - /// Return the positive fixed operation arity. - [[nodiscard]] size_t numQubits() 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; @@ -177,22 +223,47 @@ 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, Arity arity, size_t numParameters, std::vector siteTuples, std::optional duration, std::optional fidelity); std::string name_; std::string canonicalName_; - size_t numQubits_; + Arity arity_; size_t numParameters_; std::vector siteTuples_; std::optional duration_; std::optional fidelity_; }; - /** - * @brief Recognized native gate capability independent of synthesis code. - */ + /// Native-operation support. + class NativeOperations { + public: + enum class Kind : uint8_t { Unrestricted, Explicit }; + + /// Create unrestricted native-operation support. + [[nodiscard]] static NativeOperations unrestricted(); + + /// Create explicitly enumerated native-operation support. + [[nodiscard]] static NativeOperations + fromOperations(llvm::ArrayRef operations); + + /// Return the native-operation support 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, llvm::ArrayRef operations); + + Kind kind_; + llvm::SmallVector operations_; + }; + + /// Recognized native gate capability independent of synthesis code. enum class GateKind : uint8_t { U, X, @@ -211,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. @@ -224,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; @@ -235,40 +302,28 @@ class CompilerTarget { const SynthesisBasis&) = default; }; - /** - * @brief Create an unnamed target with dense site IDs `0..numQubits-1`. - */ + /// 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`. - */ + /// 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. - */ + /// 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); - /** - * @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, - std::optional> couplings = std::nullopt, - std::optional> operations = std::nullopt, + 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. @@ -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,48 +353,40 @@ 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 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; /// 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; /// Return the maximum degree of the target's routing topology. [[nodiscard]] size_t maxDegree() const noexcept; - /// Return whether the target contains an explicit operation set. - [[nodiscard]] bool hasExplicitOperations() const noexcept; + /// 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. - */ + /// Return whether an operation capability is supported by the target. [[nodiscard]] bool - supportsOperation(llvm::StringRef name, size_t numQubits, + supportsOperation(llvm::StringRef name, size_t arity, std::optional numParameters = std::nullopt) const; - /// Return whether a QCO operation is supported by the target. + /// Return whether a QCO operation is supported. [[nodiscard]] bool supports(::mlir::Operation* operation) const; - /// Return whether a recognized gate is supported by the target. + /// Return whether a recognized gate is supported. [[nodiscard]] bool supports(GateKind gate) const; /// Return the recognized gates supported by the target. @@ -355,8 +402,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..ad215d07e7 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,16 +26,50 @@ #include #include #include +#include +#include #include #include #include +#include #include #include namespace mlir { +/// Target facts that QDMI v1.3 cannot encode compactly. +/// 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) { + 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); @@ -45,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 " @@ -55,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()), @@ -80,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; } @@ -97,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) @@ -106,77 +137,106 @@ isSwapInvariantOperation(const llvm::StringRef operationName) { } [[nodiscard]] static llvm::Error validateHomogeneousSupport( - const qdmi::Operation& operation, const size_t arity, - const std::optional>& flattenedSites, + const qdmi::Operation& operation, size_t arity, + 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"); - } + llvm::StringRef deviceName) { const auto operationName = operation.getName(); - if (auto error = requireHomogeneousOperation( - flattenedSites->size() % arity == 0, deviceName, operationName, + 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; - } - - llvm::DenseSet knownSites; + 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) { - llvm::DenseSet supportedSites; - supportedSites.reserve(flattenedSites->size()); - for (const auto& site : *flattenedSites) { + std::unordered_set supportedSites; + supportedSites.reserve(flattenedSites.size()); + for (const auto& site : flattenedSites) { auto siteId = checkedSiteId(site.getIndex()); if (!siteId) { 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"); } - 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()); + std::set reportedTuples; + std::set supportedCouplings; + 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(); } 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)); @@ -187,8 +247,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)); } @@ -198,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, @@ -240,23 +299,19 @@ snapshotDurationUnit(const qdmi::Device& device) { } [[nodiscard]] static llvm::Expected> -snapshotSiteTuples(const qdmi::Operation& operation, const size_t arity, - const std::optional>& flattenedSites, - const std::optional defaultDuration, - const std::optional defaultFidelity) { - if (!flattenedSites) { - return std::vector{}; - } - +snapshotSiteTuples(const qdmi::Operation& operation, size_t arity, + const std::vector& flattenedSites, + std::optional defaultDuration, + std::optional defaultFidelity) { 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,12 +334,12 @@ 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, const std::optional>& couplings, - const llvm::StringRef deviceName) { + llvm::StringRef deviceName, bool homogeneousOperationSupport) { std::vector targetOperations; targetOperations.reserve(operations.size()); for (const auto& operation : operations) { @@ -294,36 +349,75 @@ 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 (auto error = - validateHomogeneousSupport(operation, *arity, flattenedSites, - deviceSites, couplings, deviceName)) { + 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(); - auto siteTuples = snapshotSiteTuples(operation, *arity, flattenedSites, - duration, fidelity); - if (!siteTuples) { - return siteTuples.takeError(); + std::vector siteTuples; + 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)) { + return error; + } + auto tuples = snapshotSiteTuples(operation, *arity, *flattenedSites, + duration, fidelity); + if (!tuples) { + return tuples.takeError(); + } + 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(), - std::move(*siteTuples), duration, fidelity); + operation.getName(), targetArity, operation.getParametersNum(), + std::move(siteTuples), duration, fidelity); if (!targetOperation) { return targetOperation.takeError(); } targetOperations.emplace_back(std::move(*targetOperation)); } - return targetOperations; + return CompilerTarget::NativeOperations::fromOperations(targetOperations); } [[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, @@ -368,9 +462,17 @@ 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); + snapshotOperations(device.getOperations(), sites, couplings, deviceName, + hasHomogeneousAllToAllMetadata); if (!operations) { return operations.takeError(); } @@ -378,8 +480,11 @@ snapshotCompilerTarget(const qdmi::Device& device) { if (!durationUnit) { return durationUnit.takeError(); } + auto connectivity = + couplings ? CompilerTarget::Connectivity::fromCouplings(*couplings) + : CompilerTarget::Connectivity::allToAll(); 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..2870c0c100 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -13,7 +13,6 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include #include #include #include @@ -32,9 +31,10 @@ #include #include #include -#include #include #include +#include +#include #include #include @@ -47,61 +47,53 @@ 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 -[[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") { @@ -116,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"); } @@ -125,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]"); @@ -134,21 +125,44 @@ validateFidelity(const std::optional fidelity, return llvm::Error::success(); } +CompilerTarget::Connectivity CompilerTarget::Connectivity::allToAll() { + return {Kind::AllToAll, {}}; +} + +CompilerTarget::Connectivity +CompilerTarget::Connectivity::fromCouplings(ArrayRef couplings) { + return {Kind::Explicit, couplings}; +} + +CompilerTarget::Connectivity::Kind +CompilerTarget::Connectivity::kind() const noexcept { + return kind_; +} + +ArrayRef +CompilerTarget::Connectivity::couplings() const noexcept { + return couplings_; +} + +CompilerTarget::Connectivity::Connectivity(Kind kind, + ArrayRef couplings) + : kind_(kind), couplings_(couplings) {} + [[nodiscard]] static llvm::Expected> -makeDenseSites(const size_t numQubits) { - if (numQubits == 0) { +makeDenseSites(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(); @@ -159,8 +173,7 @@ makeDenseSites(const size_t numQubits) { } 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"); } @@ -171,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_; } @@ -182,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"); } @@ -203,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_; } @@ -227,9 +239,9 @@ std::optional CompilerTarget::Site::t2() const noexcept { llvm::Expected CompilerTarget::SiteTuple::create(std::vector sites, - const std::optional duration, - const std::optional fidelity) { - std::set uniqueSites; + std::optional duration, + std::optional fidelity) { + std::unordered_set uniqueSites; for (const auto site : sites) { if (site < 0) { return invalidTarget( @@ -248,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 { @@ -264,49 +276,90 @@ 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, const size_t numQubits, const size_t numParameters, - std::vector siteTuples, const std::optional duration, - const std::optional fidelity) { + 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, 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 (numQubits == 0) { - return invalidTarget( - "Compiler target operation qubit count 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"); + } - std::set> uniqueSiteCombinations; + SmallVector> uniqueSiteCombinations; for (const auto& siteTuple : siteTuples) { - if (siteTuple.sites().size() != numQubits) { + if (!arity.accepts(siteTuple.sites().size())) { 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), 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 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)), - numQubits_(numQubits), numParameters_(numParameters), + arity_(arity), numParameters_(numParameters), siteTuples_(std::move(siteTuples)), duration_(duration), fidelity_(fidelity) {} @@ -316,8 +369,9 @@ StringRef CompilerTarget::Operation::canonicalName() const noexcept { return canonicalName_; } -size_t CompilerTarget::Operation::numQubits() const noexcept { - return numQubits_; +CompilerTarget::Operation::Arity +CompilerTarget::Operation::arity() const noexcept { + return arity_; } size_t CompilerTarget::Operation::numParameters() const noexcept { @@ -337,35 +391,69 @@ std::optional CompilerTarget::Operation::fidelity() const noexcept { return fidelity_; } +CompilerTarget::NativeOperations +CompilerTarget::NativeOperations::unrestricted() { + return {Kind::Unrestricted, {}}; +} + +CompilerTarget::NativeOperations +CompilerTarget::NativeOperations::fromOperations( + ArrayRef operations) { + return {Kind::Explicit, operations}; +} + +CompilerTarget::NativeOperations::Kind +CompilerTarget::NativeOperations::kind() const noexcept { + return kind_; +} + +ArrayRef +CompilerTarget::NativeOperations::operations() const noexcept { + return operations_; +} + +CompilerTarget::NativeOperations::NativeOperations( + Kind kind, ArrayRef operations) + : kind_(kind), operations_(operations) {} + struct CompilerTarget::Storage { Storage(std::optional targetName, std::vector targetSites, - std::optional> targetCouplings, - std::optional> targetOperations, + Connectivity::Kind targetConnectivityKind, + SmallVector targetCouplings, + NativeOperations::Kind targetNativeOperationsKind, + SmallVector targetOperations, std::optional targetDurationUnit); [[nodiscard]] static llvm::Expected> create(std::optional targetName, std::vector targetSites, - std::optional> targetCouplings, - std::optional> targetOperations, + Connectivity::Kind targetConnectivityKind, + SmallVector targetCouplings, + NativeOperations::Kind targetNativeOperationsKind, + SmallVector targetOperations, std::optional targetDurationUnit); [[nodiscard]] llvm::Error initialize(); [[nodiscard]] bool - supportsOperation(StringRef name, size_t numQubits, + 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; std::optional durationUnit; std::vector sites; SmallVector siteIds; - DenseMap siteToVertex; - std::optional> couplings; + std::unordered_map siteToVertex; + Connectivity::Kind connectivityKind; + SmallVector couplings; SmallVector> adjacency; SmallVector distances; size_t maximumDegree = 0; - std::optional> operations; + NativeOperations::Kind nativeOperationsKind; + SmallVector operations; llvm::StringMap> capabilities; SmallVector supportedGates; std::optional basis; @@ -373,21 +461,28 @@ struct CompilerTarget::Storage { CompilerTarget::Storage::Storage( std::optional targetName, std::vector targetSites, - std::optional> targetCouplings, - std::optional> targetOperations, + Connectivity::Kind targetConnectivityKind, + SmallVector targetCouplings, + NativeOperations::Kind targetNativeOperationsKind, + SmallVector 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, + Connectivity::Kind targetConnectivityKind, + SmallVector targetCouplings, + NativeOperations::Kind targetNativeOperationsKind, + SmallVector 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 +507,8 @@ llvm::Error CompilerTarget::Storage::initialize() { siteIds.emplace_back(site.id()); } - if (couplings) { - std::set canonicalCouplings; - for (auto [source, target] : *couplings) { + if (connectivityKind == Connectivity::Kind::Explicit) { + for (auto& [source, target] : couplings) { if (!siteToVertex.contains(source) || !siteToVertex.contains(target)) { return invalidTarget( "Compiler target topology references an unknown site"); @@ -426,12 +520,12 @@ 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) { + for (const auto& [source, target] : couplings) { const auto sourceVertex = siteToVertex.at(source); const auto targetVertex = siteToVertex.at(target); adjacency[sourceVertex].emplace_back(targetVertex); @@ -473,9 +567,13 @@ llvm::Error CompilerTarget::Storage::initialize() { 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().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"); } @@ -495,7 +593,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,7 +605,15 @@ llvm::Error CompilerTarget::Storage::initialize() { } for (const auto& specification : GATE_SPECIFICATIONS) { - if (supportsOperation(specification.name, specification.numQubits, + 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); } @@ -517,13 +623,34 @@ llvm::Error CompilerTarget::Storage::initialize() { } bool CompilerTarget::Storage::supportsOperation( - const StringRef operationName, const size_t numQubits, - const std::optional numParameters) const { + 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; + } + 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() || numQubits == 0 || numQubits > sites.size()) { + if (canonical.empty() || arity > sites.size()) { return false; } - if (!operations) { + if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { return true; } const auto found = capabilities.find(canonical); @@ -531,15 +658,16 @@ 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().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; @@ -574,60 +702,58 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { } llvm::Expected -CompilerTarget::create(const size_t numQubits, - std::optional> couplings, - std::optional> operations, +CompilerTarget::create(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, 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 +775,7 @@ CompilerTarget::durationUnit() const noexcept { return storage_->durationUnit; } -size_t CompilerTarget::numQubits() const noexcept { +size_t CompilerTarget::numSites() const noexcept { return storage_->sites.size(); } @@ -662,7 +788,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; @@ -670,38 +796,34 @@ CompilerTarget::vertexForSite(const SiteId site) const noexcept { return found->second; } -SiteId CompilerTarget::siteForVertex(const size_t vertex) const { - assert(vertex < numQubits() && "Compiler target vertex is out of range"); +SiteId CompilerTarget::siteForVertex(size_t vertex) const { + 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() && +bool CompilerTarget::areAdjacent(size_t source, size_t target) const { + assert(source < numSites() && target < numSites() && "Compiler target vertex is out of range"); - if (!hasExplicitTopology()) { + if (connectivityKind() == Connectivity::Kind::AllToAll) { return source != target; } return llvm::is_contained(storage_->adjacency[source], target); } 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) { + 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) { if (neighbour != vertex) { callback(neighbour); } @@ -713,18 +835,17 @@ void CompilerTarget::forEachNeighbour( } } -size_t CompilerTarget::distanceBetween(const size_t source, - const size_t target) const { - assert(source < numQubits() && target < numQubits() && +size_t CompilerTarget::distanceBetween(size_t source, size_t target) const { + assert(source < numSites() && target < numSites() && "Compiler target vertex is out of range"); - if (!hasExplicitTopology()) { + 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"); +ArrayRef CompilerTarget::explicitNeighbours(size_t vertex) const { + assert(vertex < numSites() && "Compiler target vertex is out of range"); return storage_->adjacency[vertex]; } @@ -732,22 +853,20 @@ size_t CompilerTarget::maxDegree() const noexcept { 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, - const std::optional numParameters) const { - return storage_->supportsOperation(operationName, numQubits, numParameters); + StringRef operationName, size_t arity, + std::optional numParameters) const { + return storage_->supportsOperation(operationName, arity, numParameters); } bool CompilerTarget::supports(::mlir::Operation* operation) const { @@ -756,20 +875,33 @@ bool CompilerTarget::supports(::mlir::Operation* operation) const { } 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(), @@ -784,7 +916,7 @@ bool CompilerTarget::supports(::mlir::Operation* operation) const { return false; } -bool CompilerTarget::supports(const GateKind gate) const { +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 617383db11..6be7b2c258 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -26,10 +26,13 @@ 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: pm.addPass(qco::createPlacementPass(target)); + break; } populateQCOCleanupPipeline(pm); pm.addPass(qco::createTargetNativeSynthesis(target)); 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/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index ba2493d715..096d75eece 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(); + << " program qubits, but the target site count is " + << 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..2df7dd8655 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" @@ -49,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(); @@ -299,6 +300,28 @@ static bool requiresTargetSynthesis(Operation* operation, 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(); + } + 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 { struct SynthesisPlan { @@ -451,15 +474,19 @@ struct TargetNativeSynthesisPass final protected: void runOnOperation() override { - if (!target.hasExplicitOperations()) { + if (target.nativeOperationsKind() == + CompilerTarget::NativeOperations::Kind::Unrestricted) { return; } ModuleOp moduleOp = getOperation(); + if (failed(prepareGlobalPhases(moduleOp, target))) { + signalPassFailure(); + return; + } const auto plan = planTargetSynthesis(moduleOp, target); if (plan.firstNeed == nullptr) { return; } - const auto targetBasis = target.synthesisBasis(); if (!targetBasis) { plan.firstNeed->emitError() @@ -483,7 +510,7 @@ struct TargetNativeSynthesisPass final lowerTargetOperation(rewriter, cast(operation), *targetBasis); } - if (failed(mlir::mqt::normalizeGlobalPhases(moduleOp))) { + if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); } } 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/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") diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 1f8325b39d..1114645e8b 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; @@ -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) { @@ -417,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"; @@ -456,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"; @@ -995,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 @@ -1039,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() { @@ -1088,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"; @@ -1159,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) { @@ -1203,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"; @@ -1246,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"; @@ -1302,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"; @@ -1337,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); @@ -1425,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(); @@ -1540,13 +1524,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, 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); + CompilerTarget::SingleQubitBasis::U); auto program = QCOProgram::fromMLIRString(source); ASSERT_TRUE(program); @@ -1568,9 +1553,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"; @@ -1588,7 +1571,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()); @@ -1610,9 +1595,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"; @@ -1621,7 +1604,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::allToAll(), + CompilerTarget::NativeOperations::unrestricted())); auto qc = QCProgram::fromQASMString(source); ASSERT_TRUE(qc); @@ -1645,9 +1630,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); @@ -1686,9 +1669,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(); @@ -1722,9 +1703,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"; @@ -1764,7 +1743,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); @@ -1827,11 +1808,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); @@ -1980,9 +1959,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"; @@ -2018,9 +1995,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/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 16cebd2727..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; @@ -41,8 +43,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 +73,55 @@ 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, InfersDDSIMTargetFacts) { 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::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); + 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) { @@ -114,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 6a32cda8c1..03b8d78349 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -50,10 +50,13 @@ 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 Arity = Operation::Arity; +using NativeOperations = Target::NativeOperations; using Site = Target::Site; using SiteId = Target::SiteId; using SiteTuple = Target::SiteTuple; @@ -70,10 +73,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(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 +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].numQubits(), 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); @@ -106,8 +110,11 @@ 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(), + NativeOperations::unrestricted())); + const auto named = + valid(Target::create("simulator", 2, Connectivity::allToAll(), + NativeOperations::unrestricted())); EXPECT_FALSE(target.name()); ASSERT_TRUE(named.name()); @@ -119,7 +126,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)); @@ -133,14 +140,55 @@ 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; + 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))}; - 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}}), + NativeOperations::unrestricted())); - 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); @@ -159,12 +207,15 @@ 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()), - "Compiler target qubit count exceeds the nonnegative i64 site domain"); + Target::create(std::numeric_limits::max(), + Connectivity::allToAll(), + NativeOperations::unrestricted()), + "Compiler target site count exceeds the nonnegative i64 site domain"); } expectInvalid(Site::create(-1), "Compiler target site ID must be nonnegative"); @@ -191,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 qubit count 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}))}), @@ -206,66 +267,100 @@ 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, std::nullopt, - std::vector{valid(Operation::create("x", 1, 0, {}, 1))}), - "Compiler target timing metadata requires a duration unit"); + 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, std::nullopt, - std::vector{valid(Operation::create( - "x", 1, 0, std::vector{valid(SiteTuple::create({0}, 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, std::vector{{0, 0}}), + expectInvalid(Target::create(2, Connectivity::fromCouplings({{0, 0}}), + NativeOperations::unrestricted()), "Compiler target topology contains a self-coupling"); - expectInvalid(Target::create(2, std::vector{{0, 2}}), + expectInvalid(Target::create(2, Connectivity::fromCouplings({{0, 2}}), + NativeOperations::unrestricted()), "Compiler target topology references an unknown site"); - expectInvalid(Target::create(3, std::vector{{0, 1}}), + expectInvalid(Target::create(3, Connectivity::fromCouplings({{0, 1}}), + NativeOperations::unrestricted()), "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, 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, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("cx", 2, 0))})), + "Compiler target operation arity exceeds its site count"); expectInvalid( - Target::create(1, std::nullopt, - std::vector{valid(Operation::create("cx", 2, 0))}), - "Compiler target operation arity exceeds its site count"); + 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, DistinguishesAbsentAndEmptyOperationSets) { - const auto permissive = valid(Target::create(2)); - 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()); +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); + 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("gphase", 0), true); + 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()); + + 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) { @@ -274,12 +369,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,15 +405,40 @@ 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); } } +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(result); @@ -347,6 +469,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; @@ -354,8 +477,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)) { @@ -368,6 +496,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); @@ -378,25 +507,110 @@ 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), 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)); - - 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(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, Connectivity::allToAll(), NativeOperations::fromOperations({}))); + EXPECT_EQ(closed.supports(barrier), 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 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( diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index c80a397e5f..39198207a3 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; @@ -244,7 +246,8 @@ static CompilerTarget getSquareGridTarget(const size_t n) { } return llvm::cantFail( - CompilerTarget::create(numTarget, std::move(couplings))); + CompilerTarget::create(numTarget, Connectivity::fromCouplings(couplings), + NativeOperations::unrestricted())); } /// Creates an N-qubit GHZ state, where N = `qubits.size()` using @@ -342,9 +345,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())); @@ -395,9 +398,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(); @@ -439,9 +442,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 +483,9 @@ 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(), + NativeOperations::unrestricted())); QCOProgramBuilder builder(context.get()); builder.initialize({builder.getI1Type()}); @@ -511,7 +515,9 @@ 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(), + NativeOperations::unrestricted())); QCOProgramBuilder builder(context.get()); builder.initialize({builder.getI1Type(), builder.getI1Type()}); @@ -551,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()); @@ -572,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(); @@ -589,8 +597,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) { @@ -601,8 +611,9 @@ 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(couplings), + NativeOperations::unrestricted())); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(2, builder.getI1Type())); @@ -690,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))); @@ -918,7 +930,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 +996,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 +1858,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..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,8 @@ namespace mqt::test::qco { using Target = mlir::CompilerTarget; +using Connectivity = Target::Connectivity; +using NativeOperations = Target::NativeOperations; using Operation = Target::Operation; using Site = Target::Site; using mlir::ModuleOp; @@ -63,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; @@ -154,9 +159,10 @@ 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), std::nullopt, std::move(operations))); + valid(Operation::create("cx", 2, 0)), + valid(Operation::create("gphase", 0, 1))}; + return valid(Target::create(std::move(*sites), Connectivity::allToAll(), + NativeOperations::fromOperations(operations))); } [[nodiscard]] static mlir::DenseElementsAttr @@ -231,7 +237,8 @@ class TargetSynthesisTest : public testing::Test { } // namespace TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { - const auto target = valid(Target::create(2)); + 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); @@ -500,8 +507,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, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("swap", 2, 0))}))); ASSERT_FALSE(swapTarget.synthesisBasis()); const auto before = printModule(*module); @@ -513,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); @@ -523,9 +610,11 @@ 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, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("u", 1, 3)), + valid(Operation::create("cz", 2, 0)), + valid(Operation::create("gphase", 0, 1))}))); ASSERT_TRUE(target.synthesisBasis()); ASSERT_EQ(target.synthesisBasis()->entangler, Target::GateKind::CZ); @@ -539,13 +628,16 @@ 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); + builder.gphase(0.25); return builder.intConstant(0); }); - const auto permissive = valid(Target::create(1)); + const auto permissive = valid(Target::create( + 1, Connectivity::allToAll(), NativeOperations::unrestricted())); const auto before = printModule(*module); ASSERT_TRUE(mlir::succeeded( @@ -562,8 +654,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, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("pow", 1, 1))}))); ASSERT_FALSE(powOnly.synthesisBasis()); const auto before = printModule(*module); @@ -576,7 +670,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, Connectivity::allToAll(), + NativeOperations::fromOperations({valid(Operation::create("h", 1, 0))}))); ASSERT_FALSE(hOnly.synthesisBasis()); auto supported = build([](QCOProgramBuilder& builder) { @@ -620,9 +715,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, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("u", 1, 3)), + valid(Operation::create("rxx", 2, 1))}))); const auto before = printModule(*module); ASSERT_TRUE(mlir::succeeded( @@ -683,7 +779,9 @@ 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))})); + Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("cx", 2, 0))}))); ASSERT_FALSE(target.synthesisBasis()); auto reversed = build([](QCOProgramBuilder& builder) { @@ -712,7 +810,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, Connectivity::allToAll(), + NativeOperations::fromOperations({valid(Operation::create("x", 1, 0))}))); const auto expectDynamicAllocationFailure = [&](OwningOpRef module) { const auto diagnostics = expectFailure( @@ -747,7 +846,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, Connectivity::allToAll(), + NativeOperations::fromOperations({valid(Operation::create("x", 1, 0))}))); const auto diagnostics = expectFailure(*module, mlir::qco::createVerifyTargetConformance(target)); @@ -769,8 +869,10 @@ 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))}, + Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("x", 1, 0))}))), build([](QCOProgramBuilder& builder) { auto qubit = builder.staticQubit(10); qubit = builder.h(qubit); @@ -781,7 +883,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))})), + Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("x", 2, 0))}))), build([](QCOProgramBuilder& builder) { auto qubit = builder.staticQubit(10); qubit = builder.x(qubit); @@ -790,8 +894,10 @@ 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))}, + Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("rz", 1, 0))}))), build([](QCOProgramBuilder& builder) { auto qubit = builder.staticQubit(10); qubit = builder.rz(0.25, qubit); @@ -809,7 +915,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, Connectivity::allToAll(), + 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..672ea83e19 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. + Every target has either all-to-all or explicitly enumerated connectivity and + either unrestricted or explicitly enumerated native-operation 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: ... @@ -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, - num_qubits: 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 num_qubits(self) -> int: - """The fixed operation arity.""" + def arity(self) -> CompilerTarget.OperationArity: + """The accepted operation arity.""" @property def num_parameters(self) -> int: @@ -257,6 +286,56 @@ class CompilerTarget: def entangler(self) -> CompilerTarget.GateKind: """The two-qubit entangler.""" + class ConnectivityKind(enum.Enum): + """The target connectivity model.""" + + ALL_TO_ALL = 0 + + EXPLICIT = 1 + + class Connectivity: + """A target connectivity model.""" + + def __init__(self, couplings: Sequence[tuple[int, int]]) -> None: + """Create an explicit connectivity model.""" + + @staticmethod + def all_to_all() -> CompilerTarget.Connectivity: + """Create an all-to-all connectivity model.""" + + @property + def kind(self) -> CompilerTarget.ConnectivityKind: + """The connectivity model.""" + + @property + def couplings(self) -> list[tuple[int, int]]: + """The explicit couplings, if present.""" + + class NativeOperationsKind(enum.Enum): + """The native-operation support model.""" + + UNRESTRICTED = 0 + + EXPLICIT = 1 + + class NativeOperations: + """Native-operation support.""" + + def __init__(self, operations: Sequence[CompilerTarget.Operation]) -> None: + """Create explicit native-operation support.""" + + @staticmethod + def unrestricted() -> CompilerTarget.NativeOperations: + """Create unrestricted native-operation support.""" + + @property + def kind(self) -> CompilerTarget.NativeOperationsKind: + """The native-operation support model.""" + + @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 +353,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 +361,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: + """The target connectivity model.""" @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: + """The target native-operation support model.""" @property def operations(self) -> list[CompilerTarget.Operation]: @@ -305,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, 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: + """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 ea43440489..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" @@ -74,61 +73,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{ @@ -223,6 +234,12 @@ 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) + /// 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) ADD_LIST_PROPERTY(QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS, QDMI_Program_Format, SUPPORTED_PROGRAM_FORMATS, prop, size, value, sizeRet) @@ -327,7 +344,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) @@ -342,6 +360,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/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)) diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index e65e20e0a6..970e679afe 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) @@ -453,15 +457,34 @@ 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(sites, operations=[operation], duration_unit=duration_unit), - CompilerTarget("sparse", sites, operations=[operation], 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, + ), ] - 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" @@ -470,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 qubit count 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: @@ -493,7 +547,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 +579,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.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 7bdee690b1..1bfb090603 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"; @@ -2793,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 { @@ -2819,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 { @@ -2851,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); +} 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..1e9d020d26 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()); @@ -770,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); } }