From 7c676e85146e16d78ad990c6961174eb4bc8cf51 Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Mon, 4 Aug 2025 15:06:38 -0500 Subject: [PATCH 01/27] Find permutations of matrix Uses GAP to find all permutations of a matrix that have an orbit of a certain length --- qldpc/external/groups.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/qldpc/external/groups.py b/qldpc/external/groups.py index 6185b9fc3..d79b8b5bf 100644 --- a/qldpc/external/groups.py +++ b/qldpc/external/groups.py @@ -22,6 +22,7 @@ import urllib.request import galois +from sympy.combinatorics import Permutation import qldpc.cache import qldpc.external.gap @@ -291,6 +292,40 @@ def get_primitive_central_idempotents( return tuple(idempotents) +""" +Gets all order l permutations for a nxm matrix +""" + + +def get_permutation_symmetry_of_matrix( + symmetry_length: int, n: int, m: int +) -> tuple[list[qldpc.abstract.GroupMember], list[qldpc.abstract.GroupMember]]: + # Parse output, account for zero-based indexing + def parse_permutation_output(output: str) -> list[qldpc.abstract.GroupMember]: + perm_list = [] + for perm in output.strip("[] ").split(", "): + result = [] + cycles = re.findall(r"\([^()]+\)", perm) + for cycle in cycles: + cycle = cycle.strip("()") + elements = tuple(int(x) - 1 for x in cycle.split(",")) + result.append(elements) + # Remove trivial permutation + if result: + perm_list.append(qldpc.abstract.GroupMember.from_sympy(Permutation(result))) + return perm_list + + row_permutations_output = qldpc.external.gap.get_output( + f"row_perms := Filtered(SymmetricGroup({n}), perm -> Order(perm) = {symmetry_length});Print(row_perms);", + ) + col_permutations_output = qldpc.external.gap.get_output( + f"col_perms := Filtered(SymmetricGroup({m}), perm -> Order(perm) = {symmetry_length});Print(col_perms);", + ) + return parse_permutation_output(row_permutations_output), parse_permutation_output( + col_permutations_output + ) + + KNOWN_GROUPS: dict[str, GENERATORS_LIST] = { "AutomorphismGroup(CheckMatCode([[1,0,0,0,1,1,1,0,1,1],[0,1,0,0,1,0,0,1,1,0],[0,0,1,0,1,1,1,0,0,0],[0,0,0,1,1,1,0,1,1,1]],GF(2)))": [ [(3, 7), (4, 5), (8, 9)], From 87b6c402d7bd12e97695b5d1e79f96bf121bfc4b Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Mon, 4 Aug 2025 15:43:27 -0500 Subject: [PATCH 02/27] Add tests for permutation finding --- qldpc/external/groups_test.py | 36 ++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/qldpc/external/groups_test.py b/qldpc/external/groups_test.py index ac11aae3c..ec2b26d21 100644 --- a/qldpc/external/groups_test.py +++ b/qldpc/external/groups_test.py @@ -23,7 +23,7 @@ import galois import pytest -from qldpc import external +from qldpc import abstract, external # define global testing variables ORDER, INDEX = 2, 1 @@ -252,3 +252,37 @@ def test_known_groups() -> None: gap_generators = external.groups.get_generators_with_gap(group) assert gap_generators is None or gap_generators == generators + + +def test_find_permutations() -> None: + expected_4x4_order4 = ( + [ + abstract.GroupMember(0, 3, 2, 1), + abstract.GroupMember(0, 3, 1, 2), + abstract.GroupMember(0, 1, 3, 2), + abstract.GroupMember(0, 1, 2, 3), + abstract.GroupMember(0, 2, 3, 1), + abstract.GroupMember(0, 2, 1, 3), + ], + [ + abstract.GroupMember(0, 3, 2, 1), + abstract.GroupMember(0, 3, 1, 2), + abstract.GroupMember(0, 1, 3, 2), + abstract.GroupMember(0, 1, 2, 3), + abstract.GroupMember(0, 2, 3, 1), + abstract.GroupMember(0, 2, 1, 3), + ], + ) + actual = external.groups.get_permutation_symmetry_of_matrix(4, 4, 4) + assert actual == expected_4x4_order4 + + expected_3x2_order2 = ( + [abstract.GroupMember(1, 2), abstract.GroupMember(0, 2), abstract.GroupMember(0, 1)], + [abstract.GroupMember(0, 1)], + ) + actual = external.groups.get_permutation_symmetry_of_matrix(2, 3, 2) + assert actual == expected_3x2_order2 + + expected_empty: tuple[list[abstract.GroupMember], list[abstract.GroupMember]] = ([], []) + actual = external.groups.get_permutation_symmetry_of_matrix(5, 3, 3) + assert actual == expected_empty From 4a5ae4988268c1aac2e32b941cdcae7ecd56293c Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Mon, 4 Aug 2025 15:43:49 -0500 Subject: [PATCH 03/27] Add dim option to Groupmember.to_matrix Add an option to add padding/fixed points to a permutation matrix returned by to_matrix --- qldpc/abstract.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/qldpc/abstract.py b/qldpc/abstract.py index 041bd0235..fa893fdb9 100644 --- a/qldpc/abstract.py +++ b/qldpc/abstract.py @@ -88,17 +88,25 @@ def __matmul__(self, other: GroupMember) -> GroupMember: """ return GroupMember(self.array_form + [val + self.size for val in other.array_form]) - def to_matrix(self) -> npt.NDArray[np.int_]: + def to_matrix(self, dimension: int | None = None) -> npt.NDArray[np.int_]: """Lift this permutation object to a permutation matrix. For consistency with how SymPy composes permutations, the permutation matrix constructed here is right-acting, meaning that it acts on a vector v as v --> v @ p.to_matrix(). This convension ensures that this lift is a homomorphism on SymPy Permutation objects, which is to say that (p * q).to_matrix() = p.to_matrix() @ q.to_matrix(). + + Dimension specifies to final size of the output matrix. Must be strictly larger than original + size of the permutation. Each additional element is treated as a fixed point """ - matrix = np.zeros([self.size] * 2, dtype=int) + size = dimension if dimension is not None else self.size + matrix = np.eye(size, dtype=int) for ii in range(self.size): - matrix[ii, self.apply(ii)] = 1 + j = self.apply(ii) + # Since using identity matrix not zero, have to clear the row + matrix[ii, :] = 0 + matrix[ii, j] = 1 + return matrix def to_gap_cycles(self) -> str: From a6edd429c59748b00f82b7c98c90c30375459397 Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Mon, 4 Aug 2025 15:44:38 -0500 Subject: [PATCH 04/27] Core functionality of balanced product code --- qldpc/codes/quantum.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/qldpc/codes/quantum.py b/qldpc/codes/quantum.py index abd85b250..812b4381f 100644 --- a/qldpc/codes/quantum.py +++ b/qldpc/codes/quantum.py @@ -31,6 +31,7 @@ import scipy import sympy +import qldpc.external.groups from qldpc import abstract from qldpc.abstract import DEFAULT_FIELD_ORDER from qldpc.math import first_nonzero_cols @@ -1564,6 +1565,58 @@ def __init__( CSSCode.__init__(self, matrix_x, matrix_z, field) +class BalancedProductCode(CSSCode): + """Code created from the product of classical codes. Similar to hypergraph codes. + + Binary matrix (M) must 2 permutations of order symmetryLength, r and c. Where r*M = M*C^T + + + References: + - https://errorcorrectionzoo.org/c/balanced_product + - https://arxiv.org/pdf/2505.13679 + + """ + + def __init__(self, binaryMatrix: np.typing.NDArray[np.int_], symmetryLength: int) -> None: + if not np.all((binaryMatrix == 0) | binaryMatrix == 1): + raise ValueError("Binary matrix must only have zeros or ones") + + if binaryMatrix.shape[0] != binaryMatrix.shape[1]: + raise ValueError("Only square binary matrix is supported at this time") + + self._r = None + self._c = None + + r_candidates, c_candidates = qldpc.external.groups.get_permutation_symmetry_of_matrix( + symmetryLength, binaryMatrix.shape[0], binaryMatrix.shape[1] + ) + + if not r_candidates or not c_candidates: + raise ValueError("Matrix doesn't have any permutations of that orbit length") + for r in r_candidates: + for c in c_candidates: + if np.array_equal( + r.to_matrix(binaryMatrix.shape[0]) @ binaryMatrix, + binaryMatrix @ c.to_matrix(binaryMatrix.shape[1]).T, + ): + self._c = c + self._r = r + break + if self._c is None: + raise ValueError( + "Matrix doesn't have permutation that satisfy balanced product properties" + ) + assert self._r is not None + assert self._c is not None + check_x = np.hstack( + (binaryMatrix.T, np.eye(binaryMatrix.shape[0], dtype=int) + self._c.to_matrix()) + ) + check_z = np.hstack( + (np.eye(binaryMatrix.shape[1], dtype=int) + self._r.to_matrix(), binaryMatrix) + ) + CSSCode.__init__(self, check_x, check_z) + + class BaconShorCode(SHPCode): """Bacon-Shor code on a square grid, implemented as a subsystem hypergraph product code. From 6c47e0e0dcf18216770467bd158808767301ebc1 Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Mon, 4 Aug 2025 15:44:45 -0500 Subject: [PATCH 05/27] Tests for balanced product code --- qldpc/codes/quantum_test.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index fe5979d6c..2faa458b5 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -556,3 +556,25 @@ def test_shyps_code() -> None: params = ((2**dimension - 1) ** 2, dimension**2, 2 ** (dimension - 1)) assert code.get_code_params() == params assert np.all(np.count_nonzero(code.matrix.view(np.ndarray), axis=1) == 3) + + +def test_balanced_product_code() -> None: + # Test from https://arxiv.org/pdf/2505.13679 pg 3 + matrix = np.array( + [ + [1, 1, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 1, 1], + [1, 0, 0, 0, 0, 1], + ] + ) + balanced_code = codes.BalancedProductCode(matrix, 3) + # Meets CSS orthogonality + np.array_equal( + balanced_code.code_x.matrix @ balanced_code.code_z.matrix.transpose(), np.zeros((6, 6), int) + ) + # Since many permutations that meet requirements, can't check exact form of generators + assert len(balanced_code.get_logical_ops()) == 4 + assert balanced_code.get_distance_exact() == 3 From 756d99203642f4e0b0327c81d463928851db7f8f Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Mon, 4 Aug 2025 15:45:06 -0500 Subject: [PATCH 06/27] Add balanced product code to init --- qldpc/codes/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qldpc/codes/__init__.py b/qldpc/codes/__init__.py index d0ad6019d..2613bb03b 100644 --- a/qldpc/codes/__init__.py +++ b/qldpc/codes/__init__.py @@ -21,6 +21,7 @@ ) from .quantum import ( BaconShorCode, + BalancedProductCode, BBCode, C4Code, C6Code, @@ -55,6 +56,7 @@ "CSSCode", "QuditCode", "BaconShorCode", + "BalancedProductCode", "BBCode", "C4Code", "C6Code", From b8307665ab8a12b2b3ef4dc3beb5680ed04be4ed Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Mon, 4 Aug 2025 16:17:31 -0500 Subject: [PATCH 07/27] Fix mocking in gap test --- qldpc/external/groups_test.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/qldpc/external/groups_test.py b/qldpc/external/groups_test.py index ec2b26d21..00bd9e9bf 100644 --- a/qldpc/external/groups_test.py +++ b/qldpc/external/groups_test.py @@ -255,6 +255,16 @@ def test_known_groups() -> None: def test_find_permutations() -> None: + + mocked_outputs = [ + "[ (1,4,3,2), (1,4,2,3), (1,2,4,3), (1,2,3,4), (1,3,4,2), (1,3,2,4) ]", + "[ (1,4,3,2), (1,4,2,3), (1,2,4,3), (1,2,3,4), (1,3,4,2), (1,3,2,4) ]", + "[ (2,3), (1,3), (1,2) ]", + "[ (1,2) ]", + "[ ]", + "[ ]" + ] + expected_4x4_order4 = ( [ abstract.GroupMember(0, 3, 2, 1), @@ -273,16 +283,18 @@ def test_find_permutations() -> None: abstract.GroupMember(0, 2, 1, 3), ], ) - actual = external.groups.get_permutation_symmetry_of_matrix(4, 4, 4) - assert actual == expected_4x4_order4 expected_3x2_order2 = ( [abstract.GroupMember(1, 2), abstract.GroupMember(0, 2), abstract.GroupMember(0, 1)], [abstract.GroupMember(0, 1)], ) - actual = external.groups.get_permutation_symmetry_of_matrix(2, 3, 2) - assert actual == expected_3x2_order2 expected_empty: tuple[list[abstract.GroupMember], list[abstract.GroupMember]] = ([], []) - actual = external.groups.get_permutation_symmetry_of_matrix(5, 3, 3) - assert actual == expected_empty + + with unittest.mock.patch("qldpc.external.gap.get_output", side_effect=mocked_outputs): + actual = external.groups.get_permutation_symmetry_of_matrix(4, 4, 4) + assert actual == expected_4x4_order4 + actual = external.groups.get_permutation_symmetry_of_matrix(2, 3, 2) + assert actual == expected_3x2_order2 + actual = external.groups.get_permutation_symmetry_of_matrix(5, 3, 3) + assert actual == expected_empty From a95a95df418c90ea8ae80be005e33cfd1807d2ad Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Mon, 4 Aug 2025 17:29:18 -0500 Subject: [PATCH 08/27] Fix coverage --- qldpc/codes/quantum_test.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index 2faa458b5..ec3a9f37e 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -26,6 +26,7 @@ import sympy from qldpc import abstract, codes +from qldpc.codes import BalancedProductCode from qldpc.objects import ChainComplex, Node, Pauli @@ -578,3 +579,27 @@ def test_balanced_product_code() -> None: # Since many permutations that meet requirements, can't check exact form of generators assert len(balanced_code.get_logical_ops()) == 4 assert balanced_code.get_distance_exact() == 3 + + matrix = np.array( + [ + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + ] + ) + with pytest.raises(ValueError): + BalancedProductCode(matrix, 4) + + matrix[0][0] = 2 + with pytest.raises(ValueError): + BalancedProductCode(matrix, 1) + + matrix = np.array( + [ + [1, 1, 0, 1], + [0, 1, 1, 0], + [0, 0, 1, 0], + ] + ) + with pytest.raises(ValueError): + BalancedProductCode(matrix, 2) From ee3bc47fac823a5927ab1ce345227e790e4738ae Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Mon, 4 Aug 2025 20:42:15 -0500 Subject: [PATCH 09/27] Fixed formatting+mocking --- qldpc/codes/quantum_test.py | 94 +++++++++++++++++++---------------- qldpc/external/groups_test.py | 13 +++-- 2 files changed, 57 insertions(+), 50 deletions(-) diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index ec3a9f37e..dd2bf89e4 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -560,46 +560,54 @@ def test_shyps_code() -> None: def test_balanced_product_code() -> None: - # Test from https://arxiv.org/pdf/2505.13679 pg 3 - matrix = np.array( - [ - [1, 1, 0, 0, 0, 0], - [0, 1, 1, 0, 0, 0], - [0, 0, 1, 1, 0, 0], - [0, 0, 0, 1, 1, 0], - [0, 0, 0, 0, 1, 1], - [1, 0, 0, 0, 0, 1], - ] - ) - balanced_code = codes.BalancedProductCode(matrix, 3) - # Meets CSS orthogonality - np.array_equal( - balanced_code.code_x.matrix @ balanced_code.code_z.matrix.transpose(), np.zeros((6, 6), int) - ) - # Since many permutations that meet requirements, can't check exact form of generators - assert len(balanced_code.get_logical_ops()) == 4 - assert balanced_code.get_distance_exact() == 3 - - matrix = np.array( - [ - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - ] - ) - with pytest.raises(ValueError): - BalancedProductCode(matrix, 4) - - matrix[0][0] = 2 - with pytest.raises(ValueError): - BalancedProductCode(matrix, 1) - - matrix = np.array( - [ - [1, 1, 0, 1], - [0, 1, 1, 0], - [0, 0, 1, 0], - ] - ) - with pytest.raises(ValueError): - BalancedProductCode(matrix, 2) + mocked_output = [ + "[ (4,6,5), (4,5,6), (3,6,5), (3,6,4), (3,4,6), (3,4,5), (3,5,6), (3,5,4), (2,6,5), (2,6,4), (2,6,3), (2,3,6), (2,3,4), (2,3,5), (2,4,6), (2,4,5), (2,4,3), (2,5,6), (2,5,4), (2,5,3), (1,6,5), (1,6,4), (1,6,3), (1,6,2), (1,6,2)(3,4,5), (1,6,2)(3,5,4), (1,6,5)(2,3,4), (1,6,4)(2,3,5), (1,6,5)(2,4,3), (1,6,3)(2,4,5), (1,6,4)(2,5,3), (1,6,3)(2,5,4), (1,2,6), (1,2,6)(3,4,5), (1,2,6)(3,5,4), (1,2,3), (1,2,3)(4,6,5), (1,2,3)(4,5,6), (1,2,4), (1,2,4)(3,6,5), (1,2,4)(3,5,6), (1,2,5), (1,2,5)(3,6,4), (1,2,5)(3,4,6), (1,3,6), (1,3,4), (1,3,5), (1,3,2), (1,3,2)(4,6,5), (1,3,2)(4,5,6), (1,3,4)(2,6,5), (1,3,5)(2,6,4), (1,3,6)(2,4,5), (1,3,5)(2,4,6), (1,3,6)(2,5,4), (1,3,4)(2,5,6), (1,4,6), (1,4,5), (1,4,3), (1,4,2), (1,4,2)(3,6,5), (1,4,2)(3,5,6), (1,4,5)(2,3,6), (1,4,6)(2,3,5), (1,4,5)(2,6,3), (1,4,3)(2,6,5), (1,4,6)(2,5,3), (1,4,3)(2,5,6), (1,5,6), (1,5,4), (1,5,3), (1,5,2), (1,5,2)(3,4,6), (1,5,2)(3,6,4), (1,5,6)(2,3,4), (1,5,4)(2,3,6), (1,5,6)(2,4,3), (1,5,3)(2,4,6), (1,5,4)(2,6,3), (1,5,3)(2,6,4) ]", + "[ (4,6,5), (4,5,6), (3,6,5), (3,6,4), (3,4,6), (3,4,5), (3,5,6), (3,5,4), (2,6,5), (2,6,4), (2,6,3), (2,3,6), (2,3,4), (2,3,5), (2,4,6), (2,4,5), (2,4,3), (2,5,6), (2,5,4), (2,5,3), (1,6,5), (1,6,4), (1,6,3), (1,6,2), (1,6,2)(3,4,5), (1,6,2)(3,5,4), (1,6,5)(2,3,4), (1,6,4)(2,3,5), (1,6,5)(2,4,3), (1,6,3)(2,4,5), (1,6,4)(2,5,3), (1,6,3)(2,5,4), (1,2,6), (1,2,6)(3,4,5), (1,2,6)(3,5,4), (1,2,3), (1,2,3)(4,6,5), (1,2,3)(4,5,6), (1,2,4), (1,2,4)(3,6,5), (1,2,4)(3,5,6), (1,2,5), (1,2,5)(3,6,4), (1,2,5)(3,4,6), (1,3,6), (1,3,4), (1,3,5), (1,3,2), (1,3,2)(4,6,5), (1,3,2)(4,5,6), (1,3,4)(2,6,5), (1,3,5)(2,6,4), (1,3,6)(2,4,5), (1,3,5)(2,4,6), (1,3,6)(2,5,4), (1,3,4)(2,5,6), (1,4,6), (1,4,5), (1,4,3), (1,4,2), (1,4,2)(3,6,5), (1,4,2)(3,5,6), (1,4,5)(2,3,6), (1,4,6)(2,3,5), (1,4,5)(2,6,3), (1,4,3)(2,6,5), (1,4,6)(2,5,3), (1,4,3)(2,5,6), (1,5,6), (1,5,4), (1,5,3), (1,5,2), (1,5,2)(3,4,6), (1,5,2)(3,6,4), (1,5,6)(2,3,4), (1,5,4)(2,3,6), (1,5,6)(2,4,3), (1,5,3)(2,4,6), (1,5,4)(2,6,3), (1,5,3)(2,6,4) ]", + "[ ]", + "[ ]", + ] + with unittest.mock.patch("qldpc.external.gap.get_output", side_effect=mocked_output): + # Test from https://arxiv.org/pdf/2505.13679 pg 3 + matrix = np.array( + [ + [1, 1, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 1, 1], + [1, 0, 0, 0, 0, 1], + ] + ) + balanced_code = codes.BalancedProductCode(matrix, 3) + # Meets CSS orthogonality + np.array_equal( + balanced_code.code_x.matrix @ balanced_code.code_z.matrix.transpose(), + np.zeros((6, 6), int), + ) + # Since many permutations that meet requirements, can't check exact form of generators + assert len(balanced_code.get_logical_ops()) == 4 + assert balanced_code.get_distance_exact() == 3 + + matrix = np.array( + [ + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + ] + ) + with pytest.raises(ValueError): + BalancedProductCode(matrix, 4) + + matrix[0][0] = 2 + with pytest.raises(ValueError): + BalancedProductCode(matrix, 1) + + matrix = np.array( + [ + [1, 1, 0, 1], + [0, 1, 1, 0], + [0, 0, 1, 0], + ] + ) + with pytest.raises(ValueError): + BalancedProductCode(matrix, 2) diff --git a/qldpc/external/groups_test.py b/qldpc/external/groups_test.py index 00bd9e9bf..ce5d5f80e 100644 --- a/qldpc/external/groups_test.py +++ b/qldpc/external/groups_test.py @@ -255,14 +255,13 @@ def test_known_groups() -> None: def test_find_permutations() -> None: - mocked_outputs = [ - "[ (1,4,3,2), (1,4,2,3), (1,2,4,3), (1,2,3,4), (1,3,4,2), (1,3,2,4) ]", - "[ (1,4,3,2), (1,4,2,3), (1,2,4,3), (1,2,3,4), (1,3,4,2), (1,3,2,4) ]", - "[ (2,3), (1,3), (1,2) ]", - "[ (1,2) ]", - "[ ]", - "[ ]" + "[ (1,4,3,2), (1,4,2,3), (1,2,4,3), (1,2,3,4), (1,3,4,2), (1,3,2,4) ]", + "[ (1,4,3,2), (1,4,2,3), (1,2,4,3), (1,2,3,4), (1,3,4,2), (1,3,2,4) ]", + "[ (2,3), (1,3), (1,2) ]", + "[ (1,2) ]", + "[ ]", + "[ ]", ] expected_4x4_order4 = ( From fff6fc325c6f5f548d1b18f3a3b1fc5970b00a2a Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Mon, 4 Aug 2025 20:49:19 -0500 Subject: [PATCH 10/27] Fix coverage These lines can never run --- qldpc/codes/quantum.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qldpc/codes/quantum.py b/qldpc/codes/quantum.py index 812b4381f..9b81139f3 100644 --- a/qldpc/codes/quantum.py +++ b/qldpc/codes/quantum.py @@ -1602,10 +1602,6 @@ def __init__(self, binaryMatrix: np.typing.NDArray[np.int_], symmetryLength: int self._c = c self._r = r break - if self._c is None: - raise ValueError( - "Matrix doesn't have permutation that satisfy balanced product properties" - ) assert self._r is not None assert self._c is not None check_x = np.hstack( From 51e39c0969f2a606c134347908f410a704c1e5b0 Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Wed, 6 Aug 2025 10:30:46 -0500 Subject: [PATCH 11/27] Split up finding perms and balanced code --- qldpc/codes/quantum.py | 32 +++++++++++++------------------- qldpc/external/groups.py | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/qldpc/codes/quantum.py b/qldpc/codes/quantum.py index 9b81139f3..c94227d0b 100644 --- a/qldpc/codes/quantum.py +++ b/qldpc/codes/quantum.py @@ -1577,33 +1577,27 @@ class BalancedProductCode(CSSCode): """ - def __init__(self, binaryMatrix: np.typing.NDArray[np.int_], symmetryLength: int) -> None: + def __init__( + self, + binaryMatrix: np.typing.NDArray[np.int_], + R: qldpc.abstract.GroupMember, + C: qldpc.abstract.GroupMember, + ) -> None: if not np.all((binaryMatrix == 0) | binaryMatrix == 1): raise ValueError("Binary matrix must only have zeros or ones") if binaryMatrix.shape[0] != binaryMatrix.shape[1]: raise ValueError("Only square binary matrix is supported at this time") - self._r = None - self._c = None + if not np.array_equal( + R.to_matrix(binaryMatrix.shape[0]) @ binaryMatrix, + binaryMatrix @ C.to_matrix(binaryMatrix.shape[1]).T, + ): + raise ValueError("Invalid permutations provided") - r_candidates, c_candidates = qldpc.external.groups.get_permutation_symmetry_of_matrix( - symmetryLength, binaryMatrix.shape[0], binaryMatrix.shape[1] - ) + self._r = R + self._c = C - if not r_candidates or not c_candidates: - raise ValueError("Matrix doesn't have any permutations of that orbit length") - for r in r_candidates: - for c in c_candidates: - if np.array_equal( - r.to_matrix(binaryMatrix.shape[0]) @ binaryMatrix, - binaryMatrix @ c.to_matrix(binaryMatrix.shape[1]).T, - ): - self._c = c - self._r = r - break - assert self._r is not None - assert self._c is not None check_x = np.hstack( (binaryMatrix.T, np.eye(binaryMatrix.shape[0], dtype=int) + self._c.to_matrix()) ) diff --git a/qldpc/external/groups.py b/qldpc/external/groups.py index d79b8b5bf..4374fc8f5 100644 --- a/qldpc/external/groups.py +++ b/qldpc/external/groups.py @@ -22,6 +22,7 @@ import urllib.request import galois +import numpy as np from sympy.combinatorics import Permutation import qldpc.cache @@ -326,6 +327,25 @@ def parse_permutation_output(output: str) -> list[qldpc.abstract.GroupMember]: ) +def get_balanced_permutations_of_matrix( + binaryMatrix: np.typing.NDArray[np.int_], symmetryLength: int +) -> tuple[qldpc.abstract.GroupMember, qldpc.abstract.GroupMember]: + r_candidates, c_candidates = qldpc.external.groups.get_permutation_symmetry_of_matrix( + symmetryLength, binaryMatrix.shape[0], binaryMatrix.shape[1] + ) + + if not r_candidates or not c_candidates: + raise ValueError("Matrix doesn't have any permutations of that orbit length") + for R in r_candidates: + for C in c_candidates: + if np.array_equal( + R.to_matrix(binaryMatrix.shape[0]) @ binaryMatrix, + binaryMatrix @ C.to_matrix(binaryMatrix.shape[1]).T, + ): + return R, C + raise ValueError("Matrix doesn't have any permutations that meet required constraints") + + KNOWN_GROUPS: dict[str, GENERATORS_LIST] = { "AutomorphismGroup(CheckMatCode([[1,0,0,0,1,1,1,0,1,1],[0,1,0,0,1,0,0,1,1,0],[0,0,1,0,1,1,1,0,0,0],[0,0,0,1,1,1,0,1,1,1]],GF(2)))": [ [(3, 7), (4, 5), (8, 9)], From 04d8f9c7999bebaff3096cedd8c9ecd180c54e63 Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Wed, 6 Aug 2025 10:31:01 -0500 Subject: [PATCH 12/27] Moved balance code tests around --- qldpc/codes/quantum_test.py | 88 +++++++++++++++-------------------- qldpc/external/groups_test.py | 34 ++++++++++++++ 2 files changed, 71 insertions(+), 51 deletions(-) diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index dd2bf89e4..1be7cdfa5 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -24,6 +24,7 @@ import numpy as np import pytest import sympy +from sympy.combinatorics import Permutation from qldpc import abstract, codes from qldpc.codes import BalancedProductCode @@ -560,54 +561,39 @@ def test_shyps_code() -> None: def test_balanced_product_code() -> None: - mocked_output = [ - "[ (4,6,5), (4,5,6), (3,6,5), (3,6,4), (3,4,6), (3,4,5), (3,5,6), (3,5,4), (2,6,5), (2,6,4), (2,6,3), (2,3,6), (2,3,4), (2,3,5), (2,4,6), (2,4,5), (2,4,3), (2,5,6), (2,5,4), (2,5,3), (1,6,5), (1,6,4), (1,6,3), (1,6,2), (1,6,2)(3,4,5), (1,6,2)(3,5,4), (1,6,5)(2,3,4), (1,6,4)(2,3,5), (1,6,5)(2,4,3), (1,6,3)(2,4,5), (1,6,4)(2,5,3), (1,6,3)(2,5,4), (1,2,6), (1,2,6)(3,4,5), (1,2,6)(3,5,4), (1,2,3), (1,2,3)(4,6,5), (1,2,3)(4,5,6), (1,2,4), (1,2,4)(3,6,5), (1,2,4)(3,5,6), (1,2,5), (1,2,5)(3,6,4), (1,2,5)(3,4,6), (1,3,6), (1,3,4), (1,3,5), (1,3,2), (1,3,2)(4,6,5), (1,3,2)(4,5,6), (1,3,4)(2,6,5), (1,3,5)(2,6,4), (1,3,6)(2,4,5), (1,3,5)(2,4,6), (1,3,6)(2,5,4), (1,3,4)(2,5,6), (1,4,6), (1,4,5), (1,4,3), (1,4,2), (1,4,2)(3,6,5), (1,4,2)(3,5,6), (1,4,5)(2,3,6), (1,4,6)(2,3,5), (1,4,5)(2,6,3), (1,4,3)(2,6,5), (1,4,6)(2,5,3), (1,4,3)(2,5,6), (1,5,6), (1,5,4), (1,5,3), (1,5,2), (1,5,2)(3,4,6), (1,5,2)(3,6,4), (1,5,6)(2,3,4), (1,5,4)(2,3,6), (1,5,6)(2,4,3), (1,5,3)(2,4,6), (1,5,4)(2,6,3), (1,5,3)(2,6,4) ]", - "[ (4,6,5), (4,5,6), (3,6,5), (3,6,4), (3,4,6), (3,4,5), (3,5,6), (3,5,4), (2,6,5), (2,6,4), (2,6,3), (2,3,6), (2,3,4), (2,3,5), (2,4,6), (2,4,5), (2,4,3), (2,5,6), (2,5,4), (2,5,3), (1,6,5), (1,6,4), (1,6,3), (1,6,2), (1,6,2)(3,4,5), (1,6,2)(3,5,4), (1,6,5)(2,3,4), (1,6,4)(2,3,5), (1,6,5)(2,4,3), (1,6,3)(2,4,5), (1,6,4)(2,5,3), (1,6,3)(2,5,4), (1,2,6), (1,2,6)(3,4,5), (1,2,6)(3,5,4), (1,2,3), (1,2,3)(4,6,5), (1,2,3)(4,5,6), (1,2,4), (1,2,4)(3,6,5), (1,2,4)(3,5,6), (1,2,5), (1,2,5)(3,6,4), (1,2,5)(3,4,6), (1,3,6), (1,3,4), (1,3,5), (1,3,2), (1,3,2)(4,6,5), (1,3,2)(4,5,6), (1,3,4)(2,6,5), (1,3,5)(2,6,4), (1,3,6)(2,4,5), (1,3,5)(2,4,6), (1,3,6)(2,5,4), (1,3,4)(2,5,6), (1,4,6), (1,4,5), (1,4,3), (1,4,2), (1,4,2)(3,6,5), (1,4,2)(3,5,6), (1,4,5)(2,3,6), (1,4,6)(2,3,5), (1,4,5)(2,6,3), (1,4,3)(2,6,5), (1,4,6)(2,5,3), (1,4,3)(2,5,6), (1,5,6), (1,5,4), (1,5,3), (1,5,2), (1,5,2)(3,4,6), (1,5,2)(3,6,4), (1,5,6)(2,3,4), (1,5,4)(2,3,6), (1,5,6)(2,4,3), (1,5,3)(2,4,6), (1,5,4)(2,6,3), (1,5,3)(2,6,4) ]", - "[ ]", - "[ ]", - ] - with unittest.mock.patch("qldpc.external.gap.get_output", side_effect=mocked_output): - # Test from https://arxiv.org/pdf/2505.13679 pg 3 - matrix = np.array( - [ - [1, 1, 0, 0, 0, 0], - [0, 1, 1, 0, 0, 0], - [0, 0, 1, 1, 0, 0], - [0, 0, 0, 1, 1, 0], - [0, 0, 0, 0, 1, 1], - [1, 0, 0, 0, 0, 1], - ] - ) - balanced_code = codes.BalancedProductCode(matrix, 3) - # Meets CSS orthogonality - np.array_equal( - balanced_code.code_x.matrix @ balanced_code.code_z.matrix.transpose(), - np.zeros((6, 6), int), - ) - # Since many permutations that meet requirements, can't check exact form of generators - assert len(balanced_code.get_logical_ops()) == 4 - assert balanced_code.get_distance_exact() == 3 - - matrix = np.array( - [ - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - ] - ) - with pytest.raises(ValueError): - BalancedProductCode(matrix, 4) - - matrix[0][0] = 2 - with pytest.raises(ValueError): - BalancedProductCode(matrix, 1) - - matrix = np.array( - [ - [1, 1, 0, 1], - [0, 1, 1, 0], - [0, 0, 1, 0], - ] - ) - with pytest.raises(ValueError): - BalancedProductCode(matrix, 2) + # Test from https://arxiv.org/pdf/2505.13679 pg 3 + matrix = np.array( + [ + [1, 1, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 1, 1], + [1, 0, 0, 0, 0, 1], + ] + ) + + R, C = ( + abstract.GroupMember.from_sympy(Permutation([2, 3, 4, 5, 0, 1])), + abstract.GroupMember.from_sympy(Permutation([4, 5, 0, 1, 2, 3])), + ) + + balanced_code = codes.BalancedProductCode(matrix, R, C) + + # Meets CSS orthogonality + + np.array_equal( + balanced_code.code_x.matrix @ balanced_code.code_z.matrix.transpose(), + np.zeros((6, 6), int), + ) + # Since many permutations that meet requirements, can't check exact form of generators + assert len(balanced_code.get_logical_ops()) == 4 + assert balanced_code.get_distance_exact() == 3 + + matrix_extra_row = np.vstack([matrix, np.zeros((1, 6), dtype=int)]) + with pytest.raises(ValueError): + BalancedProductCode(matrix_extra_row, R, C) + + matrix[0][0] = 2 + with pytest.raises(ValueError): + BalancedProductCode(matrix, R, C) diff --git a/qldpc/external/groups_test.py b/qldpc/external/groups_test.py index ce5d5f80e..c800fd459 100644 --- a/qldpc/external/groups_test.py +++ b/qldpc/external/groups_test.py @@ -21,7 +21,9 @@ import urllib import galois +import numpy as np import pytest +from sympy.combinatorics import Permutation from qldpc import abstract, external @@ -297,3 +299,35 @@ def test_find_permutations() -> None: assert actual == expected_3x2_order2 actual = external.groups.get_permutation_symmetry_of_matrix(5, 3, 3) assert actual == expected_empty + + +def test_find_balanced_permutations() -> None: + mocked_output = [ + "[ (4,6,5), (4,5,6), (3,6,5), (3,6,4), (3,4,6), (3,4,5), (3,5,6), (3,5,4), (2,6,5), (2,6,4), (2,6,3), (2,3,6), (2,3,4), (2,3,5), (2,4,6), (2,4,5), (2,4,3), (2,5,6), (2,5,4), (2,5,3), (1,6,5), (1,6,4), (1,6,3), (1,6,2), (1,6,2)(3,4,5), (1,6,2)(3,5,4), (1,6,5)(2,3,4), (1,6,4)(2,3,5), (1,6,5)(2,4,3), (1,6,3)(2,4,5), (1,6,4)(2,5,3), (1,6,3)(2,5,4), (1,2,6), (1,2,6)(3,4,5), (1,2,6)(3,5,4), (1,2,3), (1,2,3)(4,6,5), (1,2,3)(4,5,6), (1,2,4), (1,2,4)(3,6,5), (1,2,4)(3,5,6), (1,2,5), (1,2,5)(3,6,4), (1,2,5)(3,4,6), (1,3,6), (1,3,4), (1,3,5), (1,3,2), (1,3,2)(4,6,5), (1,3,2)(4,5,6), (1,3,4)(2,6,5), (1,3,5)(2,6,4), (1,3,6)(2,4,5), (1,3,5)(2,4,6), (1,3,6)(2,5,4), (1,3,4)(2,5,6), (1,4,6), (1,4,5), (1,4,3), (1,4,2), (1,4,2)(3,6,5), (1,4,2)(3,5,6), (1,4,5)(2,3,6), (1,4,6)(2,3,5), (1,4,5)(2,6,3), (1,4,3)(2,6,5), (1,4,6)(2,5,3), (1,4,3)(2,5,6), (1,5,6), (1,5,4), (1,5,3), (1,5,2), (1,5,2)(3,4,6), (1,5,2)(3,6,4), (1,5,6)(2,3,4), (1,5,4)(2,3,6), (1,5,6)(2,4,3), (1,5,3)(2,4,6), (1,5,4)(2,6,3), (1,5,3)(2,6,4) ]", + "[ (4,6,5), (4,5,6), (3,6,5), (3,6,4), (3,4,6), (3,4,5), (3,5,6), (3,5,4), (2,6,5), (2,6,4), (2,6,3), (2,3,6), (2,3,4), (2,3,5), (2,4,6), (2,4,5), (2,4,3), (2,5,6), (2,5,4), (2,5,3), (1,6,5), (1,6,4), (1,6,3), (1,6,2), (1,6,2)(3,4,5), (1,6,2)(3,5,4), (1,6,5)(2,3,4), (1,6,4)(2,3,5), (1,6,5)(2,4,3), (1,6,3)(2,4,5), (1,6,4)(2,5,3), (1,6,3)(2,5,4), (1,2,6), (1,2,6)(3,4,5), (1,2,6)(3,5,4), (1,2,3), (1,2,3)(4,6,5), (1,2,3)(4,5,6), (1,2,4), (1,2,4)(3,6,5), (1,2,4)(3,5,6), (1,2,5), (1,2,5)(3,6,4), (1,2,5)(3,4,6), (1,3,6), (1,3,4), (1,3,5), (1,3,2), (1,3,2)(4,6,5), (1,3,2)(4,5,6), (1,3,4)(2,6,5), (1,3,5)(2,6,4), (1,3,6)(2,4,5), (1,3,5)(2,4,6), (1,3,6)(2,5,4), (1,3,4)(2,5,6), (1,4,6), (1,4,5), (1,4,3), (1,4,2), (1,4,2)(3,6,5), (1,4,2)(3,5,6), (1,4,5)(2,3,6), (1,4,6)(2,3,5), (1,4,5)(2,6,3), (1,4,3)(2,6,5), (1,4,6)(2,5,3), (1,4,3)(2,5,6), (1,5,6), (1,5,4), (1,5,3), (1,5,2), (1,5,2)(3,4,6), (1,5,2)(3,6,4), (1,5,6)(2,3,4), (1,5,4)(2,3,6), (1,5,6)(2,4,3), (1,5,3)(2,4,6), (1,5,4)(2,6,3), (1,5,3)(2,6,4) ]", + "[ ]", + "[ ]", + ] + with unittest.mock.patch("qldpc.external.gap.get_output", side_effect=mocked_output): + matrix = np.array( + [ + [1, 1, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 1, 1], + [1, 0, 0, 0, 0, 1], + ] + ) + R, C = external.groups.get_balanced_permutations_of_matrix(matrix, 3) + assert R == abstract.GroupMember.from_sympy(Permutation([2, 3, 4, 5, 0, 1])) + assert C == abstract.GroupMember.from_sympy(Permutation([4, 5, 0, 1, 2, 3])) + matrix = np.array( + [ + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + ] + ) + with pytest.raises(ValueError): + external.groups.get_balanced_permutations_of_matrix(matrix, 4) From 744eb7408105162c5742f284263793eab7869a63 Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Wed, 6 Aug 2025 16:22:03 -0500 Subject: [PATCH 13/27] Alternative balanced code creation --- qldpc/codes/quantum.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/qldpc/codes/quantum.py b/qldpc/codes/quantum.py index c94227d0b..ef6a5f021 100644 --- a/qldpc/codes/quantum.py +++ b/qldpc/codes/quantum.py @@ -1606,6 +1606,44 @@ def __init__( ) CSSCode.__init__(self, check_x, check_z) + """ + Allows for the creation of balanced codes from a classical code and a CSS code. + + https://arxiv.org/pdf/2505.13679v1 + + """ + + @classmethod + def from_codes(cls, code_q: CSSCode, code_c: ClassicalCode) -> qldpc.codes.BalancedProductCode: + r_X, n_X = code_q.code_x.matrix.shape + # i.e. rows of quantum matrix, number qubits of quantum matrix + r_C, n_C = code_c.matrix.shape + + # Make new H_x + Hx_tensor = np.kron(code_q.code_x.matrix, np.eye(n_C, dtype=int)) + classical_T_tensor = np.kron(np.eye(r_X, dtype=int), code_c.matrix.T) + H_new_X = np.hstack((Hx_tensor, classical_T_tensor)) + + # Make new H_z + Hz_tensor = np.kron(code_q.code_z.matrix, np.eye(n_C, dtype=int)) + + bottom_left = np.kron(np.eye(n_X, dtype=int), code_c.matrix) + bottom_right = np.kron(code_q.code_x.matrix.T, np.eye(r_C, dtype=int)) + + top_right_zeros = np.zeros( + (Hz_tensor.shape[0], bottom_left.shape[1] + bottom_right.shape[1] - Hz_tensor.shape[1]), + dtype=int, + ) + top_row = np.hstack((Hz_tensor, top_right_zeros)) + + bottom_row = np.hstack((bottom_left, bottom_right)) + + H_new_Z = np.vstack((top_row, bottom_row)) + + new_code = cls.__new__(cls) + CSSCode.__init__(new_code, H_new_X, H_new_Z) + return new_code + class BaconShorCode(SHPCode): """Bacon-Shor code on a square grid, implemented as a subsystem hypergraph product code. From 709c2ddc0d223e8fd6fb0f4ce77f4ac79aaacf0f Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Wed, 6 Aug 2025 17:38:55 -0500 Subject: [PATCH 14/27] Add balanced code tests --- qldpc/codes/quantum_test.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index 1be7cdfa5..143a36f20 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -26,6 +26,7 @@ import sympy from sympy.combinatorics import Permutation +import qldpc.codes from qldpc import abstract, codes from qldpc.codes import BalancedProductCode from qldpc.objects import ChainComplex, Node, Pauli @@ -597,3 +598,16 @@ def test_balanced_product_code() -> None: matrix[0][0] = 2 with pytest.raises(ValueError): BalancedProductCode(matrix, R, C) + + +def test_balanced_product_from_codes() -> None: + css = qldpc.codes.SurfaceCode(5) + classical = qldpc.codes.classical.RepetitionCode(5) + new_code = qldpc.codes.BalancedProductCode.from_codes(css, classical) + assert ( + new_code.num_qubits + == css.num_qubits * classical.matrix.shape[1] + + css.code_x.matrix.shape[0] * classical.matrix.shape[0] + ) + assert len(new_code.get_logical_ops()) // 2 == 1 + # New code should have n*n_c + m_x * m_c qubits and k*k_c logical qubits From d6c95daf656230922155361b83cfe7c45078378e Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Wed, 6 Aug 2025 18:08:06 -0500 Subject: [PATCH 15/27] Add distance balancing --- qldpc/codes/quantum.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/qldpc/codes/quantum.py b/qldpc/codes/quantum.py index ef6a5f021..1e9572a65 100644 --- a/qldpc/codes/quantum.py +++ b/qldpc/codes/quantum.py @@ -1611,24 +1611,37 @@ def __init__( https://arxiv.org/pdf/2505.13679v1 + If 'distance_balancing', function will determine if x and z errors have the same distance + and if not, will construct the code to maximize the final distance. This involve exact distance calculations + which will only work on relatively small codes + """ @classmethod - def from_codes(cls, code_q: CSSCode, code_c: ClassicalCode) -> qldpc.codes.BalancedProductCode: - r_X, n_X = code_q.code_x.matrix.shape - # i.e. rows of quantum matrix, number qubits of quantum matrix + def from_codes(cls, code_q: CSSCode, code_c: ClassicalCode, distance_balancing: bool = False) -> qldpc.codes.BalancedProductCode: + # i.e. rows of matrix, number qubits of matrix r_C, n_C = code_c.matrix.shape + high_distance = code_q.code_z + low_distance = code_q.code_x + if distance_balancing: + if code_q.code_x.get_distance_exact() > code_q.code_z.get_distance_exact(): + temp = high_distance + high_distance = low_distance + low_distance = temp + + r_low, n_low = low_distance.matrix.shape + # Make new H_x - Hx_tensor = np.kron(code_q.code_x.matrix, np.eye(n_C, dtype=int)) - classical_T_tensor = np.kron(np.eye(r_X, dtype=int), code_c.matrix.T) + Hx_tensor = np.kron(low_distance.matrix, np.eye(n_C, dtype=int)) + classical_T_tensor = np.kron(np.eye(r_low, dtype=int), code_c.matrix.T) # shape: (r_low * n_C, r_low * r_C) H_new_X = np.hstack((Hx_tensor, classical_T_tensor)) # Make new H_z - Hz_tensor = np.kron(code_q.code_z.matrix, np.eye(n_C, dtype=int)) + Hz_tensor = np.kron(high_distance.matrix, np.eye(n_C, dtype=int)) - bottom_left = np.kron(np.eye(n_X, dtype=int), code_c.matrix) - bottom_right = np.kron(code_q.code_x.matrix.T, np.eye(r_C, dtype=int)) + bottom_left = np.kron(np.eye(n_low, dtype=int), code_c.matrix) + bottom_right = np.kron(low_distance.matrix.T, np.eye(r_C, dtype=int)) top_right_zeros = np.zeros( (Hz_tensor.shape[0], bottom_left.shape[1] + bottom_right.shape[1] - Hz_tensor.shape[1]), From 5ba3f441de4288607d468beff4e4845925bdd31c Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Wed, 6 Aug 2025 18:08:15 -0500 Subject: [PATCH 16/27] Distance balancing tests --- qldpc/codes/quantum_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index 143a36f20..412caa423 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -611,3 +611,14 @@ def test_balanced_product_from_codes() -> None: ) assert len(new_code.get_logical_ops()) // 2 == 1 # New code should have n*n_c + m_x * m_c qubits and k*k_c logical qubits + +def test_unbalanced_product_from_codes() -> None: + css = qldpc.codes.HGPCode(qldpc.codes.RepetitionCode(5), qldpc.codes.HammingCode(3)) + + classical = qldpc.codes.classical.RepetitionCode(5) + + new_code = qldpc.codes.BalancedProductCode.from_codes(css, classical, True) + + assert new_code.num_qubits == css.num_qubits * 5 + css.code_z.matrix.shape[0] * classical.matrix.shape[0] + assert len(css.get_logical_ops()) == len(new_code.get_logical_ops()) + From 69c3af35003a0983c175577f6a2eb7bbbeddee2d Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Wed, 6 Aug 2025 18:12:09 -0500 Subject: [PATCH 17/27] Formatting fixes + docs --- qldpc/codes/quantum.py | 28 +++++++++++++++++++++++----- qldpc/codes/quantum_test.py | 7 +++++-- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/qldpc/codes/quantum.py b/qldpc/codes/quantum.py index 1e9572a65..61db1b1a6 100644 --- a/qldpc/codes/quantum.py +++ b/qldpc/codes/quantum.py @@ -1568,8 +1568,23 @@ def __init__( class BalancedProductCode(CSSCode): """Code created from the product of classical codes. Similar to hypergraph codes. - Binary matrix (M) must 2 permutations of order symmetryLength, r and c. Where r*M = M*C^T - + Binary matrix (M) must have 2 permutations, r and c. Where r*M = M*C^T and r^l = I and c^l for some l. + + qldpc.external.groups.get_balanced_permutations_of_matrix can be used to find these permutations. + + I.e. + matrix = np.array( + [ + [1, 1, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 1, 1], + [1, 0, 0, 0, 0, 1], + ] + ) + R,C = ldpc.external.groups.get_balanced_permutations_of_matrix(matrix,3) + balanced_code = codes.BalancedProductCode(matrix,R,C) References: - https://errorcorrectionzoo.org/c/balanced_product @@ -1588,7 +1603,6 @@ def __init__( if binaryMatrix.shape[0] != binaryMatrix.shape[1]: raise ValueError("Only square binary matrix is supported at this time") - if not np.array_equal( R.to_matrix(binaryMatrix.shape[0]) @ binaryMatrix, binaryMatrix @ C.to_matrix(binaryMatrix.shape[1]).T, @@ -1618,7 +1632,9 @@ def __init__( """ @classmethod - def from_codes(cls, code_q: CSSCode, code_c: ClassicalCode, distance_balancing: bool = False) -> qldpc.codes.BalancedProductCode: + def from_codes( + cls, code_q: CSSCode, code_c: ClassicalCode, distance_balancing: bool = False + ) -> qldpc.codes.BalancedProductCode: # i.e. rows of matrix, number qubits of matrix r_C, n_C = code_c.matrix.shape @@ -1634,7 +1650,9 @@ def from_codes(cls, code_q: CSSCode, code_c: ClassicalCode, distance_balancing: # Make new H_x Hx_tensor = np.kron(low_distance.matrix, np.eye(n_C, dtype=int)) - classical_T_tensor = np.kron(np.eye(r_low, dtype=int), code_c.matrix.T) # shape: (r_low * n_C, r_low * r_C) + classical_T_tensor = np.kron( + np.eye(r_low, dtype=int), code_c.matrix.T + ) # shape: (r_low * n_C, r_low * r_C) H_new_X = np.hstack((Hx_tensor, classical_T_tensor)) # Make new H_z diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index 412caa423..ccfb21473 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -612,6 +612,7 @@ def test_balanced_product_from_codes() -> None: assert len(new_code.get_logical_ops()) // 2 == 1 # New code should have n*n_c + m_x * m_c qubits and k*k_c logical qubits + def test_unbalanced_product_from_codes() -> None: css = qldpc.codes.HGPCode(qldpc.codes.RepetitionCode(5), qldpc.codes.HammingCode(3)) @@ -619,6 +620,8 @@ def test_unbalanced_product_from_codes() -> None: new_code = qldpc.codes.BalancedProductCode.from_codes(css, classical, True) - assert new_code.num_qubits == css.num_qubits * 5 + css.code_z.matrix.shape[0] * classical.matrix.shape[0] + assert ( + new_code.num_qubits + == css.num_qubits * 5 + css.code_z.matrix.shape[0] * classical.matrix.shape[0] + ) assert len(css.get_logical_ops()) == len(new_code.get_logical_ops()) - From 4885a7769590766e3f2aa9e05e4f5df365261210 Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Wed, 6 Aug 2025 18:57:03 -0500 Subject: [PATCH 18/27] Coverage fix --- qldpc/codes/quantum_test.py | 7 +++++++ qldpc/external/groups.py | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index ccfb21473..b865ad753 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -591,6 +591,13 @@ def test_balanced_product_code() -> None: assert len(balanced_code.get_logical_ops()) == 4 assert balanced_code.get_distance_exact() == 3 + R, C = ( + abstract.GroupMember.from_sympy(Permutation([0, 1, 2, 3, 4, 5])), + abstract.GroupMember.from_sympy(Permutation([0, 1, 2, 3, 4, 5])), + ) + with pytest.raises(ValueError): + codes.BalancedProductCode(matrix, R, C) + matrix_extra_row = np.vstack([matrix, np.zeros((1, 6), dtype=int)]) with pytest.raises(ValueError): BalancedProductCode(matrix_extra_row, R, C) diff --git a/qldpc/external/groups.py b/qldpc/external/groups.py index 4374fc8f5..13b1d6b7a 100644 --- a/qldpc/external/groups.py +++ b/qldpc/external/groups.py @@ -343,7 +343,10 @@ def get_balanced_permutations_of_matrix( binaryMatrix @ C.to_matrix(binaryMatrix.shape[1]).T, ): return R, C - raise ValueError("Matrix doesn't have any permutations that meet required constraints") + raise ValueError( + "Matrix doesn't have any permutations that meet required constraints" + ) # pragma: no cover + # shouldn't ever get to this point, don't require test coverage KNOWN_GROUPS: dict[str, GENERATORS_LIST] = { From 9c3fc5d1537c96dfa1486114ad412c12dc6866f2 Mon Sep 17 00:00:00 2001 From: Ryan Kersten Date: Wed, 6 Aug 2025 19:15:57 -0500 Subject: [PATCH 19/27] Fix coverage --- qldpc/codes/quantum_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index b865ad753..0408a8ff5 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -592,7 +592,7 @@ def test_balanced_product_code() -> None: assert balanced_code.get_distance_exact() == 3 R, C = ( - abstract.GroupMember.from_sympy(Permutation([0, 1, 2, 3, 4, 5])), + abstract.GroupMember.from_sympy(Permutation([1, 0, 2, 3, 4, 5])), abstract.GroupMember.from_sympy(Permutation([0, 1, 2, 3, 4, 5])), ) with pytest.raises(ValueError): From 36b6ed9f073a41b6b67d2a8c368ee465a5e15bea Mon Sep 17 00:00:00 2001 From: ryankers Date: Fri, 8 Aug 2025 17:49:34 -0500 Subject: [PATCH 20/27] Fix tests Uses better way to get number of encoded logical ops in a code --- qldpc/codes/quantum_test.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index 0408a8ff5..be52b74c6 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -616,9 +616,28 @@ def test_balanced_product_from_codes() -> None: == css.num_qubits * classical.matrix.shape[1] + css.code_x.matrix.shape[0] * classical.matrix.shape[0] ) - assert len(new_code.get_logical_ops()) // 2 == 1 + assert new_code.get_logical_ops().shape[0] // 2 == 1 # New code should have n*n_c + m_x * m_c qubits and k*k_c logical qubits + from sympy.abc import x, y + + orders: tuple[int, int] | dict[sympy.Symbol, int] + orders = {x: 12, y: 4} + poly_a = 1 + y + x * y + x**9 + poly_b = 1 + x**2 + x**7 + x**9 * y**2 + css2 = codes.BBCode(orders, poly_a, poly_b, field=2) + classical2 = qldpc.codes.classical.ReedMullerCode(5, 5) + new_code = qldpc.codes.BalancedProductCode.from_codes(css2, classical2) + assert ( + new_code.num_qubits + == css2.num_qubits * classical2.matrix.shape[1] + + css2.code_x.matrix.shape[0] * classical2.matrix.shape[0] + ) + # new logical qubits should be k*k_c + assert (new_code.get_logical_ops().shape[0] // 2) == ( + css2.get_logical_ops().shape[0] // 2 + ) * classical2.dimension + def test_unbalanced_product_from_codes() -> None: css = qldpc.codes.HGPCode(qldpc.codes.RepetitionCode(5), qldpc.codes.HammingCode(3)) @@ -631,4 +650,4 @@ def test_unbalanced_product_from_codes() -> None: new_code.num_qubits == css.num_qubits * 5 + css.code_z.matrix.shape[0] * classical.matrix.shape[0] ) - assert len(css.get_logical_ops()) == len(new_code.get_logical_ops()) + assert css.get_logical_ops().shape[0] == new_code.get_logical_ops().shape[0] From 43d171667bad828f9ceb7151dca39b18a3f7b64c Mon Sep 17 00:00:00 2001 From: ryankers Date: Fri, 8 Aug 2025 18:02:26 -0500 Subject: [PATCH 21/27] add more doc for permutation fnx --- qldpc/external/groups.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qldpc/external/groups.py b/qldpc/external/groups.py index 13b1d6b7a..220e70432 100644 --- a/qldpc/external/groups.py +++ b/qldpc/external/groups.py @@ -294,7 +294,7 @@ def get_primitive_central_idempotents( """ -Gets all order l permutations for a nxm matrix +Gets all order l permutations for a nxm matrix """ @@ -327,6 +327,12 @@ def parse_permutation_output(output: str) -> list[qldpc.abstract.GroupMember]: ) +""" +Gets a pair of permutations with a certain orbit length (if one exists) for a binary matrix +that meets the requirements to construct a balanced product code +""" + + def get_balanced_permutations_of_matrix( binaryMatrix: np.typing.NDArray[np.int_], symmetryLength: int ) -> tuple[qldpc.abstract.GroupMember, qldpc.abstract.GroupMember]: From df50480b460f46e59e7bed96ef5d81248b6a4aea Mon Sep 17 00:00:00 2001 From: ryankers Date: Sat, 16 Aug 2025 07:46:18 -0400 Subject: [PATCH 22/27] Rename to BPCode --- qldpc/codes/__init__.py | 4 ++-- qldpc/codes/quantum.py | 4 ++-- qldpc/codes/quantum_test.py | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/qldpc/codes/__init__.py b/qldpc/codes/__init__.py index 2613bb03b..62beb7c54 100644 --- a/qldpc/codes/__init__.py +++ b/qldpc/codes/__init__.py @@ -21,8 +21,8 @@ ) from .quantum import ( BaconShorCode, - BalancedProductCode, BBCode, + BPCode, C4Code, C6Code, FiveQubitCode, @@ -56,8 +56,8 @@ "CSSCode", "QuditCode", "BaconShorCode", - "BalancedProductCode", "BBCode", + "BPCode", "C4Code", "C6Code", "FiveQubitCode", diff --git a/qldpc/codes/quantum.py b/qldpc/codes/quantum.py index 61db1b1a6..ae50e70fb 100644 --- a/qldpc/codes/quantum.py +++ b/qldpc/codes/quantum.py @@ -1565,7 +1565,7 @@ def __init__( CSSCode.__init__(self, matrix_x, matrix_z, field) -class BalancedProductCode(CSSCode): +class BPCode(CSSCode): """Code created from the product of classical codes. Similar to hypergraph codes. Binary matrix (M) must have 2 permutations, r and c. Where r*M = M*C^T and r^l = I and c^l for some l. @@ -1634,7 +1634,7 @@ def __init__( @classmethod def from_codes( cls, code_q: CSSCode, code_c: ClassicalCode, distance_balancing: bool = False - ) -> qldpc.codes.BalancedProductCode: + ) -> qldpc.codes.BPCode: # i.e. rows of matrix, number qubits of matrix r_C, n_C = code_c.matrix.shape diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index be52b74c6..f1aa690be 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -28,7 +28,7 @@ import qldpc.codes from qldpc import abstract, codes -from qldpc.codes import BalancedProductCode +from qldpc.codes import BPCode from qldpc.objects import ChainComplex, Node, Pauli @@ -579,7 +579,7 @@ def test_balanced_product_code() -> None: abstract.GroupMember.from_sympy(Permutation([4, 5, 0, 1, 2, 3])), ) - balanced_code = codes.BalancedProductCode(matrix, R, C) + balanced_code = codes.BPCode(matrix, R, C) # Meets CSS orthogonality @@ -596,21 +596,21 @@ def test_balanced_product_code() -> None: abstract.GroupMember.from_sympy(Permutation([0, 1, 2, 3, 4, 5])), ) with pytest.raises(ValueError): - codes.BalancedProductCode(matrix, R, C) + codes.BPCode(matrix, R, C) matrix_extra_row = np.vstack([matrix, np.zeros((1, 6), dtype=int)]) with pytest.raises(ValueError): - BalancedProductCode(matrix_extra_row, R, C) + BPCode(matrix_extra_row, R, C) matrix[0][0] = 2 with pytest.raises(ValueError): - BalancedProductCode(matrix, R, C) + BPCode(matrix, R, C) def test_balanced_product_from_codes() -> None: css = qldpc.codes.SurfaceCode(5) classical = qldpc.codes.classical.RepetitionCode(5) - new_code = qldpc.codes.BalancedProductCode.from_codes(css, classical) + new_code = qldpc.codes.BPCode.from_codes(css, classical) assert ( new_code.num_qubits == css.num_qubits * classical.matrix.shape[1] @@ -627,7 +627,7 @@ def test_balanced_product_from_codes() -> None: poly_b = 1 + x**2 + x**7 + x**9 * y**2 css2 = codes.BBCode(orders, poly_a, poly_b, field=2) classical2 = qldpc.codes.classical.ReedMullerCode(5, 5) - new_code = qldpc.codes.BalancedProductCode.from_codes(css2, classical2) + new_code = qldpc.codes.BPCode.from_codes(css2, classical2) assert ( new_code.num_qubits == css2.num_qubits * classical2.matrix.shape[1] @@ -644,7 +644,7 @@ def test_unbalanced_product_from_codes() -> None: classical = qldpc.codes.classical.RepetitionCode(5) - new_code = qldpc.codes.BalancedProductCode.from_codes(css, classical, True) + new_code = qldpc.codes.BPCode.from_codes(css, classical, True) assert ( new_code.num_qubits From bcdf09d3fef8293a9accc22a5e93004575da525d Mon Sep 17 00:00:00 2001 From: ryankers Date: Sat, 16 Aug 2025 07:53:44 -0400 Subject: [PATCH 23/27] Rename binaryMatrix --- qldpc/codes/quantum.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/qldpc/codes/quantum.py b/qldpc/codes/quantum.py index ae50e70fb..0161654a6 100644 --- a/qldpc/codes/quantum.py +++ b/qldpc/codes/quantum.py @@ -1594,29 +1594,30 @@ class BPCode(CSSCode): def __init__( self, - binaryMatrix: np.typing.NDArray[np.int_], + seed_matrix: np.typing.NDArray[np.int_], R: qldpc.abstract.GroupMember, C: qldpc.abstract.GroupMember, ) -> None: - if not np.all((binaryMatrix == 0) | binaryMatrix == 1): + if not np.all((seed_matrix == 0) | seed_matrix == 1): raise ValueError("Binary matrix must only have zeros or ones") - if binaryMatrix.shape[0] != binaryMatrix.shape[1]: + if seed_matrix.shape[0] != seed_matrix.shape[1]: raise ValueError("Only square binary matrix is supported at this time") if not np.array_equal( - R.to_matrix(binaryMatrix.shape[0]) @ binaryMatrix, - binaryMatrix @ C.to_matrix(binaryMatrix.shape[1]).T, + R.to_matrix(seed_matrix.shape[0]) @ seed_matrix, + seed_matrix @ C.to_matrix(seed_matrix.shape[1]).T, ): raise ValueError("Invalid permutations provided") self._r = R self._c = C + self._seed_matrix = seed_matrix check_x = np.hstack( - (binaryMatrix.T, np.eye(binaryMatrix.shape[0], dtype=int) + self._c.to_matrix()) + (seed_matrix.T, np.eye(seed_matrix.shape[0], dtype=int) + self._c.to_matrix()) ) check_z = np.hstack( - (np.eye(binaryMatrix.shape[1], dtype=int) + self._r.to_matrix(), binaryMatrix) + (np.eye(seed_matrix.shape[1], dtype=int) + self._r.to_matrix(), seed_matrix) ) CSSCode.__init__(self, check_x, check_z) From 8e60117558fe019aa43e9c64b3e5a3c46d9cdc10 Mon Sep 17 00:00:00 2001 From: ryankers Date: Sat, 16 Aug 2025 09:36:09 -0400 Subject: [PATCH 24/27] Fix doc strings --- qldpc/codes/quantum.py | 25 +++++++++++++------------ qldpc/external/groups.py | 27 ++++++++++++--------------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/qldpc/codes/quantum.py b/qldpc/codes/quantum.py index 0161654a6..c2046fbe0 100644 --- a/qldpc/codes/quantum.py +++ b/qldpc/codes/quantum.py @@ -1597,6 +1597,7 @@ def __init__( seed_matrix: np.typing.NDArray[np.int_], R: qldpc.abstract.GroupMember, C: qldpc.abstract.GroupMember, + field: int | None = None, ) -> None: if not np.all((seed_matrix == 0) | seed_matrix == 1): raise ValueError("Binary matrix must only have zeros or ones") @@ -1619,23 +1620,23 @@ def __init__( check_z = np.hstack( (np.eye(seed_matrix.shape[1], dtype=int) + self._r.to_matrix(), seed_matrix) ) - CSSCode.__init__(self, check_x, check_z) - - """ - Allows for the creation of balanced codes from a classical code and a CSS code. - - https://arxiv.org/pdf/2505.13679v1 - - If 'distance_balancing', function will determine if x and z errors have the same distance - and if not, will construct the code to maximize the final distance. This involve exact distance calculations - which will only work on relatively small codes - - """ + CSSCode.__init__(self, check_x, check_z, field, is_subsystem_code=False) @classmethod def from_codes( cls, code_q: CSSCode, code_c: ClassicalCode, distance_balancing: bool = False ) -> qldpc.codes.BPCode: + """ + Allows for the creation of balanced codes from a classical code and a CSS code. + + https://arxiv.org/pdf/2505.13679v1 + + If 'distance_balancing', function will determine if x and z errors have the same distance + and if not, will construct the code to maximize the final distance. This involve exact distance calculations + which will only work on relatively small codes + + """ + # i.e. rows of matrix, number qubits of matrix r_C, n_C = code_c.matrix.shape diff --git a/qldpc/external/groups.py b/qldpc/external/groups.py index 220e70432..21a9d4f33 100644 --- a/qldpc/external/groups.py +++ b/qldpc/external/groups.py @@ -117,7 +117,6 @@ def get_generators_with_gap(group: str) -> GENERATORS_LIST | None: r'for gen in gens do Print(gen, "\n"); od;', ] generators_str = qldpc.external.gap.get_output(*commands) - # collect generators generators = [] for line in generators_str.splitlines(): @@ -293,14 +292,13 @@ def get_primitive_central_idempotents( return tuple(idempotents) -""" -Gets all order l permutations for a nxm matrix -""" - - def get_permutation_symmetry_of_matrix( - symmetry_length: int, n: int, m: int + symmetry_length: int, rows: int, columns: int ) -> tuple[list[qldpc.abstract.GroupMember], list[qldpc.abstract.GroupMember]]: + """ + Gets all order l permutations for matrix of shape (rows,columns) + """ + # Parse output, account for zero-based indexing def parse_permutation_output(output: str) -> list[qldpc.abstract.GroupMember]: perm_list = [] @@ -317,25 +315,24 @@ def parse_permutation_output(output: str) -> list[qldpc.abstract.GroupMember]: return perm_list row_permutations_output = qldpc.external.gap.get_output( - f"row_perms := Filtered(SymmetricGroup({n}), perm -> Order(perm) = {symmetry_length});Print(row_perms);", + f"row_perms := Filtered(SymmetricGroup({rows}), perm -> Order(perm) = {symmetry_length});Print(row_perms);", ) col_permutations_output = qldpc.external.gap.get_output( - f"col_perms := Filtered(SymmetricGroup({m}), perm -> Order(perm) = {symmetry_length});Print(col_perms);", + f"col_perms := Filtered(SymmetricGroup({columns}), perm -> Order(perm) = {symmetry_length});Print(col_perms);", ) return parse_permutation_output(row_permutations_output), parse_permutation_output( col_permutations_output ) -""" -Gets a pair of permutations with a certain orbit length (if one exists) for a binary matrix -that meets the requirements to construct a balanced product code -""" - - def get_balanced_permutations_of_matrix( binaryMatrix: np.typing.NDArray[np.int_], symmetryLength: int ) -> tuple[qldpc.abstract.GroupMember, qldpc.abstract.GroupMember]: + """ + Gets a pair of permutations with a certain orbit length (if one exists) for a binary matrix + that meets the requirements to construct a balanced product code + """ + r_candidates, c_candidates = qldpc.external.groups.get_permutation_symmetry_of_matrix( symmetryLength, binaryMatrix.shape[0], binaryMatrix.shape[1] ) From ece3773ecc0545951cb11e9fb58feea2c5439035 Mon Sep 17 00:00:00 2001 From: ryankers Date: Tue, 19 Aug 2025 15:43:46 -0400 Subject: [PATCH 25/27] Restore original to_matrix --- qldpc/abstract.py | 16 ++++++++-------- qldpc/abstract_test.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/qldpc/abstract.py b/qldpc/abstract.py index fa893fdb9..2bcf00ce7 100644 --- a/qldpc/abstract.py +++ b/qldpc/abstract.py @@ -88,7 +88,7 @@ def __matmul__(self, other: GroupMember) -> GroupMember: """ return GroupMember(self.array_form + [val + self.size for val in other.array_form]) - def to_matrix(self, dimension: int | None = None) -> npt.NDArray[np.int_]: + def to_matrix(self, size: int | None = None) -> npt.NDArray[np.int_]: """Lift this permutation object to a permutation matrix. For consistency with how SymPy composes permutations, the permutation matrix constructed @@ -96,16 +96,16 @@ def to_matrix(self, dimension: int | None = None) -> npt.NDArray[np.int_]: convension ensures that this lift is a homomorphism on SymPy Permutation objects, which is to say that (p * q).to_matrix() = p.to_matrix() @ q.to_matrix(). - Dimension specifies to final size of the output matrix. Must be strictly larger than original + Size specifies to final size of the output matrix. Must be strictly larger than original size of the permutation. Each additional element is treated as a fixed point """ - size = dimension if dimension is not None else self.size - matrix = np.eye(size, dtype=int) + matrix = np.zeros([self.size] * 2, dtype=int) for ii in range(self.size): - j = self.apply(ii) - # Since using identity matrix not zero, have to clear the row - matrix[ii, :] = 0 - matrix[ii, j] = 1 + matrix[ii, self.apply(ii)] = 1 + + if size is not None: + if size > len(matrix): + matrix = scipy.linalg.block_diag(matrix, np.eye(size - len(matrix), dtype=int)) return matrix diff --git a/qldpc/abstract_test.py b/qldpc/abstract_test.py index c267dedb9..0f79181d9 100644 --- a/qldpc/abstract_test.py +++ b/qldpc/abstract_test.py @@ -25,7 +25,9 @@ import numpy as np import numpy.typing as npt import pytest +from sympy.combinatorics import Permutation +import qldpc from qldpc import abstract @@ -383,3 +385,12 @@ def test_small_group() -> None: group = abstract.SmallGroup(1, 1) assert group == abstract.TrivialGroup() assert group.random() == group.identity + + +def test_to_matrix() -> None: + perm = qldpc.abstract.GroupMember.from_sympy(Permutation(0, 2, 1)) + print("\n", perm.to_matrix()) + perm = qldpc.abstract.GroupMember.from_sympy(Permutation(3)(0, 2, 1)) + print("\n", perm.to_matrix()) + + ... From bca4dfadd41d4eae6e95a6e90a1c183d9660b559 Mon Sep 17 00:00:00 2001 From: ryankers Date: Tue, 19 Aug 2025 15:44:35 -0400 Subject: [PATCH 26/27] Parsing GAP generator out fnx --- qldpc/external/groups.py | 72 ++++++++++++++++------------------- qldpc/external/groups_test.py | 20 +++++----- 2 files changed, 43 insertions(+), 49 deletions(-) diff --git a/qldpc/external/groups.py b/qldpc/external/groups.py index 21a9d4f33..03886dd17 100644 --- a/qldpc/external/groups.py +++ b/qldpc/external/groups.py @@ -100,6 +100,25 @@ def get_small_group_structure(order: int, index: int) -> str: return name +def parse_cycles_from_gap_output(generators_str: str) -> GENERATORS_LIST: + generators = [] + for line in generators_str.splitlines(): + if not line.strip(): + continue + if not bool(re.search(r"\d", line)): + continue + # extract list of cycles, where each cycle is a tuple of integers + cycles_str = line[1:-1].split(")(") + try: + cycles = [tuple(map(int, cycle.split(","))) for cycle in cycles_str if cycle] + except ValueError: + raise ValueError(f"Cannot extract cycles from string: {line}") + # decrement integers in the cycle by 1 to account for 0-indexing + cycles = [tuple(index - 1 for index in cycle) for cycle in cycles] + generators.append(cycles) + return generators + + def get_generators_with_gap(group: str) -> GENERATORS_LIST | None: """Retrieve GAP group generators from GAP directly.""" @@ -117,24 +136,7 @@ def get_generators_with_gap(group: str) -> GENERATORS_LIST | None: r'for gen in gens do Print(gen, "\n"); od;', ] generators_str = qldpc.external.gap.get_output(*commands) - # collect generators - generators = [] - for line in generators_str.splitlines(): - if not line.strip(): - continue - - # extract list of cycles, where each cycle is a tuple of integers - cycles_str = line[1:-1].split(")(") - try: - cycles = [tuple(map(int, cycle.split(","))) for cycle in cycles_str if cycle] - except ValueError: - raise ValueError(f"Cannot extract cycles from string: {line}") - - # decrement integers in the cycle by 1 to account for 0-indexing - cycles = [tuple(index - 1 for index in cycle) for cycle in cycles] - generators.append(cycles) - - return generators + return parse_cycles_from_gap_output(generators_str) def get_generators_from_groupnames(group: str) -> GENERATORS_LIST | None: @@ -298,32 +300,23 @@ def get_permutation_symmetry_of_matrix( """ Gets all order l permutations for matrix of shape (rows,columns) """ - - # Parse output, account for zero-based indexing - def parse_permutation_output(output: str) -> list[qldpc.abstract.GroupMember]: - perm_list = [] - for perm in output.strip("[] ").split(", "): - result = [] - cycles = re.findall(r"\([^()]+\)", perm) - for cycle in cycles: - cycle = cycle.strip("()") - elements = tuple(int(x) - 1 for x in cycle.split(",")) - result.append(elements) - # Remove trivial permutation - if result: - perm_list.append(qldpc.abstract.GroupMember.from_sympy(Permutation(result))) - return perm_list - row_permutations_output = qldpc.external.gap.get_output( - f"row_perms := Filtered(SymmetricGroup({rows}), perm -> Order(perm) = {symmetry_length});Print(row_perms);", + f"row_perms := Filtered(SymmetricGroup({rows}), perm -> Order(perm) = {symmetry_length});", + r'for perm in row_perms do Print(perm, "\n"); od;', ) col_permutations_output = qldpc.external.gap.get_output( - f"col_perms := Filtered(SymmetricGroup({columns}), perm -> Order(perm) = {symmetry_length});Print(col_perms);", - ) - return parse_permutation_output(row_permutations_output), parse_permutation_output( - col_permutations_output + f"col_perms := Filtered(SymmetricGroup({columns}), perm -> Order(perm) = {symmetry_length});", + r'for perm in col_perms do Print(perm, "\n"); od;', ) + return [ + qldpc.abstract.GroupMember.from_sympy(Permutation(perm)) + for perm in parse_cycles_from_gap_output(row_permutations_output) + ], [ + qldpc.abstract.GroupMember.from_sympy(Permutation(perm)) + for perm in parse_cycles_from_gap_output(col_permutations_output) + ] + def get_balanced_permutations_of_matrix( binaryMatrix: np.typing.NDArray[np.int_], symmetryLength: int @@ -346,6 +339,7 @@ def get_balanced_permutations_of_matrix( binaryMatrix @ C.to_matrix(binaryMatrix.shape[1]).T, ): return R, C + raise ValueError( "Matrix doesn't have any permutations that meet required constraints" ) # pragma: no cover diff --git a/qldpc/external/groups_test.py b/qldpc/external/groups_test.py index c800fd459..53b497a75 100644 --- a/qldpc/external/groups_test.py +++ b/qldpc/external/groups_test.py @@ -258,12 +258,12 @@ def test_known_groups() -> None: def test_find_permutations() -> None: mocked_outputs = [ - "[ (1,4,3,2), (1,4,2,3), (1,2,4,3), (1,2,3,4), (1,3,4,2), (1,3,2,4) ]", - "[ (1,4,3,2), (1,4,2,3), (1,2,4,3), (1,2,3,4), (1,3,4,2), (1,3,2,4) ]", - "[ (2,3), (1,3), (1,2) ]", - "[ (1,2) ]", - "[ ]", - "[ ]", + "(1,4,3,2)\n(1,4,2,3)\n(1,2,4,3)\n(1,2,3,4)\n(1,3,4,2)\n(1,3,2,4)\n", + "(1,4,3,2)\n(1,4,2,3)\n(1,2,4,3)\n(1,2,3,4)\n(1,3,4,2)\n(1,3,2,4)\n", + "(2,3)\n(1,3)\n(1,2)\n", + "(1,2)\n", + "\n", + "\n", ] expected_4x4_order4 = ( @@ -303,10 +303,10 @@ def test_find_permutations() -> None: def test_find_balanced_permutations() -> None: mocked_output = [ - "[ (4,6,5), (4,5,6), (3,6,5), (3,6,4), (3,4,6), (3,4,5), (3,5,6), (3,5,4), (2,6,5), (2,6,4), (2,6,3), (2,3,6), (2,3,4), (2,3,5), (2,4,6), (2,4,5), (2,4,3), (2,5,6), (2,5,4), (2,5,3), (1,6,5), (1,6,4), (1,6,3), (1,6,2), (1,6,2)(3,4,5), (1,6,2)(3,5,4), (1,6,5)(2,3,4), (1,6,4)(2,3,5), (1,6,5)(2,4,3), (1,6,3)(2,4,5), (1,6,4)(2,5,3), (1,6,3)(2,5,4), (1,2,6), (1,2,6)(3,4,5), (1,2,6)(3,5,4), (1,2,3), (1,2,3)(4,6,5), (1,2,3)(4,5,6), (1,2,4), (1,2,4)(3,6,5), (1,2,4)(3,5,6), (1,2,5), (1,2,5)(3,6,4), (1,2,5)(3,4,6), (1,3,6), (1,3,4), (1,3,5), (1,3,2), (1,3,2)(4,6,5), (1,3,2)(4,5,6), (1,3,4)(2,6,5), (1,3,5)(2,6,4), (1,3,6)(2,4,5), (1,3,5)(2,4,6), (1,3,6)(2,5,4), (1,3,4)(2,5,6), (1,4,6), (1,4,5), (1,4,3), (1,4,2), (1,4,2)(3,6,5), (1,4,2)(3,5,6), (1,4,5)(2,3,6), (1,4,6)(2,3,5), (1,4,5)(2,6,3), (1,4,3)(2,6,5), (1,4,6)(2,5,3), (1,4,3)(2,5,6), (1,5,6), (1,5,4), (1,5,3), (1,5,2), (1,5,2)(3,4,6), (1,5,2)(3,6,4), (1,5,6)(2,3,4), (1,5,4)(2,3,6), (1,5,6)(2,4,3), (1,5,3)(2,4,6), (1,5,4)(2,6,3), (1,5,3)(2,6,4) ]", - "[ (4,6,5), (4,5,6), (3,6,5), (3,6,4), (3,4,6), (3,4,5), (3,5,6), (3,5,4), (2,6,5), (2,6,4), (2,6,3), (2,3,6), (2,3,4), (2,3,5), (2,4,6), (2,4,5), (2,4,3), (2,5,6), (2,5,4), (2,5,3), (1,6,5), (1,6,4), (1,6,3), (1,6,2), (1,6,2)(3,4,5), (1,6,2)(3,5,4), (1,6,5)(2,3,4), (1,6,4)(2,3,5), (1,6,5)(2,4,3), (1,6,3)(2,4,5), (1,6,4)(2,5,3), (1,6,3)(2,5,4), (1,2,6), (1,2,6)(3,4,5), (1,2,6)(3,5,4), (1,2,3), (1,2,3)(4,6,5), (1,2,3)(4,5,6), (1,2,4), (1,2,4)(3,6,5), (1,2,4)(3,5,6), (1,2,5), (1,2,5)(3,6,4), (1,2,5)(3,4,6), (1,3,6), (1,3,4), (1,3,5), (1,3,2), (1,3,2)(4,6,5), (1,3,2)(4,5,6), (1,3,4)(2,6,5), (1,3,5)(2,6,4), (1,3,6)(2,4,5), (1,3,5)(2,4,6), (1,3,6)(2,5,4), (1,3,4)(2,5,6), (1,4,6), (1,4,5), (1,4,3), (1,4,2), (1,4,2)(3,6,5), (1,4,2)(3,5,6), (1,4,5)(2,3,6), (1,4,6)(2,3,5), (1,4,5)(2,6,3), (1,4,3)(2,6,5), (1,4,6)(2,5,3), (1,4,3)(2,5,6), (1,5,6), (1,5,4), (1,5,3), (1,5,2), (1,5,2)(3,4,6), (1,5,2)(3,6,4), (1,5,6)(2,3,4), (1,5,4)(2,3,6), (1,5,6)(2,4,3), (1,5,3)(2,4,6), (1,5,4)(2,6,3), (1,5,3)(2,6,4) ]", - "[ ]", - "[ ]", + "(4,6,5)\n(4,5,6)\n(3,6,5)\n(3,6,4)\n(3,4,6)\n(3,4,5)\n(3,5,6)\n(3,5,4)\n(2,6,5)\n(2,6,4)\n(2,6,3)\n(2,3,6)\n(2,3,4)\n(2,3,5)\n(2,4,6)\n(2,4,5)\n(2,4,3)\n(2,5,6)\n(2,5,4)\n(2,5,3)\n(1,6,5)\n(1,6,4)\n(1,6,3)\n(1,6,2)\n(1,6,2)(3,4,5)\n(1,6,2)(3,5,4)\n(1,6,5)(2,3,4)\n(1,6,4)(2,3,5)\n(1,6,5)(2,4,3)\n(1,6,3)(2,4,5)\n(1,6,4)(2,5,3)\n(1,6,3)(2,5,4)\n(1,2,6)\n(1,2,6)(3,4,5)\n(1,2,6)(3,5,4)\n(1,2,3)\n(1,2,3)(4,6,5)\n(1,2,3)(4,5,6)\n(1,2,4)\n(1,2,4)(3,6,5)\n(1,2,4)(3,5,6)\n(1,2,5)\n(1,2,5)(3,6,4)\n(1,2,5)(3,4,6)\n(1,3,6)\n(1,3,4)\n(1,3,5)\n(1,3,2)\n(1,3,2)(4,6,5)\n(1,3,2)(4,5,6)\n(1,3,4)(2,6,5)\n(1,3,5)(2,6,4)\n(1,3,6)(2,4,5)\n(1,3,5)(2,4,6)\n(1,3,6)(2,5,4)\n(1,3,4)(2,5,6)\n(1,4,6)\n(1,4,5)\n(1,4,3)\n(1,4,2)\n(1,4,2)(3,6,5)\n(1,4,2)(3,5,6)\n(1,4,5)(2,3,6)\n(1,4,6)(2,3,5)\n(1,4,5)(2,6,3)\n(1,4,3)(2,6,5)\n(1,4,6)(2,5,3)\n(1,4,3)(2,5,6)\n(1,5,6)\n(1,5,4)\n(1,5,3)\n(1,5,2)\n(1,5,2)(3,4,6)\n(1,5,2)(3,6,4)\n(1,5,6)(2,3,4)\n(1,5,4)(2,3,6)\n(1,5,6)(2,4,3)\n(1,5,3)(2,4,6)\n(1,5,4)(2,6,3)\n(1,5,3)(2,6,4)\n", + "(4,6,5)\n(4,5,6)\n(3,6,5)\n(3,6,4)\n(3,4,6)\n(3,4,5)\n(3,5,6)\n(3,5,4)\n(2,6,5)\n(2,6,4)\n(2,6,3)\n(2,3,6)\n(2,3,4)\n(2,3,5)\n(2,4,6)\n(2,4,5)\n(2,4,3)\n(2,5,6)\n(2,5,4)\n(2,5,3)\n(1,6,5)\n(1,6,4)\n(1,6,3)\n(1,6,2)\n(1,6,2)(3,4,5)\n(1,6,2)(3,5,4)\n(1,6,5)(2,3,4)\n(1,6,4)(2,3,5)\n(1,6,5)(2,4,3)\n(1,6,3)(2,4,5)\n(1,6,4)(2,5,3)\n(1,6,3)(2,5,4)\n(1,2,6)\n(1,2,6)(3,4,5)\n(1,2,6)(3,5,4)\n(1,2,3)\n(1,2,3)(4,6,5)\n(1,2,3)(4,5,6)\n(1,2,4)\n(1,2,4)(3,6,5)\n(1,2,4)(3,5,6)\n(1,2,5)\n(1,2,5)(3,6,4)\n(1,2,5)(3,4,6)\n(1,3,6)\n(1,3,4)\n(1,3,5)\n(1,3,2)\n(1,3,2)(4,6,5)\n(1,3,2)(4,5,6)\n(1,3,4)(2,6,5)\n(1,3,5)(2,6,4)\n(1,3,6)(2,4,5)\n(1,3,5)(2,4,6)\n(1,3,6)(2,5,4)\n(1,3,4)(2,5,6)\n(1,4,6)\n(1,4,5)\n(1,4,3)\n(1,4,2)\n(1,4,2)(3,6,5)\n(1,4,2)(3,5,6)\n(1,4,5)(2,3,6)\n(1,4,6)(2,3,5)\n(1,4,5)(2,6,3)\n(1,4,3)(2,6,5)\n(1,4,6)(2,5,3)\n(1,4,3)(2,5,6)\n(1,5,6)\n(1,5,4)\n(1,5,3)\n(1,5,2)\n(1,5,2)(3,4,6)\n(1,5,2)(3,6,4)\n(1,5,6)(2,3,4)\n(1,5,4)(2,3,6)\n(1,5,6)(2,4,3)\n(1,5,3)(2,4,6)\n(1,5,4)(2,6,3)\n(1,5,3)(2,6,4)\n", + "\n", + "\n", ] with unittest.mock.patch("qldpc.external.gap.get_output", side_effect=mocked_output): matrix = np.array( From 7d528ef0514a75428e5a2e70a3588b8016ae2b57 Mon Sep 17 00:00:00 2001 From: RyanI70I Date: Thu, 20 Nov 2025 15:08:22 -0500 Subject: [PATCH 27/27] Basic Color codes --- qldpc/codes/__init__.py | 2 + qldpc/codes/quantum.py | 83 +++++++++++++++++++++++++++++++++++++ qldpc/codes/quantum_test.py | 14 +++++++ 3 files changed, 99 insertions(+) diff --git a/qldpc/codes/__init__.py b/qldpc/codes/__init__.py index 62beb7c54..2bd4cf918 100644 --- a/qldpc/codes/__init__.py +++ b/qldpc/codes/__init__.py @@ -25,6 +25,7 @@ BPCode, C4Code, C6Code, + CCode, FiveQubitCode, GeneralizedSurfaceCode, HGPCode, @@ -60,6 +61,7 @@ "BPCode", "C4Code", "C6Code", + "CCode", "FiveQubitCode", "GeneralizedSurfaceCode", "HGPCode", diff --git a/qldpc/codes/quantum.py b/qldpc/codes/quantum.py index c2046fbe0..4ba5d0b09 100644 --- a/qldpc/codes/quantum.py +++ b/qldpc/codes/quantum.py @@ -1728,3 +1728,86 @@ def __init__( self._dimension = dim_x * dim_z self._distance_x = code_z.get_distance() # X errors are witnessed by the Z code self._distance_z = code_x.get_distance() # Z errors are witnessed by the X code + +class CCode(CSSCode): + def __init__( + self, + colors: list[int], + plaquettes: list[list[list[int]]], + dimension : int + ) -> None: + if len(colors) != len(plaquettes): + raise ValueError("Every plaquette needs a color") + unique_colors = set() + for color in colors: + unique_colors.add(color) + if len(unique_colors) <= dimension: + raise ValueError("N-colorable, may only have n colors") + unique_qubits =set() # do i really need this + for index_f,f in enumerate(plaquettes): + for index_g,g in enumerate(plaquettes): + for edge_f,edge_g in itertools.product(f, g): + if edge_f == edge_g and colors[index_f] == colors[index_g] and index_f != index_g: + raise ValueError("Invalid coloring") + for qubit in edge_f + edge_g: + unique_qubits.add(qubit) + #each generator, loop through qubit add identity if in, otherwise not, find ordering of + ##add + code_x = [] + # code_z = [] + for plaquette in plaquettes: + qubits = set() + for edge in plaquette: + for qubit in edge: + qubits.add(qubit) + stabilizer = [] + for bit in range(0,len(unique_qubits)): + if bit in qubits: + stabilizer.append(1) + else: + stabilizer.append(0) + code_x.append(stabilizer) + + CSSCode.__init__(self,code_x,code_x) + +# +# ### +# r_C, n_C = code_c.matrix.shape +# +# high_distance = code_q.code_z +# low_distance = code_q.code_x +# if distance_balancing: +# if code_q.code_x.get_distance_exact() > code_q.code_z.get_distance_exact(): +# temp = high_distance +# high_distance = low_distance +# low_distance = temp +# +# r_low, n_low = low_distance.matrix.shape +# +# # Make new H_x +# Hx_tensor = np.kron(low_distance.matrix, np.eye(n_C, dtype=int)) +# classical_T_tensor = np.kron( +# np.eye(r_low, dtype=int), code_c.matrix.T +# ) # shape: (r_low * n_C, r_low * r_C) +# H_new_X = np.hstack((Hx_tensor, classical_T_tensor)) +# +# # Make new H_z +# Hz_tensor = np.kron(high_distance.matrix, np.eye(n_C, dtype=int)) +# +# bottom_left = np.kron(np.eye(n_low, dtype=int), code_c.matrix) +# bottom_right = np.kron(low_distance.matrix.T, np.eye(r_C, dtype=int)) +# +# top_right_zeros = np.zeros( +# (Hz_tensor.shape[0], bottom_left.shape[1] + bottom_right.shape[1] - Hz_tensor.shape[1]), +# dtype=int, +# ) +# top_row = np.hstack((Hz_tensor, top_right_zeros)) +# +# bottom_row = np.hstack((bottom_left, bottom_right)) +# +# H_new_Z = np.vstack((top_row, bottom_row)) +# +# new_code = cls.__new__(cls) +# CSSCode.__init__(new_code, H_new_X, H_new_Z) +# return new_code + diff --git a/qldpc/codes/quantum_test.py b/qldpc/codes/quantum_test.py index f1aa690be..5d3922217 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -651,3 +651,17 @@ def test_unbalanced_product_from_codes() -> None: == css.num_qubits * 5 + css.code_z.matrix.shape[0] * classical.matrix.shape[0] ) assert css.get_logical_ops().shape[0] == new_code.get_logical_ops().shape[0] + + +def test_color_code_2D() -> None: + colors = [3,2,1,2,1,3] + plaquettes = [ + [[0,1],[1,3],[3,0]], + [[0,2],[2,3],[3,0]], + [[5,1],[1,3],[3,2],[2,4],[4,5]], + [[5,4],[4,9],[9,8],[8,7],[7,5]], + [[7,8],[8,6],[6,7]], + [[8,0],[9,6],[6,8]] + ] + code = qldpc.codes.CCode(colors,plaquettes) + print(code.matrix)