From 9bf5dc1bc708bf09a35a91102dd2b1d0e36949df Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Sat, 12 Sep 2026 14:14:47 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20Import=20array-valued=20Qisk?= =?UTF-8?q?it=20gate=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read custom operations through Python to avoid the scalar C API panic. Lower permutation patterns to reusable native SWAP functions in Core and preserve whole-gate modifiers. Other custom operations use their supplied definitions. Assisted-by: GPT-6 via Codex --- CHANGELOG.md | 5 ++ bindings/mlir/qiskit/Qiskit2_5.cpp | 93 ++++++++++++--------- bindings/mlir/qiskit/QiskitImport.cpp | 62 +++++++++++++- bindings/mlir/qiskit/QiskitTranslation.h | 2 + docs/mlir/qiskit.md | 6 +- test/python/test_mlir_qiskit_translation.py | 81 ++++++++++++++++++ 6 files changed, 204 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f089464d0b..323ef1bc0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ releases may include breaking changes. ## [Unreleased] +- 🐛 Import Qiskit gates and instructions with array-valued parameters through + their circuit definitions and lower permutations to SWAPs in Core. Preserve + nested and controlled operations, and report unsupported opaque operations + without aborting the process. ([**@simon1hofmann**]) + - ⚡ Speed up qubit placement and routing. Skip layout search for flat programs whose initial qubit placement already satisfies the target topology. Place disjoint interaction paths along connected target sites to avoid needless diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index c282fd88fb..22860bcd28 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -750,9 +750,15 @@ class DefinitionRegistry final { const std::string_view name, nb::handle parameters) { const nb::tuple parameterTuple(parameters); - const auto parameterHash = PyObject_Hash(parameterTuple.ptr()); + auto parameterHash = PyObject_Hash(parameterTuple.ptr()); if (parameterHash == -1) { - throwPythonError("Qiskit Gate parameters are not hashable"); + if (PyErr_ExceptionMatches(PyExc_TypeError) == 0) { + throwPythonError("Qiskit Gate parameter hashing failed"); + } + // Array-valued parameters specialize the definition, not its scalar + // call signature. Compare those definitions in one fallback bucket. + PyErr_Clear(); + parameterHash = 0; } // ponytail: use a structural circuit hash if many same-signature Gate // definitions become common. @@ -919,28 +925,41 @@ class NativeCircuitReader final : public CircuitReader { .standardGate = {}, }; } - std::optional normalizedUnknown; if (kind == OperationKind::Unknown) { + // The C API's scalar parameter accessor aborts on Python objects such as + // PermutationGate's array. Read custom operations through Python and let + // their definitions supply the scalar call signature. + Instruction result; + normalizePythonGate(operation, result); + result.qubits = pythonInstructionBits(index, "qubits"); + result.clbits = pythonInstructionBits(index, "clbits"); if (isPythonUnitaryGate(operation)) { - Instruction result{ - .kind = OperationKind::Unitary, - .name = "unitary", - .qubits = {}, - .clbits = {}, - .parameters = {}, - .modifiers = {}, - .standardGate = {}, - }; - normalizePythonGate(operation, result); + result.kind = OperationKind::Unitary; result.name = "unitary"; - result.qubits = pythonInstructionQubits(index); - return result; - } - normalizedUnknown.emplace(); - normalizePythonGate(operation, *normalizedUnknown); - if (isPythonGate(operation)) { - normalizedUnknown->kind = OperationKind::Gate; + } else if (isPythonGate(operation)) { + result.kind = OperationKind::Gate; + const auto terminal = terminalPythonGate(operation); + if (nb::isinstance(terminal, + nb::module_::import_("qiskit.circuit.library") + .attr("PermutationGate"))) { + result.permutation.emplace(); + for (const nb::handle entry : nb::iter(terminal.attr("pattern"))) { + uint32_t position = 0; + if (!nb::try_cast(entry, position)) { + throw std::runtime_error( + "Qiskit permutation has an invalid index"); + } + result.permutation->push_back(position); + } + } else if (isPythonStandardGate(operation)) { + result.standardGate = standardGateMapping(result.name); + for (const nb::handle parameter : + nb::iter(operation.attr("params"))) { + result.parameters.push_back(normalizePythonParameter(parameter)); + } + } } + return result; } QkCircuitInstruction native{}; qk_circuit_get_instruction(circuit_, index, &native); @@ -961,8 +980,7 @@ class NativeCircuitReader final : public CircuitReader { std::copy_n(native.clbits, native.num_clbits, result.clbits.begin()); } result.parameters.reserve(native.num_params); - if (result.kind == OperationKind::Gate || - result.kind == OperationKind::Unknown) { + if (result.kind == OperationKind::Gate) { const auto parameters = pythonAttribute(operation, "params", "Qiskit operation does not expose its parameters"); @@ -981,14 +999,7 @@ class NativeCircuitReader final : public CircuitReader { throw std::runtime_error( "Qiskit non-gate instruction has unexpected scalar parameters"); } - if (kind == OperationKind::Unknown) { - result.name = std::move(normalizedUnknown->name); - result.modifiers = std::move(normalizedUnknown->modifiers); - result.kind = normalizedUnknown->kind; - } - if (kind != OperationKind::Unknown || isPythonStandardGate(operation)) { - result.standardGate = standardGateMapping(result.name); - } + result.standardGate = standardGateMapping(result.name); return result; } @@ -1104,27 +1115,27 @@ class NativeCircuitReader final : public CircuitReader { private: [[nodiscard]] std::vector - pythonInstructionQubits(const size_t index) const { + pythonInstructionBits(size_t index, const char* operandKind) const { std::vector result; try { - const auto qubits = - pythonAttribute(data_[index], "qubits", - "Qiskit circuit instruction has no qubit operands"); - result.reserve(nb::len(qubits)); + const auto bits = + pythonAttribute(data_[index], operandKind, + "Qiskit circuit instruction has no operands"); + result.reserve(nb::len(bits)); const auto findBit = pythonAttribute(pythonCircuit_, "find_bit", - "Qiskit circuit cannot resolve instruction qubits"); - for (const nb::handle qubit : nb::iter(qubits)) { - const auto location = findBit(qubit); + "Qiskit circuit cannot resolve instruction bits"); + for (const nb::handle bit : nb::iter(bits)) { + const auto location = findBit(bit); const auto position = pythonUnsignedAttribute( - location, "index", "Qiskit qubit has an invalid circuit index"); + location, "index", "Qiskit bit has an invalid circuit index"); if (position > std::numeric_limits::max()) { - throw std::runtime_error("Qiskit qubit index cannot be represented"); + throw std::runtime_error("Qiskit bit index cannot be represented"); } result.push_back(static_cast(position)); } } catch (const nb::python_error& error) { - throwPythonError("Qiskit failed to resolve unitary qubits", error); + throwPythonError("Qiskit failed to resolve instruction bits", error); } return result; } diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index 3ae708fe2c..813ca57382 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -88,6 +88,7 @@ using ValidationParameters = llvm::StringMap; namespace { struct GateImportState { llvm::DenseMap gates; + std::map, mlir::func::FuncOp> permutations; llvm::StringSet<> functionNames; llvm::StringMap nextFunctionSuffix; }; @@ -2001,7 +2002,46 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, const auto instruction = circuit.instruction(index); switch (instruction.kind) { case OperationKind::Gate: - if (instruction.standardGate) { + if (instruction.permutation) { + const auto& pattern = *instruction.permutation; + auto& function = gateState.permutations[pattern]; + if (!function) { + std::string name = "permutation"; + auto& suffix = gateState.nextFunctionSuffix[name]; + while (!gateState.functionNames.insert(name).second) { + name = "permutation_" + std::to_string(suffix++); + } + llvm::SmallVector types( + pattern.size(), mlir::qc::QubitType::get(builder.getContext())); + function = builder.createUnitaryFunction( + name, types, [&](mlir::ValueRange targets) { + // Place each requested input at its output position. Track + // inverse positions so a cycle takes linear time to lower. + std::vector inputs(pattern.size()); + std::iota(inputs.begin(), inputs.end(), 0U); + auto positions = inputs; + for (size_t output = 0; output < pattern.size(); ++output) { + const auto source = positions[pattern[output]]; + if (source == output) { + continue; + } + builder.swap(targets[output], targets[source]); + positions[inputs[output]] = source; + positions[inputs[source]] = static_cast(output); + std::swap(inputs[output], inputs[source]); + } + }); + } + llvm::SmallVector operands; + for (const auto qubit : instruction.qubits) { + operands.push_back(getQubit(qubit)); + } + emitModifiedOperation( + builder, instruction, operands, + modifiedQubitArity(instruction, pattern.size()), localParameters, + globalParameters, + [&](mlir::ValueRange targets) { builder.call(function, targets); }); + } else if (instruction.standardGate) { emitGate(builder, instruction, allQubits, qubitMap, localParameters, globalParameters); } else { @@ -2117,8 +2157,9 @@ expansionSummary(const CircuitReader& circuit, ExpansionCountState& state, continue; } const auto instruction = circuit.instruction(index); - const bool customGate = - instruction.kind == OperationKind::Gate && !instruction.standardGate; + const bool customGate = instruction.kind == OperationKind::Gate && + !instruction.standardGate && + !instruction.permutation; if (customGate || instruction.kind == OperationKind::Unknown) { if (instruction.kind == OperationKind::Unknown && !instruction.modifiers.empty()) { @@ -2670,6 +2711,21 @@ void validateCircuit(const CircuitReader& circuit, switch (instruction.kind) { case OperationKind::Gate: + if (instruction.permutation) { + const auto& pattern = *instruction.permutation; + static_cast(modifiedQubitArity(instruction, pattern.size())); + if (!instruction.clbits.empty() || !instruction.parameters.empty()) { + throw std::runtime_error("Qiskit permutation has an invalid arity"); + } + std::vector seen(pattern.size(), false); + for (const auto input : pattern) { + if (input >= pattern.size() || seen[input]) { + throw std::runtime_error("Qiskit permutation must be a bijection"); + } + seen[input] = true; + } + break; + } if (const auto arity = gateArity(instruction)) { size_t modifierControls = 0U; for (const auto& modifier : instruction.modifiers) { diff --git a/bindings/mlir/qiskit/QiskitTranslation.h b/bindings/mlir/qiskit/QiskitTranslation.h index 9c1412af40..4505709e74 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.h +++ b/bindings/mlir/qiskit/QiskitTranslation.h @@ -207,6 +207,8 @@ struct Instruction { std::vector parameters; std::vector modifiers; std::optional standardGate; + /// Output position i carries input position permutation[i]. + std::optional> permutation = std::nullopt; }; enum class ClassicalType : uint8_t { diff --git a/docs/mlir/qiskit.md b/docs/mlir/qiskit.md index f622251106..a7c62ef0d8 100644 --- a/docs/mlir/qiskit.md +++ b/docs/mlir/qiskit.md @@ -77,7 +77,11 @@ are supported and remain distinct from free symbols. Parameterized custom-instruction definitions are expanded after their symbols and expressions are resolved. Definition expansion rejects missing definitions, cycles, operand arity mismatches, nesting beyond 64 levels, and more than 10 million expanded -operations. +operations. Permutation patterns lower directly to SWAPs in Core, including +inside nested definitions and gate modifiers. Other array-valued custom +parameters are represented by the circuit definition rather than scalar program +inputs. Operations without a supported definition are rejected with a Python +exception; arbitrary Python parameter objects are not preserved on export. Structured-control export supports scalar results from {code}`scf.if` and {code}`scf.index_switch`, carried scalar state in constant-range diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 672658c4a5..63805dc719 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -1368,6 +1368,87 @@ def test_custom_gate_definitions_are_interned_by_name_and_body() -> None: assert np.allclose(Operator(restored).data, Operator(circuit).data) +@pytest.mark.parametrize("wrapper", ["plain", "nested", "controlled", "annotated"]) +def test_array_parameter_gate_definitions(wrapper: str) -> None: + """Import array-valued gates without sending objects to the scalar C API.""" + gate = library.PermutationGate([2, 0, 1]) + if wrapper == "nested": + definition = QuantumCircuit(3) + definition.append(gate, [2, 0, 1]) + gate = definition.to_gate() + elif wrapper == "controlled": + # Qiskit requires a definition before constructing an eager control. + definition = QuantumCircuit(3) + definition.swap(0, 2) + definition.swap(1, 2) + gate.definition = definition + gate = gate.control(1, annotated=False) + elif wrapper == "annotated": + gate = AnnotatedOperation(gate, [InverseModifier(), ControlModifier(1)]) + circuit = QuantumCircuit(gate.num_qubits) + circuit.append(gate, list(reversed(range(gate.num_qubits)))) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + assert np.allclose(Operator(restored).data, Operator(circuit).data) + + +def test_array_parameter_definitions_remain_distinct() -> None: + """Preserve distinct permutation patterns in the same circuit.""" + circuit = QuantumCircuit(3) + for pattern in ([2, 0, 1], [1, 0, 2], [2, 0, 1]): + circuit.append(library.PermutationGate(pattern), range(3)) + program = QCProgram.from_qiskit(circuit) + + assert program.ir.count("mqt.unitary") == 2 + assert np.allclose(Operator(program.to_qiskit()).data, Operator(circuit).data) + + +@pytest.mark.parametrize("pattern", list(permutations(range(4)))) +def test_permutation_lowering_patterns(pattern: tuple[int, ...]) -> None: + """Cover identity, disjoint cycles, and both orientations of long cycles.""" + circuit = QuantumCircuit(4) + circuit.append(library.PermutationGate(list(pattern)), range(4)) + + assert np.allclose(Operator(QCProgram.from_qiskit(circuit).to_qiskit()).data, Operator(circuit).data) + + +@pytest.mark.parametrize("pattern", [[0, 0, 2], [0, 1, 3], [-1, 1, 2]]) +def test_invalid_permutation_is_rejected(pattern: list[int]) -> None: + """Validate mutated input patterns before indexing the permutation.""" + gate = library.PermutationGate([0, 1, 2]) + gate.params[0][:] = pattern + circuit = QuantumCircuit(3) + circuit.append(gate, range(3)) + + with pytest.raises(RuntimeError, match="permutation"): + QCProgram.from_qiskit(circuit) + + +def test_array_parameter_instruction_with_classical_operands() -> None: + """Resolve custom Instruction qubits and clbits through its definition.""" + definition = QuantumCircuit(1, 1) + definition.x(0) + definition.measure(0, 0) + instruction = Instruction("array_measure", 1, 1, [np.array([1, 2])]) + instruction.definition = definition + circuit = QuantumCircuit(2, 2) + circuit.append(instruction, [1], [1]) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + assert restored == circuit.decompose() + + +def test_opaque_array_parameter_instruction_is_rejected() -> None: + """Reject opaque object-valued operations with a catchable diagnostic.""" + circuit = QuantumCircuit(1) + circuit.append(Instruction("opaque_array", 1, 0, [np.array([1, 2])]), [0]) + + with pytest.raises(RuntimeError, match="no circuit definition"): + QCProgram.from_qiskit(circuit) + + def test_custom_gate_with_standard_name_is_not_mistranslated() -> None: """Classify standard gates by Qiskit identity rather than by name.""" definition = QuantumCircuit(1) From 423008c01800cf0dfc83c2a56dbe1f3095f28b21 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Sat, 12 Sep 2026 14:25:21 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9D=20Link=20the=20importer=20fix?= =?UTF-8?q?=20to=20its=20pull=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-6 via Codex --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 323ef1bc0e..89bc7d1e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ releases may include breaking changes. - 🐛 Import Qiskit gates and instructions with array-valued parameters through their circuit definitions and lower permutations to SWAPs in Core. Preserve nested and controlled operations, and report unsupported opaque operations - without aborting the process. ([**@simon1hofmann**]) + without aborting the process. ([#2550]) ([**@simon1hofmann**]) - ⚡ Speed up qubit placement and routing. Skip layout search for flat programs whose initial qubit placement already satisfies the target topology. Place @@ -1770,3 +1770,5 @@ for previous changelogs._ [munich-quantum-toolkit/workflows]: https://github.com/munich-quantum-toolkit/workflows [MQT QMAP]: https://github.com/munich-quantum-toolkit/qmap [MQT QCEC]: https://github.com/munich-quantum-toolkit/qcec + +[#2550]: https://github.com/munich-quantum-toolkit/core/pull/2550 From 8734d5ad69b466ea8b168d545d5bd2119741fbcf Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Sun, 13 Sep 2026 21:24:37 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20Remove=20the=20importer=20ch?= =?UTF-8?q?angelog=20entry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Remove the entry and its PR link as requested during the changelog-policy review. Release preparation will collect the user-facing changes. Assisted-by: GPT-6 via Codex --- CHANGELOG.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89bc7d1e29..f089464d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,6 @@ releases may include breaking changes. ## [Unreleased] -- 🐛 Import Qiskit gates and instructions with array-valued parameters through - their circuit definitions and lower permutations to SWAPs in Core. Preserve - nested and controlled operations, and report unsupported opaque operations - without aborting the process. ([#2550]) ([**@simon1hofmann**]) - - ⚡ Speed up qubit placement and routing. Skip layout search for flat programs whose initial qubit placement already satisfies the target topology. Place disjoint interaction paths along connected target sites to avoid needless @@ -1770,5 +1765,3 @@ for previous changelogs._ [munich-quantum-toolkit/workflows]: https://github.com/munich-quantum-toolkit/workflows [MQT QMAP]: https://github.com/munich-quantum-toolkit/qmap [MQT QCEC]: https://github.com/munich-quantum-toolkit/qcec - -[#2550]: https://github.com/munich-quantum-toolkit/core/pull/2550