"""
Stabilizer circuit simulator using Stim backend
"""
from typing import Any, Dict, List, Optional, Sequence
import numpy as np
import stim
from .abstractcircuit import AbstractCircuit
Tensor = Any
[docs]
class StabilizerCircuit(AbstractCircuit):
"""
Quantum circuit simulator for stabilizer circuits using Stim backend.
Supports Clifford operations and measurements.
"""
# Add gate sets as class attributes
clifford_gates = ["h", "x", "y", "z", "cnot", "cz", "swap", "s", "sd"]
gate_map = {
"h": "H",
"x": "X",
"y": "Y",
"z": "Z",
"cnot": "CNOT",
"cz": "CZ",
"swap": "SWAP",
"s": "S",
"sd": "S_DAG",
}
[docs]
def __init__(
self, nqubits: int, inputs: Tensor = None, tableau_inputs: Tensor = None
) -> None:
"""
``StabilizerCircuit`` class based on stim package
:param nqubits: Number of qubits
:type nqubits: int
:param inputs: initial state by stabilizers, defaults to None
:type inputs: Tensor, optional
:param tableau_inputs: initial state by **inverse** tableau, defaults to None
:type tableau_inputs: Tensor, optional
"""
self._nqubits = nqubits
self._stim_circuit = stim.Circuit()
self._qir: List[Dict[str, Any]] = []
self.is_dm = False
self.inputs = inputs
self.tableau_inputs = tableau_inputs
self._extra_qir: List[Dict[str, Any]] = []
self._measure_counter = 0
self.current_sim = stim.TableauSimulator()
self.circuit_param = {
"nqubits": nqubits,
"inputs": inputs,
"tableau_inputs": tableau_inputs,
}
if inputs:
self.current_sim.set_state_from_stabilizers(inputs)
if tableau_inputs:
self.current_sim.set_inverse_tableau(tableau_inputs)
[docs]
def apply_general_gate(
self,
gate: Any,
*index: int,
name: Optional[str] = None,
**kws: Any,
) -> None:
"""
Apply a Clifford gate to the circuit.
:param gate: Gate to apply (must be Clifford)
:type gate: Any
:param index: Qubit indices to apply the gate to
:type index: int
:param name: Name of the gate operation, defaults to None
:type name: Optional[str], optional
:raises ValueError: If non-Clifford gate is applied
"""
if name is None:
name = ""
# Record gate in QIR
gate_dict = {
"gate": gate,
"index": index,
"name": name,
"split": None,
"mpo": False,
}
ir_dict = kws["ir_dict"]
if ir_dict is not None:
ir_dict.update(gate_dict)
else:
ir_dict = gate_dict
self._qir.append(ir_dict)
# Map TensorCircuit gates to Stim gates
if name.lower() in self.gate_map:
# self._stim_circuit.append(gate_map[name.lower()], list(index))
gn = self.gate_map[name.lower()]
instruction = f"{gn} {' '.join(map(str, index))}"
self._stim_circuit.append_from_stim_program_text(instruction)
# append is much slower
# self.current_sim.do(stim.Circuit(instruction))
getattr(self.current_sim, gn.lower())(*index)
else:
raise ValueError(f"Gate {name} is not supported in stabilizer simulation")
apply = apply_general_gate
[docs]
def state(self) -> Tensor:
"""
Return the wavefunction of the circuit.
Note that the state can have smaller qubit count if no gate is applied on later qubits
"""
tab = self.current_tableau()
return tab.to_state_vector(endian="big")
[docs]
def random_gate(self, *index: int, recorded: bool = False) -> None:
"""
Apply a random Clifford gate to the circuit.
This operation will not record in qir
:param index: Qubit indices to apply the gate to
:type index: int
:param recorded: Whether the gate is recorded in ``stim.Circuit``, defaults to False
:type recorded: bool, optional
"""
m = len(index)
t = stim.Tableau.random(m)
self.current_sim.do_tableau(t, index)
if recorded:
self._stim_circuit += t.to_circuit()
[docs]
def tableau_gate(self, *index: int, tableau: Any, recorded: bool = False) -> None:
"""
Apply a gate indicated by tableau to the circuit.
This operation will not record in qir
:param index: Qubit indices to apply the gate to
:type index: int
:param tableau: stim.Tableau representation of the gate
:type tableau: Any
:param recorded: Whether the gate is recorded in ``stim.Circuit``, defaults to False
:type recorded: bool, optional
"""
self.current_sim.do_tableau(tableau, index)
if recorded:
self._stim_circuit += tableau.to_circuit()
[docs]
def measure(self, *index: int, with_prob: bool = False) -> Tensor:
"""
Measure qubits in the Z basis.
:param index: Indices of the qubits to measure.
:type index: int
:param with_prob: If True, returns the theoretical probability of the measurement outcome.
defaults to False
:type with_prob: bool, optional
:return: A tensor containing the measurement results.
If `with_prob` is True, a tuple containing the results and the probability is returned.
:rtype: Tensor
"""
# Convert negative indices
index = tuple([i for i in index if i >= 0])
# Add measurement instructions
s1 = self.current_simulator().copy()
# Sample once from the circuit using sampler
if with_prob:
num_random_measurements = 0
for i in index:
if s1.peek_z(i) == 0:
num_random_measurements += 1
probability = (0.5) ** num_random_measurements
m = s1.measure_many(*index)
if with_prob:
return m, probability
return m
[docs]
def cond_measurement(self, index: int) -> Tensor:
"""
Measure a single qubit in the Z basis and collapse the state.
:param index: The index of the qubit to measure.
:type index: int
:return: The measurement result (0 or 1).
:rtype: Tensor
"""
# Convert negative indices
# Add measurement instructions
self._stim_circuit.append_from_stim_program_text("M " + str(index))
# self.current_sim = None
m = self.current_simulator().measure(index)
# Sample once from the circuit using sampler
return m
cond_measure = cond_measurement
[docs]
def cond_measure_many(self, *index: int) -> Tensor:
"""
Measure multiple qubits in the Z basis and collapse the state.
:param index: The indices of the qubits to measure.
:type index: int
:return: A tensor containing the measurement results.
:rtype: Tensor
"""
# Convert negative indices
# Add measurement instructions
self._stim_circuit.append_from_stim_program_text(
"M " + " ".join(map(str, index))
)
# self.current_sim = None
m = self.current_simulator().measure_many(*index)
# Sample once from the circuit using sampler
return m
[docs]
def sample(
self,
batch: Optional[int] = None,
**kws: Any,
) -> Tensor:
"""
Sample measurements from the circuit.
:param batch: Number of samples to take, defaults to None (single sample)
:type batch: Optional[int], optional
:return: Measurement results
:rtype: Tensor
"""
if batch is None:
batch = 1
c = self.current_circuit().copy()
for i in range(self._nqubits):
c.append("M", [i])
sampler = c.compile_sampler()
samples = sampler.sample(batch)
return np.array(samples)
[docs]
def expectation_ps( # type: ignore
self,
x: Optional[Sequence[int]] = None,
y: Optional[Sequence[int]] = None,
z: Optional[Sequence[int]] = None,
**kws: Any,
) -> Any:
"""
Compute exact expectation value of Pauli string using stim's direct calculation.
:param x: Indices for Pauli X measurements
:type x: Optional[Sequence[int]], optional
:param y: Indices for Pauli Y measurements
:type y: Optional[Sequence[int]], optional
:param z: Indices for Pauli Z measurements
:type z: Optional[Sequence[int]], optional
:return: Expectation value
:rtype: float
"""
# Build Pauli string representation
pauli_str = ["I"] * self._nqubits
if x:
for i in x:
pauli_str[i] = "X"
if y:
for i in y:
pauli_str[i] = "Y"
if z:
for i in z:
pauli_str[i] = "Z"
pauli_string = "".join(pauli_str)
# Calculate expectation using stim's direct method
expectation = self.current_simulator().peek_observable_expectation(
stim.PauliString(pauli_string)
)
return expectation
expps = expectation_ps
[docs]
def sample_expectation_ps(
self,
x: Optional[Sequence[int]] = None,
y: Optional[Sequence[int]] = None,
z: Optional[Sequence[int]] = None,
shots: Optional[int] = None,
**kws: Any,
) -> float:
"""
Compute expectation value of Pauli string measurements.
:param x: Indices for Pauli X measurements, defaults to None
:type x: Optional[Sequence[int]], optional
:param y: Indices for Pauli Y measurements, defaults to None
:type y: Optional[Sequence[int]], optional
:param z: Indices for Pauli Z measurements, defaults to None
:type z: Optional[Sequence[int]], optional
:param shots: Number of measurement shots, defaults to None
:type shots: Optional[int], optional
:return: Expectation value
:rtype: float
"""
if shots is None:
shots = 1000 # Default number of shots
circuit = self._stim_circuit.copy()
# Add basis rotations for measurements
if x:
for i in x:
circuit.append("H", [i])
if y:
for i in y:
circuit.append("S_DAG", [i])
circuit.append("H", [i])
# Add measurements
measured_qubits: List[int] = []
if x:
measured_qubits.extend(x)
if y:
measured_qubits.extend(y)
if z:
measured_qubits.extend(z)
for i in measured_qubits:
circuit.append("M", [i])
# Sample and compute expectation using sampler
sampler = circuit.compile_sampler()
samples = sampler.sample(shots)
results = np.array(samples)
# Convert from {0,1} to {1,-1}
results = 1 - 2 * results
# Average over shots
expectation = np.mean(np.prod(results, axis=1))
return float(expectation)
sexpps = sample_expectation_ps
[docs]
def mid_measurement(self, index: int, keep: int = 0) -> Tensor:
"""
Perform a mid-measurement operation on a qubit on z direction.
The post-selection cannot be recorded in ``stim.Circuit``
:param index: Index of the qubit to measure
:type index: int
:param keep: State of qubits to keep after measurement, defaults to 0 (up)
:type keep: int, optional
:return: Result of the mid-measurement operation
:rtype: Tensor
"""
if keep not in [0, 1]:
raise ValueError("keep must be 0 or 1")
self.current_sim.postselect_z(index, desired_value=keep)
mid_measure = mid_measurement
post_select = mid_measurement
post_selection = mid_measurement
[docs]
def depolarizing(self, *index: int, p: float) -> None:
"""
Apply depolarizing noise to a qubit.
:param index: Index of the qubit to apply noise to
:type index: int
:param p: Noise parameter (probability of depolarizing)
:type p: float
"""
self._stim_circuit.append_from_stim_program_text(
f"DEPOLARIZE1({p}) {' '.join(map(str, index))}"
)
self.current_sim.depolarize1(*index, p=p)
[docs]
def current_simulator(self) -> stim.TableauSimulator:
"""
Return the current simulator of the circuit.
"""
return self.current_sim
[docs]
def current_circuit(self) -> stim.Circuit:
"""
Return the current stim circuit representation of the circuit.
"""
return self._stim_circuit
[docs]
def current_tableau(self) -> stim.Tableau:
"""
Return the current tableau of the circuit.
"""
return self.current_simulator().current_inverse_tableau() ** -1
[docs]
def current_inverse_tableau(self) -> stim.Tableau:
"""
Return the current inverse tableau of the circuit.
"""
return self.current_simulator().current_inverse_tableau()
[docs]
def entanglement_entropy(self, cut: Sequence[int]) -> float:
"""
Calculate the entanglement entropy for a subset of qubits using stabilizer formalism.
:param cut: Indices of qubits to calculate entanglement entropy for
:type cut: Sequence[int]
:return: Entanglement entropy
:rtype: float
"""
# Get stabilizer tableau
tableau = self.current_tableau()
N = len(tableau)
# Pre-allocate binary matrix with proper dtype
# binary_matrix = np.zeros((N, 2 * N), dtype=np.int8)
# Vectorized conversion of stabilizers to binary matrix
# z_outputs = np.array([tableau.z_output(k) for k in range(N)])
# x_part = z_outputs == 1 # X
# z_part = z_outputs == 3 # Z
# y_part = z_outputs == 2 # Y
# binary_matrix[:, :N] = x_part | y_part
# binary_matrix[:, N:] = z_part | y_part
_, _, z2x, z2z, _, _ = tableau.to_numpy()
binary_matrix = np.concatenate([z2x, z2z], axis=1)
# Get reduced matrix for the cut using boolean indexing
cut_set = set(cut)
cut_indices = np.array(
[i for i in range(N) if i in cut_set]
+ [i + N for i in range(N) if i in cut_set]
)
reduced_matrix = binary_matrix[:, cut_indices]
# Efficient rank calculation using Gaussian elimination
matrix = reduced_matrix.copy()
n_rows, n_cols = matrix.shape
rank = 0
row = 0
for col in range(n_cols):
# Vectorized pivot finding
pivot_rows = np.nonzero(matrix[row:, col])[0]
if len(pivot_rows) > 0:
pivot_row = pivot_rows[0] + row
# Swap rows if necessary
if pivot_row != row:
matrix[row], matrix[pivot_row] = (
matrix[pivot_row].copy(),
matrix[row].copy(),
)
# Vectorized elimination
eliminate_mask = matrix[row + 1 :, col] == 1
matrix[row + 1 :][eliminate_mask] ^= matrix[row]
rank += 1
row += 1
if row == n_rows:
break
# Calculate entropy
return float((rank - len(cut)) * np.log(2))
# Call _meta_apply at module level to register the gates
StabilizerCircuit._meta_apply()