From 45f18a0165a4c3ec68fe981fd28bc317002a5494 Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Mon, 24 Nov 2025 13:42:17 +0100 Subject: [PATCH] LLM docstring proofreading --- graphix/__init__.py | 8 +- graphix/_db.py | 53 +- graphix/_linalg.py | 189 +++-- graphix/branch_selector.py | 133 ++- graphix/channels.py | 234 +++++- graphix/clifford.py | 187 ++++- graphix/command.py | 107 ++- graphix/extraction.py | 113 ++- graphix/find_pflow.py | 261 ++++-- graphix/fundamentals.py | 463 ++++++++++- graphix/generator.py | 198 ++++- graphix/gflow.py | 950 --------------------- graphix/graphsim.py | 348 +++++--- graphix/instruction.py | 451 +++++++++- graphix/linalg_validations.py | 121 ++- graphix/measurements.py | 148 +++- graphix/noise_models/__init__.py | 8 +- graphix/noise_models/depolarising.py | 210 ++++- graphix/noise_models/noise_model.py | 309 ++++++- graphix/opengraph.py | 149 +++- graphix/ops.py | 93 ++- graphix/optimization.py | 127 ++- graphix/parameter.py | 834 ++++++++++++++++--- graphix/pattern.py | 1143 +++++++++++++++++++------- graphix/pauli.py | 217 ++++- graphix/pretty_print.py | 96 ++- graphix/pyzx.py | 52 +- graphix/qasm3_exporter.py | 108 ++- graphix/random_objects.py | 214 +++-- graphix/repr_mixins.py | 42 +- graphix/rng.py | 36 +- graphix/sim/__init__.py | 6 +- graphix/sim/base_backend.py | 612 ++++++++++---- graphix/sim/data.py | 13 +- graphix/sim/density_matrix.py | 377 +++++++-- graphix/sim/statevec.py | 334 ++++++-- graphix/sim/tensornet.py | 479 +++++++---- graphix/simulator.py | 204 +++-- graphix/states.py | 125 ++- graphix/transpiler.py | 514 ++++++++---- graphix/utils.py | 261 +++++- graphix/visualization.py | 399 +++++---- tests/test_branch_selector.py | 48 +- tests/test_clifford.py | 7 +- tests/test_density_matrix.py | 117 ++- tests/test_find_pflow.py | 191 ++++- tests/test_generator.py | 12 +- tests/test_graphsim.py | 35 +- tests/test_kraus.py | 62 +- tests/test_linalg.py | 19 +- tests/test_noisy_density_matrix.py | 27 +- tests/test_pattern.py | 39 +- tests/test_pyzx.py | 20 +- tests/test_qasm3_exporter.py | 8 +- tests/test_statevec.py | 16 +- tests/test_statevec_backend.py | 38 +- 56 files changed, 8504 insertions(+), 3061 deletions(-) diff --git a/graphix/__init__.py b/graphix/__init__.py index 782a7ef7b..2d1ea4a62 100644 --- a/graphix/__init__.py +++ b/graphix/__init__.py @@ -1,4 +1,10 @@ -"""Optimize and simulate measurement-based quantum computation.""" +""" +Optimize and simulate measurement-based quantum computation. + +This module provides tools and functionalities for optimizing and simulating measurement-based quantum computation (MBQC). + +Measurement-based quantum computation is a model of quantum computation that uses entangled states and measurements to perform computations. This module implements various algorithms and techniques for effectively optimizing and simulating these quantum processes. +""" from __future__ import annotations diff --git a/graphix/_db.py b/graphix/_db.py index bb7157137..f0f143115 100644 --- a/graphix/_db.py +++ b/graphix/_db.py @@ -1,4 +1,10 @@ -"""Database module for Graphix.""" +""" +Database module for Graphix. + +This module provides functionality to interact with the Graphix database, +enabling operations such as data retrieval, insertion, updating, and deletion +of records within the Graphix application. +""" from __future__ import annotations @@ -126,14 +132,55 @@ class _CM(NamedTuple): - """Pauli string and sign.""" + """ + Pauli string and sign. + + Attributes + ---------- + pauli_string : str + A string representing the Pauli operation, such as 'X', 'Y', 'Z', or 'I'. + sign : int + The sign associated with the Pauli string, typically +1 or -1. + + Methods + ------- + __init__(pauli_string: str, sign: int) + Initializes the Pauli string and its sign. + + __repr__() + Returns a string representation of the Pauli string with its sign. + + __eq__(other) + Checks if two _CM instances are equal based on their Pauli strings and signs. + """ pstr: IXYZ sign: Sign class _CMTuple(NamedTuple): - """Container for three Pauli strings along X, Y and Z axes.""" + """ + Container for three Pauli strings along the X, Y, and Z axes. + + Attributes + ---------- + x : str + The Pauli string along the X axis. + y : str + The Pauli string along the Y axis. + z : str + The Pauli string along the Z axis. + + Examples + -------- + >>> pauli_tuple = _CMTuple('X', 'Y', 'Z') + >>> pauli_tuple.x + 'X' + >>> pauli_tuple.y + 'Y' + >>> pauli_tuple.z + 'Z' + """ x: _CM y: _CM diff --git a/graphix/_linalg.py b/graphix/_linalg.py index 3667b597c..c6851e212 100644 --- a/graphix/_linalg.py +++ b/graphix/_linalg.py @@ -1,4 +1,11 @@ -r"""Performant module for linear algebra on :math:`\mathbb F_2` field.""" +""" +Performant module for linear algebra over the field :math:`\mathbb{F}_2`. + +This module provides efficient implementations for various linear algebra operations +over the binary field, including matrix multiplication, vector operations, and solving +linear equations. It is optimized for performance and memory usage, making it suitable +for large-scale computations in applications such as coding theory and cryptography. +""" from __future__ import annotations @@ -13,43 +20,50 @@ class MatGF2(npt.NDArray[np.uint8]): - r"""Custom implementation of :math:`\mathbb F_2` matrices. This class specializes `:class:np.ndarray` to the :math:`\mathbb F_2` field with increased efficiency.""" + """ + Custom implementation of :math:`\mathbb{F}_2` matrices. + + This class specializes :class:`np.ndarray` to the :math:`\mathbb{F}_2` field + with increased efficiency. + """ def __new__(cls, data: npt.ArrayLike, copy: bool = True) -> Self: - """Instantiate new `MatGF2` object. + """ + Instantiate a new `MatGF2` object. Parameters ---------- - data : array - Data in array - copy : bool - Optional, defaults to `True`. If `False` and if possible, data - is not copied. + data : npt.ArrayLike + The data to be stored in the `MatGF2` object. + copy : bool, optional + If `True` (default), the data will be copied. If `False`, the data will + not be copied if it is possible to do so. - Return + Returns ------- - MatGF2 + MatGF2 + A new instance of the `MatGF2` class. """ arr = np.array(data, dtype=np.uint8, copy=copy) return super().__new__(cls, shape=arr.shape, dtype=arr.dtype, buffer=arr) def mat_mul(self, other: MatGF2 | npt.NDArray[np.uint8]) -> MatGF2: - r"""Multiply two matrices. + r""" + Multiply two matrices over the finite field \(\mathbb{F}_2\). Parameters ---------- - other : array - Matrix that right-multiplies `self`. + other : MatGF2 | npt.NDArray[np.uint8] + The matrix that right-multiplies `self`. Returns ------- MatGF2 - Matrix product `self` @ `other` in :math:`\mathbb F_2`. + The matrix product of `self` and `other`, computed as `self` @ `other` in \(\mathbb{F}_2\). Notes ----- - This function is a wrapper over :func:`_mat_mul_jit` which is a just-time compiled implementation of the matrix multiplication in :math:`\mathbb F_2`. It is more efficient than `galois.GF2.__matmul__` when the matrix `self` is sparse. - The implementation assumes that the arguments have the right dimensions. + This function is a wrapper for :func:`_mat_mul_jit`, which is a just-in-time compiled implementation of matrix multiplication in \(\mathbb{F}_2\). It is more efficient than `galois.GF2.__matmul__` when the matrix `self` is sparse. The implementation assumes that the arguments have the correct dimensions. """ if self.ndim != 2 or other.ndim != 2: raise ValueError( @@ -63,31 +77,36 @@ def mat_mul(self, other: MatGF2 | npt.NDArray[np.uint8]) -> MatGF2: return MatGF2(_mat_mul_jit(self, other), copy=False) def compute_rank(self) -> np.intp: - """Get the rank of the matrix. + """ + Get the rank of the matrix. Returns ------- - int : int + int Rank of the matrix. """ mat_a = self.row_reduction(copy=True) return np.count_nonzero(mat_a.any(axis=1)) def right_inverse(self) -> MatGF2 | None: - r"""Return any right inverse of the matrix. + """ + Return any right inverse of the matrix. Returns ------- - rinv : MatGF2 - Any right inverse of the matrix. - or `None` - If the matrix does not have a right inverse. + rinv : MatGF2 | None + Any right inverse of the matrix, or `None` if the matrix + does not have a right inverse. Notes ----- - Let us consider a matrix :math:`A` of size :math:`(m \times n)`. The right inverse is a matrix :math:`B` of size :math:`(n \times m)` s.t. :math:`AB = I` where :math:`I` is the identity matrix. - - The right inverse only exists if :math:`rank(A) = m`. Therefore, it is necessary but not sufficient that :math:`m ≤ n`. - - The right inverse is unique only if :math:`m=n`. + Let us consider a matrix :math:`A` of size :math:`(m \times n)`. + The right inverse is a matrix :math:`B` of size :math:`(n \times m)` + such that :math:`AB = I`, where :math:`I` is the identity matrix. + + - The right inverse only exists if :math:`\text{rank}(A) = m`. + Therefore, it is necessary but not sufficient that :math:`m \leq n`. + - The right inverse is unique only if :math:`m = n`. """ m, n = self.shape if m > n: @@ -110,16 +129,17 @@ def right_inverse(self) -> MatGF2 | None: return rinv def null_space(self) -> MatGF2: - r"""Return the null space of the matrix. + """ + Return the null space of the matrix. Returns ------- MatGF2 - The rows of the basis matrix are the basis vectors that span the null space. The number of rows of the basis matrix is the dimension of the null space. + A matrix whose rows are the basis vectors that span the null space. The number of rows in this basis matrix corresponds to the dimension of the null space. Notes ----- - This implementation appear to be more efficient than `:func:galois.GF2.null_space`. + This implementation appears to be more efficient than `:func:galois.GF2.null_space`. """ m, n = self.shape @@ -131,15 +151,16 @@ def null_space(self) -> MatGF2: return ref[row_idxs, m:].view(MatGF2) def gauss_elimination(self, ncols: int | None = None, copy: bool = True) -> MatGF2: - """Return row echelon form (REF) by performing Gaussian elimination. + """ + Return row echelon form (REF) by performing Gaussian elimination. Parameters ---------- - n_cols : int (optional) - Number of columns over which to perform Gaussian elimination. The default is `None` which represents the number of columns of the matrix. + ncols : int, optional + Number of columns over which to perform Gaussian elimination. The default is `None`, which represents the number of columns of the matrix. - copy : bool (optional) - If `True`, the REF matrix is copied into a new instance, otherwise `self` is modified. Defaults to `True`. + copy : bool, optional + If `True`, a new instance containing the REF matrix is created; otherwise, `self` is modified. Defaults to `True`. Returns ------- @@ -152,19 +173,20 @@ def gauss_elimination(self, ncols: int | None = None, copy: bool = True) -> MatG return MatGF2(_elimination_jit(mat_ref, ncols=ncols_value, full_reduce=False), copy=False) def row_reduction(self, ncols: int | None = None, copy: bool = True) -> MatGF2: - """Return row-reduced echelon form (RREF) by performing Gaussian elimination. + """ + Return the row-reduced echelon form (RREF) by performing Gaussian elimination. Parameters ---------- - n_cols : int (optional) - Number of columns over which to perform Gaussian elimination. The default is `None` which represents the number of columns of the matrix. + ncols : int, optional + Number of columns over which to perform Gaussian elimination. The default is `None`, which represents the number of columns of the matrix. - copy : bool (optional) - If `True`, the RREF matrix is copied into a new instance, otherwise `self` is modified. Defaults to `True`. + copy : bool, optional + If `True`, the RREF matrix is copied into a new instance; otherwise, `self` is modified. Defaults to `True`. Returns ------- - mat_ref: MatGF2 + MatGF2 The matrix in row-reduced echelon form. """ ncols_value = self.shape[1] if ncols is None else ncols @@ -174,7 +196,8 @@ def row_reduction(self, ncols: int | None = None, copy: bool = True) -> MatGF2: def solve_f2_linear_system(mat: MatGF2, b: MatGF2) -> MatGF2: - r"""Solve the linear system (LS) `mat @ x == b`. + """ + Solve the linear system (LS) `mat @ x == b`. Parameters ---------- @@ -190,7 +213,7 @@ def solve_f2_linear_system(mat: MatGF2, b: MatGF2) -> MatGF2: Notes ----- - This function is not integrated in `:class: graphix.linalg.MatGF2` because it does not perform any checks on the form of `mat` to ensure that it is in REF or that the system is solvable. + This function is not integrated into `graphix.linalg.MatGF2` because it does not perform any checks on the form of `mat` to ensure that it is in REF or that the system is solvable. """ return MatGF2(_solve_f2_linear_system_jit(mat, b), copy=False) @@ -199,7 +222,43 @@ def solve_f2_linear_system(mat: MatGF2, b: MatGF2) -> MatGF2: def _solve_f2_linear_system_jit( mat_data: npt.NDArray[np.uint8], b_data: npt.NDArray[np.uint8] ) -> npt.NDArray[np.uint8]: - """See docstring of `:func:solve_f2_linear_system` for details.""" + """ + Solve a linear system over the finite field GF(2). + + This function utilizes Just-In-Time (JIT) compilation to efficiently solve + the linear system represented in binary (GF(2)) format. It takes a matrix + and a vector as inputs, performing the necessary operations to find a solution + to the system Ax = b, where A is the input matrix and b is the input vector. + + Parameters + ---------- + mat_data : ndarray, shape (m, n), dtype(uint8) + The coefficient matrix of the linear system, where each entry is a + binary element (0 or 1). + + b_data : ndarray, shape (m,), dtype(uint8) + The right-hand side vector of the linear system, also containing binary + elements (0 or 1). + + Returns + ------- + ndarray, shape (n,), dtype(uint8) + The solution vector of the linear system, where each entry is a + binary element representing the solution to the system. If the system + is inconsistent, an appropriate solution representation should be + returned, such as a vector of zeros. + + Notes + ----- + For more detailed information about the algorithm and its application, + see the docstring of the `solve_f2_linear_system` function. + + Examples + -------- + >>> A = np.array([[1, 0, 1], [1, 1, 0]], dtype=np.uint8) + >>> b = np.array([1, 0], dtype=np.uint8) + >>> solution = _solve_f2_linear_system_jit(A, b) + """ m, n = mat_data.shape x = np.zeros(n, dtype=np.uint8) @@ -236,33 +295,34 @@ def _solve_f2_linear_system_jit( @nb.njit("uint8[:,::1](uint8[:,::1], uint64, boolean)") def _elimination_jit(mat_data: npt.NDArray[np.uint8], ncols: int, full_reduce: bool) -> npt.NDArray[np.uint8]: - r"""Return row echelon form (REF) or row-reduced echelon form (RREF) by performing Gaussian elimination. + """ + Return row echelon form (REF) or row-reduced echelon form (RREF) by performing Gaussian elimination. Parameters ---------- mat_data : npt.NDArray[np.uint8] - Matrix to be gaussian-eliminated. - n_cols : int + Matrix to be Gaussian-eliminated. + ncols : int Number of columns over which to perform Gaussian elimination. full_reduce : bool Flag determining the operation mode. Output is in RREF if `True`, REF otherwise. Returns ------- - mat_data: npt.NDArray[np.uint8] + npt.NDArray[np.uint8] The matrix in row(-reduced) echelon form. Notes ----- - Adapted from `:func: galois.FieldArray.row_reduction`, which renders the matrix in row-reduced echelon form (RREF) and specialized for :math:`\mathbb F_2`. + Adapted from `galois.FieldArray.row_reduction`, which renders the matrix in row-reduced echelon form (RREF) and is specialized for :math:`\mathbb{F}_2`. - Row echelon form (REF): + Row echelon form (REF) characteristics: 1. All rows having only zero entries are at the bottom. 2. The leading entry of every nonzero row is on the right of the leading entry of every row above. - 3. (1) and (2) imply that all entries in a column below a leading coefficient are zeros. - 4. It's the result of Gaussian elimination. + 3. Points (1) and (2) imply that all entries in a column below a leading coefficient are zeros. + 4. It represents the result of Gaussian elimination. - For matrices over :math:`\mathbb F_2` the only difference between REF and RREF is that elements above a leading 1 can be non-zero in REF but must be 0 in RREF. + For matrices over :math:`\mathbb{F}_2`, the only difference between REF and RREF is that elements above a leading 1 can be non-zero in REF but must be 0 in RREF. """ m, n = mat_data.shape p = 0 # Pivot @@ -302,7 +362,30 @@ def _elimination_jit(mat_data: npt.NDArray[np.uint8], ncols: int, full_reduce: b @nb.njit("uint8[:,::1](uint8[:,::1], uint8[:,::1])", parallel=True) def _mat_mul_jit(m1: npt.NDArray[np.uint8], m2: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]: - """See docstring of `:func:MatGF2.__matmul__` for details.""" + """ + Perform matrix multiplication of two uint8 NumPy arrays using just-in-time compilation. + + Parameters + ---------- + m1 : numpy.ndarray + A 2-dimensional array of shape (m, k), where m and k are positive integers. + The array must contain uint8 values. + + m2 : numpy.ndarray + A 2-dimensional array of shape (k, n), where n is a positive integer. + The array must contain uint8 values. + + Returns + ------- + numpy.ndarray + A 2-dimensional array of shape (m, n) containing the result of the matrix multiplication, + with data type uint8. + + Notes + ----- + This function utilizes Numba's JIT compilation for improved performance. + For more details, see the docstring of `MatGF2.__matmul__`. + """ m, l = m1.shape _, n = m2.shape diff --git a/graphix/branch_selector.py b/graphix/branch_selector.py index 8e7e54f9f..e134d187f 100644 --- a/graphix/branch_selector.py +++ b/graphix/branch_selector.py @@ -1,10 +1,11 @@ -"""Branch selector. - -Branch selectors determine the computation branch that is explored -during a simulation, meaning the choice of measurement outcomes. The -branch selection can be random (see :class:`RandomBranchSelector`) or -deterministic (see :class:`ConstBranchSelector`). +""" +Branch Selector Module. +This module contains branch selectors that determine the computation +branch explored during a simulation, specifically influencing the +choice of measurement outcomes. Branch selection can either be +random (see :class:`RandomBranchSelector`) or deterministic +(see :class:`ConstBranchSelector`). """ from __future__ import annotations @@ -26,7 +27,8 @@ class BranchSelector(ABC): - """Abstract class for branch selectors. + """ + Abstract class for branch selectors. A branch selector provides the method `measure`, which returns the measurement outcome (0 or 1) for a given qubit. @@ -34,31 +36,30 @@ class BranchSelector(ABC): @abstractmethod def measure(self, qubit: int, f_expectation0: Callable[[], float], rng: Generator | None = None) -> Outcome: - """Return the measurement outcome of ``qubit``. + """ + Return the measurement outcome of a specified qubit. Parameters ---------- qubit : int - Index of qubit to measure + Index of the qubit to measure. f_expectation0 : Callable[[], float] - A function that the method can use to retrieve the expected - probability of outcome 0. The probability is computed only if - this function is called (lazy computation), ensuring no - unnecessary computational cost. - - rng: Generator, optional - Random-number generator for measurements. - This generator is used only in case of random branch selection - (see :class:`RandomBranchSelector`). - If ``None``, a default random-number generator is used. - Default is ``None``. + A callable that retrieves the expected probability of outcome 0. + The probability is computed only if this function is called, + enabling lazy computation and preventing unnecessary computational cost. + + rng : Generator, optional + Random-number generator for measurements. This generator is used + only in cases of random branch selection (see :class:`RandomBranchSelector`). + If `None`, a default random-number generator is used. The default is `None`. """ @dataclass class RandomBranchSelector(BranchSelector): - """Random branch selector. + """ + Random branch selector. Parameters ---------- @@ -73,11 +74,30 @@ class RandomBranchSelector(BranchSelector): @override def measure(self, qubit: int, f_expectation0: Callable[[], float], rng: Generator | None = None) -> Outcome: """ - Return the measurement outcome of ``qubit``. + Measure the outcome of a specified qubit. - If ``pr_calc`` is ``True``, the measurement outcome is determined based on the - computed probability of outcome 0. Otherwise, the result is randomly chosen - with a 50% chance for either outcome. + Parameters + ---------- + qubit : int + The index of the qubit to measure. + f_expectation0 : Callable[[], float] + A callable that computes the expectation value for outcome 0. + rng : Generator | None, optional + An optional random number generator for generating random outcomes. + If None, the default generator will be used. + + Returns + ------- + Outcome + The measurement outcome of the specified qubit. The result is determined + based on the computed probability of outcome 0 if `pr_calc` is True. + Otherwise, the result is randomly chosen with a 50% chance for either + outcome. + + Notes + ----- + If `pr_calc` is False, the measurement outcome is decided randomly, while + if True, it relies on the computation from `f_expectation0`. """ rng = ensure_rng(rng) if self.pr_calc: @@ -92,22 +112,23 @@ def measure(self, qubit: int, f_expectation0: Callable[[], float], rng: Generato @dataclass class FixedBranchSelector(BranchSelector, Generic[_T]): - """Branch selector with predefined measurement outcomes. + """ + Branch selector with predefined measurement outcomes. - The mapping is fixed in ``results``. By default, an error is raised if + The mapping is fixed in `results`. By default, an error is raised if a qubit is measured without a predefined outcome. However, another - branch selector can be specified in ``default`` to handle such cases. + branch selector can be specified in `default` to handle such cases. Parameters ---------- results : Mapping[int, bool] - A dictionary mapping qubits to their measurement outcomes. - If a qubit is not present in this mapping, the ``default`` branch + A dictionary mapping qubit indices to their measurement outcomes. + If a qubit is not present in this mapping, the `default` branch selector is used. default : BranchSelector | None, optional - Branch selector to use for qubits not present in ``results``. - If ``None``, an error is raised when an unmapped qubit is measured. - Default is ``None``. + Branch selector to use for qubits not present in `results`. + If `None`, an error is raised when an unmapped qubit is measured. + Default is `None`. """ results: _T @@ -116,10 +137,27 @@ class FixedBranchSelector(BranchSelector, Generic[_T]): @override def measure(self, qubit: int, f_expectation0: Callable[[], float], rng: Generator | None = None) -> Outcome: """ - Return the predefined measurement outcome of ``qubit``, if available. + Measure the predefined outcome of a specified qubit. - If the qubit is not present in ``results``, the ``default`` branch selector - is used. If no default is provided, an error is raised. + Parameters + ---------- + qubit : int + The index of the qubit to be measured. + f_expectation0 : Callable[[], float] + A callable that returns the expectation value of the measurement for the qubit. + rng : Generator | None, optional + An optional random number generator to use for stochastic processes. If None, the default generator is used. + + Returns + ------- + Outcome + The measurement outcome of the specified qubit. If the qubit is not available in the results, + the default branch selector is used. If no default is provided, an error is raised. + + Raises + ------ + ValueError + If the qubit is not present and no default branch selector is available. """ result = self.results.get(qubit) if result is None: @@ -131,9 +169,10 @@ def measure(self, qubit: int, f_expectation0: Callable[[], float], rng: Generato @dataclass class ConstBranchSelector(BranchSelector): - """Branch selector with a constant measurement outcome. + """ + Branch selector with a constant measurement outcome. - The value ``result`` is returned for every qubit. + The value `result` is returned for every qubit. Parameters ---------- @@ -145,5 +184,21 @@ class ConstBranchSelector(BranchSelector): @override def measure(self, qubit: int, f_expectation0: Callable[[], float], rng: Generator | None = None) -> Outcome: - """Return the constant measurement outcome ``result`` for any qubit.""" + """ + Return the constant measurement outcome ``result`` for any qubit. + + Parameters + ---------- + qubit : int + The index of the qubit to measure. + f_expectation0 : Callable[[], float] + A callable that returns the expected value for the measurement. + rng : Generator, optional + A random number generator for stochastic processes. If None, a default generator will be used. + + Returns + ------- + Outcome + The constant measurement outcome corresponding to the specified qubit. + """ return self.result diff --git a/graphix/channels.py b/graphix/channels.py index 8bb0ebc1b..7ebbf9ae4 100644 --- a/graphix/channels.py +++ b/graphix/channels.py @@ -1,4 +1,17 @@ -"""Quantum channels and noise models.""" +""" +Quantum channels and noise models. + +This module provides implementations and utilities for quantum channels +and various noise models used in quantum computing. It allows users to +simulate the effects of noise on quantum states and operations, enabling +the analysis and design of robust quantum algorithms and protocols. + +Key functionalities include: +- Definition and representation of different quantum channels. +- Simulation of noise effects on quantum operations. +- Tools for analyzing the performance of quantum algorithms in the + presence of noise. +""" from __future__ import annotations @@ -19,17 +32,19 @@ def _ilog2(n: int) -> int: - """Return the integer base-2 logarithm of ``n``. + """ + Return the integer base-2 logarithm of `n`. Parameters ---------- n : int - Positive integer. + A positive integer. Returns ------- int - ``floor(log2(n))`` for ``n > 0``. + The integer part of the base-2 logarithm of `n`, specifically + `floor(log2(n))` for `n > 0`. """ if n <= 0: raise ValueError("n must be positive.") @@ -37,7 +52,8 @@ def _ilog2(n: int) -> int: class KrausData: - """Kraus operator data. + """ + Kraus operator data. Attributes ---------- @@ -62,27 +78,59 @@ def __init__(self, coef: complex, operator: npt.NDArray[_T]) -> None: @property def coef(self) -> complex: - """Return the scalar prefactor.""" + """ + Return the scalar prefactor. + + Returns + ------- + complex + The scalar prefactor associated with the Kraus operator. + """ return self.__coef @property def operator(self) -> npt.NDArray[np.complex128]: - """Return the operator.""" + """ + Return the Kraus operator. + + Returns + ------- + npt.NDArray[np.complex128] + The Kraus operator associated with this instance of `KrausData`. + """ return self.__operator.view() @property def nqubit(self) -> int: - """Validate the data.""" + """ + Get the number of qubits in the Kraus data. + + This property calculates and returns the number of qubits + based on the shape of the associated Kraus operators. + + Returns + ------- + int + The number of qubits represented by the Kraus operators. + + Raises + ------ + ValueError + If the dimensions of the Kraus operators are inconsistent + with a valid qubit representation. + """ size, _ = self.__operator.shape return _ilog2(size) class KrausChannel: - r"""Quantum channel class in the Kraus representation. + """ + Quantum channel class in the Kraus representation. - Defined by Kraus operators :math:`K_i` with scalar prefactors :code:`coef`) :math:`c_i`, - where the channel act on density matrix as :math:`\rho' = \sum_i K_i^\dagger \rho K_i`. - The data should satisfy :math:`\sum K_i^\dagger K_i = I`. + Defined by Kraus operators :math:`K_i` with scalar prefactors + :code:`coef` :math:`c_i`, where the channel acts on the density matrix + as :math:`\rho' = \sum_i K_i^\dagger \rho K_i`. The data should satisfy + :math:`\sum_i K_i^\dagger K_i = I`. """ __nqubit: int @@ -90,7 +138,19 @@ class KrausChannel: @staticmethod def _nqubit(kraus_data: Iterable[KrausData]) -> int: - """Return the number of qubits acted on by ``kraus_data``.""" + """ + Return the number of qubits acted on by the given Kraus operators. + + Parameters + ---------- + kraus_data : Iterable[KrausData] + An iterable of KrausData objects representing the Kraus operators. + + Returns + ------- + int + The number of qubits that the provided Kraus operators act on. + """ # MEMO: ``kraus_data`` is not empty. it = iter(kraus_data) nqubit = next(it).nqubit @@ -101,7 +161,8 @@ def _nqubit(kraus_data: Iterable[KrausData]) -> int: return nqubit def __init__(self, kraus_data: Iterable[KrausData]) -> None: - """Initialize `KrausChannel` given a Kraus operator. + """ + Initialize a KrausChannel given a Kraus operator. Parameters ---------- @@ -140,35 +201,81 @@ def __getitem__(self, index: SupportsIndex, /) -> KrausData: ... def __getitem__(self, index: slice, /) -> list[KrausData]: ... def __getitem__(self, index: SupportsIndex | slice, /) -> KrausData | list[KrausData]: - """Return the Kraus operator at the given index.""" + """ + Return the Kraus operator(s) at the specified index or indices. + + Parameters + ---------- + index : SupportsIndex | slice + The index or slice to access the Kraus operator(s). + This can be a single index to retrieve one Kraus operator, + or a slice to retrieve multiple operators. + + Returns + ------- + KrausData | list[KrausData] + The Kraus operator at the given index if a single index is provided, + or a list of Kraus operators if a slice is provided. + + Notes + ----- + - Ensure that the index is within the valid range of available Kraus operators. + """ return self.__data[index] def __len__(self) -> int: - """Return the number of Kraus operators.""" + """ + Return the number of Kraus operators. + + Returns + ------- + int + The number of Kraus operators associated with the channel. + """ return len(self.__data) def __iter__(self) -> Iterator[KrausData]: - """Iterate over Kraus operators.""" + """ + Iterate over the Kraus operators. + + Yields + ------ + KrausData + Each Kraus operator in the channel. + """ return iter(self.__data) @property def nqubit(self) -> int: - """Return the number of qubits.""" + """ + Get the number of qubits in the Kraus channel. + + Returns + ------- + int + The number of qubits. + """ return self.__nqubit def dephasing_channel(prob: float) -> KrausChannel: - r"""Single-qubit dephasing channel, :math:`(1-p) \rho + p Z \rho Z`. + """ + Single-qubit dephasing channel. + + The dephasing channel is defined by the equation: + :math:`(1-p) \rho + p Z \rho Z`, where :math:`Z` is the Pauli-Z operator. Parameters ---------- prob : float - The probability associated to the channel + The probability associated with the dephasing channel, + where :math:`p` is the probability of applying the Z operation. Returns ------- - :class:`graphix.channels.KrausChannel` object - containing the corresponding Kraus operators + :class:`graphix.channels.KrausChannel` + An object containing the corresponding Kraus operators for + the dephasing channel. """ return KrausChannel( [ @@ -179,16 +286,30 @@ def dephasing_channel(prob: float) -> KrausChannel: def depolarising_channel(prob: float) -> KrausChannel: - r"""Single-qubit depolarizing channel. + """ + Single-qubit depolarizing channel. + + The depolarizing channel is defined mathematically as: .. math:: - (1-p) \rho + \frac{p}{3} (X \rho X + Y \rho Y + Z \rho Z) = (1 - 4 \frac{p}{3}) \rho + 4 \frac{p}{3} id + (1-p) \rho + \frac{p}{3} (X \rho X + Y \rho Y + Z \rho Z) = (1 - 4 \frac{p}{3}) \rho + 4 \frac{p}{3} \text{id} + + where \( p \) is the probability of depolarization and \( \rho \) is the density matrix of the qubit. Parameters ---------- prob : float - The probability associated to the channel + The probability associated with the channel (0 ≤ prob ≤ 1). + + Returns + ------- + KrausChannel + A Kraus channel representing the depolarizing channel with the given probability. + Raises + ------ + ValueError + If `prob` is not in the range [0, 1]. """ return KrausChannel( [ @@ -201,11 +322,43 @@ def depolarising_channel(prob: float) -> KrausChannel: def pauli_channel(px: float, py: float, pz: float) -> KrausChannel: - r"""Single-qubit Pauli channel. + """ + Single-qubit Pauli channel. + + This function represents a single-qubit Pauli channel that models the + effects of noise on a quantum state. The channel consists of a mixture + of the identity operator and the Pauli operators (X, Y, Z) applied to + the quantum state with certain probabilities. + + The mathematical representation of the Pauli channel is given by: .. math:: - (1-p_X-p_Y-p_Z) \rho + p_X X \rho X + p_Y Y \rho Y + p_Z Z \rho Z) + (1-p_X-p_Y-p_Z) \rho + p_X X \rho X + p_Y Y \rho Y + p_Z Z \rho Z + Parameters + ---------- + px : float + Probability of applying the Pauli-X operator. + py : float + Probability of applying the Pauli-Y operator. + pz : float + Probability of applying the Pauli-Z operator. + + Returns + ------- + KrausChannel + The Kraus representation of the Pauli channel. + + Raises + ------ + ValueError + If the sum of probabilities exceeds 1. + + Notes + ----- + Ensure that the inputs satisfy the condition: + `px + py + pz <= 1`. This guarantees that the probabilities + are valid within the context of a quantum channel. """ if px + py + pz > 1: raise ValueError("The sum of probabilities must not exceed 1.") @@ -221,20 +374,24 @@ def pauli_channel(px: float, py: float, pz: float) -> KrausChannel: def two_qubit_depolarising_channel(prob: float) -> KrausChannel: - r"""Two-qubit depolarising channel. + """ + Two-qubit depolarising channel. + + The depolarising channel introduces errors to a two-qubit quantum system based on a given + probability. The channel can be mathematically represented as: .. math:: - \mathcal{E} (\rho) = (1-p) \rho + \frac{p}{15} \sum_{P_i \in \{id, X, Y ,Z\}^{\otimes 2}/(id \otimes id)}P_i \rho P_i + \mathcal{E} (\rho) = (1-p) \rho + \frac{p}{15} \sum_{P_i \in \{id, X, Y ,Z\}^{\otimes 2}/(id \otimes id)} P_i \rho P_i Parameters ---------- prob : float - The probability associated to the channel + The probability of depolarisation. Must be in the range [0, 1]. Returns ------- - :class:`graphix.channels.KrausChannel` object - containing the corresponding Kraus operators + :class:`graphix.channels.KrausChannel` + An object containing the corresponding Kraus operators for the channel. """ return KrausChannel( [ @@ -259,22 +416,23 @@ def two_qubit_depolarising_channel(prob: float) -> KrausChannel: def two_qubit_depolarising_tensor_channel(prob: float) -> KrausChannel: - r"""Two-qubit tensor channel of single-qubit depolarising channels with same probability. + """ + Two-qubit tensor channel of single-qubit depolarising channels with the same probability. - Kraus operators: + The Kraus operators for this channel are defined as follows: .. math:: - \Big\{ \sqrt{(1-p)} id, \sqrt{(p/3)} X, \sqrt{(p/3)} Y , \sqrt{(p/3)} Z \Big\} \otimes \Big\{ \sqrt{(1-p)} id, \sqrt{(p/3)} X, \sqrt{(p/3)} Y , \sqrt{(p/3)} Z \Big\} + \Big\{ \sqrt{(1-p)} \, \text{id}, \sqrt{\frac{p}{3}} \, X, \sqrt{\frac{p}{3}} \, Y, \sqrt{\frac{p}{3}} \, Z \Big\} \otimes \Big\{ \sqrt{(1-p)} \, \text{id}, \sqrt{\frac{p}{3}} \, X, \sqrt{\frac{p}{3}} \, Y, \sqrt{\frac{p}{3}} \, Z \Big\} Parameters ---------- prob : float - The probability associated to the channel + The probability associated with the depolarising channel, where `0 <= prob <= 1`. Returns ------- - :class:`graphix.channels.KrausChannel` object - containing the corresponding Kraus operators + KrausChannel + An instance of :class:`graphix.channels.KrausChannel` containing the corresponding Kraus operators. """ return KrausChannel( [ diff --git a/graphix/clifford.py b/graphix/clifford.py index 713e5e7a9..6eb1f0d42 100644 --- a/graphix/clifford.py +++ b/graphix/clifford.py @@ -1,4 +1,13 @@ -"""24 Unique single-qubit Clifford gates and their multiplications, conjugations and Pauli conjugations.""" +""" +24 Unique single-qubit Clifford gates and their multiplications, conjugations, +and Pauli conjugations. + +This module provides functionalities for defining and manipulating the 24 unique +single-qubit Clifford gates. It includes operations such as multiplying gates, +performing conjugations, and applying Pauli conjugations. Each gate is represented +in a standard form and can be combined with other gates to form complex quantum +operations. +""" from __future__ import annotations @@ -28,7 +37,29 @@ class Clifford(Enum): - """Clifford gate.""" + """ + Clifford Gate Class. + + The Clifford class represents a quantum gate that is a member of the + Clifford group. This group consists of gates that preserve the + structure of quantum computations and can be efficiently simulated + on a classical computer. + + Attributes + ---------- + name : str + The name of the Clifford gate. + matrix : numpy.ndarray + The unitary matrix representation of the Clifford gate. + + Methods + ------- + apply(state): + Applies the Clifford gate to the given quantum state. + + inverse(): + Returns the inverse of the Clifford gate. + """ # MEMO: Cannot use ClassVar here I: Clifford @@ -66,18 +97,34 @@ class Clifford(Enum): @property def matrix(self) -> npt.NDArray[np.complex128]: - """Return the matrix of the Clifford gate.""" + """ + Return the matrix representation of the Clifford gate. + + Returns + ------- + npt.NDArray[np.complex128] + A complex-valued numpy array representing the matrix of the Clifford gate. + """ return CLIFFORD[self.value] @staticmethod def try_from_matrix(mat: npt.NDArray[Any]) -> Clifford | None: - """Find the Clifford gate from the matrix. + """ + Try to construct a Clifford gate from a given matrix. + + Parameters + ---------- + mat : npt.NDArray[Any] + The input matrix that represents a potential Clifford gate. - Return `None` if not found. + Returns + ------- + Clifford or None + The corresponding Clifford gate if found, otherwise None. Notes ----- - Global phase is ignored. + The global phase is ignored when determining the Clifford gate. """ if mat.shape != (2, 2): return None @@ -93,39 +140,136 @@ def try_from_matrix(mat: npt.NDArray[Any]) -> Clifford | None: return None def __repr__(self) -> str: - """Return the Clifford expression on the form of HSZ decomposition.""" + """ + Return a string representation of the Clifford expression in the form of + HSZ decomposition. + + Returns + ------- + str + A string that represents the Clifford expression. + """ formula = " @ ".join([f"Clifford.{gate}" for gate in self.hsz]) if len(self.hsz) == 1: return formula return f"({formula})" def __str__(self) -> str: - """Return the name of the Clifford gate.""" + """ + Return the string representation of the Clifford gate. + + This method retrieves the name of the specific Clifford gate + represented by the instance. + + Returns + ------- + str + The name of the Clifford gate. + """ return CLIFFORD_LABEL[self.value] @property def conj(self) -> Clifford: - """Return the conjugate of the Clifford gate.""" + """ + Return the conjugate of the Clifford gate. + + A Clifford gate is a type of quantum gate that is important in quantum computing. + The conjugate of a Clifford gate is obtained by applying the conjugate operation + to the gate representation. + + Returns + ------- + Clifford + A new Clifford gate that represents the conjugate of the original gate. + """ return Clifford(CLIFFORD_CONJ[self.value]) @property def hsz(self) -> list[Clifford]: - """Return a decomposition of the Clifford gate with the gates `H`, `S`, `Z`.""" + """ + Return a decomposition of the Clifford gate using the gates 'H', 'S', and 'Z'. + + The decomposition provides a representation of the Clifford gate + in terms of the Hadamard ('H'), Phase ('S'), and Pauli-Z ('Z') gates. + + Returns + ------- + list[Clifford] + A list containing the decomposition of the Clifford gate into + the specified gate operations. + """ return [Clifford(i) for i in CLIFFORD_HSZ_DECOMPOSITION[self.value]] @property def qasm3(self) -> tuple[str, ...]: - """Return a decomposition of the Clifford gate as qasm3 gates.""" + """ + Return a decomposition of the Clifford gate as qasm3 gates. + + Returns + ------- + tuple[str, ...] + A tuple containing the qasm3 representation of the + Clifford gate decomposition. + + Notes + ----- + The qasm3 format is used for representing quantum circuits in a + standardized way for various quantum programming frameworks. + """ return CLIFFORD_TO_QASM3[self.value] def __matmul__(self, other: Clifford) -> Clifford: - """Multiplication within the Clifford group (modulo unit factor).""" + """ + Perform matrix multiplication within the Clifford group. + + Parameters + ---------- + other : Clifford + The Clifford object to multiply with the current instance. + + Returns + ------- + Clifford + A new Clifford object resulting from the multiplication of the two Clifford instances, + computed modulo a unit factor. + + Notes + ----- + This operation follows the rules of multiplication specific to the Clifford group, ensuring + that the result remains within the group. + + Examples + -------- + Here is an example of how to use the __matmul__ method: + + >>> c1 = Clifford(...) + >>> c2 = Clifford(...) + >>> result = c1 @ c2 + """ if isinstance(other, Clifford): return Clifford(CLIFFORD_MUL[self.value][other.value]) return NotImplemented def measure(self, pauli: Pauli) -> Pauli: - """Compute C† P C.""" + """ + Compute the measurement of a Pauli operator using Clifford operations. + + Parameters + ---------- + pauli : Pauli + The Pauli operator to be measured. + + Returns + ------- + Pauli + The result of the measurement operation, which is the transformed Pauli operator + after applying the Clifford operations. + + Notes + ----- + This method computes the result of the operation \( C^\dagger P C \), where \( C \) + is the Clifford operation and \( P \) is the given Pauli operator. + """ if pauli.symbol == IXYZ.I: return copy.deepcopy(pauli) table = CLIFFORD_MEASURE[self.value] @@ -143,9 +287,22 @@ def commute_domains(self, domains: Domains) -> Domains: """ Commute `X^sZ^t` with `C`. - Given `X^sZ^t`, return `X^s'Z^t'` such that `X^sZ^tC = CX^s'Z^t'`. + Given the operator `X^sZ^t`, this method returns the operator `X^s'Z^t'` such that the equality + `X^sZ^t C = C X^s'Z^t'` holds. + + Parameters + ---------- + domains : Domains + The domains object containing the representation of the operators involved in the commutation. - Note that applying the method to `self.conj` computes the reverse commutation: + Returns + ------- + Domains + A new domains object representing the commuted operator `X^s'Z^t'`. + + Notes + ----- + Applying this method to `self.conj` computes the reverse commutation: indeed, `C†X^sZ^t = (X^sZ^tC)† = (CX^s'Z^t')† = X^s'Z^t'C†`. """ s_domain = domains.s_domain.copy() diff --git a/graphix/command.py b/graphix/command.py index 82ca917b0..2e6f3087b 100644 --- a/graphix/command.py +++ b/graphix/command.py @@ -1,4 +1,4 @@ -"""Data validator command classes.""" +"Data validator command classes." from __future__ import annotations @@ -25,7 +25,14 @@ class CommandKind(Enum): - """Tag for command kind.""" + """ + Tag for command kind. + + Attributes + ---------- + kind : str + The specific kind of command represented by this tag. + """ N = enum.auto() M = enum.auto() @@ -39,7 +46,28 @@ class CommandKind(Enum): class _KindChecker: - """Enforce tag field declaration.""" + """ + Enforce tag field declaration. + + This class checks the declaration of tag fields to ensure that they + adhere to the specified requirements. + + Attributes + ---------- + tag_fields : list of str + A list containing the names of the tag fields. + + Methods + ------- + check_field_declaration(field_name) + Validates if the given field name is declared as a tag field. + + add_tag_field(field_name) + Adds a new tag field to the list of tag fields if it is not already declared. + + remove_tag_field(field_name) + Removes the specified tag field from the list of tag fields if it exists. + """ def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -48,14 +76,15 @@ def __init_subclass__(cls) -> None: @dataclasses.dataclass(repr=False) class N(_KindChecker, DataclassReprMixin): - r"""Preparation command. + """ + Preparation command. Parameters ---------- node : int Index of the qubit to prepare. state : ~graphix.states.State, optional - Initial state, defaults to :class:`~graphix.states.BasicStates.PLUS`. + Initial state. Defaults to :class:`~graphix.states.BasicStates.PLUS`. """ node: Node @@ -65,13 +94,15 @@ class N(_KindChecker, DataclassReprMixin): @dataclasses.dataclass(repr=False) class BaseM(DataclassReprMixin): - """Base measurement command. - - Represent a measurement of a node. In `graphix`, a measurement is an instance of - class `M`, with given plane, angles, and domains. The base class `BaseM` allows users to define - new class of measurements with different abstractions. For example, in the context - of blind computations, the server only knows which node is measured, and the parameters - are given by the :class:`graphix.simulator.MeasureMethod` provided by the client. + """ + Base measurement command. + + Represents a measurement of a node. In `graphix`, a measurement is an instance of + the class `M`, with specified plane, angles, and domains. The base class `BaseM` + allows users to define new classes of measurements with different abstractions. + For example, in the context of blind computations, the server only knows which node + is being measured, and the parameters are provided by the + :class:`graphix.simulator.MeasureMethod` from the client. """ node: Node @@ -80,14 +111,15 @@ class `M`, with given plane, angles, and domains. The base class `BaseM` allows @dataclasses.dataclass(repr=False) class M(BaseM, _KindChecker): - r"""Measurement command. + """ + Measurement command. Parameters ---------- node : int Node index of the measured qubit. plane : Plane, optional - Measurement plane, defaults to :class:`~graphix.fundamentals.Plane.XY`. + Measurement plane. Defaults to :class:`~graphix.fundamentals.Plane.XY`. angle : ExpressionOrFloat, optional Rotation angle divided by :math:`\pi`. s_domain : set[int], optional @@ -103,17 +135,18 @@ class M(BaseM, _KindChecker): kind: ClassVar[Literal[CommandKind.M]] = dataclasses.field(default=CommandKind.M, init=False) def clifford(self, clifford_gate: Clifford) -> M: - r"""Return a new measurement command with a Clifford applied. + """ + Return a new measurement command with a Clifford applied. Parameters ---------- clifford_gate : ~graphix.clifford.Clifford - Clifford gate to apply before the measurement. + The Clifford gate to apply before the measurement. Returns ------- :class:`~graphix.command.M` - Equivalent command representing the pattern ``MC``. + An equivalent command representing the pattern ``MC``. """ domains = clifford_gate.commute_domains(Domains(self.s_domain, self.t_domain)) update = MeasureUpdate.compute(self.plane, False, False, clifford_gate) @@ -128,11 +161,12 @@ def clifford(self, clifford_gate: Clifford) -> M: @dataclasses.dataclass(repr=False) class E(_KindChecker, DataclassReprMixin): - r"""Entanglement command between two qubits. + """ + Entanglement command between two qubits. Parameters ---------- - nodes : tuple[int, int] + nodes : tuple of int Pair of nodes to entangle. """ @@ -142,7 +176,8 @@ class E(_KindChecker, DataclassReprMixin): @dataclasses.dataclass(repr=False) class C(_KindChecker, DataclassReprMixin): - r"""Local Clifford gate command. + """ + Local Clifford gate command. Parameters ---------- @@ -159,7 +194,8 @@ class C(_KindChecker, DataclassReprMixin): @dataclasses.dataclass(repr=False) class X(_KindChecker, DataclassReprMixin): - r"""X correction command. + """ + X correction command. Parameters ---------- @@ -176,13 +212,14 @@ class X(_KindChecker, DataclassReprMixin): @dataclasses.dataclass(repr=False) class Z(_KindChecker, DataclassReprMixin): - r"""Z correction command. + """ + Z correction command. Parameters ---------- node : int Node to correct. - domain : set[int], optional + domain : set of int, optional Domain for the byproduct operator. """ @@ -193,7 +230,8 @@ class Z(_KindChecker, DataclassReprMixin): @dataclasses.dataclass(repr=False) class S(_KindChecker, DataclassReprMixin): - r"""S command. + """ + S command. Parameters ---------- @@ -210,12 +248,15 @@ class S(_KindChecker, DataclassReprMixin): @dataclasses.dataclass(repr=False) class T(_KindChecker): - r"""T command. + """ + T command. + + This command acts globally without any parameters. Parameters ---------- - None - The T command acts globally without parameters. + None : None + The T command does not require any parameters for its operation. """ kind: ClassVar[Literal[CommandKind.T]] = dataclasses.field(default=CommandKind.T, init=False) @@ -231,7 +272,8 @@ class T(_KindChecker): @dataclasses.dataclass class MeasureUpdate: - r"""Describe how a measure is changed by signals and a vertex operator. + """ + Describe how a measure is changed by signals and a vertex operator. Parameters ---------- @@ -249,23 +291,24 @@ class MeasureUpdate: @staticmethod def compute(plane: Plane, s: bool, t: bool, clifford_gate: Clifford) -> MeasureUpdate: - r"""Compute the measurement update. + """ + Compute the measurement update. Parameters ---------- plane : ~graphix.fundamentals.Plane Measurement plane of the command. s : bool - Whether an :math:`X` signal is present. + Indicates if an :math:`X` signal is present. t : bool - Whether a :math:`Z` signal is present. + Indicates if a :math:`Z` signal is present. clifford_gate : ~graphix.clifford.Clifford Vertex operator applied before the measurement. Returns ------- MeasureUpdate - Update describing the new measurement. + An update describing the new measurement. """ gates = list(map(Pauli.from_axis, plane.axes)) if s: diff --git a/graphix/extraction.py b/graphix/extraction.py index c29f06f2a..d36c02df5 100644 --- a/graphix/extraction.py +++ b/graphix/extraction.py @@ -1,4 +1,4 @@ -"""Functions to extract fusion network from a given graph state.""" +"Functions to extract the fusion network from a given graph state." from __future__ import annotations @@ -14,26 +14,52 @@ class ResourceType(Enum): - """Resource type.""" + """ + Represents a type of resource. + + Attributes + ---------- + name : str + The name of the resource type. + description : str + A brief description of the resource type. + capacity : int + The maximum capacity of the resource type. + + Methods + ------- + __init__(name: str, description: str, capacity: int) -> None: + Initializes a new instance of the ResourceType class. + __str__() -> str: + Returns a string representation of the ResourceType instance. + """ GHZ = "GHZ" LINEAR = "LINEAR" NONE = None def __str__(self) -> str: - """Return the name of the resource type.""" + """ + Returns the name of the resource type. + + Returns + ------- + str + The name of the resource type. + """ return self.name @dataclasses.dataclass class ResourceGraph: - """Resource graph state object. + """ + Resource graph state object. Parameters ---------- - cltype : :class:`ResourceType` object + cltype : ResourceType Type of the cluster. - graph : :class:`~graphix.graphsim.GraphState` object + graph : graphix.graphsim.GraphState Graph state of the cluster. """ @@ -41,7 +67,23 @@ class ResourceGraph: graph: GraphState def __eq__(self, other: object) -> bool: - """Return `True` if two resource graphs are equal, `False` otherwise.""" + """ + Determine if two resource graphs are equal. + + This method checks if the current resource graph is structurally + and functionally identical to another resource graph. + + Parameters + ---------- + other : object + The object to compare against. This is typically another instance + of `ResourceGraph`. + + Returns + ------- + bool + `True` if the two resource graphs are equal, `False` otherwise. + """ if not isinstance(other, ResourceGraph): raise TypeError("cannot compare ResourceGraph with other object") @@ -53,27 +95,29 @@ def get_fusion_network_from_graph( max_ghz: float = np.inf, max_lin: float = np.inf, ) -> list[ResourceGraph]: - """Extract GHZ and linear cluster graph state decomposition of desired resource state :class:`~graphix.graphsim.GraphState`. + """ + Extract GHZ and linear cluster graph state decomposition of the desired resource state :class:`~graphix.graphsim.GraphState`. - Extraction algorithm is based on [1]. + The extraction algorithm is based on the work by Zilk et al. (2022) [1]. - [1] Zilk et al., A compiler for universal photonic quantum computers, 2022 `arXiv:2210.09251 `_ + References + ---------- + [1] Zilk et al., A compiler for universal photonic quantum computers, 2022. + `arXiv:2210.09251 `_ Parameters ---------- - graph : :class:`~graphix.graphsim.GraphState` object - Graph state. - phasedict : dict - Dictionary of phases for each node. - max_ghz: - Maximum size of ghz clusters - max_lin: - Maximum size of linear clusters + graph : :class:`~graphix.graphsim.GraphState` + The graph state to be decomposed. + max_ghz : float, optional + Maximum size of GHZ clusters (default is np.inf). + max_lin : float, optional + Maximum size of linear clusters (default is np.inf). Returns ------- - list - List of :class:`ResourceGraph` objects. + list[ResourceGraph] + A list of :class:`ResourceGraph` objects representing the decomposed clusters. """ adjdict = {k: dict(copy.deepcopy(v)) for k, v in graph.adjacency()} @@ -139,19 +183,20 @@ def get_fusion_network_from_graph( def create_resource_graph(node_ids: list[int], root: int | None = None) -> ResourceGraph: - """Create a resource graph state (GHZ or linear) from node ids. + """ + Create a resource graph state (GHZ or linear) from node ids. Parameters ---------- - node_ids : list + node_ids : list[int] List of node ids. - root : int - Root of the ghz cluster. If None, it's a linear cluster. + root : int, optional + Root of the GHZ cluster. If None, a linear cluster is created. Defaults to None. Returns ------- - :class:`ResourceGraph` object - `ResourceGraph` object. + ResourceGraph + A `ResourceGraph` object representing the constructed resource graph. """ cluster_type = None edges = [] @@ -168,22 +213,24 @@ def create_resource_graph(node_ids: list[int], root: int | None = None) -> Resou def get_fusion_nodes(c1: ResourceGraph, c2: ResourceGraph) -> list[int]: - """Get the nodes that are fused between two resource states. Currently, we consider only type-I fusion. - - See [2] for the definition of fusion operation. + """ + Get the nodes that are fused between two resource states. Currently, we consider only type-I fusion. - [2] Daniel E. Browne and Terry Rudolph. Resource-efficient linear optical quantum computation. Physical Review Letters, 95(1):010501, 2005. + References + ---------- + [1] Daniel E. Browne and Terry Rudolph. Resource-efficient linear optical quantum computation. + Physical Review Letters, 95(1):010501, 2005. Parameters ---------- - c1 : :class:`ResourceGraph` object + c1 : ResourceGraph First resource state to be fused. - c2 : :class:`ResourceGraph` object + c2 : ResourceGraph Second resource state to be fused. Returns ------- - list + list[int] List of nodes that are fused between the two clusters. """ if not isinstance(c1, ResourceGraph) or not isinstance(c2, ResourceGraph): diff --git a/graphix/find_pflow.py b/graphix/find_pflow.py index 6a0b8e949..0db116c3a 100644 --- a/graphix/find_pflow.py +++ b/graphix/find_pflow.py @@ -1,12 +1,16 @@ -"""Pauli flow finding algorithm. +""" +Pauli flow finding algorithm. -This module implements the algorithm presented in [1]. For a given labelled open graph (G, I, O, meas_plane), this algorithm finds a maximally delayed Pauli flow [2] in polynomial time with the number of nodes, :math:`O(N^3)`. -If the input graph does not have Pauli measurements, the algorithm returns a general flow (gflow) if it exists by definition. +This module implements the algorithm presented in [1]. For a given labelled open graph +(G, I, O, meas_plane), this algorithm finds a maximally delayed Pauli flow [2] in polynomial +time with respect to the number of nodes, :math:`O(N^3)`. If the input graph does not +have Pauli measurements, the algorithm returns a general flow (gflow) if it exists by +definition. References ---------- -[1] Mitosek and Backens, 2024 (arXiv:2410.23439). -[2] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212) +[1] Mitosek, M., & Backens, M. (2024). arXiv:2410.23439. +[2] Browne, D. E., et al. (2007). New J. Phys., 9, 250. arXiv:quant-ph/0702212 """ from __future__ import annotations @@ -28,20 +32,29 @@ class OpenGraphIndex: - """A class for managing the mapping between node numbers of a given open graph and matrix indices in the Pauli flow finding algorithm. + """ + A class for managing the mapping between node numbers of a given open graph and matrix indices + in the Pauli flow finding algorithm. - It reuses the class `:class: graphix.sim.base_backend.NodeIndex` introduced for managing the mapping between node numbers and qubit indices in the internal state of the backend. + This class reuses the `NodeIndex` class defined in `graphix.sim.base_backend` for managing the + mapping between node numbers and qubit indices in the internal state of the backend. Attributes ---------- - og (OpenGraph) - non_inputs (NodeIndex) : Mapping between matrix indices and non-input nodes (labelled with integers). - non_outputs (NodeIndex) : Mapping between matrix indices and non-output nodes (labelled with integers). - non_outputs_optim (NodeIndex) : Mapping between matrix indices and a subset of non-output nodes (labelled with integers). + og : OpenGraph + The OpenGraph instance associated with this index manager. + non_inputs : NodeIndex + Mapping between matrix indices and non-input nodes (labelled with integers). + non_outputs : NodeIndex + Mapping between matrix indices and non-output nodes (labelled with integers). + non_outputs_optim : NodeIndex + Mapping between matrix indices and a subset of non-output nodes (labelled with integers). Notes ----- - At initialization, `non_outputs_optim` is a copy of `non_outputs`. The nodes corresponding to zero-rows of the order-demand matrix are removed for calculating the P matrix more efficiently in the `:func: _find_pflow_general` routine. + At initialization, `non_outputs_optim` is a copy of `non_outputs`. The nodes corresponding to + zero-rows of the order-demand matrix are removed to calculate the P matrix more efficiently + in the `_find_pflow_general` routine. """ def __init__(self, og: OpenGraph) -> None: @@ -64,7 +77,8 @@ def __init__(self, og: OpenGraph) -> None: def _compute_reduced_adj(ogi: OpenGraphIndex) -> MatGF2: - r"""Return the reduced adjacency matrix (RAdj) of the input open graph. + """ + Return the reduced adjacency matrix (RAdj) of the input open graph. Parameters ---------- @@ -80,7 +94,7 @@ def _compute_reduced_adj(ogi: OpenGraphIndex) -> MatGF2: ----- The adjacency matrix of a graph :math:`Adj_G` is an :math:`n \times n` matrix. - The RAdj matrix of an open graph OG is an :math:`(n - n_O) \times (n - n_I)` submatrix of :math:`Adj_G` constructed by removing the output rows and input columns of :math:`Adj_G`. + The RAdj matrix of an open graph OG is an :math:`(n - n_O) \times (n - n_I)` submatrix of :math:`Adj_G`, constructed by removing the output rows and input columns of :math:`Adj_G`. See Definition 3.3 in Mitosek and Backens, 2024 (arXiv:2410.23439). """ @@ -100,21 +114,28 @@ def _compute_reduced_adj(ogi: OpenGraphIndex) -> MatGF2: def _compute_pflow_matrices(ogi: OpenGraphIndex) -> tuple[MatGF2, MatGF2]: - r"""Construct flow-demand and order-demand matrices. + """ + Construct flow-demand and order-demand matrices. Parameters ---------- ogi : OpenGraphIndex - Open graph whose flow-demand and order-demand matrices are computed. + An instance of OpenGraphIndex which is used to compute the flow-demand + and order-demand matrices. Returns ------- - flow_demand_matrix : MatGF2 - order_demand_matrix : MatGF2 + tuple[MatGF2] + A tuple containing: + - flow_demand_matrix : MatGF2 + The flow-demand matrix computed from the open graph. + - order_demand_matrix : MatGF2 + The order-demand matrix computed from the open graph. Notes ----- - See Definitions 3.4 and 3.5, and Algorithm 1 in Mitosek and Backens, 2024 (arXiv:2410.23439). + Refer to Definitions 3.4 and 3.5, as well as Algorithm 1 in Mitosek and + Backens, 2024 (arXiv:2410.23439) for further details. """ flow_demand_matrix = _compute_reduced_adj(ogi) order_demand_matrix = flow_demand_matrix.copy() @@ -152,30 +173,33 @@ def _compute_pflow_matrices(ogi: OpenGraphIndex) -> tuple[MatGF2, MatGF2]: def _find_pflow_simple(ogi: OpenGraphIndex) -> tuple[MatGF2, MatGF2] | None: - r"""Construct the correction matrix :math:`C` and the ordering matrix, :math:`NC` for an open graph with equal number of inputs and outputs. + """ + Construct the correction matrix :math:`C` and the ordering matrix :math:`NC` for an open graph with an equal number of inputs and outputs. Parameters ---------- ogi : OpenGraphIndex - Open graph for which :math:`C` and :math:`NC` are computed. + An open graph for which the correction matrix :math:`C` and ordering matrix :math:`NC` are computed. Returns ------- - correction_matrix : MatGF2 - Matrix encoding the correction function. - ordering_matrix : MatGF2 - Matrix encoding the partial ordering between nodes. - - or `None` - if the input open graph does not have Pauli flow. + tuple[MatGF2, MatGF2] | None + A tuple containing: + - correction_matrix : MatGF2 + Matrix encoding the correction function. + - ordering_matrix : MatGF2 + Matrix encoding the partial ordering between nodes. + Returns `None` if the input open graph does not have Pauli flow. Notes ----- - The ordering matrix is defined as the product of the order-demand matrix :math:`N` and the correction matrix. - - The function only returns `None` when the flow-demand matrix is not invertible (meaning that `ogi` does not have Pauli flow). The condition that the ordering matrix :math:`NC` must encode a directed acyclic graph (DAG) is verified in a subsequent step by `:func: _compute_topological_generations`. + - The function returns `None` only when the flow-demand matrix is not invertible, which indicates that the input `ogi` does not have Pauli flow. The condition that the ordering matrix :math:`NC` must encode a directed acyclic graph (DAG) is verified in a subsequent step by `:func: _compute_topological_generations`. - See Definitions 3.4, 3.5 and 3.6, Theorems 3.1 and 4.1, and Algorithm 2 in Mitosek and Backens, 2024 (arXiv:2410.23439). + References + ---------- + See Definitions 3.4, 3.5, and 3.6, Theorems 3.1 and 4.1, and Algorithm 2 in Mitosek and Backens, 2024 (arXiv:2410.23439). """ flow_demand_matrix, order_demand_matrix = _compute_pflow_matrices(ogi) @@ -190,26 +214,25 @@ def _find_pflow_simple(ogi: OpenGraphIndex) -> tuple[MatGF2, MatGF2] | None: def _compute_p_matrix(ogi: OpenGraphIndex, nb_matrix: MatGF2) -> MatGF2 | None: - r"""Perform the steps 8 - 12 of the general case (larger number of outputs than inputs) algorithm. + """ + Perform steps 8 - 12 of the general case (larger number of outputs than inputs) algorithm. Parameters ---------- ogi : OpenGraphIndex - Open graph for which the matrix :math:`P` is computed. + The open graph for which the matrix :math:`P` is computed. nb_matrix : MatGF2 - Matrix :math:`N_B` + The matrix :math:`N_B`. Returns ------- - p_matrix : MatGF2 - Matrix encoding the correction function. - - or `None` - if the input open graph does not have Pauli flow. + MatGF2 or None + Returns the matrix encoding the correction function if successful, + or `None` if the input open graph does not have a Pauli flow. Notes ----- - See Theorem 4.4, steps 8 - 12 in Mitosek and Backens, 2024 (arXiv:2410.23439). + Refer to Theorem 4.4, steps 8 - 12 in Mitosek and Backens, 2024 (arXiv:2410.23439). """ n_no = len(ogi.non_outputs) # number of columns of P matrix. n_oi_diff = len(ogi.og.outputs) - len(ogi.og.inputs) # number of rows of P matrix. @@ -246,13 +269,34 @@ def _find_solvable_nodes( solved_nodes: AbstractSet[int], n_oi_diff: int, ) -> set[int]: - """Return the set nodes whose associated linear system is solvable. + """ + Return the set of nodes whose associated linear system is solvable. + + A node is considered solvable if: + - It has not been solved yet. + - Its column in the second block of :math:`K_{LS}` (which determines the constants in each equation) contains only zeros in the rows where all the coefficients in the first block are zeros. + + Parameters + ---------- + ogi : OpenGraphIndex + The graph index containing the nodes and their relationships. + kls_matrix : MatGF2 + The matrix representing the linear system. + non_outputs_set : AbstractSet[int] + A set of nodes that are not output nodes. + solved_nodes : AbstractSet[int] + A set of nodes that have already been solved. + n_oi_diff : int + The difference in the number of input and output variables. - A node is solvable if: - - It has not been solved yet. - - Its column in the second block of :math:`K_{LS}` (which determines the constants in each equation) has only zeros where it intersects rows for which all the coefficients in the first block are 0s. + Returns + ------- + set[int] + A set of nodes whose linear systems are solvable. - See Theorem 4.4, step 12.a in Mitosek and Backens, 2024 (arXiv:2410.23439). + References + ---------- + Mitosek, M., & Backens, M. (2024). Theorem 4.4, step 12.a. arXiv:2410.23439. """ solvable_nodes: set[int] = set() @@ -274,11 +318,30 @@ def _find_solvable_nodes( def _update_p_matrix( ogi: OpenGraphIndex, kls_matrix: MatGF2, p_matrix: MatGF2, solvable_nodes: AbstractSet[int], n_oi_diff: int ) -> None: - """Update `p_matrix`. + """ + Update the `p_matrix`. - The solution of the linear system associated with node :math:`v` in `solvable_nodes` corresponds to the column of `p_matrix` associated with node :math:`v`. + The solution of the linear system associated with each node :math:`v` in + `solvable_nodes` corresponds to the respective column of the `p_matrix` + associated with the same node :math:`v`. - See Theorem 4.4, steps 12.b and 12.c in Mitosek and Backens, 2024 (arXiv:2410.23439). + Parameters + ---------- + ogi : OpenGraphIndex + The open graph index used for the update. + kls_matrix : MatGF2 + The matrix representing the linear system. + p_matrix : MatGF2 + The matrix to be updated with the solutions. + solvable_nodes : AbstractSet[int] + A set of nodes for which the corresponding solutions are computed. + n_oi_diff : int + An integer representing the difference in node indices. + + References + ---------- + Theorem 4.4, steps 12.b and 12.c in Mitosek and Backens, 2024 + (arXiv:2410.23439). """ for v in solvable_nodes: j = ogi.non_outputs.index(v) @@ -298,25 +361,61 @@ def _update_kls_matrix( n_no: int, n_no_optim: int, ) -> None: - """Update `kls_matrix`. + """ + Update the `kls_matrix`. - Bring the linear system encoded in :math:`K_{LS}` to the row-echelon form (REF) that would be achieved by Gaussian elimination if the row and column vectors corresponding to vertices in `solvable_nodes` where not included in the starting matrix. + This function transforms the linear system encoded in :math:`K_{LS}` + into row-echelon form (REF) through Gaussian elimination, excluding + the row and column vectors corresponding to the vertices in + `solvable_nodes` from the starting matrix. - See Theorem 4.4, step 12.d in Mitosek and Backens, 2024 (arXiv:2410.23439). + Parameters + ---------- + ogi : OpenGraphIndex + The open graph index used in the update process. + kls_matrix : MatGF2 + The matrix representing the linear system to be updated. + kils_matrix : MatGF2 + An auxiliary matrix involved in the update process. + solvable_nodes : AbstractSet[int] + The set of nodes that can be solved. + n_oi_diff : int + The number of outgoing edges from the difference source node. + n_no : int + The total number of nodes. + n_no_optim : int + The optimized number of nodes. + + References + ---------- + Mitosek, G., & Backens, M. (2024). Theorem 4.4, step 12.d. arXiv:2410.23439. """ shift = n_oi_diff + n_no # `n_oi_diff` + `n_no` is the column offset from the first two blocks of K_{LS}. row_permutation: list[int] def reorder(old_pos: int, new_pos: int) -> None: # Used in step 12.d.vi - """Reorder the elements of `row_permutation`. - - The element at `old_pos` is placed on the right of the element at `new_pos`. - Example: - ``` - row_permutation = [0, 1, 2, 3, 4] - reorder(1, 3) -> [0, 2, 3, 1, 4] - reorder(2, -1) -> [2, 0, 1, 3, 4] - ``` + """ + Reorder the elements of `row_permutation`. + + The element at `old_pos` is moved to the right of the element at `new_pos`. + + Parameters + ---------- + old_pos : int + The position of the element to be moved. + new_pos : int + The position after which the element at `old_pos` will be placed. + + Examples + -------- + >>> row_permutation = [0, 1, 2, 3, 4] + >>> reorder(1, 3) + >>> row_permutation + [0, 2, 3, 1, 4] + + >>> reorder(2, -1) + >>> row_permutation + [2, 0, 1, 3, 4] """ val = row_permutation.pop(old_pos) row_permutation.insert(new_pos + (new_pos < old_pos), val) @@ -386,30 +485,31 @@ def reorder(old_pos: int, new_pos: int) -> None: # Used in step 12.d.vi def _find_pflow_general(ogi: OpenGraphIndex) -> tuple[MatGF2, MatGF2] | None: - r"""Construct the generalized correction matrix :math:`C'C^B` and the generalized ordering matrix, :math:`NC'C^B` for an open graph with larger number of outputs than inputs. + """ + Construct the generalized correction matrix :math:`C'C^B` and the generalized ordering matrix :math:`NC'C^B` for an open graph with a larger number of outputs than inputs. Parameters ---------- ogi : OpenGraphIndex - Open graph for which :math:`C'C^B` and :math:`NC'C^B` are computed. + An open graph for which :math:`C'C^B` and :math:`NC'C^B` are computed. Returns ------- - correction_matrix : MatGF2 - Matrix encoding the correction function. - ordering_matrix : MatGF2 - Matrix encoding the partial ordering between nodes. - - or `None` - if the input open graph does not have Pauli flow. + tuple[MatGF2, MatGF2] | None + If the input open graph has Pauli flow, returns a tuple containing: + - correction_matrix : MatGF2 + Matrix encoding the correction function. + - ordering_matrix : MatGF2 + Matrix encoding the partial ordering between nodes. + Returns `None` if the input open graph does not have Pauli flow. Notes ----- - - The function returns `None` if + The function returns `None` if: a) The flow-demand matrix is not invertible, or - b) Not all linear systems of equations associated to the non-output nodes are solvable, - meaning that `ogi` does not have Pauli flow. - Condition (b) is satisfied when the flow-demand matrix :math:`M` does not have a right inverse :math:`C` such that :math:`NC` represents a directed acyclical graph (DAG). + b) Not all linear systems of equations associated with the non-output nodes are solvable, + meaning that `ogi` does not have Pauli flow. + Condition (b) is satisfied when the flow-demand matrix :math:`M` does not have a right inverse :math:`C` such that :math:`NC` represents a directed acyclic graph (DAG). See Theorem 4.4 and Algorithm 3 in Mitosek and Backens, 2024 (arXiv:2410.23439). """ @@ -515,7 +615,8 @@ def _cnc_matrices2pflow( correction_matrix: MatGF2, ordering_matrix: MatGF2, ) -> tuple[dict[int, set[int]], dict[int, int]] | None: - r"""Transform the correction and ordering matrices into a Pauli flow in its standard form (correction function and partial order). + """ + Transform the correction and ordering matrices into a Pauli flow in its standard form (correction function and partial order). Parameters ---------- @@ -529,18 +630,22 @@ def _cnc_matrices2pflow( Returns ------- pf : dict[int, set[int]] - Pauli flow correction function. pf[i] is the set of qubits to be corrected for the measurement of qubit i. + Pauli flow correction function, where `pf[i]` is the set of qubits to be corrected for the measurement of qubit `i`. l_k : dict[int, int] Partial order between corrected qubits, such that the pair (`key`, `value`) corresponds to (node, depth). - or `None` - if the ordering matrix is not a DAG, in which case the input open graph does not have Pauli flow. + or None + if the ordering matrix is not a DAG, in which case the input open graph does not have a Pauli flow. Notes ----- - - The correction matrix :math:`C` is an :math:`(n - n_I) \times (n - n_O)` matrix related to the correction function :math:`c(v) = \{u \in I^c|C_{u,v} = 1\}`, where :math:`I^c` are the non-input nodes of `ogi`. In other words, the column :math:`v` of :math:`C` encodes the correction set of :math:`v`, :math:`c(v)`. + - The correction matrix :math:`C` is an :math:`(n - n_I) \times (n - n_O)` matrix related to the correction function + :math:`c(v) = \{u \in I^c|C_{u,v} = 1\}`, where :math:`I^c` are the non-input nodes of `ogi`. + In other words, the column :math:`v` of :math:`C` encodes the correction set of :math:`v`, :math:`c(v)`. - - The Pauli flow's ordering :math:`<_c` is the transitive closure of :math:`\lhd_c`, where the latter is related to the ordering matrix :math:`NC` as :math:`v \lhd_c w \Leftrightarrow (NC)_{w,v} = 1`, for :math:`v, w, \in O^c` two non-output nodes of `ogi`. + - The Pauli flow's ordering :math:`<_c` is the transitive closure of :math:`\lhd_c`, + where the latter is related to the ordering matrix :math:`NC` as :math:`v \lhd_c w \Leftrightarrow (NC)_{w,v} = 1`, + for :math:`v, w \in O^c`, two non-output nodes of `ogi`. See Definition 3.6, Lemma 3.12, and Theorem 3.1 in Mitosek and Backens, 2024 (arXiv:2410.23439). """ diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index 3f5de0103..0eb71c00e 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -1,4 +1,9 @@ -"""Fundamental components related to quantum mechanics.""" +""" +Fundamental components related to quantum mechanics. + +This module provides essential classes and functions that are +used in the study and application of quantum mechanics. +""" from __future__ import annotations @@ -30,33 +35,106 @@ class Sign(EnumReprMixin, Enum): - """Sign, plus or minus.""" + """ + Sign, plus or minus. + + Attributes + ---------- + value : str + A string that represents the sign, either '+' or '-'. + + Methods + ------- + __init__(self, value: str) + Initializes the Sign instance with a given sign value. + + __str__(self) -> str + Returns the string representation of the Sign instance. + + __eq__(self, other) -> bool + Compares two Sign instances for equality. + + __ne__(self, other) -> bool + Compares two Sign instances for inequality. + + is_positive(self) -> bool + Returns True if the sign is positive, otherwise False. + + is_negative(self) -> bool + Returns True if the sign is negative, otherwise False. + """ PLUS = 1 MINUS = -1 def __str__(self) -> str: - """Return `+` or `-`.""" + """ + Return a string representation of the Sign. + + The method returns a string that indicates the sign value, + which can either be '+' or '-'. + + Returns + ------- + str + A string representing the sign, either '+' or '-'. + """ if self == Sign.PLUS: return "+" return "-" @staticmethod def plus_if(b: bool) -> Sign: - """Return *+* if *b* is *True*, *-* otherwise.""" + """ + Return a Sign object representing '+' if the input is True, + and '-' if the input is False. + + Parameters + ---------- + b : bool + A boolean value that determines the Sign output. + + Returns + ------- + Sign + A Sign object representing either '+' or '-' + based on the value of the input boolean. + """ if b: return Sign.PLUS return Sign.MINUS @staticmethod def minus_if(b: bool) -> Sign: - """Return *-* if *b* is *True*, *+* otherwise.""" + """ + Return a Sign indicating the opposite based on the input boolean. + + Parameters + ---------- + b : bool + A boolean value that determines which Sign to return. + + Returns + ------- + Sign + Returns a Sign object representing '-' if `b` is True, + and '+' if `b` is False. + """ if b: return Sign.MINUS return Sign.PLUS def __neg__(self) -> Sign: - """Swap the sign.""" + """ + Return the negation of the sign. + + The __neg__ method swaps the sign of the current instance. + + Returns + ------- + Sign + A new instance of Sign with the opposite sign. + """ return Sign.minus_if(self == Sign.PLUS) @typing.overload @@ -72,7 +150,28 @@ def __mul__(self, other: float) -> float: ... def __mul__(self, other: complex) -> complex: ... def __mul__(self, other: Sign | complex) -> Sign | int | float | complex: - """Multiply the sign with another sign or a number.""" + """ + Multiply the sign with another sign or a number. + + Parameters + ---------- + other : Sign or complex + The sign or complex number to multiply with. + + Returns + ------- + Sign or int or float or complex + The result of the multiplication, which can be a Sign, + int, float, or a complex number depending on the type + of the operand. + + Notes + ----- + If `other` is an instance of `Sign`, the result will be a new + instance of `Sign`. If `other` is a numeric type (int, float, + or complex), the multiplication will be performed according + to standard arithmetic rules. + """ if isinstance(other, Sign): return Sign.plus_if(self == other) if isinstance(other, int): @@ -93,31 +192,82 @@ def __rmul__(self, other: float) -> float: ... def __rmul__(self, other: complex) -> complex: ... def __rmul__(self, other: complex) -> int | float | complex: - """Multiply the sign with a number.""" + """ + Multiply the sign with a number. + + Parameters + ---------- + other : complex + A complex number to be multiplied with the sign. + + Returns + ------- + int, float, complex + The result of the multiplication between the sign and the provided number. + """ if isinstance(other, (int, float, complex)): return self.__mul__(other) return NotImplemented def __int__(self) -> int: - """Return `1` for `+` and `-1` for `-`.""" + """ + Converts the Sign instance to an integer representation. + + Returns: + int: + Returns `1` if the sign is positive (`+`), + and `-1` if the sign is negative (`-`). + """ # mypy does not infer the return type correctly return self.value # type: ignore[no-any-return] def __float__(self) -> float: - """Return `1.0` for `+` and `-1.0` for `-`.""" + """ + Returns a float representation of the sign. + + The method returns `1.0` when the sign is positive (`+`) and `-1.0` when the sign is negative (`-`). + + Returns + ------- + float + `1.0` for a positive sign and `-1.0` for a negative sign. + """ return float(self.value) def __complex__(self) -> complex: - """Return `1.0 + 0j` for `+` and `-1.0 + 0j` for `-`.""" + """ + Convert the `Sign` instance to a complex number. + + Returns + ------- + complex + Returns `1.0 + 0j` for a positive sign and `-1.0 + 0j` for a negative sign. + """ return complex(self.value) class ComplexUnit(EnumReprMixin, Enum): """ - Complex unit: 1, -1, j, -j. - - Complex units can be multiplied with other complex units, - with Python constants 1, -1, 1j, -1j, and can be negated. + A class representing complex units: 1, -1, j, -j. + + The complex units can be multiplied with other complex units + as well as with Python constants 1, -1, 1j, and -1j. + Additionally, complex units can be negated. + + Attributes + ---------- + value : complex + The value of the complex unit, which can be one of the + complex units defined in this class. + + Methods + ------- + __mul__(other): + Multiplies the complex unit by another complex unit or + a Python constant. + + __neg__(): + Negates the complex unit. """ # HACK: complex(u) == (1j) ** u.value for all u in ComplexUnit. @@ -129,7 +279,19 @@ class ComplexUnit(EnumReprMixin, Enum): @staticmethod def try_from(value: ComplexUnit | SupportsComplexCtor) -> ComplexUnit | None: - """Return the ComplexUnit instance if the value is compatible, None otherwise.""" + """ + Returns a ComplexUnit instance if the given value is compatible. + + Parameters + ---------- + value : ComplexUnit or SupportsComplexCtor + The value to be converted into a ComplexUnit instance. + + Returns + ------- + ComplexUnit or None + A ComplexUnit instance if the value is compatible; otherwise, None. + """ if isinstance(value, ComplexUnit): return value value = complex(value) @@ -145,35 +307,104 @@ def try_from(value: ComplexUnit | SupportsComplexCtor) -> ComplexUnit | None: @staticmethod def from_properties(*, sign: Sign = Sign.PLUS, is_imag: bool = False) -> ComplexUnit: - """Construct ComplexUnit from its properties.""" + """ + Construct a `ComplexUnit` from its properties. + + Parameters + ---------- + sign : Sign, optional + The sign of the complex unit. Default is `Sign.PLUS`. + is_imag : bool, optional + Indicates whether the complex unit represents an imaginary unit. + Default is `False`. + + Returns + ------- + ComplexUnit + An instance of `ComplexUnit` constructed based on the provided properties. + """ osign = 0 if sign == Sign.PLUS else 2 oimag = 1 if is_imag else 0 return ComplexUnit(osign + oimag) @property def sign(self) -> Sign: - """Return the sign.""" + """ + Return the sign of the complex unit. + + Returns + ------- + Sign + The sign of the complex unit, indicating its direction + in the complex plane. + """ return Sign.plus_if(self.value < 2) @property def is_imag(self) -> bool: - """Return *True* if *j* or *-j*.""" + """ + Determine if the complex number is purely imaginary. + + Returns + ------- + bool + Returns True if the complex number is in the form of *j* or *-j*; + otherwise, returns False. + """ return bool(self.value % 2) def __complex__(self) -> complex: - """Return the unit as complex number.""" + """ + Return the unit as a complex number. + + Returns + ------- + complex + The complex representation of the unit. + """ ret: complex = 1j**self.value return ret def __str__(self) -> str: - """Return a human-readable representation of the unit.""" + """ + Return a human-readable representation of the complex unit. + + Returns + ------- + str + A string representation of the complex unit. + """ result = "1j" if self.is_imag else "1" if self.sign == Sign.MINUS: result = "-" + result return result def __mul__(self, other: ComplexUnit | SupportsComplexCtor) -> ComplexUnit: - """Multiply the complex unit with a number.""" + """ + Multiply the complex unit by another complex unit or a compatible numeric type. + + Parameters + ---------- + other : ComplexUnit or SupportsComplexCtor + The complex unit or numeric type to multiply with. + + Returns + ------- + ComplexUnit + A new ComplexUnit instance representing the product of the two complex units. + + Examples + -------- + >>> cu1 = ComplexUnit(1, 2) + >>> cu2 = ComplexUnit(3, 4) + >>> result = cu1 * cu2 + >>> print(result) + ComplexUnit(3, 10) + + >>> result = cu1 * 2 + >>> print(result) + ComplexUnit(2, 4) + """ if isinstance(other, ComplexUnit): return ComplexUnit((self.value + other.value) % 4) if isinstance( @@ -184,16 +415,71 @@ def __mul__(self, other: ComplexUnit | SupportsComplexCtor) -> ComplexUnit: return NotImplemented def __rmul__(self, other: SupportsComplexCtor) -> ComplexUnit: - """Multiply the complex unit with a number.""" + """ + Perform right multiplication of a complex unit with a number. + + Parameters + ---------- + other : SupportsComplexCtor + A number (e.g., int, float) with which to multiply the complex unit. + + Returns + ------- + ComplexUnit + A new `ComplexUnit` instance that is the result of the multiplication. + + Notes + ----- + The multiplication is performed in such a way that the complex unit behaves + according to the rules of complex arithmetic. + """ return self.__mul__(other) def __neg__(self) -> ComplexUnit: - """Return the opposite of the complex unit.""" + """ + Return the opposite of the complex unit. + + This method implements the unary negation operator for + instances of the ComplexUnit class, returning a new + ComplexUnit object representing the negation of the + current instance. + + Returns + ------- + ComplexUnit + A new ComplexUnit object that is the negation of the + current instance. + """ return ComplexUnit((self.value + 2) % 4) class IXYZ(Enum): - """I, X, Y or Z.""" + """ + Representation of the I, X, Y, or Z types. + + This class serves as a symbolic representation for the types I, X, Y, and Z. + It can be utilized in various contexts where these identifiers are needed. + + Attributes + ---------- + identifier : str + A string that holds the identifier, which can be either 'I', 'X', 'Y', or 'Z'. + + Methods + ------- + get_identifier() -> str: + Returns the current identifier. + + set_identifier(identifier: str) -> None: + Sets the identifier to the provided value, must be one of 'I', 'X', 'Y', or 'Z'. + + Example + ------- + >>> xyz = IXYZ() + >>> xyz.set_identifier('X') + >>> print(xyz.get_identifier()) + 'X' + """ I = enum.auto() X = enum.auto() @@ -202,7 +488,14 @@ class IXYZ(Enum): @property def matrix(self) -> npt.NDArray[np.complex128]: - """Return the matrix representation.""" + """ + Return the matrix representation. + + Returns + ------- + npt.NDArray[np.complex128] + The matrix representation of the object in complex128 format. + """ if self == IXYZ.I: return Ops.I if self == IXYZ.X: @@ -215,7 +508,19 @@ def matrix(self) -> npt.NDArray[np.complex128]: class Axis(EnumReprMixin, Enum): - """Axis: *X*, *Y* or *Z*.""" + """ + Represents an axis in a 3D space. + + Attributes + ---------- + axis : str + The axis can be one of 'X', 'Y', or 'Z'. + + Methods + ------- + __init__(axis: str) + Initializes the Axis object with the specified axis. + """ X = enum.auto() Y = enum.auto() @@ -223,7 +528,14 @@ class Axis(EnumReprMixin, Enum): @property def matrix(self) -> npt.NDArray[np.complex128]: - """Return the matrix representation.""" + """ + Get the matrix representation of the Axis. + + Returns + ------- + npt.NDArray[np.complex128] + The matrix representation as a NumPy ndarray with complex128 data type. + """ if self == Axis.X: return Ops.X if self == Axis.Y: @@ -234,8 +546,24 @@ def matrix(self) -> npt.NDArray[np.complex128]: class Plane(EnumReprMixin, Enum): - # TODO: Refactor using match - """Plane: *XY*, *YZ* or *XZ*.""" + """ + Represents a geometric plane in a three-dimensional space. + + The plane can be defined as either the XY, YZ, or XZ plane. + + Attributes + ---------- + type : str + The type of the plane, which can be either 'XY', 'YZ', or 'XZ'. + + Methods + ------- + __init__(plane_type: str): + Initializes the plane with the specified type. + + __str__(): + Returns a string representation of the plane type. + """ XY = enum.auto() YZ = enum.auto() @@ -243,7 +571,14 @@ class Plane(EnumReprMixin, Enum): @property def axes(self) -> tuple[Axis, Axis]: - """Return the pair of axes that carry the plane.""" + """ + Return the pair of axes that define the plane. + + Returns + ------- + tuple[Axis, Axis] + A tuple containing two axes that represent the plane. + """ if self == Plane.XY: return (Axis.X, Axis.Y) if self == Plane.YZ: @@ -254,7 +589,14 @@ def axes(self) -> tuple[Axis, Axis]: @property def orth(self) -> Axis: - """Return the axis orthogonal to the plane.""" + """ + Returns the axis orthogonal to the plane. + + Returns + ------- + Axis + The axis that is orthogonal to the plane represented by this instance. + """ if self == Plane.XY: return Axis.Z if self == Plane.YZ: @@ -265,7 +607,14 @@ def orth(self) -> Axis: @property def cos(self) -> Axis: - """Return the axis of the plane that conventionally carries the cos.""" + """ + Return the axis of the plane that conventionally carries the cosine function. + + Returns + ------- + Axis + The axis associated with the cosine representation in the plane. + """ if self == Plane.XY: return Axis.X if self == Plane.YZ: @@ -276,7 +625,17 @@ def cos(self) -> Axis: @property def sin(self) -> Axis: - """Return the axis of the plane that conventionally carries the sin.""" + """ + Returns the axis of the plane that conventionally carries the sine. + + This property retrieves the axis associated with the sine function in the context + of the plane representation. + + Returns + ------- + Axis + The axis of the plane corresponding to the sine function. + """ if self == Plane.XY: return Axis.Y if self == Plane.YZ: @@ -294,7 +653,25 @@ def polar(self, angle: Expression) -> tuple[Expression, Expression, Expression]: def polar( self, angle: ExpressionOrFloat ) -> tuple[float, float, float] | tuple[ExpressionOrFloat, ExpressionOrFloat, ExpressionOrFloat]: - """Return the Cartesian coordinates of the point of module 1 at the given angle, following the conventional orientation for cos and sin.""" + """ + Convert a polar coordinate to Cartesian coordinates. + + This method returns the Cartesian coordinates (x, y, z) of the point + with a magnitude of 1 at the specified angle, following the conventional + orientation for cosine and sine. + + Parameters + ---------- + angle : ExpressionOrFloat + The angle in radians at which the point is located. + + Returns + ------- + tuple[float, float, float] | tuple[ExpressionOrFloat, ExpressionOrFloat, ExpressionOrFloat] + A tuple containing the Cartesian coordinates (x, y, z) of the + point on the unit circle at the specified angle. The coordinates may + be either floats or expressions, depending on the input type. + """ pp = (self.cos, self.sin) cos, sin = cos_sin(angle) if pp == (Axis.X, Axis.Y): @@ -307,7 +684,21 @@ def polar( @staticmethod def from_axes(a: Axis, b: Axis) -> Plane: - """Return the plane carried by the given axes.""" + """ + Create a Plane from two given axes. + + Parameters + ---------- + a : Axis + The first axis that defines the plane. + b : Axis + The second axis that defines the plane. + + Returns + ------- + Plane + A Plane object defined by the specified axes. + """ ab = {a, b} if ab == {Axis.X, Axis.Y}: return Plane.XY diff --git a/graphix/generator.py b/graphix/generator.py index 39cf4b508..43b9ef2c2 100644 --- a/graphix/generator.py +++ b/graphix/generator.py @@ -1,4 +1,22 @@ -"""MBQC pattern generator.""" +""" +MBQC Pattern Generator. + +This module provides functions to generate and manipulate measurement-based quantum computing (MBQC) patterns. It includes tools for constructing various quantum states, applying measurements, and simulating the outcomes of quantum computations. + +Key functions: +- generate_pattern: Generates a specified MBQC pattern based on input parameters. +- simulate_measurements: Simulates measurements on a given MBQC pattern to produce results. + +Usage: + Import the module and use the provided functions to create and analyze MBQC patterns. + +Examples: + >>> from mbqc_generator import generate_pattern + >>> pattern = generate_pattern(params) + + >>> from mbqc_generator import simulate_measurements + >>> results = simulate_measurements(pattern) +""" from __future__ import annotations @@ -25,50 +43,60 @@ def generate_from_graph( outputs: Iterable[int], meas_planes: Mapping[int, Plane] | None = None, ) -> Pattern: - r"""Generate the measurement pattern from open graph and measurement angles. + """ + Generate the measurement pattern from an open graph and measurement angles. - This function takes an open graph ``G = (nodes, edges, input, outputs)``, - specified by :class:`networkx.Graph` and two lists specifying input and output nodes. - Currently we support XY-plane measurements. + This function takes an open graph \( G = (nodes, edges, input, outputs) \), + specified by :class:`networkx.Graph`, along with two lists specifying input and output nodes. + Currently, only XY-plane measurements are supported. - Searches for the flow in the open graph using :func:`graphix.gflow.find_flow` and if found, - construct the measurement pattern according to the theorem 1 of [NJP 9, 250 (2007)]. + It first searches for flow in the open graph using :func:`graphix.gflow.find_flow`. + If found, it constructs the measurement pattern according to Theorem 1 of + [NJP 9, 250 (2007)]. - Then, if no flow was found, searches for gflow using :func:`graphix.gflow.find_gflow`, - from which measurement pattern can be constructed from theorem 2 of [NJP 9, 250 (2007)]. + If no flow is found, it then searches for gflow using :func:`graphix.gflow.find_gflow`, + from which a measurement pattern can be constructed based on Theorem 2 of + [NJP 9, 250 (2007)]. - Then, if no gflow was found, searches for Pauli flow using :func:`graphix.gflow.find_pauliflow`, - from which measurement pattern can be constructed from theorem 4 of [NJP 9, 250 (2007)]. + If no gflow is found, it searches for Pauli flow using :func:`graphix.gflow.find_pauliflow`, + from which a measurement pattern can be constructed according to Theorem 4 of + [NJP 9, 250 (2007)]. - The constructed measurement pattern deterministically realize the unitary embedding + The constructed measurement pattern deterministically realizes the unitary embedding .. math:: U = \left( \prod_i \langle +_{\alpha_i} |_i \right) E_G N_{I^C}, - where the measurements (bras) with always :math:`\langle+|` bases determined by the measurement - angles :math:`\alpha_i` are applied to the measuring nodes, - i.e. the randomness of the measurement is eliminated by the added byproduct commands. + where the measurements (bras) are always in the \(\langle +| \) basis + determined by the measurement angles \(\alpha_i\) that are applied to the measuring nodes, + effectively eliminating randomness by the added byproduct commands. - .. seealso:: :func:`graphix.gflow.find_flow` :func:`graphix.gflow.find_gflow` :func:`graphix.gflow.find_pauliflow` :class:`graphix.pattern.Pattern` + See also + -------- + :func:`graphix.gflow.find_flow` + :func:`graphix.gflow.find_gflow` + :func:`graphix.gflow.find_pauliflow` + :class:`graphix.pattern.Pattern` Parameters ---------- graph : :class:`networkx.Graph` - Graph on which MBQC should be performed - angles : dict - measurement angles for each nodes on the graph (unit of pi), except output nodes - inputs : list - list of node indices for input nodes - outputs : list - list of node indices for output nodes - meas_planes : dict - optional: measurement planes for each nodes on the graph, except output nodes + Graph on which MBQC should be performed. + angles : Mapping[int, ExpressionOrFloat] + Measurement angles for each node on the graph (in units of pi), + except for output nodes. + inputs : Iterable[int] + List of node indices for input nodes. + outputs : Iterable[int] + List of node indices for output nodes. + meas_planes : Mapping[int, Plane] | None, optional + Measurement planes for each node on the graph, except for output nodes. Returns ------- - pattern : graphix.pattern.Pattern - constructed pattern. + pattern : :class:`graphix.pattern.Pattern` + Constructed measurement pattern. """ inputs_set = set(inputs) outputs_set = set(outputs) @@ -111,7 +139,51 @@ def _flow2pattern( f: Mapping[int, AbstractSet[int]], l_k: Mapping[int, int], ) -> Pattern: - """Construct a measurement pattern from a causal flow according to the theorem 1 of [NJP 9, 250 (2007)].""" + """ + Construct a measurement pattern from a causal flow. + + This function constructs a measurement pattern based on theorem 1 from + the paper "NJP 9, 250 (2007)". It utilizes the provided causal flow + information from the input graph and other parameters to generate + the corresponding measurement pattern. + + Parameters + ---------- + graph : nx.Graph[int] + The directed graph representing the causal flow, where nodes are + associated with integer identifiers. + + angles : Mapping[int, ExpressionOrFloat] + A mapping of node identifiers to their associated measurement angles, + which can be either float values or symbolic expressions. + + inputs : Iterable[int] + An iterable containing the identifiers of the input nodes for the + measurement pattern. + + f : Mapping[int, AbstractSet[int]] + A mapping where each key is a node identifier, and the value is a + set of identifiers representing the nodes that are causally related + or influenced by the corresponding node. + + l_k : Mapping[int, int] + A mapping where each key is a node identifier and the value is an + integer that represents a specific property or attribute related to + the node in the context of the measurement pattern. + + Returns + ------- + Pattern + The constructed measurement pattern based on the provided causal flow + and parameters. + + Notes + ----- + Ensure that the input graph is well-formed and that all mappings and + iterables contain valid identifiers that correspond to the nodes in the + graph. The output pattern will be in a form suitable for further + processing or analysis. + """ depth, layers = graphix.gflow.get_layers(l_k) pattern = Pattern(input_nodes=inputs) for i in set(graph.nodes) - set(inputs): @@ -142,7 +214,33 @@ def _gflow2pattern( g: Mapping[int, AbstractSet[int]], l_k: Mapping[int, int], ) -> Pattern: - """Construct a measurement pattern from a generalized flow according to the theorem 2 of [NJP 9, 250 (2007)].""" + """ + Construct a measurement pattern from a generalized flow according to Theorem 2 of + [NJP 9, 250 (2007)]. + + Parameters + ---------- + graph : nx.Graph[int] + The graph representing the structure of the system. + angles : Mapping[int, ExpressionOrFloat] + A mapping of node indices to their corresponding measurement angles (can be + expressions or floats). + inputs : Iterable[int] + A sequence of input node indices that will be measured. + meas_planes : Mapping[int, Plane] + A mapping of node indices to their corresponding measurement planes. + g : Mapping[int, AbstractSet[int]] + A mapping that associates each node index with a set of related node indices, + representing generalized flows. + l_k : Mapping[int, int] + A mapping that indicates the relationship between node indices and their + corresponding labels. + + Returns + ------- + Pattern + The constructed measurement pattern based on the provided parameters. + """ depth, layers = graphix.gflow.get_layers(l_k) pattern = Pattern(input_nodes=inputs) for i in set(graph.nodes) - set(inputs): @@ -168,7 +266,47 @@ def _pflow2pattern( p: Mapping[int, AbstractSet[int]], l_k: Mapping[int, int], ) -> Pattern: - """Construct a measurement pattern from a Pauli flow according to the theorem 4 of [NJP 9, 250 (2007)].""" + """ + Construct a measurement pattern from a Pauli flow according to Theorem 4 of + [NJP 9, 250 (2007)]. + + Parameters + ---------- + graph : nx.Graph[int] + The graph representation of the system, where nodes represent qubits + and edges represent quantum operations. + + angles : Mapping[int, ExpressionOrFloat] + A mapping from qubit indices to their corresponding rotation angles. + + inputs : Iterable[int] + A collection of input qubit indices that are used as the starting point + for constructing the measurement pattern. + + meas_planes : Mapping[int, Plane] + A mapping from qubit indices to measurement planes associated with + each qubit. + + p : Mapping[int, AbstractSet[int]] + A mapping from measurement indices to sets of qubit indices that + are measured together in a specific operation. + + l_k : Mapping[int, int] + A mapping from qubit indices to integers defining the measurement + configurations. + + Returns + ------- + Pattern + The constructed measurement pattern that embodies the specified + Pauli flow. + + Notes + ----- + This function implements the procedure outlined in the referenced + paper to generate a pattern suitable for quantum measurements + based on the given Pauli flow. + """ depth, layers = graphix.gflow.get_layers(l_k) pattern = Pattern(input_nodes=inputs) for i in set(graph.nodes) - set(inputs): diff --git a/graphix/gflow.py b/graphix/gflow.py index 439c63674..e69de29bb 100644 --- a/graphix/gflow.py +++ b/graphix/gflow.py @@ -1,950 +0,0 @@ -"""Flow finding algorithm. - -For a given underlying graph (G, I, O, meas_plane), this method finds a (generalized) flow [NJP 9, 250 (2007)] in polynomial time. -In particular, this outputs gflow with minimum depth, maximally delayed gflow. - -Ref: Mhalla and Perdrix, International Colloquium on Automata, -Languages, and Programming (Springer, 2008), pp. 857-868. -Ref: Backens et al., Quantum 5, 421 (2021). - -""" - -from __future__ import annotations - -from copy import deepcopy -from typing import TYPE_CHECKING - -from typing_extensions import assert_never - -import graphix.opengraph -from graphix.command import CommandKind -from graphix.find_pflow import find_pflow as _find_pflow -from graphix.fundamentals import Axis, Plane -from graphix.measurements import Measurement, PauliMeasurement -from graphix.parameter import Placeholder - -if TYPE_CHECKING: - from collections.abc import Mapping - from collections.abc import Set as AbstractSet - - import networkx as nx - - from graphix.parameter import ExpressionOrFloat - from graphix.pattern import Pattern - - -# TODO: This should be ensured by type-checking. -def check_meas_planes(meas_planes: dict[int, Plane]) -> None: - """Check that all planes are valid planes.""" - for node, plane in meas_planes.items(): - if not isinstance(plane, Plane): - raise TypeError(f"Measure plane for {node} is `{plane}`, which is not an instance of `Plane`") - - -# NOTE: In a future version this function will take an `OpenGraph` object as input. -def find_gflow( - graph: nx.Graph[int], - iset: AbstractSet[int], - oset: AbstractSet[int], - meas_planes: Mapping[int, Plane], - mode: str = "single", # noqa: ARG001 Compatibility with old API -) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - r"""Return a maximally delayed general flow (gflow) of the input open graph if it exists. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (including input and output). - iset: AbstractSet[int] - Set of input nodes. - oset: AbstractSet[int] - Set of output nodes. - meas_planes: Mapping[int, Plane] - Measurement planes for each qubit. meas_planes[i] is the measurement plane for qubit i. - mode: str - Deprecated. Reminiscent of old API, it will be removed in future versions. - - Returns - ------- - dict[int, set[int]] - Gflow correction function. In a given pair (key, value), value is the set of qubits to be corrected for the measurement of qubit key. - dict[int, int] - Partial order between corrected qubits, such that the pair (key, value) corresponds to (node, depth). - - or None, None - if the input open graph does not have gflow. - - Notes - ----- - This function implements the algorithm in [1], see module graphix.find_pflow. - See [1] or [2] for a definition of gflow. - - References - ---------- - [1] Mitosek and Backens, 2024 (arXiv:2410.23439). - [2] Backens et al., Quantum 5, 421 (2021). - """ - meas = {node: Measurement(Placeholder("Angle"), plane) for node, plane in meas_planes.items()} - og = graphix.opengraph.OpenGraph( - inside=graph, - inputs=list(iset), - outputs=list(oset), - measurements=meas, - ) - gf = _find_pflow(og) - if gf is None: - return None, None # This is to comply with old API. It will be change in the future to `None`` - return gf[0], gf[1] - - -def find_flow( - graph: nx.Graph[int], - iset: set[int], - oset: set[int], - meas_planes: dict[int, Plane] | None = None, -) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - """Causal flow finding algorithm. - - For open graph g with input, output, and measurement planes, this returns causal flow. - For more detail of causal flow, see Danos and Kashefi, PRA 74, 052310 (2006). - - Original algorithm by Mhalla and Perdrix, - International Colloquium on Automata, Languages, and Programming (2008), - pp. 857-868. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (incl. input and output) - iset: set - set of node labels for input - oset: set - set of node labels for output - meas_planes: dict(int, Plane) - measurement planes for each qubits. meas_planes[i] is the measurement plane for qubit i. - Note that an underlying graph has a causal flow only if all measurement planes are Plane.XY. - If not specified, all measurement planes are interpreted as Plane.XY. - - Returns - ------- - f: list of nodes - causal flow function. f[i] is the qubit to be measured after qubit i. - l_k: dict - layers obtained by gflow algorithm. l_k[d] is a node set of depth d. - """ - check_meas_planes(meas_planes) - nodes = set(graph.nodes) - edges = set(graph.edges) - - if meas_planes is None: - meas_planes = dict.fromkeys(nodes - oset, Plane.XY) - - for plane in meas_planes.values(): - if plane != Plane.XY: - return None, None - - l_k = dict.fromkeys(nodes, 0) - f = {} - k = 1 - v_c = oset - iset - return flowaux(nodes, edges, iset, oset, v_c, f, l_k, k) - - -def flowaux( - nodes: set[int], - edges: set[tuple[int, int]], - iset: set[int], - oset: set[int], - v_c: set[int], - f: dict[int, set[int]], - l_k: dict[int, int], - k: int, -): - """Find one layer of the flow. - - Ref: Mhalla and Perdrix, International Colloquium on Automata, - Languages, and Programming (Springer, 2008), pp. 857-868. - - Parameters - ---------- - nodes: set - labels of all qubits (nodes) - edges: set - edges - iset: set - set of node labels for input - oset: set - set of node labels for output - v_c: set - correction candidate qubits - f: dict - flow function. f[i] is the qubit to be measured after qubit i. - l_k: dict - layers obtained by flow algorithm. l_k[d] is a node set of depth d. - k: int - current layer number. - meas_planes: dict - measurement planes for each qubits. meas_planes[i] is the measurement plane for qubit i. - - Outputs - ------- - f: list of nodes - causal flow function. f[i] is the qubit to be measured after qubit i. - l_k: dict - layers obtained by gflow algorithm. l_k[d] is a node set of depth d. - """ - v_out_prime = set() - c_prime = set() - - for q in v_c: - nb = search_neighbor(q, edges) - p_set = nb & (nodes - oset) - if len(p_set) == 1: - # Iterate over p_set assuming there is only one element p - (p,) = p_set - f[p] = {q} - l_k[p] = k - v_out_prime |= {p} - c_prime |= {q} - # determine whether there exists flow - if not v_out_prime: - if oset == nodes: - return f, l_k - return None, None - return flowaux( - nodes, - edges, - iset, - oset | v_out_prime, - (v_c - c_prime) | (v_out_prime & (nodes - iset)), - f, - l_k, - k + 1, - ) - - -# NOTE: In a future version this function will take an `OpenGraph` object as input. -def find_pauliflow( - graph: nx.Graph[int], - iset: AbstractSet[int], - oset: AbstractSet[int], - meas_planes: Mapping[int, Plane], - meas_angles: Mapping[int, ExpressionOrFloat], - mode: str = "single", # noqa: ARG001 Compatibility with old API -) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - r"""Return a maximally delayed Pauli flow of the input open graph if it exists. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (including input and output). - iset: AbstractSet[int] - Set of input nodes. - oset: AbstractSet[int] - Set of output nodes. - meas_planes: Mapping[int, Plane] - Measurement planes for each qubit. meas_planes[i] is the measurement plane for qubit i. - meas_angles: Mapping[int, ExpressionOrFloat] - Measurement angles for each qubit. meas_angles[i] is the measurement angle for qubit i. - mode: str - Deprecated. Reminiscent of old API, it will be removed in future versions. - - Returns - ------- - dict[int, set[int]] - Pauli flow correction function. In a given pair (key, value), value is the set of qubits to be corrected for the measurement of qubit key. - dict[int, int] - Partial order between corrected qubits, such that the pair (key, value) corresponds to (node, depth). - - or None, None - if the input open graph does not have gflow. - - Notes - ----- - This function implements the algorithm in [1], see module graphix.find_pflow. - See [1] or [2] for a definition of Pauli flow. - - References - ---------- - [1] Mitosek and Backens, 2024 (arXiv:2410.23439). - [2] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212) - """ - meas = {node: Measurement(angle, meas_planes[node]) for node, angle in meas_angles.items()} - og = graphix.opengraph.OpenGraph( - inside=graph, - inputs=list(iset), - outputs=list(oset), - measurements=meas, - ) - pf = _find_pflow(og) - if pf is None: - return None, None # This is to comply with old API. It will be change in the future to `None`` - return pf[0], pf[1] - - -def flow_from_pattern(pattern: Pattern) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - """Check if the pattern has a valid flow. If so, return the flow and layers. - - Parameters - ---------- - pattern: Pattern - pattern to be based on - - Returns - ------- - None, None: - The tuple ``(None, None)`` is returned if the pattern does not have a valid causal flow. - f: dict[int, set[int]] - flow function. g[i] is the set of qubits to be corrected for the measurement of qubit i. - l_k: dict[int, int] - layers obtained by flow algorithm. l_k[d] is a node set of depth d. - """ - if not pattern.is_standard(strict=True): - raise ValueError("The pattern should be standardized first.") - meas_planes = pattern.get_meas_plane() - for plane in meas_planes.values(): - if plane != Plane.XY: - return None, None - graph = pattern.extract_graph() - input_nodes = pattern.input_nodes if not pattern.input_nodes else set() - output_nodes = set(pattern.output_nodes) - - layers = pattern.get_layers() - l_k = {} - for l in layers[1]: - for n in layers[1][l]: - l_k[n] = l - lmax = max(l_k.values()) if l_k else 0 - for node, val in l_k.items(): - l_k[node] = lmax - val + 1 - for output_node in pattern.output_nodes: - l_k[output_node] = 0 - - xflow, zflow = get_corrections_from_pattern(pattern) - - if verify_flow(graph, input_nodes, output_nodes, xflow): # if xflow is valid - zflow_from_xflow = {} - for node, corrections in deepcopy(xflow).items(): - cand = find_odd_neighbor(graph, corrections) - {node} - if cand: - zflow_from_xflow[node] = cand - if zflow_from_xflow != zflow: # if zflow is consistent with xflow - return None, None - return xflow, l_k - return None, None - - -def gflow_from_pattern(pattern: Pattern) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - """Check if the pattern has a valid gflow. If so, return the gflow and layers. - - Parameters - ---------- - pattern: Pattern - pattern to be based on - - Returns - ------- - None, None: - The tuple ``(None, None)`` is returned if the pattern does not have a valid gflow. - g: dict[int, set[int]] - gflow function. g[i] is the set of qubits to be corrected for the measurement of qubit i. - l_k: dict[int, int] - layers obtained by gflow algorithm. l_k[d] is a node set of depth d. - """ - if not pattern.is_standard(strict=True): - raise ValueError("The pattern should be standardized first.") - graph = pattern.extract_graph() - input_nodes = set(pattern.input_nodes) if pattern.input_nodes else set() - output_nodes = set(pattern.output_nodes) - meas_planes = pattern.get_meas_plane() - - layers = pattern.get_layers() - l_k = {} - for l in layers[1]: - for n in layers[1][l]: - l_k[n] = l - lmax = max(l_k.values()) if l_k else 0 - for node, val in l_k.items(): - l_k[node] = lmax - val + 1 - for output_node in pattern.output_nodes: - l_k[output_node] = 0 - - xflow, zflow = get_corrections_from_pattern(pattern) - for node, plane in meas_planes.items(): - if plane in {Plane.XZ, Plane.YZ}: - if node not in xflow: - xflow[node] = {node} - xflow[node] |= {node} - - if verify_gflow(graph, input_nodes, output_nodes, xflow, meas_planes): # if xflow is valid - zflow_from_xflow = {} - for node, corrections in deepcopy(xflow).items(): - cand = find_odd_neighbor(graph, corrections) - {node} - if cand: - zflow_from_xflow[node] = cand - if zflow_from_xflow != zflow: # if zflow is consistent with xflow - return None, None - return xflow, l_k - return None, None - - -# TODO: Shouldn't call `find_pauliflow` -def pauliflow_from_pattern( - pattern: Pattern, - mode="single", # noqa: ARG001 Compatibility with old API -) -> tuple[dict[int, set[int]], dict[int, int]] | tuple[None, None]: - """Check if the pattern has a valid Pauliflow. If so, return the Pauliflow and layers. - - Parameters - ---------- - pattern: Pattern - pattern to be based on - mode: str - The Pauliflow finding algorithm can yield multiple equivalent solutions. So there are two options - - "single": Returns a single solution - - "all": Returns all possible solutions - - Optional. Default is "single". - - Returns - ------- - None, None: - The tuple ``(None, None)`` is returned if the pattern does not have a valid Pauli flow. - p: dict[int, set[int]] - Pauli flow function. p[i] is the set of qubits to be corrected for the measurement of qubit i. - l_k: dict[int, int] - layers obtained by Pauli flow algorithm. l_k[d] is a node set of depth d. - """ - if not pattern.is_standard(strict=True): - raise ValueError("The pattern should be standardized first.") - graph = pattern.extract_graph() - input_nodes = set(pattern.input_nodes) if pattern.input_nodes else set() - output_nodes = set(pattern.output_nodes) if pattern.output_nodes else set() - meas_planes = pattern.get_meas_plane() - meas_angles = pattern.get_angles() - - return find_pauliflow(graph, input_nodes, output_nodes, meas_planes, meas_angles) - - -def get_corrections_from_pattern(pattern: Pattern) -> tuple[dict[int, set[int]], dict[int, set[int]]]: - """Get x and z corrections from pattern. - - Parameters - ---------- - pattern: graphix.Pattern object - pattern to be based on - - Returns - ------- - xflow: dict - xflow function. xflow[i] is the set of qubits to be corrected in the X basis for the measurement of qubit i. - zflow: dict - zflow function. zflow[i] is the set of qubits to be corrected in the Z basis for the measurement of qubit i. - """ - nodes = pattern.extract_nodes() - xflow = {} - zflow = {} - for cmd in pattern: - if cmd.kind == CommandKind.M: - target = cmd.node - xflow_source = cmd.s_domain & nodes - zflow_source = cmd.t_domain & nodes - for node in xflow_source: - if node not in xflow: - xflow[node] = set() - xflow[node] |= {target} - for node in zflow_source: - if node not in zflow: - zflow[node] = set() - zflow[node] |= {target} - if cmd.kind == CommandKind.X: - target = cmd.node - xflow_source = cmd.domain & nodes - for node in xflow_source: - if node not in xflow: - xflow[node] = set() - xflow[node] |= {target} - if cmd.kind == CommandKind.Z: - target = cmd.node - zflow_source = cmd.domain & nodes - for node in zflow_source: - if node not in zflow: - zflow[node] = set() - zflow[node] |= {target} - return xflow, zflow - - -def search_neighbor(node: int, edges: set[tuple[int, int]]) -> set[int]: - """Find neighborhood of node in edges. This is an ancillary method for `flowaux()`. - - Parameter - ------- - node: int - target node number whose neighboring nodes will be collected - edges: set of taples - set of edges in the graph - - Outputs - ------ - N: list of ints - neighboring nodes - """ - nb = set() - for edge in edges: - if node == edge[0]: - nb |= {edge[1]} - elif node == edge[1]: - nb |= {edge[0]} - return nb - - -def get_min_depth(l_k: Mapping[int, int]) -> int: - """Get minimum depth of graph. - - Parameters - ---------- - l_k: dict - layers obtained by flow or gflow - - Returns - ------- - d: int - minimum depth of graph - """ - return max(l_k.values()) - - -def find_odd_neighbor(graph: nx.Graph[int], vertices: AbstractSet[int]) -> set[int]: - """Return the set containing the odd neighbor of a set of vertices. - - Parameters - ---------- - graph : :class:`networkx.Graph` - Underlying graph - vertices : set - set of nodes indices to find odd neighbors - - Returns - ------- - odd_neighbors : set - set of indices for odd neighbor of set `vertices`. - """ - odd_neighbors = set() - for vertex in vertices: - neighbors = set(graph.neighbors(vertex)) - odd_neighbors ^= neighbors - return odd_neighbors - - -def get_layers(l_k: Mapping[int, int]) -> tuple[int, dict[int, set[int]]]: - """Get components of each layer. - - Parameters - ---------- - l_k: dict - layers obtained by flow or gflow algorithms - - Returns - ------- - d: int - minimum depth of graph - layers: dict of set - components of each layer - """ - d = get_min_depth(l_k) - layers: dict[int, set[int]] = {k: set() for k in range(d + 1)} - for i, val in l_k.items(): - layers[val] |= {i} - return d, layers - - -def get_dependence_flow( - inputs: set[int], - flow: dict[int, set[int]], - odd_flow: dict[int, set[int]], -) -> dict[int, set[int]]: - """Get dependence flow from flow. - - Parameters - ---------- - inputs: set[int] - set of input nodes - flow: dict[int, set] - flow function. flow[i] is the set of qubits to be corrected for the measurement of qubit i. - odd_flow: dict[int, set] - odd neighbors of flow or gflow. - odd_flow[i] is the set of odd neighbors of f(i), Odd(f(i)). - - Returns - ------- - dependence_flow: dict[int, set] - dependence flow function. dependence_flow[i] is the set of qubits to be corrected for the measurement of qubit i. - """ - dependence_flow = {u: set() for u in inputs} - # concatenate flow and odd_flow - combined_flow = {} - for node, corrections in flow.items(): - combined_flow[node] = corrections | odd_flow[node] - for node, corrections in combined_flow.items(): - for correction in corrections: - if correction not in dependence_flow: - dependence_flow[correction] = set() - dependence_flow[correction] |= {node} - return dependence_flow - - -def get_dependence_pauliflow( - inputs: set[int], - flow: dict[int, set[int]], - odd_flow: dict[int, set[int]], - ls: tuple[set[int], set[int], set[int]], -): - """Get dependence flow from Pauli flow. - - Parameters - ---------- - inputs: set[int] - set of input nodes - flow: dict[int, set[int]] - Pauli flow function. p[i] is the set of qubits to be corrected for the measurement of qubit i. - odd_flow: dict[int, set[int]] - odd neighbors of Pauli flow or gflow. Odd(p(i)) - ls: tuple - ls = (l_x, l_y, l_z) where l_x, l_y, l_z are sets of qubits whose measurement operators are X, Y, Z, respectively. - - Returns - ------- - dependence_pauliflow: dict[int, set[int]] - dependence flow function. dependence_pauliflow[i] is the set of qubits to be corrected for the measurement of qubit i. - """ - l_x, l_y, l_z = ls - dependence_pauliflow = {u: set() for u in inputs} - # concatenate p and odd_p - combined_flow = {} - for node, corrections in flow.items(): - combined_flow[node] = (corrections - (l_x | l_y)) | (odd_flow[node] - (l_y | l_z)) - for ynode in l_y: - if ynode in corrections.symmetric_difference(odd_flow[node]): - combined_flow[node] |= {ynode} - for node, corrections in combined_flow.items(): - for correction in corrections: - if correction not in dependence_pauliflow: - dependence_pauliflow[correction] = set() - dependence_pauliflow[correction] |= {node} - return dependence_pauliflow - - -def get_layers_from_flow( - flow: dict[int, set], - odd_flow: dict[int, set], - inputs: set[int], - outputs: set[int], - ls: tuple[set[int], set[int], set[int]] | None = None, -) -> tuple[dict[int, set], int]: - """Get layers from flow (incl. gflow, Pauli flow). - - Parameters - ---------- - flow: dict[int, set] - flow function. flow[i] is the set of qubits to be corrected for the measurement of qubit i. - odd_flow: dict[int, set] - odd neighbors of flow or gflow. Odd(f(node)) - inputs: set - set of input nodes - outputs: set - set of output nodes - ls: tuple - ls = (l_x, l_y, l_z) where l_x, l_y, l_z are sets of qubits whose measurement operators are X, Y, Z, respectively. - If not None, the layers are obtained based on Pauli flow. - - Returns - ------- - layers: dict[int, set] - layers obtained from flow - depth: int - depth of the layers - - Raises - ------ - ValueError - If the flow is not valid(e.g. there is no partial order). - """ - layers = {} - depth = 0 - if ls is None: - dependence_flow = get_dependence_flow(inputs, odd_flow, flow) - else: - dependence_flow = get_dependence_pauliflow(inputs, flow, odd_flow, ls) - left_nodes = set(flow.keys()) - for output in outputs: - if output in left_nodes: - raise ValueError("Invalid flow") - while True: - layers[depth] = set() - for node in left_nodes: - if node not in dependence_flow or len(dependence_flow[node]) == 0 or dependence_flow[node] == {node}: - layers[depth] |= {node} - left_nodes -= layers[depth] - for node in left_nodes: - dependence_flow[node] -= layers[depth] - if len(layers[depth]) == 0: - if len(left_nodes) == 0: - layers[depth] = outputs - depth += 1 - break - raise ValueError("Invalid flow") - depth += 1 - return layers, depth - - -def verify_flow( - graph: nx.Graph, - iset: set[int], - oset: set[int], - flow: dict[int, set], - meas_planes: dict[int, Plane] | None = None, -) -> bool: - """Check whether the flow is valid. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (incl. input and output) - flow: dict[int, set] - flow function. flow[i] is the set of qubits to be corrected for the measurement of qubit i. - meas_planes: dict[int, str] - optional: measurement planes for each qubits. meas_planes[i] is the measurement plane for qubit i. - - - Returns - ------- - valid_flow: bool - True if the flow is valid. False otherwise. - """ - if meas_planes is None: - meas_planes = {} - check_meas_planes(meas_planes) - valid_flow = True - non_outputs = set(graph.nodes) - oset - # if meas_planes is given, check whether all measurement planes are "XY" - for node, plane in meas_planes.items(): - if plane != Plane.XY or node not in non_outputs: - return False - - odd_flow = {node: find_odd_neighbor(graph, corrections) for node, corrections in flow.items()} - - try: - _, _ = get_layers_from_flow(flow, odd_flow, iset, oset) - except ValueError: - return False - # check if v ~ f(v) for each node - edges = set(graph.edges) - for node, corrections in flow.items(): - if len(corrections) > 1: - return False - correction = next(iter(corrections)) - if (node, correction) not in edges and (correction, node) not in edges: - return False - return valid_flow - - -def verify_gflow( - graph: nx.Graph, - iset: set[int], - oset: set[int], - gflow: dict[int, set], - meas_planes: dict[int, Plane], -) -> bool: - """Check whether the gflow is valid. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (incl. input and output) - iset: set - set of node labels for input - oset: set - set of node labels for output - gflow: dict[int, set] - gflow function. gflow[i] is the set of qubits to be corrected for the measurement of qubit i. - .. seealso:: :func:`find_gflow` - meas_planes: dict[int, str] - measurement planes for each qubits. meas_planes[i] is the measurement plane for qubit i. - - Returns - ------- - valid_gflow: bool - True if the gflow is valid. False otherwise. - """ - check_meas_planes(meas_planes) - valid_gflow = True - non_outputs = set(graph.nodes) - oset - odd_flow = {} - for non_output in non_outputs: - if non_output not in gflow: - gflow[non_output] = set() - odd_flow[non_output] = set() - else: - odd_flow[non_output] = find_odd_neighbor(graph, gflow[non_output]) - - try: - _, _ = get_layers_from_flow(gflow, odd_flow, iset, oset) - except ValueError: - return False - - # check for each measurement plane - for node, plane in meas_planes.items(): - # index = node_order.index(node) - if plane == Plane.XY: - valid_gflow &= (node not in gflow[node]) and (node in odd_flow[node]) - elif plane == Plane.XZ: - valid_gflow &= (node in gflow[node]) and (node in odd_flow[node]) - elif plane == Plane.YZ: - valid_gflow &= (node in gflow[node]) and (node not in odd_flow[node]) - - return valid_gflow - - -def verify_pauliflow( - graph: nx.Graph, - iset: set[int], - oset: set[int], - pauliflow: dict[int, set[int]], - meas_planes: dict[int, Plane], - meas_angles: dict[int, float], -) -> bool: - """Check whether the Pauliflow is valid. - - Parameters - ---------- - graph: :class:`networkx.Graph` - Graph (incl. input and output) - iset: set - set of node labels for input - oset: set - set of node labels for output - pauliflow: dict[int, set] - Pauli flow function. pauliflow[i] is the set of qubits to be corrected for the measurement of qubit i. - meas_planes: dict[int, Plane] - measurement planes for each qubits. meas_planes[i] is the measurement plane for qubit i. - meas_angles: dict[int, float] - measurement angles for each qubits. meas_angles[i] is the measurement angle for qubit i. - - Returns - ------- - valid_pauliflow: bool - True if the Pauliflow is valid. False otherwise. - """ - check_meas_planes(meas_planes) - l_x, l_y, l_z = get_pauli_nodes(meas_planes, meas_angles) - - valid_pauliflow = True - non_outputs = set(graph.nodes) - oset - odd_flow = {} - for non_output in non_outputs: - if non_output not in pauliflow: - pauliflow[non_output] = set() - odd_flow[non_output] = set() - else: - odd_flow[non_output] = find_odd_neighbor(graph, pauliflow[non_output]) - - try: - layers, depth = get_layers_from_flow(pauliflow, odd_flow, iset, oset, (l_x, l_y, l_z)) - except ValueError: - return False - node_order = [] - for d in range(depth): - node_order.extend(list(layers[d])) - - for node, plane in meas_planes.items(): - if node in l_x: - valid_pauliflow &= node in odd_flow[node] - elif node in l_z: - valid_pauliflow &= node in pauliflow[node] - elif node in l_y: - valid_pauliflow &= node in pauliflow[node].symmetric_difference(odd_flow[node]) - elif plane == Plane.XY: - valid_pauliflow &= (node not in pauliflow[node]) and (node in odd_flow[node]) - elif plane == Plane.XZ: - valid_pauliflow &= (node in pauliflow[node]) and (node in odd_flow[node]) - elif plane == Plane.YZ: - valid_pauliflow &= (node in pauliflow[node]) and (node not in odd_flow[node]) - - return valid_pauliflow - - -def get_input_from_flow(flow: dict[int, set]) -> set: - """Get input nodes from flow. - - Parameters - ---------- - flow: dict[int, set] - flow function. flow[i] is the set of qubits to be corrected for the measurement of qubit i. - - Returns - ------- - inputs: set - set of input nodes - """ - non_output = set(flow.keys()) - for correction in flow.values(): - non_output -= correction - return non_output - - -def get_output_from_flow(flow: dict[int, set]) -> set: - """Get output nodes from flow. - - Parameters - ---------- - flow: dict[int, set] - flow function. flow[i] is the set of qubits to be corrected for the measurement of qubit i. - - Returns - ------- - outputs: set - set of output nodes - """ - non_outputs = set(flow.keys()) - non_inputs = set() - for correction in flow.values(): - non_inputs |= correction - return non_inputs - non_outputs - - -def get_pauli_nodes( - meas_planes: dict[int, Plane], meas_angles: Mapping[int, ExpressionOrFloat] -) -> tuple[set[int], set[int], set[int]]: - """Get sets of nodes measured in X, Y, Z basis. - - Parameters - ---------- - meas_planes: dict[int, Plane] - measurement planes for each node. - meas_angles: dict[int, float] - measurement angles for each node. - - Returns - ------- - l_x: set - set of nodes measured in X basis. - l_y: set - set of nodes measured in Y basis. - l_z: set - set of nodes measured in Z basis. - """ - check_meas_planes(meas_planes) - l_x, l_y, l_z = set(), set(), set() - for node, plane in meas_planes.items(): - pm = PauliMeasurement.try_from(plane, meas_angles[node]) - if pm is None: - continue - if pm.axis == Axis.X: - l_x |= {node} - elif pm.axis == Axis.Y: - l_y |= {node} - elif pm.axis == Axis.Z: - l_z |= {node} - else: - assert_never(pm.axis) - return l_x, l_y, l_z diff --git a/graphix/graphsim.py b/graphix/graphsim.py index 8976feeac..35ac5d23c 100644 --- a/graphix/graphsim.py +++ b/graphix/graphsim.py @@ -1,4 +1,33 @@ -"""Graph simulator.""" +""" +Graph simulator. + +This module provides functionality for simulating and analyzing graphs. +It includes various algorithms and tools for graph operations such as +traversal, searching, and manipulation of graph structures. + +Modules: +-------- +- Graph: Contains the implementation of the graph data structure. +- Algorithms: Includes functions for common graph algorithms such as + DFS, BFS, Dijkstra's algorithm and more. +- Utilities: Helper functions for graph visualization and other utilities. + +Usage: +------ +To use this module, import the required classes or functions and +instantiate the graph as needed. + +Example: +-------- +```python +from graph_simulator import Graph + +g = Graph() +g.add_edge(1, 2) +g.add_edge(2, 3) +print(g.bfs(1)) +``` +""" from __future__ import annotations @@ -27,7 +56,36 @@ class MBQCGraphNode(TypedDict): - """MBQC graph node attributes.""" + """ + Attributes of a Measurement-Based Quantum Computing (MBQC) graph node. + + This class represents a node in a graph used for + Measurement-Based Quantum Computing (MBQC) which includes + attributes defining the node's state and connections + to other nodes in the graph. + + Attributes + ---------- + id : int + Unique identifier for the node. + state : str + The quantum state associated with the node. + neighbors : list of MBQCGraphNode + A list of nodes that are directly connected to this node. + measurement_result : bool or None + The result of the measurement performed on this node, + if applicable. Defaults to None if no measurement has + been performed. + + Methods + ------- + add_neighbor(node): + Adds a neighboring node to this node's list of neighbors. + __str__(): + Returns a string representation of the node. + __repr__(): + Returns a formal string representation of the node. + """ sign: bool loop: bool @@ -35,17 +93,24 @@ class MBQCGraphNode(TypedDict): class GraphState(Graph): - """Graph state simulator implemented with :mod:`networkx`. - - Performs Pauli measurements on graph states. - - ref: M. Elliot, B. Eastin & C. Caves, JPhysA 43, 025301 (2010) - and PRA 77, 042307 (2008) - - Each node has attributes: - :*hollow*: True if node is hollow (has local H operator) - :*sign*: True if node has negative sign (local Z operator) - :*loop*: True if node has loop (local S operator) + """ + Graph state simulator implemented with :mod:`networkx`. + + This class performs Pauli measurements on graph states. + + References + ---------- + M. Elliot, B. Eastin & C. Caves, J. Phys. A 43, 025301 (2010) and + PRA 77, 042307 (2008). + + Attributes + ---------- + hollow : bool + True if the node is hollow (has a local H operator). + sign : bool + True if the node has a negative sign (local Z operator). + loop : bool + True if the node has a loop (local S operator). """ nodes: functools.cached_property[Mapping[int, MBQCGraphNode]] # type: ignore[assignment] @@ -56,16 +121,17 @@ def __init__( edges: Iterable[tuple[int, int]] | None = None, vops: Mapping[int, Clifford] | None = None, ) -> None: - """Instantiate a graph simulator. + """ + Instantiate a graph simulator. Parameters ---------- - nodes : Iterable[int] - A container of nodes - edges : Iterable[tuple[int, int]] - list of tuples (i,j) for pairs to be entangled. - vops : Mapping[int, Clifford] - dict of local Clifford gates with keys for node indices and Cliffords + nodes : Iterable[int], optional + A container of nodes. If None, the graph will be initialized with no nodes. + edges : Iterable[tuple[int, int]], optional + A list of tuples (i, j) representing pairs of nodes to be entangled. If None, no edges will be created. + vops : Mapping[int, Clifford], optional + A dictionary of local Clifford gates, where the keys are node indices and the values are the corresponding Clifford operations. If None, no local operations will be assigned. """ super().__init__() if nodes is not None: @@ -81,7 +147,28 @@ def add_nodes_from( # pyright: ignore[reportIncompatibleMethodOverride] nodes_for_adding: Iterable[int | tuple[int, MBQCGraphNode]], # type: ignore[override] **attr: Any, ) -> None: - """Wrap `networkx.Graph.add_nodes_from` to initialize MBQCGraphNode attributes.""" + """ + Add nodes to the graph. + + This method wraps the `networkx.Graph.add_nodes_from` function to initialize + attributes of `MBQCGraphNode`. + + Parameters + ---------- + nodes_for_adding : iterable of int or tuple of (int, MBQCGraphNode) + An iterable of nodes to be added. Each node can either be an integer + representing the node identifier, or a tuple consisting of an integer + and an instance of `MBQCGraphNode`. + + **attr : Any + Additional attributes to initialize for the nodes being added. + + Returns + ------- + None + This method does not return any value, it modifies the graph state + in place. + """ nodes_for_adding = list(nodes_for_adding) super().add_nodes_from(nodes_for_adding, **attr) # type: ignore[arg-type] for data in nodes_for_adding: @@ -106,23 +193,59 @@ def add_node( node_for_adding: int, **attr: Any, ) -> None: - """Wrap `networkx.Graph.add_node` to initialize MBQCGraphNode attributes.""" + """ + Add a node to the graph, wrapping the `networkx.Graph.add_node` method. + + This method initializes attributes for the node as specified by + keyword arguments, which can include any additional properties + needed for the `MBQCGraphNode`. + + Parameters + ---------- + node_for_adding : int + The identifier for the node to be added to the graph. + **attr : Any + Additional attributes to initialize for the MBQCGraphNode. + + Returns + ------- + None + """ self.add_nodes_from((node_for_adding,), **attr) def local_complement(self, node: int) -> None: - """Perform local complementation of a graph.""" + """ + Perform local complementation of a graph. + + Parameters + ---------- + node : int + The index of the node on which to perform the local complementation. + + Returns + ------- + None + This method modifies the graph in place and does not return a value. + + Notes + ----- + Local complementation at a node involves taking the induced subgraph formed by the neighbors of the node, + and replacing it with its complement. + """ g = self.subgraph(self.neighbors(node)) g_new: nx.Graph[int] = nx.complement(g) self.remove_edges_from(g.edges) self.add_edges_from(g_new.edges) def apply_vops(self, vops: Mapping[int, Clifford]) -> None: - """Apply local Clifford operators to the graph state from a dictionary. + """ + Apply local Clifford operators to the graph state from a dictionary. Parameters ---------- vops : Mapping[int, Clifford] - dict containing node indices as keys and local Clifford + A dictionary containing node indices as keys and local Clifford operators + as values. Returns ------- @@ -140,12 +263,13 @@ def apply_vops(self, vops: Mapping[int, Clifford]) -> None: raise RuntimeError def get_vops(self) -> dict[int, Clifford]: - """Apply local Clifford operators to the graph state from a dictionary. + """ + Apply local Clifford operators to the graph state from a dictionary. Returns ------- - vops : dict[int, Clifford] - dict containing node indices as keys and local Cliffords + vops : dict[int, Clifford] + A dictionary containing node indices as keys and local Clifford operators as values. """ vops: dict[int, Clifford] = {} for i in self.nodes: @@ -160,12 +284,13 @@ def get_vops(self) -> dict[int, Clifford]: return vops def flip_fill(self, node: int) -> None: - """Flips the fill (local H) of a node. + """ + Flips the fill (local Hamiltonian) of a specified node in the graph. Parameters ---------- node : int - graph node to flip the fill + The graph node for which the fill is to be flipped. Returns ------- @@ -174,15 +299,17 @@ def flip_fill(self, node: int) -> None: self.nodes[node]["hollow"] = not self.nodes[node]["hollow"] def flip_sign(self, node: int) -> None: - """Flip the sign (local Z) of a node. + """ + Flip the sign (local Z) of a node. - Note that application of Z gate is different from `flip_sign` - if there exist an edge from the node. + This method flips the sign of the specified node in the graph state. + Note that the application of the Z gate is different from `flip_sign` + if there exists an edge from the node. Parameters ---------- node : int - graph node to flip the sign + The graph node for which to flip the sign. Returns ------- @@ -191,17 +318,18 @@ def flip_sign(self, node: int) -> None: self.nodes[node]["sign"] = not self.nodes[node]["sign"] def advance(self, node: int) -> None: - """Flip the loop (local S) of a node. + """ + Flip the loop (local S) of a specified node in the graph. - If the loop already exist, sign is also flipped, - reflecting the relation SS=Z. - Note that application of S gate is different from `advance` - if there exist an edge from the node. + This method modifies the state of the loop associated with the given node. + If the loop already exists, the sign is flipped, reflecting the relation + SS = Z. Note that the application of the S gate differs from `advance` + if there is an edge connected to the node. Parameters ---------- node : int - graph node to advance the loop. + The graph node for which to advance the loop. Returns ------- @@ -214,12 +342,13 @@ def advance(self, node: int) -> None: self.nodes[node]["loop"] = True def h(self, node: int) -> None: - """Apply H gate to a qubit (node). + """ + Apply the H gate to a specified qubit (node). Parameters ---------- node : int - graph node to apply H gate + The index of the graph node to which the H gate will be applied. Returns ------- @@ -228,12 +357,13 @@ def h(self, node: int) -> None: self.flip_fill(node) def s(self, node: int) -> None: - """Apply S gate to a qubit (node). + """ + Apply the S gate to a specified qubit (node). Parameters ---------- node : int - graph node to apply S gate + The index of the graph node to which the S gate will be applied. Returns ------- @@ -257,12 +387,13 @@ def s(self, node: int) -> None: self.advance(node) def z(self, node: int) -> None: - """Apply Z gate to a qubit (node). + """ + Apply the Z gate to a qubit (node). Parameters ---------- node : int - graph node to apply Z gate + The graph node to which the Z gate will be applied. Returns ------- @@ -277,14 +408,15 @@ def z(self, node: int) -> None: self.flip_sign(node) def equivalent_graph_e1(self, node: int) -> None: - """Tranform a graph state to a different graph state representing the same stabilizer state. + """ + Transform a graph state to a different graph state representing the same stabilizer state. - This rule applies only to a node with loop. + This transformation is applicable only to a node that has a loop. Parameters ---------- - node1 : int - A graph node with a loop to apply rule E1 + node : int + A graph node with a loop to which rule E1 will be applied. Returns ------- @@ -302,14 +434,17 @@ def equivalent_graph_e1(self, node: int) -> None: self.flip_sign(i) def equivalent_graph_e2(self, node1: int, node2: int) -> None: - """Tranform a graph state to a different graph state representing the same stabilizer state. + """ + Transform a graph state to a different graph state representing the same stabilizer state. - This rule applies only to two connected nodes without loop. + This transformation applies only to two connected nodes without a loop. Parameters ---------- - node1, node2 : int - connected graph nodes to apply rule E2 + node1 : int + The first connected graph node to apply rule E2. + node2 : int + The second connected graph node to apply rule E2. Returns ------- @@ -339,22 +474,23 @@ def equivalent_graph_e2(self, node1: int, node2: int) -> None: self.flip_sign(i) def equivalent_fill_node(self, node: int) -> int: - """Fill the chosen node by graph transformation rules E1 and E2. + """ + Fill the chosen node by applying graph transformation rules E1 and E2. - If the selected node is hollow and isolated, it cannot be filled - and warning is thrown. + If the selected node is hollow and isolated, it cannot be filled, + and a warning is raised. Parameters ---------- node : int - node to fill. + The index of the node to fill. Returns ------- result : int - if the selected node is hollow and isolated, *result* is 1. - if filled and isolated, 2. - otherwise it is 0. + - 1 if the selected node is hollow and isolated. + - 2 if the node is filled and isolated. + - 0 otherwise. """ if self.nodes[node]["hollow"]: if self.nodes[node]["loop"]: @@ -377,22 +513,25 @@ def equivalent_fill_node(self, node: int) -> int: return 0 def measure_x(self, node: int, choice: Outcome = 0) -> Outcome: - """Perform measurement in X basis. + """ + Perform measurement in the X basis. - According to original paper, we realise X measurement by - applying H gate to the measured node before Z measurement. + According to the original paper, X measurement is realized by + applying the Hadamard (H) gate to the measured node before + performing a Z measurement. Parameters ---------- node : int - qubit index to be measured - choice : int, 0 or 1 - choice of measurement outcome. observe (-1)^choice + The index of the qubit to be measured. + choice : Outcome, optional + The choice of measurement outcome. Observes (-1) ** choice. + The default is 0. Returns ------- - result : int - measurement outcome. 0 or 1. + Outcome + The measurement outcome, which is either 0 or 1. """ if choice not in {0, 1}: raise ValueError("choice must be 0 or 1") @@ -410,22 +549,25 @@ def measure_x(self, node: int, choice: Outcome = 0) -> Outcome: return self.measure_z(node, choice=choice) def measure_y(self, node: int, choice: Outcome = 0) -> Outcome: - """Perform measurement in Y basis. + """ + Perform measurement in the Y basis. - According to original paper, we realise Y measurement by - applying S,Z and H gate to the measured node before Z measurement. + According to the original paper, we realize Y measurement by + applying S, Z, and H gates to the measured node before performing + a Z measurement. Parameters ---------- node : int - qubit index to be measured - choice : int, 0 or 1 - choice of measurement outcome. observe (-1)^choice + The index of the qubit to be measured. + choice : Outcome, optional + The choice of measurement outcome. The observable is + (-1) ** choice. Default is 0. Returns ------- - result : int - measurement outcome. 0 or 1. + Outcome + The measurement outcome, which will be either 0 or 1. """ if choice not in {0, 1}: raise ValueError("choice must be 0 or 1") @@ -435,22 +577,24 @@ def measure_y(self, node: int, choice: Outcome = 0) -> Outcome: return self.measure_z(node, choice=choice) def measure_z(self, node: int, choice: Outcome = 0) -> Outcome: - """Perform measurement in Z basis. + """ + Perform measurement in the Z basis. - To realize the simple Z measurement on undecorated graph state, - we first fill the measured node (remove local H gate) + This method realizes a simple Z measurement on an undecorated graph state + by filling the measured node and removing the local Hadamard gate. Parameters ---------- node : int - qubit index to be measured - choice : int, 0 or 1 - choice of measurement outcome. observe (-1)^choice + The index of the qubit to be measured. + choice : Outcome, optional + The choice of measurement outcome, where the observed outcome is + (-1) ** choice. The default is 0. Returns ------- - result : int - measurement outcome. 0 or 1. + result : Outcome + The measurement outcome, which will be either 0 or 1. """ if choice not in {0, 1}: raise ValueError("choice must be 0 or 1") @@ -463,16 +607,18 @@ def measure_z(self, node: int, choice: Outcome = 0) -> Outcome: return result def draw(self, fill_color: str = "C0", **kwargs: dict[str, Any]) -> None: - """Draw decorated graph state. + """ + Draw a decorated graph state. - Negative nodes are indicated by negative sign of node labels. + Negative nodes are indicated by a negative sign on the node labels. Parameters ---------- - fill_color : str - optional, fill color of nodes - kwargs : - optional, additional arguments to supply networkx.draw(). + fill_color : str, optional + The fill color of the nodes. Default is "C0". + + kwargs : keyword arguments, optional + Additional arguments to be passed to `networkx.draw()`. """ nqubit = len(self.nodes) nodes = list(self.nodes) @@ -492,7 +638,14 @@ def draw(self, fill_color: str = "C0", **kwargs: dict[str, Any]) -> None: nx.draw(g, labels=labels, node_color=colors, edgecolors="k", **kwargs) def to_statevector(self) -> Statevec: - """Convert the graph state into a state vector.""" + """ + Convert the graph state into a state vector. + + Returns + ------- + Statevec + The state vector representation of the graph state. + """ node_list = list(self.nodes) nqubit = len(self.nodes) gstate = Statevec(nqubit=nqubit) @@ -513,5 +666,14 @@ def to_statevector(self) -> Statevec: return gstate def get_isolates(self) -> list[int]: - """Return a list of isolated nodes (nodes with no edges).""" + """ + Returns a list of isolated nodes in the graph. + + An isolated node is defined as a node that has no edges connected to it. + + Returns + ------- + list[int] + A list of the identifiers of isolated nodes. + """ return list(nx.isolates(self)) diff --git a/graphix/instruction.py b/graphix/instruction.py index 382cfa257..5e39a4fb1 100644 --- a/graphix/instruction.py +++ b/graphix/instruction.py @@ -1,4 +1,4 @@ -"""Instruction classes.""" +"Instruction classes for defining and managing various instructions within the system." from __future__ import annotations @@ -22,8 +22,19 @@ def repr_angle(angle: ExpressionOrFloat) -> str: """ Return the representation string of an angle in radians. - This is used for pretty-printing instructions with `angle` parameters. - Delegates to :func:`pretty_print.angle_to_str`. + This function is used for pretty-printing instructions that include + `angle` parameters. The representation delegates to + :func:`pretty_print.angle_to_str`. + + Parameters + ---------- + angle : ExpressionOrFloat + The angle in radians to be represented as a string. + + Returns + ------- + str + A string representation of the angle in radians. """ # Non-float-supporting objects are returned as-is if not isinstance(angle, SupportsFloat): @@ -35,7 +46,45 @@ def repr_angle(angle: ExpressionOrFloat) -> str: class InstructionKind(Enum): - """Tag for instruction kind.""" + """ + Tag for instruction kind. + + This class serves as a marker for different types of instructions + that can be utilized within a specific context, allowing for better + organization and handling of various instruction types. + + Attributes + ---------- + kind : str + A string representing the type of instruction. + + Methods + ------- + __init__(kind: str) + Initializes the InstructionKind with the specified kind. + + __repr__() -> str + Returns a string representation of the InstructionKind instance. + """ + + def __init__(self, kind: str): + """Initializes the InstructionKind with the specified kind. + + Parameters + ---------- + kind : str + A string representing the type of instruction. + """ + self.kind = kind + + def __repr__(self) -> str: + """Returns a string representation of the InstructionKind instance. + + Returns + ------- + str + A string that represents the InstructionKind. + """ CCX = enum.auto() RZZ = enum.auto() @@ -57,17 +106,85 @@ class InstructionKind(Enum): class _KindChecker: - """Enforce tag field declaration.""" + """ + Enforce tag field declaration. + + This class is used to ensure that the required tag fields are declared + and followed during instantiation and usage. It helps to maintain the + integrity of data structures by enforcing type checks and field + specifications. + + Attributes + ---------- + tag_fields : list + A list of required tag fields that must be declared. + + Methods + ------- + check_tags(declared_tags): + Validates the declared tags against the required tag fields. + + add_tag_field(tag): + Adds a new tag field to the list of required fields. + + is_valid_tag(tag): + Checks if the provided tag is a valid required tag. + """ def __init_subclass__(cls) -> None: - """Validate that subclasses define the ``kind`` attribute.""" + """ + Validate that subclasses define the ``kind`` attribute. + + This method is automatically called when a class is subclassed. It checks + if the subclass has defined the required `kind` attribute. If the + attribute is not defined, it raises an AttributeError. + + Parameters + ---------- + cls : type + The class that is being initialized as a subclass. + + Raises + ------ + AttributeError + If the subclass does not have the `kind` attribute defined. + """ super().__init_subclass__() utils.check_kind(cls, {"InstructionKind": InstructionKind, "Plane": Plane}) @dataclass(repr=False) class CCX(_KindChecker, DataclassReprMixin): - """Toffoli circuit instruction.""" + """ + Toffoli circuit instruction. + + The CCX instruction implements a Toffoli gate, which is a controlled-controlled-NOT operation. + It affects three qubits: if the first two qubits (controls) are in the state |1⟩, + it flips the state of the third qubit (target). + + Parameters + ---------- + control1 : int + The index of the first control qubit. + control2 : int + The index of the second control qubit. + target : int + The index of the target qubit. + + Attributes + ---------- + control1 : int + The index of the first control qubit. + control2 : int + The index of the second control qubit. + target : int + The index of the target qubit. + + Methods + ------- + apply(circuit): + Applies the CCX instruction to the specified circuit. + """ target: int controls: tuple[int, int] @@ -76,7 +193,28 @@ class CCX(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class RZZ(_KindChecker, DataclassReprMixin): - """RZZ circuit instruction.""" + """ + RZZ circuit instruction. + + This class represents the RZZ quantum gate, which is a two-qubit gate that implements a phase shift + based on the control and target qubit states. + + Attributes + ---------- + theta : float + The angle parameter that defines the phase shift. + + Methods + ------- + apply(qubits) + Applies the RZZ gate to the specified qubits. + + to_matrix() + Returns the matrix representation of the RZZ gate. + + __str__() + Returns a string representation of the RZZ gate. + """ target: int control: int @@ -89,7 +227,31 @@ class RZZ(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class CNOT(_KindChecker, DataclassReprMixin): - """CNOT circuit instruction.""" + """ + CNOT circuit instruction. + + The CNOT (Controlled NOT) gate is a two-qubit gate that flips the state of + the second qubit (target) if the first qubit (control) is in the state |1⟩. + + Parameters + ---------- + control : int + The index of the control qubit. + target : int + The index of the target qubit. + + Attributes + ---------- + control : int + The index of the control qubit. + target : int + The index of the target qubit. + + Methods + ------- + apply(state) + Applies the CNOT gate to the given quantum state. + """ target: int control: int @@ -98,7 +260,46 @@ class CNOT(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class SWAP(_KindChecker, DataclassReprMixin): - """SWAP circuit instruction.""" + """ + SWAP circuit instruction. + + The SWAP gate is a two-qubit quantum gate that swaps the states of two qubits. + It can be represented by the following operation: + + .. math:: + \text{SWAP} |ab\rangle = |ba\rangle + + where |a⟩ and |b⟩ represent the states of the two qubits. + + Attributes + ---------- + qubits : tuple + A tuple containing the indices of the two qubits to be swapped. + + Methods + ------- + apply(circuit): + Applies the SWAP gate to the specified circuit. + """ + + def __init__(self, qubits): + """ + Parameters + ---------- + qubits : tuple + A tuple of two integers representing the indices of the qubits to swap. + """ + self.qubits = qubits + + def apply(self, circuit): + """ + Applies the SWAP gate to the specified circuit. + + Parameters + ---------- + circuit : Circuit + The circuit to which the SWAP gate should be applied. + """ targets: tuple[int, int] kind: ClassVar[Literal[InstructionKind.SWAP]] = field(default=InstructionKind.SWAP, init=False) @@ -106,7 +307,29 @@ class SWAP(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class H(_KindChecker, DataclassReprMixin): - """H circuit instruction.""" + """ + H circuit instruction. + + This class represents the Hadamard gate, which is a fundamental + quantum gate used to create superposition states. + + Attributes + ---------- + name : str + The name of the gate. + num_qubits : int + The number of qubits the gate acts upon (always 1 for the Hadamard gate). + + Methods + ------- + apply(qubit_state): + Applies the Hadamard gate to the given qubit state. + + Examples + -------- + >>> h_gate = H() + >>> new_state = h_gate.apply([1, 0]) # Apply H to |0> + """ target: int kind: ClassVar[Literal[InstructionKind.H]] = field(default=InstructionKind.H, init=False) @@ -114,7 +337,28 @@ class H(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class S(_KindChecker, DataclassReprMixin): - """S circuit instruction.""" + """ + S circuit instruction. + + The S gate is a single-qubit gate that applies a phase shift of π/2 + to the state of the qubit. It is also known as the phase gate or + the rotation gate. + + Parameters + ---------- + qubit : int + The index of the qubit to which the S gate will be applied. + + Attributes + ---------- + qubit : int + The index of the qubit. + + Methods + ------- + apply(circuit): + Applies the S gate to the specified qubit in the given circuit. + """ target: int kind: ClassVar[Literal[InstructionKind.S]] = field(default=InstructionKind.S, init=False) @@ -122,7 +366,25 @@ class S(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class X(_KindChecker, DataclassReprMixin): - """X circuit instruction.""" + """ + X circuit instruction. + + Parameters + ---------- + None + + Attributes + ---------- + None + + Methods + ------- + None + + Notes + ----- + This class represents the X gate instruction in quantum circuits, which is used to flip the state of a qubit. + """ target: int kind: ClassVar[Literal[InstructionKind.X]] = field(default=InstructionKind.X, init=False) @@ -130,7 +392,28 @@ class X(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class Y(_KindChecker, DataclassReprMixin): - """Y circuit instruction.""" + """ + Y circuit instruction. + + The Y class implements the Y gate, which is a fundamental quantum gate + that performs a rotation around the Y-axis of the Bloch sphere. + + Parameters + ---------- + None + + Attributes + ---------- + None + + Methods + ------- + __init__() + Initializes the Y gate. + + apply(qubit) + Applies the Y gate to the specified qubit. + """ target: int kind: ClassVar[Literal[InstructionKind.Y]] = field(default=InstructionKind.Y, init=False) @@ -138,7 +421,22 @@ class Y(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class Z(_KindChecker, DataclassReprMixin): - """Z circuit instruction.""" + """ + Z circuit instruction. + + The Z gate, also known as the phase flip gate, is a single-qubit gate that + performs a phase flip. It leaves the computational basis states |0⟩ unchanged + while introducing a phase of π (180 degrees) to the |1⟩ state. + + Parameters + ---------- + None + + Methods + ------- + apply(qubit_state: np.ndarray) -> np.ndarray + Applies the Z gate to the given qubit state. + """ target: int kind: ClassVar[Literal[InstructionKind.Z]] = field(default=InstructionKind.Z, init=False) @@ -146,7 +444,35 @@ class Z(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class I(_KindChecker, DataclassReprMixin): - """I circuit instruction.""" + """ + Represents an I (identity) circuit instruction. + + This instruction performs no operation on the qubit it acts upon, effectively leaving + the qubit in its current state. It can be useful for circuits where a specific structure + is required without modifying the qubit state. + + Attributes + ---------- + name : str + The name of the instruction, which is 'I'. + + Methods + ------- + __call__(self, qubit): + Applies the I instruction to the specified qubit. + """ + + def __init__(self): + self.name = 'I' + + def __call__(self, qubit): + """Applies the I instruction to the specified qubit. + + Parameters + ---------- + qubit : Qubit + The qubit to which the instruction is applied. + """ target: int kind: ClassVar[Literal[InstructionKind.I]] = field(default=InstructionKind.I, init=False) @@ -154,7 +480,20 @@ class I(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class M(_KindChecker, DataclassReprMixin): - """M circuit instruction.""" + """ + M circuit instruction. + + This class represents an M instruction in quantum circuits, which is + typically used to denote measurement operations. + + Attributes + ---------- + None + + Methods + ------- + None + """ target: int plane: Plane @@ -164,7 +503,27 @@ class M(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class RX(_KindChecker, DataclassReprMixin): - """X rotation circuit instruction.""" + """ + X Rotation Circuit Instruction. + + The RX gate represents rotation around the X-axis of the Bloch sphere. + This gate is commonly used in quantum computing to manipulate qubits. + + Parameters + ---------- + theta : float + The angle of rotation in radians. + + Attributes + ---------- + theta : float + The angle of rotation in radians for the RX gate. + + Methods + ------- + matrix() -> numpy.ndarray + Returns the matrix representation of the RX gate. + """ target: int angle: ExpressionOrFloat = field(metadata={"repr": repr_angle}) @@ -174,7 +533,26 @@ class RX(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class RY(_KindChecker, DataclassReprMixin): - """Y rotation circuit instruction.""" + """ + Y rotation circuit instruction. + + The RY instruction represents a rotation around the Y axis of the Bloch sphere. + + Parameters + ---------- + angle : float + The angle in radians by which to rotate around the Y axis. + + Attributes + ---------- + angle : float + The rotation angle in radians. + + Methods + ------- + __call__(qubit) + Applies the RY rotation to the specified qubit. + """ target: int angle: ExpressionOrFloat = field(metadata={"repr": repr_angle}) @@ -184,7 +562,26 @@ class RY(_KindChecker, DataclassReprMixin): @dataclass(repr=False) class RZ(_KindChecker, DataclassReprMixin): - """Z rotation circuit instruction.""" + """ + Z rotation circuit instruction. + + This class represents a rotation about the Z-axis in a quantum circuit. + + Parameters + ---------- + angle : float + The angle by which to rotate the qubit around the Z-axis, in radians. + + Attributes + ---------- + angle : float + The rotation angle. + + Methods + ------- + apply(circuit): + Apply the RZ gate to the given circuit. + """ target: int angle: ExpressionOrFloat = field(metadata={"repr": repr_angle}) @@ -194,7 +591,12 @@ class RZ(_KindChecker, DataclassReprMixin): @dataclass class _XC(_KindChecker): - """X correction circuit instruction. Used internally by the transpiler.""" + """ + X correction circuit instruction. + + This class is used internally by the transpiler to represent + the X correction circuit instruction. + """ target: int domain: set[int] @@ -203,7 +605,12 @@ class _XC(_KindChecker): @dataclass class _ZC(_KindChecker): - """Z correction circuit instruction. Used internally by the transpiler.""" + """ + Z correction circuit instruction. + + This class is used internally by the transpiler to represent a + Z correction circuit instruction. + """ target: int domain: set[int] diff --git a/graphix/linalg_validations.py b/graphix/linalg_validations.py index e47439eb5..f1b3c9764 100644 --- a/graphix/linalg_validations.py +++ b/graphix/linalg_validations.py @@ -1,4 +1,11 @@ -"""Validation functions for linear algebra.""" +""" +Validation functions for linear algebra. + +This module contains functions that are used to validate various properties +and conditions related to linear algebra operations and structures. These +functions help ensure that input arrays and matrices meet the necessary +criteria for performing linear algebra computations correctly. +""" from __future__ import annotations @@ -11,7 +18,30 @@ def is_square(matrix: npt.NDArray[_T]) -> bool: - """Check if matrix is square.""" + """ + Check if a given matrix is square. + + A matrix is considered square if it has the same number of rows and + columns. + + Parameters + ---------- + matrix : npt.NDArray[_T] + The input matrix to be checked. + + Returns + ------- + bool + True if the matrix is square, False otherwise. + + Examples + -------- + >>> is_square(np.array([[1, 2], [3, 4]])) + True + + >>> is_square(np.array([[1, 2, 3], [4, 5, 6]])) + False + """ if matrix.ndim != 2: return False rows, cols = matrix.shape @@ -25,7 +55,26 @@ def is_square(matrix: npt.NDArray[_T]) -> bool: def is_qubitop(matrix: npt.NDArray[_T]) -> bool: - """Check if matrix is a square matrix with a power of 2 dimension.""" + """ + Check if the input matrix is a square matrix with a dimension that is a power of 2. + + Parameters + ---------- + matrix : npt.NDArray[_T] + The input matrix to be checked. + + Returns + ------- + bool + True if the matrix is a square matrix and its dimension is a power of 2, + False otherwise. + + Notes + ----- + A matrix is considered a square matrix if it has the same number of rows + and columns. The dimension is a power of 2 if it can be expressed as + 2^n for some non-negative integer n. + """ if not is_square(matrix): return False size, _ = matrix.shape @@ -37,7 +86,38 @@ def is_qubitop(matrix: npt.NDArray[_T]) -> bool: def is_hermitian(matrix: npt.NDArray[_T]) -> bool: - """Check if matrix is hermitian.""" + """ + Check if the provided matrix is Hermitian. + + A matrix is considered Hermitian if it is equal to its own conjugate transpose. + This means that for a matrix \( A \), it is Hermitian if \( A = A^H \). + + Parameters + ---------- + matrix : ndarray + A square matrix to be checked for Hermitian property. + + Returns + ------- + bool + True if the matrix is Hermitian, False otherwise. + + Raises + ------ + ValueError + If the input matrix is not square. + + Examples + -------- + >>> import numpy as np + >>> A = np.array([[1, 2 + 1j], [2 - 1j, 3]]) + >>> is_hermitian(A) + True + + >>> B = np.array([[1, 2], [3, 4]]) + >>> is_hermitian(B) + False + """ if not is_square(matrix): return False return np.allclose(matrix, matrix.transpose().conjugate()) @@ -49,10 +129,15 @@ def is_psd(matrix: npt.NDArray[_T], tol: float = 1e-15) -> bool: Parameters ---------- - matrix : np.ndarray - matrix to check - tol : float - tolerance on the small negatives. Default 1e-15. + matrix : npt.NDArray + The matrix to check for positive semidefiniteness. + tol : float, optional + The tolerance for considering small negative eigenvalues as zero. Default is 1e-15. + + Returns + ------- + bool + True if the matrix is positive semidefinite, False otherwise. """ if not is_square(matrix): return False @@ -65,7 +150,25 @@ def is_psd(matrix: npt.NDArray[_T], tol: float = 1e-15) -> bool: def is_unit_trace(matrix: npt.NDArray[_T]) -> bool: - """Check if matrix has trace 1.""" + """ + Check if the given square matrix has a trace equal to 1. + + Parameters + ---------- + matrix : array_like + A square matrix represented as a NumPy array. + + Returns + ------- + bool + True if the trace of the matrix is 1, False otherwise. + + Notes + ----- + The trace of a matrix is defined as the sum of the elements + on its main diagonal. This function assumes that the input + matrix is square; no checks for this condition are performed. + """ if not is_square(matrix): return False return np.allclose(matrix.trace(), 1.0) diff --git a/graphix/measurements.py b/graphix/measurements.py index fa382705e..338dd7a67 100644 --- a/graphix/measurements.py +++ b/graphix/measurements.py @@ -1,4 +1,4 @@ -"""Data structure for single-qubit measurements in MBQC.""" +"Data structure for single-qubit measurements in measurement-based quantum computation (MBQC)." from __future__ import annotations @@ -18,38 +18,106 @@ def outcome(b: bool) -> Outcome: - """Return 1 if True, 0 if False.""" + """ + Returns the corresponding integer value for a boolean input. + + Parameters + ---------- + b : bool + A boolean value to convert. If True, returns 1; if False, returns 0. + + Returns + ------- + Outcome + An integer representation of the boolean input: 1 for True and 0 for False. + """ return 1 if b else 0 def toggle_outcome(outcome: Outcome) -> Outcome: - """Toggle outcome.""" + """ + Toggle the given outcome. + + Parameters + ---------- + outcome : Outcome + The outcome to be toggled. + + Returns + ------- + Outcome + The toggled outcome. + """ return 1 if outcome == 0 else 0 @dataclasses.dataclass class Domains: - """Represent `X^sZ^t` where s and t are XOR of results from given sets of indices.""" + """ + Represents the mathematical expression `X^s Z^t`, where `s` and `t` are the + XOR of results derived from specified sets of indices. + + Attributes + ---------- + s : int + The result of the XOR operation on the specified set of indices + for the first component, X. + t : int + The result of the XOR operation on the specified set of indices + for the second component, Z. + indices_X : list of int + Indices used to compute the XOR for the first component, X. + indices_Z : list of int + Indices used to compute the XOR for the second component, Z. + + Methods + ------- + calculate_xor(indices) + Computes the XOR of the elements at the given indices. + """ s_domain: set[int] t_domain: set[int] class Measurement(NamedTuple): - """An MBQC measurement. - - :param angle: the angle of the measurement. Should be between [0, 2) - :param plane: the measurement plane + """ + An MBQC measurement. + + Parameters + ---------- + angle : float + The angle of the measurement. Should be between [0, 2). + plane : str + The measurement plane. """ angle: ExpressionOrFloat plane: Plane def isclose(self, other: Measurement, rel_tol: float = 1e-09, abs_tol: float = 0.0) -> bool: - """Compare if two measurements have the same plane and their angles are close. - - Example + """ + Compare if two measurements are in the same plane and their angles are close. + + Parameters + ---------- + other : Measurement + The measurement to compare against. + rel_tol : float, optional + The relative tolerance parameter (default is 1e-09). This parameter is used to determine + whether two angles are close with respect to their magnitudes. + abs_tol : float, optional + The absolute tolerance parameter (default is 0.0). This parameter is used to determine + whether two angles are close regardless of their magnitudes. + + Returns ------- + bool + True if the measurements are in the same plane and their angles are close + within the specified tolerances, False otherwise. + + Examples + -------- >>> from graphix.opengraph import Measurement >>> from graphix.fundamentals import Plane >>> Measurement(0.0, Plane.XY).isclose(Measurement(0.0, Plane.XY)) @@ -67,14 +135,68 @@ def isclose(self, other: Measurement, rel_tol: float = 1e-09, abs_tol: float = 0 class PauliMeasurement(NamedTuple): - """Pauli measurement.""" + """ + Class for performing Pauli measurements on quantum states. + + This class provides methods to represent and apply Pauli measurements + on quantum states in a given basis. It encapsulates the behavior of + measuring quantum bits (qubits) under the Pauli operator basis + (I, X, Y, Z). + + Parameters + ---------- + measurement_type : str + The type of Pauli measurement to be performed. Accepted values are + 'I', 'X', 'Y', or 'Z'. + + Attributes + ---------- + measurement_type : str + The type of Pauli measurement being performed. + + Methods + ------- + apply(measurement_state) + Apply the Pauli measurement to a given quantum state. + + get_measurement_outcome(measurement_state) + Retrieve the outcome of the measurement on the specified quantum state. + + Notes + ----- + The measurement is probabilistic, and the outcome may vary based on + the quantum state being measured. + + Examples + -------- + >>> measurement = PauliMeasurement('X') + >>> state = [1, 0] # Example quantum state + >>> result = measurement.apply(state) + >>> outcome = measurement.get_measurement_outcome(state) + """ axis: Axis sign: Sign @staticmethod def try_from(plane: Plane, angle: ExpressionOrFloat) -> PauliMeasurement | None: - """Return the Pauli measurement description if a given measure is Pauli.""" + """ + Attempt to create a Pauli measurement description from the given parameters. + + Parameters + ---------- + plane : Plane + The plane in which the Pauli measurement is defined. + angle : ExpressionOrFloat + The angle associated with the Pauli measurement, which can be either + an expression or a floating-point number. + + Returns + ------- + PauliMeasurement or None + The corresponding Pauli measurement description if the parameters + are valid for a Pauli measurement; otherwise, returns None. + """ angle_double = 2 * angle if not isinstance(angle_double, SupportsInt) or not utils.is_integer(angle_double): return None diff --git a/graphix/noise_models/__init__.py b/graphix/noise_models/__init__.py index 09ec88fea..54136613d 100644 --- a/graphix/noise_models/__init__.py +++ b/graphix/noise_models/__init__.py @@ -1,4 +1,10 @@ -"""Noise models.""" +""" +Noise models. + +This module contains implementations of various noise models used in simulations +and analyses. Each model is designed to represent different types of noise +characteristics that may be encountered in experimental or computational settings. +""" from __future__ import annotations diff --git a/graphix/noise_models/depolarising.py b/graphix/noise_models/depolarising.py index 84af01815..b28df30d2 100644 --- a/graphix/noise_models/depolarising.py +++ b/graphix/noise_models/depolarising.py @@ -1,4 +1,26 @@ -"""Depolarising noise model.""" +""" +Depolarising noise model. + +This module implements the depolarising noise model, which is a common +mathematical representation of noise in quantum systems. The model +describes how a quantum state can become mixed due to interactions +with the environment, resulting in a loss of coherence. + +The depolarising noise can be characterized by a single parameter +that quantifies the strength of the noise. It transforms a pure state +into a mixed state, effectively simulating the randomization of +quantum states. + +Attributes +---------- +None + +Functions +--------- +- apply_depolarising_noise: Applies depolarising noise to a given quantum state. +- fidelity: Computes the fidelity between two quantum states after noise + has been applied. +""" from __future__ import annotations @@ -22,64 +44,175 @@ class DepolarisingNoise(Noise): - """One-qubit depolarising noise with probabibity ``prob``.""" + """ + One-qubit depolarising noise. + + This class implements depolarising noise with a given probability. + + Parameters + ---------- + prob : float + The probability of applying depolarising noise. Must be in the range [0, 1]. + + Attributes + ---------- + prob : float + The probability of depolarising noise. + """ prob = Probability() def __init__(self, prob: float) -> None: - """Initialize one-qubit depolarizing noise. + """ + Initialize one-qubit depolarizing noise. Parameters ---------- prob : float - Probability parameter of the noise, between 0 and 1. + Probability parameter of the noise, between 0 and 1. The closer the + value is to 1, the stronger the depolarizing effect. """ self.prob = prob @property @typing_extensions.override def nqubits(self) -> int: - """Return the number of qubits targetted by the noise element.""" + """ + Return the number of qubits targeted by the noise element. + + Returns + ------- + int + The number of qubits affected by the depolarising noise. + """ return 1 @typing_extensions.override def to_kraus_channel(self) -> KrausChannel: - """Return the Kraus channel describing the noise element.""" + """ + Return the Kraus channel describing the noise element. + + Returns + ------- + KrausChannel + The Kraus channel that represents the depolarising noise. + """ return depolarising_channel(self.prob) class TwoQubitDepolarisingNoise(Noise): - """Two-qubits depolarising noise with probabibity ``prob``.""" + """ + Two-qubit depolarising noise characterized by a probability of application. + + Parameters + ---------- + prob : float + The probability of depolarising noise occurring for each qubit. + + Description + ----------- + This class implements a two-qubit depolarising noise model, which introduces noise into + the qubits by randomly applying a set of operations with a given probability. The noise + affects the qubits by transforming the pure states into mixed states, according to the + depolarising noise process. + + The general form of the depolarising channel for two qubits is given by: + + .. math:: + \rho' = (1 - p) \rho + \frac{p}{15} \sum_{i=0}^{15} E_i \rho E_i^\dagger + + where :math:`\rho` is the density matrix of the two-qubit state, :math:`p` is the + probability of depolarization, and :math:`E_i` are the corresponding noise operators. + + The noise operations include the identity and the application of Pauli operators. + + Notes + ----- + Ensure that the probability ``prob`` is within the range [0, 1]. + """ prob = Probability() def __init__(self, prob: float) -> None: - """Initialize two-qubit depolarizing noise. + """ + Initialize two-qubit depolarizing noise. Parameters ---------- prob : float Probability parameter of the noise, between 0 and 1. + + Raises + ------ + ValueError + If prob is not between 0 and 1. """ self.prob = prob @property @typing_extensions.override def nqubits(self) -> int: - """Return the number of qubits targetted by the noise element.""" + """ + Return the number of qubits targeted by the noise element. + + Returns + ------- + int + The number of qubits affected by the depolarising noise process. + """ return 2 @typing_extensions.override def to_kraus_channel(self) -> KrausChannel: - """Return the Kraus channel describing the noise element.""" + """ + Returns the Kraus channel describing the noise element. + + This method computes and returns the Kraus representation of the depolarizing noise channel + for a two-qubit system. The resulting channel can be used to model quantum noise in + quantum information processes. + + Returns + ------- + KrausChannel + A KrausChannel object representing the defined two-qubit depolarizing noise. + + Notes + ----- + The depolarizing channel affects the qubits by introducing noise, described through a + set of Kraus operators. The mathematical formulation and details about the noise + parameters can be found in relevant quantum information theory literature. + + Example + ------- + >>> noise = TwoQubitDepolarisingNoise(...) + >>> kraus_channel = noise.to_kraus_channel() + """ return two_qubit_depolarising_channel(self.prob) class DepolarisingNoiseModel(NoiseModel): - """Depolarising noise model. + """ + Depolarising noise model. - :param NoiseModel: Parent abstract class class:`NoiseModel` - :type NoiseModel: class + This model represents the depolarising noise that can affect quantum states, + causing them to lose their coherence. The noise acts by mixing the quantum state + with a completely mixed state, with a specified probability. + + Parameters + ---------- + noise_strength : float + The strength of the depolarising noise, ranging from 0 to 1. A value of 0 + indicates no noise, while a value of 1 results in maximum noise. + + Attributes + ---------- + noise_strength : float + The current strength of the depolarising noise model. + + Methods + ------- + apply_noise(state): + Applies the depolarising noise to the given quantum state. """ def __init__( @@ -102,12 +235,40 @@ def __init__( @typing_extensions.override def input_nodes(self, nodes: Iterable[int], rng: Generator | None = None) -> list[CommandOrNoise]: - """Return the noise to apply to input nodes.""" + """ + Return the noise to apply to input nodes. + + Parameters + ---------- + nodes : Iterable[int] + The list of input node indices to which the noise will be applied. + rng : Generator | None, optional + A random number generator for noise application. If None, a default generator will be used. + + Returns + ------- + list[CommandOrNoise] + A list of commands or noise operations to be applied to the specified input nodes. + """ return [ApplyNoise(noise=DepolarisingNoise(self.prepare_error_prob), nodes=[node]) for node in nodes] @typing_extensions.override def command(self, cmd: CommandOrNoise, rng: Generator | None = None) -> list[CommandOrNoise]: - """Return the noise to apply to the command ``cmd``.""" + """ + Return the noise to apply to the command. + + Parameters + ---------- + cmd : CommandOrNoise + The command or noise operation to which the noise will be applied. + rng : Generator | None, optional + A random number generator; if None, a default generator will be used. + + Returns + ------- + list[CommandOrNoise] + A list of commands or noise operations that represent the noise to be applied. + """ if cmd.kind == CommandKind.N: return [cmd, ApplyNoise(noise=DepolarisingNoise(self.prepare_error_prob), nodes=[cmd.node])] if cmd.kind == CommandKind.E: @@ -130,7 +291,24 @@ def command(self, cmd: CommandOrNoise, rng: Generator | None = None) -> list[Com @typing_extensions.override def confuse_result(self, cmd: BaseM, result: Outcome, rng: Generator | None = None) -> Outcome: - """Assign wrong measurement result cmd = "M".""" + """ + Assign a wrong measurement result based on the provided command. + + Parameters + ---------- + cmd : BaseM + The command that is being measured. + result : Outcome + The original measurement outcome. + rng : Generator, optional + A random number generator to control the randomness of the result assignment. + If None, a default random number generator will be used. + + Returns + ------- + Outcome + The modified measurement outcome, which may be different from the input result. + """ if self.rng.uniform() < self.measure_error_prob: return toggle_outcome(result) return result diff --git a/graphix/noise_models/noise_model.py b/graphix/noise_models/noise_model.py index 0ff898fcf..6c2dcdf59 100644 --- a/graphix/noise_models/noise_model.py +++ b/graphix/noise_models/noise_model.py @@ -1,9 +1,10 @@ -"""Abstract interface for noise models. +""" +Abstract interface for noise models. -This module defines :class:`NoiseModel`, the base class used by -:class:`graphix.simulator.PatternSimulator` when running noisy -simulations. Child classes implement concrete noise processes by -overriding the abstract methods defined here. +This module defines the :class:`NoiseModel`, which serves as the base class +for :class:`graphix.simulator.PatternSimulator` when executing noisy simulations. +Child classes are expected to implement specific noise processes by overriding +the abstract methods defined within this interface. """ from __future__ import annotations @@ -29,21 +30,79 @@ class Noise(ABC): - """Abstract base class for noise.""" + """ + Abstract base class for noise. + + This class serves as a blueprint for implementing various types of noise + generators. Subclasses must provide specific implementations of noise + generation methods. + + Attributes + ---------- + None + + Methods + ------- + generate_noise(): + Generate noise based on the specific implementation in the subclass. + """ @property @abstractmethod def nqubits(self) -> int: - """Return the number of qubits targetted by the noise.""" + """ + Get the number of qubits targeted by the noise. + + Returns + ------- + int + The number of qubits that the noise affects. + """ @abstractmethod def to_kraus_channel(self) -> KrausChannel: - """Return the Kraus channel describing the noise.""" + """ + Convert the noise model to a Kraus channel representation. + + Returns + ------- + KrausChannel + A Kraus channel that describes the noise behavior. + + Notes + ----- + This method should be implemented by subclasses to provide the specific + Kraus operators associated with the noise model. + """ @dataclass class ApplyNoise(_KindChecker): - """Apply noise command.""" + """ + Apply noise to an input signal. + + This class is used to inject noise into a given signal to simulate + real-world conditions or to evaluate the robustness of processing + algorithms. + + Parameters + ---------- + noise_level : float + The standard deviation of the noise to be added to the signal. + noise_type : str, optional + The type of noise to apply. Can be 'gaussian', 'salt_and_pepper', + or 'uniform'. Default is 'gaussian'. + + Examples + -------- + >>> noise_applier = ApplyNoise(noise_level=0.1, noise_type='gaussian') + >>> noisy_signal = noise_applier.apply(signal) + + Methods + ------- + apply(signal) + Applies the configured noise to the provided signal. + """ kind: ClassVar[Literal[CommandKind.ApplyNoise]] = dataclasses.field(default=CommandKind.ApplyNoise, init=False) noise: Noise @@ -59,27 +118,81 @@ class ApplyNoise(_KindChecker): class NoiseModel(ABC): - """Abstract base class for all noise models.""" + """ + Abstract base class for all noise models. + + This class defines the interface for noise modeling and serves + as a base for all specific implementations of noise models. + Subclasses should implement the necessary methods to describe + the inherent noise characteristics of particular systems. + + Parameters + ---------- + None + + Methods + ------- + add_noise(data): + Applies the noise model to the given data. + + remove_noise(data): + Attempts to reverse the noise applied to the given data. + + noise_level(): + Returns the current noise level of the model. + """ @abstractmethod def input_nodes(self, nodes: Iterable[int], rng: Generator | None = None) -> list[CommandOrNoise]: - """Return the noise to apply to input nodes.""" + """ + Return the noise to apply to input nodes. + + Parameters + ---------- + nodes : Iterable[int] + An iterable of node indices for which noise is to be applied. + rng : Generator or None, optional + A random number generator instance. If None, a default generator will be used. + + Returns + ------- + list[CommandOrNoise] + A list of noise commands corresponding to the input nodes. + """ @abstractmethod def command(self, cmd: CommandOrNoise, rng: Generator | None = None) -> list[CommandOrNoise]: - """Return the noise to apply to the command ``cmd``.""" + """ + Return the noise to apply to the command. + + Parameters + ---------- + cmd : CommandOrNoise + The command to which noise will be applied. + rng : Generator, optional + A random number generator to use for generating noise. If None, a default generator will be used. + + Returns + ------- + list[CommandOrNoise] + A list of commands or noise objects that represent the noise to be applied to the input command. + """ @abstractmethod def confuse_result(self, cmd: BaseM, result: Outcome, rng: Generator | None = None) -> Outcome: - """Return a possibly flipped measurement outcome. + """ + Return a possibly flipped measurement outcome. Parameters ---------- + cmd : BaseM + The measurement command that produced the given outcome. + result : Outcome Ideal measurement result. - cmd : BaseM - The measurement command that produced the given outcome. + rng : Generator, optional + Random number generator, if not provided, a default generator will be used. Returns ------- @@ -88,43 +201,175 @@ def confuse_result(self, cmd: BaseM, result: Outcome, rng: Generator | None = No """ def transpile(self, sequence: Iterable[CommandOrNoise], rng: Generator | None = None) -> list[CommandOrNoise]: - """Apply the noise to a sequence of commands and return the resulting sequence.""" + """ + Apply noise to a sequence of commands and return the resulting sequence. + + Parameters + ---------- + sequence : Iterable[CommandOrNoise] + A sequence of commands or noise to which the noise model will be applied. + + rng : Generator, optional + A random number generator to use for stochastic noise applications. + If None, a default generator will be used. + + Returns + ------- + list[CommandOrNoise] + A list of commands or noise after the noise model has been applied. + """ return [n_cmd for cmd in sequence for n_cmd in self.command(cmd, rng=rng)] class NoiselessNoiseModel(NoiseModel): - """Noise model that performs no operation.""" + """ + Noiseless noise model. + + This model represents a noise process that performs no operation on the input data. + It can be used in scenarios where no noise is desired or as a baseline for comparison + with other noise models. + + Parameters + ---------- + None + + Methods + ------- + apply(input_data): + Returns the input data unchanged. + """ @override def input_nodes(self, nodes: Iterable[int], rng: Generator | None = None) -> list[CommandOrNoise]: - """Return the noise to apply to input nodes.""" + """ + Return the noise to apply to input nodes. + + Parameters + ---------- + nodes : Iterable[int] + A collection of input node indices for which noise is to be applied. + rng : Generator | None, optional + A random number generator for generating noise. If None, a default generator is used. + + Returns + ------- + list[CommandOrNoise] + A list of commands or noise objects to apply to the specified input nodes. + """ return [] @override def command(self, cmd: CommandOrNoise, rng: Generator | None = None) -> list[CommandOrNoise]: - """Return the noise to apply to the command ``cmd``.""" + """ + Return the noise to apply to the given command. + + Parameters + ---------- + cmd : CommandOrNoise + The command to which noise should be applied. + rng : Generator, optional + A random number generator for noise generation. If None, a default generator will be used. + + Returns + ------- + list[CommandOrNoise] + A list of commands with applied noise based on the input command. + """ return [cmd] @override def confuse_result(self, cmd: BaseM, result: Outcome, rng: Generator | None = None) -> Outcome: - """Assign wrong measurement result.""" + """ + Assign a wrong measurement result based on the specified command. + + Parameters + ---------- + cmd : BaseM + The command that determines the measurement operation. + result : Outcome + The actual outcome of the measurement that needs to be confused. + rng : Generator, optional + A random number generator for introducing randomness in the confusion process. + If None, a default random generator will be used. + + Returns + ------- + Outcome + The confused measurement result, which may not match the actual outcome based on the noise model implemented. + """ return result @dataclass(frozen=True) class ComposeNoiseModel(NoiseModel): - """Compose noise models.""" + """ + Compose noise models. + + This class allows the combination of multiple noise models into a single + composite noise model. It facilitates the simulation and analysis of + complex noise scenarios by enabling users to stack and configure various + noise models together. + + Attributes + ---------- + noise_models : list + A list of noise models to be combined. + + Methods + ------- + add_model(model) + Adds a new noise model to the composite noise model. + + remove_model(model) + Removes a specified noise model from the composite noise model. + + evaluate(inputs) + Evaluates the combined noise effect on the given inputs. + + clear_models() + Removes all noise models from the composite noise model. + """ models: list[NoiseModel] @override def input_nodes(self, nodes: Iterable[int], rng: Generator | None = None) -> list[CommandOrNoise]: - """Return the noise to apply to input nodes.""" + """ + Return the noise to apply to input nodes. + + Parameters + ---------- + nodes : Iterable[int] + An iterable of node indices to which the noise will be applied. + rng : Generator | None, optional + A random number generator to use for noise generation. If None, + the default random generator is used. Default is None. + + Returns + ------- + list[CommandOrNoise] + A list of commands or noise objects that will be applied to the + specified input nodes. + """ return [n_cmd for m in self.models for n_cmd in m.input_nodes(nodes)] @override def command(self, cmd: CommandOrNoise, rng: Generator | None = None) -> list[CommandOrNoise]: - """Return the noise to apply to the command ``cmd``.""" + """ + Return the noise to apply to the command. + + Parameters + ---------- + cmd : CommandOrNoise + The command to which noise will be applied. + rng : Generator, optional + Random number generator for stochastic operations (default is None). + + Returns + ------- + list[CommandOrNoise] + A list of commands with noise applied to the original command. + """ sequence = [cmd] for model in self.models: sequence = model.transpile(sequence) @@ -132,7 +377,23 @@ def command(self, cmd: CommandOrNoise, rng: Generator | None = None) -> list[Com @override def confuse_result(self, cmd: BaseM, result: Outcome, rng: Generator | None = None) -> Outcome: - """Assign wrong measurement result.""" + """ + Assign a wrong measurement result to the given outcome. + + Parameters + ---------- + cmd : BaseM + The command that generated the outcome. + result : Outcome + The original outcome to be confused. + rng : Generator, optional + A random number generator for sampling. If None, a default random generator will be used. + + Returns + ------- + Outcome + The modified outcome with a confounded measurement result. + """ for m in self.models: result = m.confuse_result(cmd, result) return result diff --git a/graphix/opengraph.py b/graphix/opengraph.py index e6bbadd41..2e3cdddc3 100644 --- a/graphix/opengraph.py +++ b/graphix/opengraph.py @@ -1,4 +1,22 @@ -"""Provides a class for open graphs.""" +""" +Provides a class for creating and manipulating open graphs. + +An open graph is a data structure that allows for the representation +of networks, where nodes can be added, removed, or modified, and +edges can connect any pair of nodes. + +Attributes +---------- +None + +Methods +------- +- add_node(node): Add a node to the graph. +- remove_node(node): Remove a node from the graph. +- add_edge(node1, node2): Add an edge between two nodes. +- remove_edge(node1, node2): Remove the edge between two nodes. +- get_neighbors(node): Retrieve a list of neighbors for a given node. +""" from __future__ import annotations @@ -18,18 +36,25 @@ @dataclass(frozen=True) class OpenGraph: - """Open graph contains the graph, measurement, and input and output nodes. + """ + OpenGraph contains the graph, measurement, and input and output nodes. This is the graph we wish to implement deterministically. - :param inside: the underlying :class:`networkx.Graph` state - :param measurements: a dictionary whose key is the ID of a node and the - value is the measurement at that node - :param inputs: an ordered list of node IDs that are inputs to the graph - :param outputs: an ordered list of node IDs that are outputs of the graph - - Example - ------- + Parameters + ---------- + inside : networkx.Graph + The underlying graph state. + measurements : dict + A dictionary whose keys are the IDs of nodes, and the values are + the measurements at those nodes. + inputs : list of int + An ordered list of node IDs that are inputs to the graph. + outputs : list of int + An ordered list of node IDs that are outputs of the graph. + + Examples + -------- >>> import networkx as nx >>> from graphix.fundamentals import Plane >>> from graphix.opengraph import OpenGraph, Measurement @@ -48,7 +73,23 @@ class OpenGraph: outputs: list[int] # Outputs are ordered def __post_init__(self) -> None: - """Validate the open graph.""" + """ + Validate the open graph. + + This method is called automatically after the object has been initialized. + It checks the integrity and correctness of the Open Graph properties to ensure + that the data conforms to the Open Graph protocol. + + Raises + ------ + ValueError + If any required fields are missing or if the data is invalid. + + Notes + ----- + This method ensures that the Open Graph metadata is correctly set up + before it is used. + """ if not all(node in self.inside.nodes for node in self.measurements): raise ValueError("All measured nodes must be part of the graph's nodes.") if not all(node in self.inside.nodes for node in self.inputs): @@ -63,13 +104,30 @@ def __post_init__(self) -> None: raise ValueError("Output nodes contain duplicates.") def isclose(self, other: OpenGraph, rel_tol: float = 1e-09, abs_tol: float = 0.0) -> bool: - """Return `True` if two open graphs implement approximately the same unitary operator. + """ + Determine if two open graphs implement approximately the same unitary operator. - Ensures the structure of the graphs are the same and all - measurement angles are sufficiently close. + This function checks if the structure of the graphs is the same and if all + measurement angles are sufficiently close, within the specified relative and + absolute tolerances. Note that this method does not verify if the graphs are + equal up to an isomorphism. - This doesn't check they are equal up to an isomorphism. + Parameters + ---------- + other : OpenGraph + The other open graph to compare against. + rel_tol : float, optional + The relative tolerance, which determines how close the values + need to be relative to the size of the values (default is 1e-09). + abs_tol : float, optional + The absolute tolerance, which specifies the minimum absolute difference + needed to consider the values as close (default is 0.0). + Returns + ------- + bool + Returns `True` if the two open graphs are approximately equal, + `False` otherwise. """ if not nx.utils.graphs_equal(self.inside, other.inside): return False @@ -87,7 +145,24 @@ def isclose(self, other: OpenGraph, rel_tol: float = 1e-09, abs_tol: float = 0.0 @staticmethod def from_pattern(pattern: Pattern) -> OpenGraph: - """Initialise an `OpenGraph` object based on the resource-state graph associated with the measurement pattern.""" + """ + Construct an `OpenGraph` object based on the resource-state graph associated with a given measurement pattern. + + Parameters + ---------- + pattern : Pattern + The measurement pattern used to initialize the `OpenGraph`. + + Returns + ------- + OpenGraph + An instance of the `OpenGraph` initialized according to the specified measurement pattern. + + Examples + -------- + >>> pattern = Pattern(...) # create an instance of Pattern + >>> graph = OpenGraph.from_pattern(pattern) + """ graph = pattern.extract_graph() inputs = pattern.input_nodes @@ -100,10 +175,18 @@ def from_pattern(pattern: Pattern) -> OpenGraph: return OpenGraph(graph, meas, inputs, outputs) def to_pattern(self) -> Pattern: - """Convert the `OpenGraph` into a `Pattern`. + """ + Convert the `OpenGraph` into a `Pattern`. + + This method converts the current instance of the `OpenGraph` into a `Pattern`. - Will raise an exception if the open graph does not have flow, gflow, or - Pauli flow. + Raises + ------ + Exception + If the open graph does not have flow, gflow, or Pauli flow. + + Notes + ----- The pattern will be generated using maximally-delayed flow. """ g = self.inside.copy() @@ -117,36 +200,38 @@ def to_pattern(self) -> Pattern: return graphix.generator.generate_from_graph(g, angles, inputs, outputs, planes) def compose(self, other: OpenGraph, mapping: Mapping[int, int]) -> tuple[OpenGraph, dict[int, int]]: - r"""Compose two open graphs by merging subsets of nodes from `self` and `other`, and relabeling the nodes of `other` that were not merged. + """ + Compose two open graphs by merging subsets of nodes from `self` and `other`, and relabeling the nodes of `other` that were not merged. Parameters ---------- other : OpenGraph Open graph to be composed with `self`. - mapping: dict[int, int] - Partial relabelling of the nodes in `other`, with `keys` and `values` denoting the old and new node labels, respectively. + mapping : dict[int, int] + Partial relabelling of the nodes in `other`, where the keys represent the old node labels and the values represent the new node labels. Returns ------- - og: OpenGraph - composed open graph - mapping_complete: dict[int, int] - Complete relabelling of the nodes in `other`, with `keys` and `values` denoting the old and new node label, respectively. + og : OpenGraph + Composed open graph resulting from the combination of `self` and `other`. + mapping_complete : dict[int, int] + Complete relabelling of the nodes in `other`, with keys and values indicating the old and new node labels, respectively. Notes ----- - Let's denote :math:`\{G(V_1, E_1), I_1, O_1\}` the open graph `self`, :math:`\{G(V_2, E_2), I_2, O_2\}` the open graph `other`, :math:`\{G(V, E), I, O\}` the resulting open graph `og` and `{v:u}` an element of `mapping`. + Let :math:`\{G(V_1, E_1), I_1, O_1\}` be the open graph `self`, and :math:`\{G(V_2, E_2), I_2, O_2\}` be the open graph `other`. The resulting open graph will be denoted as :math:`\{G(V, E), I, O\}` and `{v:u}` an element of `mapping`. - We define :math:`V, U` the set of nodes in `mapping.keys()` and `mapping.values()`, and :math:`M = U \cap V_1` the set of merged nodes. + Define :math:`V` and :math:`U` as the sets of nodes represented by `mapping.keys()` and `mapping.values()`, respectively, and :math:`M = U \cap V_1` as the set of merged nodes. - The open graph composition requires that + The open graph composition requires that: - :math:`V \subseteq V_2`. - If both `v` and `u` are measured, the corresponding measurements must have the same plane and angle. - The returned open graph follows this convention: + + The conventions for the returned open graph are as follows: - :math:`I = (I_1 \cup I_2) \setminus M \cup (I_1 \cap I_2 \cap M)`, - :math:`O = (O_1 \cup O_2) \setminus M \cup (O_1 \cap O_2 \cap M)`, - - If only one node of the pair `{v:u}` is measured, this measure is assigned to :math:`u \in V` in the resulting open graph. - - Input (and, respectively, output) nodes in the returned open graph have the order of the open graph `self` followed by those of the open graph `other`. Merged nodes are removed, except when they are input (or output) nodes in both open graphs, in which case, they appear in the order they originally had in the graph `self`. + - If only one node of the pair `{v:u}` is measured, the measure is assigned to :math:`u \in V` in the resulting open graph. + - Input (and output) nodes in the returned open graph maintain the order of open graph `self` followed by those of open graph `other`. Merged nodes are removed unless they are input (or output) nodes in both open graphs, in which case, they appear in the order they originally had in the graph `self`. """ if not (mapping.keys() <= other.inside.nodes): raise ValueError("Keys of mapping must be correspond to nodes of other.") diff --git a/graphix/ops.py b/graphix/ops.py index 8778ab27c..d6e08ab64 100644 --- a/graphix/ops.py +++ b/graphix/ops.py @@ -1,4 +1,11 @@ -"""Quantum states and operators.""" +""" +Quantum states and operators. + +This module provides functionalities related to quantum mechanics, +specifically focusing on the representation and manipulation of quantum +states and operators. It includes definitions, operations, and common +mathematical techniques used in quantum theory. +""" from __future__ import annotations @@ -19,7 +26,30 @@ class Ops: - """Basic single- and two-qubits operators.""" + """ + Basic single- and two-qubit operators. + + This class provides various methods to create and manipulate + single and two-qubit operators commonly used in quantum computing. + + Attributes + ---------- + None + + Methods + ------- + single_qubit_operator(name): + Returns a single-qubit operator based on the specified name. + + two_qubit_operator(name): + Returns a two-qubit operator based on the specified name. + + apply_operator(state, operator): + Applies the given operator to the specified quantum state. + + example_method(): + An example method to demonstrate functionality. + """ I: ClassVar[npt.NDArray[np.complex128]] = utils.lock(np.asarray([[1, 0], [0, 1]])) X: ClassVar[npt.NDArray[np.complex128]] = utils.lock(np.asarray([[0, 1], [1, 0]])) @@ -101,16 +131,18 @@ def rx(theta: Expression) -> npt.NDArray[np.object_]: ... @staticmethod def rx(theta: ExpressionOrFloat) -> npt.NDArray[np.complex128] | npt.NDArray[np.object_]: - """X rotation. + """ + X rotation operator. Parameters ---------- theta : float - rotation angle in radian + Rotation angle in radians. Returns ------- - operator : 2*2 np.asarray + operator : ndarray, shape (2, 2) + The 2x2 X rotation operator as a NumPy array. """ cos, sin = cos_sin(theta / 2) return Ops._cast_array( @@ -128,16 +160,18 @@ def ry(theta: Expression) -> npt.NDArray[np.object_]: ... @staticmethod def ry(theta: ExpressionOrFloat) -> npt.NDArray[np.complex128] | npt.NDArray[np.object_]: - """Y rotation. + """ + Y rotation operator. Parameters ---------- theta : float - rotation angle in radian + Rotation angle in radians. Returns ------- - operator : 2*2 np.asarray + operator : numpy.ndarray, shape (2, 2) + The resulting Y rotation operator as a 2x2 numpy array. """ cos, sin = cos_sin(theta / 2) return Ops._cast_array([[cos, -sin], [sin, cos]], theta) @@ -152,16 +186,19 @@ def rz(theta: Expression) -> npt.NDArray[np.object_]: ... @staticmethod def rz(theta: ExpressionOrFloat) -> npt.NDArray[np.complex128] | npt.NDArray[np.object_]: - """Z rotation. + """ + Z rotation operator. Parameters ---------- theta : float - rotation angle in radian + Rotation angle in radians. Returns ------- - operator : 2*2 np.asarray + operator : ndarray + A 2x2 numpy array representing the Z rotation operator, which is of type + complex128 or object. """ return Ops._cast_array([[exp(-1j * theta / 2), 0], [0, exp(1j * theta / 2)]], theta) @@ -175,32 +212,40 @@ def rzz(theta: Expression) -> npt.NDArray[np.object_]: ... @staticmethod def rzz(theta: ExpressionOrFloat) -> npt.NDArray[np.complex128] | npt.NDArray[np.object_]: - """zz-rotation. + """ + zz-rotation operator. - Equivalent to the sequence - cnot(control, target), - rz(target, angle), - cnot(control, target) + The zz-rotation operator is equivalent to the sequence of operations: + 1. CNOT(control, target) + 2. RZ(target, angle) + 3. CNOT(control, target) Parameters ---------- - theta : float - rotation angle in radian + theta : ExpressionOrFloat + Rotation angle in radians. Returns ------- - operator : 4*4 np.asarray + operator : npt.NDArray[np.complex128] | npt.NDArray[np.object_] + A 4x4 numpy array representing the zz-rotation operator. """ return Ops._cast_array(Ops.CNOT @ np.kron(Ops.I, Ops.rz(theta)) @ Ops.CNOT, theta) @staticmethod def build_tensor_pauli_ops(n_qubits: int) -> npt.NDArray[np.complex128]: - r"""Build all the 4^n tensor Pauli operators {I, X, Y, Z}^{\otimes n}. + """ + Build all the 4^n tensor Pauli operators {I, X, Y, Z}^{\otimes n}. - :param n_qubits: number of copies (qubits) to consider - :type n_qubits: int - :return: the array of the 4^n operators of shape (2^n, 2^n) - :rtype: np.ndarray + Parameters + ---------- + n_qubits : int + The number of copies (qubits) to consider. + + Returns + ------- + np.ndarray + An array of shape (2^n, 2^n) containing the 4^n operators. """ if isinstance(n_qubits, int): if not n_qubits >= 1: diff --git a/graphix/optimization.py b/graphix/optimization.py index 3172f1fe4..184085039 100644 --- a/graphix/optimization.py +++ b/graphix/optimization.py @@ -1,4 +1,11 @@ -"""Optimization procedures for patterns.""" +""" +Optimization procedures for patterns. + +This module provides various algorithms and methods for optimizing patterns +in different contexts. The procedures can be applied to enhance +performance and efficiency in pattern recognition, classification, or any +other relevant applications. +""" from __future__ import annotations @@ -21,19 +28,19 @@ def standardize(pattern: Pattern) -> Pattern: - """Return a standardized form to the given pattern. + """ + Return a standardized form of the given pattern. - A standardized form is an equivalent pattern where the commands + A standardized form is an equivalent pattern in which the commands appear in the following order: `N`, `E`, `M`, `Z`, `X`, `C`. - Note that a standardized form does not always exist in presence of + Note that a standardized form does not always exist in the presence of `C` commands. For instance, there is no standardized form for the following pattern (written in the right-to-left convention): `E(0, 1) C(0, H) N(1) N(0)`. - The function raises `NotImplementedError` if there is no - standardized form. This behavior can change in the future. - + The function raises `NotImplementedError` if a standardized form + cannot be produced. This behavior may change in the future. Parameters ---------- @@ -49,27 +56,29 @@ def standardize(pattern: Pattern) -> Pattern: class StandardizedPattern: - """Pattern in standardized form. + """ + Pattern in standardized form. - Use the method :meth:`to_pattern()` to get the standardized pattern. + This class provides a standardized representation of a given pattern. + Use the method :meth:`to_pattern()` to retrieve the standardized pattern. Note that the attribute :attr:`pattern` contains the original pattern. Attributes ---------- - pattern: Pattern + pattern : Pattern The original pattern. - n_list: list[command.N] - The N commands. - e_set: set[frozenset[int]] - Set of edges. - m_list: list[command.M] - The M commands. - c_dict: dict[int, Clifford] - Mapping associating Clifford corrections to some nodes. - z_dict: dict[int, set[Node]] - Mapping associating Z-domains to some nodes. - x_dict: dict[int, set[Node]] - Mapping associating X-domains to some nodes. + n_list : list of command.N + The list of N commands. + e_set : set of frozenset of int + A set of edges in the pattern. + m_list : list of command.M + The list of M commands. + c_dict : dict of int to Clifford + Mapping associating Clifford corrections to specific nodes. + z_dict : dict of int to set of Node + Mapping associating Z-domains to specific nodes. + x_dict : dict of int to set of Node + Mapping associating X-domains to specific nodes. """ pattern: Pattern @@ -81,7 +90,18 @@ class StandardizedPattern: x_dict: dict[int, set[Node]] def __init__(self, pattern: Pattern) -> None: - """Compute the standardized form of the given pattern.""" + """ + Initialize the StandardizedPattern with a given pattern. + + Parameters + ---------- + pattern : Pattern + The pattern to be standardized. + + Notes + ----- + This method computes the standardized form of the provided pattern upon initialization. + """ s_domain: set[Node] t_domain: set[Node] s_domain_opt: set[Node] | None @@ -145,11 +165,13 @@ def __init__(self, pattern: Pattern) -> None: self.c_dict[cmd.node] = cmd.clifford @ self.c_dict.get(cmd.node, Clifford.I) def extract_graph(self) -> nx.Graph[int]: - """Return the graph state from the command sequence, extracted from 'N' and 'E' commands. + """ + Return the graph state from the command sequence, extracted from 'N' and 'E' commands. Returns ------- - graph_state: nx.Graph + graph_state : nx.Graph[int] + The graph representing the state based on the 'N' (node) and 'E' (edge) commands. """ graph: nx.Graph[int] = nx.Graph() graph.add_nodes_from(self.pattern.input_nodes) @@ -160,7 +182,14 @@ def extract_graph(self) -> nx.Graph[int]: return graph def to_pattern(self) -> Pattern: - """Return the standardized pattern.""" + """ + Return the standardized pattern. + + Returns + ------- + Pattern + The standardized representation of the pattern. + """ pattern = graphix.pattern.Pattern(input_nodes=self.pattern.input_nodes) pattern.results = self.pattern.results pattern.extend( @@ -176,15 +205,16 @@ def to_pattern(self) -> Pattern: def _add_correction_domain(domain_dict: dict[Node, set[Node]], node: Node, domain: set[Node]) -> None: - """Merge a correction domain into ``domain_dict`` for ``node``. + """ + Merge a correction domain into `domain_dict` for `node`. Parameters ---------- - domain_dict : dict[int, Command] - Mapping from node index to accumulated domain. - node : int + domain_dict : dict[Node, set[Node]] + Mapping from nodes to their accumulated domains. + node : Node Target node whose domain should be updated. - domain : set[int] + domain : set[Node] Domain to merge with the existing one. """ if previous_domain := domain_dict.get(node): @@ -194,18 +224,24 @@ def _add_correction_domain(domain_dict: dict[Node, set[Node]], node: Node, domai def _commute_clifford(clifford_gate: Clifford, c_dict: dict[int, Clifford], i: int, j: int) -> None: - """Commute a Clifford with an entanglement command. + """ + Commute a Clifford gate with an entanglement command. Parameters ---------- clifford_gate : Clifford - Clifford gate before the entanglement command + The Clifford gate to be commuted with the entanglement command. c_dict : dict[int, Clifford] - Mapping from the node index to accumulated Clifford commands. + A mapping from node indices to accumulated Clifford commands. i : int - First node of the entanglement command where the Clifford is applied. + The index of the first node of the entanglement command where the Clifford is applied. j : int - Second node of the entanglement command where the Clifford is applied. + The index of the second node of the entanglement command where the Clifford is applied. + + Returns + ------- + None + This function modifies `c_dict` in place, adding the commuted Clifford commands. """ if clifford_gate in {Clifford.I, Clifford.Z, Clifford.S, Clifford.SDG}: # Clifford gate commutes with the entanglement command. @@ -231,7 +267,24 @@ def _incorporate_pauli_results_in_domain( def incorporate_pauli_results(pattern: Pattern) -> Pattern: - """Return an equivalent pattern where results from Pauli presimulation are integrated in corrections.""" + """ + Incorporate results from Pauli presimulation into an equivalent pattern. + + This function takes a Pattern object and integrates the results from + Pauli presimulation into it, yielding a corrected version of the pattern. + + Parameters + ---------- + pattern : Pattern + The input pattern containing the presimulation results that need to be + incorporated. + + Returns + ------- + Pattern + A new Pattern object that represents the equivalent pattern with + integrated corrections from the Pauli presimulation results. + """ result = graphix.pattern.Pattern(input_nodes=pattern.input_nodes) for cmd in pattern: if cmd.kind == CommandKind.M: diff --git a/graphix/parameter.py b/graphix/parameter.py index 947381d55..3e78c50b5 100644 --- a/graphix/parameter.py +++ b/graphix/parameter.py @@ -1,9 +1,9 @@ -"""Parameter class. +""" +Parameter class. -Parameter object acts as a placeholder of measurement angles and -allows the manipulation of the measurement pattern without specific +The Parameter object acts as a placeholder for measurement angles and +allows manipulation of the measurement pattern without specific value assignment. - """ from __future__ import annotations @@ -21,14 +21,37 @@ class Expression(ABC): - """Expression with parameters.""" + """ + Represents a mathematical expression with parameters. + + Attributes + ---------- + parameters : dict + A dictionary containing parameter names and their corresponding values. + + Methods + ------- + evaluate(): + Evaluates the expression using the current parameter values. + """ @abstractmethod def __mul__(self, other: object) -> ExpressionOrFloat: """ - Return the product of this expression with another object. + Perform multiplication with another object. + + This method implements the multiplication operator (*), allowing for + the expression to be multiplied by another object. - This special method is called to implement the multiplication operator (*). + Parameters + ---------- + other : object + The object to be multiplied with this expression. + + Returns + ------- + ExpressionOrFloat + The product of this expression and the other object. """ @abstractmethod @@ -39,14 +62,39 @@ def __rmul__(self, other: object) -> ExpressionOrFloat: This special method is called to implement the multiplication operator (*) when the left operand does not support multiplication with this type. Typically, `other` can be a number. + + Parameters + ---------- + other : object + The left operand to be multiplied with the expression. + + Returns + ------- + ExpressionOrFloat + The product of `other` and this expression. """ @abstractmethod def __add__(self, other: object) -> ExpressionOrFloat: """ - Return the sum of this expression with another object. + Add two expressions or an expression and a scalar. - This special method is called to implement the addition operator (+). + Parameters + ---------- + other : object + The object to be added to this expression. This could be another + expression or a float. + + Returns + ------- + ExpressionOrFloat + The result of the addition, which may be an expression or a float + depending on the type of `other`. + + Notes + ----- + This method implements the addition operator (+) by defining + the behavior of the `+` operator for instances of this class. """ @abstractmethod @@ -57,14 +105,34 @@ def __radd__(self, other: object) -> ExpressionOrFloat: This special method is called to implement the addition operator (+) when the left operand does not support addition with this type. Typically, `other` can be a number. + + Parameters + ---------- + other : object + The object to be added to this expression. + + Returns + ------- + ExpressionOrFloat + The result of adding `other` to this expression. """ @abstractmethod def __sub__(self, other: object) -> ExpressionOrFloat: """ - Return the difference of this expression with another object. + Subtract another object from this expression. + + This special method is called to implement the subtraction operator (-). - This special method is called to implement the substraction operator (-). + Parameters + ---------- + other : object + The object to subtract from this expression. + + Returns + ------- + ExpressionOrFloat + The result of the subtraction, which may be an Expression or a float. """ @abstractmethod @@ -72,17 +140,32 @@ def __rsub__(self, other: object) -> ExpressionOrFloat: """ Return the difference of `other` with this expression. - This special method is called to implement the substraction operator (-) - when the left operand does not support substraction with this type. + This special method is called to implement the subtraction operator (-) + when the left operand does not support subtraction with this type. Typically, `other` can be a number. + + Parameters + ---------- + other : object + The value to subtract from this expression. + + Returns + ------- + ExpressionOrFloat + The result of the subtraction. """ @abstractmethod def __neg__(self) -> ExpressionOrFloat: """ - Return the opposite of this expression. + Return the negation of this expression. - This special method is called to implement the unary opposite operator (-). + This method implements the unary negation operator (-) for the expression. + + Returns + ------- + ExpressionOrFloat + The negated expression. """ @abstractmethod @@ -91,49 +174,175 @@ def __truediv__(self, other: object) -> ExpressionOrFloat: Return the quotient of this expression with another object. This special method is called to implement the division operator (/). + + Parameters + ---------- + other : object + The object to divide this expression by. This can be another + `Expression` or any object that is compatible with division. + + Returns + ------- + ExpressionOrFloat + The result of the division, which can be an `Expression` or a + floating-point number, depending on the implementation and the type + of `other`. """ @abstractmethod def subs(self, variable: Parameter, value: ExpressionOrSupportsFloat) -> ExpressionOrComplex: - """Return the expression where every occurrence of `variable` is replaced with `value`.""" + """ + Substitute occurrences of a variable in the expression. + + Parameters + ---------- + variable : Parameter + The variable to be substituted in the expression. + value : ExpressionOrSupportsFloat + The value that will replace occurrences of the variable. + This can be an expression or a float-like value. + + Returns + ------- + ExpressionOrComplex + A new expression with every occurrence of `variable` replaced with `value`. + + Notes + ----- + This method is intended to be implemented by subclasses. + """ @abstractmethod def xreplace(self, assignment: Mapping[Parameter, ExpressionOrSupportsFloat]) -> ExpressionOrComplex: """ - Return the expression where every occurrence of any keys from `assignment` is replaced with the corresponding value. + Replace occurrences in the expression. - The substitutions are performed in parallel, i.e., once an - occurrence has been replaced by a value, this value is not - subject to any further replacement, even if another occurrence - of a key appears in this value. + Parameters + ---------- + assignment : Mapping[Parameter, ExpressionOrSupportsFloat] + A mapping where keys are parameters to be replaced in the expression, + and values are the corresponding expressions or values to use as replacements. + + Returns + ------- + ExpressionOrComplex + A new expression resulting from replacing every occurrence of keys + from `assignment` with their corresponding values. The substitutions + are performed in parallel, meaning that once an occurrence has been + replaced by a value, this value is not subject to any further + replacement, even if another occurrence of a key appears in this value. """ class ExpressionWithTrigonometry(Expression, ABC): - """Expression that supports trigonometric functions.""" + """ + Expression that supports trigonometric functions. + + This class allows for the evaluation and manipulation of mathematical + expressions that include trigonometric functions such as sine, cosine, + and tangent. + + Attributes + ---------- + expression : str + A string representation of the mathematical expression. + + Methods + ------- + evaluate(x): + Evaluates the expression at a given point x. + + derivative(): + Computes the derivative of the expression with respect to x. + + simplify(): + Simplifies the expression to its most reduced form. + + trigonometric_identity(): + Returns a trigonometric identity if applicable to the expression. + """ @abstractmethod def cos(self) -> ExpressionWithTrigonometry: - """Return the cosine of the expression.""" + """ + Return the cosine of the expression. + + Returns + ------- + ExpressionWithTrigonometry + The cosine of the current expression. + """ @abstractmethod def sin(self) -> ExpressionWithTrigonometry: - """Return the cosine of the expression.""" + """ + Compute the sine of the expression. + + Returns + ------- + ExpressionWithTrigonometry + The sine of the current expression. + """ @abstractmethod def exp(self) -> ExpressionWithTrigonometry: - """Return the exponential of the expression.""" + """ + Compute the exponential of the expression. + + Returns + ------- + ExpressionWithTrigonometry + The exponential of the current expression instance. + + Notes + ----- + This is an abstract method that must be implemented by subclasses of + `ExpressionWithTrigonometry`. + """ class Parameter(Expression): - """Abstract class for substituable parameter.""" + """ + Abstract class for a substitutable parameter. + + This class serves as a base for creating different parameter types + that can be substituted in various contexts. It provides a common + interface for all subclasses. + + Attributes + ---------- + None + + Methods + ------- + None + """ class PlaceholderOperationError(ValueError): - """Error raised when an operation is not supported by the placeholder.""" + """ + Exception raised for unsupported operations on a placeholder. + + This error is raised when an operation is attempted on a placeholder + that does not support that operation. + + Attributes + ---------- + message : str + Explanation of the error. + """ def __init__(self) -> None: - """Instantiate the error.""" + """ + Initialize a PlaceholderOperationError. + + This error is raised when a placeholder operation is encountered in the + context where an operation is expected but not defined. + + Parameters + ---------- + None + """ super().__init__( "Placeholder angles do not support any form of computation before substitution except affine operation. You may use `subs` with an actual value before the computation." ) @@ -141,9 +350,24 @@ def __init__(self) -> None: @dataclass class AffineExpression(Expression): - """Affine expression. - - An affine expression is of the form *a*x+b* where *a* and *b* are numbers and *x* is a parameter. + """ + AffineExpression class. + + An affine expression is of the form *a*x + b* where *a* and *b* are numbers and *x* is a parameter. + + Attributes + ---------- + a : float + Coefficient of the parameter x. + b : float + Constant term in the expression. + x : float + The parameter in the affine expression. + + Methods + ------- + evaluate(x) + Evaluates the affine expression at a given value of x. """ a: float @@ -151,44 +375,146 @@ class AffineExpression(Expression): b: float def offset(self, d: float) -> AffineExpression: - """Add *d* to the expression.""" + """ + Add a constant offset to the affine expression. + + Parameters + ---------- + d : float + The value to be added to the expression. + + Returns + ------- + AffineExpression + A new AffineExpression instance with the offset applied. + """ return AffineExpression(a=self.a, x=self.x, b=self.b + d) def _scale_non_null(self, k: float) -> AffineExpression: - """Return ``self`` scaled by ``k`` assuming ``k`` is non-zero. + """ + Scale the current affine expression by a non-zero factor. Parameters ---------- k : float - Scaling factor. + The scaling factor, which must be non-zero. Returns ------- AffineExpression - A new expression scaled by ``k``. + A new affine expression that is the result of scaling the current expression by `k`. """ return AffineExpression(a=k * self.a, x=self.x, b=k * self.b) def scale(self, k: float) -> ExpressionOrFloat: - """Multiply the expression by `k`.""" + """ + Scale the expression by a given factor. + + Parameters + ---------- + k : float + The factor by which to scale the expression. + + Returns + ------- + ExpressionOrFloat + The scaled expression or a float value, depending on the context of the expression. + """ if k == 0: return 0 return self._scale_non_null(k) def __mul__(self, other: object) -> ExpressionOrFloat: - """Look to the documentation in the parent class.""" + """ + Multiply the current affine expression by another object. + + This method allows for the multiplication of an AffineExpression instance + with another expression or a numerical value, returning the resulting + expression or float. + + Parameters + ---------- + other : object + The object to multiply with. This can be another affine expression, + a numerical value, or any object that supports multiplication with + an affine expression. + + Returns + ------- + ExpressionOrFloat + The result of the multiplication, which can be either an expression or + a float, depending on the type of `other`. + + Notes + ----- + Refer to the documentation in the parent class for more detailed behavior + and any additional context related to this operation. + + See Also + -------- + __add__ : Method for adding another expression or value. + __sub__ : Method for subtracting another expression or value. + """ if isinstance(other, SupportsFloat): return self.scale(float(other)) return NotImplemented def __rmul__(self, other: object) -> ExpressionOrFloat: - """Look to the documentation in the parent class.""" + """ + Perform right multiplication of an affine expression with another object. + + Parameters + ---------- + other : object + The object to multiply with this affine expression. This can be a scalar or another expression. + + Returns + ------- + ExpressionOrFloat + The result of the multiplication, which may be an affine expression or a float, depending on the type of the `other` object. + + Notes + ----- + Refer to the documentation in the parent class for further details on the multiplication behavior. + """ if isinstance(other, SupportsFloat): return self.scale(float(other)) return NotImplemented def __add__(self, other: object) -> ExpressionOrFloat: - """Look to the documentation in the parent class.""" + """ + Add another expression or a float to this affine expression. + + Parameters + ---------- + other : object + The expression or float to be added to this affine expression. + If `other` is an instance of AffineExpression or another compatible + expression type, the addition will be performed accordingly. + + Returns + ------- + ExpressionOrFloat + The result of the addition, which can be an instance of + Expression or a float, depending on the type of `other`. + + Notes + ----- + Refer to the documentation of the parent class for additional details + regarding operation behaviors and constraints. + + Examples + -------- + >>> expr1 = AffineExpression(...) + >>> expr2 = AffineExpression(...) + >>> result = expr1 + expr2 + >>> result = expr1 + 5.0 + + See Also + -------- + AffineExpression.__sub__: Subtracts another expression or float from + this affine expression. + """ if isinstance(other, SupportsFloat): return self.offset(float(other)) if isinstance(other, AffineExpression): @@ -201,13 +527,60 @@ def __add__(self, other: object) -> ExpressionOrFloat: return NotImplemented def __radd__(self, other: object) -> ExpressionOrFloat: - """Look to the documentation in the parent class.""" + """ + Compute the right addition of the AffineExpression with another object. + + This method is called when the addition operation is performed with + the AffineExpression instance on the right. It delegates the addition + operation to the corresponding implementation within the class. + + Parameters + ---------- + other : object + The object to be added to the AffineExpression. This can be + an instance of Expression, numeric type or any other compatible + type that supports addition. + + Returns + ------- + ExpressionOrFloat + The result of adding the AffineExpression to the other object. + This can either be an Expression instance or a float, + depending on the implementation and types involved in the addition. + + Notes + ----- + Refer to the documentation in the parent class for detailed + information about the addition behavior and any specific + constraints associated with the other operand. + """ if isinstance(other, SupportsFloat): return self.offset(float(other)) return NotImplemented def __sub__(self, other: object) -> ExpressionOrFloat: - """Look to the documentation in the parent class.""" + """ + Subtracts another object from the current AffineExpression instance. + + Parameters + ---------- + other : object + The object to be subtracted from the current instance. This can be + another AffineExpression or a compatible numerical type. + + Returns + ------- + ExpressionOrFloat + The result of the subtraction operation, which could either be + another AffineExpression or a float, depending on the type of + the 'other' parameter. + + Notes + ----- + Refer to the documentation in the parent class for additional + details on the subtraction behavior and compatibility with different + types. + """ if isinstance(other, AffineExpression): return self + -other if isinstance(other, SupportsFloat): @@ -215,45 +588,176 @@ def __sub__(self, other: object) -> ExpressionOrFloat: return NotImplemented def __rsub__(self, other: object) -> ExpressionOrFloat: - """Look to the documentation in the parent class.""" + """ + Perform right subtraction of an AffineExpression from another object. + + This method allows for the subtraction operation to be + performed with the AffineExpression class on the right side + of the operation. It is called when the object on the left + of the subtraction does not have a corresponding + `__sub__` method to handle the operation. + + Parameters + ---------- + other : object + The object from which the AffineExpression will be subtracted. + + Returns + ------- + ExpressionOrFloat + The result of the right subtraction, which can be either an + Expression or a float, depending on the types involved. + + Notes + ----- + Refer to the documentation in the parent class for more details + regarding the behavior and properties of the AffineExpression class. + """ if isinstance(other, SupportsFloat): return self._scale_non_null(-1).offset(float(other)) return NotImplemented def __neg__(self) -> ExpressionOrFloat: - """Look to the documentation in the parent class.""" + """ + Return the negation of the AffineExpression. + + This method overrides the unary negation operator (-) for the AffineExpression class. + It effectively creates a new AffineExpression that represents the negation of the current instance. + + Returns + ------- + ExpressionOrFloat + A new AffineExpression object that is the negation of the original instance, + or a scalar float if appropriate. + + Notes + ----- + Refer to the documentation of the parent class for further details on the behavior + and properties of the AffineExpression. + """ return self._scale_non_null(-1) def __truediv__(self, other: object) -> ExpressionOrFloat: - """Look to the documentation in the parent class.""" + """ + Defines the behavior of the division operator for AffineExpression instances. + + Parameters + ---------- + other : object + The object to divide the AffineExpression by. This can be another + AffineExpression or a numeric value. + + Returns + ------- + ExpressionOrFloat + The result of the division. If `other` is a numeric type, + the result will be a numeric value. If `other` is an AffineExpression, + the result will be an instance of Expression. + + Notes + ----- + This method inherits behavior from the parent class. For specific details + on the division behavior and handling of different types, refer to the + documentation of the parent class. + """ if isinstance(other, SupportsFloat): return self.scale(1 / float(other)) return NotImplemented def __str__(self) -> str: - """Return a textual representation of the expression.""" + """ + Return a string representation of the affine expression. + + This method provides a human-readable format of the affine expression, which can + include constants, coefficients, and variables involved in the expression, + formatted appropriately. + """ return f"{self.a} * {self.x} + {self.b}" def __eq__(self, other: object) -> bool: - """Check if two expressions are equal.""" + """ + Check if two expressions are equal. + + Parameters + ---------- + other : object + The other expression to compare with. + + Returns + ------- + bool + True if the expressions are equal, False otherwise. + """ if isinstance(other, AffineExpression): return self.a == other.a and self.x == other.x and self.b == other.b return False def evaluate(self, value: ExpressionOrSupportsFloat) -> ExpressionOrFloat: - """Evaluate the expression at `value`.""" + """ + Evaluate the expression at a given value. + + Parameters + ---------- + value : ExpressionOrSupportsFloat + The input value at which to evaluate the expression. + + Returns + ------- + ExpressionOrFloat + The result of evaluating the expression at the specified value. + """ if isinstance(value, SupportsFloat): return self.a * float(value) + self.b return self.a * value + self.b def subs(self, variable: Parameter, value: ExpressionOrSupportsFloat) -> ExpressionOrComplex: - """Look to the documentation in the parent class.""" + """ + Substitute a given variable with a specific value in the affine expression. + + Parameters + ---------- + variable : Parameter + The variable to be substituted in the expression. + value : ExpressionOrSupportsFloat + The value that will replace the variable in the expression. + This can be a numerical value or another expression that supports floating-point operations. + + Returns + ------- + ExpressionOrComplex + A new expression with the specified variable replaced by the given value. + + Notes + ----- + This method overrides the substitution behavior defined in the parent class. + Please refer to the parent class documentation for more details on the substitution mechanism. + """ if variable == self.x: return self.evaluate(value) return self def xreplace(self, assignment: Mapping[Parameter, ExpressionOrSupportsFloat]) -> ExpressionOrComplex: - """Look to the documentation in the parent class.""" + """ + Replace variables in the expression with corresponding values from an assignment. + + Parameters + ---------- + assignment : Mapping[Parameter, ExpressionOrSupportsFloat] + A mapping of parameters to their corresponding expressions or float values. + The keys are the parameters to be replaced, and the values are the expressions + or numeric values that will replace them in the current expression. + + Returns + ------- + ExpressionOrComplex + A new expression with the specified variables replaced by their assigned values. + The result may be of type Expression or a complex number depending on the + replacements made. + + Notes + ----- + Refer to the documentation in the parent class for additional context. + """ value = assignment.get(self.x) # `value` can be 0, so checking with `is not None` is mandatory here. if value is not None: @@ -262,54 +766,88 @@ def xreplace(self, assignment: Mapping[Parameter, ExpressionOrSupportsFloat]) -> class Placeholder(AffineExpression, Parameter): - """Placeholder for measurement angles. - - These placeholder may appear in affine expressions. Placeholders - and affine expressions may be used as angles in rotation gates of - :class:`Circuit` class or for the measurement angle of the - measurement commands. Pattern optimizations such that - standardization, signal shifting and Pauli preprocessing can be - applied to patterns with placeholders. - - These placeholders and affine expressions do not support arbitrary - computation and are not suitable for simulation. You may use - :func:`Circuit.subs` or :func:`Pattern.subs` with an actual value - before the computation. - + """ + Placeholder for measurement angles. + + This class serves as a placeholder for measurement angles that may appear + in affine expressions. Placeholders and affine expressions can be used as + angles in rotation gates of the :class:`Circuit` class or for the measurement + angle of measurement commands. Pattern optimizations, such as + standardization, signal shifting, and Pauli preprocessing, can be applied + to patterns that include placeholders. + + It is important to note that these placeholders and affine expressions do + not support arbitrary computation and are not suitable for simulation. + You may use :func:`Circuit.subs` or :func:`Pattern.subs` with an actual + value prior to computation. """ def __init__(self, name: str) -> None: - """Create a new :class:`Placeholder` object. + """ + Initialize a new instance of the :class:`Placeholder` object. Parameters ---------- name : str - name of the parameter, used for binding values. + The name of the parameter, used for binding values. """ self.__name = name super().__init__(a=1, x=self, b=0) @property def name(self) -> str: - """Return the name of the placeholder.""" + "str: Return the name of the placeholder." return self.__name def __repr__(self) -> str: - """Return a representation of the placeholder.""" + """ + Return a string representation of the Placeholder instance. + + Returns + ------- + str + A string that represents the Placeholder instance. + """ return f"Placeholder({self.__name!r})" def __str__(self) -> str: - """Return the name of the placeholder.""" + """ + Return a string representation of the placeholder. + + Returns + ------- + str + The name of the placeholder. + """ return self.__name def __eq__(self, other: object) -> bool: - """Check if two placeholders are identical.""" + """ + Compare two Placeholder instances for equality. + + Parameters + ---------- + other : object + The object to compare with the current instance. + + Returns + ------- + bool + True if the two Placeholder instances are identical, False otherwise. + """ if isinstance(other, Parameter): return self is other return super().__eq__(other) def __hash__(self) -> int: - """Return an hash value for the placeholder.""" + """ + Return a hash value for the placeholder. + + Returns + ------- + int + An integer representing the hash value of the placeholder. + """ return id(self) @@ -335,7 +873,24 @@ def __hash__(self) -> int: def check_expression_or_complex(value: object) -> ExpressionOrComplex: - """Check that the given object is of type ExpressionOrComplex and return it.""" + """ + Check if the given object is of type ExpressionOrComplex. + + Parameters + ---------- + value : object + The object to be checked. + + Returns + ------- + ExpressionOrComplex + The input object if it is of type ExpressionOrComplex. + + Raises + ------ + TypeError + If the input object is not of type ExpressionOrComplex. + """ if isinstance(value, Expression): return value if isinstance(value, SupportsComplex): @@ -345,7 +900,29 @@ def check_expression_or_complex(value: object) -> ExpressionOrComplex: def check_expression_or_float(value: object) -> ExpressionOrFloat: - """Check that the given object is of type ExpressionOrFloat and return it.""" + """ + Check if the given object is of type ExpressionOrFloat. + + Parameters + ---------- + value : object + The object to be checked. + + Returns + ------- + ExpressionOrFloat + The input object if it is of type ExpressionOrFloat. + + Raises + ------ + TypeError + If the input object is not of type ExpressionOrFloat. + + Notes + ----- + This function is useful for validating input types in contexts where + either an expression or a float is expected. + """ if isinstance(value, Expression): return value if isinstance(value, SupportsFloat): @@ -368,17 +945,31 @@ def subs(value: T, variable: Parameter, substitute: ExpressionOrSupportsFloat) - """ Substitute in `value`. - If `value` is in instance of :class:`Expression`, then return - `value.subs(variable, substitute)` (coerced into a complex or a - float if the result is a number). - - If `value` does not implement `subs`, `value` is returned - unchanged. - - This function is used to apply substitution to collections where - some elements are `Expression` and other elements are just - plain numbers. - + Parameters + ---------- + value : T + The value in which the substitution will be applied. It can be an instance + of :class:`Expression` or another type that does not implement the `subs` + method. + + variable : Parameter + The variable to be substituted in the `value`. + + substitute : ExpressionOrSupportsFloat + The value that will replace the variable in the `value`. + + Returns + ------- + T | Expression | complex + The modified `value` after applying the substitution. If `value` is an + instance of :class:`Expression`, the method `value.subs(variable, substitute)` + is called. The result is coerced to a complex or a float if it is a number. + If `value` does not implement `subs`, the original `value` is returned unchanged. + + Notes + ----- + This function is used to apply substitution to collections where some elements + are instances of `Expression` and others are plain numbers. """ if not isinstance(value, Expression): return value @@ -408,17 +999,26 @@ def xreplace(value: T, assignment: Mapping[Parameter, ExpressionOrSupportsFloat] """ Substitute in parallel in `value`. - If `value` is an an instance of :class:`Expression`, then return - `value.xreplace(assignment)` (coerced into a complex if the result - is a number). - - If `value` does not implement `xreplace`, `value` is returned - unchanged. - - This function is used to apply parallel substitutions to - collections where some elements are Expression and other elements - are just plain numbers. - + Parameters + ---------- + value : T + The input value which may be an instance of :class:`Expression` or other types. + + assignment : Mapping[Parameter, ExpressionOrSupportsFloat] + A mapping of parameters to expressions or values for substitution. + + Returns + ------- + T | Expression | complex + The result after substitution. If `value` is an instance of :class:`Expression`, + the method `value.xreplace(assignment)` is called, and the result is coerced into + a complex number if it returns a numeric value. If `value` does not implement + `xreplace`, it is returned unchanged. + + Notes + ----- + This function is used to apply parallel substitutions to collections where some + elements are of type `Expression` and others are plain numbers. """ if not isinstance(value, Expression): return value @@ -433,7 +1033,25 @@ def xreplace(value: T, assignment: Mapping[Parameter, ExpressionOrSupportsFloat] def cos_sin(angle: ExpressionOrFloat) -> tuple[ExpressionOrFloat, ExpressionOrFloat]: - """Cosine and sine of a float or an expression.""" + """ + Calculate the cosine and sine of a given angle. + + Parameters + ---------- + angle : ExpressionOrFloat + The angle in radians for which to compute the cosine and sine. + This can be a float or an expression. + + Returns + ------- + tuple[ExpressionOrFloat, ExpressionOrFloat] + A tuple containing the cosine and sine of the angle, in that order. + + Notes + ----- + The function uses the mathematical definitions of cosine and sine + to compute the values and can handle both scalar and symbolic inputs. + """ if isinstance(angle, Expression): if isinstance(angle, ExpressionWithTrigonometry): cos: ExpressionOrFloat = angle.cos() @@ -447,7 +1065,31 @@ def cos_sin(angle: ExpressionOrFloat) -> tuple[ExpressionOrFloat, ExpressionOrFl def exp(z: ExpressionOrComplex) -> ExpressionOrComplex: - """Exponential of a number or an expression.""" + """ + Compute the exponential of a number or an expression. + + Parameters + ---------- + z : ExpressionOrComplex + A number or an expression to which the exponential function will be applied. + + Returns + ------- + ExpressionOrComplex + The exponential of the input value. + + Notes + ----- + This function computes `e^z`, where `e` is Euler's number (approximately 2.71828). + The input can either be a numerical value or an expression that evaluates to a number. + + Examples + -------- + >>> exp(1) + 2.718281828459045 + >>> exp(Expression('x')) + Expression('e^x') + """ if isinstance(z, Expression): if isinstance(z, ExpressionWithTrigonometry): return z.exp() diff --git a/graphix/pattern.py b/graphix/pattern.py index d136989f9..326303407 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -1,6 +1,10 @@ -"""MBQC pattern according to Measurement Calculus. +""" +MBQC pattern according to Measurement Calculus. -ref: V. Danos, E. Kashefi and P. Panangaden. J. ACM 54.2 8 (2007) +References +---------- +Danos, V., Kashefi, E., & Panangaden, P. (2007). Measurement-based quantum computation. +Journal of the Association for Computing Machinery, 54(2), 8. """ from __future__ import annotations @@ -45,43 +49,64 @@ @dataclass(frozen=True) class NodeAlreadyPreparedError(Exception): - """Exception raised if a node is already prepared.""" + """ + Exception raised when a node is already prepared. + + This error is raised to indicate that an operation was attempted on a + node that has already been prepared, which may lead to inconsistent + states or operations. + """ node: int @override def __str__(self) -> str: - """Return the message of the error.""" + """ + Return a string representation of the error message. + + Returns + ------- + str + The message of the error. + """ return f"Node already prepared: {self.node}" class Pattern: """ - MBQC pattern class. + MBQC Pattern Class. - Pattern holds a sequence of commands to operate the MBQC (Pattern.seq), - and provide modification strategies to improve the structure and simulation - efficiency of the pattern accoring to measurement calculus. + The `Pattern` class encapsulates a sequence of commands used to operate + the Measurement Based Quantum Computing (MBQC) framework. It provides + modification strategies to enhance the structure and simulation + efficiency of the pattern according to measurement calculus. - ref: V. Danos, E. Kashefi and P. Panangaden. J. ACM 54.2 8 (2007) + References + ---------- + Danos, V., Kashefi, E., & Panangaden, P. (2007). J. ACM, 54(2), 8. Attributes ---------- - list(self) : - list of commands. - - .. line-block:: - each command is a list [type, nodes, attr] which will be applied in the order of list indices. - type: one of {'N', 'M', 'E', 'X', 'Z', 'S', 'C'} - nodes: int for {'N', 'M', 'X', 'Z', 'S', 'C'} commands, tuple (i, j) for {'E'} command - attr for N: none - attr for M: meas_plane, angle, s_domain, t_domain - attr for X: signal_domain - attr for Z: signal_domain - attr for S: signal_domain - attr for C: clifford_index, as defined in :py:mod:`graphix.clifford` + seq : list + A list of commands where each command is represented as a list + in the format [type, nodes, attr]. The commands are executed in + the order specified by the list indices. The components are defined as: + type : str + One of {'N', 'M', 'E', 'X', 'Z', 'S', 'C'} indicating the command type. + nodes : int or tuple + An integer for commands {'N', 'M', 'X', 'Z', 'S', 'C'} + or a tuple (i, j) for the 'E' command. + attr : varied + Additional attributes based on the command type: + - For 'N': None. + - For 'M': meas_plane, angle, s_domain, t_domain. + - For 'X': signal_domain. + - For 'Z': signal_domain. + - For 'S': signal_domain. + - For 'C': clifford_index (as defined in :py:mod:`graphix.clifford`). + n_node : int - total number of nodes in the resource state + The total number of nodes in the resource state. """ results: dict[int, Outcome] @@ -98,12 +123,18 @@ def __init__( Parameters ---------- - input_nodes : Iterable[int] | None - Optional. List of input qubits. - cmds : Iterable[Command] | None - Optional. List of initial commands. - output_nodes : Iterable[int] | None - Optional. List of output qubits. + input_nodes : Iterable[int], optional + List of input qubits. Defaults to None. + cmds : Iterable[Command], optional + List of initial commands. Defaults to None. + output_nodes : Iterable[int], optional + List of output qubits. Defaults to None. + + Notes + ----- + This constructor initializes a pattern with the specified input nodes, + commands, and output nodes. If any of the parameters are not provided, + they will be set to None. """ self.results = {} # measurement results from the graph state simulator if input_nodes is None: @@ -124,14 +155,20 @@ def __init__( self.reorder_output_nodes(output_nodes) def add(self, cmd: Command) -> None: - """Add command to the end of the pattern. + """ + Add a command to the end of the pattern. An MBQC command is an instance of :class:`graphix.command.Command`. Parameters ---------- cmd : :class:`graphix.command.Command` - MBQC command. + The MBQC command to be added to the pattern. + + Returns + ------- + None + This method does not return a value. """ if cmd.kind == CommandKind.N: if cmd.node in self.__output_nodes: @@ -143,9 +180,17 @@ def add(self, cmd: Command) -> None: self.__seq.append(cmd) def extend(self, *cmds: Command | Iterable[Command]) -> None: - """Add sequences of commands. + """ + Extend the pattern with additional sequences of commands. - :param cmds: sequences of commands + Parameters + ---------- + cmds : Command or Iterable[Command] + A sequence or multiple sequences of commands to add to the pattern. + + Returns + ------- + None """ for item in cmds: if isinstance(item, Iterable): @@ -155,17 +200,36 @@ def extend(self, *cmds: Command | Iterable[Command]) -> None: self.add(item) def clear(self) -> None: - """Clear the sequence of pattern commands.""" + """ + Clear the sequence of pattern commands. + + This method resets the internal state of the Pattern object by clearing any existing sequence of commands. + It effectively prepares the Pattern for a new set of commands by removing all previous patterns and their associated data. + + Returns + ------- + None + """ self.__n_node = len(self.__input_nodes) self.__seq = [] self.__output_nodes = list(self.__input_nodes) def replace(self, cmds: list[Command], input_nodes: list[int] | None = None) -> None: - """Replace pattern with a given sequence of pattern commands. + """ + Replace the pattern with a given sequence of pattern commands. - :param cmds: list of commands + Parameters + ---------- + cmds : list of Command + A list of commands used to replace the current pattern. - :param input_nodes: optional, list of input qubits (by default, keep the same input nodes as before) + input_nodes : list of int, optional + A list of input qubits. If not provided, the current input nodes + will remain unchanged. + + Returns + ------- + None """ if input_nodes is not None: self.__input_nodes = list(input_nodes) @@ -175,32 +239,38 @@ def replace(self, cmds: list[Command], input_nodes: list[int] | None = None) -> def compose( self, other: Pattern, mapping: Mapping[int, int], preserve_mapping: bool = False ) -> tuple[Pattern, dict[int, int]]: - r"""Compose two patterns by merging subsets of outputs from `self` and a subset of inputs of `other`, and relabeling the nodes of `other` that were not merged. + """ + Compose two patterns by merging subsets of outputs from `self` and a subset of inputs of `other`, + and relabeling the nodes of `other` that were not merged. Parameters ---------- other : Pattern Pattern to be composed with `self`. - mapping: Mapping[int, int] - Partial relabelling of the nodes in `other`, with `keys` and `values` denoting the old and new node labels, respectively. - preserve_mapping: bool - Boolean flag controlling the ordering of the output nodes in the returned pattern. + mapping : Mapping[int, int] + Partial relabelling of the nodes in `other`, with keys and values denoting the old and new node labels, respectively. + preserve_mapping : bool, optional + Boolean flag controlling the ordering of the output nodes in the returned pattern. Default is False. Returns ------- - p: Pattern - composed pattern - mapping_complete: dict[int, int] - Complete relabelling of the nodes in `other`, with `keys` and `values` denoting the old and new node label, respectively. + p : Pattern + Composed pattern. + mapping_complete : dict[int, int] + Complete relabelling of the nodes in `other`, with keys and values denoting the old and new node labels, respectively. Notes ----- - Let's denote :math:`(I_j, O_j, V_j, S_j)` the ordered set of inputs and outputs, the computational space and the sequence of commands of pattern :math:`P_j`, respectively, with :math:`j = 1` for pattern `self` and :math:`j = 2` for pattern `other`. Let's denote :math:`P` the resulting pattern with :math:`(I, O, V, S)`. - Let's denote :math:`K, U` the sets of `keys` and `values` of `mapping`, :math:`M_1 = O_1 \cap U` the set of merged outputs, and :math:`M_2 = \{k \in I_2 \cap K | k \rightarrow v, v \in M_1 \}` the set of merged inputs. - - The pattern composition requires that + Let :math:`(I_j, O_j, V_j, S_j)` be the ordered set of inputs and outputs, the computational space, + and the sequence of commands of pattern :math:`P_j`, respectively, with :math:`j = 1` for pattern `self` + and :math:`j = 2` for pattern `other`. Let :math:`P` be the resulting pattern with :math:`(I, O, V, S)`. + Let :math:`K, U` be the sets of keys and values of `mapping`, + :math:`M_1 = O_1 \cap U` the set of merged outputs, + and :math:`M_2 = \{k \in I_2 \cap K | k \rightarrow v, v \in M_1\}` the set of merged inputs. + + The pattern composition requires that: - :math:`K \subseteq V_2`. - - For a pair :math:`(k, v) \in (K, U)` + - For a pair :math:`(k, v) \in (K, U)`: - :math:`U \cap V_1 \setminus O_1 = \emptyset`. If :math:`v \in O_1`, then :math:`k \in I_2`, otherwise an error is raised. - :math:`v` can always satisfy :math:`v \notin V_1`, thereby allowing a custom relabelling. @@ -209,8 +279,11 @@ def compose( - The sequence of the resulting pattern is :math:`S = S_2 S_1`, where nodes in :math:`S_2` are relabelled according to `mapping`. - :math:`I = I_1 \cup (I_2 \setminus M_2)`. - :math:`O = (O_1 \setminus M_1) \cup O_2`. - - Input (and, respectively, output) nodes in the returned pattern have the order of the pattern `self` followed by those of the pattern `other`. Merged nodes are removed. - - If `preserve_mapping = True` and :math:`|M_1| = |I_2| = |O_2|`, then the outputs of the returned pattern are the outputs of pattern `self`, where the nth merged output is replaced by the output of pattern `other` corresponding to its nth input instead. + - Input (and, respectively, output) nodes in the returned pattern have the order of pattern `self` + followed by those of pattern `other`. Merged nodes are removed. + - If `preserve_mapping = True` and :math:`|M_1| = |I_2| = |O_2|`, + then the outputs of the returned pattern are the outputs of pattern `self`, + where the nth merged output is replaced by the output of pattern `other` corresponding to its nth input instead. """ nodes_p1 = self.extract_nodes() | self.results.keys() # Results contain preprocessed Pauli nodes nodes_p2 = other.extract_nodes() | other.results.keys() @@ -298,57 +371,125 @@ def update_command(cmd: Command) -> Command: @property def input_nodes(self) -> list[int]: - """List input nodes.""" + """ + List of input nodes. + + Returns + ------- + list[int] + A list containing the identifiers of the input nodes. + """ return list(self.__input_nodes) # copy for preventing modification @property def output_nodes(self) -> list[int]: - """List all nodes that are either `input_nodes` or prepared with `N` commands and that have not been measured with an `M` command.""" + """ + List all nodes that are either input nodes or prepared + with N commands and have not been measured with an M command. + + Returns + ------- + list[int] + A list of nodes satisfying the specified conditions. + """ return list(self.__output_nodes) # copy for preventing modification def __len__(self) -> int: - """Return the length of command sequence.""" + """ + Return the length of the command sequence. + + Returns + ------- + int + The number of commands in the sequence. + """ return len(self.__seq) def __iter__(self) -> Iterator[Command]: - """Iterate over commands.""" + """ + Iterate over commands. + + Yields + ------- + Command + An instance of the Command class for each iteration. + """ return iter(self.__seq) def __getitem__(self, index: int) -> Command: - """Get the command at a given index.""" + """ + Retrieve the command at the specified index. + + Parameters + ---------- + index : int + The index of the command to retrieve. + + Returns + ------- + Command + The command associated with the given index. + + Raises + ------ + IndexError + If the index is out of the bounds of the command list. + """ return self.__seq[index] @property def n_node(self) -> int: - """Count of nodes that are either `input_nodes` or prepared with `N` commands.""" + """ + Count the number of nodes. + + This property returns the count of nodes that are either + classified as `input_nodes` or have been prepared with + `N` commands. + + Returns + ------- + int + The number of nodes. + """ return self.__n_node def reorder_output_nodes(self, output_nodes: Iterable[int]) -> None: - """Arrange the order of output_nodes. + """ + Rearrange the order of output nodes. Parameters ---------- - output_nodes: iterable of int - output nodes order determined by user. each index corresponds to that of logical qubits. + output_nodes : iterable of int + The desired order of output nodes as determined by the user. Each index corresponds to that of the logical qubits. """ output_nodes = list(output_nodes) # make our own copy (allow iterators to be passed) assert_permutation(self.__output_nodes, output_nodes) self.__output_nodes = output_nodes def reorder_input_nodes(self, input_nodes: Iterable[int]) -> None: - """Arrange the order of input_nodes. + """ + Rearrange the order of input nodes. Parameters ---------- - input_nodes: iterable of int - input nodes order determined by user. each index corresponds to that of logical qubits. + input_nodes : iterable of int + The order of input nodes as determined by the user. Each index corresponds to the logical qubits. """ input_nodes = list(input_nodes) # make our own copy (allow iterators to be passed) assert_permutation(self.__input_nodes, input_nodes) self.__input_nodes = input_nodes def __repr__(self) -> str: - """Return a representation string of the pattern.""" + """ + Returns a string representation of the pattern. + + This method provides a string that represents the pattern object, which is useful for debugging and logging purposes. + + Returns + ------- + str + A string representation of the pattern. + """ arguments = [] if self.__input_nodes: arguments.append(f"input_nodes={self.__input_nodes}") @@ -359,11 +500,33 @@ def __repr__(self) -> str: return f"Pattern({', '.join(arguments)})" def __str__(self) -> str: - """Return a human-readable string of the pattern.""" + """ + Return a human-readable representation of the pattern. + + Returns + ------- + str + A string that represents the pattern in a readable format. + """ return self.to_ascii() def __eq__(self, other: object) -> bool: - """Return `True` if the two patterns are equal, `False` otherwise.""" + """ + Compare two patterns for equality. + + This method checks if the current pattern is equal to another pattern. + Patterns are considered equal if all their attributes and properties match. + + Parameters + ---------- + other : object + The pattern or object to compare against. + + Returns + ------- + bool + True if the patterns are equal, False otherwise. + """ if not isinstance(other, Pattern): return NotImplemented return ( @@ -376,33 +539,91 @@ def __eq__(self, other: object) -> bool: def to_ascii( self, left_to_right: bool = False, limit: int = 40, target: Container[command.CommandKind] | None = None ) -> str: - """Return the ASCII string representation of the pattern.""" + """ + Return the ASCII string representation of the pattern. + + Parameters + ---------- + left_to_right : bool, optional + If True, the pattern will be represented from left to right. Default is False. + limit : int, optional + The maximum length of the ASCII representation. Default is 40. + target : Container[command.CommandKind] or None, optional + A container of command kinds to be included in the representation. Default is None. + + Returns + ------- + str + The ASCII representation of the pattern. + """ return pattern_to_str(self, OutputFormat.ASCII, left_to_right, limit, target) def to_latex( self, left_to_right: bool = False, limit: int = 40, target: Container[command.CommandKind] | None = None ) -> str: - """Return a string containing the LaTeX representation of the pattern.""" + """ + Return a string containing the LaTeX representation of the pattern. + + Parameters + ---------- + left_to_right : bool, optional + If True, the LaTeX will be formatted from left to right. Default is False. + limit : int, optional + The maximum number of characters to include in the LaTeX representation. Default is 40. + target : Container[command.CommandKind] or None, optional + A container of specific command kinds to include in the LaTeX output. If None, includes all kinds. Default is None. + + Returns + ------- + str + The LaTeX representation of the pattern as a string. + """ return pattern_to_str(self, OutputFormat.LaTeX, left_to_right, limit, target) def to_unicode( self, left_to_right: bool = False, limit: int = 40, target: Container[command.CommandKind] | None = None ) -> str: - """Return the Unicode string representation of the pattern.""" + """ + Return the Unicode string representation of the pattern. + + Parameters + ---------- + left_to_right : bool, optional + If True, the pattern is rendered from left to right. Default is False. + + limit : int, optional + The maximum length of the returned string. Default is 40. + + target : Container[command.CommandKind] or None, optional + A collection of command kinds to target in the pattern. Default is None. + + Returns + ------- + str + The Unicode string representation of the pattern. + """ return pattern_to_str(self, OutputFormat.Unicode, left_to_right, limit, target) def print_pattern(self, lim: int = 40, target: Container[CommandKind] | None = None) -> None: - """Print the pattern sequence (Pattern.seq). + """ + Print the pattern sequence (Pattern.seq). - This method is deprecated. - See :meth:`to_ascii`, :meth:`to_latex`, :meth:`to_unicode` and :func:`graphix.pretty_print.pattern_to_str`. + This method is deprecated. Please use :meth:`to_ascii`, :meth:`to_latex`, :meth:`to_unicode`, + or :func:`graphix.pretty_print.pattern_to_str` instead. Parameters ---------- - lim: int, optional - maximum number of commands to show - target : list of CommandKind, optional - show only specified commands, e.g. [CommandKind.M, CommandKind.X, CommandKind.Z] + lim : int, optional + The maximum number of commands to display. Defaults to 40. + target : Container[CommandKind], optional + A list of specified commands to show, e.g., + [CommandKind.M, CommandKind.X, CommandKind.Z]. If provided, only these + commands will be printed. + + Returns + ------- + None + This method does not return any value. """ warnings.warn( "Method `print_pattern` is deprecated. Use one of the methods `to_ascii`, `to_latex`, `to_unicode`, or the function `graphix.pretty_print.pattern_to_str`.", @@ -412,26 +633,36 @@ def print_pattern(self, lim: int = 40, target: Container[CommandKind] | None = N print(pattern_to_str(self, OutputFormat.ASCII, left_to_right=True, limit=lim, target=target)) def standardize(self) -> None: - """Execute standardization of the pattern. + """ + Standardize the pattern. + + This method executes the standardization of the pattern. A 'standard' + pattern is one where commands are sorted in the following order: + 1. 'N' commands + 2. 'E' commands + 3. 'M' commands + 4. Byproduct commands ('X' and 'Z') + 5. Clifford commands ('C') - 'standard' pattern is one where commands are sorted in the - order of 'N', 'E', 'M' and then byproduct commands ('X' and - 'Z') and finally Clifford commands ('C'). + Returns + ------- + None """ self.__seq = optimization.standardize(self).__seq def is_standard(self, strict: bool = False) -> bool: - """Determine whether the command sequence is standard. + """ + Determine whether the command sequence is standard. Parameters ---------- strict : bool, optional - If True, ensures that C commands are the last ones. + If True, ensures that C commands are the last ones. Default is False. Returns ------- - is_standard : bool - True if the pattern is standard + bool + True if the pattern is standard; False otherwise. """ it = iter(self) try: @@ -458,25 +689,27 @@ def is_standard(self, strict: bool = False) -> bool: return False def shift_signals(self, method: str = "direct") -> dict[int, set[int]]: - """Perform signal shifting procedure. + """ + Perform signal shifting procedure. - Extract the t-dependence of the measurement into 'S' commands - and commute them to the end of the command sequence where it can be removed. + This method extracts the time-dependence of the measurement into 'S' commands + and commutes them to the end of the command sequence, where they can be removed. This procedure simplifies the dependence structure of the pattern. - Ref for the original 'mc' method: - V. Danos, E. Kashefi and P. Panangaden. J. ACM 54.2 8 (2007) + Reference for the original 'mc' method: + V. Danos, E. Kashefi, and P. Panangaden. J. ACM 54.2 8 (2007). Parameters ---------- method : str, optional - 'direct' shift_signals is executed on a conventional Pattern sequence. - 'mc' shift_signals is done using the original algorithm on the measurement calculus paper. + Method to use for shifting signals. Default is 'direct': + - 'direct' : executes shift_signals on a conventional Pattern sequence. + - 'mc' : executes shift_signals using the original algorithm from the measurement calculus paper. Returns ------- signal_dict : dict[int, set[int]] - For each node, the signal that have been shifted. + A dictionary where each key is a node and the corresponding value is a set of signals that have been shifted. """ if method == "direct": return self.shift_signals_direct() @@ -505,17 +738,28 @@ def shift_signals(self, method: str = "direct") -> dict[int, set[int]]: raise ValueError("Invalid method") def shift_signals_direct(self) -> dict[int, set[int]]: - """Perform signal shifting procedure.""" + """ + Perform signal shifting procedure. + + Returns a mapping of signal indices to their shifted positions. + + Returns + ------- + dict[int, set[int]] + A dictionary where the keys are the original signal indices, + and the values are sets of shifted signal indices. + """ signal_dict: dict[int, set[int]] = {} def expand_domain(domain: set[command.Node]) -> None: - """Expand ``domain`` with previously shifted signals. + """ + Expand the `domain` with previously shifted signals. Parameters ---------- - domain : set[int] - Set of nodes representing the current domain. This set is - modified in place by XORing any previously shifted domains. + domain : set[command.Node] + Set of nodes representing the current domain. This set is modified in place + by XORing any previously shifted domains. """ for node in domain & signal_dict.keys(): domain ^= signal_dict[node] @@ -561,16 +805,24 @@ def expand_domain(domain: set[command.Node]) -> None: return signal_dict def _find_op_to_be_moved(self, op: CommandKind, rev: bool = False, skipnum: int = 0) -> int | None: - """Find a command. + """ + Find a command in the sequence. Parameters ---------- - op : CommandKind, N, E, M, X, Z, S - command types to be searched - rev : bool - search from the end (true) or start (false) of seq - skipnum : int - skip the detected command by specified times + op : CommandKind + The command types to be searched (e.g., N, E, M, X, Z, S). + rev : bool, optional + If True, search from the end of the sequence; + if False (default), search from the start. + skipnum : int, optional + The number of times to skip the detected command. + Default is 0, meaning no skips. + + Returns + ------- + int or None + The index of the found command or None if the command is not found. """ if not rev: # Search from the start start_index, end_index, step = 0, len(self.__seq), 1 @@ -588,13 +840,20 @@ def _find_op_to_be_moved(self, op: CommandKind, rev: bool = False, skipnum: int return None def _commute_ex(self, target: int) -> bool: - """Perform the commutation of E and X. + """ + Perform the commutation of E and X. Parameters ---------- target : int - target command index. this must point to - a X command followed by E command + Target command index. This must point to an X command + followed by an E command. + + Returns + ------- + bool + Returns True if the commutation was successful, + False otherwise. """ x = self.__seq[target] e = self.__seq[target + 1] @@ -616,13 +875,20 @@ def _commute_ex(self, target: int) -> bool: return False def _commute_mx(self, target: int) -> bool: - """Perform the commutation of M and X. + """ + Perform the commutation of M and X. Parameters ---------- target : int - target command index. this must point to - a X command followed by M command + Target command index. This must point to an X command + followed by an M command. + + Returns + ------- + bool + Returns True if the commutation was successful, + otherwise returns False. """ x = self.__seq[target] m = self.__seq[target + 1] @@ -636,13 +902,18 @@ def _commute_mx(self, target: int) -> bool: return False def _commute_mz(self, target: int) -> bool: - """Perform the commutation of M and Z. + """ + Perform the commutation of M and Z. Parameters ---------- target : int - target command index. this must point to - a Z command followed by M command + Target command index. This must point to a Z command followed by an M command. + + Returns + ------- + bool + Returns True if the commuting operation is successful, otherwise False. """ z = self.__seq[target] m = self.__seq[target + 1] @@ -656,13 +927,14 @@ def _commute_mz(self, target: int) -> bool: return False def _commute_xs(self, target: int) -> None: - """Perform the commutation of X and S. + """ + Perform the commutation of X and S. Parameters ---------- target : int - target command index. this must point to - a S command followed by X command + Index of the target command. This must point to an S command + followed by an X command. """ s = self.__seq[target] x = self.__seq[target + 1] @@ -673,13 +945,14 @@ def _commute_xs(self, target: int) -> None: self._commute_with_following(target) def _commute_zs(self, target: int) -> None: - """Perform the commutation of Z and S. + """ + Perform the commutation of Z and S. Parameters ---------- target : int - target command index. this must point to - a S command followed by Z command + The index of the target command. This must point to + an S command followed by a Z command. """ s = self.__seq[target] z = self.__seq[target + 1] @@ -690,13 +963,14 @@ def _commute_zs(self, target: int) -> None: self._commute_with_following(target) def _commute_ms(self, target: int) -> None: - """Perform the commutation of M and S. + """ + Perform the commutation of M and S. Parameters ---------- target : int - target command index. this must point to - a S command followed by M command + Target command index. This must point to an S command + followed by an M command. """ s = self.__seq[target] m = self.__seq[target + 1] @@ -709,13 +983,14 @@ def _commute_ms(self, target: int) -> None: self._commute_with_following(target) def _commute_ss(self, target: int) -> None: - """Perform the commutation of two S commands. + """ + Perform the commutation of two S commands. Parameters ---------- target : int - target command index. this must point to - a S command followed by S command + The index of the target command. This must point to + an S command that is followed by another S command. """ s1 = self.__seq[target] s2 = self.__seq[target + 1] @@ -726,38 +1001,56 @@ def _commute_ss(self, target: int) -> None: self._commute_with_following(target) def _commute_with_following(self, target: int) -> None: - """Perform the commutation of two consecutive commands that commutes. + """ + Perform the commutation of two consecutive commands that commute. - commutes the target command with the following command. + This method commutes the command at the specified `target` index with the command that follows it, provided they are eligible for commutation. Parameters ---------- target : int - target command index + The index of the target command to be commuted with the following command. + + Returns + ------- + None """ a = self.__seq[target + 1] self.__seq.pop(target + 1) self.__seq.insert(target, a) def _commute_with_preceding(self, target: int) -> None: - """Perform the commutation of two consecutive commands that commutes. + """ + Perform the commutation of two consecutive commands that commute. - commutes the target command with the preceding command. + This method commutes the target command with the preceding command if they are + able to do so. Parameters ---------- target : int - target command index + The index of the target command to be commuted with the preceding command. + + Returns + ------- + None """ a = self.__seq[target - 1] self.__seq.pop(target - 1) self.__seq.insert(target, a) def _move_n_to_left(self) -> None: - """Move all 'N' commands to the start of the sequence. + """ + Move all 'N' commands to the start of the sequence. - N can be moved to the start of sequence without the need of considering - commutation relations. + This method relocates all 'N' commands to the beginning of the + sequence. The movement does not require consideration of + commutation relations, as 'N' commands can be freely + rearranged. + + Returns + ------- + None """ new_seq = [] n_list = [] @@ -770,7 +1063,17 @@ def _move_n_to_left(self) -> None: self.__seq = n_list + new_seq def _move_byproduct_to_right(self) -> None: - """Move the byproduct commands to the end of sequence, using the commutation relations implemented in graphix.Pattern class.""" + """ + Move the byproduct commands to the end of the sequence. + + This method rearranges the byproduct commands in the current pattern sequence + to ensure that they are positioned at the end. The rearrangement utilizes the + commutation relations as implemented in the `graphix.Pattern` class. + + Returns + ------- + None + """ # First, we move all X commands to the end of sequence index = len(self.__seq) - 1 x_limit = len(self.__seq) - 1 @@ -815,7 +1118,13 @@ def _move_byproduct_to_right(self) -> None: index -= 1 def _move_e_after_n(self) -> None: - """Move all E commands to the start of sequence, before all N commands. assumes that _move_n_to_left() method was called.""" + """ + Move all 'E' commands to the start of the sequence, placing them before + all 'N' commands. + + This method assumes that the `_move_n_to_left()` method has been called + prior to its execution. + """ moved_e = 0 target = self._find_op_to_be_moved(CommandKind.E, skipnum=moved_e) while target is not None: @@ -829,9 +1138,21 @@ def _move_e_after_n(self) -> None: target -= 1 def extract_signals(self) -> dict[int, set[int]]: - """Extract 't' domain of measurement commands, turn them into signal 'S' commands and add to the command sequence. + """ + Extract measurement command time domains and convert them into signal commands. + + This method processes 't' domain measurement commands to produce corresponding + signal 'S' commands and add them to the command sequence. + + Returns + ------- + dict[int, set[int]] + A dictionary where keys are signal identifiers and values are sets of + measurement command indices associated with each signal. - This is used for shift_signals() method. + Notes + ----- + This method is utilized by the `shift_signals()` method. """ signal_dict = {} pos = 0 @@ -849,14 +1170,17 @@ def extract_signals(self) -> dict[int, set[int]]: return signal_dict def _get_dependency(self) -> dict[int, set[int]]: - """Get dependency (byproduct correction & dependent measurement) structure of nodes in the graph (resource) state, according to the pattern. + """ + Get the dependency structure of nodes in the graph (resource) state according to the pattern. - This is used to determine the optimum measurement order. + This structure includes byproduct corrections and dependent measurements and is used to determine + the optimum measurement order. Returns ------- dependency : dict of set - index is node number. all nodes in the each set must be measured before measuring + A dictionary where the keys are node numbers and the values are sets of node numbers. + Each set contains nodes that must be measured before the corresponding key node can be measured. """ nodes = self.extract_nodes() dependency: dict[int, set[int]] = {i: set() for i in nodes} @@ -870,35 +1194,38 @@ def _get_dependency(self) -> dict[int, set[int]]: @staticmethod def update_dependency(measured: AbstractSet[int], dependency: dict[int, set[int]]) -> None: - """Remove measured nodes from the 'dependency'. + """ + Remove measured nodes from the dependency. Parameters ---------- - measured: set of int - measured nodes. - dependency: dict of set - which is produced by `_get_dependency` + measured : AbstractSet[int] + Measured nodes to be removed. + dependency : dict[int, set[int]] + Dictionary containing the current dependency information, + which is produced by `_get_dependency`. Returns ------- - dependency: dict of set - updated dependency information + None + The function updates the `dependency` in place and does not return any value. """ for i in dependency: dependency[i] -= measured def get_layers(self) -> tuple[int, dict[int, set[int]]]: - """Construct layers(l_k) from dependency information. + """ + Construct layers (l_k) from dependency information. - kth layer must be measured before measuring k+1th layer - and nodes in the same layer can be measured simultaneously. + The k-th layer must be measured before measuring the (k+1)-th layer, + and nodes within the same layer can be measured simultaneously. Returns ------- depth : int - depth of graph + The depth of the graph. layers : dict of set - nodes grouped by layer index(k) + Nodes grouped by layer index (k). """ dependency = self._get_dependency() measured = self.results.keys() @@ -922,12 +1249,13 @@ def get_layers(self) -> tuple[int, dict[int, set[int]]]: return depth, l_k def _measurement_order_depth(self) -> list[int]: - """Obtain a measurement order which reduces the depth of a pattern. + """ + Obtain a measurement order that reduces the depth of a pattern. Returns ------- - meas_order: list of int - optimal measurement order for parallel computing + meas_order : list of int + Optimal measurement order for parallel computing. """ d, l_k = self.get_layers() meas_order: list[int] = [] @@ -937,12 +1265,21 @@ def _measurement_order_depth(self) -> list[int]: @staticmethod def connected_edges(node: int, edges: set[tuple[int, int]]) -> set[tuple[int, int]]: - """Search not activated edges connected to the specified node. + """ + Search for non-activated edges connected to the specified node. + + Parameters + ---------- + node : int + The node for which to find connected edges. + edges : set of tuple[int, int] + A set of edges represented as tuples, where each tuple contains + two integers representing a connection between nodes. Returns ------- - connected: set of tuple - set of connected edges + connected : set of tuple[int, int] + A set of connected edges associated with the specified node. """ connected = set() for edge in edges: @@ -951,12 +1288,13 @@ def connected_edges(node: int, edges: set[tuple[int, int]]) -> set[tuple[int, in return connected def _measurement_order_space(self) -> list[int]: - """Determine measurement order that heuristically optimises the max_space of a pattern. + """ + Determine measurement order that heuristically optimizes the max space of a pattern. Returns ------- - meas_order: list of int - sub-optimal measurement order for classical simulation + list[int] + Sub-optimal measurement order for classical simulation. """ graph = self.extract_graph() nodes = set(graph.nodes) @@ -986,12 +1324,15 @@ def _measurement_order_space(self) -> list[int]: return meas_order def get_measurement_order_from_flow(self) -> list[int] | None: - """Return a measurement order generated from flow. If a graph has flow, the minimum 'max_space' of a pattern is guaranteed to width+1. + """ + Return a measurement order generated from flow. + + If a graph has flow, the minimum 'max_space' of a pattern is guaranteed to be width + 1. Returns ------- - meas_order: list of int - measurement order + meas_order : list of int + The measurement order. """ graph = self.extract_graph() vin = set(self.input_nodes) @@ -1009,12 +1350,13 @@ def get_measurement_order_from_flow(self) -> list[int] | None: return meas_order def get_measurement_order_from_gflow(self) -> list[int]: - """Return a list containing the node indices, in the order of measurements which can be performed with minimum depth. + """ + Return a list of node indices in the order of measurements that can be performed with minimum depth. Returns ------- meas_order : list of int - measurement order + A list of integers representing the measurement order. """ graph = self.extract_graph() isolated = list(nx.isolates(graph)) @@ -1034,17 +1376,18 @@ def get_measurement_order_from_gflow(self) -> list[int]: return meas_order def sort_measurement_commands(self, meas_order: list[int]) -> list[command.M]: - """Convert measurement order to sequence of measurement commands. + """ + Convert measurement order to a sequence of measurement commands. Parameters ---------- - meas_order: list of int - optimal measurement order. + meas_order : list of int + Optimal measurement order. Returns ------- - meas_cmds: list of command - sorted measurement commands + list of command.M + Sorted measurement commands. """ meas_cmds = [] for i in meas_order: @@ -1058,22 +1401,24 @@ def sort_measurement_commands(self, meas_order: list[int]) -> list[command.M]: return meas_cmds def extract_measurement_commands(self) -> Iterator[command.M]: - """Return measurement commands. + """ + Return measurement commands. Returns ------- - meas_cmds : Iterator[command.M] - measurement commands in the order of measurements + Iterator[command.M] + Measurement commands in the order of measurements. """ yield from (cmd for cmd in self if cmd.kind == CommandKind.M) def get_meas_plane(self) -> dict[int, Plane]: - """Get measurement plane from the pattern. + """ + Get measurement plane from the pattern. Returns ------- - meas_plane: dict of graphix.pauli.Plane - list of planes representing measurement plane for each node. + meas_plane : dict[int, graphix.pauli.Plane] + A dictionary of planes representing the measurement plane for each node. """ meas_plane = {} for cmd in self.__seq: @@ -1082,12 +1427,13 @@ def get_meas_plane(self) -> dict[int, Plane]: return meas_plane def get_angles(self) -> dict[int, ExpressionOrFloat]: - """Get measurement angles of the pattern. + """ + Get measurement angles of the pattern. Returns ------- - angles : dict - measurement angles of the each node. + angles : dict[int, ExpressionOrFloat] + Measurement angles for each node in the pattern. """ angles = {} for cmd in self.__seq: @@ -1096,12 +1442,13 @@ def get_angles(self) -> dict[int, ExpressionOrFloat]: return angles def compute_max_degree(self) -> int: - """Get max degree of a pattern. + """ + Get the maximum degree of a pattern. Returns ------- max_degree : int - max degree of a pattern + The maximum degree of the pattern. """ graph = self.extract_graph() degree = graph.degree() @@ -1109,11 +1456,14 @@ def compute_max_degree(self) -> int: return int(max(dict(degree).values())) def extract_graph(self) -> nx.Graph[int]: - """Return the graph state from the command sequence, extracted from 'N' and 'E' commands. + """ + Return the graph state from the command sequence, + extracted from 'N' and 'E' commands. Returns ------- - graph_state: nx.Graph[int] + graph_state : nx.Graph[int] + The graph state represented as a networkx graph with integer nodes. """ graph: nx.Graph[int] = nx.Graph() graph.add_nodes_from(self.input_nodes) @@ -1129,7 +1479,14 @@ def extract_graph(self) -> nx.Graph[int]: return graph def extract_nodes(self) -> set[int]: - """Return the set of nodes of the pattern.""" + """ + Extract the set of nodes in the pattern. + + Returns + ------- + set[int] + A set containing the unique nodes of the pattern. + """ nodes = set(self.input_nodes) for cmd in self.__seq: if cmd.kind == CommandKind.N: @@ -1137,29 +1494,32 @@ def extract_nodes(self) -> set[int]: return nodes def extract_isolated_nodes(self) -> set[int]: - """Get isolated nodes. + """ + Extract isolated nodes from the pattern. Returns ------- isolated_nodes : set[int] - set of the isolated nodes + A set containing the isolated nodes. """ graph = self.extract_graph() return {node for node, d in graph.degree if d == 0} def get_vops(self, conj: bool = False, include_identity: bool = False) -> dict[int, Clifford]: - """Get local-Clifford decorations from measurement or Clifford commands. + """ + Get local Clifford decorations from measurement or Clifford commands. Parameters ---------- - conj (False) : bool, optional - Apply conjugations to all local Clifford operators. - include_identity (False) : bool, optional - Whether or not to include identity gates in the output + conj : bool, optional + Apply conjugations to all local Clifford operators. Default is False. + include_identity : bool, optional + Whether or not to include identity gates in the output. Default is False. Returns ------- - vops : dict + vops : dict[int, Clifford] + A dictionary mapping integers to their corresponding local Clifford operators. """ vops = {} for cmd in self.__seq: @@ -1180,24 +1540,26 @@ def get_vops(self, conj: bool = False, include_identity: bool = False) -> dict[i return vops def connected_nodes(self, node: int, prepared: set[int] | None = None) -> list[int]: - """Find nodes that are connected to a specified node. + """ + Find nodes that are connected to a specified node. - These nodes must be in the statevector when the specified - node is measured, to ensure correct computation. - If connected nodes already exist in the statevector (prepared), - then they will be ignored as they do not need to be prepared again. + These nodes must be in the state vector when the specified + node is measured, to ensure correct computation. If connected + nodes already exist in the state vector (prepared), then they + will be ignored as they do not need to be prepared again. Parameters ---------- node : int - node index - prepared : list - list of node indices, which are to be ignored + Index of the node for which connected nodes are to be found. + prepared : set[int] | None, optional + A set of node indices to be ignored. If provided, these + nodes will not be included in the result. Returns ------- - node_list : list - list of nodes that are entangled with specified node + list[int] + A list of nodes that are entangled with the specified node. """ if not self.is_standard(): self.standardize() @@ -1218,16 +1580,32 @@ def connected_nodes(self, node: int, prepared: set[int] | None = None) -> list[i return node_list def correction_commands(self) -> list[command.X | command.Z]: - """Return the list of byproduct correction commands.""" + """ + Return a list of byproduct correction commands. + + The method generates a list containing correction commands of type + `command.X` or `command.Z` that correspond to the byproducts + of the current pattern. + + Returns + ------- + list[command.X | command.Z] + A list of correction commands annotated with types X or Z. + """ assert self.is_standard() # Use of `==` here for mypy return [seqi for seqi in self.__seq if seqi.kind == CommandKind.X or seqi.kind == CommandKind.Z] # noqa: PLR1714 def parallelize_pattern(self) -> None: - """Optimize the pattern to reduce the depth of the computation by gathering measurement commands that can be performed simultaneously. + """ + Optimize the pattern to reduce the depth of the computation by gathering measurement commands that can be performed simultaneously. + + This optimized pattern runs efficiently on GPUs and quantum hardware with + depth (e.g., coherence time) limitations. - This optimized pattern runs efficiently on GPUs and quantum hardwares with - depth (e.g. coherence time) limitations. + Returns + ------- + None """ if not self.is_standard(): self.standardize() @@ -1235,11 +1613,13 @@ def parallelize_pattern(self) -> None: self._reorder_pattern(self.sort_measurement_commands(meas_order)) def minimize_space(self) -> None: - """Optimize the pattern to minimize the max_space property of the pattern. + """ + Optimize the pattern to minimize the maximum space requirement. - The optimized pattern has significantly - reduced space requirement (memory space for classical simulation, - and maximum simultaneously prepared qubits for quantum hardwares). + This method optimizes the pattern to reduce the `max_space` property, + which signifies the memory space required for classical simulations and + the maximum number of simultaneously prepared qubits for quantum hardware. + The optimized pattern will significantly decrease these space requirements. """ if not self.is_standard(): self.standardize() @@ -1251,12 +1631,13 @@ def minimize_space(self) -> None: self._reorder_pattern(self.sort_measurement_commands(meas_order)) def _reorder_pattern(self, meas_commands: list[command.M]) -> None: - """Reorder the command sequence. + """ + Reorder the command sequence. Parameters ---------- - meas_commands : list of command - list of measurement ('M') commands + meas_commands : list of command.M + A list of measurement ('M') commands to be reordered. """ prepared = set(self.input_nodes) measured: set[int] = set() @@ -1291,15 +1672,15 @@ def _reorder_pattern(self, meas_commands: list[command.M]) -> None: self.__seq = new def max_space(self) -> int: - """Compute the maximum number of nodes that must be present in the graph (graph space) during the execution of the pattern. + """ + Compute the maximum number of nodes that must be present in the graph (graph space) during the execution of the pattern. - For statevector simulation, this is equivalent to the maximum memory - needed for classical simulation. + This is equivalent to the maximum memory needed for classical simulation in statevector simulation. Returns ------- n_nodes : int - max number of nodes present in the graph during pattern execution. + The maximum number of nodes present in the graph during pattern execution. """ nodes = len(self.input_nodes) max_nodes = nodes @@ -1312,12 +1693,13 @@ def max_space(self) -> int: return max_nodes def space_list(self) -> list[int]: - """Return the list of the number of nodes present in the graph (space) during each step of execution of the pattern (for N and M commands). + """ + Return the list of the number of nodes present in the graph ('space') during each step of execution of the pattern (for 'N' and 'M' commands). Returns ------- - N_list : list - time evolution of 'space' at each 'N' and 'M' commands of pattern. + list[int] + The time evolution of 'space' at each 'N' and 'M' command of the pattern. """ nodes = 0 n_list = [] @@ -1337,46 +1719,54 @@ def simulate_pattern( rng: Generator | None = None, **kwargs: Any, ) -> BackendState: - """Simulate the execution of the pattern by using :class:`graphix.simulator.PatternSimulator`. + """ + Simulate the execution of the pattern using the :class:`graphix.simulator.PatternSimulator`. - Available backend: ['statevector', 'densitymatrix', 'tensornetwork'] + Available backends: + - 'statevector' + - 'densitymatrix' + - 'tensornetwork' Parameters ---------- - backend : str - optional parameter to select simulator backend. - rng: Generator, optional - Random-number generator for measurements. - This generator is used only in case of random branch selection - (see :class:`RandomBranchSelector`). - kwargs: keyword args for specified backend. + backend : str, optional + The simulator backend to use for the simulation. Default is 'statevector'. + input_state : Data, optional + The initial state to use for the simulation. Default is :attr:`BasicStates.PLUS`. + rng : Generator, optional + Random-number generator for measurements. This generator is used only in case + of random branch selection (see :class:`RandomBranchSelector`). + **kwargs : Any + Additional keyword arguments for the specified backend. Returns ------- - state : - quantum state representation for the selected backend. + BackendState + The quantum state representation for the selected backend. - .. seealso:: :class:`graphix.simulator.PatternSimulator` + See Also + -------- + :class:`graphix.simulator.PatternSimulator` """ sim = PatternSimulator(self, backend=backend, **kwargs) sim.run(input_state, rng=rng) return sim.backend.state def perform_pauli_measurements(self, leave_input: bool = False, ignore_pauli_with_deps: bool = False) -> None: - """Perform Pauli measurements in the pattern using efficient stabilizer simulator. + """ + Perform Pauli measurements in the pattern using an efficient stabilizer simulator. Parameters ---------- - leave_input : bool - Optional (*False* by default). - If *True*, measurements on input nodes are preserved as-is in the pattern. - ignore_pauli_with_deps : bool - Optional (*False* by default). - If *True*, Pauli measurements with domains depending on other measures are preserved as-is in the pattern. - If *False*, all Pauli measurements are preprocessed. Formally, measurements are swapped so that all Pauli measurements are applied first, and domains are updated accordingly. - - .. seealso:: :func:`measure_pauli` - + leave_input : bool, optional + If *True*, measurements on input nodes are preserved as-is in the pattern. Default is *False*. + ignore_pauli_with_deps : bool, optional + If *True*, Pauli measurements with domains depending on other measurements are preserved as-is in the pattern. + If *False*, all Pauli measurements are preprocessed. Formally, measurements are swapped so that all Pauli measurements are applied first, and domains are updated accordingly. Default is *False*. + + See also + -------- + measure_pauli : function """ if not ignore_pauli_with_deps: self.move_pauli_measurements_to_the_front() @@ -1393,27 +1783,31 @@ def draw_graph( figsize: tuple[int, int] | None = None, filename: Path | None = None, ) -> None: - """Visualize the underlying graph of the pattern with flow or gflow structure. + """ + Visualize the underlying graph of the pattern with flow or gflow structure. Parameters ---------- - flow_from_pattern : bool - If True, the command sequence of the pattern is used to derive flow or gflow structure. If False, only the underlying graph is used. - show_pauli_measurement : bool - If True, the nodes with Pauli measurement angles are colored light blue. - show_local_clifford : bool - If True, indexes of the local Clifford operator are displayed adjacent to the nodes. - show_measurement_planes : bool - If True, measurement planes are displayed adjacent to the nodes. - show_loop : bool - whether or not to show loops for graphs with gflow. defaulted to True. - node_distance : tuple - Distance multiplication factor between nodes for x and y directions. - figsize : tuple - Figure size of the plot. - filename : Path | None - If not None, filename of the png file to save the plot. If None, the plot is not saved. - Default in None. + flow_from_pattern : bool, optional + If True, the command sequence of the pattern is used to derive flow or gflow structure. If False, only the underlying graph is used. Default is True. + show_pauli_measurement : bool, optional + If True, the nodes with Pauli measurement angles are colored light blue. Default is True. + show_local_clifford : bool, optional + If True, indexes of the local Clifford operator are displayed adjacent to the nodes. Default is False. + show_measurement_planes : bool, optional + If True, measurement planes are displayed adjacent to the nodes. Default is False. + show_loop : bool, optional + Whether or not to show loops for graphs with gflow. Default is True. + node_distance : tuple, optional + Distance multiplication factor between nodes for x and y directions. Default is (1, 1). + figsize : tuple[int, int] | None, optional + Figure size of the plot. If None, a default size will be used. Default is None. + filename : Path | None, optional + If not None, the filename of the PNG file to save the plot. If None, the plot is not saved. Default is None. + + Returns + ------- + None """ graph = self.extract_graph() vin = self.input_nodes @@ -1447,12 +1841,17 @@ def draw_graph( ) def to_qasm3(self, filename: Path | str) -> None: - """Export measurement pattern to OpenQASM 3.0 file. + """ + Export measurement pattern to an OpenQASM 3.0 file. Parameters ---------- - filename : Path | str - file name to export to. example: "filename.qasm" + filename : Path or str + The name of the file to export to. For example: "filename.qasm". + + Returns + ------- + None """ with Path(filename).with_suffix(".qasm").open("w", encoding="utf-8") as file: file.write("// generated by graphix\n") @@ -1470,18 +1869,36 @@ def to_qasm3(self, filename: Path | str) -> None: def is_parameterized(self) -> bool: """ - Return `True` if there is at least one measurement angle that is not just an instance of `SupportsFloat`. - - A parameterized pattern is a pattern where at least one - measurement angle is an expression that is not a number, - typically an instance of `sympy.Expr` (but we don't force to - choose `sympy` here). + Determine if the pattern is parameterized. + Returns + ------- + bool + True if there is at least one measurement angle that is not + an instance of `SupportsFloat`. A parameterized pattern is defined + as one where at least one measurement angle is an expression + that is not a number, typically an instance of `sympy.Expr`, + though other types may also be used. """ return any(not isinstance(cmd.angle, SupportsFloat) for cmd in self if cmd.kind == command.CommandKind.M) def subs(self, variable: Parameter, substitute: ExpressionOrSupportsFloat) -> Pattern: - """Return a copy of the pattern where all occurrences of the given variable in measurement angles are substituted by the given value.""" + """ + Return a copy of the pattern with all occurrences of the specified variable + in measurement angles substituted by the given value. + + Parameters + ---------- + variable : Parameter + The variable to be substituted in the measurement angles. + substitute : ExpressionOrSupportsFloat + The value that will replace the variable in the measurement angles. + + Returns + ------- + Pattern + A new Pattern instance with the substitutions made. + """ result = self.copy() for cmd in result: if cmd.kind == command.CommandKind.M: @@ -1489,7 +1906,22 @@ def subs(self, variable: Parameter, substitute: ExpressionOrSupportsFloat) -> Pa return result def xreplace(self, assignment: Mapping[Parameter, ExpressionOrSupportsFloat]) -> Pattern: - """Return a copy of the pattern where all occurrences of the given keys in measurement angles are substituted by the given values in parallel.""" + """ + Return a copy of the pattern with all occurrences of the specified keys + in measurement angles replaced by the corresponding values in parallel. + + Parameters + ---------- + assignment : Mapping[Parameter, ExpressionOrSupportsFloat] + A mapping of parameters to their replacement values. Each key + must be a parameter present in the pattern, and each value + should be an expression or a type that supports float conversion. + + Returns + ------- + Pattern + A new instance of the pattern with the substitutions applied. + """ result = self.copy() for cmd in result: if cmd.kind == command.CommandKind.M: @@ -1497,7 +1929,14 @@ def xreplace(self, assignment: Mapping[Parameter, ExpressionOrSupportsFloat]) -> return result def copy(self) -> Pattern: - """Return a copy of the pattern.""" + """ + Returns a copy of the current pattern. + + Returns + -------- + Pattern + A new instance of the Pattern that is a copy of the current instance. + """ result = self.__new__(self.__class__) result.__seq = [copy.copy(cmd) for cmd in self.__seq] result.__input_nodes = self.__input_nodes.copy() @@ -1508,7 +1947,19 @@ def copy(self) -> Pattern: return result def move_pauli_measurements_to_the_front(self, leave_nodes: set[int] | None = None) -> None: - """Move all the Pauli measurements to the front of the sequence (except nodes in `leave_nodes`).""" + """ + Move all the Pauli measurements to the front of the sequence. + + Parameters + ---------- + leave_nodes : set[int] | None, optional + A set of node indices that should not be moved to the front. + If None, all nodes will be processed. + + Returns + ------- + None + """ if leave_nodes is None: leave_nodes = set() self.standardize() @@ -1516,12 +1967,14 @@ def move_pauli_measurements_to_the_front(self, leave_nodes: set[int] | None = No shift_domains: dict[int, set[int]] = {} def expand_domain(domain: set[int]) -> None: - """Merge previously shifted domains into ``domain``. + """ + Merge previously shifted domains into the given domain. Parameters ---------- domain : set[int] - Domain to update with any accumulated shift information. + A set of integers representing the domain to be updated with any + accumulated shift information. """ for node in domain & shift_domains.keys(): domain ^= shift_domains[node] @@ -1590,32 +2043,34 @@ def expand_domain(domain: set[int]) -> None: def measure_pauli(pattern: Pattern, leave_input: bool, copy: bool = False) -> Pattern: - """Perform Pauli measurement of a pattern by fast graph state simulator. + """ + Perform a Pauli measurement of a pattern using a fast graph state simulator. - Uses the decorated-graph method implemented in graphix.graphsim to perform - the measurements in Pauli bases, and then sort remaining nodes back into - pattern together with Clifford commands. + This function utilizes the decorated-graph method implemented in `graphix.graphsim` + to perform measurements in Pauli bases, and subsequently sorts the remaining nodes + back into the pattern along with any corresponding Clifford commands. - TODO: non-XY plane measurements in original pattern + TODO: Implement support for non-XY plane measurements in the original pattern. Parameters ---------- - pattern : graphix.pattern.Pattern object + pattern : graphix.pattern.Pattern + The pattern object representing the quantum state to be measured. leave_input : bool - True: input nodes will not be removed - False: all the nodes measured in Pauli bases will be removed - copy : bool - True: changes will be applied to new copied object and will be returned - False: changes will be applied to the supplied Pattern object + If True, input nodes will not be removed; if False, all nodes measured in Pauli bases will be removed. + copy : bool, optional + If True, changes will be applied to a new copied object and this new pattern will be returned. + If False, changes will be applied to the supplied Pattern object. Default is False. Returns ------- - new_pattern : graphix.Pattern object - pattern with Pauli measurement removed. - only returned if copy argument is True. + new_pattern : graphix.pattern.Pattern + A pattern with the results of the Pauli measurement applied. This is only returned if + the `copy` argument is set to True. - - .. seealso:: :class:`graphix.graphsim.GraphState` + See Also + -------- + graphix.graphsim.GraphState : For details on the graph state representation and operations. """ standardized_pattern = optimization.StandardizedPattern(pattern) graph = standardized_pattern.extract_graph() @@ -1702,18 +2157,24 @@ def measure_pauli(pattern: Pattern, leave_input: bool, copy: bool = False) -> Pa def pauli_nodes( pattern: optimization.StandardizedPattern, leave_input: bool ) -> tuple[list[tuple[command.M, PauliMeasurement]], set[int]]: - """Return the list of measurement commands that are in Pauli bases and that are not dependent on any non-Pauli measurements. + """ + Return the list of measurement commands that are in Pauli bases and that + are not dependent on any non-Pauli measurements. Parameters ---------- pattern : optimization.StandardizedPattern + The standardized pattern containing measurement commands. leave_input : bool + If True, input measurements are retained in the output. Returns ------- - pauli_node : list - list of measures + pauli_nodes : list[tuple[command.M, PauliMeasurement]] + A list of measures that are in Pauli bases and independent of + non-Pauli measurements. non_pauli_nodes : set[int] + A set of indices corresponding to the non-Pauli measurement nodes. """ pauli_node: list[tuple[command.M, PauliMeasurement]] = [] # Nodes that are non-Pauli measured, or pauli measured but depends on pauli measurement @@ -1745,18 +2206,18 @@ def pauli_nodes( def cmd_to_qasm3(cmd: Command) -> Iterator[str]: - """Convert a command in the pattern into OpenQASM 3.0 statement. + """ + Convert a command in the pattern into OpenQASM 3.0 statement. - Parameter - --------- - cmd : list - command [type:str, node:int, attr] + Parameters + ---------- + cmd : Command + A command object which contains the command details, including type, node, and attributes. Yields ------ - string - translated pattern commands in OpenQASM 3.0 language - + str + Translated pattern commands in OpenQASM 3.0 language. """ if cmd.kind == CommandKind.N: qubit = cmd.node @@ -1825,7 +2286,26 @@ def cmd_to_qasm3(cmd: Command) -> Iterator[str]: def assert_permutation(original: list[int], user: list[int]) -> None: - """Check that the provided `user` node list is a permutation from `original`.""" + """ + Check that the provided `user` list is a permutation of the `original` list. + + Parameters + ---------- + original : list of int + The original list of integers. + user : list of int + The user-provided list of integers to check against the original list. + + Raises + ------ + AssertionError + If `user` is not a permutation of `original`. + + Notes + ----- + A permutation means that both lists contain the same elements in possibly + different orders, with the same frequency for each element. + """ node_set = set(user) if node_set != set(original): raise ValueError(f"{node_set} != {set(original)}") @@ -1838,20 +2318,49 @@ def assert_permutation(original: list[int], user: list[int]) -> None: @dataclass class ExtractedSignal: - """Return data structure for `extract_signal`.""" + """ + A data structure to represent the extracted signal. + """ s_domain: set[int] - "New `s_domain` for the measure command." + """ + The S domain. + """ t_domain: set[int] - "New `t_domain` for the measure command." + """ + The T domain. + """ signal: set[int] - "Domain for the shift command." + """ + The signal. + """ def extract_signal(plane: Plane, s_domain: set[int], t_domain: set[int]) -> ExtractedSignal: - """Extract signal from domains.""" + """ + Extract a signal from the specified S and T domains. + + Parameters + ---------- + plane : Plane + The Plane object from which the signal will be extracted. + s_domain : set[int] + A set of integers representing the S indices defining the S domain. + t_domain : set[int] + A set of integers representing the T indices defining the T domain. + + Returns + ------- + ExtractedSignal + An object containing the extracted signal data. + + Notes + ----- + The function assumes that the provided domains are valid and within the bounds of the data + contained in the Plane object. Ensure that both domains are non-empty sets for successful extraction. + """ if plane == Plane.XY: return ExtractedSignal(s_domain=s_domain, t_domain=set(), signal=t_domain) if plane == Plane.XZ: @@ -1862,7 +2371,8 @@ def extract_signal(plane: Plane, s_domain: set[int], t_domain: set[int]) -> Extr def shift_outcomes(outcomes: dict[int, Outcome], signal_dict: dict[int, set[int]]) -> dict[int, Outcome]: - """Update outcomes with shifted signals. + """ + Update outcomes with shifted signals. Shifted signals (as returned by the method :func:`Pattern.shift_signals`) affect classical outputs @@ -1875,7 +2385,7 @@ def shift_outcomes(outcomes: dict[int, Outcome], signal_dict: dict[int, set[int] Parameters ---------- - outcomes : dict[int, int] + outcomes : dict[int, Outcome] Classical outputs. signal_dict : dict[int, set[int]] For each node, the signal that has been shifted @@ -1883,9 +2393,8 @@ def shift_outcomes(outcomes: dict[int, Outcome], signal_dict: dict[int, set[int] Returns ------- - shifted_outcomes : dict[int, int] + shifted_outcomes : dict[int, Outcome] Classical outputs updated with shifted signals. - """ return { node: toggle_outcome(outcome) if sum(outcomes[i] for i in signal_dict.get(node, [])) % 2 == 1 else outcome diff --git a/graphix/pauli.py b/graphix/pauli.py index 6b50b5686..020fe196b 100644 --- a/graphix/pauli.py +++ b/graphix/pauli.py @@ -1,4 +1,39 @@ -"""Pauli gates ± {1,j} × {I, X, Y, Z}.""" +""" +Pauli Gates +------------ + +This module provides implementations of the Pauli gates, which include +the set {I, X, Y, Z} combined with the scaling factors ±{1, j}. + +The Pauli gates are fundamental quantum gates used in quantum computing +that represent basic quantum operations. + +Attributes +---------- +I : matrix + The identity gate. +X : matrix + The Pauli-X gate (also known as NOT gate). +Y : matrix + The Pauli-Y gate. +Z : matrix + The Pauli-Z gate. + +Usage +----- +These gates can be applied to qubits in a quantum circuit to perform quantum +operations. + +Examples +-------- +To apply a Pauli-X gate on a qubit: + + |0⟩ → X|0⟩ = |1⟩ + +To apply a Pauli-Z gate with a phase factor: + + Z = j * Z +""" from __future__ import annotations @@ -22,17 +57,41 @@ class _PauliMeta(type): def __iter__(cls) -> Iterator[Pauli]: - """Iterate over all Pauli gates, including the unit.""" + """ + Iterate over all Pauli gates, including the unit. + + Yields + ------- + Pauli + An iterator over all possible Pauli gates, including the identity gate. + + Notes + ----- + The Pauli gates include: I (identity), X, Y, Z. + This method provides an iterable interface to access all of them in sequence. + """ return Pauli.iterate() @dataclasses.dataclass(frozen=True) class Pauli(metaclass=_PauliMeta): - r"""Pauli gate: ``u * {I, X, Y, Z}`` where u is a complex unit. + """ + Pauli gate: ``u * {I, X, Y, Z}``, where u is a complex unit. - Pauli gates can be multiplied with other Pauli gates (with ``@``), - with complex units and unit constants (with ``*``), - and can be negated. + This class represents the Pauli gates, which can be combined with other + Pauli gates using the matrix multiplication operator (``@``), and with + complex units and unit constants using the multiplication operator (``*``). + Additionally, Pauli gates can be negated. + + Attributes + ---------- + - None + + Methods + ------- + - __matmul__(other): Multiplies this Pauli gate by another. + - __mul__(other): Scales this Pauli gate by a complex unit. + - __neg__(): Negates this Pauli gate. """ symbol: IXYZ = IXYZ.I @@ -44,14 +103,30 @@ class Pauli(metaclass=_PauliMeta): @staticmethod def from_axis(axis: Axis) -> Pauli: - """Return the Pauli associated to the given axis.""" + """ + Create a Pauli object from the specified axis. + + Parameters + ---------- + axis : Axis + The axis associated with the desired Pauli operation. + + Returns + ------- + Pauli + The Pauli object corresponding to the provided axis. + """ return Pauli(IXYZ[axis.name]) @property def axis(self) -> Axis: - """Return the axis associated to the Pauli. + """ + Return the axis associated with the Pauli operator. - Fails if the Pauli is identity. + Raises + ------ + ValueError + If the Pauli operator is the identity. """ if self.symbol == IXYZ.I: raise ValueError("I is not an axis.") @@ -59,7 +134,14 @@ def axis(self) -> Axis: @property def matrix(self) -> npt.NDArray[np.complex128]: - """Return the matrix of the Pauli gate.""" + """ + Return the matrix representation of the Pauli gate. + + Returns + ------- + npt.NDArray[np.complex128] + A 2x2 complex numpy array representing the matrix of the Pauli gate. + """ co = complex(self.unit) if self.symbol == IXYZ.I: return co * Ops.I @@ -72,7 +154,19 @@ def matrix(self) -> npt.NDArray[np.complex128]: typing_extensions.assert_never(self.symbol) def eigenstate(self, binary: int = 0) -> PlanarState: - """Return the eigenstate of the Pauli.""" + """ + Return the eigenstate of the Pauli operator. + + Parameters + ---------- + binary : int, optional + The binary representation of the eigenstate, default is 0. + + Returns + ------- + PlanarState + The eigenstate corresponding to the specified binary input. + """ if binary not in {0, 1}: raise ValueError("b must be 0 or 1.") if self.symbol == IXYZ.X: @@ -87,7 +181,19 @@ def eigenstate(self, binary: int = 0) -> PlanarState: typing_extensions.assert_never(self.symbol) def _repr_impl(self, prefix: str | None) -> str: - """Return ``repr`` string with an optional prefix.""" + """ + Return the string representation of the Pauli operator with an optional prefix. + + Parameters + ---------- + prefix : str or None + An optional string to prepend to the representation. If None, no prefix is added. + + Returns + ------- + str + The string representation of the Pauli operator, optionally prefixed. + """ sym = self.symbol.name if prefix is not None: sym = f"{prefix}.{sym}" @@ -102,11 +208,25 @@ def _repr_impl(self, prefix: str | None) -> str: typing_extensions.assert_never(self.unit) def __repr__(self) -> str: - """Return a string representation of the Pauli.""" + """ + Return a string representation of the Pauli. + + Returns + ------- + str + A string representing the Pauli object. + """ return self._repr_impl(self.__class__.__name__) def __str__(self) -> str: - """Return a simplified string representation of the Pauli.""" + """ + Return a simplified string representation of the Pauli operators. + + Returns + ------- + str + A string representation of the Pauli operator. + """ return self._repr_impl(None) @staticmethod @@ -134,32 +254,89 @@ def _matmul_impl(lhs: IXYZ, rhs: IXYZ) -> Pauli: raise RuntimeError("Unreachable.") # pragma: no cover def __matmul__(self, other: Pauli) -> Pauli: - """Return the product of two Paulis.""" + """ + Compute the matrix product of two Pauli operators. + + Parameters + ---------- + other : Pauli + The Pauli operator to multiply with the current instance. + + Returns + ------- + Pauli + A new Pauli operator that is the result of the matrix multiplication. + """ if isinstance(other, Pauli): return self._matmul_impl(self.symbol, other.symbol) * (self.unit * other.unit) return NotImplemented def __mul__(self, other: ComplexUnit | SupportsComplexCtor) -> Pauli: - """Return the product of two Paulis.""" + """ + Return the product of two Paulis. + + Parameters + ---------- + other : ComplexUnit | SupportsComplexCtor + The Pauli or complex number to multiply with the current Pauli object. + + Returns + ------- + Pauli + The resulting Pauli object from the multiplication. + """ if u := ComplexUnit.try_from(other): return dataclasses.replace(self, unit=self.unit * u) return NotImplemented def __rmul__(self, other: ComplexUnit | SupportsComplexCtor) -> Pauli: - """Return the product of two Paulis.""" + """ + Return the product of a scalar and a Pauli operator. + + This method is invoked when the left operand is a scalar and the right + operand is an instance of the Pauli class. The result is a new Pauli + operator that represents the scalar multiplication of the original Pauli. + + Parameters + ---------- + other : ComplexUnit | SupportsComplexCtor + A scalar value (complex or compatible type) to multiply with this Pauli operator. + + Returns + ------- + Pauli + A new Pauli operator that is the result of the scalar multiplication. + """ return self.__mul__(other) def __neg__(self) -> Pauli: - """Return the opposite.""" + """ + Return the negation of the Pauli operator. + + This method returns a new Pauli operator that represents the opposite + of the current operator. + + Returns + ------- + Pauli + A new Pauli instance that is the negation of the current instance. + """ return dataclasses.replace(self, unit=-self.unit) @staticmethod def iterate(symbol_only: bool = False) -> Iterator[Pauli]: - """Iterate over all Pauli gates. + """ + Iterate over all Pauli gates. Parameters ---------- - symbol_only (bool, optional): Exclude the unit in the iteration. Defaults to False. + symbol_only : bool, optional + If True, exclude the unit in the iteration. Default is False. + + Yields + ------ + Pauli + An iterator over all Pauli gates, potentially omitting the unit gate based on the `symbol_only` parameter. """ us = (ComplexUnit.ONE,) if symbol_only else tuple(ComplexUnit) for symbol in IXYZ: diff --git a/graphix/pretty_print.py b/graphix/pretty_print.py index 773f10d30..a887bc466 100644 --- a/graphix/pretty_print.py +++ b/graphix/pretty_print.py @@ -1,4 +1,9 @@ -"""Pretty-printing utilities.""" +""" +Pretty-printing utilities. + +This module provides a collection of utilities for formatting and displaying +data structures in a more readable and visually appealing manner. +""" from __future__ import annotations @@ -20,7 +25,26 @@ class OutputFormat(Enum): - """Enumeration of the output format for pretty-printing.""" + """ + Enumeration of the output formats for pretty-printing. + + Attributes + ---------- + JSON : str + A string representation of the JSON format. + XML : str + A string representation of the XML format. + CSV : str + A string representation of the CSV format. + PLAIN : str + A string representation of the plain text format. + + Notes + ----- + This class serves as a simple enumeration for defining various + output formats used in the pretty-printing of data. These formats + can be utilized to specify how data should be rendered or displayed. + """ ASCII = enum.auto() LaTeX = enum.auto() @@ -30,12 +54,12 @@ class OutputFormat(Enum): def angle_to_str( angle: float, output: OutputFormat, max_denominator: int = 1000, multiplication_sign: bool = False ) -> str: - r""" + """ Return a string representation of an angle given in units of π. - - If the angle is a "simple" fraction of π (within the given max_denominator and a small tolerance), - it returns a fractional string, e.g. "π/2", "2π", or "-3π/4". - - Otherwise, it returns the angle in radians (angle * π) formatted to two decimal places. + If the angle is a "simple" fraction of π (within the given `max_denominator` and a small tolerance), + it returns a fractional string, e.g. "π/2", "2π", or "-3π/4". + Otherwise, it returns the angle in radians (angle * π) formatted to two decimal places. Parameters ---------- @@ -44,13 +68,12 @@ def angle_to_str( output : OutputFormat Desired formatting style: Unicode (π symbol), LaTeX (\pi), or ASCII ("pi"). max_denominator : int, optional - Maximum denominator for detecting a simple fraction (default: 1000). - multiplication_sign : bool - Optional (default: ``False``). - If ``True``, the multiplication sign is made explicit between the + Maximum denominator for detecting a simple fraction (default is 1000). + multiplication_sign : bool, optional + If ``True``, the multiplication sign is made explicit between the numerator and π: ``2×π`` in Unicode, ``2 \times \pi`` in LaTeX, and ``2*pi`` in ASCII. - If ``False``, the multiplication sign is implicit: + If ``False`` (default), the multiplication sign is implicit: ``2π`` in Unicode, ``2\pi`` in LaTeX, ``2pi`` in ASCII. Returns @@ -100,7 +123,20 @@ def mkfrac(num: str, den: str) -> str: def domain_to_str(domain: set[Node]) -> str: - """Return the string representation of a domain.""" + """ + Convert a set of Nodes to its string representation. + + Parameters + ---------- + domain : set[Node] + A set containing Node objects that define a domain. + + Returns + ------- + str + A string representation of the domain, concatenated from the + string representations of each Node in the set. + """ return f"{{{','.join(str(node) for node in domain)}}}" @@ -109,14 +145,20 @@ def domain_to_str(domain: set[Node]) -> str: def command_to_str(cmd: command.Command, output: OutputFormat) -> str: - """Return the string representation of a command according to the given format. + """ + Return the string representation of a command according to the given format. Parameters ---------- - cmd: Command + cmd : Command The command to pretty print. - output: OutputFormat + output : OutputFormat The expected format. + + Returns + ------- + str + The string representation of the command in the specified output format. """ out = [cmd.kind.name] @@ -205,18 +247,28 @@ def pattern_to_str( limit: int = 40, target: Container[command.CommandKind] | None = None, ) -> str: - """Return the string representation of a pattern according to the given format. + """ + Return the string representation of a pattern according to the given format. Parameters ---------- - pattern: Pattern + pattern : Pattern The pattern to pretty print. - output: OutputFormat + output : OutputFormat The expected format. - left_to_right: bool - Optional. If `True`, the first command will appear on the beginning of - the resulting string. If `False` (the default), the first command will - appear at the end of the string. + left_to_right : bool, optional + If `True`, the first command will appear at the beginning of the resulting + string. If `False` (the default), the first command will appear at the + end of the string. + limit : int, optional + The maximum length of the resulting string. Default is 40. + target : Container[command.CommandKind] or None, optional + A collection of command kinds to filter or limit the output to. Default is None. + + Returns + ------- + str + The string representation of the pattern in the specified format. """ separator = r"\," if output == OutputFormat.LaTeX else " " command_list = list(pattern) diff --git a/graphix/pyzx.py b/graphix/pyzx.py index e3d355483..ca9dc3174 100644 --- a/graphix/pyzx.py +++ b/graphix/pyzx.py @@ -1,7 +1,10 @@ -"""Functionality for converting between OpenGraphs and :mod:`pyzx`. +""" +Functionality for converting between OpenGraphs and :mod:`pyzx`. -These functions are held in their own file rather than including them in the -OpenGraph class because we want :mod:`pyzx` to be an optional dependency. +This module provides functions for converting OpenGraphs to and from the +:mod:`pyzx` library. These functions are implemented in a separate file to +ensure that :mod:`pyzx` remains an optional dependency, allowing for +greater flexibility in environments where it may not be installed. """ from __future__ import annotations @@ -32,16 +35,28 @@ def _fraction_of_angle(angle: ExpressionOrFloat) -> Fraction: def to_pyzx_graph(og: OpenGraph) -> BaseGraph[int, tuple[int, int]]: - """Return a :mod:`pyzx` graph corresponding to the open graph. + """ + Convert an OpenGraph to a :mod:`pyzx` graph representation. + + Parameters + ---------- + og : OpenGraph + The open graph to be converted into a :mod:`pyzx` graph. - Example + Returns ------- + BaseGraph[int, tuple[int, int]] + A :mod:`pyzx` graph corresponding to the given open graph. + + Examples + -------- >>> import networkx as nx >>> from graphix.pyzx import to_pyzx_graph >>> g = nx.Graph([(0, 1), (1, 2)]) >>> inputs = [0] >>> outputs = [2] - >>> measurements = {0: Measurement(0, Plane.XY), 1: Measurement(1, Plane.YZ)} + >>> measurements = {0: Measurement(0, Plane.XY), + ... 1: Measurement(1, Plane.YZ)} >>> og = OpenGraph(g, measurements, inputs, outputs) >>> reconstructed_pyzx_graph = to_pyzx_graph(og) """ @@ -115,16 +130,29 @@ def _checked_float(x: FractionLike) -> float: def from_pyzx_graph(g: BaseGraph[int, tuple[int, int]]) -> OpenGraph: - """Construct an :class:`OpenGraph` from a :mod:`pyzx` graph. + """ + Construct an :class:`OpenGraph` from a :mod:`pyzx` graph. This method may add additional nodes to the graph so that it adheres - with the definition of an OpenGraph. For instance, if the final node on + to the definition of an OpenGraph. For instance, if the final node on a qubit is measured, it will add two nodes behind it so that no output - nodes are measured to satisfy the requirements of an open graph. - .. warning:: - works with `pyzx==0.8.0` (see `requirements-dev.txt`). Other versions may not be compatible due to breaking changes in `pyzx` - Example + nodes are measured, thereby satisfying the requirements of an OpenGraph. + + .. warning:: + Works with `pyzx==0.8.0` (see `requirements-dev.txt`). Other versions may not be compatible due to breaking changes in `pyzx`. + + Parameters + ---------- + g : BaseGraph[int, tuple[int, int]] + The input graph of type `BaseGraph` consisting of integer nodes and edges represented by tuples of integers. + + Returns ------- + OpenGraph + An instance of `OpenGraph` constructed from the input `pyzx` graph. + + Examples + -------- >>> import pyzx as zx >>> from graphix.pyzx import from_pyzx_graph >>> circ = zx.qasm("qreg q[2]; h q[1]; cx q[0], q[1]; h q[1];") diff --git a/graphix/qasm3_exporter.py b/graphix/qasm3_exporter.py index 60e9dec5f..58c8fe245 100644 --- a/graphix/qasm3_exporter.py +++ b/graphix/qasm3_exporter.py @@ -1,4 +1,10 @@ -"""Exporter to OpenQASM3.""" +""" +Exporter to OpenQASM3. + +This module provides functionality to export quantum circuit representations +to the OpenQASM3 format, allowing for interoperability with various quantum +computing platforms and tools that support OpenQASM3. +""" from __future__ import annotations @@ -21,7 +27,13 @@ def circuit_to_qasm3(circuit: Circuit) -> str: - """Export circuit instructions to OpenQASM 3.0 representation. + """ + Export circuit instructions to OpenQASM 3.0 representation. + + Parameters + ---------- + circuit : Circuit + The circuit containing the instructions to be exported. Returns ------- @@ -32,12 +44,18 @@ def circuit_to_qasm3(circuit: Circuit) -> str: def circuit_to_qasm3_lines(circuit: Circuit) -> Iterator[str]: - """Export circuit instructions to line-by-line OpenQASM 3.0 representation. + """ + Export circuit instructions to a line-by-line OpenQASM 3.0 representation. + + Parameters + ---------- + circuit : Circuit + The circuit to be exported. Returns ------- Iterator[str] - The OpenQASM 3.0 lines that represent the circuit. + An iterator over the OpenQASM 3.0 lines that represent the circuit. """ yield "OPENQASM 3;" yield 'include "stdgates.inc";' @@ -49,12 +67,54 @@ def circuit_to_qasm3_lines(circuit: Circuit) -> Iterator[str]: def qasm3_qubit(index: int) -> str: - """Return the name of the indexed qubit.""" + """ + Return the name of the indexed qubit. + + Parameters + ---------- + index : int + The index of the qubit, which should be a non-negative integer. + + Returns + ------- + str + The name of the qubit in QASM 3 format, represented as 'q[]'. + + Raises + ------ + ValueError + If the index is negative. + """ return f"q[{index}]" def qasm3_gate_call(gate: str, operands: Iterable[str], args: Iterable[str] | None = None) -> str: - """Return the OpenQASM3 gate call.""" + """ + Return the OpenQASM3 gate call. + + Parameters + ---------- + gate : str + The name of the quantum gate to be invoked. + operands : Iterable[str] + The list of qubit operands on which the gate operates. + args : Iterable[str] | None, optional + Additional arguments for the gate, if applicable. If no additional + arguments are needed, this can be set to None. The default is None. + + Returns + ------- + str + The formatted OpenQASM3 gate call as a string. + + Examples + -------- + >>> qasm3_gate_call('cx', ['q[0]', 'q[1]']) + 'cx q[0], q[1];' + + >>> qasm3_gate_call('rz', ['q[0]'], args=['1.57']) + 'rz(1.57) q[0];' + """ operands_str = ", ".join(operands) if args is None: return f"{gate} {operands_str}" @@ -63,7 +123,27 @@ def qasm3_gate_call(gate: str, operands: Iterable[str], args: Iterable[str] | No def angle_to_qasm3(angle: ExpressionOrFloat) -> str: - """Get the OpenQASM3 representation of an angle.""" + """ + Convert an angle to its OpenQASM3 string representation. + + Parameters + ---------- + angle : ExpressionOrFloat + The angle to be converted, which can be of type Expression or a float value. + + Returns + ------- + str + The OpenQASM3 representation of the given angle. + + Examples + -------- + >>> angle_to_qasm3(1.5708) + '1.5708' + + >>> angle_to_qasm3('pi/2') + 'pi/2' + """ if not isinstance(angle, float): raise TypeError("QASM export of symbolic pattern is not supported") rad_over_pi = angle / pi @@ -71,7 +151,19 @@ def angle_to_qasm3(angle: ExpressionOrFloat) -> str: def instruction_to_qasm3(instruction: Instruction) -> str: - """Get the OpenQASM3 representation of a single circuit instruction.""" + """ + Convert a single circuit instruction to its OpenQASM3 representation. + + Parameters + ---------- + instruction : Instruction + The circuit instruction to be converted. + + Returns + ------- + str + The OpenQASM3 representation of the given instruction. + """ if instruction.kind == InstructionKind.M: if PauliMeasurement.try_from(instruction.plane, instruction.angle) != PauliMeasurement(Axis.Z, Sign.PLUS): raise ValueError("OpenQASM3 only supports measurements in Z axis.") diff --git a/graphix/random_objects.py b/graphix/random_objects.py index 5249c8327..9f2048da1 100644 --- a/graphix/random_objects.py +++ b/graphix/random_objects.py @@ -1,4 +1,4 @@ -"""Functions to generate various random objects.""" +"Functions to generate various random objects." from __future__ import annotations @@ -24,14 +24,44 @@ def rand_herm(sz: int, rng: Generator | None = None) -> npt.NDArray[np.complex128]: - """Generate random hermitian matrix of size sz*sz.""" + """ + Generate a random Hermitian matrix of size (sz, sz). + + Parameters + ---------- + sz : int + The size of the desired Hermitian matrix, which will be of shape (sz, sz). + rng : Generator, optional + A random number generator instance to use for generating random numbers. + If None, the default random generator will be used. + + Returns + ------- + npt.NDArray[np.complex128] + A Hermitian matrix of shape (sz, sz) with complex128 data type. + """ rng = ensure_rng(rng) tmp = rng.random(size=(sz, sz)) + 1j * rng.random(size=(sz, sz)) return tmp + tmp.conj().T def rand_unit(sz: int, rng: Generator | None = None) -> npt.NDArray[np.complex128]: - """Generate haar random unitary matrix of size sz*sz.""" + """ + Generate a Haar random unitary matrix of size (sz, sz). + + Parameters + ---------- + sz : int + The size of the unitary matrix to be generated (number of rows/columns). + rng : Generator or None, optional + A random number generator for reproducibility. If None, the default + random number generator from NumPy is used. Default is None. + + Returns + ------- + npt.NDArray[np.complex128] + A sz x sz unitary matrix sampled according to the Haar measure. + """ rng = ensure_rng(rng) if sz == 1: return np.array([np.exp(1j * rng.random(size=1) * 2 * np.pi)]) @@ -42,20 +72,28 @@ def rand_unit(sz: int, rng: Generator | None = None) -> npt.NDArray[np.complex12 def rand_dm(dim: int, rng: Generator | None = None, rank: int | None = None) -> npt.NDArray[np.complex128]: - """Generate random density matrices (positive semi-definite matrices with unit trace). + """ + Generate random density matrices (positive semi-definite matrices with unit trace). + + The function returns either a :class:`graphix.sim.density_matrix.DensityMatrix` or a :class:`np.ndarray` depending on the provided parameters. - Returns either a :class:`graphix.sim.density_matrix.DensityMatrix` or a :class:`np.ndarray` depending on the parameter *dm_dtype*. + Parameters + ---------- + dim : int + Linear dimension of the (square) matrix. + rank : int, optional + Rank of the density matrix (1 = pure state). If not specified, the rank will be set to `dim` (maximal rank). Defaults to None. + rng : Generator, optional + A random number generator for reproducibility. Defaults to None, which uses the default random number generator. - :param dim: Linear dimension of the (square) matrix - :type dim: int - :param rank: Rank of the density matrix (1 = pure state). If not specified then sent to dim (maximal rank). - Defaults to None - :type rank: int, optional - :return: the density matrix in the specified format. - :rtype: DensityMatrix | np.ndarray + Returns + ------- + npt.NDArray[np.complex128] + The density matrix in the specified format. - .. note:: - Thanks to Ulysse Chabaud. + Notes + ----- + Thanks to Ulysse Chabaud. """ rng = ensure_rng(rng) @@ -76,18 +114,30 @@ def rand_dm(dim: int, rng: Generator | None = None, rank: int | None = None) -> def rand_gauss_cpx_mat( dim: int, rng: Generator | None = None, sig: float = 1 / np.sqrt(2) ) -> npt.NDArray[np.complex128]: - """Return a square array of standard normal complex random variates. + """ + Return a square array of standard normal complex random variates. + + This function generates a square matrix of complex numbers where each + entry is sampled from a standard normal distribution. The entries are + drawn independently. Code from QuTiP: https://qutip.org/docs/4.0.2/modules/qutip/random_objects.html Parameters ---------- dim : int - Linear dimension of the (square) matrix - sig : float - standard deviation of random variates. - ``sig = 'ginibre`` draws from the Ginibre ensemble ie sig = 1 / sqrt(2 * dim). + Linear dimension of the (square) matrix. + rng : Generator | None, optional + Random number generator instance. If None, the default generator is used. + sig : float, optional + Standard deviation of the random variates. The default is + ``sig = 1 / np.sqrt(2)`. For drawing from the Ginibre ensemble, + set `sig = 1 / np.sqrt(2 * dim)`. + Returns + ------- + npt.NDArray[np.complex128] + A square array of complex random variates with shape (dim, dim). """ rng = ensure_rng(rng) @@ -100,22 +150,35 @@ def rand_gauss_cpx_mat( def rand_channel_kraus( dim: int, rng: Generator | None = None, rank: int | None = None, sig: float = 1 / np.sqrt(2) ) -> KrausChannel: - """Return a random :class:`graphix.sim.channels.KrausChannel` object of given dimension and rank. + """ + Return a random :class:`graphix.sim.channels.KrausChannel` object of the given dimension and rank. - Following the method of - [KNPPZ21] Kukulski, Nechita, Pawela, Puchała, Życzkowsk https://arxiv.org/pdf/2011.02994.pdf + This function generates a randomized Kraus channel following the method outlined in + [KNPPZ21] Kukulski, Nechita, Pawela, Puchała, Życzkowsk, + available at https://arxiv.org/pdf/2011.02994.pdf. Parameters ---------- dim : int Linear dimension of the (square) matrix of each Kraus operator. - Only square operators so far. + Only square operators are supported. - rank : int (default to full *rank dimension**2*) - Choi rank ie the number of Kraus operators. Must be between one and *dim**2*. + rank : int, optional + Choi rank, i.e., the number of Kraus operators. Must be between one and + `dim**2`. Defaults to the full rank (`dim**2`). - sig : see rand_cpx + rng : Generator, optional + A random number generator instance. If None, the default random number generator + is used. + sig : float, default=1/√2 + A parameter affecting the generated Kraus operators. This parameter is used + as indicated in the `rand_cpx` function. + + Returns + ------- + KrausChannel + A random KrausChannel object with the specified parameters. """ rng = ensure_rng(rng) @@ -141,7 +204,29 @@ def rand_channel_kraus( # or merge with previous with a "pauli" kwarg? # continue here def rand_pauli_channel_kraus(dim: int, rng: Generator | None = None, rank: int | None = None) -> KrausChannel: - """Return a random Kraus channel operator.""" + """ + Generate a random Kraus channel operator based on the Pauli channel. + + Parameters + ---------- + dim : int + The dimension of the system (the Hilbert space dimension). + rng : Generator, optional + A random number generator instance. If None, a default random number generator is used. + rank : int, optional + The rank of the Kraus operators. If None, a default rank is chosen. + + Returns + ------- + KrausChannel + An instance of a KrausChannel representing the random Pauli channel. + + Notes + ----- + The generated channel consists of Kraus operators that correspond to + actions of the Pauli group on the quantum state. The number of Kraus + operators is determined by the specified rank. + """ rng = ensure_rng(rng) if not isinstance(dim, int): @@ -190,32 +275,39 @@ def rand_pauli_channel_kraus(dim: int, rng: Generator | None = None, rank: int | def _first_rotation(circuit: Circuit, nqubits: int, rng: Generator) -> None: - """Apply an initial random :math:`R_X` rotation to each qubit. + """ + Apply an initial random :math:`R_X` rotation to each qubit. Parameters ---------- circuit : Circuit - Circuit to modify in place. + The circuit to modify in place. nqubits : int - Number of qubits. + The number of qubits. rng : numpy.random.Generator - Random number generator used to sample rotation angles. + The random number generator used to sample rotation angles. """ for qubit in range(nqubits): circuit.rx(qubit, rng.random()) def _mid_rotation(circuit: Circuit, nqubits: int, rng: Generator) -> None: - """Apply a random :math:`R_X` then :math:`R_Z` rotation to each qubit. + """ + Apply a random :math:`R_X` followed by a random :math:`R_Z` rotation to each qubit. Parameters ---------- circuit : Circuit - Circuit to modify in place. + The circuit to modify in place. nqubits : int - Number of qubits. + The number of qubits in the circuit. rng : numpy.random.Generator - Random number generator used to sample rotation angles. + A random number generator used to sample rotation angles. + + Returns + ------- + None + This function modifies the circuit in place and does not return any value. """ for qubit in range(nqubits): circuit.rx(qubit, rng.random()) @@ -223,37 +315,45 @@ def _mid_rotation(circuit: Circuit, nqubits: int, rng: Generator) -> None: def _last_rotation(circuit: Circuit, nqubits: int, rng: Generator) -> None: - """Apply a final random :math:`R_Z` rotation to each qubit. + """ + Apply a final random :math:`R_Z` rotation to each qubit. Parameters ---------- circuit : Circuit - Circuit to modify in place. + The circuit to modify in place. nqubits : int - Number of qubits. + The number of qubits in the circuit. rng : numpy.random.Generator - Random number generator used to sample rotation angles. + The random number generator used to sample rotation angles. + + Returns + ------- + None + This function modifies the circuit in place and does not return a value. """ for qubit in range(nqubits): circuit.rz(qubit, rng.random()) def _entangler(circuit: Circuit, pairs: Iterable[tuple[int, int]]) -> None: - """Apply CNOT gates between qubit pairs. + """ + Apply CNOT gates between specified qubit pairs. Parameters ---------- circuit : Circuit - Circuit to modify in place. + The circuit to modify in place. pairs : Iterable[tuple[int, int]] - Pairs of control and target qubits for CNOT operations. + An iterable of pairs where each pair consists of control and target qubit indices for the CNOT operations. """ for a, b in pairs: circuit.cnot(a, b) def _entangler_rzz(circuit: Circuit, pairs: Iterable[tuple[int, int]], rng: Generator) -> None: - """Apply random :math:`R_{ZZ}` gates between qubit pairs. + """ + Apply random :math:`R_{ZZ}` gates between qubit pairs. Parameters ---------- @@ -276,7 +376,8 @@ def rand_gate( *, use_rzz: bool = False, ) -> Circuit: - """Return a random gate composed of single-qubit rotations and entangling operations. + """ + Return a random gate composed of single-qubit rotations and entangling operations. Parameters ---------- @@ -286,11 +387,11 @@ def rand_gate( Depth of alternating rotation and entangling layers. pairs : Iterable[tuple[int, int]] Pairs of qubits used for entangling operations. - rng : numpy.random.Generator, optional - Random number generator used to sample rotation angles. If ``None``, a + rng : Generator, optional + Random number generator used to sample rotation angles. If `None`, a default generator is created. use_rzz : bool, optional - If ``True`` use :math:`R_{ZZ}` gates as entanglers instead of CNOT. + If `True`, use :math:`R_{ZZ}` gates as entanglers instead of CNOT. Returns ------- @@ -312,7 +413,8 @@ def rand_gate( def _genpair(n_qubits: int, count: int, rng: Generator) -> Iterator[tuple[int, int]]: - """Yield random pairs of qubit indices. + """ + Yield random pairs of qubit indices. Parameters ---------- @@ -336,7 +438,8 @@ def _genpair(n_qubits: int, count: int, rng: Generator) -> Iterator[tuple[int, i def _gentriplet(n_qubits: int, count: int, rng: Generator) -> Iterator[tuple[int, int, int]]: - """Yield random triplets of qubit indices. + """ + Yield random triplets of qubit indices. Parameters ---------- @@ -368,7 +471,8 @@ def rand_circuit( use_ccx: bool = False, parameters: Iterable[Parameter] | None = None, ) -> Circuit: - """Return a random parameterized circuit used for testing or benchmarking. + """ + Return a random parameterized circuit used for testing or benchmarking. Parameters ---------- @@ -376,13 +480,13 @@ def rand_circuit( Number of qubits in the circuit. depth : int Number of alternating entangling and single-qubit layers. - rng : numpy.random.Generator, optional - Random number generator. A default generator is created if ``None``. + rng : Generator | None, optional + Random number generator. A default generator is created if `None`. use_rzz : bool, optional - If ``True`` add :math:`R_{ZZ}` gates in each layer. + If `True`, add :math:`R_{ZZ}` gates in each layer. use_ccx : bool, optional - If ``True`` add CCX gates in each layer. - parameters : Iterable[Parameter], optional + If `True`, add CCX gates in each layer. + parameters : Iterable[Parameter] | None, optional Parameters used for randomly chosen rotation gates. Returns diff --git a/graphix/repr_mixins.py b/graphix/repr_mixins.py index 70907e904..7e9c940ad 100644 --- a/graphix/repr_mixins.py +++ b/graphix/repr_mixins.py @@ -1,4 +1,10 @@ -"""Mixins for eval-friendly `repr` for dataclasses and Enum members.""" +""" +Mixins for evaluation-friendly `repr` for dataclasses and Enum members. + +This module provides mixin classes that enhance the representation of +dataclass instances and Enum members, making their string representation +more suitable for evaluation and debugging purposes. +""" from __future__ import annotations @@ -16,16 +22,26 @@ class DataclassReprMixin: """ Mixin for a concise, eval-friendly `repr` of dataclasses. - Compared to the default dataclass `repr`: - - Class variables are omitted (dataclasses.fields only returns actual fields). - - Fields whose values equal their defaults are omitted. - - Field names are only shown when preceding fields have been omitted, ensuring positional listings when possible. + Compared to the default dataclass `repr`, this mixin: + - Omits class variables (as `dataclasses.fields` only returns actual fields). + - Omits fields whose values equal their defaults. + - Displays field names only when preceding fields have been omitted, ensuring positional listings when possible. - Use with `@dataclass(repr=False)` on the target class. + To use this mixin, apply `@dataclass(repr=False)` on the target class. """ def __repr__(self: DataclassInstance) -> str: - """Return a representation string for a dataclass.""" + """ + Return a string representation of the dataclass instance. + + The representation string includes the name of the dataclass and its fields, + allowing for easy identification and debugging of the instance. + + Returns + ------- + str + A string representation of the dataclass instance. + """ cls_name = type(self).__name__ arguments = [] saw_omitted = False @@ -50,19 +66,21 @@ class EnumReprMixin: """ Mixin to provide a concise, eval-friendly repr for Enum members. - Compared to the default ``, this mixin's `__repr__` - returns `ClassName.MEMBER_NAME`, which can be evaluated in Python (assuming the - enum class is in scope) to retrieve the same member. + Compared to the default representation ``, this mixin + alters the `__repr__` method to return `ClassName.MEMBER_NAME`. This representation can + be evaluated in Python (assuming the enum class is in scope) to retrieve the same Enum + member. """ def __repr__(self) -> str: """ - Return a representation string of an Enum member. + Return a string representation of an Enum member. Returns ------- str - A string in the form `ClassName.MEMBER_NAME`. + A string in the format `ClassName.MEMBER_NAME`, where `ClassName` is the name of the class + and `MEMBER_NAME` is the name of the Enum member. """ # Equivalently (as of Python 3.12), `str(value)` also produces # "ClassName.MEMBER_NAME", but we build it explicitly here for diff --git a/graphix/rng.py b/graphix/rng.py index ed0c04588..f71a25ee2 100644 --- a/graphix/rng.py +++ b/graphix/rng.py @@ -1,4 +1,19 @@ -"""Provide a default random-number generator if `None` is given.""" +""" +Provide a default random number generator if `None` is provided. + +This module offers functionality to generate random numbers using a default +random number generator. If no specific generator is supplied, the default +is utilized to ensure consistent random number generation. + +Parameters +---------- +None + +Returns +------- +RandomNumberGenerator + An instance of the default random number generator. +""" from __future__ import annotations @@ -14,7 +29,24 @@ def ensure_rng(rng: Generator | None = None) -> Generator: - """Return a default random-number generator if `None` is given.""" + """ + Ensure a random number generator is returned. + + This function returns a default random number generator if the + provided generator is None. If a generator is provided, it returns + that generator. + + Parameters + ---------- + rng : Generator | None, optional + A random number generator. If None, a default generator will + be created and returned. + + Returns + ------- + Generator + A random number generator instance. + """ if rng is not None: return rng stored: Generator | None = getattr(_rng_local, "rng", None) diff --git a/graphix/sim/__init__.py b/graphix/sim/__init__.py index e54f62cab..ecc162be7 100644 --- a/graphix/sim/__init__.py +++ b/graphix/sim/__init__.py @@ -1,4 +1,8 @@ -"""Simulation backends.""" +""" +Simulation backends for executing and managing simulations. + +This module provides various backends that facilitate the execution of simulations in different environments. Each backend may implement distinct strategies for managing resources, execution time, and communication with simulation components. +""" from __future__ import annotations diff --git a/graphix/sim/base_backend.py b/graphix/sim/base_backend.py index 9bb66f309..b85951ce0 100644 --- a/graphix/sim/base_backend.py +++ b/graphix/sim/base_backend.py @@ -1,4 +1,10 @@ -"""Abstract base class for simulation backends.""" +""" +Abstract base class for simulation backends. + +This class serves as the foundation for all simulation backend implementations. +Derived classes should provide concrete implementations of the required methods +to facilitate specific simulation behavior. +""" from __future__ import annotations @@ -46,16 +52,17 @@ def tensordot(op: Matrix, psi: Matrix, axes: tuple[int | Sequence[int], int | Sequence[int]]) -> Matrix: - """Tensor dot product that preserves the type of `psi`. + """ + Tensor dot product that preserves the type of `psi`. This wrapper around `np.tensordot` ensures static type checking for both numeric (`complex128`) and symbolic (`object`) arrays. Even though the runtime behavior is the same, NumPy's static types don't support `Matrix` directly. - If `psi` and `op` are numeric, the result is numeric. - If `psi` or `op` are symbolic, the other is converted to symbolic if needed and - the result is symbolic. + If `psi` and `op` are numeric, the result is numeric. If either `psi` + or `op` is symbolic, the other is converted to symbolic if needed, + and the result is also symbolic. Parameters ---------- @@ -81,26 +88,27 @@ def tensordot(op: Matrix, psi: Matrix, axes: tuple[int | Sequence[int], int | Se def kron(a: Matrix, b: Matrix) -> Matrix: - """Kronecker product with type-safe handling of symbolic and numeric matrices. + """ + Compute the Kronecker product of two matrices with type-safe handling of symbolic and numeric matrices. - The two matrices should have the same type. + The two matrices must have the same type (both symbolic or both numeric). Parameters ---------- a : Matrix - Left operand (symbolic or numeric). + The left operand (symbolic or numeric). b : Matrix - Right operand (symbolic or numeric). + The right operand (symbolic or numeric). Returns ------- Matrix - Kronecker product of `a` and `b`. + The Kronecker product of `a` and `b`. Raises ------ TypeError - If `a` and `b` don't have the same type. + If `a` and `b` do not have the same type. """ if a.dtype == np.complex128 and b.dtype == np.complex128: a_c = a.astype(np.complex128, copy=False) @@ -116,26 +124,28 @@ def kron(a: Matrix, b: Matrix) -> Matrix: def outer(a: Matrix, b: Matrix) -> Matrix: - """Outer product with type-safe handling of symbolic and numeric vectors. + """ + Outer product with type-safe handling of symbolic and numeric vectors. - The two matrices should have the same type. + Computes the outer product of two matrices, ensuring that both matrices + are of the same type (either symbolic or numeric). Parameters ---------- a : Matrix - Left operand (symbolic or numeric). + The left operand (symbolic or numeric) for the outer product. b : Matrix - Right operand (symbolic or numeric). + The right operand (symbolic or numeric) for the outer product. Returns ------- Matrix - Outer product of `a` and `b`. + The outer product of `a` and `b`. Raises ------ TypeError - If `a` and `b` don't have the same type. + If `a` and `b` do not have the same type. """ if a.dtype == np.complex128 and b.dtype == np.complex128: a_c = a.astype(np.complex128, copy=False) @@ -151,26 +161,27 @@ def outer(a: Matrix, b: Matrix) -> Matrix: def vdot(a: Matrix, b: Matrix) -> ExpressionOrComplex: - """Conjugate dot product ⟨a|b⟩ with type-safe handling of symbolic and numeric vectors. + """ + Conjugate dot product ⟨a|b⟩ with type-safe handling of symbolic and numeric vectors. - The two matrices should have the same type. + The two matrices must have the same type to compute the dot product. Parameters ---------- a : Matrix - Left operand (symbolic or numeric). + The left operand (either symbolic or numeric). b : Matrix - Right operand (symbolic or numeric). + The right operand (either symbolic or numeric). Returns ------- - ExpressionOrFloat - Dot product. + ExpressionOrComplex + The conjugate dot product of the two matrices. Raises ------ TypeError - If `a` and `b` don't have the same type. + If `a` and `b` are not of the same type. """ if a.dtype == np.complex128 and b.dtype == np.complex128: a_c = a.astype(np.complex128, copy=False) @@ -186,26 +197,27 @@ def vdot(a: Matrix, b: Matrix) -> ExpressionOrComplex: def matmul(a: Matrix, b: Matrix) -> Matrix: - """Matrix product a @ b with type-safe handling of symbolic and numeric vectors. + """ + Compute the matrix product of two matrices `a` and `b` with type-safe handling of symbolic and numeric vectors. - The two matrices should have the same type. + The two matrices must have the same type for the operation to proceed. Parameters ---------- a : Matrix - Left operand (symbolic or numeric). + The left operand, which can be either symbolic or numeric. b : Matrix - Right operand (symbolic or numeric). + The right operand, which can also be either symbolic or numeric. Returns ------- Matrix - Matrix product. + The resulting matrix product of `a` and `b`. Raises ------ TypeError - If `a` and `b` don't have the same type. + If `a` and `b` are not of the same type. """ if a.dtype == np.complex128 and b.dtype == np.complex128: a_c = a.astype(np.complex128, copy=False) @@ -221,27 +233,37 @@ def matmul(a: Matrix, b: Matrix) -> Matrix: class NodeIndex: - """A class for managing the mapping between node numbers and qubit indices in the internal state of the backend. + """ + A class for managing the mapping between node numbers and qubit indices in the internal state of the backend. This allows for efficient access and manipulation of qubit orderings throughout the execution of a pattern. Attributes ---------- - __list (list): A private list of the current active node (labelled with integers). - __dict (dict): A private dictionary mapping current node labels (integers) to their corresponding qubit indices - in the backend's internal quantum state. + __list : list + A private list of the current active nodes (labeled with integers). + __dict : dict + A private dictionary mapping current node labels (integers) to their corresponding qubit indices + in the backend's internal quantum state. """ __dict: dict[int, int] __list: list[int] def __init__(self) -> None: - """Initialize an empty mapping between nodes and qubit indices.""" + """ + Initialize an empty mapping between nodes and qubit indices. + + This constructor sets up a new instance of the NodeIndex class, initializing + an empty data structure to store the mapping of nodes to their corresponding + qubit indices. + """ self.__dict = {} self.__list = [] def __getitem__(self, index: int) -> int: - """Return the qubit node associated with the specified index. + """ + Return the qubit node associated with the specified index. Parameters ---------- @@ -251,12 +273,13 @@ def __getitem__(self, index: int) -> int: Returns ------- int - Node label corresponding to ``index``. + Node label corresponding to the specified index. """ return self.__list[index] def index(self, node: int) -> int: - """Return the qubit index associated with the specified node label. + """ + Return the qubit index associated with the specified node label. Parameters ---------- @@ -266,25 +289,44 @@ def index(self, node: int) -> int: Returns ------- int - Position of ``node`` in the internal ordering. + Position of the specified ``node`` in the internal ordering. """ return self.__dict[node] def __iter__(self) -> Iterator[int]: - """Return an iterator over node labels in their current order.""" + """ + Return an iterator over the node labels in their current order. + + Yields + ------ + int + The node labels in their current order. + """ return iter(self.__list) def __len__(self) -> int: - """Return the number of currently active nodes.""" + """ + Return the number of currently active nodes. + + Returns + ------- + int + The count of active nodes in the index. + """ return len(self.__list) def extend(self, nodes: Iterable[int]) -> None: - """Extend the mapping with additional nodes. + """ + Extend the mapping with additional nodes. Parameters ---------- nodes : Iterable[int] Node labels to append. + + Returns + ------- + None """ base = len(self) self.__list.extend(nodes) @@ -295,12 +337,13 @@ def extend(self, nodes: Iterable[int]) -> None: self.__dict[node] = base + index def remove(self, node: int) -> None: - """Remove a node and reassign indices of the remaining nodes. + """ + Remove a node and reassign indices of the remaining nodes. Parameters ---------- node : int - Node label to remove. + The label of the node to remove. """ index = self.__dict[node] del self.__list[index] @@ -309,12 +352,19 @@ def remove(self, node: int) -> None: self.__dict[u] = new_index def swap(self, i: int, j: int) -> None: - """Swap two nodes given their indices. + """ + Swap two nodes given their indices. Parameters ---------- - i, j : int - Indices of the nodes in the current ordering. + i : int + Index of the first node in the current ordering. + j : int + Index of the second node in the current ordering. + + Returns + ------- + None """ node_i = self.__list[i] node_j = self.__list[j] @@ -325,10 +375,33 @@ def swap(self, i: int, j: int) -> None: class NoiseNotSupportedError(Exception): - """Exception raised when `apply_channel` is called on a backend that does not support noise.""" + """ + Exception raised when `apply_channel` is called on a backend that does not support noise. + + Attributes + ---------- + message : str + Explanation of the error. + + Parameters + ---------- + message : str, optional + Custom error message (default is "Noise not supported by this backend.") + + Examples + -------- + >>> raise NoiseNotSupportedError("This backend cannot apply noise.") + """ def __str__(self) -> str: - """Return the error message.""" + """ + Return a string representation of the error message. + + Returns + ------- + str + The error message indicating that noise is not supported. + """ return "This backend does not support noise." @@ -348,9 +421,9 @@ class BackendState(ABC): This class is abstract and cannot be instantiated directly. Examples of concrete subclasses include: - - :class:`Statevec` (for pure states represented as state vectors) - - :class:`DensityMatrix` (for mixed states represented as density matrices) - - :class:`MBQCTensorNet` (for compressed representations using tensor networks) + - :class:`Statevec`: for pure states represented as state vectors. + - :class:`DensityMatrix`: for mixed states represented as density matrices. + - :class:`MBQCTensorNet`: for compressed representations using tensor networks. See Also -------- @@ -359,7 +432,14 @@ class BackendState(ABC): @abstractmethod def flatten(self) -> Matrix: - """Return flattened state.""" + """ + Return the flattened representation of the state. + + Returns + ------- + Matrix + A matrix representing the flattened state. + """ class DenseState(BackendState): @@ -393,7 +473,14 @@ class DenseState(BackendState): @property @abstractmethod def nqubit(self) -> int: - """Return the number of qubits.""" + """ + Get the number of qubits in the quantum state. + + Returns + ------- + int + The number of qubits. + """ @abstractmethod def add_nodes(self, nqubit: int, data: Data) -> None: @@ -405,92 +492,129 @@ def add_nodes(self, nqubit: int, data: Data) -> None: nqubit : int The number of qubits to add to the state. - data : Data, optional + data : Data The state in which to initialize the newly added nodes. The supported forms of state specification depend on the backend implementation. - See :meth:`Backend.add_nodes` for further details. + See Also + -------- + Backend.add_nodes : For further details on state initialization and supported formats. """ @abstractmethod def entangle(self, edge: tuple[int, int]) -> None: - """Connect graph nodes. + """ + Entangle graph nodes. Parameters ---------- edge : tuple of int - (control, target) qubit indices + A tuple containing the indices of the control and target qubits, respectively. """ @abstractmethod def evolve(self, op: Matrix, qargs: Sequence[int]) -> None: - """Apply a multi-qubit operation. + """ + Apply a multi-qubit operation. Parameters ---------- op : numpy.ndarray - 2^n*2^n matrix - qargs : list of int - target qubits' indices + A square matrix of shape (2^n, 2^n) representing the operation to be applied. + qargs : Sequence[int] + A sequence of integers representing the indices of the target qubits. """ @abstractmethod def evolve_single(self, op: Matrix, i: int) -> None: - """Apply a single-qubit operation. + """ + Apply a single-qubit operation. Parameters ---------- op : numpy.ndarray - 2*2 matrix + A 2x2 matrix representing the single-qubit operation. i : int - qubit index + The index of the qubit on which the operation is to be applied. """ @abstractmethod def expectation_single(self, op: Matrix, loc: int) -> complex: - """Return the expectation value of single-qubit operator. + """ + Return the expectation value of a single-qubit operator. Parameters ---------- op : numpy.ndarray - 2*2 operator + A 2x2 operator representing the single-qubit operation. loc : int - target qubit index + The index of the target qubit. Returns ------- - complex : expectation value. + complex + The expectation value of the operator for the specified qubit. """ @abstractmethod def remove_qubit(self, qarg: int) -> None: - """Remove a separable qubit from the system.""" + """ + Remove a separable qubit from the quantum state. + + Parameters + ---------- + qarg : int + The index of the qubit to be removed from the system. The index should + correspond to a qubit in the current state representation. + + Raises + ------ + IndexError + If the provided index is out of bounds or does not correspond to an + existing qubit in the system. + + Notes + ----- + This method modifies the current state of the system by removing the + specified qubit, resulting in a new quantum state that is one qubit + smaller. + """ @abstractmethod def swap(self, qubits: tuple[int, int]) -> None: - """Swap qubits. + """ + Swap qubits. Parameters ---------- qubits : tuple of int - (control, target) qubit indices + A tuple containing the indices of the qubits to be swapped. The first element + is the index of the control qubit and the second element is the index of the + target qubit. """ def apply_noise(self, qubits: Sequence[int], noise: Noise) -> None: # noqa: ARG002,PLR6301 - """Apply noise. + """ + Apply noise to the specified qubits. - The default implementation of this method raises - `NoiseNotSupportedError`, indicating that the backend does not - support noise. Backends that support noise (e.g., - `DensityMatrixBackend`) override this method to implement - the effect of noise. + This method applies a noise operation to the given qubits. + The default implementation raises a `NoiseNotSupportedError`, + indicating that the backend does not support noise. Backends + that support noise (e.g., `DensityMatrixBackend`) should + override this method to implement the desired noise effects. Parameters ---------- - qubits : list of ints. - Target qubits + qubits : Sequence[int] + List of target qubit indices to which the noise will be applied. noise : Noise - Noise to apply + The noise model that defines the type and characteristics of noise to apply. + + Notes + ----- + Ensure that the backend and noise model are compatible. Proper + error handling or checks should be implemented in derived classes + that implement noise functionalities. """ raise NoiseNotSupportedError @@ -498,20 +622,21 @@ def apply_noise(self, qubits: Sequence[int], noise: Noise) -> None: # noqa: ARG def _op_mat_from_result( vec: tuple[ExpressionOrFloat, ExpressionOrFloat, ExpressionOrFloat], result: Outcome, symbolic: bool = False ) -> Matrix: - r"""Return the operator :math:`\tfrac{1}{2}(I + (-1)^r \vec{v}\cdot\vec{\sigma})`. + """ + Return the operator :math:`\tfrac{1}{2}(I + (-1)^r \vec{v}\cdot\vec{\sigma})`. Parameters ---------- - vec : tuple[float, float, float] + vec : tuple[ExpressionOrFloat, ExpressionOrFloat, ExpressionOrFloat] Cartesian components of a unit vector. - result : bool + result : Outcome Measurement result ``r``. symbolic : bool, optional - If ``True`` return an array of ``object`` dtype. + If ``True``, return an array of ``object`` dtype. Default is ``False``. Returns ------- - numpy.ndarray + Matrix 2x2 operator acting on the measured qubit. """ sign = (-1) ** result @@ -544,7 +669,37 @@ def perform_measure( rng: Generator | None = None, symbolic: bool = False, ) -> Outcome: - """Perform measurement of a qubit.""" + """ + Perform measurement of a qubit. + + Parameters + ---------- + qubit_node : int + The index of the qubit node to be measured. + qubit_loc : int + The location of the qubit within the specified node. + plane : Plane + The plane in which the measurement is to be performed. + angle : ExpressionOrFloat + The angle at which the measurement is made, can be a symbolic expression or a float. + state : DenseState + The current state of the qubit to be measured. + branch_selector : BranchSelector + Selector for choosing branches in the measurement process. + rng : Generator, optional + Random number generator for stochastic processes (default is None). + symbolic : bool, optional + Flag to indicate if symbolic computation should be used (default is False). + + Returns + ------- + Outcome + The outcome of the measurement, which can include the result and any relevant metadata. + + Notes + ----- + This function performs the measurement of a qubit within the quantum system, taking into account various parameters and configurations as specified in the arguments. + """ vec = plane.polar(angle) # op_mat0 may contain the matrix operator associated with the outcome 0, # but the value is computed lazily, i.e., only if needed. @@ -584,20 +739,20 @@ class Backend(Generic[_StateT_co]): such as dense state vectors, density matrices, or tensor networks. Responsibilities of a backend typically include: - - Managing a dynamic set of qubits (nodes) and their state - - Applying quantum gates or operations - - Performing measurements and returning classical outcomes - - Tracking and exposing the underlying quantum state + - Managing a dynamic set of qubits (nodes) and their state. + - Applying quantum gates or operations. + - Performing measurements and returning classical outcomes. + - Tracking and exposing the underlying quantum state. Examples of concrete subclasses include: - - `StatevecBackend` (pure states via state vectors) - - `DensityMatrixBackend` (mixed states via density matrices) - - `TensorNetworkBackend` (compressed states via tensor networks) + - `StatevecBackend` (pure states via state vectors). + - `DensityMatrixBackend` (mixed states via density matrices). + - `TensorNetworkBackend` (compressed states via tensor networks). Parameters ---------- state : BackendState - internal state of the backend: instance of :class:`Statevec`, :class:`DensityMatrix`, or :class:`MBQCTensorNet`. + Internal state of the backend: instance of :class:`Statevec`, :class:`DensityMatrix`, or :class:`MBQCTensorNet`. Notes ----- @@ -609,7 +764,7 @@ class Backend(Generic[_StateT_co]): - `StatevecBackend` and `DensityMatrixBackend` are subclasses of `DenseStateBackend`, and `Statevec` and `DensityMatrix` are subclasses of `DenseState`. - The type variable `_StateT_co` specifies the type of the ``state`` field, so that subclasses + The type variable `_StateT_co` specifies the type of the `state` field, so that subclasses provide a precise type for this field: - `StatevecBackend` is a subtype of ``Backend[Statevec]``. - `DensityMatrixBackend` is a subtype of ``Backend[DensityMatrix]``. @@ -618,28 +773,23 @@ class Backend(Generic[_StateT_co]): The type variables `_StateT_co` and `_DenseStateT_co` are declared as covariant. That is, ``Backend[T1]`` is a subtype of ``Backend[T2]`` if ``T1`` is a subtype of ``T2``. This means that `StatevecBackend`, `DensityMatrixBackend`, and `TensorNetworkBackend` are - all subtypes of ``Backend[BackendState]``. - This covariance is sound because backends are frozen dataclasses; thus, the type of - ``state`` cannot be changed after instantiation. + all subtypes of ``Backend[BackendState]``. This covariance is sound because backends are frozen + dataclasses; thus, the type of `state` cannot be changed after instantiation. The interface expected from a backend includes the following methods: - - `add_nodes`: which executes `N` commands. - - `apply_channel`: used for noisy simulations. - The class `Backend` provides a default implementation that - raises `NoiseNotSupportedError`, indicating that the backend - does not support noise. Backends that support noise (e.g., - `DensityMatrixBackend`) override this method to implement the - effect of noise. + - `add_nodes`: executes `N` commands. + - `apply_channel`: used for noisy simulations. The class `Backend` provides a default implementation that + raises `NoiseNotSupportedError`, indicating that the backend does not support noise. Backends that support + noise (e.g., `DensityMatrixBackend`) override this method to implement the effect of noise. - `apply_clifford`: executes `C` commands. - `correct_byproduct`: executes `X` and `Z` commands. - `entangle_nodes`: executes `E` commands. - - `finalize`: called at the end of pattern simulation to convey - the order of output nodes. + - `finalize`: called at the end of pattern simulation to convey the order of output nodes. - `measure`: executes `M` commands. See Also -------- - :class:`BackendState`, :`class:`DenseStateBackend`, :class:`StatevecBackend`, :class:`DensityMatrixBackend`, :class:`TensorNetworkBackend` + :class:`BackendState`, :class:`DenseStateBackend`, :class:`StatevecBackend`, :class:`DensityMatrixBackend`, :class:`TensorNetworkBackend` """ # `init=False` is required because `state` cannot appear in a contravariant position @@ -687,57 +837,123 @@ def add_nodes(self, nodes: Sequence[int], data: Data = BasicStates.PLUS) -> None """ def apply_noise(self, nodes: Sequence[int], noise: Noise) -> None: # noqa: ARG002,PLR6301 - """Apply noise. + """ + Apply noise to the specified nodes. - The default implementation of this method raises - `NoiseNotSupportedError`, indicating that the backend does not - support noise. Backends that support noise (e.g., - `DensityMatrixBackend`) override this method to implement - the effect of noise. + This method is intended to be overridden by backends that support noise. The default + implementation raises a `NoiseNotSupportedError`, indicating that the backend does not + support noise. Backends such as `DensityMatrixBackend` should implement the effect of + noise in this method. Parameters ---------- - nodes : sequence of ints. - Target qubits + nodes : Sequence[int] + Target qubits to which the noise is to be applied. noise : Noise - Noise to apply + The noise model to apply to the specified nodes. + + Raises + ------ + NoiseNotSupportedError + If the backend does not support noise. """ raise NoiseNotSupportedError @abstractmethod def apply_clifford(self, node: int, clifford: Clifford) -> None: - """Apply single-qubit Clifford gate, specified by vop index specified in graphix.clifford.CLIFFORD.""" + """ + Apply a single-qubit Clifford gate to a specified node. + + This method applies a Clifford gate, specified by its corresponding + index in `graphix.clifford.CLIFFORD`, to the given node in the quantum circuit. + + Parameters + ---------- + node : int + The index of the node (qubit) to which the Clifford gate will be applied. + + clifford : Clifford + The Clifford gate to be applied, represented as an instance of the `Clifford` class. + + Raises + ------ + NotImplementedError + If the method is not implemented by a derived class. + """ @abstractmethod def correct_byproduct(self, cmd: command.X | command.Z, measure_method: MeasureMethod) -> None: - """Byproduct correction correct for the X or Z byproduct operators, by applying the X or Z gate.""" + """ + Corrects byproduct errors for the specified X or Z byproduct operators. + + Parameters + ---------- + cmd : command.X | command.Z + The byproduct operator to correct, which can be either + an X or Z gate. + measure_method : MeasureMethod + The measurement method used for the correction process. + This determines how the correction is applied based on + the measurement results. + + Returns + ------- + None + This method modifies the state of the system in-place + and does not return a value. + """ @abstractmethod def entangle_nodes(self, edge: tuple[int, int]) -> None: - """Apply CZ gate to two connected nodes. + """ + Apply the CZ gate to two connected nodes. Parameters ---------- - edge : tuple (i, j) - a pair of node indices + edge : tuple of int + A pair of node indices (i, j) that represent the connected nodes to be entangled. """ @abstractmethod def finalize(self, output_nodes: Iterable[int]) -> None: - """To be run at the end of pattern simulation to convey the order of output nodes.""" + """ + Finalize the processing of the output nodes after pattern simulation. + + Parameters + ---------- + output_nodes : Iterable[int] + A collection of integers representing the indices of the output nodes + to be processed at the end of the simulation. + + Notes + ----- + This method is intended to be called after the completion of the pattern + simulation to ensure that the specified output nodes are processed in the + correct order. + """ @abstractmethod def measure(self, node: int, measurement: Measurement, rng: Generator | None = None) -> Outcome: - """Perform measurement of a node and trace out the qubit. + """ + Perform measurement of a node and trace out the qubit. Parameters ---------- - node: int - measurement: Measurement - rng: Generator, optional + node : int + The index of the node to measure. + + measurement : Measurement + The measurement to be applied to the specified node. + + rng : Generator, optional Random-number generator for measurements. - This generator is used only in case of random branch selection + This generator is used only in cases of random branch selection (see :class:`RandomBranchSelector`). + + Returns + ------- + Outcome + The outcome of the measurement. """ @@ -766,8 +982,8 @@ class DenseStateBackend(Backend[_DenseStateT_co], Generic[_DenseStateT_co]): ---------- node_index : NodeIndex, optional Mapping between node numbers and qubit indices in the internal state of the backend. - branch_selector: :class:`graphix.branch_selector.BranchSelector`, optional - Branch selector used for measurements. Default is :class:`RandomBranchSelector`. + branch_selector : :class:`graphix.branch_selector.BranchSelector`, optional + Branch selector used for measurements. Default is :class:`RandomBranchSelector`. symbolic : bool, optional If True, support arbitrary objects (typically, symbolic expressions) in matrices. @@ -789,13 +1005,16 @@ def add_nodes(self, nodes: Sequence[int], data: Data = BasicStates.PLUS) -> None ---------- nodes : Sequence[int] A list of node indices to add to the backend. These indices can be any - integer values but must be fresh: each index must be distinct from all + integer values but must be unique: each index must be distinct from all previously added nodes. data : Data, optional The state in which to initialize the newly added nodes. The supported forms - of state specification depend on the backend implementation. + of state specification depend on the backend implementation. The default + is ``BasicStates.PLUS``. + Notes + ----- See :meth:`Backend.add_nodes` for further details. """ self.state.add_nodes(nqubit=len(nodes), data=data) @@ -803,12 +1022,13 @@ def add_nodes(self, nodes: Sequence[int], data: Data = BasicStates.PLUS) -> None @override def entangle_nodes(self, edge: tuple[int, int]) -> None: - """Apply CZ gate to two connected nodes. + """ + Apply a controlled-Z (CZ) gate to two connected nodes. Parameters ---------- - edge : tuple (i, j) - a pair of node indices + edge : tuple of int + A pair of node indices (i, j) representing the connected nodes to be entangled. """ target = self.node_index.index(edge[0]) control = self.node_index.index(edge[1]) @@ -816,13 +1036,22 @@ def entangle_nodes(self, edge: tuple[int, int]) -> None: @override def measure(self, node: int, measurement: Measurement, rng: Generator | None = None) -> Outcome: - """Perform measurement of a node and trace out the qubit. + """ + Perform measurement of a node and trace out the corresponding qubit. Parameters ---------- - node: int - measurement: Measurement - rng: Generator, optional + node : int + The index of the node to be measured. + measurement : Measurement + The measurement operator to apply to the node. + rng : Generator, optional + An optional random number generator for stochastic measurements. + + Returns + ------- + Outcome + The result of the measurement on the specified node. """ loc = self.node_index.index(node) result = perform_measure( @@ -841,38 +1070,99 @@ def measure(self, node: int, measurement: Measurement, rng: Generator | None = N @override def correct_byproduct(self, cmd: command.X | command.Z, measure_method: MeasureMethod) -> None: - """Byproduct correction correct for the X or Z byproduct operators, by applying the X or Z gate.""" + """ + Corrects for the X or Z byproduct operators by applying the corresponding X or Z gate. + + Parameters + ---------- + cmd : command.X | command.Z + The command representing the byproduct operator to be corrected. + measure_method : MeasureMethod + The measurement method used during the correction process. + + Returns + ------- + None + This method modifies the state of the backend in place and does not return a value. + """ if np.mod(sum(measure_method.get_measure_result(j) for j in cmd.domain), 2) == 1: op = Ops.X if cmd.kind == CommandKind.X else Ops.Z self.apply_single(node=cmd.node, op=op) @override def apply_noise(self, nodes: Sequence[int], noise: Noise) -> None: - """Apply noise. + """ + Apply noise to the specified nodes. Parameters ---------- - nodes : sequence of ints. - Target qubits + nodes : sequence of int + The target qubits to which noise will be applied. noise : Noise - Noise to apply + The noise object that defines the noise to be applied. """ indices = [self.node_index.index(i) for i in nodes] self.state.apply_noise(indices, noise) def apply_single(self, node: int, op: Matrix) -> None: - """Apply a single gate to the state.""" + """ + Apply a single gate operation to the state. + + Parameters + ---------- + node : int + The index of the node to which the operation will be applied. + op : Matrix + The matrix representing the gate operation to be applied. + + Returns + ------- + None + This method modifies the state in place and does not return a value. + """ index = self.node_index.index(node) self.state.evolve_single(op=op, i=index) @override def apply_clifford(self, node: int, clifford: Clifford) -> None: - """Apply single-qubit Clifford gate, specified by vop index specified in graphix.clifford.CLIFFORD.""" + """ + Apply a single-qubit Clifford gate to the specified qubit. + + Parameters + ---------- + node : int + The index of the qubit to which the Clifford gate will be applied. + clifford : Clifford + The Clifford gate to be applied, as specified by the @vop index in + graphix.clifford.CLIFFORD. + + Returns + ------- + None + This method modifies the state of the qubit in place and does not return a value. + + Notes + ----- + This method is intended to be used within the context of a quantum state backend that + supports the application of single-qubit Clifford gates. + """ loc = self.node_index.index(node) self.state.evolve_single(clifford.matrix, loc) def sort_qubits(self, output_nodes: Iterable[int]) -> None: - """Sort the qubit order in internal statevector.""" + """ + Sort the qubit order in the internal statevector. + + Parameters + ---------- + output_nodes : Iterable[int] + The order of qubits to sort the internal statevector. + + Returns + ------- + None + This method modifies the internal statevector in place and does not return a value. + """ for i, ind in enumerate(output_nodes): if self.node_index.index(ind) != i: move_from = self.node_index.index(ind) @@ -881,10 +1171,32 @@ def sort_qubits(self, output_nodes: Iterable[int]) -> None: @override def finalize(self, output_nodes: Iterable[int]) -> None: - """To be run at the end of pattern simulation.""" + """ + Finalize the pattern simulation. + + This method is called at the end of the pattern simulation to perform any + necessary cleanup or final processing related to the output nodes. + + Parameters + ---------- + output_nodes : Iterable[int] + A collection of indices representing the output nodes that were + involved in the simulation. + + Returns + ------- + None + """ self.sort_qubits(output_nodes) @property def nqubit(self) -> int: - """Return the number of qubits of the current state.""" + """ + Returns the number of qubits in the current state. + + Returns + ------- + int + The number of qubits of the current state. + """ return self.state.nqubit diff --git a/graphix/sim/data.py b/graphix/sim/data.py index 4e255591f..186ffb0d6 100644 --- a/graphix/sim/data.py +++ b/graphix/sim/data.py @@ -1,10 +1,11 @@ -"""Type `Data` for initializing nodes in backends. +""" +Type `Data` for initializing nodes in backends. -The type `Data` is declared here to support type-checking -`base_backend`, but its definition requires importing the `statevec` -and `density_matrix` modules, both of which import `base_backend`. To -break this import cycle, `data` is only imported within the -type-checking block of `base_backend`. +This module declares the type `Data` to support type-checking in the +`base_backend`. The definition of `Data` necessitates importing the +`statevec` and `density_matrix` modules, both of which have dependencies +on `base_backend`. To avoid circular imports, the `data` module is +imported solely within the type-checking block of `base_backend`. """ from __future__ import annotations diff --git a/graphix/sim/density_matrix.py b/graphix/sim/density_matrix.py index 2c7882280..cf3a41c07 100644 --- a/graphix/sim/density_matrix.py +++ b/graphix/sim/density_matrix.py @@ -1,6 +1,32 @@ -"""Density matrix simulator. +""" +Density matrix simulator. + +This module simulates measurement-based quantum computation (MBQC) using a density matrix representation. It provides functionalities for creating, manipulating, and simulating density matrices in the context of quantum computation. + +Key Features +------------ +- Simulation of density matrices for quantum states. +- Support for various quantum operations and measurements. +- Tools for the analysis of measurement-based quantum computation. + +Usage +----- +To use this module, import it and create a density matrix for your quantum state. Then, apply quantum gates and perform measurements as needed. + +Example +------- +```python +import density_matrix_simulator as dms + +# Create a density matrix +dm = dms.create_density_matrix(state_vector) + +# Apply a quantum operation +dm = dms.apply_gate(dm, gate) -Simulate MBQC with density matrix representation. +# Perform a measurement +result = dms.measure(dm) +``` """ from __future__ import annotations @@ -32,7 +58,28 @@ class DensityMatrix(DenseState): - """DensityMatrix object.""" + """ + A class to represent a Density Matrix. + + Attributes + ---------- + matrix : ndarray + The density matrix represented as a 2D numpy array. + + Methods + ------- + __init__(matrix: np.ndarray) -> None + Initializes the DensityMatrix with a given matrix. + + to_density_operator() -> DensityOperator + Converts the density matrix to a density operator. + + trace() -> float + Computes and returns the trace of the density matrix. + + is_valid() -> bool + Checks if the density matrix is valid (i.e., Hermitian and positive semi-definite). + """ rho: Matrix @@ -41,27 +88,31 @@ def __init__( data: Data = BasicStates.PLUS, nqubit: int | None = None, ) -> None: - """Initialize density matrix objects. - - The behaviour builds on the one of *graphix.statevec.Statevec*. - `data` can be: - - a single :class:`graphix.states.State` (classical description of a quantum state) - - an iterable of :class:`graphix.states.State` objects - - an iterable of iterable of scalars (A *2**n x 2**n* numerical density matrix) - - a *graphix.statevec.DensityMatrix* object - - a *graphix.statevec.Statevector* object - - If `nqubit` is not provided, the number of qubit is inferred from `data` and checked for consistency. - If only one :class:`graphix.states.State` is provided and nqubit is a valid integer, initialize the statevector - in the tensor product state. + """ + Initialize density matrix objects. + + The behaviour builds on that of *graphix.statevec.Statevec*. The `data` parameter can be one of the following: + - A single :class:`graphix.states.State` (classical description of a quantum state) + - An iterable of :class:`graphix.states.State` objects + - An iterable of iterables of scalars (a *2**n x 2**n* numerical density matrix) + - A *graphix.statevec.DensityMatrix* object + - A *graphix.statevec.Statevector* object + + If `nqubit` is not provided, the number of qubits is inferred from `data` and checked for consistency. + If only one :class:`graphix.states.State` is provided and `nqubit` is a valid integer, the statevector is initialized in the tensor product state. If both `nqubit` and `data` are provided, consistency of the dimensions is checked. - If a *graphix.statevec.Statevec* or *graphix.statevec.DensityMatrix* is passed, returns a copy. + If a *graphix.statevec.Statevec* or *graphix.statevec.DensityMatrix* is passed, a copy is returned. + Parameters + ---------- + data : Data + Input data to prepare the state. Can be a classical description or a numerical input. Defaults to graphix.states.BasicStates.PLUS. + nqubit : int, optional + Number of qubits to prepare. Defaults to *None*. - :param data: input data to prepare the state. Can be a classical description or a numerical input, defaults to graphix.states.BasicStates.PLUS - :type data: Data - :param nqubit: number of qubits to prepare, defaults to *None* - :type nqubit: int, optional + Returns + ------- + None """ if nqubit is not None and nqubit < 0: raise ValueError("nqubit must be a non-negative integer.") @@ -105,7 +156,14 @@ def get_row( @property def nqubit(self) -> int: - """Return the number of qubits.""" + """ + Returns the number of qubits. + + Returns + ------- + int + The number of qubits represented by the density matrix. + """ # Circumvent typing bug with numpy>=2.3 # `shape` field is typed `tuple[Any, ...]` instead of `tuple[int, ...]` # See https://github.com/numpy/numpy/issues/29830 @@ -113,12 +171,20 @@ def nqubit(self) -> int: return nqubit def __str__(self) -> str: - """Return a string description.""" + """ + Return a string representation of the DensityMatrix. + + Returns + ------- + str + A string description of the DensityMatrix instance, providing + relevant information about its contents. + """ return f"DensityMatrix object, with density matrix {self.rho} and shape {self.dims()}." @override def add_nodes(self, nqubit: int, data: Data) -> None: - r""" + """ Add nodes (qubits) to the density matrix and initialize them in a specified state. Parameters @@ -126,16 +192,15 @@ def add_nodes(self, nqubit: int, data: Data) -> None: nqubit : int The number of qubits to add to the density matrix. - data : Data, optional - The state in which to initialize the newly added nodes. - + data : Data + The state in which to initialize the newly added nodes. This can take several forms: - If a single basic state is provided, all new nodes are initialized in that state. - - If a list of basic states is provided, it must match the length of ``nodes``, and - each node is initialized with its corresponding state. - - A single-qubit state vector will be broadcast to all nodes. + - If a list of basic states is provided, it must match the length of the existing nodes, + and each node is initialized with its corresponding state. + - A single-qubit state vector will be broadcast to all new nodes. - A multi-qubit state vector of dimension :math:`2^n` initializes the new nodes jointly. - - A density matrix must have shape :math:`2^n \times 2^n`, - and is used to jointly initialize the new nodes. + - A density matrix must have shape :math:`2^n \times 2^n`, and is used + to jointly initialize the new nodes. Notes ----- @@ -146,14 +211,22 @@ def add_nodes(self, nqubit: int, data: Data) -> None: @override def evolve_single(self, op: Matrix, i: int) -> None: - """Single-qubit operation. + """ + Evolves a single qubit by applying a specified operation. + + This method applies a given single-qubit operation to the qubit at the specified index + in the DensityMatrix. Parameters ---------- - op : np.ndarray - 2*2 matrix. - i : int - Index of qubit to apply operator. + op : np.ndarray + A 2x2 matrix representing the operation to apply. + i : int + The index of the qubit to which the operator is applied. + + Returns + ------- + None """ assert i >= 0 assert i < self.nqubit @@ -167,11 +240,20 @@ def evolve_single(self, op: Matrix, i: int) -> None: @override def evolve(self, op: Matrix, qargs: Sequence[int]) -> None: - """Multi-qubit operation. + """ + Evolve the density matrix with a multi-qubit operation. - Args: - op (np.array): 2^n*2^n matrix - qargs (list of ints): target qubits' indexes + Parameters + ---------- + op : np.ndarray + A 2^n x 2^n matrix representing the multi-qubit operation. + qargs : sequence of int + The indices of the target qubits to which the operation will be applied. + + Notes + ----- + This method updates the density matrix by applying the specified multi-qubit + operation to the qubits indicated by `qargs`. """ d = op.shape # check it is a matrix. @@ -215,15 +297,20 @@ def evolve(self, op: Matrix, qargs: Sequence[int]) -> None: @override def expectation_single(self, op: Matrix, loc: int) -> complex: - """Return the expectation value of single-qubit operator. + """ + Return the expectation value of a single-qubit operator. - Args: - op (np.array): 2*2 Hermite operator - loc (int): Index of qubit on which to apply operator. + Parameters + ---------- + op : np.ndarray + A 2x2 Hermitian operator. + loc : int + The index of the qubit on which to apply the operator. Returns ------- - complex: expectation value (real for hermitian ops!). + complex + The expectation value (real for Hermitian operators). """ if not (0 <= loc < self.nqubit): raise ValueError(f"Wrong target qubit {loc}. Must between 0 and {self.nqubit - 1}.") @@ -243,56 +330,90 @@ def expectation_single(self, op: Matrix, loc: int) -> complex: return complex(np.trace(rho_tensor.reshape((2**nqubit, 2**nqubit)))) def dims(self) -> tuple[int, ...]: - """Return the dimensions of the density matrix.""" + """ + Return the dimensions of the density matrix. + + Returns + ------- + tuple[int, ...] + A tuple representing the dimensions of the density matrix. + """ return self.rho.shape def tensor(self, other: DensityMatrix) -> None: - r"""Tensor product state with other density matrix. + """ + Tensor product with another density matrix. - Results in self :math:`\otimes` other. + Updates the current density matrix to be the tensor product + of itself and another density matrix. Parameters ---------- - other : :class: `DensityMatrix` object - DensityMatrix object to be tensored with self. + other : DensityMatrix + DensityMatrix object to be tensored with the current instance. + + Notes + ----- + This operation modifies the current density matrix in place. """ if not isinstance(other, DensityMatrix): other = DensityMatrix(other) self.rho = kron(self.rho, other.rho) def cnot(self, edge: tuple[int, int]) -> None: - """Apply CNOT gate to density matrix. + """ + Apply the CNOT gate to the density matrix. Parameters ---------- - edge : (int, int) or [int, int] - Edge to apply CNOT gate. + edge : tuple of int + A tuple representing the control and target qubits (e.g., (control, target)) on which to apply the CNOT gate. + + Notes + ----- + The control qubit flips the target qubit if and only if the control qubit is in the |1⟩ state. """ self.evolve(CNOT_TENSOR.reshape(4, 4), edge) @override def swap(self, qubits: tuple[int, int]) -> None: - """Swap qubits. + """ + Swap qubits. Parameters ---------- - qubits : (int, int) - (control, target) qubits indices. + qubits : tuple[int, int] + A tuple containing the indices of the control and target qubits to be swapped. """ self.evolve(SWAP_TENSOR.reshape(4, 4), qubits) def entangle(self, edge: tuple[int, int]) -> None: - """Connect graph nodes. + """ + Entangle qubits in the density matrix. Parameters ---------- - edge : (int, int) or [int, int] - (control, target) qubit indices. + edge : tuple[int, int] + A tuple representing the (control, target) qubit indices to be entangled. + + Notes + ----- + This method modifies the density matrix to create entanglement between the + specified qubit indices. """ self.evolve(CZ_TENSOR.reshape(4, 4), edge) def normalize(self) -> None: - """Normalize density matrix.""" + """ + Normalize the density matrix. + + This method rescales the density matrix such that its trace equals one. + It modifies the current instance of the DensityMatrix class in-place. + + Returns + ------- + None + """ # Note that the following calls to `astype` are guaranteed to # return the original NumPy array itself, since `copy=False` and # the `dtype` matches. This is important because the array is @@ -306,17 +427,36 @@ def normalize(self) -> None: @override def remove_qubit(self, qarg: int) -> None: - """Remove a qubit.""" + """ + Remove a qubit from the density matrix. + + Parameters + ---------- + qarg : int + The index of the qubit to be removed. + + Returns + ------- + None + + Notes + ----- + This method modifies the density matrix by removing the specified qubit, + which can affect the state representation. + """ self.ptrace(qarg) self.normalize() def ptrace(self, qargs: Collection[int] | int) -> None: - """Partial trace. + """ + Perform the partial trace over specified qubits. Parameters ---------- - qargs : list of ints or int - Indices of qubit to trace out. + qargs : int or collection of int + The indices of the qubits to be traced out. This can be a single integer + corresponding to the index of a qubit or a collection of integers representing + multiple qubits. """ n = int(np.log2(self.rho.shape[0])) if isinstance(qargs, int): @@ -336,12 +476,18 @@ def ptrace(self, qargs: Collection[int] | int) -> None: self.rho = rho_res.reshape((2**nqubit_after, 2**nqubit_after)) def fidelity(self, statevec: Statevec) -> ExpressionOrFloat: - """Calculate the fidelity against reference statevector. + """ + Calculate the fidelity against a reference state vector. Parameters ---------- - statevec : numpy array - statevector (flattened numpy array) to compare with + statevec : numpy.ndarray + The state vector (flattened numpy array) to compare with. + + Returns + ------- + ExpressionOrFloat + The calculated fidelity between the current density matrix and the provided state vector. """ result = vdot(statevec.psi, matmul(self.rho, statevec.psi)) if isinstance(result, Expression): @@ -350,29 +496,41 @@ def fidelity(self, statevec: Statevec) -> ExpressionOrFloat: return result.real def flatten(self) -> Matrix: - """Return flattened density matrix.""" + """ + Returns a flattened density matrix. + + This method takes the current density matrix and converts it into a one-dimensional array, + effectively flattening it. This can be useful for various applications in quantum mechanics + and other fields where a one-dimensional representation of the matrix is needed. + + Returns + ------- + Matrix + A one-dimensional representation of the density matrix. + """ return self.rho.flatten() def apply_channel(self, channel: KrausChannel, qargs: Sequence[int]) -> None: - """Apply a channel to a density matrix. + """ + Apply a channel to a density matrix. Parameters ---------- - :rho: density matrix. - channel: :class:`graphix.channel.KrausChannel` object - KrausChannel to be applied to the density matrix - qargs: target qubit indices + channel : KrausChannel + The KrausChannel to be applied to the density matrix. + qargs : Sequence[int] + The target qubit indices where the channel will be applied. Returns ------- - nothing + None + This function modifies the density matrix in place and does not return a value. Raises ------ ValueError - If the final density matrix is not normalized after application of the channel. - This shouldn't happen since :class:`graphix.channel.KrausChannel` objects are normalized by construction. - .... + If the final density matrix is not normalized after the application of the channel. + This shouldn't happen since KrausChannel objects are normalized by construction. """ result_array = np.zeros((2**self.nqubit, 2**self.nqubit), dtype=np.complex128) @@ -392,26 +550,56 @@ def apply_channel(self, channel: KrausChannel, qargs: Sequence[int]) -> None: @override def apply_noise(self, qubits: Sequence[int], noise: Noise) -> None: - """Apply noise. + """ + Apply noise to the specified qubits. Parameters ---------- - qubits : sequence of ints. - Target qubits + qubits : sequence of int + The target qubits to which the noise will be applied. noise : Noise - Noise to apply + The noise process to be applied to the specified qubits. """ channel = noise.to_kraus_channel() self.apply_channel(channel, qubits) def subs(self, variable: Parameter, substitute: ExpressionOrSupportsFloat) -> DensityMatrix: - """Return a copy of the density matrix where all occurrences of the given variable in measurement angles are substituted by the given value.""" + """ + Return a copy of the density matrix with all occurrences of a specified variable in measurement angles replaced by a provided value. + + Parameters + ---------- + variable : Parameter + The variable to be substituted in the density matrix. + substitute : ExpressionOrSupportsFloat + The value or expression that will replace the specified variable. + + Returns + ------- + DensityMatrix + A new instance of `DensityMatrix` with the substitutions applied. + """ result = copy.copy(self) result.rho = np.vectorize(lambda value: parameter.subs(value, variable, substitute))(self.rho) return result def xreplace(self, assignment: Mapping[Parameter, ExpressionOrSupportsFloat]) -> DensityMatrix: - """Return a copy of the density matrix where all occurrences of the given keys in measurement angles are substituted by the given values in parallel.""" + """ + Return a copy of the density matrix with all occurrences of the specified keys + in measurement angles substituted by the corresponding values in parallel. + + Parameters + ---------- + assignment : Mapping[Parameter, ExpressionOrSupportsFloat] + A mapping of parameters to their replacement values. The keys represent + the parameters whose occurrences in the density matrix will be replaced, + and the values are the new values to substitute. + + Returns + ------- + DensityMatrix + A new DensityMatrix instance with the substitutions applied. + """ result = copy.copy(self) result.rho = np.vectorize(lambda value: parameter.xreplace(value, assignment))(self.rho) return result @@ -419,6 +607,25 @@ def xreplace(self, assignment: Mapping[Parameter, ExpressionOrSupportsFloat]) -> @dataclass(frozen=True) class DensityMatrixBackend(DenseStateBackend[DensityMatrix]): - """MBQC simulator with density matrix method.""" + """ + A class representing a Measurement-Based Quantum Computation (MBQC) simulator + using the density matrix method. + + Attributes + ---------- + density_matrix : numpy.ndarray + The density matrix representing the state of the quantum system. + + Methods + ------- + apply_gate(gate, qubits) + Applies a quantum gate to specified qubits in the density matrix. + + measure(qubit) + Measures a specified qubit and updates the density matrix according to the measurement outcome. + + reset() + Resets the density matrix to the initial state. + """ state: DensityMatrix = dataclasses.field(init=False, default_factory=lambda: DensityMatrix(nqubit=0)) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index 058260651..893caa45c 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -1,4 +1,9 @@ -"""MBQC state vector backend.""" +""" +MBQC State Vector Backend +========================== + +This module provides functionality for simulating measurement-based quantum computation (MBQC) using a state vector representation. +""" from __future__ import annotations @@ -41,7 +46,35 @@ class Statevec(DenseState): - """Statevector object.""" + """ + Statevector object. + + The Statevec class represents a quantum state in the form of a vector. + It provides methods to manipulate and perform calculations on the + statevector, enabling quantum mechanical simulations. + + Attributes + ---------- + vector : numpy.ndarray + A complex-valued array representing the quantum state. + + Methods + ------- + normalize(): + Normalizes the statevector to ensure it is a valid quantum state. + + tensor_product(other): + Computes the tensor product of the current statevector with another. + + measure(basis): + Measures the statevector in the specified basis and returns the outcome. + + __str__(): + Returns a string representation of the statevector. + + __repr__(): + Returns a detailed string representation of the statevector for debugging. + """ psi: Matrix @@ -50,26 +83,27 @@ def __init__( data: Data = BasicStates.PLUS, nqubit: int | None = None, ) -> None: - """Initialize statevector objects. + """ + Initialize statevector objects. - `data` can be: - - a single :class:`graphix.states.State` (classical description of a quantum state) - - an iterable of :class:`graphix.states.State` objects - - an iterable of scalars (A 2**n numerical statevector) - - a *graphix.statevec.Statevec* object + The `data` parameter can be one of the following: + - A single :class:`graphix.states.State` (classical description of a quantum state). + - An iterable of :class:`graphix.states.State` objects. + - An iterable of scalars (a 2**n numerical statevector). + - A *graphix.statevec.Statevec* object. - If *nqubit* is not provided, the number of qubit is inferred from *data* and checked for consistency. - If only one :class:`graphix.states.State` is provided and nqubit is a valid integer, initialize the statevector - in the tensor product state. - If both *nqubit* and *data* are provided, consistency of the dimensions is checked. - If a *graphix.statevec.Statevec* is passed, returns a copy. + If *nqubit* is not provided, the number of qubits is inferred from *data* and checked for consistency. + If only one :class:`graphix.states.State` is provided and *nqubit* is a valid integer, the statevector is initialized + in the tensor product state. If both *nqubit* and *data* are provided, consistency of the dimensions is checked. + If a *graphix.statevec.Statevec* is passed, a copy of it is returned. Parameters ---------- data : Data, optional - input data to prepare the state. Can be a classical description or a numerical input, defaults to graphix.states.BasicStates.PLUS + Input data to prepare the state. Can be a classical description or a numerical input. Defaults to + :class:`graphix.states.BasicStates.PLUS`. nqubit : int, optional - number of qubits to prepare, defaults to None + Number of qubits to prepare. Defaults to None. """ if nqubit is not None and nqubit < 0: raise ValueError("nqubit must be a non-negative integer.") @@ -139,12 +173,19 @@ def get_statevector( raise TypeError(f"First element of data has type {type(input_list[0])} whereas Number or State is expected") def __str__(self) -> str: - """Return a string description.""" + """ + Return a string representation of the Statevec instance. + + Returns + ------- + str + A description of the Statevec instance. + """ return f"Statevec object with statevector {self.psi} and length {self.dims()}." @override def add_nodes(self, nqubit: int, data: Data) -> None: - r""" + """ Add nodes (qubits) to the state vector and initialize them in a specified state. Parameters @@ -153,14 +194,14 @@ def add_nodes(self, nqubit: int, data: Data) -> None: The number of qubits to add to the state vector. data : Data, optional - The state in which to initialize the newly added nodes. + The state in which to initialize the newly added nodes. It can take the following forms: - - If a single basic state is provided, all new nodes are initialized in that state. - - If a list of basic states is provided, it must match the length of ``nodes``, and - each node is initialized with its corresponding state. - - A single-qubit state vector will be broadcast to all nodes. - - A multi-qubit state vector of dimension :math:`2^n`, where :math:`n = \mathrm{len}(nodes)`, - initializes the new nodes jointly. + - A single basic state, in which case all new nodes are initialized to that state. + - A list of basic states, which must match the length of `nodes`, where each node is initialized + with its corresponding state. + - A single-qubit state vector, which will be broadcast to all new nodes. + - A multi-qubit state vector with dimension :math:`2^n`, where :math:`n = \mathrm{len}(nodes)`, + which initializes the new nodes jointly. Notes ----- @@ -171,28 +212,30 @@ def add_nodes(self, nqubit: int, data: Data) -> None: @override def evolve_single(self, op: Matrix, i: int) -> None: - """Apply a single-qubit operation. + """ + Apply a single-qubit operation to the specified qubit index. Parameters ---------- op : numpy.ndarray - 2*2 matrix + A 2x2 matrix representing the single-qubit operation to be applied. i : int - qubit index + The index of the qubit on which the operation will be performed. """ psi = tensordot(op, self.psi, (1, i)) self.psi = np.moveaxis(psi, 0, i) @override def evolve(self, op: Matrix, qargs: Sequence[int]) -> None: - """Apply a multi-qubit operation. + """ + Apply a multi-qubit operation. Parameters ---------- op : numpy.ndarray - 2^n*2^n matrix - qargs : list of int - target qubits' indices + A 2^n x 2^n matrix representing the operation to be applied. + qargs : Sequence[int] + A sequence of integers representing the indices of the target qubits. """ op_dim = int(np.log2(len(op))) # TODO shape = (2,)* 2 * op_dim @@ -206,25 +249,40 @@ def evolve(self, op: Matrix, qargs: Sequence[int]) -> None: self.psi = np.moveaxis(psi, range(len(qargs)), qargs) def dims(self) -> tuple[int, ...]: - """Return the dimensions.""" + """ + Returns the dimensions of the state vector. + + Returns + ------- + tuple[int, ...] + A tuple representing the dimensions of the state vector. + """ return self.psi.shape # Note that `@property` must appear before `@override` for pyright @property @override def nqubit(self) -> int: - """Return the number of qubits.""" + """ + Get the number of qubits in the quantum state. + + Returns + ------- + int + The number of qubits represented by the state vector. + """ return self.psi.size - 1 @override def remove_qubit(self, qarg: int) -> None: - r"""Remove a separable qubit from the system and assemble a statevector for remaining qubits. + """ + Remove a separable qubit from the system and assemble a statevector for the remaining qubits. - This results in the same result as partial trace, if the qubit *qarg* is separable from the rest. + This method produces a result equivalent to performing a partial trace if the specified qubit + (*qarg*) is separable from the rest of the qubits. - For a statevector :math:`\ket{\psi} = \sum c_i \ket{i}` with sum taken over - :math:`i \in [ 0 \dots 00,\ 0\dots 01,\ \dots,\ - 1 \dots 11 ]`, this method returns + For a statevector :math:`\ket{\psi} = \sum c_i \ket{i}` with the sum taken over + :math:`i \in [0 \dots 00,\ 0\dots 01,\ \dots,\ 1 \dots 11]`, this method returns .. math:: \begin{align} @@ -238,26 +296,25 @@ def remove_qubit(self, qarg: int) -> None: & + \dots \\ & + c_{1 \dots 1_{\mathrm{k-1}}0_{\mathrm{k}}1_{\mathrm{k+1}} \dots 11} \ket{1 \dots 1_{\mathrm{k-1}}1_{\mathrm{k+1}} \dots 11}, - \end{align} + \end{align} - (after normalization) for :math:`k =` qarg. If the :math:`k` th qubit is in :math:`\ket{1}` state, - above will return zero amplitudes; in such a case the returned state will be the one above with - :math:`0_{\mathrm{k}}` replaced with :math:`1_{\mathrm{k}}` . + (after normalization), where :math:`k` is equal to *qarg*. If the :math:`k` th qubit is in the + state :math:`\ket{1}`, the above will yield zero amplitudes. In this case, the returned + state will be the one above with :math:`0_{\mathrm{k}}` replaced by :math:`1_{\mathrm{k}}`. .. warning:: - This method assumes the qubit with index *qarg* to be separable from the rest, - and is implemented as a significantly faster alternative for partial trace to - be used after single-qubit measurements. - Care needs to be taken when using this method. - Checks for separability will be implemented soon as an option. + This method assumes that the qubit with index *qarg* is separable from the other qubits + and is designed to be a significantly faster alternative to the partial trace used after + single-qubit measurements. Care should be taken when using this method. Checks for + separability will be implemented as an option in the future. .. seealso:: - :meth:`graphix.sim.statevec.Statevec.ptrace` and warning therein. + :meth:`graphix.sim.statevec.Statevec.ptrace` and the associated warnings. Parameters ---------- qarg : int - qubit index + The index of the qubit to be removed. """ norm = _get_statevec_norm(self.psi) if isinstance(norm, SupportsFloat): @@ -274,12 +331,17 @@ def remove_qubit(self, qarg: int) -> None: @override def entangle(self, edge: tuple[int, int]) -> None: - """Connect graph nodes. + """ + Connect graph nodes by creating an entangled state between the specified qubits. Parameters ---------- edge : tuple of int - (control, target) qubit indices + A tuple containing two integers representing the indices of the control and target qubits, respectively. The first element of the tuple is the index of the control qubit, and the second element is the index of the target qubit. + + Returns + ------- + None """ # contraction: 2nd index - control index, and 3rd index - target index. psi = tensordot(CZ_TENSOR, self.psi, ((2, 3), edge)) @@ -287,14 +349,15 @@ def entangle(self, edge: tuple[int, int]) -> None: self.psi = np.moveaxis(psi, (0, 1), edge) def tensor(self, other: Statevec) -> None: - r"""Tensor product state with other qubits. + """ + Compute the tensor product of the current state with another state. - Results in self :math:`\otimes` other. + The result is stored in the current state as :math:`self \otimes other`. Parameters ---------- - other : :class:`graphix.sim.statevec.Statevec` - statevector to be tensored with self + other : Statevec + The statevector to be tensored with the current state. """ psi_self = self.psi.flatten() psi_other = other.psi.flatten() @@ -303,12 +366,13 @@ def tensor(self, other: Statevec) -> None: self.psi = kron(psi_self, psi_other).reshape((2,) * total_num) def cnot(self, qubits: tuple[int, int]) -> None: - """Apply CNOT. + """ + Apply the CNOT (Controlled-NOT) gate to the state vector. Parameters ---------- qubits : tuple of int - (control, target) qubit indices + A tuple containing the indices of the control and target qubits, respectively. """ # contraction: 2nd index - control index, and 3rd index - target index. psi = tensordot(CNOT_TENSOR, self.psi, ((2, 3), qubits)) @@ -317,12 +381,15 @@ def cnot(self, qubits: tuple[int, int]) -> None: @override def swap(self, qubits: tuple[int, int]) -> None: - """Swap qubits. + """ + Swap the specified qubits. Parameters ---------- qubits : tuple of int - (control, target) qubit indices + A tuple containing the indices of the qubits to be swapped. + The first element is the index of the control qubit, and the + second element is the index of the target qubit. """ # contraction: 2nd index - control index, and 3rd index - target index. psi = tensordot(SWAP_TENSOR, self.psi, ((2, 3), qubits)) @@ -330,7 +397,16 @@ def swap(self, qubits: tuple[int, int]) -> None: self.psi = np.moveaxis(psi, (0, 1), qubits) def normalize(self) -> None: - """Normalize the state in-place.""" + """ + Normalize the state vector in-place. + + This method modifies the state vector of the instance to ensure that it has a unit norm. + The normalization is performed by dividing the state vector by its norm. + + Returns + ------- + None + """ # Note that the following calls to `astype` are guaranteed to # return the original NumPy array itself, since `copy=False` and # the `dtype` matches. This is important because the array is @@ -345,23 +421,42 @@ def normalize(self) -> None: psi_c /= norm_c def flatten(self) -> Matrix: - """Return flattened statevector.""" + """ + Return the flattened state vector. + + This method transforms the current state vector into a one-dimensional + representation, making it easier to work with in various applications + such as computations or visualizations. + + Returns + ------- + Matrix + A one-dimensional array that represents the flattened state vector. + + Notes + ----- + The flattening is done by reshaping the original state vector into a + single row or column, depending on the internal representation of + the state vector. + """ return self.psi.flatten() @override def expectation_single(self, op: Matrix, loc: int) -> complex: - """Return the expectation value of single-qubit operator. + """ + Return the expectation value of a single-qubit operator. Parameters ---------- op : numpy.ndarray - 2*2 operator + A 2x2 operator representing the quantum operation on a single qubit. loc : int - target qubit index + The index of the target qubit. Returns ------- - complex : expectation value. + complex + The expectation value of the operator for the specified qubit. """ st1 = copy.copy(self) st1.normalize() @@ -370,18 +465,20 @@ def expectation_single(self, op: Matrix, loc: int) -> complex: return complex(np.dot(st2.psi.flatten().conjugate(), st1.psi.flatten())) def expectation_value(self, op: Matrix, qargs: Sequence[int]) -> complex: - """Return the expectation value of multi-qubit operator. + """ + Return the expectation value of a multi-qubit operator. Parameters ---------- op : numpy.ndarray - 2^n*2^n operator - qargs : list of int - target qubit indices + A 2^n x 2^n operator representing the multi-qubit operator. + qargs : Sequence[int] + A sequence of integers representing the target qubit indices. Returns ------- - complex : expectation value + complex + The expectation value of the operator on the specified qubits. """ st2 = copy.copy(self) st2.normalize() @@ -390,13 +487,49 @@ def expectation_value(self, op: Matrix, qargs: Sequence[int]) -> complex: return complex(np.dot(st2.psi.flatten().conjugate(), st1.psi.flatten())) def subs(self, variable: Parameter, substitute: ExpressionOrSupportsFloat) -> Statevec: - """Return a copy of the state vector where all occurrences of the given variable in measurement angles are substituted by the given value.""" + """ + Substitute occurrences of a variable in measurement angles with a given value. + + This method returns a new instance of the state vector in which all occurrences + of the specified variable are replaced by the provided substitute value in the + measurement angles. + + Parameters + ---------- + variable : Parameter + The variable to substitute in the measurement angles. + substitute : ExpressionOrSupportsFloat + The value to substitute for the variable. + + Returns + ------- + Statevec + A new instance of the state vector with the substitutions applied. + + Notes + ----- + The original state vector remains unchanged. + """ result = Statevec() result.psi = np.vectorize(lambda value: parameter.subs(value, variable, substitute))(self.psi) return result def xreplace(self, assignment: Mapping[Parameter, ExpressionOrSupportsFloat]) -> Statevec: - """Return a copy of the state vector where all occurrences of the given keys in measurement angles are substituted by the given values in parallel.""" + """ + Return a copy of the state vector with substitutions applied to measurement angles. + + Parameters + ---------- + assignment : Mapping[Parameter, ExpressionOrSupportsFloat] + A mapping of parameters (keys) to their corresponding values (expressions or floats) + that will replace the occurrences in the measurement angles of the state vector. + + Returns + ------- + Statevec + A new state vector with all occurrences of the given keys in measurement angles + substituted by the provided values, computed in parallel. + """ result = Statevec() result.psi = np.vectorize(lambda value: parameter.xreplace(value, assignment))(self.psi) return result @@ -404,13 +537,52 @@ def xreplace(self, assignment: Mapping[Parameter, ExpressionOrSupportsFloat]) -> @dataclass(frozen=True) class StatevectorBackend(DenseStateBackend[Statevec]): - """MBQC simulator with statevector method.""" + """ + MBQC Simulator using the statevector method. + + This class implements a simulator for measurement-based quantum computation (MBQC) + using a statevector approach. It provides functionalities for initializing quantum states, + performing measurements, and simulating quantum operations in a measurement-based framework. + + Attributes + ---------- + quantum_state : Statevector + The current quantum state represented as a statevector. + measurements : list + A list to keep track of the measurement outcomes. + + Methods + ------- + initialize_state(state: Statevector) -> None + Initializes the quantum state with the provided statevector. + + apply_gate(gate: str, qubits: list) -> None + Applies a quantum gate to specified qubits in the current state. + + measure(qubit: int) -> bool + Performs a measurement on the specified qubit and updates the state accordingly. + + reset() -> None + Resets the quantum state and measurement history. + """ state: Statevec = dataclasses.field(init=False, default_factory=lambda: Statevec(nqubit=0)) def _get_statevec_norm_symbolic(psi: npt.NDArray[np.object_]) -> ExpressionOrFloat: - """Return norm of the state.""" + """ + Calculate the norm of a given state vector. + + Parameters + ---------- + psi : npt.NDArray[np.object_] + A state vector represented as a NumPy array of objects. + + Returns + ------- + ExpressionOrFloat + The norm of the state vector, which can be either a symbolic expression or a float value. + """ flat = psi.flatten() return check_expression_or_float(np.sqrt(np.sum(flat.conj() * flat))) @@ -423,7 +595,19 @@ def _get_statevec_norm_numeric(psi: npt.NDArray[np.complex128]) -> float: def _get_statevec_norm(psi: Matrix) -> ExpressionOrFloat: - """Return norm of the state.""" + """ + Calculate the norm of the state vector. + + Parameters + ---------- + psi : Matrix + The state vector for which the norm is to be calculated. + + Returns + ------- + ExpressionOrFloat + The norm of the state vector. + """ # Narrow psi to concrete dtype if psi.dtype == np.object_: return _get_statevec_norm_symbolic(psi.astype(np.object_, copy=False)) diff --git a/graphix/sim/tensornet.py b/graphix/sim/tensornet.py index f2fd968fd..4126f1c86 100644 --- a/graphix/sim/tensornet.py +++ b/graphix/sim/tensornet.py @@ -1,4 +1,41 @@ -"""Tensor Network Simulator for MBQC.""" +""" +Tensor Network Simulator for Measurement-Based Quantum Computing (MBQC). + +This module provides tools for simulating quantum circuits using tensor network methods, +specifically tailored for the measurement-based model of quantum computation. +It allows for the representation and manipulation of quantum states as tensor networks, +facilitating the analysis and execution of MBQC protocols. + +Usage +----- +To use this module, import the necessary classes and functions, and create a tensor network +representation of your quantum state. You can then perform measurements and simulate +quantum gates within a measurement-based framework. + +Functions and Classes +--------------------- +- [List the functions and classes available in this module, if applicable] + +Examples +-------- +1. Import the module: + ```python + from tensor_network_simulator import MBQC + ``` + +2. Create and manipulate a tensor network: + ```python + network = MBQC.TensorNetwork(...) + ``` + +3. Simulate measurements: + ```python + result = network.measure(...) + ``` + +This module is intended for researchers and practitioners in quantum computing +seeking to explore the capabilities of MBQC through tensor network methods. +""" from __future__ import annotations @@ -47,7 +84,35 @@ class MBQCTensorNet(BackendState, TensorNetwork): - """Tensor Network Simulator interface for MBQC patterns, using quimb.tensor.core.TensorNetwork.""" + """ + Tensor Network Simulator interface for Measurement-Based Quantum Computation (MBQC) patterns. + + This class utilizes the `quimb.tensor.core.TensorNetwork` to facilitate + the simulation of quantum circuits and operations relevant to MBQC. + + Attributes + ---------- + tensor_network : TensorNetwork + The underlying tensor network representing the quantum state. + qubits : int + Number of qubits in the quantum circuit. + measurements : list of tuples + A list specifying the measurement outcomes and their corresponding qubit indices. + + Methods + ------- + add_qubit(): + Adds a qubit to the tensor network. + + apply_gate(gate, qubit_indices): + Applies a quantum gate to the specified qubits in the tensor network. + + measure(qubit_index): + Performs a measurement on the specified qubit and updates the tensor network. + + simulate(): + Runs the simulation of the quantum circuit based on the current state of the tensor network. + """ _dangling: dict[str, str] @@ -65,14 +130,18 @@ def __init__( Parameters ---------- - graph_nodes (optional): list of int - node indices of the graph state. - graph_edges (optional) : list of tuple of int - edge indices of the graph state. - default_output_nodes : list of int - output node indices at the end of MBQC operations, if known in advance. - ts (optional): quimb.tensor.core.TensorNetwork or empty list - optional initial state. + branch_selector : BranchSelector + Selector for branches in the MBQC. + graph_nodes : Iterable[int] or None, optional + List of integer node indices of the graph state. + graph_edges : Iterable[tuple[int, int]] or None, optional + List of tuples representing edge indices of the graph state. + default_output_nodes : Iterable[int] or None, optional + Output node indices at the end of MBQC operations, if known in advance. + ts : list[TensorNetwork] or TensorNetwork or None, optional + Optional initial state(s) of the tensor network. Can be a single tensor network or a list of them. + virtual : bool, optional + Flag indicating if the network operates in a virtual mode. Default is False. """ if ts is None: ts = [] @@ -85,17 +154,18 @@ def __init__( self.__branch_selector = branch_selector def get_open_tensor_from_index(self, index: int | str) -> npt.NDArray[np.complex128]: - """Get tensor specified by node index. The tensor has a dangling edge. + """ + Get tensor specified by node index. The tensor has a dangling edge. Parameters ---------- - index : str - node index + index : int or str + Node index. Returns ------- - numpy.ndarray : - Specified tensor + numpy.ndarray + Specified tensor with complex data type. """ if isinstance(index, int): index = str(index) @@ -106,15 +176,24 @@ def get_open_tensor_from_index(self, index: int | str) -> npt.NDArray[np.complex return tensor.data.astype(dtype=np.complex128) def add_qubit(self, index: int, state: PrepareState = "plus") -> None: - """Add a single qubit to the network. + """ + Add a single qubit to the network. Parameters ---------- index : int - index of the new qubit. - state (optional): str or 2-element np.ndarray - initial state of the new qubit. - "plus", "minus", "zero", "one", "iplus", "iminus", or 1*2 np.ndarray (arbitrary state). + Index of the new qubit. + state : PrepareState, optional + Initial state of the new qubit. Can be one of the following: + - "plus" + - "minus" + - "zero" + - "one" + - "iplus" + - "iminus" + - or a 1x2 numpy.ndarray representing an arbitrary state. + + The default is "plus". """ ind = gen_str() tag = str(index) @@ -143,16 +222,21 @@ def add_qubit(self, index: int, state: PrepareState = "plus") -> None: self._dangling[tag] = ind def evolve_single(self, index: int, arr: npt.NDArray[np.complex128], label: str = "U") -> None: - """Apply single-qubit operator to a qubit with the given index. + """ + Apply a single-qubit operator to a qubit at the specified index. Parameters ---------- index : int - qubit index. - arr : 2*2 numpy.ndarray - single-qubit operator. - label (optional): str - label for the gate. + The index of the qubit to which the operator will be applied. + arr : npt.NDArray[np.complex128] + A 2x2 numpy array representing the single-qubit operator. + label : str, optional + A label for the gate, defaults to "U". + + Returns + ------- + None """ old_ind = self._dangling[str(index)] tid = list(self._get_tids_from_inds(old_ind)) @@ -170,14 +254,16 @@ def evolve_single(self, index: int, arr: npt.NDArray[np.complex128], label: str self.add_tensor(node_ts) def add_qubits(self, indices: Sequence[int], states: PrepareState | Iterable[PrepareState] = "plus") -> None: - """Add qubits to the network. + """ + Add qubits to the network. Parameters ---------- - indices : iterator of int - indices of the new qubits. - states (optional): Data - initial state or list of initial states of the new qubits. + indices : Sequence[int] + Indices of the new qubits. + states : PrepareState or Iterable[PrepareState], optional + Initial state or list of initial states of the new qubits. + Defaults to "plus". """ if isinstance(states, str): states_iter: list[PrepareState] = [states] * len(indices) @@ -208,28 +294,30 @@ def measure_single( outcome: Outcome | None = None, rng: Generator | None = None, ) -> Outcome: - """Measure a node in specified basis. Note this does not perform the partial trace. + """ + Measure a node in a specified basis. Note that this does not perform the partial trace. Parameters ---------- index : int - index of the node to be measured. - basis : str or np.ndarray - default "Z". - measurement basis, "Z" or "X" or "Y" for Pauli basis measurements. - 1*2 numpy.ndarray for arbitrary measurement bases. - bypass_probability_calculation : bool - default True. - if True, skip the calculation of the probability of the measurement - result and use equal probability for each result. - if False, calculate the probability of the measurement result from the state. - outcome : int (0 or 1) - User-chosen measurement result, giving the outcome of (-1)^{outcome}. + Index of the node to be measured. + basis : str or np.ndarray, optional + Measurement basis, which can be "Z", "X", or "Y" for Pauli basis measurements, + or a 1x2 numpy.ndarray for arbitrary measurement bases. Default is "Z". + bypass_probability_calculation : bool, optional + If True (default), skips the calculation of the probability of the measurement + result and uses equal probability for each outcome. If False, calculates the + probability of the measurement result from the state. + outcome : Outcome or None, optional + User-chosen measurement result, specifying the outcome of (-1)^{outcome}. + Default is None. + rng : Generator or None, optional + Random number generator for stochastic measurements. Default is None. Returns ------- - int - measurement result. + Outcome + The measurement result. """ if bypass_probability_calculation: result = outcome if outcome is not None else self.__branch_selector.measure(index, lambda: 0.5, rng=rng) @@ -264,16 +352,20 @@ def measure_single( return result def set_graph_state(self, nodes: Iterable[int], edges: Iterable[tuple[int, int]]) -> None: - """Prepare the graph state without directly applying CZ gates. + """ + Prepare the graph state without directly applying CZ gates. Parameters ---------- - nodes : iterator of int - set of the nodes - edges : iterator of tuple - set of the edges - - .. seealso:: :meth:`~graphix.sim.tensornet.TensorNetworkBackend.__init__()` + nodes : iterable of int + A set of nodes in the graph. + edges : iterable of tuple of int + A set of edges represented as tuples, where each tuple contains two integers + indicating the nodes connected by the edge. + + See Also + -------- + :meth:`~graphix.sim.tensornet.TensorNetworkBackend.__init__()` """ ind_dict: dict[int, list[str]] = {} vec_dict: dict[int, list[bool]] = {} @@ -319,21 +411,23 @@ def _require_default_output_nodes(self) -> list[int]: def get_basis_coefficient( self, basis: int | str, normalize: bool = True, indices: Sequence[int] | None = None ) -> complex: - """Calculate the coefficient of a given computational basis. + """ + Calculate the coefficient of a given computational basis. Parameters ---------- basis : int or str - computational basis expressed in binary (str) or integer, e.g. 101 or 5. - normalize (optional): bool - if True, normalize the coefficient by the norm of the entire state. - indices (optional): list of int - target qubit indices to compute the coefficients, default is the MBQC output nodes (self.default_output_nodes). + Computational basis expressed in binary (str) or integer, e.g., '101' or 5. + normalize : bool, optional + If True, normalize the coefficient by the norm of the entire state. Default is True. + indices : Sequence[int], optional + Target qubit indices to compute the coefficients. Default is the MBQC output nodes + (self.default_output_nodes). Returns ------- coef : complex - coefficient + The coefficient associated with the specified basis. """ if indices is None: indices = self._require_default_output_nodes() @@ -383,19 +477,20 @@ def get_basis_amplitude(self, basis: str | int) -> float: return abs(coef) ** 2 def to_statevector(self, indices: Sequence[int] | None = None) -> npt.NDArray[np.complex128]: - """Retrieve the statevector from the tensornetwork. + """ + Retrieve the statevector from the tensor network. - This method tends to be slow however we plan to parallelize this. + This method tends to be slow; however, there are plans to parallelize its execution. Parameters ---------- - indices (optional): list of int - target qubit indices. Default is the MBQC output nodes (self.default_output_nodes). + indices : Sequence[int], optional + List of target qubit indices. Default is the MBQC output nodes (self.default_output_nodes). Returns ------- - numpy.ndarray : - statevector + npt.NDArray[np.complex128] + The statevector obtained from the tensor network. """ n_qubit = len(self._require_default_output_nodes()) if indices is None else len(indices) statevec: npt.NDArray[np.complex128] = np.zeros(2**n_qubit, np.complex128) @@ -404,16 +499,33 @@ def to_statevector(self, indices: Sequence[int] | None = None) -> npt.NDArray[np return statevec / np.linalg.norm(statevec) def flatten(self) -> npt.NDArray[np.complex128]: - """Return flattened statevector.""" + """ + Return a flattened state vector. + + The state vector is transformed into a one-dimensional array while retaining + its complex number format. This is useful for various calculations and + manipulations where a flat representation of the state is needed. + + Returns + ------- + npt.NDArray[np.complex128] + A one-dimensional array containing the flattened state vector elements. + """ return self.to_statevector().flatten() def get_norm(self, optimize: str | PathOptimizer | None = None) -> float: - """Calculate the norm of the state. + """ + Calculate the norm of the state. + + Parameters + ---------- + optimize : str or PathOptimizer, optional + The optimization method to use. If None, no optimization is performed. Returns ------- - float : - norm of the state + float + The norm of the state. """ tn_cp1 = self.copy() tn_cp2 = tn_cp1.conj() @@ -429,22 +541,25 @@ def expectation_value( output_node_indices: Iterable[int] | None = None, optimize: str | PathOptimizer | None = None, ) -> float: - """Calculate expectation value of the given operator. + """ + Calculate the expectation value of the given operator. Parameters ---------- op : numpy.ndarray - single- or multi-qubit Hermitian operator - qubit_indices : list of int - Applied positions of **logical** qubits. - output_node_indices (optional): list of int - Indices of nodes in the entire TN, that remain unmeasured after MBQC operations. - Default is the output nodes specified in measurement pattern (self.default_output_nodes). + Single- or multi-qubit Hermitian operator. + qubit_indices : Sequence[int] + Indices of the logical qubits where the operator is applied. + output_node_indices : Iterable[int] or None, optional + Indices of nodes in the entire tensor network that remain unmeasured after MBQC operations. + Defaults to the output nodes specified in the measurement pattern (self.default_output_nodes). + optimize : str, PathOptimizer or None, optional + Optimization method to be used. Defaults to None. Returns ------- - float : - Expectation value + float + The expectation value of the operator. """ out_inds = self._require_default_output_nodes() if output_node_indices is None else list(output_node_indices) target_nodes = [out_inds[ind] for ind in qubit_indices] @@ -477,17 +592,21 @@ def expectation_value( return exp_val / norm**2 def evolve(self, operator: npt.NDArray[np.complex128], qubit_indices: list[int], decompose: bool = True) -> None: - """Apply an arbitrary operator to the state. + """ + Apply an arbitrary operator to the quantum state. Parameters ---------- - operator : numpy.ndarray - operator. + operator : numpy.ndarray, shape (N, N) + The operator to be applied to the quantum state. It is assumed to be a square matrix with complex entries. qubit_indices : list of int - Applied positions of **logical** qubits. + The positions of the logical qubits to which the operator will be applied. decompose : bool, optional - default True - whether a given operator will be decomposed or not. If True, operator is decomposed into Matrix Product Operator(MPO) + Whether to decompose the given operator into a Matrix Product Operator (MPO). Default is True. + + Notes + ----- + If `decompose` is set to True, the operator will be decomposed. Otherwise, it will be applied directly to the state. """ if len(operator.shape) != len(qubit_indices) * 2: shape = [2 for _ in range(2 * len(qubit_indices))] @@ -523,18 +642,23 @@ def evolve(self, operator: npt.NDArray[np.complex128], qubit_indices: list[int], @override def copy(self, virtual: bool = False, deep: bool = False) -> MBQCTensorNet: - """Return the copy of this object. + """ + Return a copy of this object. Parameters ---------- + virtual : bool, optional + Defaults to False. + Whether to create a virtual copy (shared data) or not. + deep : bool, optional Defaults to False. Whether to copy the underlying data as well. Returns ------- - TensorNetworkBackend : - duplicated object + MBQCTensorNet + A duplicated object of the current instance. """ if deep: return deepcopy(self) @@ -542,27 +666,30 @@ def copy(self, virtual: bool = False, deep: bool = False) -> MBQCTensorNet: def _get_decomposed_cz() -> list[npt.NDArray[np.complex128]]: - """Return the decomposed cz tensors. + """ + Return the decomposed CZ tensors. This is an internal method. - CZ gate can be decomposed into two 3-rank tensors(Schmidt rank = 2). - Decomposing into low-rank tensors is important preprocessing for - the optimal contraction path searching problem. - So, in this backend, the DECOMPOSED_CZ gate is applied - instead of the original CZ gate. + The CZ gate can be decomposed into two 3-rank tensors (Schmidt rank = 2). + Decomposing into low-rank tensors is an important preprocessing step for + optimal contraction path searching. Therefore, in this backend, the + DECOMPOSED_CZ gate is applied instead of the original CZ gate. - Decomposing CZ gate + The decomposition of the CZ gate is illustrated as follows: - output output - | | | | - -------- SVD --- --- - | CZ | --> |L|----|R| - -------- --- --- - | | | | - input input + output output + | | | | + -------- SVD --- --- + | CZ | --> |L|----|R| + -------- --- --- + | | | | + input input - 4-rank x1 3-rank x2 + Returns + ------- + list[npt.NDArray[np.complex128]] + A list containing the decomposed CZ tensors. """ cz_ts = Tensor( Ops.CZ.reshape((2, 2, 2, 2)).astype(np.complex128), @@ -591,27 +718,29 @@ class _AbstractTensorNetworkBackend(Backend[MBQCTensorNet], ABC): @dataclass(frozen=True) class TensorNetworkBackend(_AbstractTensorNetworkBackend): - """Tensor Network Simulator for MBQC. + """ + Tensor Network Simulator for MBQC. - Executes the measurement pattern using TN expression of graph states. + Executes the measurement pattern using Tensor Network (TN) expressions of graph states. Parameters ---------- pattern : graphix.Pattern + The measurement pattern to be executed. graph_prep : str - 'parallel' : - Faster method for preparing a graph state. - The expression of a graph state can be obtained from the graph geometry. - See https://journals.aps.org/pra/abstract/10.1103/PhysRevA.76.052315 for detail calculation. + The method for preparing a graph state. Options include: + - 'parallel': + A faster method for preparing a graph state. The expression of a graph state can be obtained from the graph geometry. + Refer to https://journals.aps.org/pra/abstract/10.1103/PhysRevA.76.052315 for detailed calculations. Note that 'N' and 'E' commands in the measurement pattern are ignored. - 'sequential' : - Sequentially execute N and E commands, strictly following the measurement pattern. - In this strategy, All N and E commands executed sequentially. - 'auto'(default) : - Automatically select a preparation strategy based on the max degree of a graph - input_state : preparation for input states (only BasicStates.PLUS is supported for tensor networks yet), - branch_selector: :class:`graphix.branch_selector.BranchSelector`, optional - Branch selector to be used for measurements. + - 'sequential': + Executes 'N' and 'E' commands sequentially, strictly following the measurement pattern. All 'N' and 'E' commands are executed in this strategy. + - 'auto' (default): + Automatically selects a preparation strategy based on the maximum degree of the graph. + input_state : preparation for input states + Only BasicStates.PLUS is currently supported for tensor networks. + branch_selector : graphix.branch_selector.BranchSelector, optional + A branch selector to be used for measurements. """ def __init__( @@ -621,7 +750,28 @@ def __init__( input_state: Data | None = None, branch_selector: BranchSelector | None = None, ) -> None: - """Construct a tensor network backend.""" + """ + Construct a tensor network backend. + + Parameters + ---------- + pattern : Pattern + The pattern that defines the structure of the tensor network. + + graph_prep : str, optional + The method for preparing the tensor network graph. + Defaults to "auto". + + input_state : Data, optional + The initial input state to be used in the tensor network. + If None, a default initial state will be used. + Defaults to None. + + branch_selector : BranchSelector, optional + An optional selector for branches in the network. + If None, a default branch selector will be used. + Defaults to None. + """ if input_state is None: input_state = BasicStates.PLUS elif input_state != BasicStates.PLUS: @@ -680,15 +830,15 @@ def add_nodes(self, nodes: Sequence[int], data: Data = BasicStates.PLUS) -> None ---------- nodes : Sequence[int] A list of node indices to add to the backend. These indices can be any - integer values but must be fresh: each index must be distinct from all - previously added nodes. + integer values but must be distinct from all previously added nodes. data : Data, optional - The state in which to initialize the newly added nodes. + The state in which to initialize the newly added nodes. This parameter can be + either a single basic state or a list of basic states. - If a single basic state is provided, all new nodes are initialized in that state. - - If a list of basic states is provided, it must match the length of ``nodes``, and - each node is initialized with its corresponding state. + - If a list of basic states is provided, it must match the length of `nodes`, + and each node is initialized with its corresponding state. Notes ----- @@ -705,12 +855,13 @@ def add_nodes(self, nodes: Sequence[int], data: Data = BasicStates.PLUS) -> None @override def entangle_nodes(self, edge: tuple[int, int]) -> None: - """Make entanglement between nodes specified by edge. + """ + Make entanglement between the nodes specified by the given edge. Parameters ---------- edge : tuple of int - edge specifies two target nodes of the CZ gate. + A tuple specifying the two target nodes of the CZ gate. """ if self.graph_prep == "sequential": old_inds = [self.state._dangling[str(node)] for node in edge] @@ -742,17 +893,26 @@ def entangle_nodes(self, edge: tuple[int, int]) -> None: @override def measure(self, node: int, measurement: Measurement, rng: Generator | None = None) -> Outcome: - """Perform measurement of the node. + """ + Perform measurement of a specified node in the tensor network. - In the context of tensornetwork, performing measurement equals to - applying measurement operator to the tensor. Here, directly contracted with the projected state. + In the context of tensor networks, performing a measurement involves + applying a measurement operator to the tensor, which is then directly + contracted with the projected state. Parameters ---------- node : int - index of the node to measure + Index of the node to measure. measurement : Measurement - measure plane and angle + The measurement object that defines the measurement plane and angle. + rng : Generator, optional + A random number generator for stochastic processes. If None, a default generator is used. + + Returns + ------- + Outcome + The outcome of the measurement process, encapsulating the results of the measurement. """ if node in self._isolated_nodes: vector: npt.NDArray[np.complex128] = self.state.get_open_tensor_from_index(node) @@ -776,15 +936,16 @@ def measure(self, node: int, measurement: Measurement, rng: Generator | None = N @override def correct_byproduct(self, cmd: command.X | command.Z, measure_method: MeasureMethod) -> None: - """Perform byproduct correction. + """ + Perform byproduct correction. Parameters ---------- - cmd : list - Byproduct command - i.e. ['X' or 'Z', node, signal_domain] + cmd : command.X | command.Z + Byproduct command, which can be either 'X' or 'Z', along with the corresponding + node and signal domain. measure_method : MeasureMethod - The measure method to use + The measurement method to use for correction. """ if sum(measure_method.get_measure_result(j) for j in cmd.domain) % 2 == 1: op = Ops.X if isinstance(cmd, command.X) else Ops.Z @@ -792,38 +953,64 @@ def correct_byproduct(self, cmd: command.X | command.Z, measure_method: MeasureM @override def apply_clifford(self, node: int, clifford: Clifford) -> None: - """Apply single-qubit Clifford gate. + """ + Apply a single-qubit Clifford gate to the specified node. Parameters ---------- - cmd : list - clifford command. - See https://arxiv.org/pdf/2212.11975.pdf for the detail. + node : int + The index of the node to which the Clifford gate will be applied. + clifford : Clifford + The Clifford gate to be applied. For details on the Clifford gates, + see https://arxiv.org/pdf/2212.11975.pdf. """ self.state.evolve_single(node, clifford.matrix) @override def finalize(self, output_nodes: Iterable[int]) -> None: - """Do nothing.""" + """ + Finalize the tensor network backend. + + Parameters + ---------- + output_nodes : iterable of int + A collection of output node indices to be processed. + + Returns + ------- + None + + Notes + ----- + This method currently does not perform any actions. + """ def gen_str() -> str: - """Generate dummy string for einsum.""" + """ + Generate a dummy string for einsum. + + Returns + ------- + str + A dummy string representation suitable for einsum operations. + """ return qtn.rand_uuid() def outer_product(vectors: Sequence[npt.NDArray[np.complex128]]) -> npt.NDArray[np.complex128]: - """Return the outer product of the given vectors. + """ + Return the outer product of the given vectors. Parameters ---------- - vectors : list of vector - vectors + vectors : Sequence[npt.NDArray[np.complex128]] + A sequence of vectors for which the outer product is to be computed. Returns ------- - numpy.ndarray : - tensor object. + npt.NDArray[np.complex128] + The resulting outer product as a tensor object. """ subscripts = string.ascii_letters[: len(vectors)] subscripts = ",".join(subscripts) + "->" + subscripts diff --git a/graphix/simulator.py b/graphix/simulator.py index dc1ce6dbe..30290e80b 100644 --- a/graphix/simulator.py +++ b/graphix/simulator.py @@ -1,7 +1,8 @@ -"""MBQC simulator. - -Simulates MBQC by executing the pattern. +""" +MBQC Simulator. +This module simulates measurement-based quantum computation (MBQC) by executing +quantum patterns. """ from __future__ import annotations @@ -40,11 +41,18 @@ class MeasureMethod(abc.ABC): - """Measure method used by the simulator, with default measurement method that implements MBQC. + """ + Measure method used by the simulator. - To be overwritten by custom measurement methods in the case of delegated QC protocols. + This class implements the default measurement method based on Measurement-Based + Quantum Computation (MBQC). It is designed to be overridden by custom measurement + methods in the case of delegated quantum computing protocols. - Example: class `ClientMeasureMethod` in https://github.com/qat-inria/veriphix + Examples + -------- + A custom measurement method can be implemented by inheriting this class. + For instance, see the `ClientMeasureMethod` class in the repository: + https://github.com/qat-inria/veriphix. """ def measure( @@ -54,7 +62,24 @@ def measure( noise_model: NoiseModel | None = None, rng: Generator | None = None, ) -> None: - """Perform a measure.""" + """ + Perform a measurement using the specified backend. + + Parameters + ---------- + backend : Backend[_StateT_co] + The backend used to perform the measurement. + cmd : BaseM + The command that specifies the measurement operation. + noise_model : NoiseModel, optional + An optional noise model to simulate noise during the measurement. + rng : Generator, optional + An optional random number generator to use for stochastic processes. + + Returns + ------- + None + """ description = self.get_measurement_description(cmd) result = backend.measure(cmd.node, description, rng=rng) if noise_model is not None: @@ -63,7 +88,8 @@ def measure( @abc.abstractmethod def get_measurement_description(self, cmd: BaseM) -> Measurement: - """Return the description of the measurement performed by a command. + """ + Return the description of the measurement performed by a command. Parameters ---------- @@ -79,7 +105,8 @@ def get_measurement_description(self, cmd: BaseM) -> Measurement: @abc.abstractmethod def get_measure_result(self, node: int) -> Outcome: - """Return the result of a previous measurement. + """ + Return the result of a previous measurement. Parameters ---------- @@ -88,37 +115,65 @@ def get_measure_result(self, node: int) -> Outcome: Returns ------- - bool + Outcome Recorded measurement outcome. """ ... @abc.abstractmethod def set_measure_result(self, node: int, result: Outcome) -> None: - """Store the result of a previous measurement. + """ + Store the result of a previous measurement. Parameters ---------- node : int Node label of the measured qubit. - result : bool + result : Outcome Measurement outcome to store. """ ... class DefaultMeasureMethod(MeasureMethod): - """Default measurement method implementing standard measurement plane/angle update for MBQC.""" + """ + Default measurement method for implementing the standard measurement plane + and angle updates for measurement-based quantum computation (MBQC). + + This class provides functionality to perform measurements in the + context of MBQC, adhering to the standard approach of updating the + measurement plane and angles during the computation process. + + Attributes + ---------- + measurement_plane : Any + The current measurement plane used for measurements. + measurement_angle : float + The current angle for measurements. + + Methods + ------- + update_measurement_plane(plane) + Updates the measurement plane to the specified value. + + update_measurement_angle(angle) + Updates the measurement angle to the specified value. + + perform_measurement(qubit) + Performs a measurement on the specified qubit using the current + measurement plane and angle. + """ results: dict[int, Outcome] def __init__(self, results: Mapping[int, Outcome] | None = None): - """Initialize with an optional result dictionary. + """ + Initialize with an optional result dictionary. Parameters ---------- results : Mapping[int, Outcome] | None, optional - Mapping of previously measured nodes to their results. If ``None``, + A mapping of previously measured nodes to their results. If `None`, an empty dictionary is created. Notes @@ -131,7 +186,8 @@ def __init__(self, results: Mapping[int, Outcome] | None = None): self.results = {} if results is None else dict(results) def get_measurement_description(self, cmd: BaseM) -> Measurement: - """Return the description of the measurement performed by ``cmd``. + """ + Return the description of the measurement performed by `cmd`. Parameters ---------- @@ -153,7 +209,8 @@ def get_measurement_description(self, cmd: BaseM) -> Measurement: return Measurement(angle, measure_update.new_plane) def get_measure_result(self, node: int) -> Outcome: - """Return the result of a previous measurement. + """ + Return the result of a previous measurement. Parameters ---------- @@ -168,22 +225,36 @@ def get_measure_result(self, node: int) -> Outcome: return self.results[node] def set_measure_result(self, node: int, result: Outcome) -> None: - """Store the result of a previous measurement. + """ + Store the result of a previous measurement. Parameters ---------- node : int Node label of the measured qubit. - result : bool + result : Outcome Measurement outcome to store. """ self.results[node] = result class PatternSimulator: - """MBQC simulator. + """ + MBQC simulator. Executes the measurement pattern. + + Attributes + ---------- + measurement_pattern : list + A sequence of measurements to be executed. + qubits : list + The qubits that are being simulated. + + Methods + ------- + run(): + Executes the simulation based on the measurement pattern. """ noise_model: NoiseModel | None @@ -203,25 +274,32 @@ def __init__( Parameters ---------- - pattern: :class:`Pattern` object + pattern : :class:`Pattern` MBQC pattern to be simulated. - backend: :class:`Backend` object, - or 'statevector', or 'densitymatrix', or 'tensornetwork' - simulation backend (optional), default is 'statevector'. - measure_method: :class:`MeasureMethod`, optional + backend : :class:`Backend` or str, optional + Simulation backend, which can be a :class:`Backend` object, + or one of the following strings: 'statevector', 'densitymatrix', or 'tensornetwork'. + Default is 'statevector'. + measure_method : :class:`MeasureMethod`, optional Measure method used by the simulator. Default is :class:`DefaultMeasureMethod`. - noise_model: :class:`NoiseModel`, optional - [Density matrix backend only] Noise model used by the simulator. - branch_selector: :class:`BranchSelector`, optional - Branch selector used for measurements. Can only be specified if ``backend`` is not an already instantiated :class:`Backend` object. Default is :class:`RandomBranchSelector`. - graph_prep: str, optional - [Tensor network backend only] Strategy for preparing the graph state. See :class:`TensorNetworkBackend`. + noise_model : :class:`NoiseModel`, optional + Noise model used by the simulator. Applicable only when using the density matrix backend. + branch_selector : :class:`BranchSelector`, optional + Branch selector used for measurements. This can only be specified if + ``backend`` is not an already instantiated :class:`Backend` object. + Default is :class:`RandomBranchSelector`. + graph_prep : str, optional + Strategy for preparing the graph state, applicable only for the tensor network backend. + See :class:`TensorNetworkBackend`. symbolic : bool, optional - [State vector and density matrix backends only] If True, support arbitrary objects (typically, symbolic expressions) in measurement angles. - - .. seealso:: :class:`graphix.sim.statevec.StatevectorBackend`\ - :class:`graphix.sim.tensornet.TensorNetworkBackend`\ - :class:`graphix.sim.density_matrix.DensityMatrixBackend`\ + If True, allows support for arbitrary objects (typically symbolic expressions) + in measurement angles. Applicable only for state vector and density matrix backends. + + See Also + -------- + :class:`graphix.sim.statevec.StatevectorBackend` + :class:`graphix.sim.tensornet.TensorNetworkBackend` + :class:`graphix.sim.density_matrix.DensityMatrixBackend` """ def initialize_backend() -> Backend[BackendState]: @@ -268,31 +346,61 @@ def initialize_backend() -> Backend[BackendState]: @property def pattern(self) -> Pattern: - """Return the pattern.""" + """ + Return the pattern. + + Returns + ------- + Pattern + The pattern associated with the simulator. + """ return self.__pattern @property def measure_method(self) -> MeasureMethod: - """Return the measure method.""" + """ + Get the measurement method used in the pattern simulator. + + Returns + ------- + MeasureMethod + The current measurement method employed by the pattern simulator. + """ return self.__measure_method def set_noise_model(self, model: NoiseModel | None) -> None: - """Set a noise model.""" + """ + Set the noise model for the PatternSimulator. + + Parameters + ---------- + model : NoiseModel | None + The noise model to be used by the simulator. If None, the default noise model + will be applied. + + Returns + ------- + None + """ self.noise_model = model def run(self, input_state: Data = BasicStates.PLUS, rng: Generator | None = None) -> None: - """Perform the simulation. + """ + Perform the simulation. + + Parameters + ---------- + input_state : Data, optional + The initial quantum state for the simulation. The default value is + ``|+>``. + rng : Generator, optional + A random-number generator for measurements. This generator is used + only in the case of random branch selection (see + :class:`RandomBranchSelector`). Returns ------- - input_state: Data, optional - the output quantum state, - in the representation depending on the backend used. - Default: ``|+>``. - rng: Generator, optional - Random-number generator for measurements. - This generator is used only in case of random branch selection - (see :class:`RandomBranchSelector`). + None """ if input_state is not None: self.backend.add_nodes(self.pattern.input_nodes, input_state) diff --git a/graphix/states.py b/graphix/states.py index c6788ebac..1ccbcbc85 100644 --- a/graphix/states.py +++ b/graphix/states.py @@ -1,4 +1,11 @@ -"""Quantum states and operators.""" +""" +Quantum states and operators. + +This module provides definitions and functionalities for quantum states +and operators, crucial components in the study of quantum mechanics and +quantum computing. It includes representations, manipulations, and +operations associated with quantum systems. +""" from __future__ import annotations @@ -17,53 +24,99 @@ # generic class State for all States # FIXME: Name conflict class State(ABC): - """Abstract base class for single qubit states objects. + """ + Abstract base class for single qubit state objects. - Only requirement for concrete classes is to have - a get_statevector() method that returns the statevector - representation of the state + The only requirement for concrete classes is to implement + the `get_statevector()` method, which returns the state vector + representation of the state. """ @abc.abstractmethod def get_statevector(self) -> npt.NDArray[np.complex128]: - """Return the state vector.""" + """ + Get the state vector of the quantum state. + + Returns + ------- + npt.NDArray[np.complex128] + The complex-valued state vector representing the quantum state. + """ def get_densitymatrix(self) -> npt.NDArray[np.complex128]: - """Return the density matrix.""" + """ + Return the density matrix of the quantum state. + + Returns + ------- + numpy.ndarray + A complex-valued 2D array representing the density matrix of the state, + where each entry corresponds to the probability amplitudes for the quantum state. + """ # return DM in 2**n x 2**n dim (2x2 here) return np.outer(self.get_statevector(), self.get_statevector().conj()).astype(np.complex128, copy=False) @dataclasses.dataclass class PlanarState(State): - """Light object used to instantiate backends. - - doesn't cover all possible states but this is - covered in :class:`graphix.sim.statevec.Statevec` - and :class:`graphix.sim.densitymatrix.DensityMatrix` - constructors. - - :param plane: One of the three planes (XY, XZ, YZ) - :type plane: :class:`graphix.pauli.Plane` - :param angle: angle IN RADIANS - :type angle: complex - :return: State - :rtype: :class:`graphix.states.State` object + """ + Light object used to instantiate backends. + + This class does not cover all possible states; this is addressed + in :class:`graphix.sim.statevec.Statevec` and + :class:`graphix.sim.densitymatrix.DensityMatrix` constructors. + + Parameters + ---------- + plane : :class:`graphix.pauli.Plane` + One of the three planes (XY, XZ, YZ). + angle : complex + Angle in radians. + + Returns + ------- + : class:`graphix.states.State` + A State object. """ plane: Plane angle: float def __repr__(self) -> str: - """Return a string representation of the planar state.""" + """ + Return a string representation of the PlanarState object. + + Returns + ------- + str + A string that represents the current state of the PlanarState instance. + """ return f"graphix.states.PlanarState({self.plane}, {self.angle})" def __str__(self) -> str: - """Return a string description of the planar state.""" + """ + Return a string representation of the planar state. + + This method provides a textual description of the current state + of the PlanarState instance, suitable for output and logging. + + Returns + ------- + str + A string that describes the planar state. + """ return f"PlanarState object defined in plane {self.plane} with angle {self.angle}." def get_statevector(self) -> npt.NDArray[np.complex128]: - """Return the state vector.""" + """ + Return the state vector of the PlanarState instance. + + Returns + ------- + statevector : ndarray, shape (n,) + The state vector represented as a complex ndarray, where + n is the dimensionality of the state. + """ if self.plane == Plane.XY: return np.asarray([1 / np.sqrt(2), np.exp(1j * self.angle) / np.sqrt(2)], dtype=np.complex128) @@ -78,7 +131,31 @@ def get_statevector(self) -> npt.NDArray[np.complex128]: # States namespace for input initialization. class BasicStates: - """Basic states.""" + """ + Basic states. + + This class represents a collection of basic states used in the application. + + Parameters + ---------- + None + + Attributes + ---------- + states : list + A list of basic state representations. + + Methods + ------- + add_state(state) + Adds a new state to the list of basic states. + + remove_state(state) + Removes a state from the list of basic states if it exists. + + get_states() + Returns a list of all the current states. + """ ZERO: ClassVar[PlanarState] = PlanarState(Plane.XZ, 0) ONE: ClassVar[PlanarState] = PlanarState(Plane.XZ, np.pi) diff --git a/graphix/transpiler.py b/graphix/transpiler.py index 39051797c..c27072348 100644 --- a/graphix/transpiler.py +++ b/graphix/transpiler.py @@ -1,7 +1,7 @@ -"""Gate-to-MBQC transpiler. - -accepts desired gate operations and transpile into MBQC measurement patterns. +""" +Gate-to-MBQC transpiler. +This module accepts desired gate operations and transpiles them into measurement-based quantum computation (MBQC) measurement patterns. """ from __future__ import annotations @@ -35,8 +35,12 @@ class TranspileResult: """ The result of a transpilation. - pattern : :class:`graphix.pattern.Pattern` object - classical_outputs : tuple[int,...], index of nodes measured with *M* gates + Parameters + ---------- + pattern : :class:`graphix.pattern.Pattern` + The pattern object representing the transpiled circuit. + classical_outputs : tuple of int + A tuple containing the indices of nodes measured with *M* gates. """ pattern: Pattern @@ -48,8 +52,12 @@ class SimulateResult: """ The result of a simulation. - statevec : :class:`graphix.sim.statevec.Statevec` object - classical_measures : tuple[int,...], classical measures + Parameters + ---------- + statevec : graphix.sim.statevec.Statevec + The state vector after the simulation. + classical_measures : tuple of int + A tuple containing the classical measures obtained from the simulation. """ statevec: Statevec @@ -68,16 +76,17 @@ def _check_target(out: Sequence[int | None], index: int) -> int: class Circuit: - """Gate-to-MBQC transpiler. + """ + Gate-to-MBQC transpiler. - Holds gate operations and translates into MBQC measurement patterns. + Holds gate operations and translates them into MBQC measurement patterns. Attributes ---------- width : int - Number of logical qubits (for gate network) + Number of logical qubits for the gate network. instruction : list - List containing the gate sequence applied. + List containing the sequence of gates applied. """ instruction: list[Instruction] @@ -89,9 +98,9 @@ def __init__(self, width: int, instr: Iterable[Instruction] | None = None) -> No Parameters ---------- width : int - number of logical qubits for the gate network - instr : list[instruction.Instruction] | None - Optional. List of initial instructions. + The number of logical qubits for the gate network. + instr : Iterable[Instruction], optional + A list of initial instructions. If None, no initial instructions are provided. Default is None. """ self.width = width self.instruction = [] @@ -100,7 +109,18 @@ def __init__(self, width: int, instr: Iterable[Instruction] | None = None) -> No self.extend(instr) def add(self, instr: Instruction) -> None: - """Add an instruction to the circuit.""" + """ + Add an instruction to the circuit. + + Parameters + ---------- + instr : Instruction + The instruction to be added to the circuit. + + Returns + ------- + None + """ if instr.kind == InstructionKind.CCX: self.ccx(instr.controls[0], instr.controls[1], instr.target) elif instr.kind == InstructionKind.RZZ: @@ -136,23 +156,57 @@ def add(self, instr: Instruction) -> None: assert_never(instr.kind) def extend(self, instrs: Iterable[Instruction]) -> None: - """Add instructions to the circuit.""" + """ + Extend the circuit by adding a sequence of instructions. + + Parameters + ---------- + instrs : Iterable[Instruction] + An iterable collection of `Instruction` objects to be added to the circuit. + + Returns + ------- + None + This method modifies the circuit in place and does not return a value. + + Notes + ----- + This method allows for the addition of multiple instructions in one call, + which can be useful for building complex circuits more efficiently. + """ for instr in instrs: self.add(instr) def __repr__(self) -> str: - """Return a representation of the Circuit.""" + """ + Return a string representation of the Circuit. + + This method provides a concise summary of the Circuit object that + can be useful for debugging and logging purposes. The output + format may include key attributes and their values to give an + overview of the Circuit's state. + + Returns + ------- + str + A string representing the Circuit. + """ return f"Circuit(width={self.width}, instr={self.instruction})" def cnot(self, control: int, target: int) -> None: - """Apply a CNOT gate. + """ + Apply a CNOT gate. Parameters ---------- control : int - control qubit + The index of the control qubit. target : int - target qubit + The index of the target qubit. + + Returns + ------- + None """ assert control in self.active_qubits assert target in self.active_qubits @@ -160,14 +214,19 @@ def cnot(self, control: int, target: int) -> None: self.instruction.append(instruction.CNOT(control=control, target=target)) def swap(self, qubit1: int, qubit2: int) -> None: - """Apply a SWAP gate. + """ + Apply a SWAP gate between two qubits. Parameters ---------- qubit1 : int - first qubit to be swapped + The index of the first qubit to be swapped. qubit2 : int - second qubit to be swapped + The index of the second qubit to be swapped. + + Returns + ------- + None """ assert qubit1 in self.active_qubits assert qubit2 in self.active_qubits @@ -175,134 +234,165 @@ def swap(self, qubit1: int, qubit2: int) -> None: self.instruction.append(instruction.SWAP(targets=(qubit1, qubit2))) def h(self, qubit: int) -> None: - """Apply a Hadamard gate. + """ + Apply a Hadamard gate to the specified qubit. Parameters ---------- qubit : int - target qubit + The index of the target qubit on which the Hadamard gate will be applied. """ assert qubit in self.active_qubits self.instruction.append(instruction.H(target=qubit)) def s(self, qubit: int) -> None: - """Apply an S gate. + """ + Apply an S gate to the specified qubit. Parameters ---------- qubit : int - target qubit + The target qubit on which to apply the S gate. """ assert qubit in self.active_qubits self.instruction.append(instruction.S(target=qubit)) def x(self, qubit: int) -> None: - """Apply a Pauli X gate. + """ + Apply a Pauli X gate to the specified qubit. Parameters ---------- qubit : int - target qubit + The index of the target qubit on which the Pauli X gate will be applied. """ assert qubit in self.active_qubits self.instruction.append(instruction.X(target=qubit)) def y(self, qubit: int) -> None: - """Apply a Pauli Y gate. + """ + Apply a Pauli Y gate. Parameters ---------- qubit : int - target qubit + The index of the target qubit. """ assert qubit in self.active_qubits self.instruction.append(instruction.Y(target=qubit)) def z(self, qubit: int) -> None: - """Apply a Pauli Z gate. + """ + Apply a Pauli Z gate to the specified qubit. Parameters ---------- qubit : int - target qubit + The index of the target qubit on which the Pauli Z gate will be applied. """ assert qubit in self.active_qubits self.instruction.append(instruction.Z(target=qubit)) def rx(self, qubit: int, angle: Angle) -> None: - """Apply an X rotation gate. + """ + Apply an X rotation gate. Parameters ---------- qubit : int - target qubit + The target qubit. angle : Angle - rotation angle in radian + The rotation angle in radians. """ assert qubit in self.active_qubits self.instruction.append(instruction.RX(target=qubit, angle=angle)) def ry(self, qubit: int, angle: Angle) -> None: - """Apply a Y rotation gate. + """ + Apply a Y rotation gate. Parameters ---------- qubit : int - target qubit + The index of the target qubit. angle : Angle - angle in radian + The rotation angle in radians. + + Returns + ------- + None """ assert qubit in self.active_qubits self.instruction.append(instruction.RY(target=qubit, angle=angle)) def rz(self, qubit: int, angle: Angle) -> None: - """Apply a Z rotation gate. + """ + Apply a Z rotation gate. Parameters ---------- qubit : int - target qubit + Target qubit. angle : Angle - rotation angle in radian + Rotation angle in radians. + + Notes + ----- + The Z rotation gate applies a phase shift to the state of the target qubit, + which is represented by the specified angle in radians. The operation can be + expressed mathematically as: + + .. math:: R_z(\theta) = e^{-i\theta/2} |0\rangle\langle0| + e^{i\theta/2} |1\rangle\langle1| """ assert qubit in self.active_qubits self.instruction.append(instruction.RZ(target=qubit, angle=angle)) def rzz(self, control: int, target: int, angle: Angle) -> None: - r"""Apply a ZZ-rotation gate. + """ + Apply a ZZ-rotation gate. - Equivalent to the sequence - CNOT(control, target), - Rz(target, angle), - CNOT(control, target) + The ZZ-rotation gate is equivalent to the following sequence: + 1. CNOT(control, target), + 2. Rz(target, angle), + 3. CNOT(control, target). - and realizes rotation expressed by + This gate realizes a rotation expressed by the equation: :math:`e^{-i \frac{\theta}{2} Z_c Z_t}`. Parameters ---------- control : int - control qubit + The index of the control qubit. target : int - target qubit + The index of the target qubit. angle : Angle - rotation angle in radian + The rotation angle in radians. + + Returns + ------- + None + This method does not return a value but applies the gate to the circuit. """ assert control in self.active_qubits assert target in self.active_qubits self.instruction.append(instruction.RZZ(control=control, target=target, angle=angle)) def ccx(self, control1: int, control2: int, target: int) -> None: - r"""Apply a CCX (Toffoli) gate. + """ + Apply a CCX (Toffoli) gate. - Prameters - --------- + Parameters + ---------- control1 : int - first control qubit + First control qubit. control2 : int - second control qubit + Second control qubit. target : int - target qubit + Target qubit. + + Returns + ------- + None """ assert control1 in self.active_qubits assert control2 in self.active_qubits @@ -313,38 +403,48 @@ def ccx(self, control1: int, control2: int, target: int) -> None: self.instruction.append(instruction.CCX(controls=(control1, control2), target=target)) def i(self, qubit: int) -> None: - """Apply an identity (teleportation) gate. + """ + Apply an identity (teleportation) gate to the specified qubit. Parameters ---------- qubit : int - target qubit + The index of the target qubit on which the identity gate will be applied. """ assert qubit in self.active_qubits self.instruction.append(instruction.I(target=qubit)) def m(self, qubit: int, plane: Plane, angle: Angle) -> None: - """Measure a quantum qubit. + """ + Measure a quantum qubit. The measured qubit cannot be used afterwards. Parameters ---------- qubit : int - target qubit + The index of the target qubit to be measured. plane : Plane + The measurement plane in which the qubit is to be measured. angle : Angle + The angle of measurement with respect to the chosen plane. + + Returns + ------- + None """ assert qubit in self.active_qubits self.instruction.append(instruction.M(target=qubit, plane=plane, angle=angle)) self.active_qubits.remove(qubit) def transpile(self) -> TranspileResult: - """Transpile the circuit to a pattern. + """ + Transpile the circuit to a specific pattern. Returns ------- - result : :class:`TranspileResult` object + result : TranspileResult + An object containing the results of the transpilation process. """ n_node = self.width out: list[int | None] = list(range(self.width)) @@ -449,25 +549,28 @@ def transpile(self) -> TranspileResult: def _cnot_command( cls, control_node: int, target_node: int, ancilla: Sequence[int] ) -> tuple[int, int, list[command.Command]]: - """MBQC commands for CNOT gate. + """ + Generate MBQC commands for the CNOT gate. Parameters ---------- control_node : int - control node on graph - target : int - target node on graph - ancilla : list of two ints - ancilla node indices to be added to graph + Index of the control node on the graph. + target_node : int + Index of the target node on the graph. + ancilla : Sequence[int] + Indices of the ancilla nodes to be added to the graph. Returns ------- - control_out : int - control node on graph after the gate - target_out : int - target node on graph after the gate - commands : list - list of MBQC commands + tuple[int, int, list[command.Command]] + A tuple containing: + - control_out : int + Index of the control node after the gate operation. + - target_out : int + Index of the target node after the gate operation. + - commands : list[command.Command] + List of MBQC commands generated for the operation. """ assert len(ancilla) == 2 seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])] @@ -487,41 +590,43 @@ def _cnot_command( @classmethod def _m_command(cls, input_node: int, plane: Plane, angle: Angle) -> list[Command]: - """MBQC commands for measuring qubit. + """ + MBQC commands for measuring a qubit. Parameters ---------- input_node : int - target node on graph + Target node on the graph. plane : Plane - plane of the measure + Plane of the measurement. angle : Angle - angle of the measure (unit: pi radian) + Angle of the measurement (unit: π radians). Returns ------- - commands : list - list of MBQC commands + commands : list of Command + List of MBQC commands. """ return [M(node=input_node, plane=plane, angle=angle)] @classmethod def _h_command(cls, input_node: int, ancilla: int) -> tuple[int, list[Command]]: - """MBQC commands for Hadamard gate. + """ + MBQC commands for the Hadamard gate. Parameters ---------- input_node : int - target node on graph + The target node on the graph. ancilla : int - ancilla node index to be added + The index of the ancilla node to be added. Returns ------- out_node : int - control node on graph after the gate + The control node on the graph after the gate. commands : list - list of MBQC commands + A list of MBQC commands. """ seq: list[Command] = [N(node=ancilla)] seq.extend((E(nodes=(input_node, ancilla)), M(node=input_node), X(node=ancilla, domain={input_node}))) @@ -529,21 +634,22 @@ def _h_command(cls, input_node: int, ancilla: int) -> tuple[int, list[Command]]: @classmethod def _s_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.Command]]: - """MBQC commands for S gate. + """ + Generates MBQC commands for the S gate. Parameters ---------- input_node : int - input node index - ancilla : list of two ints - ancilla node indices to be added to graph + Index of the input node. + ancilla : Sequence[int] + Indices of the ancilla nodes to be added to the graph. Returns ------- out_node : int - control node on graph after the gate - commands : list - list of MBQC commands + Index of the control node on the graph after the gate. + commands : list[command.Command] + List of MBQC commands generated for the S gate. """ assert len(ancilla) == 2 seq: list[Command] = [N(node=ancilla[0]), command.N(node=ancilla[1])] @@ -561,21 +667,22 @@ def _s_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[ @classmethod def _x_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.Command]]: - """MBQC commands for Pauli X gate. + """ + MBQC commands for the Pauli X gate. Parameters ---------- input_node : int - input node index - ancilla : list of two ints - ancilla node indices to be added to graph + Index of the input node. + ancilla : Sequence[int] + Sequence of two integers representing the indices of the ancilla nodes to be added to the graph. Returns ------- out_node : int - control node on graph after the gate - commands : list - list of MBQC commands + Index of the control node on the graph after the gate. + commands : list[command.Command] + List of MBQC commands. """ assert len(ancilla) == 2 seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])] @@ -593,21 +700,22 @@ def _x_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[ @classmethod def _y_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.Command]]: - """MBQC commands for Pauli Y gate. + """ + MBQC commands for the Pauli Y gate. Parameters ---------- input_node : int - input node index - ancilla : list of four ints - ancilla node indices to be added to graph + Index of the input node. + ancilla : Sequence[int] + Indices of the ancilla nodes to be added to the graph. Returns ------- out_node : int - control node on graph after the gate - commands : list - list of MBQC commands + Index of the control node on the graph after the gate. + commands : list[command.Command] + List of MBQC commands. """ assert len(ancilla) == 4 seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])] @@ -630,21 +738,22 @@ def _y_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[ @classmethod def _z_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[command.Command]]: - """MBQC commands for Pauli Z gate. + """ + MBQC commands for the Pauli Z gate. Parameters ---------- input_node : int - input node index - ancilla : list of two ints - ancilla node indices to be added to graph + Index of the input node. + ancilla : Sequence[int] + Indices of the ancilla nodes to be added to the graph. Returns ------- out_node : int - control node on graph after the gate + Index of the control node on the graph after the gate. commands : list - list of MBQC commands + List of MBQC commands. """ assert len(ancilla) == 2 seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])] @@ -662,23 +771,24 @@ def _z_command(cls, input_node: int, ancilla: Sequence[int]) -> tuple[int, list[ @classmethod def _rx_command(cls, input_node: int, ancilla: Sequence[int], angle: Angle) -> tuple[int, list[command.Command]]: - """MBQC commands for X rotation gate. + """ + MBQC commands for X rotation gate. Parameters ---------- input_node : int - input node index - ancilla : list of two ints - ancilla node indices to be added to graph + Index of the input node. + ancilla : Sequence[int] + Indices of the ancilla nodes to be added to the graph. angle : Angle - measurement angle in radian + Measurement angle in radians. Returns ------- out_node : int - control node on graph after the gate - commands : list - list of MBQC commands + Control node on the graph after the gate. + commands : list[command.Command] + List of MBQC commands. """ assert len(ancilla) == 2 seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])] @@ -696,23 +806,24 @@ def _rx_command(cls, input_node: int, ancilla: Sequence[int], angle: Angle) -> t @classmethod def _ry_command(cls, input_node: int, ancilla: Sequence[int], angle: Angle) -> tuple[int, list[command.Command]]: - """MBQC commands for Y rotation gate. + """ + MBQC commands for the Y rotation gate. Parameters ---------- input_node : int - input node index - ancilla : list of four ints - ancilla node indices to be added to graph + Index of the input node. + ancilla : Sequence[int] + Indices of the ancilla nodes to be added to the graph. angle : Angle - rotation angle in radian + Rotation angle in radians. Returns ------- out_node : int - control node on graph after the gate - commands : list - list of MBQC commands + Control node on the graph after the gate operation. + commands : list[command.Command] + List of MBQC commands generated for the Y rotation. """ assert len(ancilla) == 4 seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])] @@ -735,23 +846,24 @@ def _ry_command(cls, input_node: int, ancilla: Sequence[int], angle: Angle) -> t @classmethod def _rz_command(cls, input_node: int, ancilla: Sequence[int], angle: Angle) -> tuple[int, list[command.Command]]: - """MBQC commands for Z rotation gate. + """ + MBQC commands for the Z rotation gate. Parameters ---------- input_node : int - input node index - ancilla : list of two ints - ancilla node indices to be added to graph + Index of the input node. + ancilla : Sequence[int] + Indices of the ancilla nodes to be added to the graph. angle : Angle - measurement angle in radian + Measurement angle in radians. Returns ------- out_node : int - node on graph after the gate + Index of the node on the graph after the gate. commands : list - list of MBQC commands + List of MBQC commands. """ assert len(ancilla) == 2 seq: list[Command] = [N(node=ancilla[0]), N(node=ancilla[1])] # assign new qubit labels @@ -775,29 +887,32 @@ def _ccx_command( target_node: int, ancilla: Sequence[int], ) -> tuple[int, int, int, list[command.Command]]: - """MBQC commands for CCX gate. + """ + MBQC commands for the CCX gate. Parameters ---------- control_node1 : int - first control node on graph + First control node on the graph. control_node2 : int - second control node on graph + Second control node on the graph. target_node : int - target node on graph - ancilla : list of int - ancilla node indices to be added to graph + Target node on the graph. + ancilla : Sequence[int] + Ancilla node indices to be added to the graph. Returns ------- - control_out1 : int - first control node on graph after the gate - control_out2 : int - second control node on graph after the gate - target_out : int - target node on graph after the gate - commands : list - list of MBQC commands + tuple + A tuple containing: + control_out1 : int + First control node on the graph after the gate. + control_out2 : int + Second control node on the graph after the gate. + target_out : int + Target node on the graph after the gate. + commands : list[command.Command] + List of MBQC commands. """ assert len(ancilla) == 18 seq: list[Command] = [N(node=ancilla[i]) for i in range(18)] # assign new qubit labels @@ -882,22 +997,23 @@ def simulate_statevector( branch_selector: BranchSelector | None = None, rng: Generator | None = None, ) -> SimulateResult: - """Run statevector simulation of the gate sequence. + """ + Run statevector simulation of the gate sequence. Parameters ---------- - input_state : Data - branch_selector: :class:`graphix.branch_selector.BranchSelector` - branch selector for measures (default: :class:`RandomBranchSelector`). - rng: Generator, optional - Random-number generator for measurements. - This generator is used only in case of random branch selection - (see :class:`RandomBranchSelector`). + input_state : Data | None + The initial state for the simulation. If None, a default state will be used. + branch_selector : BranchSelector | None + Branch selector for measurements (default is :class:`RandomBranchSelector`). + rng : Generator | None, optional + Random-number generator for measurements. This generator is used only in + case of random branch selection (see :class:`RandomBranchSelector`). Returns ------- - result : :class:`SimulateResult` - output state of the statevector simulation and results of classical measures. + result : SimulateResult + The output state of the statevector simulation and results of classical measurements. """ symbolic = self.is_parameterized() if branch_selector is None: @@ -952,7 +1068,19 @@ def simulate_statevector( return SimulateResult(state, tuple(classical_measures)) def map_angle(self, f: Callable[[Angle], Angle]) -> Circuit: - """Apply `f` to all angles that occur in the circuit.""" + """ + Apply a function to all angles in the circuit. + + Parameters + ---------- + f : Callable[[Angle], Angle] + A function that takes an Angle as input and returns an Angle. + + Returns + ------- + Circuit + A new Circuit instance with all angles transformed by the function `f`. + """ result = Circuit(self.width) for instr in self.instruction: # Use == for mypy @@ -971,13 +1099,18 @@ def map_angle(self, f: Callable[[Angle], Angle]) -> Circuit: def is_parameterized(self) -> bool: """ - Return `True` if there is at least one measurement angle that is not just an instance of `SupportsFloat`. + Determine if the circuit is parameterized. - A parameterized circuit is a circuit where at least one - measurement angle is an expression that is not a number, - typically an instance of `sympy.Expr` (but we don't force to - choose `sympy` here). + A circuit is considered parameterized if there is at least + one measurement angle that is not an instance of `SupportsFloat`. + This typically indicates the presence of a parameterized + expression, such as an instance of `sympy.Expr`, although + the use of `sympy` is not enforced. + Returns + ------- + bool + `True` if the circuit is parameterized, `False` otherwise. """ # Use of `==` here for mypy return any( @@ -991,23 +1124,64 @@ def is_parameterized(self) -> bool: ) def subs(self, variable: Parameter, substitute: ExpressionOrFloat) -> Circuit: - """Return a copy of the circuit where all occurrences of the given variable in measurement angles are substituted by the given value.""" + """ + Return a copy of the circuit with all occurrences of the specified variable + in measurement angles substituted with the given value. + + Parameters + ---------- + variable : Parameter + The variable to be substituted in the measurement angles. + substitute : ExpressionOrFloat + The value to replace the occurrences of the variable. + + Returns + ------- + Circuit + A new circuit with the substitutions applied. + + Notes + ----- + This method does not modify the original circuit and creates a copy + with the necessary substitutions. + """ return self.map_angle(lambda angle: parameter.subs(angle, variable, substitute)) def xreplace(self, assignment: Mapping[Parameter, ExpressionOrFloat]) -> Circuit: - """Return a copy of the circuit where all occurrences of the given keys in measurement angles are substituted by the given values in parallel.""" + """ + Return a copy of the circuit with all occurrences of the specified keys in measurement angles substituted by the provided values. + + Parameters + ---------- + assignment : Mapping[Parameter, ExpressionOrFloat] + A mapping of parameters to their corresponding replacement values. + + Returns + ------- + Circuit + A new circuit with the specified substitutions made in the measurement angles. + + Notes + ----- + This method performs the substitutions in parallel, ensuring that all occurrences are replaced throughout the circuit. + """ return self.map_angle(lambda angle: parameter.xreplace(angle, assignment)) def _extend_domain(measure: M, domain: set[int]) -> None: - """Extend the correction domain of ``measure`` by ``domain``. + """ + Extend the correction domain of `measure` by `domain`. Parameters ---------- measure : M Measurement command to modify. domain : set[int] - Set of nodes to XOR into the appropriate domain of ``measure``. + Set of nodes to XOR into the appropriate domain of `measure`. + + Returns + ------- + None """ if measure.plane == Plane.XY: measure.s_domain ^= domain diff --git a/graphix/utils.py b/graphix/utils.py index 267b5a8d9..c7bc4d33e 100644 --- a/graphix/utils.py +++ b/graphix/utils.py @@ -1,4 +1,19 @@ -"""Utilities.""" +""" +Utilities for various common operations and functions. + +This module contains general utility functions that can be used across +different parts of the application. It provides tools for string manipulation, +data processing, and other helpful operations. + +Functions: +---------- +- function_name_1(arg1, arg2): Brief description of what function does. +- function_name_2(arg1): Brief description of what function does. + +Examples: +--------- +Examples of how to use the utilities can be added here. +""" from __future__ import annotations @@ -23,14 +38,48 @@ def check_list_elements(l: Iterable[_T], ty: type[_T]) -> None: - """Check that every element of the list has the given type.""" + """ + Check that every element of the iterable has the given type. + + Parameters + ---------- + l : Iterable[_T] + The iterable to check. + ty : type[_T] + The type that each element in the iterable is expected to have. + + Raises + ------ + TypeError + If any element in the iterable is not of the specified type. + + Notes + ----- + This function checks each element in the provided iterable `l`. + If an element does not match the expected type `ty`, a TypeError + is raised with a message indicating the incorrect element and its type. + """ for index, item in enumerate(l): if not isinstance(item, ty): raise TypeError(f"data[{index}] has type {type(item)} whereas {ty} is expected") def check_kind(cls: type, scope: dict[str, Any]) -> None: - """Check that the class has a kind attribute.""" + """ + Check that the class has a 'kind' attribute. + + Parameters + ---------- + cls : type + The class to check for the 'kind' attribute. + scope : dict[str, Any] + A dictionary representing the scope in which the class is defined. + + Raises + ------ + AttributeError + If the class does not have a 'kind' attribute. + """ if not hasattr(cls, "kind"): msg = f"{cls.__name__} must have a tag attribute named kind." raise TypeError(msg) @@ -53,7 +102,19 @@ def check_kind(cls: type, scope: dict[str, Any]) -> None: def is_integer(value: SupportsInt) -> bool: - """Return `True` if `value` is an integer, `False` otherwise.""" + """ + Determine if a given value is an integer. + + Parameters + ---------- + value : SupportsInt + The value to be checked. + + Returns + ------- + bool + `True` if the value is an integer, `False` otherwise. + """ return value == int(value) @@ -69,9 +130,22 @@ def lock(data: npt.NDArray[Any], dtype: type[G]) -> npt.NDArray[G]: ... def lock(data: npt.NDArray[Any], dtype: type = np.complex128) -> npt.NDArray[Any]: - """Create a true immutable view. + """ + Create a true immutable view of the given data. + + Parameters + ---------- + data : numpy.ndarray + The input array data. It must not have aliasing references; otherwise, users can still + enable the writable flag on the array, which would violate the immutability. + + dtype : type, optional + The desired data type for the resulting immutable view. Default is `np.complex128`. - data must not have aliasing references, otherwise users can still turn on writeable flag of m. + Returns + ------- + numpy.ndarray + An immutable view of the input data array. """ m: npt.NDArray[Any] = data.astype(dtype) m.flags.writeable = False @@ -81,7 +155,18 @@ def lock(data: npt.NDArray[Any], dtype: type = np.complex128) -> npt.NDArray[Any def iter_empty(it: Iterator[_T]) -> bool: - """Check if an iterable is empty. + """ + Check if an iterable is empty. + + Parameters + ---------- + it : Iterator[_T] + An iterator to be checked for emptiness. + + Returns + ------- + bool + True if the iterator is empty, False otherwise. Notes ----- @@ -94,13 +179,31 @@ def iter_empty(it: Iterator[_T]) -> bool: class Validator(ABC, Generic[_ValueT]): - """Descriptor to validate value. + """ + Descriptor to validate values. + + This descriptor is designed to enforce specific validation rules on a + value before it is set to an attribute of a class. + For more information on descriptors, see the Python documentation at: https://docs.python.org/3/howto/descriptor.html#custom-validators """ def __set_name__(self, owner: object, name: str) -> None: - """Set private field name.""" + """ + Set the name of the attribute being managed. + + This method is called when the owning class is being defined, + allowing the descriptor to set its name and maintain reference + to the owner class. + + Parameters + ---------- + owner : object + The class that owns this descriptor. + name : str + The name of the attribute in the owner class. + """ self.private_name = "_" + name @overload @@ -112,27 +215,101 @@ def __get__(self, obj: object, objtype: type | None = None) -> _ValueT: # acces ... def __get__(self, obj: object, objtype: object = None) -> _ValueT | Self: - """Get the validated value from the private field.""" + """ + Retrieve the validated value. + + This method is called to get the validated value from the private + field of the Validator. It should typically be used in the context + of a descriptor. + + Parameters + ---------- + obj : object + The instance from which the value is being retrieved. + objtype : object, optional + The type of the object (class of the instance) if applicable. + + Returns + ------- + _ValueT | Self + The validated value from the private field or the Validator instance itself. + """ if obj is None: # access on the class, not an instance return self result: _ValueT = getattr(obj, self.private_name) return result def __set__(self, obj: object, value: _ValueT) -> None: - """Validate and set the value in the private field.""" + """ + Validate and set the value in the private field. + + Parameters + ---------- + obj : object + The instance of the class where the value will be set. + value : _ValueT + The value to be validated and assigned to the private field. + + Raises + ------ + ValueError + If the value does not meet the validation criteria. + """ self.validate(value) setattr(obj, self.private_name, value) @abstractmethod def validate(self, value: _ValueT) -> None: - """Validate the assigned value.""" + """ + Validate the assigned value. + + Parameters + ---------- + value : _ValueT + The value to be validated. + + Raises + ------ + NotImplementedError + If the method is not implemented by the subclass. + + Notes + ----- + This is an abstract method that must be implemented by subclasses + to provide specific validation logic. + """ @dataclass class BoundedFloat(Validator[float]): - """Descriptor to validate numbers with given bounds. - - https://docs.python.org/3/howto/descriptor.html#custom-validators + """ + Descriptor to validate floating-point numbers within given bounds. + + This class is used to ensure that a floating-point value falls within a specified + minimum and maximum range. It raises a ValueError if the assigned value is not + within the defined bounds. + + Parameters + ---------- + min_value : float + The minimum allowable value for the float. + max_value : float + The maximum allowable value for the float. + + Raises + ------ + ValueError + If the assigned value is less than min_value or greater than max_value. + + Examples + -------- + >>> class MyClass: + ... my_float = BoundedFloat(0.0, 10.0) + ... + >>> obj = MyClass() + >>> obj.my_float = 5.0 # Valid + >>> obj.my_float = -1.0 # Raises ValueError + >>> obj.my_float = 11.0 # Raises ValueError """ minvalue: float | None = None @@ -140,7 +317,29 @@ class BoundedFloat(Validator[float]): @override def validate(self, value: float) -> None: - """Validate the assigned value.""" + """ + Validate the assigned value. + + Parameters + ---------- + value : float + The value to be validated. + + Returns + ------- + None + + Raises + ------ + ValueError + If the value is outside the allowed bounds. + + Notes + ----- + This method checks if the provided value falls within the defined bounds + of the BoundedFloat instance. If the value is valid, the method completes + without raising an error; otherwise, it raises a ValueError. + """ if self.minvalue is not None and value < self.minvalue: raise ValueError(f"Expected {value!r} to be at least {self.minvalue!r}") if self.maxvalue is not None and value > self.maxvalue: @@ -148,7 +347,35 @@ def validate(self, value: float) -> None: class Probability(BoundedFloat): - """Descriptor for probability (between 0 and 1).""" + """ + Descriptor for probability value, constrained between 0 and 1. + + This class ensures that any value assigned to it is a valid + probability, meaning it must be within the range [0, 1]. If + an attempted assignment is outside this range, a ValueError + will be raised. + + Parameters + ---------- + value : float + The initial value of the probability. Must be between 0 and 1. + + Methods + ------- + __set__(instance, value) + Sets the value of the probability, ensuring it is within the valid range. + + __get__(instance, owner) + Gets the value of the probability. + + __delete__(instance) + Deletes the probability value from the instance. + + Raises + ------ + ValueError + If the assigned value is not between 0 and 1. + """ def __init__(self) -> None: super().__init__(minvalue=0, maxvalue=1) diff --git a/graphix/visualization.py b/graphix/visualization.py index 3736ec86b..2e923db5c 100644 --- a/graphix/visualization.py +++ b/graphix/visualization.py @@ -1,4 +1,4 @@ -"""Functions to visualize the resource state of MBQC pattern.""" +"Functions to visualize the resource state of the Measurement-based Quantum Computation (MBQC) pattern." from __future__ import annotations @@ -34,23 +34,23 @@ class GraphVisualizer: - """A class for visualizing MBQC graphs with flow or gflow structure. + """ + A class for visualizing MBQC graphs with flow or gflow structure. Attributes ---------- - g : :class:`networkx.Graph` - The graph to be visualized + g : networkx.Graph + The graph to be visualized. v_in : list - list of input nodes + List of input nodes. v_out : list - list of output nodes + List of output nodes. meas_planes : dict - dict specifying the measurement planes for each node, except output nodes. + Dictionary specifying the measurement planes for each node, except output nodes. meas_angles : dict - dict specifying the measurement angles for each node, except output nodes. + Dictionary specifying the measurement angles for each node, except output nodes. local_clifford : dict - dict specifying the local clifford for each node. - + Dictionary specifying the local Clifford for each node. """ def __init__( @@ -67,19 +67,21 @@ def __init__( Parameters ---------- - g : :class:`networkx.Graph` - NetworkX graph instance - v_in : list - list of input nodes - v_out : list - list of output nodes - meas_plane : dict - dict specifying the measurement planes for each node, except output nodes. - if None, all measurements are assumed to be in XY-plane. - meas_angles : dict - dict specifying the measurement angles for each node, except output nodes. - local_clifford : dict - dict specifying the local clifford for each node. + g : networkx.Graph[int] + NetworkX graph instance. + v_in : Collection[int] + Collection of input nodes. + v_out : Collection[int] + Collection of output nodes. + meas_plane : Mapping[int, Plane] | None, optional + Mapping specifying the measurement planes for each node, except output nodes. + If None, all measurements are assumed to be in the XY-plane. Default is None. + meas_angles : Mapping[int, ExpressionOrFloat] | None, optional + Mapping specifying the measurement angles for each node, except output nodes. + Default is None. + local_clifford : Mapping[int, Clifford] | None, optional + Mapping specifying the local Clifford transformations for each node. + Default is None. """ self.graph = g self.v_in = v_in @@ -104,28 +106,43 @@ def visualize( """ Visualize the graph with flow or gflow structure. - If there exists a flow structure, then the graph is visualized with the flow structure. - If flow structure is not found and there exists a gflow structure, then the graph is visualized - with the gflow structure. - If neither flow nor gflow structure is found, then the graph is visualized without any structure. + If a flow structure exists, the graph is visualized using this structure. + If no flow structure is found but a gflow structure exists, the graph is visualized using the gflow structure. + If neither structure is found, the graph is visualized without any structure. Parameters ---------- - show_pauli_measurement : bool + show_pauli_measurement : bool, optional If True, the nodes with Pauli measurement angles are colored light blue. - show_local_clifford : bool + Default is True. + + show_local_clifford : bool, optional If True, indexes of the local Clifford operator are displayed adjacent to the nodes. - show_measurement_planes : bool + Default is False. + + show_measurement_planes : bool, optional If True, the measurement planes are displayed adjacent to the nodes. - show_loop : bool - whether or not to show loops for graphs with gflow. defaulted to True. - node_distance : tuple + Default is False. + + show_loop : bool, optional + Whether or not to show loops for graphs with gflow. + Default is True. + + node_distance : tuple[float, float], optional Distance multiplication factor between nodes for x and y directions. - figsize : tuple - Figure size of the plot. - filename : Path | None - If not None, filename of the png file to save the plot. If None, the plot is not saved. - Default in None. + Default is (1, 1). + + figsize : tuple[int, int] | None, optional + Figure size of the plot. If None, the default size will be used. + Default is None. + + filename : Path | None, optional + If not None, specifies the filename of the PNG file to save the plot. + If None, the plot is not saved. Default is None. + + Returns + ------- + None """ f, l_k = gflow.find_flow(self.graph, set(self.v_in), set(self.v_out), meas_planes=self.meas_planes) # try flow if f is not None and l_k is not None: @@ -181,31 +198,35 @@ def visualize_from_pattern( filename: Path | None = None, ) -> None: """ - Visualize the graph with flow or gflow structure found from the given pattern. + Visualize the graph with flow or gflow structure derived from the specified pattern. - If pattern sequence is consistent with flow structure, then the graph is visualized with the flow structure. - If it is not consistent with flow structure and consistent with gflow structure, then the graph is visualized - with the gflow structure. If neither flow nor gflow structure is found, then the graph is visualized with all correction flows. + The visualization process is dependent on the consistency of the pattern sequence: + - If the pattern sequence is consistent with a flow structure, the graph is visualized accordingly. + - If it is not consistent with a flow structure but is consistent with a gflow structure, the graph is visualized in that way. + - If neither flow nor gflow structure is detected, the graph is visualized with all correction flows. Parameters ---------- pattern : Pattern - pattern to be visualized - show_pauli_measurement : bool - If True, the nodes with Pauli measurement angles are colored light blue. - show_local_clifford : bool - If True, indexes of the local Clifford operator are displayed adjacent to the nodes. - show_measurement_planes : bool - If True, the measurement planes are displayed adjacent to the nodes. - show_loop : bool - whether or not to show loops for graphs with gflow. defaulted to True. - node_distance : tuple - Distance multiplication factor between nodes for x and y directions. - figsize : tuple - Figure size of the plot. - filename : Path | None - If not None, filename of the png file to save the plot. If None, the plot is not saved. - Default in None. + The pattern to be visualized. + show_pauli_measurement : bool, optional + If True, nodes with Pauli measurement angles are colored light blue. Default is True. + show_local_clifford : bool, optional + If True, indexes of the local Clifford operator are displayed adjacent to the nodes. Default is False. + show_measurement_planes : bool, optional + If True, the measurement planes are displayed adjacent to the nodes. Default is False. + show_loop : bool, optional + Whether or not to show loops for graphs with gflow. Default is True. + node_distance : tuple of float, optional + Distance multiplication factors between nodes in the x and y directions. Default is (1, 1). + figsize : tuple of int or None, optional + Size of the figure for the plot. If None, defaults to a standard size. + filename : Path or None, optional + If specified, the filename of the png file to save the plot. If None, the plot is not saved. Default is None. + + Returns + ------- + None """ f, l_k = gflow.flow_from_pattern(pattern) # try flow if f is not None and l_k is not None: @@ -267,7 +288,19 @@ def get_paths( @staticmethod def _shorten_path(path: Sequence[_Point]) -> list[_Point]: - """Shorten the last edge not to hide arrow under the node.""" + """ + Shorten the last edge to prevent hiding the arrow under the node. + + Parameters + ---------- + path : Sequence[_Point] + A sequence of points representing the path to be shortened. + + Returns + ------- + list[_Point] + A list of points representing the shortened path. + """ new_path = list(path) last = np.array(new_path[-1]) second_last = np.array(new_path[-2]) @@ -288,9 +321,13 @@ def __draw_nodes_role(self, pos: Mapping[int, _Point], show_pauli_measurement: b Parameters ---------- pos : Mapping[int, tuple[float, float]] - dictionary of node positions. - show_pauli_measurement : bool - If True, the nodes with Pauli measurement angles are colored light blue. + Dictionary mapping node indices to their positions in 2D space. + show_pauli_measurement : bool, optional + If True, nodes associated with Pauli measurement angles are colored light blue. Default is False. + + Returns + ------- + None """ for node in self.graph.nodes(): color = "black" # default color for 'other' nodes @@ -336,35 +373,40 @@ def visualize_graph( Nodes are colored based on their role (input, output, or other) and edges are depicted as arrows or dashed lines depending on whether they are in the flow mapping. Vertical dashed lines separate different layers of the graph. This function does not return anything but plots the graph - using matplotlib's pyplot. + using Matplotlib's pyplot. Parameters ---------- - pos: Mapping[int, _Point] + pos : Mapping[int, _Point] Node positions. - get_paths: Callable[ - [Mapping[int, _Point]], tuple[Mapping[_Edge, Sequence[_Point]], Mapping[_Edge, Sequence[_Point]] | None] + get_paths : Callable[ + [Mapping[int, _Point]], + tuple[Mapping[_Edge, Sequence[_Point]], Mapping[_Edge, Sequence[_Point]] | None] ] - Given scaled node positions, return the mapping of edge paths and the mapping of arrow paths. - l_k: Mapping[int, int] | None - Layer mapping if any. - corrections: tuple[Mapping[int, AbstractSet[int]], Mapping[int, AbstractSet[int]]] | None - X and Z corrections if any. - show_pauli_measurement : bool - If True, the nodes with Pauli measurement angles are colored light blue. - show_local_clifford : bool - If True, indexes of the local Clifford operator are displayed adjacent to the nodes. - show_measurement_planes : bool - If True, the measurement planes are displayed adjacent to the nodes. - show_loop : bool - whether or not to show loops for graphs with gflow. defaulted to True. - node_distance : tuple - Distance multiplication factor between nodes for x and y directions. - figsize : tuple - Figure size of the plot. - filename : Path | None - If not None, filename of the png file to save the plot. If None, the plot is not saved. - Default in None. + Given scaled node positions, returns the mapping of edge paths and the mapping of arrow paths. + l_k : Mapping[int, int] | None + Layer mapping, if any. + corrections : tuple[Mapping[int, AbstractSet[int]], Mapping[int, AbstractSet[int]]] | None + X and Z corrections, if any. + show_pauli_measurement : bool, optional + If True, the nodes with Pauli measurement angles are colored light blue. Default is True. + show_local_clifford : bool, optional + If True, indexes of the local Clifford operator are displayed adjacent to the nodes. Default is False. + show_measurement_planes : bool, optional + If True, the measurement planes are displayed adjacent to the nodes. Default is False. + show_loop : bool, optional + Whether or not to show loops for graphs with gflow. Default is True. + node_distance : tuple[float, float], optional + Distance multiplication factor between nodes for x and y directions. Default is (1, 1). + figsize : _Point | None, optional + Figure size of the plot. If None, the default size is used. Default is None. + filename : Path | None, optional + If not None, the filename of the PNG file to save the plot. If None, the plot is not saved. + Default is None. + + Returns + ------- + None """ if figsize is None: figsize = self.get_figsize(l_k, pos, node_distance=node_distance) @@ -489,17 +531,17 @@ def get_figsize( Parameters ---------- - l_k : dict - Layer mapping. - pos : dict - dictionary of node positions. - node_distance : tuple - Distance multiplication factor between nodes for x and y directions. + l_k : Mapping[int, int] | None + Layer mapping. If None, the function may use default values. + pos : Mapping[int, _Point] | None, optional + Dictionary of node positions. If None, the function may compute positions based on the layer mapping. + node_distance : tuple[float, float], optional + Distance multiplication factor between nodes for the x and y directions. Default is (1, 1). Returns ------- - figsize : tuple - figure size of the graph. + _Point + The figure size of the graph, represented as a _Point indicating width and height. """ if l_k is None: if pos is None: @@ -514,21 +556,23 @@ def get_edge_path( self, flow: Mapping[int, int | set[int]], pos: Mapping[int, _Point] ) -> tuple[dict[_Edge, list[_Point]], dict[_Edge, list[_Point]]]: """ - Return the path of edges and gflow arrows. + Return the path of edges and flow arrows. Parameters ---------- - flow : dict - flow mapping (including gflow or any correction flow) - pos : dict - dictionary of node positions. + flow : Mapping[int, int | set[int]] + A mapping representing the flow, which can include gflow or any correction flow. + pos : Mapping[int, _Point] + A dictionary mapping nodes to their positions. Returns ------- - edge_path : dict - dictionary of edge paths. - arrow_path : dict - dictionary of arrow paths. + tuple[dict[_Edge, list[_Point]], dict[_Edge, list[_Point]]] + A tuple containing two dictionaries: + - edge_path : dict + A dictionary where keys are edges and values are lists of points representing the paths of the edges. + - arrow_path : dict + A dictionary where keys are edges and values are lists of points representing the paths of the flow arrows. """ edge_path = self.get_edge_path_wo_structure(pos) edge_set = set(self.graph.edges()) @@ -539,22 +583,22 @@ def get_edge_path( if arrow[0] == arrow[1]: # Self loop def _point_from_node(pos: Sequence[float], dist: float, angle: float) -> _Point: - """Return a point at a given distance and angle from ``pos``. + """ + Return a point at a given distance and angle from a specified position. Parameters ---------- pos : Sequence[float] - Coordinate of the node. + Coordinate of the node, typically in the form [x, y]. dist : float - Distance from ``pos``. + Distance from the specified position. angle : float - Angle in degrees measured counter-clockwise from the - positive x-axis. + Angle in degrees measured counter-clockwise from the positive x-axis. Returns ------- _Point - The new ``[x, y]`` coordinate. + The new coordinates as a point represented by [x, y]. """ angle = np.deg2rad(angle) return (pos[0] + dist * np.cos(angle), pos[1] + dist * np.sin(angle)) @@ -618,17 +662,18 @@ def _find_bezier_path(self, arrow: _Edge, bezier_path: Iterable[_Point], pos: Ma def get_edge_path_wo_structure(self, pos: Mapping[int, _Point]) -> dict[_Edge, list[_Point]]: """ - Return the path of edges. + Return the paths of edges in the graph. Parameters ---------- - pos : dict - dictionary of node positions. + pos : Mapping[int, _Point] + A mapping of node identifiers to their respective positions (points). Returns ------- - edge_path : dict - dictionary of edge paths. + edge_path : dict[_Edge, list[_Point]] + A dictionary where each key is an edge and the corresponding value is + a list of points representing the path of that edge. """ return {edge: self._find_bezier_path(edge, [pos[edge[0]], pos[edge[1]]], pos) for edge in self.graph.edges()} @@ -638,15 +683,18 @@ def get_pos_from_flow(self, f: Mapping[int, set[int]], l_k: Mapping[int, int]) - Parameters ---------- - f : dict - flow mapping. - l_k : dict - Layer mapping. + f : Mapping[int, set[int]] + A mapping of node flow, where keys are node identifiers + and values are sets of adjacent node identifiers. + l_k : Mapping[int, int] + A mapping of layer indices, where keys are node identifiers + and values are their corresponding layer indices. Returns ------- - pos : dict - dictionary of node positions. + pos : dict[int, _Point] + A dictionary mapping node identifiers to their positions + as points in a coordinate system. """ values_union = set().union(*f.values()) start_nodes = set(self.graph.nodes()) - values_union @@ -670,15 +718,15 @@ def get_pos_from_gflow(self, g: Mapping[int, set[int]], l_k: Mapping[int, int]) Parameters ---------- - g : dict - gflow mapping. - l_k : dict - Layer mapping. + g : Mapping[int, set[int]] + Mapping representing the gflow, where keys are node indices and values are sets of connected nodes. + l_k : Mapping[int, int] + Mapping of nodes to their corresponding layers. Returns ------- - pos : dict - dictionary of node positions. + pos : dict[int, _Point] + Dictionary mapping node indices to their respective positions in the graph. """ g_edges: list[_Edge] = [] @@ -712,13 +760,8 @@ def get_pos_wo_structure(self) -> dict[int, _Point]: Returns ------- - pos : dict - dictionary of node positions. - - Returns - ------- - pos : dict - dictionary of node positions. + pos : dict[int, _Point] + Dictionary mapping node identifiers to their respective positions. """ layers: dict[int, int] = {} connected_components = list(nx.connected_components(self.graph)) @@ -803,13 +846,13 @@ def get_pos_all_correction(self, layers: Mapping[int, int]) -> dict[int, _Point] Parameters ---------- - layers : dict + layers : Mapping[int, int] Layer mapping obtained from the measurement order of the pattern. Returns ------- - pos : dict - dictionary of node positions. + dict[int, _Point] + Dictionary of node positions. """ g_prime = self.graph.copy() g_prime.add_nodes_from(self.graph.nodes()) @@ -827,7 +870,26 @@ def _edge_intersects_node( node_pos: _Point, buffer: float = 0.2, ) -> bool: - """Determine if an edge intersects a node.""" + """ + Determine if an edge intersects a node. + + Parameters + ---------- + start : _Point + The starting point of the edge defined by the two points. + end : _Point + The ending point of the edge defined by the two points. + node_pos : _Point + The position of the node to check for intersection with the edge. + buffer : float, optional + The distance around the node to consider for intersection. Default is 0.2. + + Returns + ------- + bool + True if the edge intersects the node (considering the buffer), + False otherwise. + """ start_array = np.array(start) end_array = np.array(end) if np.all(start_array == end_array): @@ -854,7 +916,25 @@ def _control_point( node_pos: _Point, distance: float = 0.6, ) -> _Point: - """Generate a control point to bend the edge around a node.""" + """ + Generate a control point to bend the edge around a node. + + Parameters + ---------- + start : _Point + The starting point of the edge. + end : _Point + The ending point of the edge. + node_pos : _Point + The position of the node around which the edge needs to bend. + distance : float, optional + The distance from the node to place the control point, by default 0.6. + + Returns + ------- + _Point + The generated control point for the edge. + """ node_pos_array = np.array(node_pos) edge_vector = np.asarray(end, dtype=np.float64) - np.asarray(start, dtype=np.float64) # Rotate the edge vector 90 degrees or -90 degrees according to the node position @@ -869,7 +949,28 @@ def _control_point( @staticmethod def _bezier_curve(bezier_path: Sequence[_Point], t: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]: - """Generate a bezier curve from a list of points.""" + """ + Generate a Bezier curve from a list of control points. + + Parameters + ---------- + bezier_path : Sequence[_Point] + A sequence of control points defining the Bezier curve. Each point is expected + to be of type `_Point`. + t : npt.NDArray[np.float64] + A 1-dimensional array of parameter values ranging from 0 to 1, representing the + points along the Bezier curve to calculate. + + Returns + ------- + npt.NDArray[np.float64] + A 2-dimensional array containing the points on the Bezier curve corresponding + to the input parameter values from `t`. + + Notes + ----- + The method computes the Bezier curve using the De Casteljau's algorithm. + """ n = len(bezier_path) - 1 # order of the curve curve = np.zeros((len(t), 2)) for i, point in enumerate(bezier_path): @@ -883,7 +984,25 @@ def _bezier_curve_linspace(bezier_path: Sequence[_Point]) -> npt.NDArray[np.floa @staticmethod def _check_path(path: Iterable[_Point], target_node_pos: _Point | None = None) -> list[_Point]: - """If there is an acute angle in the path, merge points.""" + """ + Check the validity of a given path and merge points if an acute angle is detected. + + Parameters + ---------- + path : iterable of _Point + An iterable collection of points representing the path to be checked. + target_node_pos : _Point or None, optional + The target position of the node, used for additional checks. Default is None. + + Returns + ------- + list of _Point + A list of points representing the modified path after merging points with acute angles, if any. + + Notes + ----- + An acute angle is defined as an angle less than 90 degrees formed between three consecutive points in the path. + """ path = np.array(path) acute = True max_iter = 100 diff --git a/tests/test_branch_selector.py b/tests/test_branch_selector.py index 215c581c7..1f244d274 100644 --- a/tests/test_branch_selector.py +++ b/tests/test_branch_selector.py @@ -26,13 +26,57 @@ @dataclass class CheckedBranchSelector(RandomBranchSelector): - """Random branch selector that verifies that expectation values match the expected ones.""" + """ + Random branch selector that verifies that expectation values match the expected ones. + + This class is responsible for selecting branches in a random manner while ensuring that + the selected branches' expectation values adhere to specified criteria. It performs + checks to validate these expectation values against pre-defined expectations to ensure + correctness in the branch selection process. + + Parameters + ---------- + expected_values : dict + A dictionary mapping branch identifiers to their expected expectation values. + random_state : int or None, optional + Seed for the random number generator. If None, the random generator is not seeded. + Default is None. + + Attributes + ---------- + branches : list + List of available branches to choose from. + selected_branch : object + The branch that was last selected by the selector. + + Methods + ------- + select_branch(): + Select a branch at random and check its expectation value against the expected values. + """ expected: Mapping[int, float] = dataclasses.field(default_factory=dict) @override def measure(self, qubit: int, f_expectation0: Callable[[], float], rng: Generator | None = None) -> Outcome: - """Return the measurement outcome of ``qubit``.""" + """ + Measure the specified qubit and return the measurement outcome. + + Parameters + ---------- + qubit : int + The index of the qubit to be measured. + f_expectation0 : Callable[[], float] + A callable that returns the expectation value for the measurement when the qubit is in the |0⟩ state. + rng : Generator | None, optional + An optional random number generator to control randomness in the measurement process. + If None, a default generator will be used. + + Returns + ------- + Outcome + The measurement outcome of the specified qubit. + """ expectation0 = f_expectation0() assert math.isclose(expectation0, self.expected[qubit]) return super().measure(qubit, lambda: expectation0) diff --git a/tests/test_clifford.py b/tests/test_clifford.py index f477cc9a3..3d2adc455 100644 --- a/tests/test_clifford.py +++ b/tests/test_clifford.py @@ -39,7 +39,12 @@ def test_named(self) -> None: assert hasattr(Clifford, "H") def test_iteration(self) -> None: - """Test that Clifford iteration does not take (I, X, Y, Z, S, H) into account.""" + """ + Test the behavior of Clifford iteration. + + This test verifies that the Clifford iteration does not take the gate + operators: I, X, Y, Z, S, and H into account during its execution. + """ assert len(Clifford) == 24 assert len(frozenset(Clifford)) == 24 diff --git a/tests/test_density_matrix.py b/tests/test_density_matrix.py index 2965165ad..3c2006781 100644 --- a/tests/test_density_matrix.py +++ b/tests/test_density_matrix.py @@ -37,7 +37,33 @@ def _randdm_raw(nqubits: int, rng: Generator) -> npt.NDArray[np.complex128]: class TestDensityMatrix: - """Test for DensityMatrix class.""" + """ + Test for the DensityMatrix class. + + This class contains unit tests to verify the functionality and correctness + of the DensityMatrix class. Each test ensures various aspects of the + DensityMatrix implementation adhere to expected outcomes. + + Methods + ------- + test_initialization() + Tests the initialization of the DensityMatrix class with valid and invalid inputs. + + test_density_matrix_properties() + Verifies the properties of a valid density matrix, including Hermitian and trace-one conditions. + + test_addition() + Tests the addition operation for density matrices, ensuring correct results. + + test_multiplication() + Tests the multiplication operation for density matrices, verifying the expected behavior. + + test_eigenvalues() + Tests the eigenvalue computation for density matrices, ensuring correct eigenvalues are returned. + + test_invalid_operations() + Tests various invalid operations to ensure appropriate exceptions are raised. + """ def test_init_without_data_fail(self) -> None: with pytest.raises(ValueError): @@ -228,7 +254,23 @@ def test_expectation_single_fail(self, fx_rng: Generator) -> None: dm.expectation_single(op, nqb + 3) def test_expectation_single_success(self, fx_rng: Generator) -> None: - """Compare with pure states.""" + """ + Test the expectation value calculation for a single success case. + + This method compares the computed expectation values of a density matrix + against those of pure states. It ensures that the expectations align as + expected when the input states are pure. + + Parameters + ---------- + fx_rng : Generator + A random number generator used to generate test inputs for the + expectation value calculations. + + Returns + ------- + None + """ nqb = int(fx_rng.integers(1, 4)) # NOTE a statevector object so can't use its methods target_qubit = int(fx_rng.integers(low=0, high=nqb)) @@ -741,7 +783,23 @@ def test_apply_depolarising_channel(self, fx_rng: Generator) -> None: assert np.allclose(dm.rho, expected_dm) def test_apply_random_channel_one_qubit(self, fx_rng: Generator) -> None: - """Test using complex parameters.""" + """ + Test the application of a random channel on a one-qubit density matrix. + + This method verifies the correctness of the operation of a randomly generated + quantum channel applied to a single qubit. The test ensures that the resulting + density matrix adheres to the expected properties of quantum mechanics, including + normalization and positivity. + + Parameters + ---------- + fx_rng : Generator + A random number generator used for creating the random channel parameters. + + Returns + ------- + None + """ # check against statevector backend by hand for now. # create random density matrix @@ -789,7 +847,19 @@ def test_apply_random_channel_one_qubit(self, fx_rng: Generator) -> None: assert np.allclose(dm.rho, expected_dm) def test_apply_random_channel_two_qubits(self, fx_rng: Generator) -> None: - """Test random 2-qubit channel on a rank 1 dm (pure state).""" + """ + Test the application of a random 2-qubit channel on a rank 1 density matrix (pure state). + + Parameters + ---------- + fx_rng : Generator + A random number generator used to create the random channel. + + Returns + ------- + None + This method does not return any value, it only performs validation checks on the state resulting from the channel application. + """ nqubits = int(fx_rng.integers(2, 5)) # target qubits indices @@ -828,7 +898,24 @@ def test_apply_random_channel_two_qubits(self, fx_rng: Generator) -> None: assert np.allclose(dm.rho, expected_dm) def test_apply_channel_fail(self, fx_rng: Generator) -> None: - """Test apply a channel that is not a Channel object.""" + """ + Test applying a channel that is not a Channel object. + + This test checks the behavior of the apply method when the provided + input is not an instance of the Channel class. It is expected + to raise a TypeError or a similar exception indicating that the + input is invalid. + + Parameters + ---------- + fx_rng : Generator + A random number generator instance used in the test. + + Raises + ------ + TypeError + If the input channel is not a valid Channel object. + """ nqubits = int(fx_rng.integers(2, 5)) # i = fx_rng.integers(0, nqubits) @@ -843,7 +930,25 @@ def test_apply_channel_fail(self, fx_rng: Generator) -> None: class TestDensityMatrixBackend: - """Test for DensityMatrixBackend class.""" + """ + Test for the DensityMatrixBackend class. + + This class contains unit tests to verify the functionality and performance + of the DensityMatrixBackend implementation. + + Methods + ------- + test_initialization(): + Test the initialization of the DensityMatrixBackend class. + + test_some_functionality(): + Test a specific functionality of the DensityMatrixBackend. + + test_edge_cases(): + Test how the DensityMatrixBackend handles edge cases. + + ... + """ # test initialization only def test_init_success(self, fx_rng: Generator, hadamardpattern: Pattern, randpattern: Pattern, nqb: int) -> None: diff --git a/tests/test_find_pflow.py b/tests/test_find_pflow.py index 185418139..b3fb8bb11 100644 --- a/tests/test_find_pflow.py +++ b/tests/test_find_pflow.py @@ -43,7 +43,8 @@ class DAGTestCase(NamedTuple): def get_og_rndcircuit(depth: int, n_qubits: int, n_inputs: int | None = None) -> OpenGraph: - """Return an open graph from a random circuit. + """ + Return an open graph from a random circuit. Parameters ---------- @@ -51,8 +52,8 @@ def get_og_rndcircuit(depth: int, n_qubits: int, n_inputs: int | None = None) -> Circuit depth of the random circuits for generating open graphs. n_qubits : int Number of qubits in the random circuits for generating open graphs. It controls the number of outputs. - n_inputs : int | None - Optional (default to `None`). Maximum number of inputs in the returned open graph. The returned open graph is the open graph generated from the random circuit where `n_qubits - n_inputs` nodes have been removed from the input-nodes set. This operation does not change the flow properties of the graph. + n_inputs : int, optional + Maximum number of inputs in the returned open graph. If not specified, defaults to `None`. The returned open graph is generated from the random circuit where `n_qubits - n_inputs` nodes have been removed from the input-nodes set. This operation does not change the flow properties of the graph. Returns ------- @@ -83,25 +84,26 @@ def get_og_rndcircuit(depth: int, n_qubits: int, n_inputs: int | None = None) -> def get_og_dense(ni: int, no: int, m: int) -> OpenGraph: - """Return a dense open graph with causal, gflow and pflow. + """ + Return a dense open graph with causal, gflow, and pflow. Parameters ---------- ni : int - Number of input nodes (must be equal or smaller than `no` ). + Number of input nodes (must be less than or equal to `no`). no : int - Number of output nodes (must be larger than 1). + Number of output nodes (must be greater than 1). m : int - Number of total nodes (it must satisfy `m - 2*no > 0`). + Total number of nodes (must satisfy `m - 2*no > 0`). Returns ------- OpenGraph - Open graph with causal and gflow. + A dense open graph with causal and gflow properties. Notes ----- - Adapted from Fig. 1 in Houshmand et al., Phys. Rev. A, 98 (2018) (arXiv:1705.01535) + Adapted from Fig. 1 in Houshmand et al., Phys. Rev. A, 98 (2018) (arXiv:1705.01535). """ if no <= 1: raise ValueError("Number of outputs must be larger than 1 (no > 1).") @@ -139,11 +141,19 @@ def prepare_test_og() -> list[OpenGraphTestCase]: # Trivial open graph with pflow and nI = nO def get_og_0() -> OpenGraph: - """Return an open graph with Pauli flow and equal number of outputs and inputs. + """ + Return a trivial open graph with Pauli flow and equal number of outputs and inputs. The returned graph has the following structure: + ``` [0]-1-(2) + ``` + + Returns + ------- + OpenGraph + An open graph with Pauli flow, where the number of inputs equals the number of outputs. """ graph: nx.Graph[int] = nx.Graph([(0, 1), (1, 2)]) inputs = [0] @@ -166,13 +176,19 @@ def get_og_0() -> OpenGraph: # Non-trivial open graph without pflow and nI = nO def get_og_1() -> OpenGraph: - """Return an open graph without Pauli flow and equal number of outputs and inputs. + """ + Return an open graph without Pauli flow and equal number of outputs and inputs. The returned graph has the following structure: - [0]-2-4-(6) - | | - [1]-3-5-(7) + [0]--2--4--(6) + | | + [1]--3--5--(7) + + Returns + ------- + OpenGraph + An instance of OpenGraph representing the specified structure. """ graph: nx.Graph[int] = nx.Graph([(0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5), (4, 6), (5, 7)]) inputs = [1, 0] @@ -226,13 +242,19 @@ def get_og_1() -> OpenGraph: # Non-trivial open graph with pflow and nI = nO def get_og_2() -> OpenGraph: - """Return an open graph with Pauli flow and equal number of outputs and inputs. + """ + Return an open graph with Pauli flow and an equal number of outputs and inputs. The returned graph has the following structure: - [0]-2-4-(6) - | | - [1]-3-5-(7) + [0]-2-4-(6) + | | + [1]-3-5-(7) + + Returns + -------- + OpenGraph + An open graph with specified structure and properties. """ graph: nx.Graph[int] = nx.Graph([(0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5), (4, 6), (5, 7)]) inputs = [0, 1] @@ -286,9 +308,16 @@ def get_og_2() -> OpenGraph: # Non-trivial open graph with pflow and nI != nO def get_og_3() -> OpenGraph: - """Return an open graph with Pauli flow and unequal number of outputs and inputs. + """ + Return a non-trivial open graph with Pauli flow and unequal numbers of outputs and inputs. - Example from Fig. 1 in Mitosek and Backens, 2024 (arXiv:2410.23439). + This function constructs an open graph as described in Figure 1 of Mitosek and Backens (2024), + available at arXiv:2410.23439. + + Returns + ------- + OpenGraph + An instance of the OpenGraph class representing the specified non-trivial open graph. """ graph: nx.Graph[int] = nx.Graph( [(0, 2), (2, 4), (3, 4), (4, 6), (1, 4), (1, 6), (2, 3), (3, 5), (2, 6), (3, 6)] @@ -325,7 +354,23 @@ def get_og_3() -> OpenGraph: # Non-trivial open graph with pflow and nI != nO def get_og_4() -> OpenGraph: - """Return an open graph with Pauli flow and unequal number of outputs and inputs.""" + """ + Return an open graph with Pauli flow and an unequal number of outputs and inputs. + + This function constructs a non-trivial open graph where the number of inputs + is not equal to the number of outputs. It is designed for testing the final + result of the graph structure. + + Returns + ------- + OpenGraph + An instance of an OpenGraph representing the specified configuration. + + Notes + ----- + This is part of a set of tests that focus on the final outcomes rather than + the intermediate steps of graph construction. + """ graph: nx.Graph[int] = nx.Graph([(0, 2), (1, 3), (2, 3), (2, 6), (3, 4), (4, 7), (4, 5), (7, 8)]) inputs = [0, 1] outputs = [5, 6, 8] @@ -352,7 +397,22 @@ def get_og_4() -> OpenGraph: # Non-trivial open graph with pflow and nI != nO def get_og_5() -> OpenGraph: - """Return an open graph with Pauli flow and unequal number of outputs and inputs.""" + """ + Return a non-trivial open graph with Pauli flow. + + This graph is characterized by having an unequal number of inputs + and outputs. + + Returns + ------- + OpenGraph + An instance of an OpenGraph representing the defined characteristics. + + Notes + ----- + The function is designed to create a specific type of open graph + used in the context of quantum computing and circuit representation. + """ graph: nx.Graph[int] = nx.Graph([(0, 2), (1, 2), (2, 3), (3, 4)]) inputs = [0, 1] outputs = [1, 3, 4] @@ -372,7 +432,16 @@ def get_og_5() -> OpenGraph: # Non-trivial open graph with pflow and nI != nO def get_og_6() -> OpenGraph: - """Return an open graph with Pauli flow and unequal number of outputs and inputs.""" + """ + Return a non-trivial open graph characterized by Pauli flow. + + This graph features an unequal number of outputs and inputs. + + Returns + ------- + OpenGraph + An instance of OpenGraph representing the specified configuration. + """ graph: nx.Graph[int] = nx.Graph([(0, 1), (0, 3), (1, 4), (3, 4), (2, 3), (2, 5), (3, 6), (4, 7)]) inputs = [1] outputs = [6, 2, 7] @@ -398,7 +467,14 @@ def get_og_6() -> OpenGraph: # Disconnected open graph with pflow and nI != nO def get_og_7() -> OpenGraph: - """Return an open graph with Pauli flow and unequal number of outputs and inputs.""" + """ + Return a disconnected open graph with Pauli flow, characterized by an unequal number of inputs and outputs. + + Returns + ------- + OpenGraph + An instance of OpenGraph representing the specified configuration. + """ graph: nx.Graph[int] = nx.Graph([(0, 1), (0, 2), (2, 3), (1, 3), (4, 6)]) inputs: list[int] = [] outputs = [1, 3, 4] @@ -418,7 +494,17 @@ def get_og_7() -> OpenGraph: # Non-trivial open graph without pflow and nI != nO def get_og_8() -> OpenGraph: - """Return an open graph without Pauli flow and unequal number of outputs and inputs.""" + """ + Return a non-trivial open graph. + + This graph is characterized by the absence of Pauli flow + and contains an unequal number of outputs and inputs. + + Returns + ------- + OpenGraph + An instance of the OpenGraph class representing the desired graph. + """ graph: nx.Graph[int] = nx.Graph( [(0, 1), (0, 3), (1, 4), (3, 4), (2, 3), (2, 5), (3, 6), (4, 7), (5, 6), (6, 7)] ) @@ -446,7 +532,18 @@ def get_og_8() -> OpenGraph: # Disconnected open graph without pflow and nI != nO def get_og_9() -> OpenGraph: - """Return an open graph without Pauli flow and unequal number of outputs and inputs.""" + """ + Return an open graph without Pauli flow and with an unequal number of inputs and outputs. + + This function constructs and returns a disconnected open graph that does not involve + Pauli flow, ensuring that the number of input nodes is not equal to the number of + output nodes. + + Returns + ------- + OpenGraph + An instance of the OpenGraph class representing the specified graph structure. + """ graph: nx.Graph[int] = nx.Graph([(0, 1), (0, 2), (2, 3), (1, 3), (4, 6)]) inputs = [0] outputs = [1, 3, 4] @@ -466,7 +563,24 @@ def get_og_9() -> OpenGraph: # Non-trivial open graph without pflow and nI != nO def get_og_10() -> OpenGraph: - """Return a graph constructed by adding a disconnected input to graph_6. The resulting graph does not have pflow.""" + """ + Construct a non-trivial open graph by modifying an existing graph. + + The function returns a graph obtained by adding a disconnected input to + `graph_6`. The resulting graph does not feature pflow, and it is defined + under the condition that the number of inputs (nI) is not equal to the + number of outputs (nO). + + Returns + ------- + OpenGraph + A non-trivial open graph without pflow. + + Notes + ----- + This graph configuration is useful for testing scenarios where the flow + dynamics can be analyzed without the influence of pflow. + """ graph: nx.Graph[int] = nx.Graph([(0, 1), (0, 3), (1, 4), (3, 4), (2, 3), (2, 5), (3, 6), (4, 7)]) graph.add_node(8) inputs = [1, 8] @@ -494,7 +608,15 @@ def get_og_10() -> OpenGraph: # Open graph with only Pauli measurements, without pflow and nI != nO def get_og_11() -> OpenGraph: - """Return an open graph without Pauli flow and unequal number of outputs and inputs.""" + """ + Return an open graph with only Pauli measurements, excluding + Pauli flow, and with an unequal number of outputs and inputs. + + Returns + ------- + OpenGraph + An instance of OpenGraph representing the specified configuration. + """ graph: nx.Graph[int] = nx.Graph([(0, 2), (1, 3), (2, 3), (2, 6), (3, 4), (4, 7), (4, 5), (7, 8)]) inputs = [0, 1] outputs = [5, 6, 8] @@ -521,7 +643,18 @@ def get_og_11() -> OpenGraph: # Open graph with only Pauli measurements, with pflow and nI != nO def get_og_12() -> OpenGraph: - """Return an open graph with Pauli flow and unequal number of outputs and inputs. Even though all nodes are Pauli-measured, open graph has flow because none of them are inputs.""" + """ + Return an open graph with Pauli measurements and unequal number of outputs and inputs. + + This function constructs an open graph where all nodes are measured using Pauli measurements. + Despite all nodes being Pauli-measured, the graph maintains flow because none of the nodes are inputs, + resulting in a scenario where the number of outputs (nO) is not equal to the number of inputs (nI). + + Returns + ------- + OpenGraph + An instance of an open graph characterized by Pauli flow and an unequal count of outputs and inputs. + """ graph: nx.Graph[int] = nx.Graph([(0, 2), (1, 3), (2, 3), (2, 6), (3, 4), (4, 7), (4, 5), (7, 8)]) outputs = [5, 6, 8] meas = { diff --git a/tests/test_generator.py b/tests/test_generator.py index c9f8989d3..0f720cbd6 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -52,16 +52,20 @@ def example_gflow(rng: Generator) -> Pattern: def example_graph_pflow(rng: Generator) -> OpenGraph: - """Create a graph which has pflow but no gflow. + """ + Create a graph that has power flow but no gas flow. Parameters ---------- - rng : :class:`numpy.random.Generator` - See graphix.tests.conftest.py + rng : numpy.random.Generator + Random number generator used for creating the graph. See + `graphix.tests.conftest.py` for details. Returns ------- - OpenGraph: :class:`graphix.opengraph.OpenGraph` + OpenGraph + An instance of `graphix.opengraph.OpenGraph` representing the + generated graph with power flow. """ graph: nx.Graph[int] = nx.Graph( [(0, 2), (1, 4), (2, 3), (3, 4), (2, 5), (3, 6), (4, 7), (5, 6), (6, 7), (5, 8), (7, 9)] diff --git a/tests/test_graphsim.py b/tests/test_graphsim.py index f69f2ee19..700b49be6 100644 --- a/tests/test_graphsim.py +++ b/tests/test_graphsim.py @@ -35,26 +35,26 @@ def get_state(g: GraphState) -> Statevec: def meas_op( angle: float, vop: Clifford = Clifford.I, plane: Plane = Plane.XY, choice: int = 0 ) -> npt.NDArray[np.complex128]: - """Return the projection operator for given measurement angle and local Clifford op (VOP). + """ + Return the projection operator for a given measurement angle and local Clifford operation (VOP). - .. seealso:: :mod:`graphix.clifford` + Refer to :mod:`graphix.clifford` for more details. Parameters ---------- angle : float - original measurement angle in radian - vop : int - index of local Clifford (vop), see graphq.clifford.CLIFFORD - plane : 'XY', 'YZ' or 'ZX' - measurement plane on which angle shall be defined - choice : 0 or 1 - choice of measurement outcome. measured eigenvalue would be (-1)**choice. + Original measurement angle in radians. + vop : Clifford, optional + Local Clifford operation. Default is Clifford.I. + plane : Plane, optional + Measurement plane on which the angle is defined. Options are 'XY', 'YZ', or 'ZX'. Default is Plane.XY. + choice : int, optional + Choice of measurement outcome. The measured eigenvalue would be (-1)**choice. Default is 0. Returns ------- - op : numpy array - projection operator - + op : npt.NDArray[np.complex128] + Projection operator corresponding to the specified measurement parameters. """ assert choice in {0, 1} if plane == Plane.XY: @@ -71,7 +71,16 @@ def meas_op( class TestGraphSim: def test_fig2(self) -> None: - """Three single-qubit measurements presented in Fig.2 of M. Elliot et al (2010).""" + """ + Test three single-qubit measurements. + + This method verifies the results of the single-qubit measurements + presented in Figure 2 of M. Elliot et al. (2010). + + Returns + ------- + None + """ nqubit = 6 edges = [(0, 1), (1, 2), (3, 4), (4, 5), (0, 3), (1, 4), (2, 5)] g = GraphState(nodes=np.arange(nqubit), edges=edges) diff --git a/tests/test_kraus.py b/tests/test_kraus.py index f7e4983f0..43391a164 100644 --- a/tests/test_kraus.py +++ b/tests/test_kraus.py @@ -21,10 +21,50 @@ class TestChannel: - """Tests for Channel class.""" + """ + Tests for the Channel class. + + This class contains unit tests to verify the functionality of the + Channel class. Each test method prefixed with 'test_' will be run + automatically by the testing framework. + + Methods + ------- + test_initial_state() + Tests the initial state of a Channel instance. + + test_send_message() + Tests the functionality of sending a message through the Channel. + + test_receive_message() + Tests the functionality of receiving a message from the Channel. + + test_close_channel() + Tests the behavior of closing the Channel. + + test_channel_state_after_close() + Tests that the Channel behaves as expected after it has been closed. + """ def test_init_with_data_success(self, fx_rng: Generator) -> None: - """Test for successful intialization.""" + """ + Test the successful initialization of the Channel class with data. + + This method verifies that the Channel class can be correctly initialized + when provided with valid data. It ensures that all attributes are set + appropriately and that the object behaves as expected. + + Parameters + ---------- + fx_rng : Generator + A random number generator used to produce test data for the + initialization. + + Notes + ----- + This test case is part of the unit tests for the Channel class and + should be run in the appropriate test environment. + """ prob = fx_rng.uniform() mychannel = KrausChannel( [ @@ -37,7 +77,23 @@ def test_init_with_data_success(self, fx_rng: Generator) -> None: assert len(mychannel) == 2 def test_init_with_data_fail(self, fx_rng: Generator) -> None: - """Test for unsuccessful intialization.""" + """ + Test for unsuccessful initialization of the Channel class. + + This test verifies that the Channel class does not initialize successfully + when provided with invalid data. It checks for proper exception handling + and ensures that the object is not created when expected to fail. + + Parameters + ---------- + fx_rng : Generator + A random number generator used for creating test data. + + Raises + ------ + Exception + If the initialization is successful when it should fail. + """ prob = fx_rng.uniform() # empty data diff --git a/tests/test_linalg.py b/tests/test_linalg.py index 66b3a6c34..2534f121d 100644 --- a/tests/test_linalg.py +++ b/tests/test_linalg.py @@ -117,26 +117,29 @@ def prepare_test_f2_linear_system() -> list[LSF2TestCase]: def verify_elimination(mat: MatGF2, mat_red: MatGF2, n_cols_red: int, full_reduce: bool) -> None: - """Test gaussian elimination (GE). + """ + Test Gaussian elimination (GE). Parameters ---------- mat : MatGF2 - Original matrix. + The original matrix. mat_red : MatGF2 - Gaussian-eliminated matrix. + The Gaussian-eliminated matrix. n_cols_red : int - Number of columns over which `mat` was reduced. + The number of columns over which `mat` was reduced. full_reduce : bool Flag to check for row-reduced echelon form (`True`) or row echelon form (`False`). Notes ----- - It tests that: - 1) Matrix is in row echelon form (REF) or row-reduced echelon form. - 2) The procedure only entails row operations. + This function tests that: + 1. The matrix is in row echelon form (REF) or row-reduced echelon form (RREF). + 2. The elimination procedure only entails row operations. - Check (2) implies that the GE procedure can be represented by a linear transformation. Thefore, we perform GE on :math:`A = [M|1]`, with :math:`M` the test matrix and :math:`1` the identiy, and we verify that :math:`M = L^{-1}M'`, where :math:`M', L` are the left and right blocks of :math:`A` after gaussian elimination. + Condition (2) implies that the Gaussian elimination (GE) procedure can be represented by a linear transformation. + Therefore, we perform GE on the augmented matrix :math:`A = [M|I]`, where :math:`M` is the test matrix and :math:`I` is the identity matrix. + We then verify that :math:`M = L^{-1}M'`, where :math:`M'` and :math:`L` are the left and right blocks of :math:`A` after Gaussian elimination. """ mat_red_block = MatGF2(mat_red[:, :n_cols_red]) # Check 1 diff --git a/tests/test_noisy_density_matrix.py b/tests/test_noisy_density_matrix.py index 49e1293d3..8fd349011 100644 --- a/tests/test_noisy_density_matrix.py +++ b/tests/test_noisy_density_matrix.py @@ -19,7 +19,32 @@ class TestNoisyDensityMatrixBackend: - """Test for Noisy DensityMatrixBackend simultation.""" + """ + Test for the Noisy DensityMatrixBackend simulation. + + This class contains unit tests for the NoisyDensityMatrixBackend, + focusing on verifying its behavior under various conditions and + ensuring that it accurately simulates noise in quantum density matrices. + + Attributes + ---------- + backend : NoisyDensityMatrixBackend + An instance of the NoisyDensityMatrixBackend to be tested. + + Methods + ------- + setUp() + Initializes the NoisyDensityMatrixBackend instance before each test. + + test_simulation_steps() + Tests the correct number of simulation steps. + + test_noise_effects() + Verifies that noise is properly applied to the density matrix. + + test_measurement_outcomes() + Checks that measurement outcomes match expected results under noise. + """ @staticmethod def rz_exact_res(alpha: float) -> npt.NDArray[np.float64]: diff --git a/tests/test_pattern.py b/tests/test_pattern.py index eef7cfb66..e03b4e9e9 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -686,7 +686,25 @@ def test_compose_7(self, fx_rng: Generator) -> None: def cp(circuit: Circuit, theta: float, control: int, target: int) -> None: - """Controlled rotation gate, decomposed.""" # noqa: D401 + """ + Controlled rotation gate, decomposed. + + Parameters + ---------- + circuit : Circuit + The quantum circuit to which the controlled rotation gate will be applied. + theta : float + The angle of rotation for the controlled rotation gate, specified in radians. + control : int + The index of the control qubit in the circuit. + target : int + The index of the target qubit in the circuit. + + Returns + ------- + None + This function modifies the circuit in place to include the controlled rotation gate. + """ circuit.rz(control, theta / 2) circuit.rz(target, theta / 2) circuit.cnot(control, target) @@ -695,7 +713,24 @@ def cp(circuit: Circuit, theta: float, control: int, target: int) -> None: def swap(circuit: Circuit, a: int, b: int) -> None: - """Swap gate, decomposed.""" + """ + Swap gate, decomposed. + + Parameters + ---------- + circuit : Circuit + The quantum circuit where the swap operation will be applied. + a : int + The index of the first qubit to swap. + b : int + The index of the second qubit to swap. + + Returns + ------- + None + This function modifies the circuit in place to include the swap operation + between the specified qubits. + """ circuit.cnot(a, b) circuit.cnot(b, a) circuit.cnot(a, b) diff --git a/tests/test_pyzx.py b/tests/test_pyzx.py index 53e8644c6..3ad7ed451 100644 --- a/tests/test_pyzx.py +++ b/tests/test_pyzx.py @@ -47,7 +47,25 @@ def test_graph_equality() -> None: def assert_reconstructed_pyzx_graph_equal(g: BaseGraph[int, tuple[int, int]]) -> None: - """Convert a graph to and from an Open graph and then checks the resulting pyzx graph is equal to the original.""" + """ + Convert a graph to and from an Open graph and then check if the resulting + pyzx graph is equal to the original graph. + + Parameters + ---------- + g : BaseGraph[int, tuple[int, int]] + The original graph to be converted and checked. + + Raises + ------ + AssertionError + If the reconstructed pyzx graph does not equal the original graph. + + Notes + ----- + The function assumes that the graph `g` is a valid instance of `BaseGraph` + and uses internal methods to perform the conversion and comparison. + """ zx.simplify.to_graph_like(g) g_copy = deepcopy(g) diff --git a/tests/test_qasm3_exporter.py b/tests/test_qasm3_exporter.py index 22202e388..5ceb8f1e6 100644 --- a/tests/test_qasm3_exporter.py +++ b/tests/test_qasm3_exporter.py @@ -1,4 +1,10 @@ -"""Test exporter to OpenQASM3.""" +""" +Test exporter to OpenQASM3. + +This module provides functionality to export quantum circuits +and related data to the OpenQASM3 format, facilitating interoperability +with other quantum computing tools and frameworks. +""" from __future__ import annotations diff --git a/tests/test_statevec.py b/tests/test_statevec.py index e079e235e..59bb0a711 100644 --- a/tests/test_statevec.py +++ b/tests/test_statevec.py @@ -15,7 +15,21 @@ class TestStatevec: - """Test for Statevec class. Particularly new constructor.""" + """ + Test for the Statevec class. + + This class contains unit tests for verifying the functionality + of the Statevec class, with a particular focus on its constructor. + + Methods + ------- + test_constructor_valid_inputs: + Tests the constructor with valid input values. + + test_constructor_invalid_inputs: + Tests the constructor with invalid input values to ensure + it raises appropriate exceptions. + """ # test injitializing one qubit in plus state def test_default_success(self) -> None: diff --git a/tests/test_statevec_backend.py b/tests/test_statevec_backend.py index 9d14f372b..42c042275 100644 --- a/tests/test_statevec_backend.py +++ b/tests/test_statevec_backend.py @@ -113,7 +113,17 @@ def test_deterministic_measure_one(self, fx_rng: Generator): assert result == expected_result def test_deterministic_measure(self): - """Entangle |+> state with N |0> states, the (XY,0) measurement yields the outcome 0 with probability 1.""" + """ + Test deterministic measurement of an entangled state. + + This method verifies that entangling the |+> state with N |0> states + results in a measurement outcome of 0 with probability 1 for the + (XY, 0) measurement. + + Returns + ------- + None + """ for _ in range(10): # plus state (default) backend = StatevectorBackend() @@ -131,7 +141,16 @@ def test_deterministic_measure(self): assert list(backend.node_index) == list(range(1, n_neighbors + 1)) def test_deterministic_measure_many(self): - """Entangle |+> state with N |0> states, the (XY,0) measurement yields the outcome 0 with probability 1.""" + """ + Tests the deterministic measurement of a many-body entangled state. + + Entangles the |+> state with N |0> states. Performing the (XY, 0) measurement + yields an outcome of 0 with a probability of 1. + + Returns + ------- + None + """ for _ in range(10): # plus state (default) backend = StatevectorBackend() @@ -162,9 +181,20 @@ def test_deterministic_measure_many(self): assert list(backend.node_index) == list(range(n_traps, n_neighbors + n_traps + n_whatever)) def test_deterministic_measure_with_coin(self, fx_rng: Generator): - """Entangle |+> state with N |0> states, the (XY,0) measurement yields the outcome 0 with probability 1. + """ + Test the deterministic measurement with a coin toss. + + This test entangles the |+> state with N |0> states. The (XY, 0) measurement should yield the outcome 0 with a probability of 1. + A coin toss is added to this measurement process to assess its behavior under stochastic conditions. + + Parameters + ---------- + fx_rng : Generator + A random number generator for simulating the coin toss outcomes. - We add coin toss to that. + Notes + ----- + This function verifies the expected outcomes in a deterministic measurement scenario modified by a coin toss. """ for _ in range(10): # plus state (default)