Skip to content

Statevector Simulator

quanta.simulator.statevector

quanta.simulator.statevector -- NumPy-based statevector simulator.

v0.2: Rewritten with tensor contraction method.

OLD (v0.1): Kronecker expansion O(4^n) -- max 12 qubits, 1.8s NEW (v0.2): Tensor contraction O(2^n) -- max 26 qubits, <20s

Method

Keep statevector as [2,2,...,2] tensor. Apply gate only to relevant axes (np.tensordot).

Example

sim = StateVectorSimulator(num_qubits=20) sim.apply("H", (0,)) sim.apply("CX", (0, 1)) sim.probabilities()[:4] array([0.5, 0. , 0. , 0.5])

SimulatorError

Bases: QuantaError

Simulator runtime error.

Source code in quanta/simulator/statevector.py
class SimulatorError(QuantaError):
    """Simulator runtime error."""

StateVectorSimulator

Bases: SimulatorBackend

Tensor-based statevector simulator.

Simulates quantum circuits on a 2^n dimensional complex vector. v0.2: O(2^n) performance with np.tensordot -- supports 26 qubits.

Parameters:

Name Type Description Default
num_qubits int

Number of qubits to simulate.

required
seed int | None

Random seed for reproducibility.

None
Source code in quanta/simulator/statevector.py
class StateVectorSimulator(SimulatorBackend):
    """Tensor-based statevector simulator.

    Simulates quantum circuits on a 2^n dimensional complex vector.
    v0.2: O(2^n) performance with np.tensordot -- supports 26 qubits.

    Args:
        num_qubits: Number of qubits to simulate.
        seed: Random seed for reproducibility.
    """

    MAX_QUBITS = 27  # Memory limit: ~2 GB

    __slots__ = ("num_qubits", "_state", "_rng")

    def __init__(self, num_qubits: int, seed: int | None = None) -> None:
        if num_qubits > self.MAX_QUBITS:
            raise SimulatorError(
                f"Max {self.MAX_QUBITS} qubits supported, "
                f"requested: {num_qubits}"
            )

        self.num_qubits = num_qubits
        self._rng = np.random.default_rng(seed)

        # Initial state: |00...0>
        dim = 2 ** num_qubits
        self._state = np.zeros(dim, dtype=complex)
        self._state[0] = 1.0

    def apply(
        self,
        gate_name: str,
        qubits: tuple[int, ...],
        params: tuple[float, ...] = (),
    ) -> None:
        """Applies a gate to the statevector via tensor contraction.

        Args:
            gate_name: Gate name (e.g., "H", "CX").
            qubits: Target qubit indices.
            params: Angles for parametric gates.
        """
        matrix = self._get_gate_matrix(gate_name, params)
        self._apply_tensor(matrix, qubits)

    def _apply_tensor(
        self, gate: np.ndarray, qubits: tuple[int, ...]
    ) -> None:
        """Gate application via tensor contraction.

        Uses JAX/CuPy acceleration if available, falls back to NumPy.
        """
        try:
            from quanta.simulator.accelerated import tensor_contract
            self._state = tensor_contract(
                gate, self._state, qubits, self.num_qubits
            )
            return
        except ImportError:
            pass

        # NumPy fallback (always works)
        n = self.num_qubits
        num_gate_qubits = len(qubits)

        state_tensor = self._state.reshape([2] * n)
        gate_tensor = gate.reshape([2] * (2 * num_gate_qubits))

        gate_axes = list(range(num_gate_qubits, 2 * num_gate_qubits))
        state_axes = list(qubits)

        result = np.tensordot(gate_tensor, state_tensor, axes=(gate_axes, state_axes))
        result = np.moveaxis(result, list(range(num_gate_qubits)), list(qubits))

        self._state = result.reshape(-1)

    def _get_gate_matrix(
        self, name: str, params: tuple[float, ...]
    ) -> np.ndarray:
        """Gets gate matrix from name."""
        gate = GATE_REGISTRY.get(name)

        if gate is None:
            raise SimulatorError(f"Unknown gate: {name}")

        # Parametric gates need params to build matrix
        if isinstance(gate, MultiParametricGate):
            if not params:
                raise SimulatorError(f"{name} gate requires parameters")
            return gate(*params).matrix
        if isinstance(gate, ParametricGate):
            if not params:
                raise SimulatorError(f"{name} gate requires parameters")
            return gate(params[0]).matrix

        return gate.matrix

    def probabilities(self) -> np.ndarray:
        """Returns measurement probability of each state: P = |a|^2."""
        return np.abs(self._state) ** 2

    def sample(self, shots: int) -> dict[str, int]:
        """Performs measurement sampling based on probabilities.

        Args:
            shots: Number of samples.

        Returns:
            dict: Result -> count. E.g. {'00': 512, '11': 512}
        """
        probs = self.probabilities()
        dim = len(probs)
        n = self.num_qubits

        # Sample indices
        indices = self._rng.choice(dim, size=shots, p=probs)

        # Vectorized counting with numpy
        unique, unique_counts = np.unique(indices, return_counts=True)

        # Pre-built format string
        fmt = f"0{n}b"
        return {format(idx, fmt): int(cnt) for idx, cnt in zip(unique, unique_counts, strict=True)}

    @property
    def state(self) -> np.ndarray:
        """Copy of the current statevector."""
        return self._state.copy()

    @state.setter
    def state(self, new_state: np.ndarray) -> None:
        """Sets the statevector. Used by Layer 3 algorithms (Grover, etc.)."""
        if len(new_state) != 2 ** self.num_qubits:
            raise SimulatorError(
                f"State dimension mismatch: expected {2 ** self.num_qubits}, "
                f"got {len(new_state)}"
            )
        self._state = np.asarray(new_state, dtype=complex)

    def apply_phase(self, index: int, phase: complex) -> None:
        """Applies a phase factor to a specific basis state.

        Used by Grover oracle and other state-manipulation algorithms.

        Args:
            index: Basis state index (0 to 2^n - 1).
            phase: Phase factor to multiply (e.g., -1 for phase flip).
        """
        self._state[index] *= phase

    def apply_noise(
        self,
        noise_model: object,
        qubits: tuple[int, ...],
        rng: np.random.Generator,
    ) -> None:
        """Applies a noise model to the statevector after a gate.

        Public interface for noise integration — avoids external
        access to _state.

        Args:
            noise_model: NoiseModel with apply_noise(state, qubits, n, rng).
            qubits: Qubits the gate acted on.
            rng: Random number generator for stochastic noise.
        """
        self._state = noise_model.apply_noise(
            self._state, qubits, self.num_qubits, rng,
        )
