From 1e7112425e758b266ab26356a96ce1b615f335ee Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 4 Sep 2026 21:44:50 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Preserve=20reusable=20Qiskit=20gate?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Import custom Qiskit Gate definitions as private unitary QC functions and export supported QC calls back to Qiskit Gates. Preserve nested definitions, parameters, names, and supported modifiers while keeping generic Instructions flattened. Bound definition interning and validation keep large reusable-gate graphs compact and fail closed on unsupported inputs. Assisted-by: GPT-5.6 via Codex --- .agent/plans/qiskit-reusable-gates.md | 77 ++++ CHANGELOG.md | 7 +- bindings/mlir/CMakeLists.txt | 1 + bindings/mlir/qiskit/Qiskit2_5.cpp | 401 ++++++++++++++++++-- bindings/mlir/qiskit/QiskitExport.cpp | 229 +++++++++-- bindings/mlir/qiskit/QiskitImport.cpp | 307 +++++++++++---- bindings/mlir/qiskit/QiskitTranslation.h | 32 +- test/python/test_mlir_qiskit_translation.py | 376 ++++++++++++++++-- 8 files changed, 1259 insertions(+), 171 deletions(-) create mode 100644 .agent/plans/qiskit-reusable-gates.md diff --git a/.agent/plans/qiskit-reusable-gates.md b/.agent/plans/qiskit-reusable-gates.md new file mode 100644 index 0000000000..b18c2a945f --- /dev/null +++ b/.agent/plans/qiskit-reusable-gates.md @@ -0,0 +1,77 @@ +# Preserve reusable Qiskit gates in QC + +Status: complete. + +## Goal and scope + +Import nonstandard Qiskit `Gate` definitions as private QC unitary functions and +preserve their applications as `qc.call`. Export supported private unitary +functions and calls as Qiskit Gates. Nested and repeated definitions must stay +linear in the size of the definition graph and preserve parameters, public +names, global phases, qubit order, and supported modifiers. + +The normalized boundary is `bindings/mlir/qiskit/QiskitTranslation.h`. The +Qiskit 2.5 implementation is in `bindings/mlir/qiskit/Qiskit2_5.cpp`; +format-independent import and export are in `QiskitImport.cpp` and +`QiskitExport.cpp`. Behavioral coverage is in +`test/python/test_mlir_qiskit_translation.py`. + +Generic Qiskit `Instruction` definitions remain flattened. They can contain +classical bits, measurement, reset, and control flow, for which the unitary QC +function ABI is not valid. Gate functions cannot contain classical bits, +standalone classical variables, barriers, or nonunitary operations. + +## Decisions + +- Use Qiskit's public `Gate` type as the reusable-function boundary. Determine + built-in gates from Qiskit's standard-gate identity, not from user-controlled + names. +- Build a function from the bound definition circuit's remaining free + parameters. Qiskit specializes copied definitions during parameter binding, so + recovering an erased generic template would require private provenance. +- Intern definitions by source name and parameter hash, then use object identity + and Qiskit circuit equality within that bucket. Qiskit deep-copies a Gate and + its definition when appending it, so pointer identity alone duplicates equal + functions. Signature buckets avoid a global quadratic equality scan. +- Unique colliding MLIR symbols deterministically and retain the Qiskit name in + `mqt.source_name`. The first available symbol keeps the source name. +- Preserve inverse, closed-control, and finite numeric power modifiers around + custom Gate calls. Reject open controls and symbolic powers that Qiskit 2.5 + cannot bind reliably. +- Use MLIR `CallGraph` and LLVM SCC traversal for callee-first export order and + recursion rejection. Reject unreachable functions because a Qiskit circuit + cannot retain unused Gate declarations. +- Retain the 64-level definition and exported call-depth limits. Qiskit exposes + mutable Python definition graphs with arbitrary cycles, and parameterized + `QuantumCircuit.to_gate(parameter_map=...)` recursively copies nested + definitions. The export limit also keeps emitted circuits within the + importer's supported range. +- Construct custom Gates in the version-specific bridge through the existing + deferred-placeholder pattern. The Qiskit C API does not append arbitrary + Python Gates or `AnnotatedOperation` values. +- Do not add a classical callable ABI. Standalone variables and control-flow + captures remain entry- or block-owned and do not enter unitary functions. + +## Validation + +From the repository root, build and test with: + + cmake --build --preset release --target mqt-core-mlir-bindings -j2 + pytest -n0 -q test/python/test_mlir_qiskit_translation.py + uvx nox -s stubs + uvx nox -s cpp-lint + uvx nox -s lint + +The focused file passes all 294 tests. Stub generation, repository lint, and C++ +lint pass; the C++ lint session performs a clean build before running +clang-tidy. Hosted CI is separate evidence and must run on the final published +commit. + +## Outcome + +The implementation uses the existing QC function model, Qiskit public Gate +model, MLIR call graph, and normalized translation boundary. It adds no dialect +operation, external dependency, generic call-graph framework, or classical +function ABI. A parameterized QC helper can split into specialized helpers on a +Qiskit round trip because Qiskit's public circuit retains the specialized +definitions rather than their erased template. diff --git a/CHANGELOG.md b/CHANGELOG.md index 697a3154da..7839ac87b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,9 +45,9 @@ releases may include breaking changes. #### Import and export -- ✨ Add Qiskit circuit import and target-aware export to the compiler - collection ([#2031], [#2133], [#2140], [#2150], [#2175], [#2176], [#2178]) - ([**@burgholzer**], [**@simon1hofmann**]) +- ✨ Add Qiskit circuit import, target-aware export, and reusable custom Gate + round trips to the compiler collection ([#2031], [#2133], [#2140], [#2150], + [#2175], [#2176], [#2178], [#2342]) ([**@burgholzer**], [**@simon1hofmann**]) - ✨ Add conversions between `jeff` and QCO ([#1479], [#1548], [#1565], [#1637], [#1676], [#1706], [#1776], [#1836], [#1934], [#2000], [#2018], [#2105], [#2339]) ([**@denialhaag**], [**@burgholzer**]) @@ -928,6 +928,7 @@ for previous changelogs._ [#2368]: https://github.com/munich-quantum-toolkit/core/pull/2368 [#2358]: https://github.com/munich-quantum-toolkit/core/pull/2358 [#2349]: https://github.com/munich-quantum-toolkit/core/pull/2349 +[#2342]: https://github.com/munich-quantum-toolkit/core/pull/2342 [#2340]: https://github.com/munich-quantum-toolkit/core/pull/2340 [#2339]: https://github.com/munich-quantum-toolkit/core/pull/2339 [#2338]: https://github.com/munich-quantum-toolkit/core/pull/2338 diff --git a/bindings/mlir/CMakeLists.txt b/bindings/mlir/CMakeLists.txt index 5b79b97c43..f261c7ee20 100644 --- a/bindings/mlir/CMakeLists.txt +++ b/bindings/mlir/CMakeLists.txt @@ -83,6 +83,7 @@ if(NOT TARGET ${TARGET_NAME}) MQT::CoreBenchGenerate MQTCompilerQDMIAdapter MQTCompilerPipeline + MLIRAnalysis MLIRMQTDialect MLIRMQTUtils MLIRQCTranslationSupport diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index 3a8088f953..76ba6fa872 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include @@ -597,6 +598,21 @@ static void appendControlModifier(const nb::handle object, nb::module_::import_("qiskit.circuit").attr("Store")); } +[[nodiscard]] static bool isPythonGate(nb::handle operation) { + const auto terminal = terminalPythonGate(operation); + return nb::isinstance(terminal, + nb::module_::import_("qiskit.circuit").attr("Gate")); +} + +[[nodiscard]] static bool isPythonStandardGate(nb::handle operation) { + const auto terminal = terminalPythonGate(operation); + const auto baseClass = pythonAttribute( + terminal, "base_class", "Qiskit Gate does not expose its base class"); + return !pythonAttribute(baseClass, "_standard_gate", + "Qiskit Gate base class has no standard identity") + .is_none(); +} + static void normalizePythonModifier(const nb::handle modifier, std::vector& modifiers) { const auto type = pythonAttribute(modifier, "__class__", @@ -811,14 +827,56 @@ standardGateMapping(const std::string_view name) { namespace { class NativeControlFlowReader; +class DefinitionRegistry final { +public: + [[nodiscard]] uintptr_t identify(nb::handle definition, + const std::string_view name, + nb::handle parameters) { + const nb::tuple parameterTuple(parameters); + const auto parameterHash = PyObject_Hash(parameterTuple.ptr()); + if (parameterHash == -1) { + throwPythonError("Qiskit Gate parameters are not hashable"); + } + // ponytail: use a structural circuit hash if many same-signature Gate + // definitions become common. + auto& bucket = + definitions_[llvm::StringRef(name.data(), name.size())][parameterHash]; + for (const auto& entry : bucket) { + if (entry.definition.is(definition) || + entry.definition.equal(definition)) { + return entry.identity; + } + } + const auto identity = nextIdentity_++; + bucket.push_back({ + .definition = nb::borrow(definition), + .identity = identity, + }); + return identity; + } + +private: + struct Definition { + nb::object definition; + uintptr_t identity; + }; + + using ParameterBuckets = + std::unordered_map>; + llvm::StringMap definitions_; + uintptr_t nextIdentity_ = 1U; +}; + class NativeCircuitReader final : public CircuitReader { public: - explicit NativeCircuitReader(const nb::handle circuit) + NativeCircuitReader(nb::handle circuit, + std::shared_ptr definitions) : pythonCircuit_(nb::borrow(circuit)), data_(pythonAttribute( circuit, "_data", "expected a Qiskit QuantumCircuit with native CircuitData")), - circuit_(qk_circuit_borrow_from_python(data_.ptr())) { + circuit_(qk_circuit_borrow_from_python(data_.ptr())), + definitions_(std::move(definitions)) { if (circuit_ == nullptr) { throwPythonError("Qiskit rejected QuantumCircuit._data"); } @@ -827,12 +885,14 @@ class NativeCircuitReader final : public CircuitReader { NativeCircuitReader(nb::object pythonCircuit, const QkCircuit* circuit, const QkCircuit* rootCircuit, - const QkControlFlowInstruction* parent) + const QkControlFlowInstruction* parent, + std::shared_ptr definitions) : pythonCircuit_(std::move(pythonCircuit)), data_(pythonAttribute( pythonCircuit_, "_data", "Qiskit control-flow block has no native CircuitData")), - circuit_(circuit), rootCircuit_(rootCircuit), parent_(parent) {} + circuit_(circuit), rootCircuit_(rootCircuit), parent_(parent), + definitions_(std::move(definitions)) {} [[nodiscard]] uint32_t numQubits() const override { return qk_circuit_num_qubits(circuit_); @@ -966,6 +1026,9 @@ class NativeCircuitReader final : public CircuitReader { } normalizedUnknown.emplace(); normalizePythonGate(operation, *normalizedUnknown); + if (isPythonGate(operation)) { + normalizedUnknown->kind = OperationKind::Gate; + } } QkCircuitInstruction native{}; qk_circuit_get_instruction(circuit_, index, &native); @@ -1008,14 +1071,14 @@ class NativeCircuitReader final : public CircuitReader { result.parameters.emplace_back(normalizeParameter(parameter)); } } - if (result.kind == OperationKind::Unknown) { + if (kind == OperationKind::Unknown) { result.name = std::move(normalizedUnknown->name); result.modifiers = std::move(normalizedUnknown->modifiers); - if (!result.modifiers.empty()) { - result.kind = OperationKind::Gate; - } + result.kind = normalizedUnknown->kind; + } + if (kind != OperationKind::Unknown || isPythonStandardGate(operation)) { + result.standardGate = standardGateMapping(result.name); } - result.standardGate = standardGateMapping(result.name); return result; } @@ -1101,7 +1164,7 @@ class NativeCircuitReader final : public CircuitReader { [[nodiscard]] std::unique_ptr definition(const size_t index) const override { - const auto operation = pythonOperation(index); + const auto operation = terminalPythonGate(pythonOperation(index)); const auto definition = pythonAttribute( operation, "definition", "Qiskit instruction does not expose a circuit definition"); @@ -1110,18 +1173,23 @@ class NativeCircuitReader final : public CircuitReader { instruction(index).name + "' has no circuit definition"); } - return std::make_unique(definition); + return std::make_unique(definition, definitions_); } [[nodiscard]] uintptr_t definitionIdentity(const size_t index) const override { + const auto operation = terminalPythonGate(pythonOperation(index)); const auto definition = pythonAttribute( - pythonOperation(index), "definition", + operation, "definition", "Qiskit instruction does not expose a circuit definition"); if (definition.is_none()) { return 0U; } - return reinterpret_cast(definition.ptr()); + const auto name = pythonStringAttribute( + operation, "name", "Qiskit instruction has no valid name"); + const auto parameters = pythonAttribute( + operation, "params", "Qiskit instruction has no parameter list"); + return definitions_->identify(definition, name, parameters); } private: @@ -1164,6 +1232,7 @@ class NativeCircuitReader final : public CircuitReader { const QkCircuit* circuit_ = nullptr; const QkCircuit* rootCircuit_ = circuit_; const QkControlFlowInstruction* parent_ = nullptr; + std::shared_ptr definitions_; }; } // namespace @@ -1437,13 +1506,15 @@ class NativeControlFlowReader final : public ControlFlowReader { const QkCircuit* circuit, const size_t index, const QkControlFlowInstruction* parent, nb::object instruction, - nb::object containingPythonCircuit) + nb::object containingPythonCircuit, + std::shared_ptr definitions) : rootCircuit_(rootCircuit), circuit_(circuit), parent_(parent), instruction_(std::move(instruction)), operation_(pythonAttribute( instruction_, "operation", "Qiskit circuit instruction has no control-flow operation")), containingPythonCircuit_(std::move(containingPythonCircuit)), + definitions_(std::move(definitions)), controlFlow_( qk_circuit_get_control_flow_instruction(circuit, index, parent)) { if (controlFlow_ == nullptr) { @@ -1490,7 +1561,7 @@ class NativeControlFlowReader final : public ControlFlowReader { const auto block = nb::borrow(blocks[index]); return std::make_unique( block, qk_control_flow_block_circuit(controlFlow_, index), rootCircuit_, - controlFlow_); + controlFlow_, definitions_); } [[nodiscard]] std::vector qubitMap() const override { @@ -1726,6 +1797,7 @@ class NativeControlFlowReader final : public ControlFlowReader { nb::object instruction_; nb::object operation_; nb::object containingPythonCircuit_; + std::shared_ptr definitions_; QkControlFlowInstruction* controlFlow_ = nullptr; }; } // namespace @@ -1811,7 +1883,7 @@ std::unique_ptr NativeCircuitReader::controlFlow(const size_t index) const { return std::make_unique( rootCircuit_, circuit_, index, parent_, - nb::borrow(data_[index]), pythonCircuit_); + nb::borrow(data_[index]), pythonCircuit_, definitions_); } namespace { @@ -2091,13 +2163,17 @@ struct NativeSymbol { using NativeSymbolTable = llvm::StringMap; using PythonParameterGroups = llvm::StringMap; +using PythonSymbols = llvm::StringMap; + +using NativeGateRegistry = llvm::StringMap; class NativeCircuitWriter final : public CircuitWriter { public: NativeCircuitWriter(const uint32_t looseQubits, const uint32_t looseClbits, - std::shared_ptr symbols) + std::shared_ptr symbols, + std::shared_ptr gates) : circuit_(qk_circuit_new(looseQubits, looseClbits)), - symbols_(std::move(symbols)) { + symbols_(std::move(symbols)), gates_(std::move(gates)) { if (circuit_ == nullptr) { throwPythonError("Qiskit failed to allocate a circuit"); } @@ -2172,6 +2248,21 @@ class NativeCircuitWriter final : public CircuitWriter { "adding parameterized gate"); } + void addCustomGate(std::string_view name, const std::vector& qubits, + const std::vector& parameters, + const std::vector& modifiers) override { + const auto instructionIndex = qk_circuit_num_instructions(circuit_); + checkExitCode(qk_circuit_barrier(circuit_, nullptr, 0U), + "adding custom-gate placeholder"); + pendingCustomGates_.push_back({ + .instructionIndex = instructionIndex, + .name = std::string(name), + .qubits = qubits, + .parameters = parameters, + .modifiers = modifiers, + }); + } + void addMeasure(const uint32_t qubit, const uint32_t clbit) override { checkExitCode(qk_circuit_measure(circuit_, qubit, clbit), "adding measurement"); @@ -2290,8 +2381,9 @@ class NativeCircuitWriter final : public CircuitWriter { [[nodiscard]] nb::object finish() override { PythonParameterGroups groups; + PythonSymbols symbols; return finishImpl(false, nb::none(), nb::none(), nb::none(), nb::none(), - groups, {}); + groups, symbols, {}); } private: @@ -2299,7 +2391,7 @@ class NativeCircuitWriter final : public CircuitWriter { finishImpl(const bool rebase, const nb::handle exactQubits, const nb::handle exactClbits, const nb::handle exactQregs, const nb::handle exactCregs, PythonParameterGroups& groups, - PythonVariables variables) { + PythonSymbols& symbols, PythonVariables variables) { if (circuit_ == nullptr) { throw std::runtime_error( "Qiskit circuit writer has already been finalized"); @@ -2316,6 +2408,8 @@ class NativeCircuitWriter final : public CircuitWriter { exactQregs, exactCregs); } replacePendingControlledUnitaries(pythonCircuit); + synchronizePythonSymbols(pythonCircuit, symbols); + replacePendingCustomGates(pythonCircuit, symbols); restoreParameterGroups(pythonCircuit, *symbols_, groups); const PythonClassicalBuilder classical(pythonCircuit, variables); for (const auto& variable : variables_) { @@ -2331,7 +2425,7 @@ class NativeCircuitWriter final : public CircuitWriter { } } replacePendingStores(pythonCircuit, variables); - replacePendingControlFlow(pythonCircuit, groups, variables); + replacePendingControlFlow(pythonCircuit, groups, symbols, variables); } catch (const nb::python_error& error) { throwPythonError("Qiskit failed to construct deferred instructions", error); @@ -2351,6 +2445,14 @@ class NativeCircuitWriter final : public CircuitWriter { std::unique_ptr value; }; + struct PendingCustomGate { + size_t instructionIndex = 0U; + std::string name; + std::vector qubits; + std::vector parameters; + std::vector modifiers; + }; + struct PendingControlFlow { size_t instructionIndex = 0U; ControlFlowKind kind = ControlFlowKind::IfElse; @@ -2434,6 +2536,223 @@ class NativeCircuitWriter final : public CircuitWriter { } } + static void synchronizePythonSymbols(nb::handle circuit, + PythonSymbols& symbols) { + nb::dict replacements; + const auto parameters = pythonAttribute( + circuit, "parameters", "Qiskit circuit has no parameter collection"); + for (const nb::handle parameter : nb::iter(parameters)) { + const auto name = pythonStringAttribute( + parameter, "name", "Qiskit circuit parameter has no name"); + const auto [symbol, inserted] = + symbols.try_emplace(name, nb::borrow(parameter)); + if (!inserted && !symbol->second.equal(parameter)) { + replacements[parameter] = symbol->second; + } + } + if (nb::len(replacements) != 0U) { + pythonAttribute(circuit, "assign_parameters", + "Qiskit circuit cannot unify output parameters")( + replacements, nb::arg("inplace") = true, + nb::arg("flat_input") = true); + } + } + + [[nodiscard]] nb::object pythonParameter(const Parameter& parameter, + PythonSymbols& symbols, + size_t& nodeCount, size_t depth) { + countParameterExpressionNode(nodeCount); + if (depth > MAX_PARAMETER_EXPRESSION_DEPTH) { + throwParameterExpressionDepthError(); + } + if (const auto* number = parameter.getNumber()) { + return nb::float_(number->value); + } + if (const auto* symbol = parameter.getSymbol()) { + symbols_->try_emplace(symbol->name, symbol->name, symbol->group); + auto pythonSymbol = symbols.find(symbol->name); + if (pythonSymbol == symbols.end()) { + pythonSymbol = symbols + .try_emplace(symbol->name, + nb::module_::import_("qiskit.circuit") + .attr("Parameter")(symbol->name)) + .first; + } + return nb::borrow(pythonSymbol->second); + } + if (const auto* unary = parameter.getUnary()) { + auto operand = + pythonParameter(*unary->operand, symbols, nodeCount, depth + 1U); + if (nb::isinstance(operand)) { + auto numeric = nb::cast(operand); + switch (unary->operation) { + case UnaryParameterKind::Negate: + numeric = -numeric; + break; + case UnaryParameterKind::Sin: + numeric = std::sin(numeric); + break; + case UnaryParameterKind::Cos: + numeric = std::cos(numeric); + break; + case UnaryParameterKind::Tan: + numeric = std::tan(numeric); + break; + case UnaryParameterKind::ArcSin: + numeric = std::asin(numeric); + break; + case UnaryParameterKind::ArcCos: + numeric = std::acos(numeric); + break; + case UnaryParameterKind::ArcTan: + numeric = std::atan(numeric); + break; + case UnaryParameterKind::Exp: + numeric = std::exp(numeric); + break; + case UnaryParameterKind::Log: + numeric = std::log(numeric); + break; + case UnaryParameterKind::Abs: + numeric = std::abs(numeric); + break; + case UnaryParameterKind::Conjugate: + break; + } + if (!std::isfinite(numeric)) { + throw std::runtime_error( + "cannot construct a non-finite Qiskit parameter"); + } + return nb::float_(numeric); + } + switch (unary->operation) { + case UnaryParameterKind::Negate: + return -operand; + case UnaryParameterKind::Sin: + return operand.attr("sin")(); + case UnaryParameterKind::Cos: + return operand.attr("cos")(); + case UnaryParameterKind::Tan: + return operand.attr("tan")(); + case UnaryParameterKind::ArcSin: + return operand.attr("arcsin")(); + case UnaryParameterKind::ArcCos: + return operand.attr("arccos")(); + case UnaryParameterKind::ArcTan: + return operand.attr("arctan")(); + case UnaryParameterKind::Exp: + return operand.attr("exp")(); + case UnaryParameterKind::Log: + return operand.attr("log")(); + case UnaryParameterKind::Abs: + return operand.attr("abs")(); + case UnaryParameterKind::Conjugate: + return operand.attr("conjugate")(); + } + } + if (const auto* binary = parameter.getBinary()) { + auto left = + pythonParameter(*binary->left, symbols, nodeCount, depth + 1U); + auto right = + pythonParameter(*binary->right, symbols, nodeCount, depth + 1U); + switch (binary->operation) { + case BinaryParameterKind::Add: + return left + right; + case BinaryParameterKind::Subtract: + return left - right; + case BinaryParameterKind::Multiply: + return left * right; + case BinaryParameterKind::Divide: + return left / right; + case BinaryParameterKind::Power: + return nb::module_::import_("builtins").attr("pow")(left, right); + } + } + throw std::runtime_error("unknown normalized parameter expression"); + } + + [[nodiscard]] nb::object pythonParameter(const Parameter& parameter, + PythonSymbols& symbols) { + size_t nodeCount = 0U; + return pythonParameter(parameter, symbols, nodeCount, 1U); + } + + void replacePendingCustomGates(nb::handle pythonCircuit, + PythonSymbols& symbols) { + auto data = pythonAttribute(pythonCircuit, "data", + "Qiskit circuit has no instruction data"); + const auto circuitQubits = pythonAttribute(pythonCircuit, "qubits", + "Qiskit circuit has no qubits"); + const auto circuitModule = nb::module_::import_("qiskit.circuit"); + for (const auto& pending : pendingCustomGates_) { + if (pending.instructionIndex >= nb::len(data)) { + throw std::runtime_error("Qiskit custom-gate placeholder is missing"); + } + const auto gate = gates_->find(pending.name); + if (gate == gates_->end()) { + throw std::runtime_error("Qiskit custom Gate '" + pending.name + + "' has no registered definition"); + } + const nb::object formalParameters = gate->second.attr("params"); + if (nb::len(formalParameters) != pending.parameters.size()) { + throw std::runtime_error("Qiskit custom Gate '" + pending.name + + "' has incompatible parameters"); + } + nb::dict parameterMap; + for (size_t index = 0U; index < pending.parameters.size(); ++index) { + parameterMap[formalParameters[index]] = + pythonParameter(pending.parameters[index], symbols); + } + nb::object operation = gate->second; + if (!pending.parameters.empty()) { + operation = + pythonAttribute(operation.attr("definition"), "to_gate", + "Qiskit custom definition cannot become a Gate")( + nb::arg("parameter_map") = parameterMap); + } + if (!pending.modifiers.empty()) { + nb::list modifiers; + for (const auto& modifier : pending.modifiers) { + switch (modifier.kind) { + case GateModifierKind::Inverse: + modifiers.append(circuitModule.attr("InverseModifier")()); + break; + case GateModifierKind::Control: + modifiers.append( + circuitModule.attr("ControlModifier")(modifier.numControls)); + break; + case GateModifierKind::Power: { + const auto* exponent = modifier.exponent.getNumber(); + if (exponent == nullptr) { + throw std::runtime_error( + "Qiskit custom Gate power must be numeric"); + } + modifiers.append( + circuitModule.attr("PowerModifier")(exponent->value)); + break; + } + } + } + operation = + circuitModule.attr("AnnotatedOperation")(operation, modifiers); + } + nb::list qargs; + for (const auto qubit : pending.qubits) { + if (qubit >= nb::len(circuitQubits)) { + throw std::runtime_error( + "Qiskit custom Gate references an invalid qubit"); + } + qargs.append(circuitQubits[qubit]); + } + const auto placeholder = + nb::borrow(data[pending.instructionIndex]); + data[pending.instructionIndex] = + pythonAttribute(placeholder, "replace", + "Qiskit custom-gate placeholder cannot be replaced")( + nb::arg("operation") = operation, nb::arg("qubits") = qargs); + } + } + [[nodiscard]] static nb::object rebaseCircuit(const nb::handle circuit, const nb::handle exactQubits, const nb::handle exactClbits, @@ -2555,6 +2874,7 @@ class NativeCircuitWriter final : public CircuitWriter { void replacePendingControlFlow(const nb::handle pythonCircuit, PythonParameterGroups& groups, + PythonSymbols& symbols, const PythonVariables& variables) { auto data = pythonAttribute(pythonCircuit, "data", "Qiskit circuit has no instruction data"); @@ -2584,7 +2904,7 @@ class NativeCircuitWriter final : public CircuitWriter { } blocks.emplace_back( writer->finishImpl(true, circuitQubits, circuitClbits, circuitQregs, - circuitCregs, groups, variables)); + circuitCregs, groups, symbols, variables)); for (auto captured : nb::iter(blocks.back().attr("iter_captured_vars")())) { if (!nb::cast(pythonCircuit.attr("has_var")(captured))) { @@ -2716,15 +3036,18 @@ class NativeCircuitWriter final : public CircuitWriter { std::vector pendingControlledUnitaries_; std::vector pendingStores_; std::vector variables_; + std::vector pendingCustomGates_; std::vector pendingControlFlow_; std::shared_ptr symbols_; + std::shared_ptr gates_; }; class NativeTranslation final : public VersionedTranslation { public: [[nodiscard]] std::unique_ptr openCircuit(const nb::handle circuit) const override { - return std::make_unique(circuit); + return std::make_unique( + circuit, std::make_shared()); } [[nodiscard]] bool supportsGate(const StandardGateMapping gate) const override { @@ -2735,12 +3058,42 @@ class NativeTranslation final : public VersionedTranslation { createCircuit(const uint32_t looseQubits, const uint32_t looseClbits) const override { return std::make_unique(looseQubits, looseClbits, - symbols_); + symbols_, gates_); + } + + void registerCustomGate(std::string_view symbol, std::string_view name, + const std::vector& formalParameters, + std::unique_ptr definition) override { + if (gates_->contains(symbol)) { + throw std::runtime_error("Qiskit custom Gate '" + std::string(symbol) + + "' is already registered"); + } + auto circuit = definition->finish(); + circuit.attr("name") = nb::str(name.data(), name.size()); + nb::list parameters; + try { + for (const auto& parameter : formalParameters) { + parameters.append(pythonAttribute( + circuit, "get_parameter", + "Qiskit custom definition cannot resolve a formal parameter")( + parameter)); + } + } catch (const nb::python_error& error) { + throwPythonError( + "Qiskit custom definition cannot resolve a formal parameter", error); + } + auto gate = nb::module_::import_("qiskit.circuit") + .attr("Gate")(nb::str(name.data(), name.size()), + circuit.attr("num_qubits"), parameters); + gate.attr("definition") = circuit; + gates_->try_emplace(symbol, std::move(gate)); } private: std::shared_ptr symbols_ = std::make_shared(); + std::shared_ptr gates_ = + std::make_shared(); }; } // namespace diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index 0bcc8ff176..b8508c14c8 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -29,11 +29,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -72,8 +74,9 @@ namespace mqt::bindings::qiskit { constexpr size_t MAX_EXPORT_CONTROL_FLOW_DEPTH = 64U; constexpr size_t MAX_EXPORT_EXPRESSION_DEPTH = 64U; -/// Bounded bit-based casts between jeff native widths expand expression trees. +// Bounded bit-based casts between jeff native widths expand expression trees. constexpr size_t MAX_EXPORT_EXPRESSION_NODES = 16384U; +constexpr size_t MAX_EXPORT_GATE_CALL_DEPTH = 64U; namespace { struct ExportedControlFlow; @@ -81,6 +84,7 @@ struct ExportedControlFlow; struct ExportedInstruction { enum class Kind : uint8_t { Gate, + CustomGate, Measure, Reset, Barrier, @@ -90,10 +94,12 @@ struct ExportedInstruction { }; Kind kind = Kind::Gate; StandardGateMapping gate; + std::string customGate; std::vector qubits; std::vector clbits; std::vector parameters; std::vector> matrix; + std::vector modifiers; uint32_t unitaryControls = 0; ClassicalTarget target; std::unique_ptr value; @@ -108,6 +114,14 @@ struct ExportedCircuit { std::vector instructions; }; +struct ExportedGateDefinition { + std::string symbol; + std::string name; + std::vector formalParameters; + uint32_t numQubits = 0U; + ExportedCircuit circuit; +}; + struct ExportedControlFlow { ControlFlowKind kind = ControlFlowKind::IfElse; ClassicalTarget target; @@ -470,9 +484,10 @@ static void validateExportParameters(const ExportedCircuit& circuit, } } -static void collectParameters(mlir::func::FuncOp function, ExportState& state) { - for (const auto [index, argument] : - llvm::enumerate(function.getArguments())) { +static void collectParameters(mlir::func::FuncOp function, ExportState& state, + size_t count) { + for (size_t index = 0U; index < count; ++index) { + auto argument = function.getArgument(index); const auto name = function.getArgAttrOfType( index, mlir::mqt::MQTDialect::InputNameAttrHelper::getNameStr()); if (!argument.getType().isF64() || !name) { @@ -675,6 +690,17 @@ static void invertGate(ExportedInstruction& instruction) { collectUnitaryInstruction(mlir::Operation& operation, const llvm::DenseMap& qubits, ExportedParameters& parameters) { + if (auto call = llvm::dyn_cast(operation)) { + ExportedInstruction result{ + .kind = ExportedInstruction::Kind::CustomGate, + .customGate = call.getCallee().str(), + .qubits = mapQubits(call.getQubits(), qubits), + }; + for (auto parameter : call.getParameters()) { + result.parameters.push_back(exportParameter(parameter, parameters)); + } + return result; + } if (auto control = llvm::dyn_cast(operation)) { auto bodyOperations = modifierBodyOperations(control.getRegion()); const auto controls = mapQubits(control.getControls(), qubits); @@ -703,10 +729,20 @@ collectUnitaryInstruction(mlir::Operation& operation, } if (bodyOperations.size() != 1U) { throw std::runtime_error( - "QC control export requires one standard gate in the modifier body"); + "QC control export requires one gate in the modifier body"); } auto result = collectUnitaryInstruction(*bodyOperations.front(), nestedMap, parameters); + if (result.kind == ExportedInstruction::Kind::CustomGate) { + result.modifiers.push_back({ + .kind = GateModifierKind::Control, + .numControls = + checkedIndex(static_cast(controls.size()), "control"), + }); + result.qubits.insert(result.qubits.begin(), controls.begin(), + controls.end()); + return result; + } auto& numControls = result.kind == ExportedInstruction::Kind::Unitary ? result.unitaryControls : result.gate.controls; @@ -723,31 +759,45 @@ collectUnitaryInstruction(mlir::Operation& operation, auto bodyOperations = modifierBodyOperations(inverse.getRegion()); if (bodyOperations.size() != 1U) { throw std::runtime_error( - "QC inverse export requires one standard gate in the modifier body"); + "QC inverse export requires one gate in the modifier body"); } auto nestedMap = modifierQubitMap(qubits, inverse.getRegion().front(), inverse.getQubits()); auto result = collectUnitaryInstruction(*bodyOperations.front(), nestedMap, parameters); - invertGate(result); + if (result.kind == ExportedInstruction::Kind::CustomGate) { + result.modifiers.push_back({.kind = GateModifierKind::Inverse}); + } else { + invertGate(result); + } return result; } if (auto power = llvm::dyn_cast(operation)) { auto bodyOperations = modifierBodyOperations(power.getRegion()); if (bodyOperations.size() != 1U) { throw std::runtime_error( - "QC power export requires one standard gate in the modifier body"); + "QC power export requires one gate in the modifier body"); } const auto exponent = exportParameter(power.getExponent(), parameters); const auto* number = exponent.getNumber(); - if (number == nullptr || (number->value != 1.0 && number->value != -1.0)) { - throw std::runtime_error( - "QC power export supports only constant exponents 1 and -1"); - } auto nestedMap = modifierQubitMap(qubits, power.getRegion().front(), power.getQubits()); auto result = collectUnitaryInstruction(*bodyOperations.front(), nestedMap, parameters); + if (result.kind == ExportedInstruction::Kind::CustomGate) { + if (number == nullptr || !std::isfinite(number->value)) { + throw std::runtime_error( + "Qiskit custom Gate power must have a finite numeric exponent"); + } + result.modifiers.push_back( + {.kind = GateModifierKind::Power, .exponent = exponent}); + return result; + } + if (number == nullptr || (number->value != 1.0 && number->value != -1.0)) { + throw std::runtime_error( + "QC power export supports only constant exponents 1 and -1 for " + "standard gates and dense unitaries"); + } if (number->value == -1.0) { invertGate(result); } @@ -915,8 +965,8 @@ static void collectResources(mlir::func::FuncOp function, ExportState& state, } } llvm::append_range(registers, returnedValues); - /// Qiskit exposes all register storage; put observable outputs last in count - /// order. + // Qiskit exposes all register storage. Put observable outputs last in count + // order. for (auto result : registers) { if (auto alloc = result.getDefiningOp()) { if (const auto name = alloc->getAttrOfType( @@ -1305,7 +1355,7 @@ exportExpressionImpl(mlir::Value value, ExportState& state, return operand; } if (operand->kind == ExpressionKind::ClassicalRegister) { - /// Qiskit's OpenQASM exporter needs a matching-width register cast first. + // Qiskit's OpenQASM exporter needs a matching-width register cast first. countExpressionNode(nodeCount); auto exact = std::make_unique(); exact->kind = ExpressionKind::Cast; @@ -2631,6 +2681,19 @@ validateConstructibleGates(const ExportedCircuit& circuit, descriptor.operationSymbol.str() + "' with " + std::to_string(instruction.gate.controls) + " controls"); } + if (instruction.kind == ExportedInstruction::Kind::CustomGate) { + llvm::DenseSet qubits; + if (instruction.customGate.empty()) { + throw std::runtime_error("Qiskit custom Gate has an empty name"); + } + for (const auto qubit : instruction.qubits) { + if (!qubits.insert(qubit).second) { + throw std::runtime_error("Qiskit custom Gate '" + + instruction.customGate + + "' uses the same qubit more than once"); + } + } + } if (instruction.kind != ExportedInstruction::Kind::ControlFlow) { continue; } @@ -2653,6 +2716,10 @@ static void emitCircuit(ExportedCircuit& circuit, CircuitWriter& writer, writer.addGate(instruction.gate, instruction.qubits, instruction.parameters); break; + case ExportedInstruction::Kind::CustomGate: + writer.addCustomGate(instruction.customGate, instruction.qubits, + instruction.parameters, instruction.modifiers); + break; case ExportedInstruction::Kind::Measure: writer.addMeasure(instruction.qubits.at(0), instruction.clbits.at(0)); break; @@ -2688,32 +2755,138 @@ static void emitCircuit(ExportedCircuit& circuit, CircuitWriter& writer, } } +[[nodiscard]] static size_t gateParameterCount(mlir::func::FuncOp function) { + size_t numParameters = 0U; + while (numParameters < function.getNumArguments() && + function.getArgument(numParameters).getType().isF64()) { + ++numParameters; + } + return numParameters; +} + +static void validateGateFunction(mlir::func::FuncOp function) { + if (!mlir::mqt::isUnitaryFunction(function)) { + throw std::runtime_error( + "Qiskit custom Gates require private, defined, single-block unitary " + "functions with no results"); + } + if (function + .walk( + [](mlir::qc::BarrierOp) { return mlir::WalkResult::interrupt(); }) + .wasInterrupted()) { + throw std::runtime_error( + "Qiskit custom Gate definitions cannot contain barriers"); + } +} + +[[nodiscard]] static std::vector +collectGateFunctions(mlir::ModuleOp moduleOp, mlir::func::FuncOp entryPoint) { + const mlir::CallGraph graph(moduleOp); + llvm::DenseMap gateCallDepths; + std::vector ordered; + for (auto component = + llvm::scc_iterator>:: + begin(graph.lookupNode(&entryPoint.getBody())); + !component.isAtEnd(); ++component) { + auto* node = component->front(); + if (node->isExternal() || component.hasCycle()) { + throw std::runtime_error( + "Qiskit custom Gates require defined, non-recursive functions"); + } + auto function = mlir::dyn_cast( + node->getCallableRegion()->getParentOp()); + if (!function) { + throw std::runtime_error( + "Qiskit custom Gates require func.func definitions"); + } + const auto ownDepth = function == entryPoint ? 0U : 1U; + size_t depth = ownDepth; + for (const auto& edge : *node) { + depth = + std::max(depth, gateCallDepths.lookup(edge.getTarget()) + ownDepth); + } + if (depth > MAX_EXPORT_GATE_CALL_DEPTH) { + throw std::runtime_error( + "Qiskit custom Gate calls exceed the nesting limit of 64"); + } + gateCallDepths[node] = depth; + if (function != entryPoint) { + validateGateFunction(function); + ordered.push_back(function); + } + } + for (auto function : moduleOp.getOps()) { + if (!gateCallDepths.contains(graph.lookupNode(&function.getBody()))) { + throw std::runtime_error("Qiskit circuit export cannot preserve " + "function '" + + function.getName().str() + "'"); + } + } + return ordered; +} + +[[nodiscard]] static ExportedGateDefinition +collectGateDefinition(mlir::func::FuncOp function) { + const auto numParameters = gateParameterCount(function); + ExportState state; + collectParameters(function, state, numParameters); + const auto numQubits = function.getNumArguments() - numParameters; + state.numQubits = checkedIndex(static_cast(numQubits), "qubit"); + for (auto [index, argument] : + llvm::enumerate(function.getArguments().drop_front(numParameters))) { + state.qubits[argument] = + checkedIndex(static_cast(index), "qubit"); + } + auto circuit = collectBlock(function.getBody().front(), state, 0U); + validateExportParameters(circuit, state.inputParameters); + std::vector formalParameters; + formalParameters.reserve(state.inputParameters.size()); + for (const auto& parameter : state.inputParameters) { + formalParameters.push_back(parameter.getSymbol()->name); + } + auto sourceName = function->getAttrOfType( + mlir::mqt::MQTDialect::SourceNameAttrHelper::getNameStr()); + return { + .symbol = function.getName().str(), + .name = sourceName ? sourceName.str() : function.getName().str(), + .formalParameters = std::move(formalParameters), + .numQubits = state.numQubits, + .circuit = std::move(circuit), + }; +} + nb::object exportCircuit(const mlir::QCProgram& program, const mlir::CompilerTarget* const target) { mlir::OwningOpRef expanded = program.module().clone(); auto moduleOp = *expanded; mlir::RewritePatternSet patterns(moduleOp.getContext()); mlir::mqt::populateIntegerExpansionPatterns(patterns); - /// Expand missing operations and eliminate dead expressions, without folding - /// unrelated control flow or changing the source program. + // Expand missing operations and eliminate dead expressions without folding + // unrelated control flow or changing the source program. if (mlir::failed(mlir::applyPatternsGreedily( moduleOp, std::move(patterns), mlir::GreedyRewriteConfig().enableFolding(false)))) { throw std::runtime_error("failed to expand integer operations for Qiskit"); } - const auto functions = moduleOp.getOps(); - if (functions.empty() || !llvm::hasSingleElement(functions)) { + auto function = mlir::mqt::getEntryPoint(moduleOp); + if (!function) { throw std::runtime_error( - "QC to Qiskit export requires exactly one entry function"); + "QC to Qiskit export requires an mqt.entry_point function"); } - auto function = *functions.begin(); if (function.getBody().empty() || !llvm::hasSingleElement(function.getBody())) { throw std::runtime_error( "QC to Qiskit export requires a single-block entry function"); } + const auto gateFunctions = collectGateFunctions(moduleOp, function); + std::vector gateDefinitions; + gateDefinitions.reserve(gateFunctions.size()); + for (auto gateFunction : gateFunctions) { + gateDefinitions.push_back(collectGateDefinition(gateFunction)); + } ExportState state; - collectParameters(function, state); + collectParameters(function, state, function.getNumArguments()); if (target != nullptr) { state.numQubits = checkedIndex(static_cast(target->numSites()), "target qubit count"); @@ -2756,7 +2929,19 @@ nb::object exportCircuit(const mlir::QCProgram& program, state.numClbits, "classical"); auto translation = selectTranslation(); + for (const auto& definition : gateDefinitions) { + validateConstructibleGates(definition.circuit, *translation); + } validateConstructibleGates(circuit, *translation); + for (auto& definition : gateDefinitions) { + auto definitionWriter = + translation->createCircuit(definition.numQubits, 0U); + emitCircuit(definition.circuit, *definitionWriter, *translation, + definition.numQubits, 0U); + translation->registerCustomGate(definition.symbol, definition.name, + definition.formalParameters, + std::move(definitionWriter)); + } auto writer = translation->createCircuit(looseQubits, looseClbits); for (const auto& reg : state.quantumRegisters) { writer->addQuantumRegister(reg.name, diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index d90b10892e..4a87cdda2f 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -12,7 +12,6 @@ #include "Qiskit.h" // IWYU pragma: keep #include "QiskitTranslation.h" #include "QiskitVersion.h" -#include "jeff/IR/JeffDialect.h" #include "mlir/Compiler/Programs.h" #include "mlir/Dialect/CBit/IR/CBitDialect.h" #include "mlir/Dialect/CBit/IR/CBitOps.h" @@ -21,8 +20,6 @@ #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/Translation/StandardGate.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" -#include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Support/IntegerExpressions.h" #include @@ -42,7 +39,6 @@ #include #include #include -#include #include #include #include @@ -51,15 +47,11 @@ #include #include #include -#include -#include #include #include #include #include #include -#include -#include #include #include @@ -89,6 +81,18 @@ using LocalParameters = llvm::StringMap; using GlobalParameters = llvm::StringMap; using ValidationParameters = llvm::StringMap; +namespace { +struct GateImportState { + explicit GateImportState(ParameterGroupRegistry& groups) + : parameterGroups(groups) {} + + llvm::DenseMap gates; + llvm::StringSet<> functionNames; + llvm::StringMap nextFunctionSuffix; + ParameterGroupRegistry& parameterGroups; +}; +} // namespace + constexpr size_t MAX_DEFINITION_DEPTH = 64U; constexpr size_t MAX_CONTROL_FLOW_DEPTH = 64U; constexpr size_t MAX_EXPANDED_OPERATIONS = 10'000'000U; @@ -126,22 +130,6 @@ parameterGroupAttribute(mlir::Builder& builder, const ParameterGroup& group) { std::to_string(MAX_PARAMETER_EXPRESSION_DEPTH) + "-level nesting depth"); } -[[nodiscard]] static std::shared_ptr createContext() { - mlir::DialectRegistry registry; - registry.insert(); - mlir::registerBuiltinDialectTranslation(registry); - mlir::registerLLVMDialectTranslation(registry); - auto context = std::make_shared(registry); - context->loadAllAvailableDialects(); - return context; -} - static void validateParameterImpl(const Parameter& parameter, const ValidationParameters& localParameters, const ValidationParameters& freeParameters, @@ -242,7 +230,7 @@ parameterValueImpl(mlir::qc::QCProgramBuilder& builder, if (const auto* unary = parameter.getUnary()) { if (unary->operation == UnaryParameterKind::Conjugate) { - /// QC scalar parameters are real-valued, so conjugation is the identity. + // QC scalar parameters are real-valued, so conjugation is the identity. return parameterValueImpl(builder, *unary->operand, localParameters, globalParameters, depth + 1U, nodes); } @@ -1292,7 +1280,8 @@ static void translateCircuit(mlir::qc::QCProgramBuilder& builder, llvm::ArrayRef classicalBits, const LocalParameters& localParameters, const GlobalParameters& globalParameters, - size_t definitionDepth, size_t controlFlowDepth, + GateImportState& gateState, size_t definitionDepth, + size_t controlFlowDepth, ImportedVariables& variables); [[nodiscard]] static int64_t rangeLength(const Loop& loop) { @@ -1375,6 +1364,7 @@ static void translateControlFlow(mlir::qc::QCProgramBuilder& builder, llvm::ArrayRef rootClbitMap, const LocalParameters& localParameters, const GlobalParameters& globalParameters, + GateImportState& gateState, const size_t definitionDepth, const size_t controlFlowDepth, ImportedVariables& variables) { @@ -1404,8 +1394,8 @@ static void translateControlFlow(mlir::qc::QCProgramBuilder& builder, const LocalParameters& parameters) { translateCircuit(builder, block, qubitMap, clbitMap, rootQubitMap, rootClbitMap, allQubits, classicalBits, parameters, - globalParameters, definitionDepth, controlFlowDepth + 1U, - variables); + globalParameters, gateState, definitionDepth, + controlFlowDepth + 1U, variables); }; const auto translateLoopWithJumps = @@ -1782,7 +1772,7 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, const llvm::ArrayRef classicalBits, const LocalParameters& localParameters, const GlobalParameters& globalParameters, - const size_t definitionDepth, + GateImportState& gateState, const size_t definitionDepth, const size_t controlFlowDepth, ImportedVariables& variables) { std::vector declared; @@ -1805,8 +1795,8 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, throw std::runtime_error( "Qiskit variable is declared more than once in an enclosing scope"); } - /// Definite initialization rejects reads until a store; the initial loop - /// state still needs a representable, unobservable scalar placeholder. + // Definite initialization rejects reads until a store. The initial loop + // state still needs a representable, unobservable scalar placeholder. variables.variables.emplace( variable.identity, ImportedVariables::Entry{ .value = mlir::arith::ConstantOp::create( @@ -1864,7 +1854,96 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, translateCircuit(builder, *definition, definitionQubits, definitionClbits, definitionQubits, definitionClbits, allQubits, classicalBits, localParameters, globalParameters, - definitionDepth + 1U, controlFlowDepth, variables); + gateState, definitionDepth + 1U, controlFlowDepth, + variables); + }; + const auto translateGate = [&](size_t index, const Instruction& instruction) { + auto definition = circuit.definition(index); + const auto definitionParameters = definition->parameters(); + const auto identity = circuit.definitionIdentity(index); + auto known = gateState.gates.find(identity); + auto function = + known == gateState.gates.end() ? mlir::func::FuncOp{} : known->second; + if (!function) { + llvm::SmallVector argumentTypes(definitionParameters.size(), + builder.getF64Type()); + argumentTypes.append(definition->numQubits(), + mlir::qc::QubitType::get(builder.getContext())); + std::string symbol = instruction.name; + if (!gateState.functionNames.insert(symbol).second) { + auto& suffix = gateState.nextFunctionSuffix[instruction.name]; + do { + symbol = instruction.name + "_" + std::to_string(suffix++); + } while (!gateState.functionNames.insert(symbol).second); + } + function = builder.createUnitaryFunction( + symbol, argumentTypes, [&](mlir::ValueRange arguments) { + LocalParameters formalParameters; + for (const auto [parameterIndex, parameter] : + llvm::enumerate(definitionParameters)) { + const auto* symbol = parameter.getSymbol(); + if (symbol == nullptr || symbol->name.empty()) { + throw std::runtime_error( + "Qiskit Gate definition has an invalid formal parameter"); + } + formalParameters[symbol->name] = arguments[parameterIndex]; + } + llvm::SmallVector definitionQubits( + arguments.drop_front(definitionParameters.size())); + std::vector definitionQubitMap(definitionQubits.size()); + std::iota(definitionQubitMap.begin(), definitionQubitMap.end(), 0U); + ImportedVariables gateVariables; + translateCircuit(builder, *definition, definitionQubitMap, {}, + definitionQubitMap, {}, definitionQubits, {}, + formalParameters, {}, gateState, + definitionDepth + 1U, controlFlowDepth, + gateVariables); + }); + if (symbol != instruction.name) { + function->setAttr( + mlir::mqt::MQTDialect::SourceNameAttrHelper::getNameStr(), + builder.getStringAttr(instruction.name)); + } + for (const auto [parameterIndex, parameter] : + llvm::enumerate(definitionParameters)) { + const auto* symbol = parameter.getSymbol(); + function.setArgAttr( + parameterIndex, + mlir::mqt::MQTDialect::InputNameAttrHelper::getNameStr(), + builder.getStringAttr(symbol->name)); + if (symbol->group) { + gateState.parameterGroups.add(*symbol->group); + function.setArgAttr( + parameterIndex, + mlir::mqt::MQTDialect::ParameterGroupAttrHelper::getNameStr(), + parameterGroupAttribute(builder, *symbol->group)); + } + } + gateState.gates.insert({identity, function}); + } + + llvm::SmallVector qubits; + qubits.reserve(instruction.qubits.size()); + for (const auto qubit : instruction.qubits) { + qubits.push_back(getQubit(qubit)); + } + llvm::SmallVector parameters; + for (const auto& parameter : definitionParameters) { + auto value = + parameterValue(builder, parameter, localParameters, globalParameters); + parameters.push_back(std::holds_alternative(value) + ? floatConstant(builder, std::get(value)) + : std::get(value)); + } + emitModifiedOperation( + builder, instruction, qubits, + modifiedQubitArity(instruction, definition->numQubits()), + localParameters, globalParameters, + [&](mlir::ValueRange targetArguments) { + llvm::SmallVector operands(parameters); + llvm::append_range(operands, targetArguments); + builder.call(function, operands); + }); }; for (size_t index = 0; index < circuit.numInstructions(); ++index) { @@ -1878,7 +1957,7 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, emitGate(builder, instruction, allQubits, qubitMap, localParameters, globalParameters); } else { - translateDefinition(index, instruction); + translateGate(index, instruction); } break; case OperationKind::Barrier: { @@ -1933,8 +2012,8 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, const auto controlFlow = circuit.controlFlow(index); translateControlFlow(builder, *controlFlow, allQubits, classicalBits, rootQubitMap, rootClbitMap, localParameters, - globalParameters, definitionDepth, controlFlowDepth, - variables); + globalParameters, gateState, definitionDepth, + controlFlowDepth, variables); break; } case OperationKind::Delay: @@ -1956,6 +2035,8 @@ struct ExpansionSummary { struct ExpansionCountState { llvm::DenseMap definitions; llvm::DenseSet activeDefinitions; + llvm::DenseSet materializedGateDefinitions; + size_t materializedDefinitionOperations = 0U; }; } // namespace @@ -1984,10 +2065,11 @@ expansionSummary(const CircuitReader& circuit, ExpansionCountState& state, for (size_t index = 0; index < circuit.numInstructions(); ++index) { addExpandedOperations(result.operations, 1U); const auto instruction = circuit.instruction(index); - if ((instruction.kind == OperationKind::Gate && - !instruction.standardGate) || - instruction.kind == OperationKind::Unknown) { - if (!instruction.modifiers.empty()) { + const bool customGate = + instruction.kind == OperationKind::Gate && !instruction.standardGate; + if (customGate || instruction.kind == OperationKind::Unknown) { + if (instruction.kind == OperationKind::Unknown && + !instruction.modifiers.empty()) { throw std::runtime_error( "Qiskit circuit import does not support modifiers on custom " "instructions"); @@ -2002,17 +2084,27 @@ expansionSummary(const CircuitReader& circuit, ExpansionCountState& state, "Qiskit instruction definitions contain a cycle"); } ExpansionSummary definitionSummary; + bool materializeGate = false; try { if (definitionDepth >= MAX_DEFINITION_DEPTH) { throw std::runtime_error( "Qiskit instruction definitions exceed the nesting limit of 64"); } const auto definition = circuit.definition(index); - if (definition->numQubits() != instruction.qubits.size() || + const auto controls = + customGate ? modifierControlCount(instruction) : 0U; + if (definition->numQubits() + controls != instruction.qubits.size() || definition->numClbits() != instruction.clbits.size()) { throw std::runtime_error("Qiskit instruction '" + instruction.name + "' does not match its definition arity"); } + if (customGate) { + if (instruction.name.empty()) { + throw std::runtime_error("Qiskit Gate has an empty name"); + } + materializeGate = + state.materializedGateDefinitions.insert(identity).second; + } if (const auto cached = state.definitions.find(identity); cached != state.definitions.end()) { definitionSummary = cached->second; @@ -2040,7 +2132,14 @@ expansionSummary(const CircuitReader& circuit, ExpansionCountState& state, } result.controlFlowDepth = std::max(result.controlFlowDepth, definitionSummary.controlFlowDepth); - addExpandedOperations(result.operations, definitionSummary.operations); + if (customGate) { + if (materializeGate) { + addExpandedOperations(state.materializedDefinitionOperations, + definitionSummary.operations); + } + } else { + addExpandedOperations(result.operations, definitionSummary.operations); + } continue; } if (instruction.kind != OperationKind::ControlFlow) { @@ -2079,8 +2178,10 @@ static void validateCircuit(const CircuitReader& circuit, const ValidationParameters& localParameters, const ValidationParameters& freeParameters, llvm::StringSet<>& parameterNames, + llvm::DenseSet& validatedGateDefinitions, uint32_t rootQubits, uint32_t rootClbits, - size_t definitionDepth, size_t controlFlowDepth); + size_t definitionDepth, size_t controlFlowDepth, + bool gateDefinition); static void validateExpression(const Expression& expression, const uint32_t rootClbits, @@ -2267,14 +2368,12 @@ static void validateTarget(const ClassicalTarget& target, } } -static void validateControlFlow(const ControlFlowReader& controlFlow, - ValidationParameters localParameters, - const ValidationParameters& freeParameters, - llvm::StringSet<>& parameterNames, - const uint32_t rootQubits, - const uint32_t rootClbits, - const size_t definitionDepth, - const size_t controlFlowDepth) { +static void validateControlFlow( + const ControlFlowReader& controlFlow, ValidationParameters localParameters, + const ValidationParameters& freeParameters, + llvm::StringSet<>& parameterNames, + llvm::DenseSet& validatedGateDefinitions, uint32_t rootQubits, + uint32_t rootClbits, size_t definitionDepth, size_t controlFlowDepth) { if (controlFlowDepth >= MAX_CONTROL_FLOW_DEPTH) { throw std::runtime_error( "Qiskit control flow exceeds the nesting limit of 64"); @@ -2399,35 +2498,68 @@ static void validateControlFlow(const ControlFlowReader& controlFlow, "Qiskit control-flow block operands do not match its bit mapping"); } validateCircuit(*block, localParameters, freeParameters, parameterNames, - rootQubits, rootClbits, definitionDepth, - controlFlowDepth + 1U); + validatedGateDefinitions, rootQubits, rootClbits, + definitionDepth, controlFlowDepth + 1U, false); } } -static void validateDefinition(const CircuitReader& circuit, const size_t index, - const ValidationParameters& localParameters, - const ValidationParameters& freeParameters, - llvm::StringSet<>& parameterNames, - const size_t definitionDepth, - const size_t controlFlowDepth) { +static void +validateDefinition(const CircuitReader& circuit, size_t index, + const ValidationParameters& localParameters, + const ValidationParameters& freeParameters, + llvm::StringSet<>& parameterNames, + llvm::DenseSet& validatedGateDefinitions, + size_t definitionDepth, size_t controlFlowDepth, + bool gateDefinition) { if (definitionDepth >= MAX_DEFINITION_DEPTH) { throw std::runtime_error( "Qiskit instruction definitions exceed the nesting limit of 64"); } const auto definition = circuit.definition(index); - validateCircuit(*definition, localParameters, freeParameters, parameterNames, - definition->numQubits(), definition->numClbits(), - definitionDepth + 1U, controlFlowDepth); + if (!gateDefinition) { + validateCircuit(*definition, localParameters, freeParameters, + parameterNames, validatedGateDefinitions, + definition->numQubits(), definition->numClbits(), + definitionDepth + 1U, controlFlowDepth, false); + return; + } + if (definition->numClbits() != 0U) { + throw std::runtime_error( + "Qiskit Gate definitions must not contain classical bits"); + } + ValidationParameters formalParameters; + for (const auto& parameter : definition->parameters()) { + validateParameter(parameter, localParameters, freeParameters); + const auto* symbol = parameter.getSymbol(); + if (symbol == nullptr || symbol->name.empty() || + !formalParameters.try_emplace(symbol->name, parameter).second) { + throw std::runtime_error( + "Qiskit Gate definition has invalid formal parameters"); + } + } + if (!validatedGateDefinitions.insert(circuit.definitionIdentity(index)) + .second) { + return; + } + validateCircuit(*definition, formalParameters, {}, parameterNames, + validatedGateDefinitions, definition->numQubits(), 0U, + definitionDepth + 1U, controlFlowDepth, true); } void validateCircuit(const CircuitReader& circuit, const ValidationParameters& localParameters, const ValidationParameters& freeParameters, llvm::StringSet<>& parameterNames, + llvm::DenseSet& validatedGateDefinitions, const uint32_t rootQubits, const uint32_t rootClbits, - const size_t definitionDepth, - const size_t controlFlowDepth) { - for (const auto& variable : circuit.variables()) { + size_t definitionDepth, size_t controlFlowDepth, + bool gateDefinition) { + const auto variables = circuit.variables(); + if (gateDefinition && !variables.empty()) { + throw std::runtime_error( + "Qiskit Gate definitions must not contain classical variables"); + } + for (const auto& variable : variables) { if (variable.type == ClassicalType::Uint && variable.width > 64U) { throw std::runtime_error("Qiskit local variables support unsigned " "integers of at most 64 bits"); @@ -2463,9 +2595,19 @@ void validateCircuit(const CircuitReader& circuit, } for (const auto& modifier : instruction.modifiers) { if (modifier.kind == GateModifierKind::Power) { - validateParameter(modifier.exponent, localParameters, freeParameters); + const auto* number = modifier.exponent.getNumber(); + if (number == nullptr || !std::isfinite(number->value)) { + throw std::runtime_error( + "Qiskit Gate power must have a finite numeric exponent"); + } } } + if (gateDefinition && instruction.kind != OperationKind::Gate && + instruction.kind != OperationKind::Unitary) { + throw std::runtime_error( + "Qiskit Gate definitions may contain only Gate and unitary " + "operations"); + } switch (instruction.kind) { case OperationKind::Gate: @@ -2487,13 +2629,9 @@ void validateCircuit(const CircuitReader& circuit, } break; } - if (!instruction.modifiers.empty()) { - throw std::runtime_error( - "Qiskit circuit import does not support modifiers on custom " - "instructions"); - } validateDefinition(circuit, index, localParameters, freeParameters, - parameterNames, definitionDepth, controlFlowDepth); + parameterNames, validatedGateDefinitions, + definitionDepth, controlFlowDepth, true); break; case OperationKind::Unknown: if (!instruction.modifiers.empty()) { @@ -2502,7 +2640,8 @@ void validateCircuit(const CircuitReader& circuit, "instructions"); } validateDefinition(circuit, index, localParameters, freeParameters, - parameterNames, definitionDepth, controlFlowDepth); + parameterNames, validatedGateDefinitions, + definitionDepth, controlFlowDepth, false); break; case OperationKind::Barrier: if (!instruction.parameters.empty() || !instruction.clbits.empty()) { @@ -2578,8 +2717,8 @@ void validateCircuit(const CircuitReader& circuit, case OperationKind::ControlFlow: { const auto controlFlow = circuit.controlFlow(index); validateControlFlow(*controlFlow, localParameters, freeParameters, - parameterNames, rootQubits, rootClbits, - definitionDepth, controlFlowDepth); + parameterNames, validatedGateDefinitions, rootQubits, + rootClbits, definitionDepth, controlFlowDepth); break; } case OperationKind::Delay: @@ -2618,9 +2757,13 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { } ExpansionCountState expansion; - static_cast(expansionSummary(*view, expansion)); + auto expanded = expansionSummary(*view, expansion); + addExpandedOperations(expanded.operations, + expansion.materializedDefinitionOperations); + llvm::DenseSet validatedGateDefinitions; validateCircuit(*view, {}, freeParameterSymbols, parameterNames, - view->numQubits(), view->numClbits(), 0U, 0U); + validatedGateDefinitions, view->numQubits(), + view->numClbits(), 0U, 0U, false); const auto quantumRegisters = circuitRegisters(*view, true); const auto classicalRegisters = circuitRegisters(*view, false); for (const auto& reg : @@ -2635,7 +2778,7 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { const auto looseClbits = validateRegisterLayout( classicalRegisters, view->numClbits(), "classical"); - auto context = createContext(); + auto context = mlir::createCompilerContext(); mlir::qc::QCProgramBuilder builder(context.get()); llvm::SmallVector resultTypes; if (view->numClbits() == 0U) { @@ -2667,9 +2810,7 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { parameterGroupAttribute(builder, *symbol->group))); } const auto index = function.getNumArguments(); - // MLIR types are handles. Converting FloatType to Type keeps the same - // storage and does not slice object state. - const mlir::Type parameterType = builder.getF64Type(); + mlir::Type parameterType = builder.getF64Type(); if (failed(function.insertArgument( index, parameterType, builder.getDictionaryAttr(argumentAttributes), builder.getLoc()))) { @@ -2717,9 +2858,11 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { std::iota(qubitMap.begin(), qubitMap.end(), 0U); std::iota(clbitMap.begin(), clbitMap.end(), 0U); ImportedVariables variables; + GateImportState gateState(parameterGroups); + gateState.functionNames.insert(function.getName()); translateCircuit(builder, *view, qubitMap, clbitMap, qubitMap, clbitMap, - qubits, classicalBits, {}, globalParameters, 0U, 0U, - variables); + qubits, classicalBits, {}, globalParameters, gateState, 0U, + 0U, variables); auto moduleOp = classicalStorage.empty() ? builder.finalize() : builder.finalize(classicalStorage); diff --git a/bindings/mlir/qiskit/QiskitTranslation.h b/bindings/mlir/qiskit/QiskitTranslation.h index e69c5cf986..24ea4af613 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.h +++ b/bindings/mlir/qiskit/QiskitTranslation.h @@ -48,7 +48,7 @@ struct Register { std::vector bits; }; -/** Validate canonical register membership and return the leading loose bits. */ +/// Validate canonical register membership and return the leading loose bits. [[nodiscard]] uint32_t validateRegisterLayout(const std::vector& registers, uint32_t total, std::string_view kind); @@ -57,7 +57,7 @@ inline constexpr size_t MAX_PARAMETER_EXPRESSION_DEPTH = 64U; inline constexpr size_t MAX_PARAMETER_EXPRESSION_NODES = 4096U; inline constexpr uint64_t MAX_PARAMETER_GROUP_SIZE = 65'536U; -/** Source-level vector metadata for one scalar parameter. */ +/// Source-level vector metadata for one scalar parameter. struct ParameterGroup { std::string identity; std::string name; @@ -98,7 +98,7 @@ enum class BinaryParameterKind : uint8_t { Power, }; -/** One normalized scalar parameter-expression tree. */ +/// One normalized scalar parameter-expression tree. class Parameter { public: struct Number { @@ -151,27 +151,27 @@ class Parameter { } [[nodiscard]] const Number* getNumber() const { - return std::get_if(&storage); + return std::get_if(&storage_); } [[nodiscard]] const Symbol* getSymbol() const { - return std::get_if(&storage); + return std::get_if(&storage_); } [[nodiscard]] const Unary* getUnary() const { - return std::get_if(&storage); + return std::get_if(&storage_); } [[nodiscard]] const Binary* getBinary() const { - return std::get_if(&storage); + return std::get_if(&storage_); } private: using Value = std::variant; - explicit Parameter(Value value) : storage(std::move(value)) {} + explicit Parameter(Value value) : storage_(std::move(value)) {} - Value storage = Number{0.0}; + Value storage_ = Number{0.0}; }; enum class GateModifierKind : uint8_t { @@ -257,7 +257,7 @@ struct ClassicalVariable { bool input = false; }; -/** One normalized Qiskit classical-expression tree. */ +/// One normalized Qiskit classical-expression tree. struct Expression { ExpressionKind kind = ExpressionKind::Value; ClassicalType type = ClassicalType::Bool; @@ -335,7 +335,7 @@ class CircuitReader { [[nodiscard]] virtual std::vector variables() const = 0; [[nodiscard]] virtual Register quantumRegister(size_t index) const = 0; [[nodiscard]] virtual Register classicalRegister(size_t index) const = 0; - /** Return the circuit's free scalar parameters in a stable order. */ + /// Return the circuit's free scalar parameters in a stable order. [[nodiscard]] virtual std::vector parameters() const = 0; [[nodiscard]] virtual Parameter globalPhase() const = 0; [[nodiscard]] virtual Instruction instruction(size_t index) const = 0; @@ -386,6 +386,10 @@ class CircuitWriter { virtual void addGate(StandardGateMapping gate, const std::vector& qubits, const std::vector& parameters) = 0; + virtual void addCustomGate(std::string_view name, + const std::vector& qubits, + const std::vector& parameters, + const std::vector& modifiers) = 0; virtual void addMeasure(uint32_t qubit, uint32_t clbit) = 0; virtual void addReset(uint32_t qubit) = 0; virtual void addBarrier(const std::vector& qubits) = 0; @@ -398,7 +402,7 @@ class CircuitWriter { addControlFlow(ControlFlowKind kind, ClassicalTarget target, Loop loop, std::vector switchCases, std::vector> blocks) = 0; - /** Transfer the native circuit to a new owned Python QuantumCircuit. */ + /// Transfer the native circuit to a new owned Python QuantumCircuit. [[nodiscard]] virtual nb::object finish() = 0; }; @@ -416,6 +420,10 @@ class VersionedTranslation { [[nodiscard]] virtual bool supportsGate(StandardGateMapping gate) const = 0; [[nodiscard]] virtual std::unique_ptr createCircuit(uint32_t looseQubits, uint32_t looseClbits) const = 0; + virtual void + registerCustomGate(std::string_view symbol, std::string_view name, + const std::vector& formalParameters, + std::unique_ptr definition) = 0; }; #define MQT_QISKIT_DECLARE_VERSION_IMPL(suffix) \ diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index e6ed7e0d5d..596fb7bb68 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -27,6 +27,7 @@ Clbit, ControlModifier, Gate, + Instruction, InverseModifier, Parameter, ParameterExpression, @@ -46,6 +47,8 @@ if TYPE_CHECKING: from collections.abc import Callable + from qiskit.circuit.annotated_operation import Modifier + installed_qiskit = Version(qiskit.__version__) candidate_version = os.environ.get("MQT_QISKIT_TEST_CANDIDATE_VERSION") if not (Version("2.5.0") <= installed_qiskit < Version("2.6.0") or qiskit.__version__ == candidate_version): @@ -1264,24 +1267,127 @@ def test_layout_is_accepted_and_ignored() -> None: assert np.allclose(Operator(restored).data, Operator(laid_out).data) -def test_nested_numeric_custom_definitions_are_inlined() -> None: - """Bind numeric call parameters and recursively inline definitions.""" +def test_nested_numeric_custom_definitions_are_preserved() -> None: + """Keep nested custom Gates as reusable functions after binding.""" theta = Parameter("theta") - definition = QuantumCircuit(1) + definition = QuantumCircuit(1, name="inner") definition.rx(theta, 0) - inner = definition.to_gate(label="inner") - middle_definition = QuantumCircuit(1) + inner = definition.to_gate() + middle_definition = QuantumCircuit(1, name="outer") middle_definition.append(inner, [0]) - outer = middle_definition.to_gate(label="outer") + outer = middle_definition.to_gate() circuit = QuantumCircuit(1) circuit.append(outer, [0]) circuit.assign_parameters({theta: 0.25}, inplace=True) program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + assert program.ir.count("mqt.unitary") == 2 + assert "qc.call @inner" in program.ir + assert "qc.call @outer" in program.ir assert "qc.rx" in program.ir assert "2.500000e-01" in program.ir assert circuit.parameters == set() + assert restored.data[0].operation.name == "outer" + assert np.allclose(Operator(restored).data, Operator(circuit).data) + + +def test_custom_gate_definitions_are_interned_by_name_and_body() -> None: + """Reuse copied definitions and unique distinct same-named bodies.""" + definition = QuantumCircuit(1, name="shared") + definition.h(0) + gate = definition.to_gate() + circuit = QuantumCircuit(1) + circuit.append(gate, [0]) + circuit.append(gate, [0]) + + assert QCProgram.from_qiskit(circuit).ir.count("mqt.unitary") == 1 + + conflicting_definition = QuantumCircuit(1, name="shared") + conflicting_definition.x(0) + circuit.append(conflicting_definition.to_gate(), [0]) + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + + assert program.ir.count("mqt.unitary") == 2 + assert 'mqt.source_name = "shared"' in program.ir + assert [item.operation.name for item in restored.data] == ["shared", "shared", "shared"] + assert np.allclose(Operator(restored).data, Operator(circuit).data) + + +def test_custom_gate_with_standard_name_is_not_mistranslated() -> None: + """Classify standard gates by Qiskit identity rather than by name.""" + definition = QuantumCircuit(1) + definition.h(0) + custom = Gate("x", 1, []) + custom.definition = definition + circuit = QuantumCircuit(1) + circuit.append(custom, [0]) + + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + + assert "func.func private @x" in program.ir + assert "qc.h" in program.ir + assert np.allclose(Operator(restored).data, Operator(circuit).data) + + +def test_custom_gate_export_rejects_duplicate_qargs() -> None: + """Reject a call Qiskit cannot apply to distinct Gate inputs.""" + program = QCProgram.from_mlir_str( + """module { + func.func private @pair(%a: !qc.qubit, %b: !qc.qubit) attributes {mqt.unitary} { + return + } + func.func @main() attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + qc.call @pair(%q, %q) : !qc.qubit, !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + + with pytest.raises(RuntimeError, match="uses the same qubit more than once"): + program.to_qiskit() + + +def test_custom_gate_export_rejects_unreferenced_functions() -> None: + """Reject helper definitions that a Qiskit circuit cannot retain.""" + program = QCProgram.from_mlir_str( + """module { + func.func private @unused(%q: !qc.qubit) attributes {mqt.unitary} { + qc.x %q : !qc.qubit + return + } + func.func @main() attributes {mqt.entry_point} { + return + } +} +""" + ) + + with pytest.raises(RuntimeError, match="cannot preserve function 'unused'"): + program.to_qiskit() + + +def test_generic_instruction_with_clbits_remains_flattened() -> None: + """Keep classical Instruction definitions outside the unitary call ABI.""" + definition = QuantumCircuit(1, 1) + definition.measure(0, 0) + instruction = Instruction("observe", 1, 1, []) + instruction.definition = definition + circuit = QuantumCircuit(1, 1) + circuit.append(instruction, [0], [0]) + + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + + assert "mqt.unitary" not in program.ir + assert "qc.measure" in program.ir + assert restored.data[0].operation.name == "measure" def test_ambiguous_custom_parameter_binding_is_rejected() -> None: @@ -1296,7 +1402,7 @@ def test_ambiguous_custom_parameter_binding_is_rejected() -> None: circuit = QuantumCircuit(1) circuit.append(gate, [0]) - with pytest.raises(RuntimeError, match="parameter symbol 'z' is not defined"): + with pytest.raises(RuntimeError, match=r"parameter symbol '[az]' is not defined"): QCProgram.from_qiskit(circuit) @@ -1319,18 +1425,56 @@ def test_custom_definition_uses_call_parameter_order_after_binding() -> None: @pytest.mark.parametrize("modifier", [InverseModifier(), PowerModifier(0.5), ControlModifier(1)]) -def test_modified_custom_definitions_are_rejected( +def test_modified_custom_definitions_round_trip( modifier: InverseModifier | PowerModifier | ControlModifier, ) -> None: - """Reject modifiers whose semantics cannot be preserved while inlining.""" - definition = QuantumCircuit(1) + """Preserve supported modifiers around reusable custom Gates.""" + definition = QuantumCircuit(1, name="custom") definition.h(0) - custom = definition.to_gate(label="custom") + custom = definition.to_gate() operation = AnnotatedOperation(custom, modifier) circuit = QuantumCircuit(operation.num_qubits) circuit.append(operation, circuit.qubits) - with pytest.raises(RuntimeError, match="does not support modifiers on custom instructions"): + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + + assert "qc.call @custom" in program.ir + assert isinstance(restored.data[0].operation, AnnotatedOperation) + assert restored.data[0].operation.modifiers == [modifier] + assert np.allclose(Operator(restored).data, Operator(circuit).data) + + +def test_mixed_custom_gate_modifiers_are_canonicalized() -> None: + """Preserve semantics while combining commuting closed controls.""" + definition = QuantumCircuit(1, name="custom") + definition.h(0) + source_modifiers: list[Modifier] = [ + ControlModifier(1), + InverseModifier(), + PowerModifier(0.5), + ControlModifier(2), + ] + operation = AnnotatedOperation(definition.to_gate(), source_modifiers) + circuit = QuantumCircuit(operation.num_qubits) + circuit.append(operation, circuit.qubits) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + restored_operation = restored.data[0].operation + + assert isinstance(restored_operation, AnnotatedOperation) + assert restored_operation.modifiers == [InverseModifier(), PowerModifier(0.5), ControlModifier(3)] + assert np.allclose(Operator(restored).data, Operator(circuit).data) + + +def test_symbolic_power_modifier_is_rejected() -> None: + """Reject symbolic modifier state that Qiskit does not track or bind.""" + theta = Parameter("theta") + operation = AnnotatedOperation(library.XGate(), PowerModifier(theta)) + circuit = QuantumCircuit(1) + circuit.append(operation, [0]) + + with pytest.raises(RuntimeError, match="power must have a finite numeric exponent"): QCProgram.from_qiskit(circuit) @@ -1373,13 +1517,14 @@ def test_cyclic_and_excessively_nested_definitions_are_rejected() -> None: QCProgram.from_qiskit(too_deep) -def test_exponential_definition_expansion_is_rejected_by_budget() -> None: - """Count repeated definitions without materializing their full expansion.""" +def test_repeated_definition_graph_remains_compact() -> None: + """Keep a branching Gate graph compact through import and export.""" leaf_definition = QuantumCircuit(1) leaf_definition.h(0) nested = Gate("leaf", 1, []) nested.definition = leaf_definition - for level in range(22): + levels = 25 + for level in range(levels): definition = QuantumCircuit(1) definition.append(nested, [0]) definition.append(nested, [0]) @@ -1388,28 +1533,203 @@ def test_exponential_definition_expansion_is_rejected_by_budget() -> None: circuit = QuantumCircuit(1) circuit.append(nested, [0]) - with pytest.raises(RuntimeError, match="expansion exceeds 10000000 operations"): - QCProgram.from_qiskit(circuit) + program = QCProgram.from_qiskit(circuit) + + assert program.ir.count("mqt.unitary") == levels + 1 + assert program.ir.count("qc.call") == (2 * levels) + 1 + restored = QCProgram.from_qiskit(program.to_qiskit()) + assert restored.ir.count("mqt.unitary") == levels + 1 + assert restored.ir.count("qc.call") == (2 * levels) + 1 + + +def test_custom_gate_export_reuses_a_long_call_chain() -> None: + """Export repeated calls after visiting enough helpers to grow the graph.""" + functions = [ + """ func.func private @leaf(%q: !qc.qubit) attributes {mqt.unitary} { + qc.h %q : !qc.qubit + return + }""" + ] + callee = "leaf" + for level in range(59): + name = f"level_{level}" + functions.append( + f""" func.func private @{name}(%q: !qc.qubit) attributes {{mqt.unitary}} {{ + qc.call @{callee}(%q) : !qc.qubit + return + }}""" + ) + callee = name + functions.append( + f""" func.func @main() attributes {{mqt.entry_point}} {{ + %q = qc.alloc : !qc.qubit + qc.call @{callee}(%q) : !qc.qubit + qc.call @{callee}(%q) : !qc.qubit + qc.dealloc %q : !qc.qubit + return + }}""" + ) + program = QCProgram.from_mlir_str("module {\n" + "\n".join(functions) + "\n}\n") + + restored = program.to_qiskit() + + assert np.allclose(Operator(restored).data, np.eye(2)) + assert QCProgram.from_qiskit(restored).ir.count("mqt.unitary") == 60 + + +def test_custom_gate_export_checks_longest_shared_call_path() -> None: + """Reject a deep path even when its shared leaf was already visited.""" + functions = [ + """ func.func private @leaf(%q: !qc.qubit) attributes {mqt.unitary} { + qc.h %q : !qc.qubit + return + }""" + ] + callee = "leaf" + for level in range(64): + name = f"level_{level}" + functions.append( + f""" func.func private @{name}(%q: !qc.qubit) attributes {{mqt.unitary}} {{ + qc.call @{callee}(%q) : !qc.qubit + return + }}""" + ) + callee = name + functions.append( + f""" func.func @main() attributes {{mqt.entry_point}} {{ + %q = qc.alloc : !qc.qubit + qc.call @leaf(%q) : !qc.qubit + qc.call @{callee}(%q) : !qc.qubit + qc.dealloc %q : !qc.qubit + return + }}""" + ) + program = QCProgram.from_mlir_str("module {\n" + "\n".join(functions) + "\n}\n") + + with pytest.raises(RuntimeError, match="custom Gate calls exceed the nesting limit of 64"): + program.to_qiskit() + + +def test_constant_unary_custom_gate_argument_is_folded() -> None: + """Fold constant expressions before reconstructing Python parameters.""" + program = QCProgram.from_mlir_str( + """module { + func.func private @custom(%theta: f64 {mqt.input_name = "theta"}, %q: !qc.qubit) attributes {mqt.unitary} { + qc.rx(%theta) %q : !qc.qubit + return + } + func.func @main() attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %constant = arith.constant 5.000000e-01 : f64 + %angle = math.sin %constant : f64 + qc.call @custom(%angle, %q) : f64, !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + + restored = program.to_qiskit() + + assert restored.data[0].operation.params == [pytest.approx(np.sin(0.5))] -def test_value_list_loop_expansion_counts_each_iteration() -> None: - """Apply the expansion budget to every statically unrolled loop value.""" +def test_float_castable_custom_gate_argument_keeps_its_symbol() -> None: + """Do not fold a unary expression whose operand still tracks a symbol.""" + program = QCProgram.from_mlir_str( + """module { + func.func private @custom(%angle: f64 {mqt.input_name = "angle"}, %q: !qc.qubit) attributes {mqt.unitary} { + qc.rx(%angle) %q : !qc.qubit + return + } + func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %zero = arith.subf %theta, %theta : f64 + %angle = math.sin %zero : f64 + qc.call @custom(%angle, %q) : f64, !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + + restored = program.to_qiskit() + + assert {parameter.name for parameter in restored.parameters} == {"theta"} + assert restored.data[0].operation.params[0].parameters == restored.parameters + + +def test_distinct_custom_gate_specializations_round_trip() -> None: + """Preserve same-named numeric and symbolic specializations.""" + theta = Parameter("theta") + x = Parameter("x") + y = Parameter("y") + definition = QuantumCircuit(1, name="custom") + definition.rx(theta, 0) + circuit = QuantumCircuit(1) + for actual in (0.1, 0.2, x + 0.25, y * 2): + circuit.append(definition.to_gate(parameter_map={theta: actual}), [0]) + + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + + assert program.ir.count("mqt.unitary") == 4 + assert 'mqt.source_name = "custom"' in program.ir + assert [item.operation.name for item in restored.data] == ["custom"] * 4 + values = {"x": 0.3, "y": -0.2} + assert np.allclose( + Operator(_assign_parameter_values(restored, values)).data, + Operator(_assign_parameter_values(circuit, values)).data, + ) + + +def test_parameterized_helper_specializes_across_qiskit_round_trip() -> None: + """Retain semantics and source names when Qiskit erases a shared ABI.""" + program = QCProgram.from_mlir_str( + """module { + func.func private @custom(%theta: f64 {mqt.input_name = "theta"}, %q: !qc.qubit) attributes {mqt.unitary} { + qc.rx(%theta) %q : !qc.qubit + return + } + func.func @main() attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %first = arith.constant 1.000000e-01 : f64 + %second = arith.constant 2.000000e-01 : f64 + qc.call @custom(%first, %q) : f64, !qc.qubit + qc.call @custom(%second, %q) : f64, !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + + qiskit_circuit = program.to_qiskit() + restored_program = QCProgram.from_qiskit(qiskit_circuit) + restored_circuit = restored_program.to_qiskit() + + assert restored_program.ir.count("mqt.unitary") == 2 + assert 'mqt.source_name = "custom"' in restored_program.ir + assert [item.operation.name for item in restored_circuit.data] == ["custom", "custom"] + assert np.allclose(Operator(restored_circuit).data, Operator(qiskit_circuit).data) + + +def test_custom_gate_in_value_list_loop_is_reused() -> None: + """Reuse one custom Gate across a statically unrolled value-list loop.""" leaf_definition = QuantumCircuit(1) leaf_definition.h(0) nested = Gate("leaf", 1, []) nested.definition = leaf_definition - for level in range(20): - definition = QuantumCircuit(1) - definition.append(nested, [0]) - definition.append(nested, [0]) - nested = Gate(f"branch_{level}", 1, []) - nested.definition = definition circuit = QuantumCircuit(1) with circuit.for_loop([0, 2, 5, 9], None, None, None, None, label=None): circuit.append(nested, [0]) - with pytest.raises(RuntimeError, match="expansion exceeds 10000000 operations"): - QCProgram.from_qiskit(circuit) + program = QCProgram.from_qiskit(circuit) + + assert program.ir.count("mqt.unitary") == 1 + assert program.ir.count("qc.call @leaf") == 4 def test_rejections_do_not_modify_source_circuits() -> None: @@ -2942,7 +3262,7 @@ def test_float_castable_symbolic_expression_keeps_parameter_identity() -> None: def test_parameterized_custom_definition_round_trip() -> None: - """Substitute symbolic call parameters while recursively inlining a definition.""" + """Substitute symbolic call parameters while preserving its definition.""" formal = Parameter("formal") definition = QuantumCircuit(1) definition.rx(formal + 1, 0)