"""Convert Qiskit ``QuantumCircuit`` ↔ MIMIQ ``Circuit``.
Coverage:
- All standard gates from :data:`mimiq_qiskit.gate_map.QISKIT_TO_MIMIQ`.
- ``Measure``, ``Reset``, ``Barrier``.
- ``UnitaryGate`` (Qiskit ``"unitary"``) maps to
:class:`mimiqcircuits.GateCustom`, so arbitrary matrix gates (state
preparation, custom ansätze) pass through without a manual
decomposition.
- ``IfElseOp`` (mid-circuit measurement feed-forward) maps to
:class:`mimiqcircuits.IfStatement`, for the common case of a single
conditional gate with no ``else`` branch.
- Multi-register circuits: qubits and clbits are flattened in the order
Qiskit assigns them on the circuit, so the resulting MIMIQ circuit
uses 0-based indexing matching ``circuit.find_bit(q).index``.
- Global phase is dropped, because it does not affect measurement
statistics or expectation values, the only quantities this bridge
computes.
- Free :class:`qiskit.circuit.Parameter` references are not resolved;
bind them with ``QuantumCircuit.assign_parameters`` before conversion.
Unknown gates raise :class:`UnsupportedGateError` with the Qiskit name,
so callers can decide whether to decompose upstream or extend the
mapping.
"""
from __future__ import annotations
from typing import Sequence
import mimiqcircuits as mc
import numpy as np
from qiskit.circuit import ControlledGate
from mimiq_qiskit.gate_map import MIMIQ_TO_QISKIT, QISKIT_TO_MIMIQ
[docs]
class UnsupportedGateError(NotImplementedError):
"""Raised when a Qiskit or MIMIQ operation has no mapping."""
def _resolve_param(p) -> float:
"""Coerce a Qiskit parameter into a float.
Symbolic ``Parameter`` references are not supported; bind them before
conversion. The guard surfaces the error here rather than deep inside
a MIMIQ gate constructor.
"""
try:
return float(p)
except (TypeError, ValueError) as exc:
raise UnsupportedGateError(
f"unbound symbolic parameter {p!r}; call "
"QuantumCircuit.assign_parameters before conversion"
) from exc
def _gate_for(name: str, params: Sequence[float]) -> mc.Operation:
"""Return the MIMIQ gate for a plain Qiskit gate ``name``.
Only covers unitary gates from the gate map, not measure, reset,
barrier, unitary, or control flow, which the caller handles
separately.
"""
factory = QISKIT_TO_MIMIQ.get(name)
if factory is None:
raise UnsupportedGateError(
f"Qiskit operation {name!r} has no MIMIQ mapping; "
"decompose it upstream or extend gate_map.QISKIT_TO_MIMIQ"
)
return factory(params)
def _condition_bitstring(condition, qc) -> tuple[mc.BitString, list[int]]:
"""Translate a Qiskit instruction ``condition`` into a MIMIQ
``BitString`` and the global clbit indices it tests.
``condition`` is ``(target, value)`` where ``target`` is a single
``Clbit`` or a ``ClassicalRegister``. The returned bitstring is
LSB-first over the register bits, matching MIMIQ's convention that
bit ``i`` of the condition reads clbit ``i`` of the listed targets.
"""
from qiskit.circuit import Clbit, ClassicalRegister
target, value = condition
if isinstance(target, ClassicalRegister):
bits = list(target)
elif isinstance(target, Clbit):
bits = [target]
else:
raise UnsupportedGateError(
f"unsupported condition target {type(target).__name__}"
)
indices = [qc.find_bit(b).index for b in bits]
bitstring = mc.BitString.fromint(len(bits), int(value))
return bitstring, indices
def _convert_if_else(out: mc.Circuit, instr, qc, qidx) -> None:
"""Translate an ``IfElseOp`` into a MIMIQ ``IfStatement``.
Supports the common feed-forward shape: a single conditional gate and
no ``else`` branch. Richer control flow (multi-instruction bodies,
``else`` branches, loops) raises :class:`UnsupportedGateError`.
"""
op = instr.operation
blocks = op.blocks
if len(blocks) != 1:
raise UnsupportedGateError(
"IfElseOp with an else branch is not supported; "
"split it into separate conditionals"
)
if op.condition is None:
raise UnsupportedGateError("IfElseOp without a condition")
body = blocks[0]
body_instrs = list(body.data)
if len(body_instrs) != 1:
raise UnsupportedGateError(
"only single-gate conditional bodies are supported; "
f"got {len(body_instrs)} instructions"
)
inner = body_instrs[0]
if inner.clbits or inner.operation.name in ("measure", "reset", "barrier"):
raise UnsupportedGateError(
"conditional body must be a single unitary gate "
"(no measure/reset/barrier/nested classical ops)"
)
inner_params = (
[_resolve_param(p) for p in inner.operation.params]
if inner.operation.params
else []
)
inner_gate = _gate_for(inner.operation.name, inner_params)
# The body circuit's qubit ``j`` is the outer instruction's qubit ``j``.
body_qubit_index = {q: i for i, q in enumerate(body.qubits)}
inner_qubits = [
qidx(instr.qubits[body_qubit_index[q]]) for q in inner.qubits
]
bitstring, cond_indices = _condition_bitstring(op.condition, qc)
# IfStatement target layout is [op qubits..., condition bits...].
out.push(
mc.IfStatement(inner_gate, bitstring), *inner_qubits, *cond_indices
)
# A gate whose name is not in the map is decomposed through its own Qiskit
# ``definition``. Nesting is shallow in practice (``mcx`` on many controls
# is the deepest thing in the standard library), so the cap only exists to
# turn a pathological or self-referential definition into a clear error
# instead of a RecursionError.
_MAX_DEFINITION_DEPTH = 32
def _push_operation(out: mc.Circuit, op, qubits, clbits, depth: int) -> None:
"""Push one Qiskit operation onto ``out``, in terms of global indices.
``qubits`` and ``clbits`` are the MIMIQ indices the operation acts on.
Unmapped gates recurse through their Qiskit ``definition``, so the
converter covers the whole standard library rather than only the names
listed in :data:`~mimiq_qiskit.gate_map.QISKIT_TO_MIMIQ`.
"""
name = op.name
if name == "measure":
if len(qubits) != 1 or len(clbits) != 1:
raise UnsupportedGateError(
"Measure must target exactly one qubit and one clbit"
)
out.push(mc.Measure(), qubits[0], clbits[0])
return
if name == "reset":
if len(qubits) != 1:
raise UnsupportedGateError("Reset must target one qubit")
out.push(mc.Reset(), qubits[0])
return
if name == "barrier":
out.push(mc.Barrier(len(qubits)), *qubits)
return
if name == "delay":
# Qiskit stores a duration plus a unit; MIMIQ ``Delay`` takes a
# plain time, so the duration is normalised to seconds. ``dt`` is
# backend-relative with no seconds equivalent here, so it is
# passed through as given.
out.push(mc.Delay(_delay_seconds(op)), *qubits)
return
if name == "unitary":
matrix = np.asarray(op.to_matrix(), dtype=complex)
# Qiskit indexes a multi-qubit matrix little-endian (its first
# wire is the least significant bit), MIMIQ big-endian. Handing
# the matrix over unchanged on reversed wires converts between
# the two without permuting 4**n entries.
out.push(mc.GateCustom(matrix), *reversed(qubits))
return
if name == "global_phase":
# A global phase changes no measurement statistic or expectation
# value, the only quantities this bridge computes.
return
factory = QISKIT_TO_MIMIQ.get(name)
if factory is not None:
params = [_resolve_param(p) for p in op.params] if op.params else []
out.push(factory(params), *qubits)
return
control = _control_for(op)
if control is not None:
out.push(control, *qubits)
return
_push_definition(out, op, qubits, clbits, depth)
def _control_for(op):
"""Wrap a Qiskit ``ControlledGate`` as a MIMIQ ``Control``, or return
``None`` if it has no direct form.
Worth the special case because Qiskit synthesises a controlled gate
into dozens of primitives (a 4-control ``mcx`` is 69 of them) while
MIMIQ carries the control natively and decomposes it itself. Only the
all-ones control state maps directly; an open control needs the X
conjugation that Qiskit's own definition already encodes.
"""
if not isinstance(op, ControlledGate):
return None
nctrl = op.num_ctrl_qubits
if op.ctrl_state != (1 << nctrl) - 1:
return None
base = op.base_gate
factory = QISKIT_TO_MIMIQ.get(base.name)
if factory is None:
return None
base_params = [_resolve_param(p) for p in base.params] if base.params else []
return mc.Control(nctrl, factory(base_params))
def _push_definition(out: mc.Circuit, op, qubits, clbits, depth: int) -> None:
"""Decompose an unmapped gate through its Qiskit ``definition``.
This is what lets composites Qiskit builds from other gates
(``mcx`` with three or more controls, ``initialize``, ``r``, anything
from ``QuantumCircuit.to_gate()``) convert without an entry of their
own. Control flow and non-unitary operations have no usable
definition and raise instead.
"""
if depth >= _MAX_DEFINITION_DEPTH:
raise UnsupportedGateError(
f"Qiskit operation {op.name!r} nests definitions more than "
f"{_MAX_DEFINITION_DEPTH} levels deep; decompose it upstream"
)
try:
definition = op.definition
except Exception: # a definition Qiskit cannot build at all
definition = None
if definition is None:
raise UnsupportedGateError(
f"Qiskit operation {op.name!r} has no MIMIQ mapping and no "
"definition to decompose; add it to "
"gate_map.QISKIT_TO_MIMIQ or decompose it upstream"
)
if len(definition.qubits) != len(qubits):
raise UnsupportedGateError(
f"definition of {op.name!r} spans {len(definition.qubits)} "
f"qubits but the operation acts on {len(qubits)}; ancilla-using "
"definitions are not supported"
)
# The definition's own wires are positional: its qubit ``j`` is the
# outer operation's qubit ``j``.
qmap = dict(zip(definition.qubits, qubits))
cmap = dict(zip(definition.clbits, clbits))
for inner in definition.data:
_push_operation(
out,
inner.operation,
[qmap[q] for q in inner.qubits],
[cmap[c] for c in inner.clbits],
depth + 1,
)
def _delay_seconds(op) -> float:
"""Duration of a Qiskit ``Delay`` in seconds."""
scale = {"s": 1.0, "ms": 1e-3, "us": 1e-6, "ns": 1e-9, "ps": 1e-12}
return float(op.duration) * scale.get(op.unit, 1.0)
[docs]
def qiskit_to_mimiq(qc) -> mc.Circuit:
"""Convert a Qiskit :class:`QuantumCircuit` to a MIMIQ
:class:`mimiqcircuits.Circuit`.
Args:
qc: A Qiskit ``QuantumCircuit``.
Returns:
A MIMIQ ``Circuit`` with operations pushed in the same order.
Raises:
UnsupportedGateError: An operation in ``qc`` has neither a MIMIQ
mapping nor a Qiskit definition to decompose.
"""
out = mc.Circuit()
def qidx(qubit) -> int:
return qc.find_bit(qubit).index
def cidx(clbit) -> int:
return qc.find_bit(clbit).index
for instr in qc.data:
if instr.operation.name == "if_else":
_convert_if_else(out, instr, qc, qidx)
continue
_push_operation(
out,
instr.operation,
[qidx(q) for q in instr.qubits],
[cidx(c) for c in instr.clbits],
0,
)
return out
[docs]
def mimiq_to_qiskit(circuit: mc.Circuit):
"""Convert a MIMIQ :class:`Circuit` back to a Qiskit
:class:`QuantumCircuit`.
Gates map onto concrete Qiskit gate classes, so the result is a fully
defined circuit that Qiskit can transpile and simulate. Any unitary
operation with no named counterpart -- ``Control``, ``Inverse``,
``Power``, ``Parallel``, ``GateCustom``, and gates Qiskit has no
equivalent for such as ``GateSY`` or ``GateRNZ`` -- becomes a
``UnitaryGate`` carrying its matrix. That is operator-faithful but not
structure-faithful, so a round trip through both converters preserves
the unitary, not the gate names.
The result uses single anonymous quantum and classical registers sized
to the circuit; register identity from any original Qiskit circuit is
not preserved.
Raises:
UnsupportedGateError: The circuit holds a non-unitary operation
with no Qiskit equivalent (a noise channel, for instance), or
a gate with unbound symbolic parameters.
"""
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
nq = circuit.num_qubits()
nb = circuit.num_bits()
qreg = QuantumRegister(max(nq, 1), name="q")
if nb > 0:
creg = ClassicalRegister(nb, name="c")
qc = QuantumCircuit(qreg, creg)
else:
qc = QuantumCircuit(qreg)
for instruction in circuit:
op = instruction.operation
qubits = list(instruction.get_qubits())
clbits = list(instruction.get_bits())
if isinstance(op, mc.Measure):
qc.measure(qubits[0], clbits[0])
continue
if isinstance(op, mc.Reset):
qc.reset(qubits[0])
continue
if isinstance(op, mc.Barrier):
qc.barrier(*qubits)
continue
if isinstance(op, mc.Delay):
qc.delay(float(op.t), qubits[0], unit="s")
continue
if isinstance(op, mc.IfStatement):
_append_if_statement(qc, op, qubits, clbits)
continue
entry = MIMIQ_TO_QISKIT.get(type(op))
if entry is not None:
gate_cls, nparams = entry
params = _extract_mimiq_params(op, nparams)
qc.append(gate_cls(*params), qubits)
continue
# No named counterpart: fall back to the operation's matrix. This
# covers GateCustom, the composite wrappers, and MIMIQ-only gates.
qc.append(_unitary_gate_for(op), list(reversed(qubits)))
return qc
def _matrix_to_numpy(op):
"""Materialise a MIMIQ operation's matrix as a numpy complex array.
``matrix()`` hands back a symengine matrix, whose ``__array__`` numpy
2 warns about, so the entries are coerced one at a time.
"""
sym = op.matrix()
return np.array(
[
[complex(sym[r, c]) for c in range(sym.cols)]
for r in range(sym.rows)
],
dtype=complex,
)
def _unitary_gate_for(op):
"""Wrap an unmapped MIMIQ unitary as a Qiskit ``UnitaryGate``.
The caller applies it on reversed wires, for the same endianness
reason as the forward path.
"""
from qiskit.circuit.library import UnitaryGate
if not isinstance(op, mc.Gate):
raise UnsupportedGateError(
f"MIMIQ operation {type(op).__name__} is not unitary and has "
"no Qiskit equivalent"
)
try:
matrix = _matrix_to_numpy(op)
except (TypeError, RuntimeError) as exc:
raise UnsupportedGateError(
f"could not read a numeric matrix off {type(op).__name__}; "
"evaluate its symbolic parameters first"
) from exc
return UnitaryGate(matrix, label=op.name)
def _append_if_statement(qc, op, qubits, clbits) -> None:
"""Rebuild a MIMIQ ``IfStatement`` as a Qiskit ``if_test`` block.
An ``IfStatement``'s targets are laid out as ``[op qubits...,
condition bits...]``, and its bitstring is LSB-first over those
condition bits, matching how :func:`qiskit_to_mimiq` emits it.
"""
inner = op.get_operation()
bitstring = op.get_bitstring()
value = sum(1 << i for i, bit in enumerate(bitstring.to01()) if bit == "1")
cond_bits = [qc.clbits[i] for i in clbits]
body_qubits = [qc.qubits[i] for i in qubits]
# Qiskit conditions on a single Clbit or a whole register. A MIMIQ
# condition over several loose bits has no single-expression Qiskit
# form, so build one over a synthetic register only when the bits are
# already contiguous from the start of the circuit's register.
if len(cond_bits) == 1:
condition = (cond_bits[0], value)
elif any(list(reg) == cond_bits for reg in qc.cregs):
condition = (next(r for r in qc.cregs if list(r) == cond_bits), value)
else:
raise UnsupportedGateError(
"IfStatement conditions on classical bits "
f"{clbits}, which Qiskit cannot express as a single "
"clbit or register comparison"
)
entry = MIMIQ_TO_QISKIT.get(type(inner))
if entry is not None:
gate_cls, nparams = entry
gate = gate_cls(*_extract_mimiq_params(inner, nparams))
targets = body_qubits
else:
gate = _unitary_gate_for(inner)
targets = list(reversed(body_qubits))
with qc.if_test(condition):
qc.append(gate, targets)
def _extract_mimiq_params(op: mc.Operation, nparams: int) -> list[float]:
"""Pull the first ``nparams`` numeric parameters off a MIMIQ gate.
``getparams()`` is the public accessor every operation implements, and
it can report more than the Qiskit counterpart takes: ``GateU`` adds a
trailing ``gamma`` global phase that ``UGate`` has no slot for. Extra
parameters are dropped, which is why the mapping records how many
Qiskit wants.
"""
if nparams == 0:
return []
params = op.getparams()
if len(params) < nparams:
raise UnsupportedGateError(
f"could not extract {nparams} parameters from "
f"{type(op).__name__}; getparams() gave {params!r}"
)
return [float(v) for v in params[:nparams]]