state property writable
state: ndarray

Copy of the current statevector.

apply
apply(
    gate_name: str,
    qubits: tuple[int, ...],
    params: tuple[float, ...] = (),
) -> None

Applies a gate to the statevector via tensor contraction.

Parameters:

Name Type Description Default
gate_name str

Gate name (e.g., "H", "CX").

required
qubits tuple[int, ...]

Target qubit indices.

required
params tuple[float, ...]

Angles for parametric gates.

()
Source code in quanta/simulator/statevector.py
def apply(
    self,
    gate_name: str,
    qubits: tuple[int, ...],
    params: tuple[float, ...] = (),
) -> None:
    """Applies a gate to the statevector via tensor contraction.

    Args:
        gate_name: Gate name (e.g., "H", "CX").
        qubits: Target qubit indices.
        params: Angles for parametric gates.
    """
    matrix = self._get_gate_matrix(gate_name, params)
    self._apply_tensor(matrix, qubits)
apply_noise
apply_noise(
    noise_model: object,
    qubits: tuple[int, ...],
    rng: Generator,
) -> None

Applies a noise model to the statevector after a gate.

Public interface for noise integration — avoids external access to _state.

Parameters:

Name Type Description Default
noise_model object

NoiseModel with apply_noise(state, qubits, n, rng).

required
qubits tuple[int, ...]

Qubits the gate acted on.

required
rng Generator

Random number generator for stochastic noise.

required
Source code in quanta/simulator/statevector.py
def apply_noise(
    self,
    noise_model: object,
    qubits: tuple[int, ...],
    rng: np.random.Generator,
) -> None:
    """Applies a noise model to the statevector after a gate.

    Public interface for noise integration — avoids external
    access to _state.

    Args:
        noise_model: NoiseModel with apply_noise(state, qubits, n, rng).
        qubits: Qubits the gate acted on.
        rng: Random number generator for stochastic noise.
    """
    self._state = noise_model.apply_noise(
        self._state, qubits, self.num_qubits, rng,
    )
apply_phase
apply_phase(index: int, phase: complex) -> None

Applies a phase factor to a specific basis state.

Used by Grover oracle and other state-manipulation algorithms.

Parameters:

Name Type Description Default
index int

Basis state index (0 to 2^n - 1).

required
phase complex

Phase factor to multiply (e.g., -1 for phase flip).

required
Source code in quanta/simulator/statevector.py
def apply_phase(self, index: int, phase: complex) -> None:
    """Applies a phase factor to a specific basis state.

    Used by Grover oracle and other state-manipulation algorithms.

    Args:
        index: Basis state index (0 to 2^n - 1).
        phase: Phase factor to multiply (e.g., -1 for phase flip).
    """
    self._state[index] *= phase
probabilities
probabilities() -> np.ndarray

Returns measurement probability of each state: P = |a|^2.

Source code in quanta/simulator/statevector.py
def probabilities(self) -> np.ndarray:
    """Returns measurement probability of each state: P = |a|^2."""
    return np.abs(self._state) ** 2
sample
sample(shots: int) -> dict[str, int]

Performs measurement sampling based on probabilities.

Parameters:

Name Type Description Default
shots int

Number of samples.

required

Returns:

Name Type Description
dict dict[str, int]

Result -> count. E.g. {'00': 512, '11': 512}

Source code in quanta/simulator/statevector.py
def sample(self, shots: int) -> dict[str, int]:
    """Performs measurement sampling based on probabilities.

    Args:
        shots: Number of samples.

    Returns:
        dict: Result -> count. E.g. {'00': 512, '11': 512}
    """
    probs = self.probabilities()
    dim = len(probs)
    n = self.num_qubits

    # Sample indices
    indices = self._rng.choice(dim, size=shots, p=probs)

    # Vectorized counting with numpy
    unique, unique_counts = np.unique(indices, return_counts=True)

    # Pre-built format string
    fmt = f"0{n}b"
    return {format(idx, fmt): int(cnt) for idx, cnt in zip(unique, unique_counts, strict=True)}