from abc import ABC, abstractmethod
from typing import Type
from dataclasses import dataclass, field
import warnings
from qutip_qip.operations import Gate, Measurement
def _validate_non_negative_int_tuple(T: any, txt: str = ""):
if type(T) is not tuple:
raise TypeError(f"Must pass a tuple for {txt}, got {type(T)}")
for q in T:
if type(q) is not int:
raise ValueError(f"All {txt} indices must be an int, found {q}")
if q < 0:
raise ValueError(f"{txt} indices must be non-negative, found {q}")
[docs]
@dataclass(frozen=True, slots=True)
class CircuitInstruction(ABC):
operation: Gate | Type[Gate] | Measurement
qubits: tuple[int, ...] = tuple()
cbits: tuple[int, ...] = tuple()
style: dict = field(default_factory=dict)
def __post_init__(self):
"""Basic validation for all instructions."""
if not len(self.qubits) and not len(self.cbits):
raise ValueError(
"Circuit Instruction must operate on at least one qubit or cbit."
)
_validate_non_negative_int_tuple(self.qubits, "qubit")
_validate_non_negative_int_tuple(self.cbits, "cbit")
if len(self.qubits) != len(set(self.qubits)):
raise ValueError("Found repeated qubits")
if len(self.cbits) != len(set(self.cbits)):
raise ValueError("Found repeated cbits")
@staticmethod
def is_gate_instruction() -> bool:
return False
@staticmethod
def is_measurement_instruction() -> bool:
return False
@abstractmethod
def to_qasm(self, qasm_out) -> None:
raise NotImplementedError
@abstractmethod
def __str__(self) -> str:
raise NotImplementedError
def __repr__(self) -> str:
return str(self)
[docs]
@dataclass(frozen=True, slots=True)
class GateInstruction(CircuitInstruction):
operation: Gate | Type[Gate]
cbits_ctrl_value: int | None = None
def __post_init__(self) -> None:
super(GateInstruction, self).__post_init__()
# Don't make it super(), it will throw an error because slots=True
# destroys __class__ reference to the original class until Python 3.13
# Check CPython Issue #90562, this has been resolved in Python 3.14
if not (isinstance(self.operation, Gate) or issubclass(self.operation, Gate)):
raise TypeError(f"Operation must be a Gate, got {self.operation}")
if len(self.qubits) != self.operation.num_qubits:
raise ValueError(
f"Gate '{self.operation.name}' requires {self.operation.num_qubits} qubits."
f" But got {len(self.qubits)}."
)
if len(self.cbits) > 0 and self.cbits_ctrl_value is None:
raise ValueError(
"cbits_ctrl_value can't be None if classical controls are provided."
)
if self.cbits_ctrl_value is not None:
if self.cbits_ctrl_value < 0:
raise ValueError(
f"Classical Control value can't be negative, got {self.cbits_ctrl_value}"
)
if self.cbits_ctrl_value > 2 ** len(self.cbits) - 1:
raise ValueError(
f"Classical Control value can't be greater than 2^num_cbits -1, got {self.cbits_ctrl_value}."
)
@property
def controls(self) -> tuple[int, ...]:
if self.operation.is_controlled:
return self.qubits[: self.operation.num_ctrl_qubits]
return ()
@property
def targets(self) -> tuple[int, ...]:
if self.operation.is_controlled:
return self.qubits[self.operation.num_ctrl_qubits :]
return self.qubits
@staticmethod
def is_gate_instruction() -> bool:
return True
def __getattr__(self, name):
"""
Temporary backward compatibility layer to:
forward selected old Gate attributes to ``operation`` with a
deprecation warning.
"""
if name in ("name", "num_qubits", "arg_value"):
warnings.warn(
f"Gate object in a circuit has been replaced by GateInstructions."
f"use GateInstruction.operation.{name} instead.",
DeprecationWarning,
stacklevel=2,
)
value = getattr(self.operation, name)
if name == "arg_value" and isinstance(value, tuple) and len(value) == 1:
return value[0]
return value
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{name}'"
)
def to_qasm(self, qasm_out) -> None:
gate = self.operation
args = None
if gate.is_parametric:
args = gate.arg_value
qasm_gate = qasm_out.qasm_name(gate.name)
if not qasm_gate:
error_str = f"{gate.name} gate's qasm defn is not specified"
raise NotImplementedError(error_str)
if self.cbits:
err_msg = "Exporting controlled gates is not implemented yet."
raise NotImplementedError(err_msg)
else:
qasm_out.output(
qasm_out._qasm_str(
q_name=qasm_gate,
q_targets=list(self.targets),
q_controls=list(self.controls),
q_args=args,
)
)
def __str__(self) -> str:
return f"Gate({self.operation}), qubits({self.qubits}),\
cbits({self.cbits}), style({self.style})"
[docs]
@dataclass(frozen=True, slots=True)
class MeasurementInstruction(CircuitInstruction):
operation: Measurement
def __post_init__(self) -> None:
super(MeasurementInstruction, self).__post_init__()
if not isinstance(self.operation, Measurement):
raise TypeError(f"Operation must be a measurement, got {self.operation}")
if len(self.qubits) != len(self.cbits):
raise ValueError("Measurement requires equal number of qubits and cbits.")
@staticmethod
def is_measurement_instruction() -> bool:
return True
def to_qasm(self, qasm_out) -> None:
for qubit, cbit in zip(self.qubits, self.cbits):
qasm_out.output(f"measure q[{qubit}] -> c[{cbit}];")
def __str__(self) -> str:
return f"Measure(q{self.qubits} -> c{self.cbits})"