diff --git a/qldpc/abstract.py b/qldpc/abstract.py index 041bd0235..2bcf00ce7 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, 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 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(). + + 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 """ matrix = np.zeros([self.size] * 2, dtype=int) for ii in range(self.size): 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 def to_gap_cycles(self) -> str: 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()) + + ... diff --git a/qldpc/codes/__init__.py b/qldpc/codes/__init__.py index d0ad6019d..2bd4cf918 100644 --- a/qldpc/codes/__init__.py +++ b/qldpc/codes/__init__.py @@ -22,8 +22,10 @@ from .quantum import ( BaconShorCode, BBCode, + BPCode, C4Code, C6Code, + CCode, FiveQubitCode, GeneralizedSurfaceCode, HGPCode, @@ -56,8 +58,10 @@ "QuditCode", "BaconShorCode", "BBCode", + "BPCode", "C4Code", "C6Code", + "CCode", "FiveQubitCode", "GeneralizedSurfaceCode", "HGPCode", diff --git a/qldpc/codes/quantum.py b/qldpc/codes/quantum.py index abd85b250..4ba5d0b09 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,119 @@ def __init__( CSSCode.__init__(self, matrix_x, matrix_z, field) +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. + + 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 + - https://arxiv.org/pdf/2505.13679 + + """ + + def __init__( + self, + 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") + + 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(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( + (seed_matrix.T, np.eye(seed_matrix.shape[0], dtype=int) + self._c.to_matrix()) + ) + 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, 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 + + 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 + + class BaconShorCode(SHPCode): """Bacon-Shor code on a square grid, implemented as a subsystem hypergraph product code. @@ -1614,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 fe5979d6c..5d3922217 100644 --- a/qldpc/codes/quantum_test.py +++ b/qldpc/codes/quantum_test.py @@ -24,8 +24,11 @@ import numpy as np import pytest import sympy +from sympy.combinatorics import Permutation +import qldpc.codes from qldpc import abstract, codes +from qldpc.codes import BPCode from qldpc.objects import ChainComplex, Node, Pauli @@ -556,3 +559,109 @@ 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], + ] + ) + + 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.BPCode(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 + + R, C = ( + 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): + codes.BPCode(matrix, R, C) + + matrix_extra_row = np.vstack([matrix, np.zeros((1, 6), dtype=int)]) + with pytest.raises(ValueError): + BPCode(matrix_extra_row, R, C) + + matrix[0][0] = 2 + with pytest.raises(ValueError): + 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.BPCode.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 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.BPCode.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)) + + classical = qldpc.codes.classical.RepetitionCode(5) + + new_code = qldpc.codes.BPCode.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 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) diff --git a/qldpc/external/groups.py b/qldpc/external/groups.py index 6185b9fc3..03886dd17 100644 --- a/qldpc/external/groups.py +++ b/qldpc/external/groups.py @@ -22,6 +22,8 @@ import urllib.request import galois +import numpy as np +from sympy.combinatorics import Permutation import qldpc.cache import qldpc.external.gap @@ -98,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.""" @@ -115,25 +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: @@ -291,6 +294,58 @@ def get_primitive_central_idempotents( return tuple(idempotents) +def get_permutation_symmetry_of_matrix( + 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) + """ + row_permutations_output = qldpc.external.gap.get_output( + 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});", + 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 +) -> 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] + ) + + 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" + ) # pragma: no cover + # shouldn't ever get to this point, don't require test coverage + + 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)], diff --git a/qldpc/external/groups_test.py b/qldpc/external/groups_test.py index ac11aae3c..53b497a75 100644 --- a/qldpc/external/groups_test.py +++ b/qldpc/external/groups_test.py @@ -21,9 +21,11 @@ import urllib import galois +import numpy as np import pytest +from sympy.combinatorics import Permutation -from qldpc import external +from qldpc import abstract, external # define global testing variables ORDER, INDEX = 2, 1 @@ -252,3 +254,80 @@ 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: + mocked_outputs = [ + "(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 = ( + [ + 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), + ], + ) + + expected_3x2_order2 = ( + [abstract.GroupMember(1, 2), abstract.GroupMember(0, 2), abstract.GroupMember(0, 1)], + [abstract.GroupMember(0, 1)], + ) + + expected_empty: tuple[list[abstract.GroupMember], list[abstract.GroupMember]] = ([], []) + + 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 + + +def test_find_balanced_permutations() -> None: + mocked_output = [ + "(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( + [ + [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)