Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 52 additions & 41 deletions bindings/mlir/qiskit/Qiskit2_5.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -919,28 +925,41 @@ class NativeCircuitReader final : public CircuitReader {
.standardGate = {},
};
}
std::optional<Instruction> 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);
Expand All @@ -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");
Expand All @@ -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;
}

Expand Down Expand Up @@ -1104,27 +1115,27 @@ class NativeCircuitReader final : public CircuitReader {

private:
[[nodiscard]] std::vector<uint32_t>
pythonInstructionQubits(const size_t index) const {
pythonInstructionBits(size_t index, const char* operandKind) const {
std::vector<uint32_t> 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<uint32_t>::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<uint32_t>(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;
}
Expand Down
62 changes: 59 additions & 3 deletions bindings/mlir/qiskit/QiskitImport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ using ValidationParameters = llvm::StringMap<Parameter>;
namespace {
struct GateImportState {
llvm::DenseMap<uintptr_t, mlir::func::FuncOp> gates;
std::map<std::vector<uint32_t>, mlir::func::FuncOp> permutations;
llvm::StringSet<> functionNames;
llvm::StringMap<size_t> nextFunctionSuffix;
};
Expand Down Expand Up @@ -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<mlir::Type> 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<uint32_t> 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<uint32_t>(output);
std::swap(inputs[output], inputs[source]);
}
});
}
llvm::SmallVector<mlir::Value> 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 {
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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<void>(modifiedQubitArity(instruction, pattern.size()));
if (!instruction.clbits.empty() || !instruction.parameters.empty()) {
throw std::runtime_error("Qiskit permutation has an invalid arity");
}
std::vector<bool> 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) {
Expand Down
2 changes: 2 additions & 0 deletions bindings/mlir/qiskit/QiskitTranslation.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ struct Instruction {
std::vector<Parameter> parameters;
std::vector<GateModifier> modifiers;
std::optional<StandardGateMapping> standardGate;
/// Output position i carries input position permutation[i].
std::optional<std::vector<uint32_t>> permutation = std::nullopt;
};

enum class ClassicalType : uint8_t {
Expand Down
6 changes: 5 additions & 1 deletion docs/mlir/qiskit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 81 additions & 0 deletions test/python/test_mlir_qiskit_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading