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)