From b531b5830aa22df0194d1be9116989bac1beda1c Mon Sep 17 00:00:00 2001 From: matulni Date: Wed, 4 Mar 2026 14:10:26 +0100 Subject: [PATCH 01/12] wip --- graphix/sim/statevec.py | 33 +++++++++++++++++++++++++++++++-- tests/test_statevec.py | 22 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index ea7abe1a..f5fac2e6 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -8,7 +8,7 @@ import math from collections.abc import Iterable from dataclasses import dataclass -from typing import TYPE_CHECKING, SupportsComplex, SupportsFloat +from typing import TYPE_CHECKING, Literal, SupportsComplex, SupportsFloat, TypeVar import numpy as np import numpy.typing as npt @@ -20,11 +20,13 @@ from graphix.states import BasicStates if TYPE_CHECKING: - from collections.abc import Mapping, Sequence + from collections.abc import Callable, Mapping, Sequence from graphix.parameter import ExpressionOrFloat, ExpressionOrSupportsFloat, Parameter from graphix.sim.data import Data +_ENCODING = Literal["LSB", "MSB"] +_A = TypeVar("_A") CZ_TENSOR = np.array( [[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, -1]]]], @@ -427,6 +429,33 @@ 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 = "LSB", + f: Callable[[complex], _A] = lambda _: _, + *, + rel_tol: float = 0.0, + abs_tol: float = 1e-8, + ) -> dict[str, _A]: + + def format_encoding(i: int) -> str: + display_width = self.nqubit + output = f"{i:0{display_width}b}" + if encoding == "MSB": + return output[::-1] + return output + + return { + format_encoding(i): f(amp) + for i, amp in enumerate(self.flatten()) + if not math.isclose(abs(amp), 0, rel_tol=rel_tol, abs_tol=abs_tol) + } + + def to_prob_dict( + self, encoding: _ENCODING = "LSB", *, rel_tol: float = 0.0, abs_tol: float = 1e-8 + ) -> dict[str, float]: + return self.to_dict(encoding, lambda amp: float(abs(amp) ** 2), rel_tol=rel_tol, abs_tol=abs_tol) + 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() diff --git a/tests/test_statevec.py b/tests/test_statevec.py index 6593c8dd..f525757b 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, "001": -0.5, "011": -0.5} + msb_ref = {"000": 0.5, "010": 0.5, "100": -0.5, "110": -0.5} + for (k_lsb, v_lsb), (k_msb, v_msb) in zip( + sv.to_dict().items(), sv.to_dict(encoding="MSB").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 = {"100": 0.25, "110": 0.25, "101": 0.25, "111": 0.25} + msb_ref = {"001": 0.25, "011": 0.25, "101": 0.25, "111": 0.25} + for (k_lsb, v_lsb), (k_msb, v_msb) in zip( + sv.to_prob_dict().items(), sv.to_prob_dict(encoding="MSB").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) From 73ace3db800e040a2e6aceea06795e6479aeb7e5 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 5 Mar 2026 13:52:02 +0100 Subject: [PATCH 02/12] Write nqubit property more idiomatically --- graphix/sim/statevec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index f5fac2e6..56f7dad7 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -216,7 +216,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: From c26d41357fc40185ccabc566d5aad322687bbcad Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 5 Mar 2026 17:13:51 +0100 Subject: [PATCH 03/12] Remove function argument and add docs --- graphix/sim/statevec.py | 90 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index 56f7dad7..af0b4cb7 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -8,7 +8,7 @@ import math from collections.abc import Iterable from dataclasses import dataclass -from typing import TYPE_CHECKING, Literal, SupportsComplex, SupportsFloat, TypeVar +from typing import TYPE_CHECKING, Literal, SupportsComplex, SupportsFloat import numpy as np import numpy.typing as npt @@ -20,13 +20,12 @@ from graphix.states import BasicStates if TYPE_CHECKING: - from collections.abc import Callable, Mapping, Sequence + from collections.abc import Mapping, Sequence from graphix.parameter import ExpressionOrFloat, ExpressionOrSupportsFloat, Parameter from graphix.sim.data import Data _ENCODING = Literal["LSB", "MSB"] -_A = TypeVar("_A") CZ_TENSOR = np.array( [[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, -1]]]], @@ -432,11 +431,57 @@ def isclose(self, other: Statevec, *, rtol: float = 1e-09, atol: float = 0.0) -> def to_dict( self, encoding: _ENCODING = "LSB", - f: Callable[[complex], _A] = lambda _: _, *, rel_tol: float = 0.0, abs_tol: float = 1e-8, - ) -> dict[str, _A]: + ) -> 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 : _ENCODING, default="LSB" + Encoding for the basis kets. See `Notes` for additional information. + + rel_tol : 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. + + abs_tol : 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:: + + |\psi\rangle = q_0 \otimes q_1 \otimes q_2. + + If ``encoding == "LSB"`` (least significant bit), the state is represented as `q0q1q2`. This is the default representation in Graphix. + If ``encoding == "MSB"`` (most significant bit), the state is represented as `q2q1q1`. 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': 1} + >>> sv.to_dict(encoding="MSB") + {'10': 1} + """ def format_encoding(i: int) -> str: display_width = self.nqubit @@ -446,7 +491,7 @@ def format_encoding(i: int) -> str: return output return { - format_encoding(i): f(amp) + format_encoding(i): amp for i, amp in enumerate(self.flatten()) if not math.isclose(abs(amp), 0, rel_tol=rel_tol, abs_tol=abs_tol) } @@ -454,7 +499,38 @@ def format_encoding(i: int) -> str: def to_prob_dict( self, encoding: _ENCODING = "LSB", *, rel_tol: float = 0.0, abs_tol: float = 1e-8 ) -> dict[str, float]: - return self.to_dict(encoding, lambda amp: float(abs(amp) ** 2), rel_tol=rel_tol, abs_tol=abs_tol) + 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: _ENCODING, default="LSB" + Encoding for the basis kets. See :meth:`to_dict` for additional information. + + rel_tol : 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. + + abs_tol : 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` + """ + return { + key: float(abs(amp) ** 2) for key, amp in self.to_dict(encoding, rel_tol=rel_tol, abs_tol=abs_tol).items() + } 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.""" From e5d43e76c7530571f0bad99c64f955ff20469831 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 5 Mar 2026 17:23:30 +0100 Subject: [PATCH 04/12] Fix docs --- graphix/sim/statevec.py | 16 ++++++++-------- tests/test_statevec.py | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index af0b4cb7..709e7aaf 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -430,7 +430,7 @@ def isclose(self, other: Statevec, *, rtol: float = 1e-09, atol: float = 0.0) -> def to_dict( self, - encoding: _ENCODING = "LSB", + encoding: _ENCODING = "MSB", *, rel_tol: float = 0.0, abs_tol: float = 1e-8, @@ -442,7 +442,7 @@ def to_dict( Parameters ---------- - encoding : _ENCODING, default="LSB" + encoding : _ENCODING, default="MSB" Encoding for the basis kets. See `Notes` for additional information. rel_tol : float, default=0.0 @@ -469,8 +469,8 @@ def to_dict( |\psi\rangle = q_0 \otimes q_1 \otimes q_2. - If ``encoding == "LSB"`` (least significant bit), the state is represented as `q0q1q2`. This is the default representation in Graphix. - If ``encoding == "MSB"`` (most significant bit), the state is represented as `q2q1q1`. This is the default representation in other software packages such as Qiskit. + 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 -> `q2q1q1`. This is the default representation in other software packages such as Qiskit. Example ------- @@ -479,14 +479,14 @@ def to_dict( >>> sv = Statevec(data=[BasicStates.ZERO, BasicStates.ONE]) >>> sv.to_dict() {'01': 1} - >>> sv.to_dict(encoding="MSB") + >>> sv.to_dict(encoding="LSB") {'10': 1} """ def format_encoding(i: int) -> str: display_width = self.nqubit output = f"{i:0{display_width}b}" - if encoding == "MSB": + if encoding == "LSB": return output[::-1] return output @@ -497,7 +497,7 @@ def format_encoding(i: int) -> str: } def to_prob_dict( - self, encoding: _ENCODING = "LSB", *, rel_tol: float = 0.0, abs_tol: float = 1e-8 + self, encoding: _ENCODING = "MSB", *, rel_tol: float = 0.0, abs_tol: float = 1e-8 ) -> dict[str, float]: r"""Convert the statevector to a probability distirbution in a dictionary form. @@ -506,7 +506,7 @@ def to_prob_dict( Parameters ---------- - encoding: _ENCODING, default="LSB" + encoding: _ENCODING, default="MSB" Encoding for the basis kets. See :meth:`to_dict` for additional information. rel_tol : float, default=0.0 diff --git a/tests/test_statevec.py b/tests/test_statevec.py index f525757b..a0c8ef6d 100644 --- a/tests/test_statevec.py +++ b/tests/test_statevec.py @@ -209,10 +209,10 @@ def test_isclose_tolerance(self) -> None: def test_to_dict(self) -> None: sv = Statevec(data=[BasicStates.ZERO, BasicStates.PLUS, BasicStates.MINUS]) - lsb_ref = {"000": 0.5, "010": 0.5, "001": -0.5, "011": -0.5} - msb_ref = {"000": 0.5, "010": 0.5, "100": -0.5, "110": -0.5} + 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().items(), sv.to_dict(encoding="MSB").items(), strict=True + 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) @@ -221,10 +221,10 @@ def test_to_dict(self) -> None: def test_to_prob_dict(self) -> None: sv = Statevec(data=[BasicStates.ONE, BasicStates.PLUS, BasicStates.MINUS]) - lsb_ref = {"100": 0.25, "110": 0.25, "101": 0.25, "111": 0.25} - msb_ref = {"001": 0.25, "011": 0.25, "101": 0.25, "111": 0.25} + 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().items(), sv.to_prob_dict(encoding="MSB").items(), strict=True + 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) From 974ef7bfaae2f9f0c87b6ce31d50bbae05b70b18 Mon Sep 17 00:00:00 2001 From: matulni Date: Thu, 5 Mar 2026 17:30:47 +0100 Subject: [PATCH 05/12] Fix example --- graphix/sim/statevec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index 709e7aaf..aa7d6634 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -478,9 +478,9 @@ def to_dict( >>> from graphix.sim.statevec import Statevec >>> sv = Statevec(data=[BasicStates.ZERO, BasicStates.ONE]) >>> sv.to_dict() - {'01': 1} + {'01': np.complex128(1+0j)} >>> sv.to_dict(encoding="LSB") - {'10': 1} + {'10': np.complex128(1+0j)} """ def format_encoding(i: int) -> str: From 7fff4cde104fccc015ebacf3ac8eba992fd9d0c1 Mon Sep 17 00:00:00 2001 From: matulni Date: Fri, 6 Mar 2026 09:15:15 +0100 Subject: [PATCH 06/12] Fix docs --- graphix/sim/statevec.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index aa7d6634..87bd3557 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -443,7 +443,7 @@ def to_dict( Parameters ---------- encoding : _ENCODING, default="MSB" - Encoding for the basis kets. See `Notes` for additional information. + Encoding for the basis kets. See notes for additional information. rel_tol : float, default=0.0 Relative tolerance used when deciding whether a coefficient should be @@ -469,8 +469,8 @@ def to_dict( |\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 -> `q2q1q1`. This is the default representation in other software packages such as Qiskit. + 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 -> ``q2q1q1``. This is the default representation in other software packages such as Qiskit. Example ------- From f178b332d6339547e8a5c4a95e676f478522f07e Mon Sep 17 00:00:00 2001 From: matulni Date: Fri, 6 Mar 2026 09:21:22 +0100 Subject: [PATCH 07/12] Fix docs again --- graphix/sim/statevec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index 87bd3557..d13b840e 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -469,8 +469,8 @@ def to_dict( |\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 -> ``q2q1q1``. This is the default representation in other software packages such as Qiskit. + 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 -> ``q2q1q1``\. This is the default representation in other software packages such as Qiskit. Example ------- From 35b39c5fb492409cad63a87d597dfcf9b81019bc Mon Sep 17 00:00:00 2001 From: matulni Date: Fri, 6 Mar 2026 14:25:23 +0100 Subject: [PATCH 08/12] Fix docstrings --- graphix/sim/statevec.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index d13b840e..7c808a55 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -467,10 +467,10 @@ def to_dict( .. math:: - |\psi\rangle = q_0 \otimes q_1 \otimes q_2. + \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 -> ``q2q1q1``\. This is the default representation in other software packages such as Qiskit. + 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 -> ``q2q1q1``. This is the default representation in other software packages such as Qiskit. Example ------- From 06da72ec386c70a857753bce0cd183c5ae5feb98 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 10 Mar 2026 09:05:53 +0100 Subject: [PATCH 09/12] Add review suggestions --- graphix/sim/statevec.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index 7c808a55..86c7f66e 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -8,7 +8,7 @@ import math from collections.abc import Iterable from dataclasses import dataclass -from typing import TYPE_CHECKING, Literal, SupportsComplex, SupportsFloat +from typing import TYPE_CHECKING, SupportsComplex, SupportsFloat import numpy as np import numpy.typing as npt @@ -21,11 +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"] + _ENCODING = Literal["LSB", "MSB"] + CZ_TENSOR = np.array( [[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], [[[0, 0], [1, 0]], [[0, 0], [0, -1]]]], @@ -442,7 +444,7 @@ def to_dict( Parameters ---------- - encoding : _ENCODING, default="MSB" + encoding : Literal["LSB", "MSB"], default="MSB" Encoding for the basis kets. See notes for additional information. rel_tol : float, default=0.0 @@ -470,7 +472,7 @@ def to_dict( \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 -> ``q2q1q1``. This is the default representation in other software packages such as Qiskit. + 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 ------- @@ -506,7 +508,7 @@ def to_prob_dict( Parameters ---------- - encoding: _ENCODING, default="MSB" + encoding: Literal["LSB", "MSB"], default="MSB" Encoding for the basis kets. See :meth:`to_dict` for additional information. rel_tol : float, default=0.0 From 788fc756a5b73fb7d2394cbcd5ee632fb0641ab0 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 10 Mar 2026 14:16:53 +0100 Subject: [PATCH 10/12] Refactor to use numpy vectorization --- graphix/sim/statevec.py | 58 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index 86c7f66e..f9df8533 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -434,25 +434,24 @@ def to_dict( self, encoding: _ENCODING = "MSB", *, - rel_tol: float = 0.0, - abs_tol: float = 1e-8, + 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. + 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. - rel_tol : float, default=0.0 + 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. - abs_tol : float, default=1e-8 + 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. @@ -484,39 +483,29 @@ def to_dict( >>> 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] - def format_encoding(i: int) -> str: - display_width = self.nqubit - output = f"{i:0{display_width}b}" - if encoding == "LSB": - return output[::-1] - return output - - return { - format_encoding(i): amp - for i, amp in enumerate(self.flatten()) - if not math.isclose(abs(amp), 0, rel_tol=rel_tol, abs_tol=abs_tol) - } - - def to_prob_dict( - self, encoding: _ENCODING = "MSB", *, rel_tol: float = 0.0, abs_tol: float = 1e-8 - ) -> dict[str, float]: + 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. + 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. - rel_tol : float, default=0.0 + 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. - abs_tol : float, default=1e-8 + 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. @@ -530,9 +519,11 @@ def to_prob_dict( -------- .. :meth:`to_dict` """ - return { - key: float(abs(amp) ** 2) for key, amp in self.to_dict(encoding, rel_tol=rel_tol, abs_tol=abs_tol).items() - } + 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.""" @@ -573,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 From bbb0a627d525be5a5a9dda98a56dd1cf9fada034 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:27:58 +0100 Subject: [PATCH 11/12] Bump the python-packages group with 2 updates (#463) * Bump the python-packages group with 2 updates Bumps the python-packages group with 2 updates: [ruff](https://github.com/astral-sh/ruff) and [types-networkx](https://github.com/typeshed-internal/stub_uploader). Updates `ruff` from 0.15.4 to 0.15.5 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.15.4...0.15.5) Updates `types-networkx` from 3.6.1.20260210 to 3.6.1.20260303 - [Commits](https://github.com/typeshed-internal/stub_uploader/commits) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: python-packages - dependency-name: types-networkx dependency-version: 3.6.1.20260303 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: python-packages ... Signed-off-by: dependabot[bot] * fixing ruff errors * fixing ruff errors --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Emlyn Graham Co-authored-by: Emlyn <42484330+emlynsg@users.noreply.github.com> --- graphix/flow/_find_gpflow.py | 23 ++++++++++++----------- graphix/flow/core.py | 26 +++++++++++++------------- graphix/opengraph.py | 8 ++++---- requirements-dev.txt | 4 ++-- 4 files changed, 31 insertions(+), 30 deletions(-) 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/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 From b9d84bb231ee28f1757737a1c92f1ec850819834 Mon Sep 17 00:00:00 2001 From: matulni Date: Tue, 10 Mar 2026 14:19:47 +0100 Subject: [PATCH 12/12] Up changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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