Source code for graphqomb.simulator_backend

"""The base class for simulator backends.

This module provides:

- `QubitIndexManager`: Manages the mapping of external qubit indices to internal indices
- `BaseSimulatorBackend`: Abstract base class for simulator backends.
- `BaseFullStateSimulator`: Abstract base class for full state simulators.
"""

from __future__ import annotations

import abc
import itertools
import typing
from abc import ABC
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Sequence

    import numpy as np
    from numpy.typing import NDArray

    from graphqomb.common import MeasBasis


[docs] class QubitIndexManager: """Manages the mapping of external qubit indices to internal indices."""
[docs] def __init__(self, num_qubits: int) -> None: """Initialize the QubitIndexManager with a list of initial indices.""" self.__indices = list(range(num_qubits))
@property def num_qubits(self) -> int: """Get the number of qubits managed by this manager. Returns ------- `int` The number of qubits. """ return len(self.__indices)
[docs] def add_qubits(self, num_qubits: int) -> None: """Add a specified number of qubits to the index manager. Parameters ---------- num_qubits : `int` The number of qubits to add. """ current_max = max(self.__indices, default=-1) self.__indices.extend(range(current_max + 1, current_max + 1 + num_qubits))
[docs] def remove_qubit(self, qubit: int) -> None: r"""Remove specified qubit from the index manager. Parameters ---------- qubit : `int` The qubit to remove. """ self.__indices = [q if q < qubit else q - 1 for q in self.__indices if q != qubit]
[docs] def match(self, order: Sequence[int]) -> bool: r"""Check if the current indices match the given order. Parameters ---------- order : `collections.abc.Sequence`\[`int`\] A sequence of indices to compare against the current indices. Returns ------- `bool` True if the current indices match the given order, False otherwise. """ return all(lhs == rhs for lhs, rhs in itertools.zip_longest(self.__indices, order, fillvalue=None))
[docs] def reorder(self, permutation: Sequence[int]) -> None: r"""Reorder the indices based on a given permutation. if permutation is [2, 0, 1], then # [q0, q1, q2] -> [q1, q2, q0] Parameters ---------- permutation : `collections.abc.Sequence`\[`int`\] A sequence of indices that defines the new order of the indices. Raises ------ ValueError If the length of the permutation does not match the number of indices. """ if len(permutation) != len(self.__indices): msg = "Permutation length must match the number of indices." raise ValueError(msg) self.__indices = [self.__indices[i] for i in permutation]
[docs] def inverse_permutation(self) -> list[int]: r"""Get the permutation that would recover the original order of indices. Returns ------- `list`\[`int`\] A sequence of indices that maps the current order back to the original order. """ inverse_perm = [0] * len(self.__indices) for i, index in enumerate(self.__indices): inverse_perm[index] = i return inverse_perm
@typing.overload def external_to_internal(self, external_qubits: int) -> int: ... @typing.overload def external_to_internal(self, external_qubits: Sequence[int]) -> tuple[int, ...]: ...
[docs] def external_to_internal(self, external_qubits: int | Sequence[int]) -> int | tuple[int, ...]: r"""Convert external qubit indices to internal indices. Parameters ---------- external_qubits : `int` | `collections.abc.Sequence`\[`int`\] A sequence of external qubit indices. Returns ------- `int` | `tuple`\[`int`, ...\] A list of internal qubit indices corresponding to the external ones. """ if isinstance(external_qubits, int): return self.__indices[external_qubits] return tuple(self.__indices[q] for q in external_qubits)
@typing.overload def internal_to_external(self, internal_qubits: int) -> int: ... @typing.overload def internal_to_external(self, internal_qubits: Sequence[int]) -> tuple[int, ...]: ...
[docs] def internal_to_external(self, internal_qubits: int | Sequence[int]) -> int | tuple[int, ...]: r"""Convert internal qubit indices to external indices. Parameters ---------- internal_qubits : `int` | `collections.abc.Sequence`\[`int`\] A sequence of internal qubit indices. Returns ------- `int` | `tuple`\[`int`, ...\] A list of external qubit indices corresponding to the internal ones. """ inverse_perm = self.inverse_permutation() if isinstance(internal_qubits, int): return inverse_perm[internal_qubits] return tuple(inverse_perm[q] for q in internal_qubits)
# backend for all simulator backends
[docs] class BaseSimulatorBackend(ABC): """Base class for simulator backends.""" @property @abc.abstractmethod def num_qubits(self) -> int: """Get the number of qubits in the state. Returns ------- `int` The number of qubits in the state. """
[docs] @abc.abstractmethod def evolve(self, operator: NDArray[np.complex128], qubits: int | Sequence[int]) -> None: r"""Evolve the state by applying an operator to a subset of qubits. Parameters ---------- operator : `numpy.typing.NDArray`\[`numpy.complex128`\] The operator to apply. qubits : `int` | `collections.abc.Sequence`\[`int`\] The qubits to apply the operator to. """
[docs] @abc.abstractmethod def measure(self, qubit: int, meas_basis: MeasBasis, result: int) -> None: """Measure a qubit in a given measurement basis. Parameters ---------- qubit : `int` The qubit to measure. meas_basis : `MeasBasis` The measurement basis to use. result : `int` The measurement result. """
[docs] class BaseFullStateSimulator(BaseSimulatorBackend): """Base class for full state simulators."""
[docs] @abc.abstractmethod def state(self) -> NDArray[np.complex128]: r"""Get the current state vector. Returns ------- `numpy.typing.NDArray`\[`numpy.complex128`\] The current state vector. """
[docs] @abc.abstractmethod def norm(self) -> float: r"""Get the current state vector norm. Returns ------- `float` The current state vector norm. """
[docs] @abc.abstractmethod def add_node(self, num_qubits: int) -> None: r"""Add a node to the state. Parameters ---------- num_qubits : `int` The number of qubits in the new node. """
[docs] @abc.abstractmethod def entangle(self, qubit1: int, qubit2: int) -> None: r"""Entangle two qubits in the state. Parameters ---------- qubit1 : `int` The first qubit to entangle. qubit2 : `int` The second qubit to entangle. """
[docs] @abc.abstractmethod def reorder(self, permutation: list[int]) -> None: r"""Reorder the qubits in the state. Parameters ---------- permutation : `list`\[`int`\] The permutation to apply. """
[docs] @abc.abstractmethod def expectation(self, operator: NDArray[np.complex128], qubits: int | Sequence[int]) -> float: r"""Calculate the expectation value of an operator. Parameters ---------- operator : `numpy.typing.NDArray`\[`numpy.complex128`\] The operator to calculate the expectation value for. qubits : `int` | `collections.abc.Sequence`\[`int`\] The qubits to apply the operator to. Returns ------- `float` The expectation value of the operator. """