From 8da55e0fbb24b88ded26d820ce7d13dcea358245 Mon Sep 17 00:00:00 2001 From: Tom Peham Date: Wed, 4 Sep 2024 12:01:31 +0200 Subject: [PATCH 1/8] Added Stabilizer codes --- src/mqt/qecc/__init__.py | 3 ++- src/mqt/qecc/codes/__init__.py | 2 ++ test/python/test_code.py | 44 +++++++++++++++++++++++++++------- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/mqt/qecc/__init__.py b/src/mqt/qecc/__init__.py index 5656e7b4..3600f1ac 100644 --- a/src/mqt/qecc/__init__.py +++ b/src/mqt/qecc/__init__.py @@ -9,7 +9,7 @@ from ._version import version as __version__ from .analog_information_decoding.simulators.analog_tannergraph_decoding import AnalogTannergraphDecoder, AtdSimulator from .analog_information_decoding.simulators.quasi_single_shot_v2 import QssSimulator -from .codes import CSSCode, InvalidCSSCodeError +from .codes import CSSCode, InvalidCSSCodeError, StabilizerCode from .pyqecc import ( Code, Decoder, @@ -37,6 +37,7 @@ "InvalidCSSCodeError", # "SoftInfoDecoder", "QssSimulator", + "StabilizerCode", "UFDecoder", "UFHeuristic", "__version__", diff --git a/src/mqt/qecc/codes/__init__.py b/src/mqt/qecc/codes/__init__.py index e2af951e..82e3f47a 100644 --- a/src/mqt/qecc/codes/__init__.py +++ b/src/mqt/qecc/codes/__init__.py @@ -7,6 +7,7 @@ from .css_code import CSSCode, InvalidCSSCodeError from .hexagonal_color_code import HexagonalColorCode from .square_octagon_color_code import SquareOctagonColorCode +from .stabilizer_code import StabilizerCode __all__ = [ "CSSCode", @@ -15,5 +16,6 @@ "InvalidCSSCodeError", "LatticeType", "SquareOctagonColorCode", + "StabilizerCode", "construct_bb_code", ] diff --git a/test/python/test_code.py b/test/python/test_code.py index 8f887a67..f365f755 100644 --- a/test/python/test_code.py +++ b/test/python/test_code.py @@ -7,7 +7,7 @@ import numpy as np import pytest -from mqt.qecc import CSSCode, InvalidCSSCodeError +from mqt.qecc import CSSCode, InvalidCSSCodeError, StabilizerCode from mqt.qecc.codes import construct_bb_code if TYPE_CHECKING: # pragma: no cover @@ -15,7 +15,7 @@ @pytest.fixture -def rep_code() -> tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8] | None]: +def rep_code_checks() -> tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8] | None]: """Return the parity check matrices for the repetition code.""" hx = np.array([[1, 1, 0], [0, 0, 1]]) hz = None @@ -23,13 +23,19 @@ def rep_code() -> tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8] | None @pytest.fixture -def steane_code() -> tuple[npt.NDArray[np.int8], npt.NDArray[np.int8]]: +def steane_code_checks() -> tuple[npt.NDArray[np.int8], npt.NDArray[np.int8]]: """Return the check matrices for the Steane code.""" hx = np.array([[1, 1, 1, 1, 0, 0, 0], [1, 0, 1, 0, 1, 0, 1], [0, 1, 1, 0, 1, 1, 0]]) hz = hx return hx, hz +@pytest.fixture +def five_qubit_code_stabs() -> list[str]: + """Return the five qubit code.""" + return ["XZZXI", "IXZZX", "XIXZZ", "ZXIXZ"] + + def test_invalid_css_codes() -> None: """Test that an invalid CSS code raises an error.""" # Violates CSS condition @@ -57,7 +63,7 @@ def test_invalid_css_codes() -> None: CSSCode(distance=3) -@pytest.mark.parametrize("checks", ["steane_code", "rep_code"]) +@pytest.mark.parametrize("checks", ["steane_code_checks", "rep_code_checks"]) def test_logicals(checks: tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8] | None], request) -> None: # type: ignore[no-untyped-def] """Test the logical operators of the CSSCode class.""" hx, hz = request.getfixturevalue(checks) @@ -77,9 +83,9 @@ def test_logicals(checks: tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8 assert np.all(code.Lz @ code.Hx.T % 2 == 0) -def test_errors(steane_code: tuple[npt.NDArray[np.int8], npt.NDArray[np.int8]]) -> None: +def test_errors(steane_code_checks: tuple[npt.NDArray[np.int8], npt.NDArray[np.int8]]) -> None: """Test error detection and symdromes.""" - hx, hz = steane_code + hx, hz = steane_code_checks code = CSSCode(distance=3, Hx=hx, Hz=hz) e1 = np.array([1, 0, 0, 0, 0, 0, 0]) e2 = np.array([0, 1, 0, 0, 1, 0, 0]) @@ -111,9 +117,9 @@ def test_errors(steane_code: tuple[npt.NDArray[np.int8], npt.NDArray[np.int8]]) assert code.stabilizer_eq_z_error(e1, e4) -def test_steane(steane_code: tuple[npt.NDArray[np.int8], npt.NDArray[np.int8]]) -> None: +def test_steane(steane_code_checks: tuple[npt.NDArray[np.int8], npt.NDArray[np.int8]]) -> None: """Test utility functions and correctness of the Steane code.""" - hx, hz = steane_code + hx, hz = steane_code_checks code = CSSCode(distance=3, Hx=hx, Hz=hz) assert code.n == 7 assert code.k == 1 @@ -147,3 +153,25 @@ def test_bb_codes(n: int) -> None: assert code.Hx is not None assert code.Hz is not None assert np.all(code.Hx @ code.Hx.T % 2) == 0 + + +def test_five_qubit_code(five_qubit_code_stabs: list[str]) -> None: + """Test that the five qubit code is constructed as a valid stabilizer code.""" + Lz = ["ZZZZZ"] # noqa: N806 + Lx = ["XXXXX"] # noqa: N806 + + # Many assertions are already made in the constructor + code = StabilizerCode(five_qubit_code_stabs, distance=3, Lx=Lx, Lz=Lz) + assert code.n == 5 + assert code.k == 1 + assert code.distance == 3 + + error = "XIIII" + syndrome = code.get_syndrome(error) + assert np.array_equal(syndrome, np.array([0, 0, 0, 1])) + + stabilizer_eq_error = "IZZXI" + assert code.stabilizer_equivalent(error, stabilizer_eq_error) + + different_error = "IZIII" + assert not code.stabilizer_equivalent(error, different_error) From ef8e85a773ff7aa0ac0e772bd29e24f6623620de Mon Sep 17 00:00:00 2001 From: Tom Peham Date: Wed, 4 Sep 2024 12:38:38 +0200 Subject: [PATCH 2/8] Added missing class --- src/mqt/qecc/codes/stabilizer_code.py | 228 ++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 src/mqt/qecc/codes/stabilizer_code.py diff --git a/src/mqt/qecc/codes/stabilizer_code.py b/src/mqt/qecc/codes/stabilizer_code.py new file mode 100644 index 00000000..b531feaf --- /dev/null +++ b/src/mqt/qecc/codes/stabilizer_code.py @@ -0,0 +1,228 @@ +"""Class for representing general stabilizer codes.""" + +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING + +import numpy as np +import numpy.typing as npt + +if sys.version_info >= (3, 10): + from typing import TypeAlias + + Pauli: TypeAlias = npt.NDArray[np.int8] | list[str] +else: + from typing import Union + + from typing_extensions import TypeAlias + + Pauli: TypeAlias = Union[npt.NDArray[np.int8], list[str]] + +from ldpc import mod2 + +if TYPE_CHECKING: + from collections.abc import Iterable + + +class StabilizerCode: + """A class for representing stabilizer codes.""" + + def __init__( + self, + generators: npt.NDArray | list[str], + distance: int | None = None, + Lz: Pauli | None = None, # noqa: N803 + Lx: Pauli | None = None, # noqa: N803 + ) -> None: + """Initialize the code. + + Args: + generators: The stabilizer generators of the code. Qiskit has a reverse order of qubits in PauliList. We assume that stabilizers are ordered from left to right in ascending order of qubits. + distance: The distance of the code. + Lz: The logical Z-operators. + Lx: The logical X-operators. + """ + if len(generators) == 0: + msg = "Stabilizer code must have at least one generator." + raise InvalidStabilizerCodeError(msg) + + self.generators = paulis_to_binary(generators) + self.n = get_n_qubits_from_pauli(self.generators[0]) + self.symplectic_matrix = self.generators[:, :-1] # discard the phase + self.phases = self.generators[:, -1] + self.k = self.n - mod2.rank(self.generators) + self.distance = distance + + if Lz is not None: + self.Lz = paulis_to_binary(Lz) + self.Lz_symplectic = self.Lz[:, :-1] + else: + self.Lz = None + self.Lz_symplectic = None + + if Lx is not None: + self.Lx = paulis_to_binary(Lx) + self.Lx_symplectic = self.Lx[:, :-1] + else: + self.Lx = None + self.Lx_symplectic = None + + self._check_code_correct() + + def __hash__(self) -> int: + """Compute a hash for the stabilizer code.""" + return hash(int.from_bytes(self.generators.tobytes(), sys.byteorder)) + + def __eq__(self, other: object) -> bool: + """Check if two stabilizer codes are equal.""" + if not isinstance(other, StabilizerCode): + return NotImplemented + rnk = mod2.rank(self.generators) + return bool( + rnk == mod2.rank(other.generators) and rnk == mod2.rank(np.vstack((self.generators, other.generators))) + ) + + def get_syndrome(self, error: Pauli) -> npt.NDArray: + """Compute the syndrome of the error. + + Args: + error: The error as a pauli string or binary vector. + """ + return symplectic_matrix_mul(self.symplectic_matrix, pauli_to_symplectic_vec(error)) + + def stabs_as_pauli_strings(self) -> list[str]: + """Return the stabilizers as Pauli strings.""" + return [binary_to_pauli_string(s) for s in self.generators] + + def stabilizer_equivalent(self, p1: Pauli, p2: Pauli) -> bool: + """Check if two Pauli strings are equivalent up to stabilizers of the code.""" + v1 = pauli_to_binary(p1) + v2 = pauli_to_binary(p2) + return bool(mod2.rank(np.vstack((self.generators, v1, v2))) == mod2.rank(np.vstack((self.generators, v1)))) + + def _check_code_correct(self) -> None: + """Check if the code is correct. Throws an exception if not.""" + if self.distance is not None and self.distance <= 0: + msg = "Distance must be a positive integer." + raise InvalidStabilizerCodeError(msg) + + if self.Lz is not None or self.Lx is not None: + if self.Lz is None: + msg = "If logical X-operators are given, logical Z-operators must also be given." + raise InvalidStabilizerCodeError(msg) + if self.Lx is None: + msg = "If logical Z-operators are given, logical X-operators must also be given." + raise InvalidStabilizerCodeError(msg) + + if self.Lz is None: + return + + if get_n_qubits_from_pauli(self.Lz[0]) != self.n: + msg = "Logical operators must have the same number of qubits as the stabilizer generators." + raise InvalidStabilizerCodeError(msg) + + if self.Lz.shape[0] != self.k: + msg = "Number of logical Z-operators must be equal to the number of logical qubits." + raise InvalidStabilizerCodeError(msg) + + if get_n_qubits_from_pauli(self.Lz[0]) != self.n: + msg = "Logical operators must have the same number of qubits as the stabilizer generators." + raise InvalidStabilizerCodeError(msg) + + if self.Lx.shape[0] != self.k: + msg = "Number of logical X-operators must be equal to the number of logical qubits." + raise InvalidStabilizerCodeError(msg) + + if not all_commute(self.Lz_symplectic, self.symplectic_matrix): + msg = "Logical Z-operators must anti-commute with the stabilizer generators." + raise InvalidStabilizerCodeError(msg) + if not all_commute(self.Lx_symplectic, self.symplectic_matrix): + msg = "Logical X-operators must commute with the stabilizer generators." + raise InvalidStabilizerCodeError(msg) + + commutations = symplectic_matrix_product(self.Lz_symplectic, self.Lx_symplectic) + if not np.all(np.sum(commutations, axis=1) == 1): + msg = "Every logical X-operator must anti-commute with exactly one logical Z-operator." + raise InvalidStabilizerCodeError(msg) + + +def pauli_to_binary(p: Pauli) -> npt.NDArray: + """Convert a Pauli string to a binary array.""" + if isinstance(p, np.ndarray) and p.dtype == np.int8: + return p + + # check if there is a sign + phase = 0 + if p[0] in {"+", "-"}: + phase = 0 if p[0] == "+" else 1 + x_part = np.array([int(p == "X") for p in p]) + z_part = np.array([int(p == "Z") for p in p]) + y_part = np.array([int(p == "Y") for p in p]) + x_part += y_part + z_part += y_part + return np.hstack((x_part, z_part, np.array([phase]))) + + +def paulis_to_binary(ps: Iterable[Pauli]) -> npt.NDArray: + """Convert a list of Pauli strings to a 2d binary array.""" + return np.array([pauli_to_binary(p) for p in ps]) + + +def binary_to_pauli_string(b: npt.NDArray) -> str: + """Convert a binary array to a Pauli string.""" + x_part = b[: len(b) // 2] + z_part = b[len(b) // 2 : -1] + phase = b[-1] + + pauli = ["X" if x and not z else "Z" if z and not x else "Y" if x and z else "I" for x, z in zip(x_part, z_part)] + return f"{'+' if phase == 1 else '-'}" + "".join(pauli) + + +def get_n_qubits_from_pauli(p: Pauli) -> int: + """Get the number of qubits from a Pauli string.""" + if isinstance(p, np.ndarray): + return len(p) // 2 + return len(p) + + +def commute(p1: npt.NDArray[np.int8], p2: npt.NDArray[np.int8]) -> bool: + """Check if two Paulistrings in binary representation commute.""" + return bool(symplectic_inner_product(p1, p2) == 0) + + +def anti_commute(p1: npt.NDArray[np.int8], p2: npt.NDArray[np.int8]) -> bool: + """Check if two Paulistrings in binary representation anti-commute.""" + return not commute(p1, p2) + + +def all_commute(ps1: npt.NDArray[np.int8], ps2: npt.NDArray[np.int8]) -> bool: + """Check if all Paulistrings in binary representation commute.""" + return bool((symplectic_matrix_product(ps1, ps2) == 0).all()) + + +def symplectic_inner_product(p1: npt.NDArray[np.int8], p2: npt.NDArray[np.int8]) -> int: + """Compute the symplectic inner product of two symplectic vectors.""" + n = p1.shape[0] // 2 + return int((p1[:n] @ p2[n:] + p1[n:] @ p2[:n]) % 2) + + +def symplectic_matrix_product(m1: npt.NDArray[np.int8], m2: npt.NDArray[np.int8]) -> npt.NDArray[np.int8]: + """Compute the symplectic matrix product of two symplectic matrices.""" + n = m1.shape[1] // 2 + return ((m1[:, :n] @ m2[:, n:].T) + (m1[:, n:] @ m2[:, :n].T)) % 2 + + +def symplectic_matrix_mul(m: npt.NDArray[np.int8], v: npt.NDArray[np.int8]) -> npt.NDArray[np.int8]: + """Compute the symplectic matrix product of symplectic matrix with symplectic vector.""" + n = m.shape[1] // 2 + return (m[:, :n] @ v[n:] + m[:, n:] @ v[:n]) % 2 + + +def pauli_to_symplectic_vec(p: Pauli) -> npt.NDArray: + """Convert a Pauli string to a symplectic vector.""" + return pauli_to_binary(p)[:-1] + + +class InvalidStabilizerCodeError(ValueError): + """Raised when the stabilizer code is invalid.""" From 85a9ff65869a8a672ba87735cea889c01588c6f7 Mon Sep 17 00:00:00 2001 From: Tom Peham Date: Wed, 4 Sep 2024 15:05:54 +0200 Subject: [PATCH 3/8] Adapted CSS codes to the new class --- src/mqt/qecc/__init__.py | 5 +- src/mqt/qecc/codes/__init__.py | 3 +- src/mqt/qecc/codes/css_code.py | 115 ++++++++---------- src/mqt/qecc/codes/hexagonal_color_code.py | 4 +- .../qecc/codes/square_octagon_color_code.py | 4 +- src/mqt/qecc/codes/stabilizer_code.py | 30 ++++- test/python/test_code.py | 63 ++++++++-- 7 files changed, 142 insertions(+), 82 deletions(-) diff --git a/src/mqt/qecc/__init__.py b/src/mqt/qecc/__init__.py index 3600f1ac..807561cd 100644 --- a/src/mqt/qecc/__init__.py +++ b/src/mqt/qecc/__init__.py @@ -9,7 +9,7 @@ from ._version import version as __version__ from .analog_information_decoding.simulators.analog_tannergraph_decoding import AnalogTannergraphDecoder, AtdSimulator from .analog_information_decoding.simulators.quasi_single_shot_v2 import QssSimulator -from .codes import CSSCode, InvalidCSSCodeError, StabilizerCode +from .codes import CSSCode, StabilizerCode from .pyqecc import ( Code, Decoder, @@ -33,9 +33,6 @@ "DecodingResultStatus", "DecodingRunInformation", "GrowthVariant", - "InvalidCSSCodeError", - "InvalidCSSCodeError", - # "SoftInfoDecoder", "QssSimulator", "StabilizerCode", "UFDecoder", diff --git a/src/mqt/qecc/codes/__init__.py b/src/mqt/qecc/codes/__init__.py index 82e3f47a..dc4c6768 100644 --- a/src/mqt/qecc/codes/__init__.py +++ b/src/mqt/qecc/codes/__init__.py @@ -7,13 +7,14 @@ from .css_code import CSSCode, InvalidCSSCodeError from .hexagonal_color_code import HexagonalColorCode from .square_octagon_color_code import SquareOctagonColorCode -from .stabilizer_code import StabilizerCode +from .stabilizer_code import InvalidStabilizerCodeError, StabilizerCode __all__ = [ "CSSCode", "ColorCode", "HexagonalColorCode", "InvalidCSSCodeError", + "InvalidStabilizerCodeError", "LatticeType", "SquareOctagonColorCode", "StabilizerCode", diff --git a/src/mqt/qecc/codes/css_code.py b/src/mqt/qecc/codes/css_code.py index ae144d99..7e222b15 100644 --- a/src/mqt/qecc/codes/css_code.py +++ b/src/mqt/qecc/codes/css_code.py @@ -2,18 +2,19 @@ from __future__ import annotations -import sys from pathlib import Path from typing import TYPE_CHECKING import numpy as np from ldpc import mod2 +from .stabilizer_code import StabilizerCode + if TYPE_CHECKING: # pragma: no cover import numpy.typing as npt -class CSSCode: +class CSSCode(StabilizerCode): """A class for representing CSS codes.""" def __init__( @@ -25,64 +26,55 @@ def __init__( z_distance: int | None = None, ) -> None: """Initialize the code.""" + self._check_valid_check_matrices(Hx, Hz) + + if Hx is None: + assert Hz is not None + self.n = Hz.shape[1] + self.Hx = np.zeros((0, self.n), dtype=np.int8) + else: + self.Hx = Hx + if Hz is None: + assert Hx is not None + self.n = Hx.shape[1] + self.Hz = np.zeros((0, self.n), dtype=np.int8) + else: + self.Hz = Hz + + z_padding = np.zeros(self.Hx.shape, dtype=np.int8) + x_padding = np.zeros(self.Hz.shape, dtype=np.int8) + + x_padded = np.hstack([self.Hx, z_padding]) + z_padded = np.hstack([x_padding, self.Hz]) + phases = np.zeros((x_padded.shape[0] + z_padded.shape[0], 1), dtype=np.int8) + super().__init__(np.hstack((np.vstack((x_padded, z_padded)), phases)), distance) + self.distance = distance self.x_distance = x_distance if x_distance is not None else distance self.z_distance = z_distance if z_distance is not None else distance - if self.distance < 0: - msg = "The distance must be a non-negative integer" - raise InvalidCSSCodeError(msg) - if Hx is None and Hz is None: - msg = "At least one of the check matrices must be provided" - raise InvalidCSSCodeError(msg) if self.x_distance < self.distance or self.z_distance < self.distance: msg = "The x and z distances must be greater than or equal to the distance" raise InvalidCSSCodeError(msg) - if Hx is not None and Hz is not None: - if Hx.shape[1] != Hz.shape[1]: - msg = "Check matrices must have the same number of columns" - raise InvalidCSSCodeError(msg) - if np.any(Hx @ Hz.T % 2 != 0): - msg = "The check matrices must be orthogonal" - raise InvalidCSSCodeError(msg) - self.Hx = Hx - self.Hz = Hz - self.n = Hx.shape[1] if Hx is not None else Hz.shape[1] # type: ignore[union-attr] - self.k = self.n - (Hx.shape[0] if Hx is not None else 0) - (Hz.shape[0] if Hz is not None else 0) self.Lx = CSSCode._compute_logical(self.Hz, self.Hx) self.Lz = CSSCode._compute_logical(self.Hx, self.Hz) - def __hash__(self) -> int: - """Compute a hash for the CSS code.""" - x_hash = int.from_bytes(self.Hx.tobytes(), sys.byteorder) if self.Hx is not None else 0 - z_hash = int.from_bytes(self.Hz.tobytes(), sys.byteorder) if self.Hz is not None else 0 - return hash(x_hash ^ z_hash) - - def __eq__(self, other: object) -> bool: - """Check if two CSS codes are equal.""" - if not isinstance(other, CSSCode): - return NotImplemented - if self.Hx is None and other.Hx is None: - assert self.Hz is not None - assert other.Hz is not None - return np.array_equal(self.Hz, other.Hz) - if self.Hz is None and other.Hz is None: - assert self.Hx is not None - assert other.Hx is not None - return np.array_equal(self.Hx, other.Hx) - if (self.Hx is None and other.Hx is not None) or (self.Hx is not None and other.Hx is None): - return False - if (self.Hz is None and other.Hz is not None) or (self.Hz is not None and other.Hz is None): - return False - assert self.Hx is not None - assert other.Hx is not None - assert self.Hz is not None - assert other.Hz is not None - return bool( - mod2.rank(self.Hx) == mod2.rank(np.vstack([self.Hx, other.Hx])) - and mod2.rank(self.Hz) == mod2.rank(np.vstack([self.Hz, other.Hz])) - ) + def x_checks_as_pauli_strings(self) -> list[str]: + """Return the x checks as Pauli strings.""" + return ["".join("X" if bit == 1 else "I" for bit in row) for row in self.Hx] + + def z_checks_as_pauli_strings(self) -> list[str]: + """Return the z checks as Pauli strings.""" + return ["".join("Z" if bit == 1 else "I" for bit in row) for row in self.Hz] + + def x_logicals_as_pauli_strings(self) -> list[str]: + """Return the x logicals as a Pauli strings.""" + return ["".join("X" if bit == 1 else "I" for bit in row) for row in self.Lx] + + def z_logicals_as_pauli_strings(self) -> list[str]: + """Return the z logicals as Pauli strings.""" + return ["".join("Z" if bit == 1 else "I" for bit in row) for row in self.Lz] @staticmethod def _compute_logical(m1: npt.NDArray[np.int8] | None, m2: npt.NDArray[np.int8] | None) -> npt.NDArray[np.int8]: @@ -161,19 +153,20 @@ def is_self_dual(self) -> bool: self.Hx.shape[0] == self.Hz.shape[0] and mod2.rank(self.Hx) == mod2.rank(np.vstack([self.Hx, self.Hz])) ) - def stabs_as_pauli_strings(self) -> tuple[list[str] | None, list[str] | None]: - """Return the stabilizers as Pauli strings.""" - x_str = None if self.Hx is None else ["".join(["I" if x == 0 else "X" for x in row]) for row in self.Hx] - z_str = None if self.Hz is None else ["".join(["I" if z == 0 else "Z" for z in row]) for row in self.Hz] - return x_str, z_str - - def z_logicals_as_pauli_string(self) -> str: - """Return the logical Z operator as a Pauli string.""" - return "".join(["I" if z == 0 else "Z" for z in self.Lx[0]]) + @staticmethod + def _check_valid_check_matrices(Hx: npt.NDArray[np.int8] | None, Hz: npt.NDArray[np.int8] | None) -> None: # noqa: N803 + """Check if the code is a valid CSS code.""" + if Hx is None and Hz is None: + msg = "At least one of the check matrices must be provided" + raise InvalidCSSCodeError(msg) - def x_logicals_as_pauli_string(self) -> str: - """Return the logical X operator as a Pauli string.""" - return "".join(["I" if x == 0 else "X" for x in self.Lz[0]]) + if Hx is not None and Hz is not None: + if Hx.shape[1] != Hz.shape[1]: + msg = "Check matrices must have the same number of columns" + raise InvalidCSSCodeError(msg) + if np.any(Hx @ Hz.T % 2 != 0): + msg = "The check matrices must be orthogonal" + raise InvalidCSSCodeError(msg) @staticmethod def from_code_name(code_name: str, distance: int | None = None) -> CSSCode: diff --git a/src/mqt/qecc/codes/hexagonal_color_code.py b/src/mqt/qecc/codes/hexagonal_color_code.py index 888e72dd..f86f22bb 100644 --- a/src/mqt/qecc/codes/hexagonal_color_code.py +++ b/src/mqt/qecc/codes/hexagonal_color_code.py @@ -14,12 +14,14 @@ class HexagonalColorCode(ColorCode): def __init__(self, distance: int) -> None: """Hexagonal Color Code initialization from base class.""" - ColorCode.__init__(self, distance=distance, lattice_type=LatticeType.HEXAGON) + super().__init__(distance=distance, lattice_type=LatticeType.HEXAGON) + assert self.distance is not None def add_qubits(self) -> None: """Add qubits to the code.""" colour = ["r", "b", "g"] y = 0 + assert self.distance is not None x_max = self.distance + self.distance // 2 while x_max > 0: ancilla_colour = colour[y % 3] diff --git a/src/mqt/qecc/codes/square_octagon_color_code.py b/src/mqt/qecc/codes/square_octagon_color_code.py index 33ba6ffc..5c7f61aa 100644 --- a/src/mqt/qecc/codes/square_octagon_color_code.py +++ b/src/mqt/qecc/codes/square_octagon_color_code.py @@ -19,12 +19,13 @@ def __init__(self, distance: int) -> None: # additionally to ancilla_qubits (on squares) we have the ones on octagons self.octagon_ancilla_qubits: set[tuple[int, int]] = set() self.square_ancilla_qubits: set[tuple[int, int]] = set() - ColorCode.__init__(self, distance=distance, lattice_type=LatticeType.SQUARE_OCTAGON) + super().__init__(distance=distance, lattice_type=LatticeType.SQUARE_OCTAGON) def add_qubits(self) -> None: """Add qubits to the code.""" self.bottom_row_ancillas() y = 1 + assert self.distance is not None x_max = self.distance while y <= (self.distance + self.distance // 2): @@ -112,6 +113,7 @@ def odd_data_qubit_row(self, x_max: int, y: int) -> None: def bottom_row_ancillas(self) -> None: """Create ancilla qubits on the bottom row of the lattice.""" + assert self.distance is not None for x in range(4, self.distance // 2 * 6, 6): self.octagon_ancilla_qubits.add((x, 0)) diff --git a/src/mqt/qecc/codes/stabilizer_code.py b/src/mqt/qecc/codes/stabilizer_code.py index b531feaf..a3a4cc49 100644 --- a/src/mqt/qecc/codes/stabilizer_code.py +++ b/src/mqt/qecc/codes/stabilizer_code.py @@ -43,9 +43,7 @@ def __init__( Lz: The logical Z-operators. Lx: The logical X-operators. """ - if len(generators) == 0: - msg = "Stabilizer code must have at least one generator." - raise InvalidStabilizerCodeError(msg) + self._check_stabilizer_generators(generators) self.generators = paulis_to_binary(generators) self.n = get_n_qubits_from_pauli(self.generators[0]) @@ -101,6 +99,23 @@ def stabilizer_equivalent(self, p1: Pauli, p2: Pauli) -> bool: v2 = pauli_to_binary(p2) return bool(mod2.rank(np.vstack((self.generators, v1, v2))) == mod2.rank(np.vstack((self.generators, v1)))) + @staticmethod + def _check_stabilizer_generators(generators: npt.NDArray[np.int8] | list[str]) -> None: + """Check if the stabilizer generators are valid. Throws an exception if not.""" + if len(generators) == 0: + msg = "Stabilizer code must have at least one generator." + raise InvalidStabilizerCodeError(msg) + if not all(len(generators[0]) == len(g) for g in generators): + msg = "All stabilizer generators must have the same length." + raise InvalidStabilizerCodeError(msg) + + if not isinstance(generators[0], str): + return + + if not all(is_pauli_string(g) for g in generators): + msg = "When providing stabilizer generators as strings, they must be valid Pauli strings." + raise InvalidStabilizerCodeError(msg) + def _check_code_correct(self) -> None: """Check if the code is correct. Throws an exception if not.""" if self.distance is not None and self.distance <= 0: @@ -149,7 +164,7 @@ def _check_code_correct(self) -> None: def pauli_to_binary(p: Pauli) -> npt.NDArray: """Convert a Pauli string to a binary array.""" - if isinstance(p, np.ndarray) and p.dtype == np.int8: + if isinstance(p, np.ndarray): return p # check if there is a sign @@ -179,10 +194,15 @@ def binary_to_pauli_string(b: npt.NDArray) -> str: return f"{'+' if phase == 1 else '-'}" + "".join(pauli) +def is_pauli_string(p: str) -> bool: + """Check if a string is a valid Pauli string.""" + return len(p) > 0 and all(c in {"I", "X", "Y", "Z"} for c in p[1:]) and p[0] in {"+", "-", "I", "X", "Y", "Z"} + + def get_n_qubits_from_pauli(p: Pauli) -> int: """Get the number of qubits from a Pauli string.""" if isinstance(p, np.ndarray): - return len(p) // 2 + return int(p.shape[0] // 2) return len(p) diff --git a/test/python/test_code.py b/test/python/test_code.py index f365f755..0b55c2f6 100644 --- a/test/python/test_code.py +++ b/test/python/test_code.py @@ -7,8 +7,8 @@ import numpy as np import pytest -from mqt.qecc import CSSCode, InvalidCSSCodeError, StabilizerCode -from mqt.qecc.codes import construct_bb_code +from mqt.qecc import CSSCode, StabilizerCode +from mqt.qecc.codes import InvalidCSSCodeError, InvalidStabilizerCodeError, construct_bb_code if TYPE_CHECKING: # pragma: no cover import numpy.typing as npt @@ -54,10 +54,6 @@ def test_invalid_css_codes() -> None: with pytest.raises(InvalidCSSCodeError): CSSCode(distance=3, Hx=hx, Hz=hz) - # Invalid distance - with pytest.raises(InvalidCSSCodeError): - CSSCode(distance=-1, Hx=hx) - # Checks not provided with pytest.raises(InvalidCSSCodeError): CSSCode(distance=3) @@ -126,15 +122,16 @@ def test_steane(steane_code_checks: tuple[npt.NDArray[np.int8], npt.NDArray[np.i assert code.distance == 3 assert code.is_self_dual() - x_paulis, z_paulis = code.stabs_as_pauli_strings() + x_paulis = code.x_checks_as_pauli_strings() + z_paulis = code.z_checks_as_pauli_strings() assert x_paulis is not None assert z_paulis is not None assert len(x_paulis) == len(z_paulis) == 3 assert x_paulis == ["XXXXIII", "XIXIXIX", "IXXIXXI"] assert z_paulis == ["ZZZZIII", "ZIZIZIZ", "IZZIZZI"] - x_log = code.x_logicals_as_pauli_string() - z_log = code.z_logicals_as_pauli_string() + x_log = code.x_logicals_as_pauli_strings()[0] + z_log = code.z_logicals_as_pauli_strings()[0] assert x_log.count("X") == 3 assert x_log.count("I") == 4 assert z_log.count("Z") == 3 @@ -175,3 +172,51 @@ def test_five_qubit_code(five_qubit_code_stabs: list[str]) -> None: different_error = "IZIII" assert not code.stabilizer_equivalent(error, different_error) + + +def test_no_stabilizers() -> None: + """Test that an error is raised if no stabilizers are provided.""" + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode([]) + + +def test_different_length_stabilizers() -> None: + """Test that an error is raised if stabilizers have different lengths.""" + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ZZZZ", "X", "Y"]) + + +def test_invalid_pauli_strings() -> None: + """Test that invalid Pauli strings raise an error.""" + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ABCD", "XIXI", "YIYI"]) + + +def test_no_x_logical() -> None: + """Test that an error is raised if no X logical is provided when a Z logical is provided.""" + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ZZZZ", "XXXX"], Lz=["XXII"]) + + +def test_no_z_logical() -> None: + """Test that an error is raised if no Z logical is provided when an X logical is provided.""" + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ZZZZ", "XXXX"], Lx=["ZZII"]) + + +def test_logicals_wrong_length() -> None: + """Test that an error is raised if the logicals have the wrong length.""" + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ZZZZ", "XXXX"], Lx=["XX"], Lz=["ZZ"]) + + +def test_commuting_logicals() -> None: + """Test that an error is raised if the logicals commute.""" + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ZZZZ", "XXXX"], Lx=["ZZII"], Lz=["XXII"]) + + +def test_anticommuting_logicals() -> None: + """Test that an error is raised if the logicals anticommute with the stabilizer generators.""" + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ZZZZ", "XXXX"], Lx=["ZIII"], Lz=["XIII"]) From 0bca22d23200a206658afee08fe8e524640c5170 Mon Sep 17 00:00:00 2001 From: Tom Peham Date: Wed, 4 Sep 2024 16:14:28 +0200 Subject: [PATCH 4/8] Don't store distance as optional --- src/mqt/qecc/codes/hexagonal_color_code.py | 3 +-- src/mqt/qecc/codes/square_octagon_color_code.py | 2 -- src/mqt/qecc/codes/stabilizer_code.py | 11 ++++++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/mqt/qecc/codes/hexagonal_color_code.py b/src/mqt/qecc/codes/hexagonal_color_code.py index f86f22bb..8317a5a6 100644 --- a/src/mqt/qecc/codes/hexagonal_color_code.py +++ b/src/mqt/qecc/codes/hexagonal_color_code.py @@ -15,13 +15,12 @@ class HexagonalColorCode(ColorCode): def __init__(self, distance: int) -> None: """Hexagonal Color Code initialization from base class.""" super().__init__(distance=distance, lattice_type=LatticeType.HEXAGON) - assert self.distance is not None def add_qubits(self) -> None: """Add qubits to the code.""" colour = ["r", "b", "g"] y = 0 - assert self.distance is not None + x_max = self.distance + self.distance // 2 while x_max > 0: ancilla_colour = colour[y % 3] diff --git a/src/mqt/qecc/codes/square_octagon_color_code.py b/src/mqt/qecc/codes/square_octagon_color_code.py index 5c7f61aa..d62f232d 100644 --- a/src/mqt/qecc/codes/square_octagon_color_code.py +++ b/src/mqt/qecc/codes/square_octagon_color_code.py @@ -25,7 +25,6 @@ def add_qubits(self) -> None: """Add qubits to the code.""" self.bottom_row_ancillas() y = 1 - assert self.distance is not None x_max = self.distance while y <= (self.distance + self.distance // 2): @@ -113,7 +112,6 @@ def odd_data_qubit_row(self, x_max: int, y: int) -> None: def bottom_row_ancillas(self) -> None: """Create ancilla qubits on the bottom row of the lattice.""" - assert self.distance is not None for x in range(4, self.distance // 2 * 6, 6): self.octagon_ancilla_qubits.add((x, 0)) diff --git a/src/mqt/qecc/codes/stabilizer_code.py b/src/mqt/qecc/codes/stabilizer_code.py index a3a4cc49..46060995 100644 --- a/src/mqt/qecc/codes/stabilizer_code.py +++ b/src/mqt/qecc/codes/stabilizer_code.py @@ -50,7 +50,12 @@ def __init__( self.symplectic_matrix = self.generators[:, :-1] # discard the phase self.phases = self.generators[:, -1] self.k = self.n - mod2.rank(self.generators) - self.distance = distance + + if distance is not None and distance <= 0: + msg = "Distance must be a positive integer." + raise InvalidStabilizerCodeError(msg) + + self.distance = 1 if distance is None else distance # default distance is 1 if Lz is not None: self.Lz = paulis_to_binary(Lz) @@ -118,10 +123,6 @@ def _check_stabilizer_generators(generators: npt.NDArray[np.int8] | list[str]) - def _check_code_correct(self) -> None: """Check if the code is correct. Throws an exception if not.""" - if self.distance is not None and self.distance <= 0: - msg = "Distance must be a positive integer." - raise InvalidStabilizerCodeError(msg) - if self.Lz is not None or self.Lx is not None: if self.Lz is None: msg = "If logical X-operators are given, logical Z-operators must also be given." From 05f3af4d9268d3f03eb557108414a1bbb5858fe1 Mon Sep 17 00:00:00 2001 From: Tom Peham Date: Wed, 4 Sep 2024 16:31:38 +0200 Subject: [PATCH 5/8] Improve Coverage --- src/mqt/qecc/codes/stabilizer_code.py | 10 +++++----- test/python/test_code.py | 27 +++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/mqt/qecc/codes/stabilizer_code.py b/src/mqt/qecc/codes/stabilizer_code.py index 46060995..fc1d0e32 100644 --- a/src/mqt/qecc/codes/stabilizer_code.py +++ b/src/mqt/qecc/codes/stabilizer_code.py @@ -138,16 +138,16 @@ def _check_code_correct(self) -> None: msg = "Logical operators must have the same number of qubits as the stabilizer generators." raise InvalidStabilizerCodeError(msg) - if self.Lz.shape[0] != self.k: - msg = "Number of logical Z-operators must be equal to the number of logical qubits." + if self.Lz.shape[0] > self.k: + msg = "Number of logical Z-operators must be at most the number of logical qubits." raise InvalidStabilizerCodeError(msg) if get_n_qubits_from_pauli(self.Lz[0]) != self.n: msg = "Logical operators must have the same number of qubits as the stabilizer generators." raise InvalidStabilizerCodeError(msg) - if self.Lx.shape[0] != self.k: - msg = "Number of logical X-operators must be equal to the number of logical qubits." + if self.Lx.shape[0] > self.k: + msg = "Number of logical X-operators must be at most the number of logical qubits." raise InvalidStabilizerCodeError(msg) if not all_commute(self.Lz_symplectic, self.symplectic_matrix): @@ -192,7 +192,7 @@ def binary_to_pauli_string(b: npt.NDArray) -> str: phase = b[-1] pauli = ["X" if x and not z else "Z" if z and not x else "Y" if x and z else "I" for x, z in zip(x_part, z_part)] - return f"{'+' if phase == 1 else '-'}" + "".join(pauli) + return f"{'' if phase == 0 else '-'}" + "".join(pauli) def is_pauli_string(p: str) -> bool: diff --git a/test/python/test_code.py b/test/python/test_code.py index 0b55c2f6..cd202ef0 100644 --- a/test/python/test_code.py +++ b/test/python/test_code.py @@ -22,6 +22,14 @@ def rep_code_checks() -> tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8] return hx, hz +@pytest.fixture +def rep_code_checks_reverse() -> tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8] | None]: + """Return the parity check matrices for the repetition code.""" + hz = np.array([[1, 1, 0], [0, 0, 1]]) + hx = None + return hx, hz + + @pytest.fixture def steane_code_checks() -> tuple[npt.NDArray[np.int8], npt.NDArray[np.int8]]: """Return the check matrices for the Steane code.""" @@ -59,7 +67,7 @@ def test_invalid_css_codes() -> None: CSSCode(distance=3) -@pytest.mark.parametrize("checks", ["steane_code_checks", "rep_code_checks"]) +@pytest.mark.parametrize("checks", ["steane_code_checks", "rep_code_checks", "rep_code_checks_reverse"]) def test_logicals(checks: tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8] | None], request) -> None: # type: ignore[no-untyped-def] """Test the logical operators of the CSSCode class.""" hx, hz = request.getfixturevalue(checks) @@ -173,6 +181,9 @@ def test_five_qubit_code(five_qubit_code_stabs: list[str]) -> None: different_error = "IZIII" assert not code.stabilizer_equivalent(error, different_error) + strings = code.stabs_as_pauli_strings() + assert strings == five_qubit_code_stabs + def test_no_stabilizers() -> None: """Test that an error is raised if no stabilizers are provided.""" @@ -180,6 +191,12 @@ def test_no_stabilizers() -> None: StabilizerCode([]) +def test_negative_distance() -> None: + """Test that an error is raised if a negative distance is provided.""" + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ZZZZ", "XXXX"], distance=-1) + + def test_different_length_stabilizers() -> None: """Test that an error is raised if stabilizers have different lengths.""" with pytest.raises(InvalidStabilizerCodeError): @@ -219,4 +236,10 @@ def test_commuting_logicals() -> None: def test_anticommuting_logicals() -> None: """Test that an error is raised if the logicals anticommute with the stabilizer generators.""" with pytest.raises(InvalidStabilizerCodeError): - StabilizerCode(["ZZZZ", "XXXX"], Lx=["ZIII"], Lz=["XIII"]) + StabilizerCode(["ZZZZ", "XXXX"], Lx=["ZI II"], Lz=["XIII"]) + + +def test_too_many_logicals() -> None: + """Test that an error is raised if too many logicals are provided.""" + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ZZZZ", "XXXX"], Lx=["ZZII", "ZZII", "ZZII"], Lz=["XXII", "IIXX", "IIXX"]) From 444be7b28c36f6c64404fdad6d21d8002f3c5880 Mon Sep 17 00:00:00 2001 From: Tom Peham Date: Wed, 4 Sep 2024 17:04:32 +0200 Subject: [PATCH 6/8] Fixed test --- src/mqt/qecc/codes/css_code.py | 13 +------------ test/python/test_code.py | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/mqt/qecc/codes/css_code.py b/src/mqt/qecc/codes/css_code.py index 7e222b15..5373ba0b 100644 --- a/src/mqt/qecc/codes/css_code.py +++ b/src/mqt/qecc/codes/css_code.py @@ -77,19 +77,8 @@ def z_logicals_as_pauli_strings(self) -> list[str]: return ["".join("Z" if bit == 1 else "I" for bit in row) for row in self.Lz] @staticmethod - def _compute_logical(m1: npt.NDArray[np.int8] | None, m2: npt.NDArray[np.int8] | None) -> npt.NDArray[np.int8]: + def _compute_logical(m1: npt.NDArray[np.int8], m2: npt.NDArray[np.int8]) -> npt.NDArray[np.int8]: """Compute the logical matrix L.""" - if m1 is None: - ker_m2 = mod2.nullspace(m2) # compute the kernel basis of m2 - pivots = mod2.row_echelon(ker_m2)[-1] - logs = np.zeros_like(ker_m2, dtype=np.int8) # type: npt.NDArray[np.int8] - for i, pivot in enumerate(pivots): - logs[i, pivot] = 1 - return logs - - if m2 is None: - return mod2.nullspace(m1).astype(np.int8) - ker_m1 = mod2.nullspace(m1) # compute the kernel basis of m1 im_m2_transp = mod2.row_basis(m2) # compute the image basis of m2 log_stack = np.vstack([im_m2_transp, ker_m1]) diff --git a/test/python/test_code.py b/test/python/test_code.py index cd202ef0..aea189f9 100644 --- a/test/python/test_code.py +++ b/test/python/test_code.py @@ -74,7 +74,7 @@ def test_logicals(checks: tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8 code = CSSCode(distance=3, Hx=hx, Hz=hz) assert code.Lx is not None assert code.Lz is not None - assert code.Lx.shape[1] == code.Lz.shape[1] == hx.shape[1] + assert code.Lx.shape[1] == code.Lz.shape[1] == code.n assert code.Lx.shape[0] == code.Lz.shape[0] # assert that logicals anticommute From 607ab62537cc852de143b340ff4550289482a152 Mon Sep 17 00:00:00 2001 From: Tom Peham Date: Thu, 5 Sep 2024 09:23:22 +0200 Subject: [PATCH 7/8] More tests --- src/mqt/qecc/codes/stabilizer_code.py | 6 ++++-- test/python/test_code.py | 30 +++++++++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/mqt/qecc/codes/stabilizer_code.py b/src/mqt/qecc/codes/stabilizer_code.py index fc1d0e32..b8f59166 100644 --- a/src/mqt/qecc/codes/stabilizer_code.py +++ b/src/mqt/qecc/codes/stabilizer_code.py @@ -44,9 +44,8 @@ def __init__( Lx: The logical X-operators. """ self._check_stabilizer_generators(generators) - + self.n = get_n_qubits_from_pauli(generators[0]) self.generators = paulis_to_binary(generators) - self.n = get_n_qubits_from_pauli(self.generators[0]) self.symplectic_matrix = self.generators[:, :-1] # discard the phase self.phases = self.generators[:, -1] self.k = self.n - mod2.rank(self.generators) @@ -172,6 +171,7 @@ def pauli_to_binary(p: Pauli) -> npt.NDArray: phase = 0 if p[0] in {"+", "-"}: phase = 0 if p[0] == "+" else 1 + p = p[1:] x_part = np.array([int(p == "X") for p in p]) z_part = np.array([int(p == "Z") for p in p]) y_part = np.array([int(p == "Y") for p in p]) @@ -204,6 +204,8 @@ def get_n_qubits_from_pauli(p: Pauli) -> int: """Get the number of qubits from a Pauli string.""" if isinstance(p, np.ndarray): return int(p.shape[0] // 2) + if p[0] in {"+", "-"}: + return len(p) - 1 return len(p) diff --git a/test/python/test_code.py b/test/python/test_code.py index aea189f9..ad6a43a2 100644 --- a/test/python/test_code.py +++ b/test/python/test_code.py @@ -185,6 +185,18 @@ def test_five_qubit_code(five_qubit_code_stabs: list[str]) -> None: assert strings == five_qubit_code_stabs +def test_stabilizer_sign() -> None: + """Test that (negative) signs are correctly handled in stabilizer codes.""" + s = ["-ZZZZ", "-XXXX"] + code = StabilizerCode(s) + assert code.n == 4 + assert code.k == 2 + + error = "XIII" + syndrome = code.get_syndrome(error) + assert np.array_equal(syndrome, np.array([1, 0])) + + def test_no_stabilizers() -> None: """Test that an error is raised if no stabilizers are provided.""" with pytest.raises(InvalidStabilizerCodeError): @@ -212,34 +224,40 @@ def test_invalid_pauli_strings() -> None: def test_no_x_logical() -> None: """Test that an error is raised if no X logical is provided when a Z logical is provided.""" with pytest.raises(InvalidStabilizerCodeError): - StabilizerCode(["ZZZZ", "XXXX"], Lz=["XXII"]) + StabilizerCode(["ZZZZ", "XXXX"], Lx=["XXII"]) def test_no_z_logical() -> None: """Test that an error is raised if no Z logical is provided when an X logical is provided.""" with pytest.raises(InvalidStabilizerCodeError): - StabilizerCode(["ZZZZ", "XXXX"], Lx=["ZZII"]) + StabilizerCode(["ZZZZ", "XXXX"], Lz=["ZZII"]) def test_logicals_wrong_length() -> None: """Test that an error is raised if the logicals have the wrong length.""" with pytest.raises(InvalidStabilizerCodeError): - StabilizerCode(["ZZZZ", "XXXX"], Lx=["XX"], Lz=["ZZ"]) + StabilizerCode(["ZZZZ", "XXXX"], Lx=["XX"], Lz=["IZZI"]) + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ZZZZ", "XXXX"], Lx=["IXXI"], Lz=["ZZ"]) def test_commuting_logicals() -> None: """Test that an error is raised if the logicals commute.""" with pytest.raises(InvalidStabilizerCodeError): - StabilizerCode(["ZZZZ", "XXXX"], Lx=["ZZII"], Lz=["XXII"]) + StabilizerCode(["ZZZZ", "XXXX"], Lz=["ZZII"], Lx=["XXII"]) def test_anticommuting_logicals() -> None: """Test that an error is raised if the logicals anticommute with the stabilizer generators.""" with pytest.raises(InvalidStabilizerCodeError): - StabilizerCode(["ZZZZ", "XXXX"], Lx=["ZI II"], Lz=["XIII"]) + StabilizerCode(["ZZZZ", "XXXX"], Lz=["ZIII"], Lx=["IXXI"]) + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ZZZZ", "XXXX"], Lz=["IZZI"], Lx=["XIII"]) def test_too_many_logicals() -> None: """Test that an error is raised if too many logicals are provided.""" with pytest.raises(InvalidStabilizerCodeError): - StabilizerCode(["ZZZZ", "XXXX"], Lx=["ZZII", "ZZII", "ZZII"], Lz=["XXII", "IIXX", "IIXX"]) + StabilizerCode(["ZZZZ", "XXXX"], Lz=["ZZII", "ZZII", "ZZII"], Lx=["IXXI"]) + with pytest.raises(InvalidStabilizerCodeError): + StabilizerCode(["ZZZZ", "XXXX"], Lz=["IZZI"], Lx=["XXII", "XXII", "XXII"]) From cf2e5ebea3cf7987bc495c3a89d57195a371c1f9 Mon Sep 17 00:00:00 2001 From: Tom Peham Date: Thu, 5 Sep 2024 09:31:41 +0200 Subject: [PATCH 8/8] Fix check --- src/mqt/qecc/codes/stabilizer_code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mqt/qecc/codes/stabilizer_code.py b/src/mqt/qecc/codes/stabilizer_code.py index b8f59166..88832ea8 100644 --- a/src/mqt/qecc/codes/stabilizer_code.py +++ b/src/mqt/qecc/codes/stabilizer_code.py @@ -141,7 +141,7 @@ def _check_code_correct(self) -> None: msg = "Number of logical Z-operators must be at most the number of logical qubits." raise InvalidStabilizerCodeError(msg) - if get_n_qubits_from_pauli(self.Lz[0]) != self.n: + if get_n_qubits_from_pauli(self.Lx[0]) != self.n: msg = "Logical operators must have the same number of qubits as the stabilizer generators." raise InvalidStabilizerCodeError(msg)