"""Module for simulating circuits and Measurement Patterns.
This module provides:
- `SimulatorBackend` : Enum class for circuit simulator backends.
- `CircuitSimulator` : Class for simulating circuits.
- `PatternSimulator` : Class for simulating Measurement Patterns.
"""
from __future__ import annotations
import functools
from enum import Enum, auto
from typing import TYPE_CHECKING
import numpy as np
from graphqomb.command import TICK, E, M, N, X, Z
from graphqomb.common import MeasBasis, Plane
from graphqomb.gates import MultiGate, SingleGate, TwoQubitGate
from graphqomb.pattern import is_runnable
from graphqomb.rng import ensure_rng
from graphqomb.statevec import StateVector
if TYPE_CHECKING:
from graphqomb.circuit import BaseCircuit
from graphqomb.command import Command
from graphqomb.gates import Gate
from graphqomb.pattern import Pattern
from graphqomb.simulator_backend import BaseFullStateSimulator
[docs]
class SimulatorBackend(Enum):
"""Enum class for circuit simulator backend.
Available backends are:
- StateVector
- DensityMatrix
"""
StateVector = auto()
DensityMatrix = auto()
[docs]
class CircuitSimulator:
r"""Class for simulating circuits.
Attributes
----------
state : `BaseFullStateSimulator`
The quantum state of the simulator.
gate_instructions : `list`\[`Gate`\]
The list of gate instructions to be applied.
"""
state: BaseFullStateSimulator
gate_instructions: list[Gate]
[docs]
def __init__(self, mbqc_circuit: BaseCircuit, backend: SimulatorBackend) -> None:
if backend == SimulatorBackend.StateVector:
self.state = StateVector.from_num_qubits(mbqc_circuit.num_qubits)
elif backend == SimulatorBackend.DensityMatrix:
raise NotImplementedError
else:
msg = f"Invalid backend: {backend}"
raise ValueError(msg)
self.gate_instructions = mbqc_circuit.instructions()
[docs]
def apply_gate(self, gate: Gate) -> None:
"""Apply a gate to the circuit.
Parameters
----------
gate : `Gate`
The gate to apply.
Raises
------
TypeError
If the gate type is not supported.
"""
operator = gate.matrix()
# Get qubits that the gate acts on
if isinstance(gate, SingleGate):
# Single qubit gate
qubits = [gate.qubit]
elif isinstance(gate, (TwoQubitGate, MultiGate)):
# Multi-qubit gate (both TwoQubitGate and MultiGate have qubits attribute)
qubits = list(gate.qubits)
else:
msg = f"Cannot determine qubits for gate: {gate}"
raise TypeError(msg)
self.state.evolve(operator, qubits)
[docs]
def simulate(self) -> None:
"""Simulate the circuit."""
for gate in self.gate_instructions:
self.apply_gate(gate)
[docs]
class PatternSimulator:
r"""Class for simulating Measurement Patterns.
Attributes
----------
state : `BaseFullStateSimulator`
The quantum state of the simulator.
node_indices : `list`\[`int`\]
The list of node indices in the pattern.
results : `dict`\[`int`, `bool`\]
The measurement results for each node.
calc_prob : `bool`
Whether to calculate probabilities.
"""
state: BaseFullStateSimulator
node_indices: list[int]
results: dict[int, bool]
calc_prob: bool
__pattern: Pattern
[docs]
def __init__(
self,
pattern: Pattern,
backend: SimulatorBackend,
*,
calc_prob: bool = False,
) -> None:
self.node_indices = list(pattern.input_node_indices.keys())
self.results = {}
self.calc_prob = calc_prob
self.__pattern = pattern
# Pattern runnability check is done via is_runnable function
is_runnable(self.__pattern)
if backend == SimulatorBackend.StateVector:
# Note: deterministic check skipped for now
self.state = StateVector.from_num_qubits(len(self.__pattern.input_node_indices))
elif backend == SimulatorBackend.DensityMatrix:
raise NotImplementedError
else:
msg = f"Invalid backend: {backend}"
raise ValueError(msg)
[docs]
@functools.singledispatchmethod
def apply_cmd(self, cmd: Command, *, rng: np.random.Generator) -> None:
"""Apply a command to the state.
Parameters
----------
cmd : `Command`
The command to apply.
rng : `numpy.random.Generator`
Random number generator to use.
"""
self.apply_cmd(cmd, rng=rng)
@apply_cmd.register
def _(self, cmd: N, *, rng: np.random.Generator) -> None: # noqa: ARG002
self.state.add_node(1)
self.node_indices.append(cmd.node)
@apply_cmd.register
def _(self, cmd: E, *, rng: np.random.Generator) -> None: # noqa: ARG002
node_id1 = self.node_indices.index(cmd.nodes[0])
node_id2 = self.node_indices.index(cmd.nodes[1])
self.state.entangle(node_id1, node_id2)
def _updated_measurement_basis(self, cmd: M) -> MeasBasis:
basis = cmd.meas_basis
x_pauli = self.__pattern.pauli_frame.x_pauli[cmd.node]
z_pauli = self.__pattern.pauli_frame.z_pauli[cmd.node]
if cmd.meas_basis.plane == Plane.XY:
if x_pauli:
basis = basis.conjugate()
if z_pauli:
basis = basis.flip()
elif cmd.meas_basis.plane == Plane.YZ:
if x_pauli:
basis = basis.flip()
if z_pauli:
basis = basis.conjugate()
else:
if x_pauli ^ z_pauli:
basis = basis.conjugate()
if x_pauli:
basis = basis.flip()
return basis
@apply_cmd.register
def _(self, cmd: M, *, rng: np.random.Generator) -> None:
if self.calc_prob:
raise NotImplementedError
result = rng.uniform() < 1 / 2
node_id = self.node_indices.index(cmd.node)
self.state.measure(node_id, self._updated_measurement_basis(cmd), result)
self.results[cmd.node] = result
self.node_indices.remove(cmd.node)
if result:
self.__pattern.pauli_frame.meas_flip(cmd.node)
@apply_cmd.register
def _(self, cmd: X, *, rng: np.random.Generator) -> None: # noqa: ARG002
node_id = self.node_indices.index(cmd.node)
if self.__pattern.pauli_frame.x_pauli[cmd.node]:
self.state.evolve(np.asarray([[0, 1], [1, 0]]), node_id)
@apply_cmd.register
def _(self, cmd: Z, *, rng: np.random.Generator) -> None: # noqa: ARG002
node_id = self.node_indices.index(cmd.node)
if self.__pattern.pauli_frame.z_pauli[cmd.node]:
self.state.evolve(np.asarray([[1, 0], [0, -1]]), node_id)
@apply_cmd.register
def _(self, cmd: TICK, *, rng: np.random.Generator) -> None:
# TICK is a time separator that doesn't affect quantum state
pass
[docs]
def simulate(self, rng: np.random.Generator | None = None) -> None:
"""
Simulate the pattern.
Parameters
----------
rng : `numpy.random.Generator` | None, optional
Random number generator to use for measurement outcomes.
If None, a new generator will be created using the default random source. Default is None.
"""
rng = ensure_rng(rng)
for cmd in self.__pattern.commands:
self.apply_cmd(cmd, rng=rng)
# Create a mapping from current node indices to output node indices
permutation = [self.__pattern.output_node_indices[node] for node in self.node_indices]
self.state.reorder(permutation)