diff --git a/CHANGELOG.md b/CHANGELOG.md index bf672471..4f11acbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #450: `Circuit.visit` and `BaseInstruction.visit` performs simple replacements on circuits and instructions, given an `InstructionVisitor`. +- #457: Added `Statevec.to_dict` and `Statevec.to_prob_dict` methods to convert a statevector to dictionary form as suggested in #100. + ### Fixed - #429 diff --git a/graphix/flow/_find_gpflow.py b/graphix/flow/_find_gpflow.py index faaecdc5..c32c6a4a 100644 --- a/graphix/flow/_find_gpflow.py +++ b/graphix/flow/_find_gpflow.py @@ -39,6 +39,13 @@ class AlgebraicOpenGraph(Generic[_AM_co]): 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. + 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). + 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 :math:`P` matrix more efficiently in the `:func: _compute_correction_matrix_general` routine. @@ -46,13 +53,6 @@ class AlgebraicOpenGraph(Generic[_AM_co]): References ---------- [1] Mitosek and Backens, 2024 (arXiv:2410.23439). - - 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). """ def __init__(self, og: OpenGraph[_AM_co]) -> None: @@ -230,16 +230,17 @@ def _node_measurement_label(self, node: int) -> Plane: class CorrectionMatrix(Generic[_AM_co]): r"""A dataclass to bundle the correction matrix and its associated open graph. + Attributes + ---------- + aog (AgebraicOpenGraph) : Open graph in an algebraic representation. + c_matrix (MatGF2) : Matrix encoding the correction function of a Pauli (or generalised) flow, :math:`C`. + 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 `aog`. In other words, the column :math:`v` of :math:`C` encodes the correction set of :math:`v`, :math:`c(v)`. See Definition 3.6 in Mitosek and Backens, 2024 (arXiv:2410.23439). - Attributes - ---------- - aog (AgebraicOpenGraph) : Open graph in an algebraic representation. - c_matrix (MatGF2) : Matrix encoding the correction function of a Pauli (or generalised) flow, :math:`C`. """ aog: AlgebraicOpenGraph[_AM_co] diff --git a/graphix/flow/core.py b/graphix/flow/core.py index c6e3de23..e551148a 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -64,10 +64,6 @@ class XZCorrections(Generic[_AM_co]): """An unmutable dataclass providing a representation of XZ-corrections. - Notes - ----- - The XZ-corrections mappings define a partial order, therefore, only `og`, `x_corrections` and `z_corrections` are necessary to initialize an `XZCorrections` instance (see :func:`XZCorrections.from_measured_nodes_mapping`). However, XZ-corrections are often extracted from a flow whose partial order is known and can be used to construct a pattern, so it can also be passed as an argument to the `dataclass` constructor. The correctness of the input parameters is not verified automatically. - Attributes ---------- og : OpenGraph[_AM_co] @@ -78,6 +74,10 @@ class XZCorrections(Generic[_AM_co]): Mapping of Z-corrections: in each (`key`, `value`) pair, `key` is a measured node, and `value` is the set of nodes on which an Z-correction must be applied depending on the measurement result of `key`. partial_order_layers : Sequence[AbstractSet[int]] Partial order between the open graph's nodes in a layer form determined by the corrections. The set `layers[i]` comprises the nodes in layer `i`. Nodes in layer `i` are "larger" in the partial order than nodes in layer `i+1`. If the open graph has output nodes, they are always in layer 0. Non-corrected, measured nodes are always in the last layer. + + Notes + ----- + The XZ-corrections mappings define a partial order, therefore, only `og`, `x_corrections` and `z_corrections` are necessary to initialize an `XZCorrections` instance (see :func:`XZCorrections.from_measured_nodes_mapping`). However, XZ-corrections are often extracted from a flow whose partial order is known and can be used to construct a pattern, so it can also be passed as an argument to the `dataclass` constructor. The correctness of the input parameters is not verified automatically. """ og: OpenGraph[_AM_co] @@ -406,6 +406,15 @@ def xreplace( class PauliFlow(Generic[_AM_co]): """An unmutable dataclass providing a representation of a Pauli flow. + Attributes + ---------- + og : OpenGraph[_AM_co] + The open graph with respect to which the Pauli flow is defined. + correction_function : Mapping[int, AbstractSet[int] + Pauli flow correction function. `correction_function[i]` is the set of qubits correcting the measurement of qubit `i`. + partial_order_layers : Sequence[AbstractSet[int]] + Partial order between the open graph's nodes in a layer form. The set `layers[i]` comprises the nodes in layer `i`. Nodes in layer `i` are "larger" in the partial order than nodes in layer `i+1`. Output nodes are always in layer 0. + Notes ----- - See Definition 5 in Ref. [1] for a definition of Pauli flow. @@ -418,15 +427,6 @@ class PauliFlow(Generic[_AM_co]): ---------- [1] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212). [2] Mitosek and Backens, 2024 (arXiv:2410.23439). - - Attributes - ---------- - og : OpenGraph[_AM_co] - The open graph with respect to which the Pauli flow is defined. - correction_function : Mapping[int, AbstractSet[int] - Pauli flow correction function. `correction_function[i]` is the set of qubits correcting the measurement of qubit `i`. - partial_order_layers : Sequence[AbstractSet[int]] - Partial order between the open graph's nodes in a layer form. The set `layers[i]` comprises the nodes in layer `i`. Nodes in layer `i` are "larger" in the partial order than nodes in layer `i+1`. Output nodes are always in layer 0. """ og: OpenGraph[_AM_co] diff --git a/graphix/opengraph.py b/graphix/opengraph.py index 49306879..5428bbdc 100644 --- a/graphix/opengraph.py +++ b/graphix/opengraph.py @@ -33,10 +33,6 @@ class OpenGraph(Generic[_AM_co]): """An unmutable dataclass providing a representation of open graph states. - Notes - ----- - The inputs and outputs of `OpenGraph` instances in Graphix are defined as ordered sequences of node labels. This contrasts the usual definition of open graphs in the literature, where inputs and outputs are unordered sets of nodes labels. This restriction facilitates the interplay with `Pattern` objects, where the order of input and output nodes represents a choice of Hilbert space basis. - Attributes ---------- graph : networkx.Graph[int] @@ -48,6 +44,10 @@ class OpenGraph(Generic[_AM_co]): measurements : Mapping[int, _AM_co] A mapping between the non-output nodes of the open graph (``key``) and their corresponding measurement label (``value``). Measurement labels can be specified as `Measurement`, `Plane` or `Axis` instances. + Notes + ----- + The inputs and outputs of `OpenGraph` instances in Graphix are defined as ordered sequences of node labels. This contrasts the usual definition of open graphs in the literature, where inputs and outputs are unordered sets of nodes labels. This restriction facilitates the interplay with `Pattern` objects, where the order of input and output nodes represents a choice of Hilbert space basis. + Example ------- >>> import networkx as nx diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index ea7abe1a..f9df8533 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -21,10 +21,13 @@ if TYPE_CHECKING: from collections.abc import Mapping, Sequence + from typing import Literal from graphix.parameter import ExpressionOrFloat, ExpressionOrSupportsFloat, Parameter from graphix.sim.data import Data + _ENCODING = Literal["LSB", "MSB"] + CZ_TENSOR = np.array( [[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, -1]]]], @@ -214,7 +217,7 @@ def dims(self) -> tuple[int, ...]: @override def nqubit(self) -> int: """Return the number of qubits.""" - return len(self.psi.shape) + return self.psi.ndim @override def remove_qubit(self, qarg: int) -> None: @@ -427,6 +430,101 @@ def isclose(self, other: Statevec, *, rtol: float = 1e-09, atol: float = 0.0) -> """ return math.isclose(self.fidelity(other), 1, rel_tol=rtol, abs_tol=atol) + def to_dict( + self, + encoding: _ENCODING = "MSB", + *, + rtol: float = 0.0, + atol: float = 1e-8, + ) -> dict[str, complex]: + r"""Convert the statevector to dictionary form. + + This dictionary representation uses a ket-like notation where the dictionary ``keys`` are qubit strings for the basis vectors and ``values`` are the corresponding complex amplitudes. Amplitudes below a certain threshold are filtered out. + + Parameters + ---------- + encoding : Literal["LSB", "MSB"], default="MSB" + Encoding for the basis kets. See notes for additional information. + + rtol : float, default=0.0 + Relative tolerance used when deciding whether a coefficient should be + treated as zero. Values whose magnitude is within this relative tolerance + of zero are omitted from the resulting dictionary. + + atol : float, default=1e-8 + Absolute tolerance used when deciding whether a coefficient should be + treated as zero. Values whose magnitude is within this relative tolerance + of zero are omitted from the resulting dictionary. + + Returns + ------- + dict[str, complex] + The statevector in dictionary form. + + Notes + ----- + The encoding determines the bit ordering convention used when mapping basis states to dictionary + keys. Consider a tensor product of three qubits: + + .. math:: + + \lvert\psi\rangle = q_0 \otimes q_1 \otimes q_2. + + If ``encoding == "MSB"`` the first qubit is represented in the Most Significant Bit -> ``q0q1q2``. This is the default representation in Graphix. + If ``encoding == "LSB"`` the first qubit is represented in the Least Significant Bit -> ``q2q1q0``. This is the default representation in other software packages such as Qiskit. + + Example + ------- + >>> from graphix.states import BasicStates + >>> from graphix.sim.statevec import Statevec + >>> sv = Statevec(data=[BasicStates.ZERO, BasicStates.ONE]) + >>> sv.to_dict() + {'01': np.complex128(1+0j)} + >>> sv.to_dict(encoding="LSB") + {'10': np.complex128(1+0j)} + """ + mask = np.logical_not(np.isclose(np.abs(self.flatten()), 0, rtol=rtol, atol=atol)) + i_vals = np.arange(1 << self.nqubit)[mask] + amp_vals = self.flatten()[mask] + + return {_format_encoding(self.nqubit, i, encoding): amp for i, amp in zip(i_vals, amp_vals, strict=True)} + + def to_prob_dict(self, encoding: _ENCODING = "MSB", *, rtol: float = 0.0, atol: float = 1e-8) -> dict[str, float]: + r"""Convert the statevector to a probability distirbution in a dictionary form. + + This dictionary representation uses a ket-like notation where the dictionary ``keys`` are qubit strings for the basis vectors and ``values`` are the corresponding probabilities. + Basis vector whose amplitude is below a certain threshold are filtered out. + + Parameters + ---------- + encoding: Literal["LSB", "MSB"], default="MSB" + Encoding for the basis kets. See :meth:`to_dict` for additional information. + + rtol : float, default=0.0 + Relative tolerance used when deciding whether a coefficient should be + treated as zero. Values whose magnitude is within this relative tolerance + of zero are omitted from the resulting dictionary. + + atol : float, default=1e-8 + Absolute tolerance used when deciding whether a coefficient should be + treated as zero. Values whose magnitude is within this relative tolerance + of zero are omitted from the resulting dictionary. + + Returns + ------- + dict[str, float] + The probability distribution associated to the statevector in dictionary form. + + See Also + -------- + .. :meth:`to_dict` + """ + mask = np.logical_not(np.isclose(np.abs(self.flatten()), 0, rtol=rtol, atol=atol)) + i_vals = np.arange(1 << self.nqubit)[mask] + amp2_vals = np.abs(self.flatten()[mask]) ** 2 + + return {_format_encoding(self.nqubit, i, encoding): amp2 for i, amp2 in zip(i_vals, amp2_vals, strict=True)} + 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.""" result = Statevec() @@ -466,3 +564,12 @@ def _norm(psi: Matrix) -> ExpressionOrFloat: if psi.dtype == np.object_: return _norm_symbolic(psi.astype(np.object_, copy=False)) return _norm_numeric(psi.astype(np.complex128, copy=False)) + + +def _format_encoding(nqubit: int, i: int, encoding: _ENCODING) -> str: + """Format the i-th basis vector as a ket. See :meth:`Statevec.to_dict` for additional details.""" + display_width = nqubit + output = f"{i:0{display_width}b}" + if encoding == "LSB": + return output[::-1] + return output diff --git a/requirements-dev.txt b/requirements-dev.txt index 8004a19e..a9a830ed 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,10 +2,10 @@ mypy==1.19.1 pre-commit # for language-agnostic hooks pyright -ruff==0.15.4 +ruff==0.15.5 # Stubs -types-networkx==3.6.1.20260210 +types-networkx==3.6.1.20260303 types-psutil types-setuptools scipy-stubs diff --git a/tests/test_statevec.py b/tests/test_statevec.py index 6593c8dd..a0c8ef6d 100644 --- a/tests/test_statevec.py +++ b/tests/test_statevec.py @@ -207,6 +207,28 @@ def test_isclose_tolerance(self) -> None: assert not zero.isclose(almost) assert zero.isclose(almost, atol=1e-6) + def test_to_dict(self) -> None: + sv = Statevec(data=[BasicStates.ZERO, BasicStates.PLUS, BasicStates.MINUS]) + lsb_ref = {"000": 0.5, "010": 0.5, "100": -0.5, "110": -0.5} + msb_ref = {"000": 0.5, "010": 0.5, "001": -0.5, "011": -0.5} + for (k_lsb, v_lsb), (k_msb, v_msb) in zip( + sv.to_dict(encoding="LSB").items(), sv.to_dict().items(), strict=True + ): + assert np.isclose(lsb_ref[k_lsb], v_lsb.real) + assert np.isclose(0, v_lsb.imag) + assert np.isclose(msb_ref[k_msb], v_msb.real) + assert np.isclose(0, v_msb.imag) + + def test_to_prob_dict(self) -> None: + sv = Statevec(data=[BasicStates.ONE, BasicStates.PLUS, BasicStates.MINUS]) + lsb_ref = {"001": 0.25, "011": 0.25, "101": 0.25, "111": 0.25} + msb_ref = {"100": 0.25, "110": 0.25, "101": 0.25, "111": 0.25} + for (k_lsb, v_lsb), (k_msb, v_msb) in zip( + sv.to_prob_dict(encoding="LSB").items(), sv.to_prob_dict().items(), strict=True + ): + assert np.isclose(lsb_ref[k_lsb], v_lsb) + assert np.isclose(msb_ref[k_msb], v_msb) + def test_normalize() -> None: statevec = Statevec(nqubit=1, data=BasicStates.PLUS)