"""Opcode simulator, a fundamental simulator for UnifiedQuantum.
It simulates from a basic opcode
"""
__all__ = ["backend_alias", "OpcodeSimulator"]
from typing import TYPE_CHECKING
import numpy as np
from uniqc_cpp import *
from uniqc.circuit_builder.qcircuit import OpcodeType
if TYPE_CHECKING:
from .uniqc_cpp import *
[docs]
def backend_alias(backend_type):
"""Resolve backend type aliases to canonical names.
Recognised aliases (case-insensitive)::
statevector / state_vector -> statevector
density_matrix / density_operator
/ densitymatrix / densityoperator -> density_operator
density_matrix_qutip / density_operator_qutip
-> density_operator_qutip
Returns:
Canonical backend type string ("statevector", "density_operator",
or "density_operator_qutip").
Raises:
ValueError: If ``backend_type`` is not a recognised alias.
"""
statevector_alias = ["statevector", "state_vector"]
density_operator_alias = ["density_matrix", "density_operator", "densitymatrix", "densityoperator"]
density_operator_qutip_alias = ["density_matrix_qutip", "density_operator_qutip"]
backend_type = backend_type.lower()
if backend_type in statevector_alias:
return "statevector"
elif backend_type in density_operator_alias:
return "density_operator"
elif backend_type in density_operator_qutip_alias:
return "density_operator_qutip"
else:
raise ValueError(f"Unknown backend type: {backend_type}")
[docs]
class OpcodeSimulator:
"""A quantum circuit simulator based on C++ that runs locally.
Args:
backend_type: Backend type for simulation. Supported: 'statevector',
'density_matrix', 'density_matrix_qutip'. Defaults to 'statevector'.
Attributes:
simulator: The underlying C++ simulator instance.
typestr: Human-readable backend type string.
"""
def __init__(self, backend_type="statevector"):
"""Initialize the OpcodeSimulator.
Args:
backend_type: The backend type for simulation ("statevector" or "density_matrix").
Supported aliases: statevector, state_vector, density_matrix,
density_operator, density_matrix_qutip, density_operator_qutip.
"""
original_backend_type = backend_type
backend_type = backend_alias(backend_type)
if backend_type == "statevector":
self.SimulatorType = StatevectorSimulator
self.simulator_typestr = "statevector"
elif backend_type == "density_operator":
self.SimulatorType = DensityOperatorSimulator
self.simulator_typestr = "density_operator"
elif backend_type == "density_operator_qutip":
try:
from .qutip_sim_impl import DensityOperatorSimulatorQutip
except ImportError as exc:
raise ImportError(
f"backend_type={original_backend_type!r} (canonical name "
"'density_operator_qutip', alias 'density_matrix_qutip') "
"requires the optional 'simulation' extras "
"(qutip + qutip-qip). Install with "
"``pip install unified-quantum[simulation]``."
) from exc
self.SimulatorType = DensityOperatorSimulatorQutip
self.simulator_typestr = "density_operator"
else:
raise ValueError(f"Unknown backend type: {backend_type}")
self.simulator = self.SimulatorType()
def _clear(self):
"""Reset the simulator by creating a fresh simulator instance."""
self.simulator = self.SimulatorType()
def _simulate_common_gate(self, operation, qubit, cbit, parameter, is_dagger, control_qubits_set):
"""Dispatch a single gate to the underlying simulator."""
if operation == "RX":
self.simulator.rx(qubit, parameter, control_qubits_set, is_dagger)
elif operation == "RY":
self.simulator.ry(qubit, parameter, control_qubits_set, is_dagger)
elif operation == "RZ":
self.simulator.rz(qubit, parameter, control_qubits_set, is_dagger)
elif operation == "U1":
self.simulator.u1(qubit, parameter, control_qubits_set, is_dagger)
elif operation == "U2":
self.simulator.u2(qubit, parameter[0], parameter[1], control_qubits_set, is_dagger)
elif operation == "H":
self.simulator.hadamard(qubit, control_qubits_set, is_dagger)
elif operation == "X":
self.simulator.x(qubit, control_qubits_set, is_dagger)
elif operation == "SX":
self.simulator.sx(qubit, control_qubits_set, is_dagger)
elif operation == "Y":
self.simulator.y(qubit, control_qubits_set, is_dagger)
elif operation == "Z":
self.simulator.z(qubit, control_qubits_set, is_dagger)
elif operation == "S":
self.simulator.s(qubit, control_qubits_set, is_dagger)
elif operation == "T":
self.simulator.t(qubit, control_qubits_set, is_dagger)
elif operation == "CZ":
self.simulator.cz(qubit[0], qubit[1], control_qubits_set, is_dagger)
elif operation == "SWAP":
self.simulator.swap(qubit[0], qubit[1], control_qubits_set, is_dagger)
elif operation == "ISWAP":
self.simulator.iswap(qubit[0], qubit[1], control_qubits_set, is_dagger)
elif operation == "TOFFOLI":
self.simulator.toffoli(qubit[0], qubit[1], qubit[2], control_qubits_set, is_dagger)
elif operation == "CSWAP":
self.simulator.cswap(qubit[0], qubit[1], qubit[2], control_qubits_set, is_dagger)
elif operation == "XY":
self.simulator.xy(qubit[0], qubit[1], parameter, control_qubits_set, is_dagger)
elif operation == "CNOT":
self.simulator.cnot(qubit[0], qubit[1], control_qubits_set, is_dagger)
elif operation == "RPhi":
self.simulator.rphi(qubit, parameter[0], parameter[1], control_qubits_set, is_dagger)
elif operation == "RPhi90":
self.simulator.rphi90(qubit, parameter, control_qubits_set, is_dagger)
elif operation == "RPhi180":
self.simulator.rphi180(qubit, parameter, control_qubits_set, is_dagger)
elif operation == "U3":
self.simulator.u3(qubit, parameter[0], parameter[1], parameter[2], control_qubits_set, is_dagger)
elif operation == "XX":
self.simulator.xx(qubit[0], qubit[1], parameter, control_qubits_set, is_dagger)
elif operation == "YY":
self.simulator.yy(qubit[0], qubit[1], parameter, control_qubits_set, is_dagger)
elif operation == "ZZ":
self.simulator.zz(qubit[0], qubit[1], parameter, control_qubits_set, is_dagger)
elif operation == "UU15":
self.simulator.uu15(qubit[0], qubit[1], parameter, control_qubits_set, True)
elif operation == "PHASE2Q":
self.simulator.phase2q(
qubit[0], qubit[1], parameter[0], parameter[1], parameter[2], control_qubits_set, is_dagger
)
elif operation == "PauliError1Q":
# parameter[0]: probability of X error
# parameter[1]: probability of Y error
# parameter[2]: probability of Z error
self.simulator.pauli_error_1q(qubit, parameter[0], parameter[1], parameter[2])
elif operation == "Depolarizing":
# parameter: depolarizing probability
self.simulator.depolarizing(qubit, parameter)
elif operation == "BitFlip":
# parameter: bit flip probability
self.simulator.bitflip(qubit, parameter)
elif operation == "PhaseFlip":
# parameter: phase flip probability
self.simulator.phaseflip(qubit, parameter)
elif operation == "AmplitudeDamping":
# parameter: phase flip probability
self.simulator.amplitude_damping(qubit, parameter)
elif operation == "PauliError2Q":
# parameter: List[float]
self.simulator.pauli_error_2q(qubit[0], qubit[1], parameter)
elif operation == "TwoQubitDepolarizing":
# parameter: depolarizing probability
self.simulator.twoqubit_depolarizing(qubit[0], qubit[1], parameter)
elif operation == "Kraus1Q":
# parameter: List[List[complex]]
if not isinstance(parameter, list):
raise ValueError("Kraus1Q parameter should be a list of U22 matrices.")
def build_u22(arr):
# the cases include the following:
# 1. a 2x2 np.ndarray
# 2. a 4 elements np.ndarray[complex]
# 3. a 2x2 List[List[complex]]
# 4. a 4-element List[complex]
# Final target is to transform into case (4), a 4-element List[complex]
if isinstance(arr, np.ndarray):
if arr.shape == (2, 2):
return [arr[0][0], arr[0][1], arr[1][0], arr[1][1]]
elif arr.shape == (4,):
return arr.tolist()
else:
raise ValueError("Kraus1Q parameter should be a 2x2 or 4-element list or numpy array.")
elif isinstance(arr, list):
if len(arr) == 4:
return arr
elif len(arr) == 2 and len(arr[0]) == 2 and len(arr[1]) == 2:
return [arr[0][0], arr[0][1], arr[1][0], arr[1][1]]
else:
raise ValueError("Kraus1Q parameter should be a 2x2 or 4-element list or numpy array.")
else:
raise ValueError("Kraus1Q parameter should be a 2x2 or 4-element list or numpy array.")
parameters_ = [build_u22(arr) for arr in parameter]
self.simulator.kraus1q(qubit, parameters_)
elif operation == "AmplitudeDamping":
# parameter: gamma
self.simulator.amplitude_damping(qubit, parameter)
elif operation == "ECR":
# ECR (Echoed Cross-Resonance) decomposition using native gates.
# ECR(0,1) = SX(0)·SX(1)·X(0)·X(1)·CNOT(0,1)·S(0) (right-to-left)
# SX is self-adjoint (SXdagger = SX).
# Note: only the non-dagger form is implemented here.
if is_dagger:
# ECR^dagger = S^dagger(0)·CNOT(0,1)·X(0)·X(1)·SX(0)·SX(1)
self.simulator.sx(qubit[0], control_qubits_set, True)
self.simulator.sx(qubit[1], control_qubits_set, True)
self.simulator.x(qubit[0], control_qubits_set, True)
self.simulator.x(qubit[1], control_qubits_set, True)
self.simulator.cnot(qubit[0], qubit[1], control_qubits_set, True)
self.simulator.s(qubit[0], control_qubits_set, True)
else:
self.simulator.s(qubit[0], control_qubits_set, False)
self.simulator.cnot(qubit[0], qubit[1], control_qubits_set, False)
self.simulator.x(qubit[0], control_qubits_set, False)
self.simulator.x(qubit[1], control_qubits_set, False)
self.simulator.sx(qubit[0], control_qubits_set, False)
self.simulator.sx(qubit[1], control_qubits_set, False)
elif (
operation == "I"
or operation == None
or operation == "QINIT"
or operation == "CREG"
or operation == "BARRIER"
):
pass
else:
raise RuntimeError(
"Unknown Opcode operation. "
f"Operation: {operation}."
f"Full opcode: {(operation, qubit, cbit, parameter, control_qubits_set, is_dagger)}"
)
[docs]
def simulate_gate(self, operation, qubit, cbit, parameter, is_dagger, control_qubits_set):
"""Apply a single gate opcode to the simulator.
Args:
operation: Gate name string.
qubit: Qubit index or list of indices.
cbit: Classical bit index.
parameter: Gate parameters.
is_dagger: Whether to apply the dagger version of the gate.
control_qubits_set: Set of control qubits.
"""
# convert from set to list (to adapt to C++ input)
if control_qubits_set:
control_qubits_set = list(control_qubits_set)
else:
control_qubits_set = list()
self._simulate_common_gate(operation, qubit, cbit, parameter, is_dagger, control_qubits_set)
[docs]
def simulate_opcodes_pmeasure(self, n_qubit, program_body, measure_qubits):
"""Compute measurement probabilities for a list of opcodes.
Args:
n_qubit: Number of qubits.
program_body: List of opcodes to simulate.
measure_qubits: Qubits to measure.
Returns:
List of probabilities for each measurement outcome.
"""
self.simulator.init_n_qubit(n_qubit)
for opcode in program_body:
operation, qubit, cbit, parameter, is_dagger, control_qubits_set = opcode
self.simulate_gate(operation, qubit, cbit, parameter, is_dagger, control_qubits_set)
prob_list = self.simulator.pmeasure(measure_qubits)
return prob_list
[docs]
def simulate_opcodes_statevector(self, n_qubit, program_body):
"""Compute the final statevector after executing a list of opcodes.
Args:
n_qubit: Number of qubits.
program_body: List of opcodes to simulate.
Returns:
Statevector as a complex numpy array.
Raises:
ValueError: If backend is density_matrix type.
"""
if self.simulator_typestr == "density_matrix":
raise ValueError("Density matrix is not supported for statevector simulation.")
self.simulator.init_n_qubit(n_qubit)
for opcode in program_body:
operation, qubit, cbit, parameter, is_dagger, control_qubits_set = opcode
self.simulate_gate(operation, qubit, cbit, parameter, is_dagger, control_qubits_set)
statevector = self.simulator.state
return statevector
[docs]
def simulate_opcodes_stateprob(self, n_qubit, program_body):
"""Compute state probabilities ``(|amplitude|^2)`` for all basis states.
Args:
n_qubit: Number of qubits.
program_body: List of opcodes to simulate.
Returns:
Array of probabilities for each basis state.
Raises:
ValueError: If simulator type is unknown.
"""
if self.simulator_typestr == "statevector":
statevector = self.simulate_opcodes_statevector(n_qubit, program_body)
statevector = np.array(statevector)
return np.abs(statevector) ** 2
if self.simulator_typestr == "density_operator":
self.simulator.init_n_qubit(n_qubit)
for opcode in program_body:
operation, qubit, cbit, parameter, is_dagger, control_qubits_set = opcode
self.simulate_gate(operation, qubit, cbit, parameter, is_dagger, control_qubits_set)
return self.simulator.stateprob()
raise ValueError("Unknown simulator type.")
[docs]
def simulate_opcodes_density_operator(self, n_qubit, program_body):
"""Compute the density matrix after executing a list of opcodes.
Args:
n_qubit: Number of qubits.
program_body: List of opcodes to simulate.
Returns:
Density matrix as a 2D numpy array.
Raises:
ValueError: If simulator type is unknown.
"""
if self.simulator_typestr == "density_operator":
self.simulator.init_n_qubit(n_qubit)
for opcode in program_body:
operation, qubit, cbit, parameter, is_dagger, control_qubits_set = opcode
self.simulate_gate(operation, qubit, cbit, parameter, is_dagger, control_qubits_set)
state = self.simulator.state
state = np.array(state)
density_matrix = np.reshape(state, (2**n_qubit, 2**n_qubit), order="C")
return density_matrix
if self.simulator_typestr == "statevector":
statevector = self.simulate_opcodes_statevector(n_qubit, program_body)
statevector = np.array(statevector)
density_matrix = np.outer(statevector, np.conj(statevector))
return density_matrix
raise ValueError("Unknown simulator type.")
[docs]
def simulate_opcodes_shot(self, n_qubit, program_body: list[OpcodeType], measure_qubits):
"""Execute a list of opcodes and return a single measurement sample.
Args:
n_qubit: Number of qubits.
program_body: List of opcodes to simulate.
measure_qubits: Qubits to measure.
Returns:
Integer representing the measured bitstring (decimal).
Raises:
NotImplementedError: If backend is density_operator type.
"""
if self.simulator_typestr == "density_operator":
raise NotImplementedError("Density matrix is not supported for shot simulation.")
self.simulator.init_n_qubit(n_qubit)
for opcode in program_body:
operation, qubit, cbit, parameter, is_dagger, control_qubits_set = opcode
self.simulate_gate(operation, qubit, cbit, parameter, is_dagger, control_qubits_set)
return self.simulator.measure_single_shot(measure_qubits)