diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc63ad14e..9b91015b2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ releases may include breaking changes. topology, operations, and calibration data ([#1980]) ([**@burgholzer**]) - ✨ Expose registered QDMI device IDs without loading device libraries ([#1972]) ([**@burgholzer**]) +- ✨ Add progressive native targeting (`QCOProgram::targetNative` / Python + `target_native` / `target_device`, `mqt-cc --coupling-map`) ([#1969]) + ([**@simon1hofmann**]) - ✨ Add typed runtime configuration transport and relocatable assets for QDMI device descriptions ([#1967]) ([**@burgholzer**]) - ✨ Add and improve QIR generation support in the MQT Compiler Collection @@ -714,6 +717,7 @@ for previous changelogs._ [#1976]: https://github.com/munich-quantum-toolkit/core/pull/1976 [#1975]: https://github.com/munich-quantum-toolkit/core/pull/1975 [#1972]: https://github.com/munich-quantum-toolkit/core/pull/1972 +[#1969]: https://github.com/munich-quantum-toolkit/core/pull/1969 [#1967]: https://github.com/munich-quantum-toolkit/core/pull/1967 [#1965]: https://github.com/munich-quantum-toolkit/core/pull/1965 [#1961]: https://github.com/munich-quantum-toolkit/core/pull/1961 diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 0c096e0b80..2c1a140b27 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -10,7 +10,9 @@ #include "ir/QuantumComputation.hpp" #include "mlir/Compiler/Programs.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" +#include #include #include // NOLINT(misc-include-cleaner) #include // NOLINT(misc-include-cleaner) @@ -237,6 +239,48 @@ compileProgram(const nb::object& program, const mlir::ProgramFormat output, enableStatistics)); } +[[nodiscard]] std::string +nativeGatesMenuOrThrow(const std::vector& names) { + mlir::SmallVector refs; + refs.reserve(names.size()); + for (const auto& name : names) { + refs.emplace_back(name); + } + const auto gateset = + mlir::qco::decomposition::NativeGateset::fromOperationNames(refs); + if (!gateset) { + throw nb::value_error( + "cannot derive a supported native-gates menu from the given " + "operation names"); + } + return gateset->toMenuString(); +} + +[[nodiscard]] std::string nativeGatesMenuFromDevice(const nb::object& device) { + std::vector names; + for (const auto& op : device.attr("operations")()) { + names.push_back(nb::cast(nb::handle(op).attr("name")())); + } + return nativeGatesMenuOrThrow(names); +} + +[[nodiscard]] std::vector> +couplingFromDevice(const nb::object& device) { + std::vector> edges; + const nb::object cmap = device.attr("coupling_map")(); + if (cmap.is_none()) { + return edges; + } + for (const auto& pair : cmap) { + const auto edge = nb::cast(nb::handle(pair)); + const auto a = static_cast(nb::cast(edge[0].attr("index")())); + const auto b = static_cast(nb::cast(edge[1].attr("index")())); + edges.emplace_back(a, b); + edges.emplace_back(b, a); + } + return edges; +} + } // namespace NB_MODULE(MQT_CORE_MODULE_NAME, m) { @@ -424,10 +468,10 @@ operations.)pb"); .def( "place_and_route", [](mlir::QCOProgram& value, - const std::vector>& coupling, - const std::size_t nlookahead, const float alpha, - const float lambda, const std::size_t niterations, - const std::size_t ntrials, const std::size_t seed) { + const std::vector>& coupling, + const size_t nlookahead, const float alpha, const float lambda, + const size_t niterations, const size_t ntrials, + const size_t seed) { requireSuccess(value.placeAndRoute(std::span(coupling), nlookahead, alpha, lambda, niterations, ntrials, seed)); @@ -435,6 +479,32 @@ operations.)pb"); "coupling"_a, nb::kw_only(), "nlookahead"_a = 1, "alpha"_a = 1.F, "lambda_"_a = 0.5F, "niterations"_a = 1, "ntrials"_a = 4, "seed"_a = 42, "Place and route the program for a coupling graph.") + .def( + "target_native", + [](mlir::QCOProgram& value, const std::string& nativeGates, + const nb::object& coupling) { + if (coupling.is_none()) { + requireSuccess(value.targetNative(nativeGates)); + } else { + const auto edges = + nb::cast>>(coupling); + requireSuccess(value.targetNative(nativeGates, std::span(edges))); + } + }, + nb::kw_only(), "native_gates"_a, "coupling"_a = nb::none(), + "Decompose multi-controlled gates, optionally place/route, then fuse " + "to " + "native_gates.") + .def( + "target_device", + [](mlir::QCOProgram& value, const nb::object& device) { + const auto menu = nativeGatesMenuFromDevice(device); + const auto coupling = couplingFromDevice(device); + requireSuccess(value.targetNative(menu, std::span(coupling))); + }, + "device"_a, + "Target a FoMaC device: derive native menu and coupling, then run " + "target_native.") .def( "to_qc", [](mlir::QCOProgram& value, const bool copy) { @@ -527,6 +597,31 @@ LLVM bitcode.)pb"); &BooleanMemberAdapter<&mlir::QIRProgram::writeBitcode>::call, "path"_a, "Write this program as LLVM bitcode."); + m.def( + "native_gates_from_operation_names", &nativeGatesMenuOrThrow, "names"_a, + R"pb(Derive a comma-separated native-gates menu from operation name strings. + +Args: + names: Operation name strings (aliases such as ``u3`` / ``cnot`` are normalized). + +Returns: + Comma-separated native gate menu string. + +Raises: + ValueError: When no supported menu can be derived.)pb"); + + m.def("native_gates_from_device", &nativeGatesMenuFromDevice, "device"_a, + R"pb(Derive a comma-separated native-gates menu from a FoMaC device. + +Args: + device: A FoMaC device exposing ``operations()`` with ``name()``. + +Returns: + Comma-separated native gate menu string. + +Raises: + ValueError: When no supported menu can be derived.)pb"); + m.def("compile_program", &compileProgram, "program"_a, nb::kw_only(), "output"_a = mlir::ProgramFormat::QC, "inplace"_a = false, "qco_pipeline"_a = "mqt-qco-default", "enable_timing"_a = false, diff --git a/bindings/patterns.txt b/bindings/patterns.txt index 5ee2e8895e..a232dc58cf 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -169,3 +169,12 @@ mqt.core.mlir.compile_program: enable_statistics: bool = False, ) -> QCProgram | QCOProgram | JeffProgram | QIRProgram: \doc + +mqt.core.mlir.QCOProgram.target_native: + def target_native( + self, + *, + native_gates: str, + coupling: Sequence[tuple[int, int]] | None = None, + ) -> None: + \doc diff --git a/mlir/include/mlir/Compiler/Programs.h b/mlir/include/mlir/Compiler/Programs.h index ac62a967c4..82b2f30b5a 100644 --- a/mlir/include/mlir/Compiler/Programs.h +++ b/mlir/include/mlir/Compiler/Programs.h @@ -209,10 +209,18 @@ class QCOProgram final : public Program { /// Place and route the program on a coupling graph. [[nodiscard]] bool - placeAndRoute(std::span> coupling, - std::size_t nlookahead = 1, float alpha = 1.F, - float lambda = 0.5F, std::size_t niterations = 1, - std::size_t ntrials = 4, std::size_t seed = 42); + placeAndRoute(std::span> coupling, + size_t nlookahead = 1, float alpha = 1.F, float lambda = 0.5F, + size_t niterations = 1, size_t ntrials = 4, size_t seed = 42); + + /// Progressive native targeting: decompose multi-controlled gates, + /// optionally place/route on @p coupling (treated as undirected; reverse + /// edges are added automatically), then fuse to @p nativeGates (required, + /// non-empty, and must parse as a supported native menu). The menu is + /// validated before any IR mutation. + [[nodiscard]] bool + targetNative(std::string_view nativeGates, + std::span> coupling = {}); /// Consume this program and convert it to QC. [[nodiscard]] std::optional intoQC() &&; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h index bb48a23383..d453bda59d 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h @@ -13,10 +13,11 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" -#include +#include #include #include +#include namespace mlir { class Operation; @@ -57,7 +58,7 @@ struct TwoQubitNativeDecomposition; * `rxx`/`ryy`/`rzx`/`rzz` at a fixed angle of π/2. */ struct NativeGateset { - llvm::DenseSet gates; + DenseSet gates; std::optional eulerBasis; std::optional entangler; @@ -70,6 +71,26 @@ struct NativeGateset { [[nodiscard]] static std::optional parse(StringRef nativeGates); + /** + * @brief Builds a gateset from device/backend operation names. + * + * Normalizes known aliases, ignores unrecognized names, and resolves the + * Euler basis and entangler with the same priority as @ref parse. The + * resulting @p gates set contains only the selected strategy tokens. + * + * @return Resolved gateset, or `std::nullopt` when no supported menu exists. + */ + [[nodiscard]] static std::optional + fromOperationNames(ArrayRef names); + + /** + * @brief Comma-separated menu for the selected Euler factors and entangler. + * + * Token order is deterministic (Euler constituents, then entangler), e.g. + * `"x,sx,rz,cz"`, `"u,rxx"`, or `"u,ecr"`. + */ + [[nodiscard]] std::string toMenuString() const; + /** * @brief Basis decomposition of @p target under this gateset, if supported. */ diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index d5a8e3e751..4b8c5f98e1 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -21,6 +21,7 @@ #include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" #include "mlir/Dialect/QC/Translation/TranslateQuantumComputationToQC.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" @@ -413,11 +414,10 @@ bool QCOProgram::decomposeMultiControlled(const uint64_t minControls) { } bool QCOProgram::placeAndRoute( - const std::span> coupling, - const std::size_t nlookahead, const float alpha, const float lambda, - const std::size_t niterations, const std::size_t ntrials, - const std::size_t seed) { - DenseSet> couplingSet; + const std::span> coupling, + const size_t nlookahead, const float alpha, const float lambda, + const size_t niterations, const size_t ntrials, const size_t seed) { + DenseSet> couplingSet; couplingSet.insert(coupling.begin(), coupling.end()); qco::MappingPassOptions options; options.nlookahead = nlookahead; @@ -434,6 +434,40 @@ bool QCOProgram::placeAndRoute( "failed to place and route the QCO program")); } +bool QCOProgram::targetNative( + const std::string_view nativeGates, + const std::span> coupling) { + if (StringRef(nativeGates).trim().empty()) { + mod().emitError("the native gate menu must not be empty"); + return false; + } + if (!qco::decomposition::NativeGateset::parse(nativeGates).has_value()) { + mod().emitError("unsupported native gate menu '") + << nativeGates + << "' (expected a recognised Euler basis plus one entangler)"; + return false; + } + if (!decomposeMultiControlled(/*minControls=*/2)) { + return false; + } + if (!coupling.empty()) { + // Treat coupling as undirected: placeAndRoute requires both (u,v) and + // (v,u). + SmallVector> symmetric; + symmetric.reserve(coupling.size() * 2); + for (const auto& [u, v] : coupling) { + symmetric.emplace_back(u, v); + if (u != v) { + symmetric.emplace_back(v, u); + } + } + if (!placeAndRoute(symmetric)) { + return false; + } + } + return fuseTwoQubitUnitaryRuns(nativeGates); +} + std::optional QCOProgram::intoQC() && { if (failed(runPasses( mod(), [](OpPassManager& pm) { pm.addPass(createQCOToQC()); }, diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp index 74a0dd9cd7..b88ad5b88c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp @@ -24,6 +24,7 @@ #include #include +#include #include namespace mlir::qco::decomposition { @@ -271,4 +272,144 @@ std::optional NativeGateset::parse(StringRef nativeGates) { }; } +static StringRef normalizeGateAlias(StringRef token) { + token = token.trim(); + if (token.equals_insensitive("prx")) { + return "r"; + } + if (token.equals_insensitive("u3")) { + return "u"; + } + if (token.equals_insensitive("cnot")) { + return "cx"; + } + return token; +} + +static void insertEulerConstituents(DenseSet& selected, + EulerBasis euler) { + switch (euler) { + case EulerBasis::U: + selected.insert(NativeGateKind::U); + break; + case EulerBasis::ZSXX: + selected.insert(NativeGateKind::X); + selected.insert(NativeGateKind::SX); + selected.insert(NativeGateKind::RZ); + break; + case EulerBasis::R: + selected.insert(NativeGateKind::R); + break; + case EulerBasis::XZX: + selected.insert(NativeGateKind::RX); + selected.insert(NativeGateKind::RZ); + break; + case EulerBasis::XYX: + selected.insert(NativeGateKind::RX); + selected.insert(NativeGateKind::RY); + break; + case EulerBasis::ZYZ: + selected.insert(NativeGateKind::RY); + selected.insert(NativeGateKind::RZ); + break; + } +} + +std::optional +NativeGateset::fromOperationNames(ArrayRef names) { + DenseSet recognized; + for (StringRef name : names) { + std::string lowered = name.trim().lower(); + if (lowered.empty()) { + continue; + } + const StringRef token = normalizeGateAlias(lowered); + const auto gate = parseGateToken(token); + if (gate) { + recognized.insert(*gate); + } + } + const auto euler = resolveEulerBasis(recognized); + const auto entangler = selectEntangler(recognized); + if (!euler || !entangler) { + return std::nullopt; + } + DenseSet selected; + insertEulerConstituents(selected, *euler); + selected.insert(*entangler); + return NativeGateset{ + .gates = std::move(selected), + .eulerBasis = euler, + .entangler = entangler, + }; +} + +std::string NativeGateset::toMenuString() const { + if (!eulerBasis || !entangler) { + return {}; + } + std::string out; + auto append = [&](StringRef tok) { + if (!out.empty()) { + out.push_back(','); + } + out.append(tok.str()); + }; + switch (*eulerBasis) { + case EulerBasis::U: + append("u"); + break; + case EulerBasis::ZSXX: + append("x"); + append("sx"); + append("rz"); + break; + case EulerBasis::R: + append("r"); + break; + case EulerBasis::XZX: + append("rx"); + append("rz"); + break; + case EulerBasis::XYX: + append("rx"); + append("ry"); + break; + case EulerBasis::ZYZ: + append("ry"); + append("rz"); + break; + } + switch (*entangler) { + case NativeGateKind::RXX: + append("rxx"); + break; + case NativeGateKind::RYY: + append("ryy"); + break; + case NativeGateKind::RZX: + append("rzx"); + break; + case NativeGateKind::RZZ: + append("rzz"); + break; + case NativeGateKind::ISWAP: + append("iswap"); + break; + case NativeGateKind::CZ: + append("cz"); + break; + case NativeGateKind::CX: + append("cx"); + break; + case NativeGateKind::ECR: + append("ecr"); + break; + default: + llvm_unreachable( + "only RXX/RYY/RZX/RZZ/ISWAP/CZ/CX/ECR are valid entanglers"); + } + return out; +} + } // namespace mlir::qco::decomposition diff --git a/mlir/tools/mqt-cc/CMakeLists.txt b/mlir/tools/mqt-cc/CMakeLists.txt index bb241b0273..a4f7469a65 100644 --- a/mlir/tools/mqt-cc/CMakeLists.txt +++ b/mlir/tools/mqt-cc/CMakeLists.txt @@ -28,3 +28,56 @@ mqt_mlir_target_use_project_options(mqt-cc) llvm_update_compile_flags(mqt-cc) mlir_check_all_link_libraries(mqt-cc) export_executable_symbols_for_plugins(mqt-cc) + +if(BUILD_MQT_CORE_TESTS) + set_target_properties(mqt-cc PROPERTIES EXCLUDE_FROM_ALL FALSE) + + add_test(NAME mqt-cc-coupling-map-qco-optimized + COMMAND $ ${CMAKE_CURRENT_SOURCE_DIR}/tests/coupling-line.qasm + --emit=qco-optimized --native-gates=u,cx --coupling-map=0-1,1-2) + set_tests_properties( + mqt-cc-coupling-map-qco-optimized + PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION "qco\\.ctrl" + FAIL_REGULAR_EXPRESSION "qco\\.swap;qco\\.ctrl\\(%0\\).*targets.*%2") + + add_test( + NAME mqt-cc-coupling-map-requires-native-gates + COMMAND + ${CMAKE_COMMAND} -DMQT_CC=$ + -DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/tests/coupling-line.qasm -P + ${CMAKE_CURRENT_SOURCE_DIR}/tests/check-coupling-map-requires-native-gates.cmake) + set_tests_properties(mqt-cc-coupling-map-requires-native-gates PROPERTIES LABELS + mqt-mlir-unittests) + + add_test( + NAME mqt-cc-coupling-map-must-not-be-empty + COMMAND + ${CMAKE_COMMAND} -DMQT_CC=$ + -DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/tests/coupling-line.qasm + "-DARGS=--emit=qco-optimized;--native-gates=u,cx;--coupling-map=" + "-DEXPECTED=--coupling-map must not be empty" -P + ${CMAKE_CURRENT_SOURCE_DIR}/tests/check-mqt-cc-fails.cmake) + set_tests_properties(mqt-cc-coupling-map-must-not-be-empty PROPERTIES LABELS mqt-mlir-unittests) + + add_test( + NAME mqt-cc-coupling-map-rejects-invalid-entry + COMMAND + ${CMAKE_COMMAND} -DMQT_CC=$ + -DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/tests/coupling-line.qasm + "-DARGS=--emit=qco-optimized;--native-gates=u,cx;--coupling-map=0-x" + "-DEXPECTED=invalid --coupling-map entry" -P + ${CMAKE_CURRENT_SOURCE_DIR}/tests/check-mqt-cc-fails.cmake) + set_tests_properties(mqt-cc-coupling-map-rejects-invalid-entry PROPERTIES LABELS + mqt-mlir-unittests) + + add_test( + NAME mqt-cc-native-gates-requires-qco-optimization + COMMAND + ${CMAKE_COMMAND} -DMQT_CC=$ + -DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/tests/coupling-line.qasm + "-DARGS=--emit=qco;--native-gates=u,cx" + "-DEXPECTED=--native-gates requires an output that passes through QCO optimization" -P + ${CMAKE_CURRENT_SOURCE_DIR}/tests/check-mqt-cc-fails.cmake) + set_tests_properties(mqt-cc-native-gates-requires-qco-optimization PROPERTIES LABELS + mqt-mlir-unittests) +endif() diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 0f22d57975..05dfb9e41c 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -17,6 +17,7 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Support/Passes.h" @@ -53,6 +54,7 @@ #include #include +#include #include #include #include @@ -92,6 +94,12 @@ static llvm::cl::opt nativeGates( "pass"), llvm::cl::value_desc("csv"), llvm::cl::init("")); +static llvm::cl::opt couplingMap( + "coupling-map", + llvm::cl::desc("Undirected coupling edges as comma-separated pairs 'u-v' " + "(requires --native-gates). Example: 0-1,1-2"), + llvm::cl::value_desc("edges"), llvm::cl::init("")); + namespace { enum class InputFormat : std::uint8_t { MLIR, QASM, Jeff }; enum class InputDialect : std::uint8_t { QC, QCO }; @@ -184,6 +192,38 @@ parseOutputFormat(const StringRef format) { return std::nullopt; } +static LogicalResult +parseCouplingMap(StringRef text, + SmallVectorImpl>& out) { + out.clear(); + text = text.trim(); + if (text.empty()) { + return success(); + } + while (!text.empty()) { + auto [piece, rest] = text.split(','); + text = rest; + piece = piece.trim(); + if (piece.empty()) { + continue; + } + auto [left, right] = piece.split('-'); + left = left.trim(); + right = right.trim(); + size_t a = 0; + size_t b = 0; + if (left.getAsInteger(10, a) || right.getAsInteger(10, b) || left.empty() || + right.empty()) { + llvm::errs() << "invalid --coupling-map entry '" << piece + << "' (expected u-v)\n"; + return failure(); + } + out.emplace_back(a, b); + out.emplace_back(b, a); + } + return success(); +} + static llvm::cl::opt enableDecomposeMultiControlled( "decompose-multi-controlled", llvm::cl::desc( @@ -397,8 +437,7 @@ static int runCompiler(int argc, char** argv) { "QCO optimization.\n"; return 1; } - const llvm::StringRef nativeGateMenu = - llvm::StringRef(nativeGates.getValue()).trim(); + const StringRef nativeGateMenu = StringRef(nativeGates.getValue()).trim(); if (nativeGates.getNumOccurrences() > 0 && nativeGateMenu.empty()) { llvm::errs() << "--native-gates must not be empty.\n"; return 1; @@ -410,6 +449,25 @@ static int runCompiler(int argc, char** argv) { "QCO optimization.\n"; return 1; } + SmallVector> couplingEdges; + if (failed(parseCouplingMap(couplingMap.getValue(), couplingEdges))) { + return 1; + } + if (couplingMap.getNumOccurrences() > 0 && couplingEdges.empty()) { + llvm::errs() << "--coupling-map must not be empty.\n"; + return 1; + } + if (!couplingEdges.empty() && nativeGateMenu.empty()) { + llvm::errs() << "--coupling-map requires --native-gates.\n"; + return 1; + } + if (couplingMap.getNumOccurrences() > 0 && + (*parsedOutputFormat == OutputFormat::QCImport || + *parsedOutputFormat == OutputFormat::QCO)) { + llvm::errs() << "--coupling-map requires an output that passes through " + "QCO optimization.\n"; + return 1; + } if (enableDecomposeMultiControlled && !isDecomposeMultiControlledConfigValid( decomposeMultiControlledMinControls.getValue())) { @@ -460,6 +518,15 @@ static int runCompiler(int argc, char** argv) { } populateQCOCleanupPipeline(pm); if (!nativeGateMenu.empty()) { + if (!enableDecomposeMultiControlled) { + populateDecomposeMultiControlledPipeline(pm, /*minControls=*/2); + } + if (!couplingEdges.empty()) { + DenseSet> couplingSet( + couplingEdges.begin(), couplingEdges.end()); + pm.addPass(qco::createMappingPass(couplingSet, + qco::MappingPassOptions{})); + } pm.addPass(qco::createFuseTwoQubitUnitaryRuns( qco::FuseTwoQubitUnitaryRunsOptions{ .nativeGates = nativeGateMenu.str(), diff --git a/mlir/tools/mqt-cc/tests/check-coupling-map-requires-native-gates.cmake b/mlir/tools/mqt-cc/tests/check-coupling-map-requires-native-gates.cmake new file mode 100644 index 0000000000..6d75321c6a --- /dev/null +++ b/mlir/tools/mqt-cc/tests/check-coupling-map-requires-native-gates.cmake @@ -0,0 +1,23 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(expected "--coupling-map requires --native-gates") + +execute_process( + COMMAND "${MQT_CC}" "${INPUT}" --emit=qco-optimized --coupling-map=0-1 + RESULT_VARIABLE result + ERROR_VARIABLE error) + +if(result EQUAL 0) + message(FATAL_ERROR "mqt-cc accepted --coupling-map without --native-gates") +endif() + +string(FIND "${error}" "${expected}" diagnostic_position) +if(diagnostic_position EQUAL -1) + message(FATAL_ERROR "mqt-cc did not emit the expected diagnostic:\n${error}") +endif() diff --git a/mlir/tools/mqt-cc/tests/check-mqt-cc-fails.cmake b/mlir/tools/mqt-cc/tests/check-mqt-cc-fails.cmake new file mode 100644 index 0000000000..eb0b0ba706 --- /dev/null +++ b/mlir/tools/mqt-cc/tests/check-mqt-cc-fails.cmake @@ -0,0 +1,25 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +if(NOT DEFINED EXPECTED) + message(FATAL_ERROR "EXPECTED must be set") +endif() + +execute_process( + COMMAND ${MQT_CC} ${INPUT} ${ARGS} + RESULT_VARIABLE result + ERROR_VARIABLE error) + +if(result EQUAL 0) + message(FATAL_ERROR "mqt-cc unexpectedly succeeded:\n${error}") +endif() + +string(FIND "${error}" "${EXPECTED}" diagnostic_position) +if(diagnostic_position EQUAL -1) + message(FATAL_ERROR "mqt-cc did not emit the expected diagnostic '${EXPECTED}':\n${error}") +endif() diff --git a/mlir/tools/mqt-cc/tests/coupling-line.qasm b/mlir/tools/mqt-cc/tests/coupling-line.qasm new file mode 100644 index 0000000000..e2a6c622d5 --- /dev/null +++ b/mlir/tools/mqt-cc/tests/coupling-line.qasm @@ -0,0 +1,11 @@ +// Copyright (c) 2026 Munich Quantum Software Company GmbH +// All rights reserved. +// +// SPDX-License-Identifier: MIT +// +// Licensed under the MIT License + +OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +cx q[0], q[2]; diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 48ddc1c017..d046ca986c 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -16,9 +16,12 @@ #include "mlir/Dialect/QC/Translation/TranslateQuantumComputationToQC.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" +#include "mlir/Dialect/Utils/Utils.h" #include "mlir/Support/IRVerification.h" #include "mlir/Support/Passes.h" #include "qasm_programs.h" @@ -30,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -73,11 +77,57 @@ using namespace mlir; using namespace mlir::qc; using namespace mlir::qco; using namespace mlir::qir; +using namespace mlir::utils; using QCProgramBuilderFn = NamedMLIRBuilder; using QIRProgramBuilderFn = NamedMLIRBuilder; using QuantumComputationBuilderFn = NamedBuilder<::qc::QuantumComputation>; +/// Return true if two-qubit unitaries in a straight-line entry point obey +/// coupling constraints. +static bool isExecutableStraightLine( + func::FuncOp entry, + const DenseSet>& couplingSet) { + DenseMap m; + for (Operation& op : entry.getFunctionBody().getOps()) { + if (auto staticOp = dyn_cast(op)) { + m.try_emplace(staticOp.getQubit(), staticOp.getIndex()); + continue; + } + + if (auto unitaryOp = dyn_cast(op)) { + if (!isa(op)) { + const auto numQubits = unitaryOp.getNumQubits(); + if (numQubits > 2) { + return false; + } + if (numQubits > 1) { + const auto hwA = m.at(unitaryOp.getInputQubit(0)); + const auto hwB = m.at(unitaryOp.getInputQubit(1)); + if (!couplingSet.contains(std::make_pair(hwA, hwB))) { + return false; + } + } + } + for (const auto [pred, succ] : llvm::zip_equal( + unitaryOp.getInputQubits(), unitaryOp.getOutputQubits())) { + m.try_emplace(succ, m.at(pred)); + } + continue; + } + + if (auto resetOp = dyn_cast(op)) { + m.try_emplace(resetOp.getQubitOut(), m.at(resetOp.getQubitIn())); + continue; + } + + if (auto measOp = dyn_cast(op)) { + m.try_emplace(measOp.getQubitOut(), m.at(measOp.getQubitIn())); + } + } + return true; +} + namespace { struct CompilerPipelineTestCase { @@ -921,7 +971,7 @@ cx q[0], q[2]; const auto beforeTwoQubitFusion = qco.str(); EXPECT_TRUE(qco.fuseTwoQubitUnitaryRuns("u,cx")); EXPECT_NE(qco.str(), beforeTwoQubitFusion); - const std::vector> coupling = { + const std::vector> coupling = { {0, 1}, {1, 0}, {1, 2}, {2, 1}}; EXPECT_TRUE(qco.placeAndRoute(coupling)); EXPECT_TRUE(qco.runPassPipeline("mqt-qco-default", true, true)); @@ -939,6 +989,130 @@ cx q[0], q[2]; EXPECT_EQ(loopProgram->str().find("scf.for"), std::string::npos); } +TEST_F(CompilerPipelineTest, TargetNativeMenuOnlyFuses) { + const std::string qasm = R"(OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +)"; + auto qc = QCProgram::fromQASMString(qasm); + ASSERT_TRUE(qc); + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco); + ASSERT_TRUE(qco->cleanup()); + ASSERT_TRUE(qco->targetNative("u,cx")); + const auto ir = qco->str(); + EXPECT_EQ(ir.find("qco.h"), std::string::npos); + EXPECT_NE(ir.find("qco.u"), std::string::npos); +} + +TEST_F(CompilerPipelineTest, TargetNativeWithCouplingLowersSwaps) { + // CX on (0,2) needs routing on a line 0-1-2. + const std::string qasm = R"(OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +cx q[0], q[2]; +)"; + auto qc = QCProgram::fromQASMString(qasm); + ASSERT_TRUE(qc); + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco); + ASSERT_TRUE(qco->cleanup()); + const std::vector> coupling = { + {0, 1}, {1, 0}, {1, 2}, {2, 1}}; + ASSERT_TRUE(qco->targetNative("u,cx", coupling)); + const auto ir = qco->str(); + EXPECT_EQ(ir.find("qco.swap"), std::string::npos); + EXPECT_NE(ir.find("qco.ctrl"), std::string::npos); + EXPECT_EQ(ir.find("qco.ctrl(%0) targets (%arg0 = %2)"), std::string::npos); + + auto module = parseRecordedModule(ir); + ASSERT_TRUE(module); + const DenseSet> couplingSet(coupling.begin(), + coupling.end()); + EXPECT_TRUE( + isExecutableStraightLine(getEntryPoint(module.get()), couplingSet)); +} + +TEST_F(CompilerPipelineTest, TargetNativeRejectsEmptyMenu) { + const std::string qasm = R"(OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +h q; +)"; + auto qc = QCProgram::fromQASMString(qasm); + ASSERT_TRUE(qc); + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco); + EXPECT_FALSE(qco->targetNative("")); + EXPECT_FALSE(qco->targetNative(" ")); + const std::vector> coupling = {{0, 1}, {1, 0}}; + EXPECT_FALSE(qco->targetNative("", coupling)); +} + +TEST_F(CompilerPipelineTest, TargetNativeRejectsInvalidMenuWithoutMutating) { + const std::string qasm = R"(OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +h q[0]; +)"; + auto qc = QCProgram::fromQASMString(qasm); + ASSERT_TRUE(qc); + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco); + ASSERT_TRUE(qco->cleanup()); + const auto before = qco->str(); + EXPECT_FALSE(qco->targetNative("not-a-gate")); + EXPECT_FALSE(qco->targetNative("cx")); + EXPECT_EQ(qco->str(), before); + EXPECT_NE(before.find("qco.h"), std::string::npos); +} + +TEST_F(CompilerPipelineTest, TargetNativeAcceptsOneWayCoupling) { + const std::string qasm = R"(OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +cx q[0], q[2]; +)"; + auto qc = QCProgram::fromQASMString(qasm); + ASSERT_TRUE(qc); + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco); + ASSERT_TRUE(qco->cleanup()); + // One direction only; targetNative must symmetrize before placeAndRoute. + const std::vector> coupling = {{0, 1}, {1, 2}}; + ASSERT_TRUE(qco->targetNative("u,cx", coupling)); + const auto ir = qco->str(); + EXPECT_EQ(ir.find("qco.swap"), std::string::npos); + EXPECT_NE(ir.find("qco.ctrl"), std::string::npos); + + auto module = parseRecordedModule(ir); + ASSERT_TRUE(module); + const DenseSet> couplingSet = { + {0, 1}, {1, 0}, {1, 2}, {2, 1}}; + EXPECT_TRUE( + isExecutableStraightLine(getEntryPoint(module.get()), couplingSet)); +} + +TEST_F(CompilerPipelineTest, TargetNativeFailsWhenArchitectureTooSmall) { + const std::string qasm = R"(OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +cx q[0], q[1]; +x q[2]; +)"; + auto qc = QCProgram::fromQASMString(qasm); + ASSERT_TRUE(qc); + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco); + ASSERT_TRUE(qco->cleanup()); + // Coupling only spans two hardware qubits; three live program qubits must + // fail. + const std::vector> coupling = {{0, 1}}; + EXPECT_FALSE(qco->targetNative("u,cx", coupling)); +} + /** * @brief Test: QCO programs expose the raw and composite qubit-reuse flows. */ diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index e5efedc328..1e179d9536 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -747,6 +747,120 @@ TEST(NativeSpecTest, RejectsGatesetWithoutSingleQubitStrategy) { EXPECT_FALSE(NativeGateset::parse("rx,cx").has_value()); } +TEST(NativeGatesetFromNamesTest, DerivesIbmLikeMenu) { + const SmallVector names = {"x", "sx", "rz", "cx", "h", "measure"}; + const auto gs = NativeGateset::fromOperationNames(names); + ASSERT_TRUE(gs); + EXPECT_EQ(gs->eulerBasis, EulerBasis::ZSXX); + EXPECT_EQ(gs->entangler, NativeGateKind::CX); + EXPECT_EQ(gs->toMenuString(), "x,sx,rz,cx"); +} + +TEST(NativeGatesetFromNamesTest, PrefersCzAndMapsPrxAlias) { + const SmallVector names = {"prx", "cz", "cx"}; + const auto gs = NativeGateset::fromOperationNames(names); + ASSERT_TRUE(gs); + EXPECT_EQ(gs->eulerBasis, EulerBasis::R); + EXPECT_EQ(gs->entangler, NativeGateKind::CZ); + EXPECT_EQ(gs->toMenuString(), "r,cz"); +} + +TEST(NativeGatesetFromNamesTest, PrefersUAndCz) { + const SmallVector names = {"u", "u3", "cx", "cz"}; + const auto gs = NativeGateset::fromOperationNames(names); + ASSERT_TRUE(gs); + EXPECT_EQ(gs->eulerBasis, EulerBasis::U); + EXPECT_EQ(gs->entangler, NativeGateKind::CZ); + EXPECT_EQ(gs->toMenuString(), "u,cz"); +} + +TEST(NativeGatesetFromNamesTest, RotationPairXzx) { + const SmallVector names = {"rx", "rz", "cx"}; + const auto gs = NativeGateset::fromOperationNames(names); + ASSERT_TRUE(gs); + EXPECT_EQ(gs->eulerBasis, EulerBasis::XZX); + EXPECT_EQ(gs->toMenuString(), "rx,rz,cx"); +} + +TEST(NativeGatesetFromNamesTest, RotationPairXyx) { + const SmallVector names = {"rx", "ry", "cx"}; + const auto gs = NativeGateset::fromOperationNames(names); + ASSERT_TRUE(gs); + EXPECT_EQ(gs->eulerBasis, EulerBasis::XYX); + EXPECT_EQ(gs->toMenuString(), "rx,ry,cx"); +} + +TEST(NativeGatesetFromNamesTest, RotationPairZyz) { + const SmallVector names = {"ry", "rz", "cx"}; + const auto gs = NativeGateset::fromOperationNames(names); + ASSERT_TRUE(gs); + EXPECT_EQ(gs->eulerBasis, EulerBasis::ZYZ); + EXPECT_EQ(gs->toMenuString(), "ry,rz,cx"); +} + +TEST(NativeGatesetFromNamesTest, IgnoresEmptyAndNormalizesAliases) { + const SmallVector names = {" ", " U3 ", "", " CNOT "}; + const auto gs = NativeGateset::fromOperationNames(names); + ASSERT_TRUE(gs); + EXPECT_EQ(gs->eulerBasis, EulerBasis::U); + EXPECT_EQ(gs->entangler, NativeGateKind::CX); + EXPECT_EQ(gs->toMenuString(), "u,cx"); +} + +TEST(NativeGatesetFromNamesTest, RejectsInsufficientMenus) { + EXPECT_FALSE(NativeGateset::fromOperationNames( + SmallVector{"h", "measure"})); + EXPECT_FALSE(NativeGateset::fromOperationNames(SmallVector{"cx"})); + EXPECT_FALSE( + NativeGateset::fromOperationNames(SmallVector{"cnot"})); +} + +TEST(NativeGatesetFromNamesTest, MapsCnotAlias) { + const SmallVector names = {"u", "cnot"}; + const auto gs = NativeGateset::fromOperationNames(names); + ASSERT_TRUE(gs); + EXPECT_EQ(gs->entangler, NativeGateKind::CX); + EXPECT_EQ(gs->toMenuString(), "u,cx"); +} + +TEST(NativeGatesetFromNamesTest, PrefersRxxOverOtherEntanglers) { + const SmallVector names = {"u", "ecr", "cx", "cz", "iswap", + "rzz", "rzx", "ryy", "rxx"}; + const auto gs = NativeGateset::fromOperationNames(names); + ASSERT_TRUE(gs); + EXPECT_EQ(gs->entangler, NativeGateKind::RXX); + EXPECT_EQ(gs->toMenuString(), "u,rxx"); +} + +TEST(NativeGatesetFromNamesTest, EmitsEachNewEntanglerToken) { + const auto ecr = NativeGateset::fromOperationNames( + SmallVector{"x", "sx", "rz", "ecr"}); + ASSERT_TRUE(ecr); + EXPECT_EQ(ecr->entangler, NativeGateKind::ECR); + EXPECT_EQ(ecr->toMenuString(), "x,sx,rz,ecr"); + + const auto iswap = + NativeGateset::fromOperationNames(SmallVector{"u", "iswap"}); + ASSERT_TRUE(iswap); + EXPECT_EQ(iswap->entangler, NativeGateKind::ISWAP); + EXPECT_EQ(iswap->toMenuString(), "u,iswap"); + + const auto ryy = + NativeGateset::fromOperationNames(SmallVector{"u", "ryy"}); + ASSERT_TRUE(ryy); + EXPECT_EQ(ryy->toMenuString(), "u,ryy"); + + const auto rzx = + NativeGateset::fromOperationNames(SmallVector{"u", "rzx"}); + ASSERT_TRUE(rzx); + EXPECT_EQ(rzx->toMenuString(), "u,rzx"); + + const auto rzz = + NativeGateset::fromOperationNames(SmallVector{"u", "rzz"}); + ASSERT_TRUE(rzz); + EXPECT_EQ(rzz->toMenuString(), "u,rzz"); +} + TEST(NativeSpecTest, ResolvesEulerBasisFromGateset) { const auto uGateset = NativeGateset::parse("u,cx"); ASSERT_TRUE(uGateset); diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 771fda6f26..e55163adb6 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -181,6 +181,17 @@ class QCOProgram(Program): ) -> None: """Place and route the program for a coupling graph.""" + def target_native( + self, + *, + native_gates: str, + coupling: Sequence[tuple[int, int]] | None = None, + ) -> None: + """Decompose multi-controlled gates, optionally place/route, then fuse to native_gates.""" + + def target_device(self, device: object) -> None: + """Target a FoMaC device: derive native menu and coupling, then run target_native.""" + def to_qc(self, *, copy: bool = False) -> QCProgram: """Convert this program to QC. @@ -253,6 +264,32 @@ class QIRProgram(Program): def write_bitcode(self, path: str | os.PathLike) -> None: """Write this program as LLVM bitcode.""" +def native_gates_from_operation_names(names: Sequence[str]) -> str: + """Derive a comma-separated native-gates menu from operation name strings. + + Args: + names: Operation name strings (aliases such as ``u3`` / ``cnot`` are normalized). + + Returns: + Comma-separated native gate menu string. + + Raises: + ValueError: When no supported menu can be derived. + """ + +def native_gates_from_device(device: object) -> str: + """Derive a comma-separated native-gates menu from a FoMaC device. + + Args: + device: A FoMaC device exposing ``operations()`` with ``name()``. + + Returns: + Comma-separated native gate menu string. + + Raises: + ValueError: When no supported menu can be derived. + """ + @overload def compile_program( program: str diff --git a/test/python/test_native_gates_from_device.py b/test/python/test_native_gates_from_device.py new file mode 100644 index 0000000000..1e77afa3e5 --- /dev/null +++ b/test/python/test_native_gates_from_device.py @@ -0,0 +1,70 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for deriving native gate menus from device operation names.""" + +from __future__ import annotations + +import pytest +from plugins.qiskit.test_mock_backend import MockQDMIDevice + +from mqt.core.mlir import ( + native_gates_from_device, + native_gates_from_operation_names, +) + + +@pytest.fixture +def mock_qdmi_device_factory() -> type[MockQDMIDevice]: + """Return the mock QDMI device class for parameterized device tests.""" + return MockQDMIDevice + + +def test_native_gates_from_operation_names_ibm_like() -> None: + """Map an IBM-like op list to an x/sx/rz/cx menu.""" + assert native_gates_from_operation_names(["x", "sx", "rz", "cx", "h", "measure"]) == "x,sx,rz,cx" + + +def test_native_gates_from_operation_names_iqm_prx() -> None: + """Alias prx to r and prefer cz.""" + assert native_gates_from_operation_names(["prx", "cz"]) == "r,cz" + + +def test_native_gates_from_operation_names_prefers_rxx() -> None: + """New bases participate; preference picks RXX when present.""" + assert native_gates_from_operation_names(["u", "cx", "cz", "ecr", "iswap", "rxx", "ryy", "rzx", "rzz"]) == "u,rxx" + + +def test_native_gates_from_operation_names_ecr() -> None: + """ECR-only menus are emitted correctly.""" + assert native_gates_from_operation_names(["x", "sx", "rz", "ecr"]) == "x,sx,rz,ecr" + + +def test_native_gates_from_operation_names_rejects_insufficient() -> None: + """Reject name lists that lack a supported Euler + entangler pair.""" + with pytest.raises(ValueError, match="native-gates"): + native_gates_from_operation_names(["h", "measure"]) + + +def test_native_gates_from_device_ibm_like(mock_qdmi_device_factory: type[MockQDMIDevice]) -> None: + """Derive an IBM-like menu from a FoMaC-style device.""" + device = mock_qdmi_device_factory(operations=["x", "sx", "rz", "cx", "h", "measure"]) + assert native_gates_from_device(device) == "x,sx,rz,cx" + + +def test_native_gates_from_device_iqm_prx(mock_qdmi_device_factory: type[MockQDMIDevice]) -> None: + """Derive an IQM-like prx/cz menu from a FoMaC-style device.""" + device = mock_qdmi_device_factory(operations=["prx", "cz"]) + assert native_gates_from_device(device) == "r,cz" + + +def test_native_gates_from_device_rejects_insufficient(mock_qdmi_device_factory: type[MockQDMIDevice]) -> None: + """Raise when a device exposes no supported native menu.""" + device = mock_qdmi_device_factory(operations=["h", "measure"]) + with pytest.raises(ValueError, match="native-gates"): + native_gates_from_device(device) diff --git a/test/python/test_target_native.py b/test/python/test_target_native.py new file mode 100644 index 0000000000..2381d3205a --- /dev/null +++ b/test/python/test_target_native.py @@ -0,0 +1,80 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for progressive QCOProgram.target_native / target_device.""" + +from __future__ import annotations + +import pytest +from plugins.qiskit.test_mock_backend import MockQDMIDevice + +from mqt.core.ir import QuantumComputation +from mqt.core.mlir import OutputFormat, QCOProgram, compile_program + + +def test_target_native_menu_only() -> None: + """Menu-only targeting removes H in favor of native u factors.""" + qc = QuantumComputation(2) + qc.h(0) + qc.cx(0, 1) + qco = compile_program(qc, output=OutputFormat.QCO) + assert isinstance(qco, QCOProgram) + qco.target_native(native_gates="u,cx") + assert "qco.h" not in qco.ir + + +def test_target_device_with_coupling() -> None: + """Device-derived menu+coupling lowers CX(0,2) without leftover swaps.""" + device = MockQDMIDevice( + num_qubits=3, + operations=["u", "cx"], + coupling_map=[(0, 1), (1, 2)], + ) + qc = QuantumComputation(3) + qc.cx(0, 2) + qco = compile_program(qc, output=OutputFormat.QCO) + assert isinstance(qco, QCOProgram) + qco.target_device(device) + assert "qco.swap" not in qco.ir + assert "qco.ctrl" in qco.ir + # Unrouted CX(0,2) would keep static qubits 0 and 2 on the same ctrl. + assert "qco.ctrl(%0) targets (%arg0 = %2)" not in qco.ir + + +def test_target_native_rejects_empty_menu() -> None: + """Empty native_gates must fail.""" + qc = QuantumComputation(1) + qc.h(0) + qco = compile_program(qc, output=OutputFormat.QCO) + assert isinstance(qco, QCOProgram) + with pytest.raises(RuntimeError, match=r"(?i)fail|empty|native"): + qco.target_native(native_gates="") + + +def test_target_native_rejects_invalid_menu() -> None: + """Unsupported menus fail before mutating the IR.""" + qc = QuantumComputation(1) + qc.h(0) + qco = compile_program(qc, output=OutputFormat.QCO) + assert isinstance(qco, QCOProgram) + before = qco.ir + with pytest.raises(RuntimeError, match=r"(?i)unsupported|native|fail"): + qco.target_native(native_gates="not-a-gate") + assert qco.ir == before + + +def test_target_native_one_way_coupling() -> None: + """One-direction coupling edges are treated as undirected.""" + qc = QuantumComputation(3) + qc.cx(0, 2) + qco = compile_program(qc, output=OutputFormat.QCO) + assert isinstance(qco, QCOProgram) + qco.target_native(native_gates="u,cx", coupling=[(0, 1), (1, 2)]) + assert "qco.swap" not in qco.ir + assert "qco.ctrl" in qco.ir + assert "qco.ctrl(%0) targets (%arg0 = %2)" not in qco.ir