Source code for qc_executor.qiskit.qiskit_circuit
"""Qiskit native circuit, compiled from the framework-independent circuit IR."""
from __future__ import annotations
import hashlib
from collections import OrderedDict
from typing import Any, List
from qc_executor.base.circuit_base import QuantumCircuitBase
from qc_executor.base.circuit_ir import CircuitIR
from qc_executor.qiskit._hash import _circuit_key
from qc_executor.qiskit._ir_bridge import SUPPORTED_OPCODES, ir_to_qiskit
from qc_executor.qiskit._param_binding import build_params_dict
[docs]
class QiskitCircuit(QuantumCircuitBase):
"""A quantum circuit that compiles to Qiskit.
Built like any other circuit -- ``QiskitCircuit(2)`` then ``circuit.h(0)``
-- or converted from an existing one with :meth:`from_quantum_circuit`.
The Qiskit circuit is produced on demand and rebuilt whenever the
instruction store changes.
A pre-built Qiskit circuit can also be adopted through :meth:`from_qiskit`,
which stores it directly instead of deriving it; see that method for why.
Args:
num_qubits: Number of qubits in the circuit.
num_clbits: Number of classical bits, for mid-circuit measurement.
_ir: Adopt this instruction store instead of starting empty.
_native: Adopt this Qiskit circuit instead of compiling one.
"""
[docs]
@classmethod
def supported_opcodes(cls) -> frozenset:
"""Return the opcodes the Qiskit bridge emits directly."""
return SUPPORTED_OPCODES
def __init__(
self,
num_qubits: int = 0,
num_clbits: int = 0,
*,
_ir: "CircuitIR | None" = None,
_native: Any = None,
):
super().__init__(num_qubits, num_clbits, _ir=_ir)
self._native_override = _native
# ------------------------------------------------------------------
# Backend hooks
# ------------------------------------------------------------------
def _build_native(self):
"""Compile the instruction store into a Qiskit circuit."""
if self._native_override is not None:
return self._native_override
return ir_to_qiskit(self._lowered_ir())
[docs]
@classmethod
def from_quantum_circuit(cls, circuit: Any) -> "QiskitCircuit":
"""Convert a circuit, or adopt a Qiskit circuit passed directly.
Args:
circuit: A :class:`~qc_executor.base.circuit_base.QuantumCircuitBase`
or a raw ``qiskit.QuantumCircuit``.
Returns:
The circuit in this native form.
"""
if not isinstance(circuit, QuantumCircuitBase):
return cls.from_qiskit(circuit)
return super().from_quantum_circuit(circuit) # type: ignore[return-value]
[docs]
@classmethod
def from_qiskit(cls, qiskit_circuit) -> "QiskitCircuit":
"""Adopt an existing Qiskit circuit without deriving it from the IR.
The circuit is stored as-is rather than imported, because the ones that
arrive here cannot be expressed in the IR: an ISA-transpiled circuit
carries a global phase, a ``TranspileLayout``, possibly ``Delay`` and
``IfElseOp`` instructions, and is widened to the backend's full qubit
count. Round-tripping it would silently discard all of that, so the
instruction store is left empty and this object is a carrier for the
compiled circuit.
The result runs on the Qiskit backend only: its instruction store is
empty, so converting it to another backend's circuit yields an empty
circuit. To run a Qiskit circuit everywhere, import it with
:meth:`QuantumCircuit.from_qiskit <qc_executor.QuantumCircuit.from_qiskit>`.
Args:
qiskit_circuit: The Qiskit circuit to adopt.
Returns:
A :class:`QiskitCircuit` wrapping it.
"""
return cls(
qiskit_circuit.num_qubits,
qiskit_circuit.num_clbits,
_native=qiskit_circuit,
)
# ------------------------------------------------------------------
# Properties
# ------------------------------------------------------------------
@property
def qiskit_circuit(self):
"""The underlying Qiskit circuit."""
return self.native
@property
def num_qubits(self) -> int:
"""Number of qubits in the circuit."""
if self._native_override is not None:
return self._native_override.num_qubits
return super().num_qubits
@property
def free_parameters(self) -> set:
"""The Qiskit parameter objects appearing in the compiled circuit."""
return set(self.native.parameters)
@property
def parameter_dimensions(self) -> dict:
"""Number of elements used from each Qiskit parameter vector."""
dimensions: OrderedDict = OrderedDict()
for parameter in self.native.parameters:
name = parameter.vector.name if hasattr(parameter, "vector") else parameter.name
dimensions[name] = dimensions.get(name, 0) + 1
return dict(dimensions)
@property
def parameter_names(self) -> List[str]:
"""Names of the Qiskit parameter vectors used by the circuit."""
return list(self.parameter_dimensions)
# ------------------------------------------------------------------
# Operations
# ------------------------------------------------------------------
[docs]
def bind_parameters(self, parameter_values: dict):
"""Bind values and return the resulting **Qiskit** circuit.
Distinct from :meth:`assign_parameters`, which stays in this
abstraction and returns a :class:`QiskitCircuit`.
Args:
parameter_values: Values keyed by parameter name.
Returns:
The bound ``qiskit.QuantumCircuit``.
"""
params_dict = build_params_dict(self.free_parameters, parameter_values)
if params_dict:
return self.native.assign_parameters(params_dict)
return self.native
[docs]
def copy(self) -> "QiskitCircuit":
"""Return an independent copy of this circuit."""
if self._native_override is not None:
return type(self).from_qiskit(self._native_override.copy())
return type(self)(self.num_qubits, self.num_clbits, _ir=self._ir.copy())
[docs]
def fingerprint(self) -> bytes:
"""Return a stable digest of the circuit's content."""
if self._native_override is not None:
# An adopted circuit has no instruction store to digest, so the
# digest comes from the Qiskit content key. It has to be content
# based: repr() on a Qiskit circuit includes the object address,
# which would give equal circuits different hashes.
return hashlib.blake2b(
repr(_circuit_key(self._native_override)).encode("utf-8"), digest_size=32
).digest()
return super().fingerprint()
def __hash__(self) -> int:
return hash(self.fingerprint())
def __eq__(self, other: Any) -> bool:
if type(self) is not type(other):
return False
if (self._native_override is None) != (other._native_override is None):
return False
if self._native_override is not None:
return self._native_override == other._native_override
return self._ir == other.ir
def __repr__(self):
return f"QiskitCircuit({self.num_qubits} qubits, {len(self.free_parameters)} parameters)"