From d9b5480b0f56829dc192f8213c7a0ec22c5418ee Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Fri, 19 Jun 2026 14:39:39 -0400 Subject: [PATCH 1/4] trivial group with no local gates --- src/qldpc/circuits/transversal.py | 3 +++ src/qldpc/circuits/transversal_test.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/qldpc/circuits/transversal.py b/src/qldpc/circuits/transversal.py index 09c26cb8d..bcfe73609 100644 --- a/src/qldpc/circuits/transversal.py +++ b/src/qldpc/circuits/transversal.py @@ -96,6 +96,9 @@ def get_transversal_automorphism_group( Uses the methods of https://arxiv.org/abs/2409.18175. """ + if not local_gates: + return abstract.TrivialGroup() + local_gates = _validate_local_gates(local_gates) allow_swaps = "SWAP" in local_gates local_gates.discard("SWAP") diff --git a/src/qldpc/circuits/transversal_test.py b/src/qldpc/circuits/transversal_test.py index 005afb490..84b5eb0b9 100644 --- a/src/qldpc/circuits/transversal_test.py +++ b/src/qldpc/circuits/transversal_test.py @@ -23,7 +23,7 @@ import pytest import stim -from qldpc import circuits, codes, external +from qldpc import abstract, circuits, codes, external def test_transversal_ops() -> None: @@ -40,6 +40,8 @@ def test_transversal_ops() -> None: transversal_ops = circuits.get_transversal_ops(code, local_gates) assert len(transversal_ops) == len(local_gates) + 1 + assert circuits.get_transversal_automorphism_group(code, []) == abstract.TrivialGroup() + with pytest.raises(ValueError, match="Local Clifford gates"): circuits.get_transversal_automorphism_group(code, ["SQRT_Y"]) From 1a5800c3b3d48ba59c0c54fea52a73670645b121 Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Fri, 19 Jun 2026 15:19:50 -0400 Subject: [PATCH 2/4] different trivial group --- src/qldpc/circuits/transversal.py | 2 +- src/qldpc/circuits/transversal_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qldpc/circuits/transversal.py b/src/qldpc/circuits/transversal.py index bcfe73609..4f352728c 100644 --- a/src/qldpc/circuits/transversal.py +++ b/src/qldpc/circuits/transversal.py @@ -97,7 +97,7 @@ def get_transversal_automorphism_group( Uses the methods of https://arxiv.org/abs/2409.18175. """ if not local_gates: - return abstract.TrivialGroup() + return abstract.Group(abstract.GroupMember(range(len(code)))) local_gates = _validate_local_gates(local_gates) allow_swaps = "SWAP" in local_gates diff --git a/src/qldpc/circuits/transversal_test.py b/src/qldpc/circuits/transversal_test.py index 84b5eb0b9..08f5fff20 100644 --- a/src/qldpc/circuits/transversal_test.py +++ b/src/qldpc/circuits/transversal_test.py @@ -23,7 +23,7 @@ import pytest import stim -from qldpc import abstract, circuits, codes, external +from qldpc import circuits, codes, external def test_transversal_ops() -> None: @@ -40,7 +40,7 @@ def test_transversal_ops() -> None: transversal_ops = circuits.get_transversal_ops(code, local_gates) assert len(transversal_ops) == len(local_gates) + 1 - assert circuits.get_transversal_automorphism_group(code, []) == abstract.TrivialGroup() + assert len(circuits.get_transversal_ops(code, [])) == 0 with pytest.raises(ValueError, match="Local Clifford gates"): circuits.get_transversal_automorphism_group(code, ["SQRT_Y"]) From a7938141cece3c751b4e776b24c13aeb6ec55e03 Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Sat, 20 Jun 2026 10:59:20 -0400 Subject: [PATCH 3/4] add unit test --- src/qldpc/circuits/transversal.py | 33 ++++++++++++++++++++--- src/qldpc/circuits/transversal_test.py | 25 ++++++++++-------- src/qldpc/external/groups.py | 36 ++++++++++++++++++++++---- 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/src/qldpc/circuits/transversal.py b/src/qldpc/circuits/transversal.py index 4f352728c..3994e96e5 100644 --- a/src/qldpc/circuits/transversal.py +++ b/src/qldpc/circuits/transversal.py @@ -125,10 +125,35 @@ def get_transversal_automorphism_group( # we are looking for transversal gates involving only two-qubit SWAPs matrix: npt.NDArray[np.int_] = parity_checks _code = codes.QuditCode(matrix).maybe_to_css() - if isinstance(_code, codes.CSSCode) and np.array_equal( - (canonicalized_code := _code.canonicalized).matrix_x, canonicalized_code.matrix_z - ): - matrix = canonicalized_code.matrix_x + if isinstance(_code, codes.CSSCode): + canonicalized_code = _code.canonicalized + if np.array_equal(canonicalized_code.matrix_x, canonicalized_code.matrix_z): + # Self-dual CSS: column permutations preserving H_x automatically preserve H_z + matrix = canonicalized_code.matrix_x + else: + # Non-self-dual CSS: the joint parity-check matrix has block-diagonal support, so + # its column-permutation automorphism group factors as Aut(H_x) x Aut(H_z) on + # disjoint column sets. SWAPs act diagonally on X- and Z-columns, so the SWAP + # transversal group is Aut(H_x) ∩ Aut(H_z). Recurse on fake self-dual codes built + # from H_x and H_z so the self-dual branch above handles each half, then intersect. + fake_code_x = codes.CSSCode( + canonicalized_code.matrix_x, canonicalized_code.matrix_x + ) + fake_code_z = codes.CSSCode( + canonicalized_code.matrix_z, canonicalized_code.matrix_z + ) + group_x = get_transversal_automorphism_group( + fake_code_x, ["SWAP"], deform_code=False, with_magma=with_magma + ) + group_z = get_transversal_automorphism_group( + fake_code_z, ["SWAP"], deform_code=False, with_magma=with_magma + ) + return abstract.Group( + *map( + abstract.GroupMember, + _sympy_group_intersection_generators(group_x, group_z), + ) + ) else: # we are looking for transversal gates involving two-qubit SWAPs and single-qubit Cliffords diff --git a/src/qldpc/circuits/transversal_test.py b/src/qldpc/circuits/transversal_test.py index 08f5fff20..fe9528c62 100644 --- a/src/qldpc/circuits/transversal_test.py +++ b/src/qldpc/circuits/transversal_test.py @@ -28,19 +28,22 @@ def test_transversal_ops() -> None: """Construct SWAP-transversal logical Cliffords of a code.""" - code = codes.ToricCode(2) - - for local_gates in [ - ["SWAP"], - ["SWAP", "H"], - ["SWAP", "S"], - ["SWAP", "SQRT_X"], - ["SWAP", "H", "S"], - ]: - transversal_ops = circuits.get_transversal_ops(code, local_gates) - assert len(transversal_ops) == len(local_gates) + 1 + code: codes.CSSCode + code = codes.ToricCode(2) assert len(circuits.get_transversal_ops(code, [])) == 0 + assert len(circuits.get_transversal_ops(code, ["SWAP"])) == 2 + assert len(circuits.get_transversal_ops(code, ["SWAP", "H"])) == 3 + assert len(circuits.get_transversal_ops(code, ["SWAP", "S"])) == 3 + assert len(circuits.get_transversal_ops(code, ["SWAP", "SQRT_X"])) == 3 + assert len(circuits.get_transversal_ops(code, ["SWAP", "H", "S"])) == 4 + + # non-self-dual [[8, 3, 2]] CSS code with nontrivial permutation automorphisms + code = codes.CSSCode( + [[1, 1, 1, 1, 0, 0, 0, 0], [1, 1, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 0, 0, 1, 1]], + [[1, 0, 1, 0, 1, 0, 1, 0], [0, 1, 0, 1, 0, 1, 0, 1]], + ) + assert len(circuits.get_transversal_ops(code, ["SWAP"])) == 3 with pytest.raises(ValueError, match="Local Clifford gates"): circuits.get_transversal_automorphism_group(code, ["SQRT_Y"]) diff --git a/src/qldpc/external/groups.py b/src/qldpc/external/groups.py index fe72cd6a3..d728de555 100644 --- a/src/qldpc/external/groups.py +++ b/src/qldpc/external/groups.py @@ -371,13 +371,15 @@ def get_primitive_central_idempotents(group: str, field: int) -> IdempotentsList "SmallGroup(1,1)": [[]], "Group(())": [[]], "SmallGroup(21,1)": [[(1, 2, 4), (3, 6, 5)], [(0, 1, 2, 3, 4, 5, 6)]], + # FiveQubitCode automorphism group (SWAP only) "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)], [(1, 5), (2, 7), (3, 9), (6, 8)], [(2, 6), (3, 8), (4, 5), (7, 9)], [(0, 9, 2, 8), (1, 6), (3, 5, 4, 7)], [(0, 7, 3), (1, 9, 8), (2, 4, 5)], - ], # FiveQubitCode automorphism group (SWAP only) + ], + # FiveQubitCode automorphism group (SWAP + Cliffords) "AutomorphismGroup(CheckMatCode([[1,0,0,0,1,1,1,0,1,1,0,1,0,1,0],[0,1,0,0,1,0,0,1,1,0,0,1,1,1,1],[0,0,1,0,1,1,1,0,0,0,1,1,1,0,1],[0,0,0,1,1,1,0,1,1,1,1,0,1,0,0]],GF(2)))": [ [(0, 1), (5, 12), (6, 14), (7, 9)], [(2, 3), (6, 9), (7, 14), (8, 11)], @@ -388,12 +390,14 @@ def get_primitive_central_idempotents(group: str, field: int) -> IdempotentsList [(2, 11), (3, 8), (6, 14), (7, 9)], [(3, 8), (4, 10), (5, 12), (7, 9)], [(3, 9), (4, 12), (5, 10), (7, 8)], - ], # FiveQubitCode automorphism group (SWAP + Cliffords) + ], + # ToricCode(2) automorphism group (SWAP only) "AutomorphismGroup(CheckMatCode([[1,1,1,1]],GF(2)))": [ [(0, 1)], [(1, 2)], [(2, 3)], - ], # ToricCode(2) automorphism group (SWAP only) + ], + # ToricCode(2) automorphism group (SWAP + H/S/SQRT_X) "AutomorphismGroup(CheckMatCode([[1,1,1,1,0,0,0,0],[0,0,0,0,1,1,1,1]],GF(2)))": [ [(2, 3), (4, 7), (5, 6)], [(4, 7, 6, 5)], @@ -403,7 +407,8 @@ def get_primitive_central_idempotents(group: str, field: int) -> IdempotentsList [(1, 3), (4, 6), (5, 7)], [(0, 6, 3, 7), (1, 5), (2, 4)], [(0, 7), (1, 4), (2, 5, 3, 6)], - ], # ToricCode(2) automorphism group (SWAP + H/S/SQRT_X) + ], + # ToricCode(2) automorphism group (SWAP + Cliffords) "AutomorphismGroup(CheckMatCode([[1,1,1,1,0,0,0,0,1,1,1,1],[0,0,0,0,1,1,1,1,1,1,1,1]],GF(2)))": [ [(4, 9), (5, 8, 6, 11), (7, 10)], [(4, 7, 6, 5), (9, 11, 10)], @@ -419,7 +424,28 @@ def get_primitive_central_idempotents(group: str, field: int) -> IdempotentsList [(0, 4, 10, 1, 5, 9, 2, 7, 8), (3, 6, 11)], [(2, 3), (4, 7), (8, 11)], [(0, 10, 2, 11, 1, 9), (3, 8), (4, 7)], - ], # ToricCode(2) automorphism group (SWAP + Cliffords) + ], + # [[8, 3, 2]] X check automorphism group + "AutomorphismGroup(CheckMatCode([[1,1,0,0,0,0,1,1],[0,0,1,1,0,0,1,1],[0,0,0,0,1,1,1,1]],GF(2)))": [ + [(6, 7)], + [(4, 5), (6, 7)], + [(4, 7, 5, 6)], + [(2, 3), (6, 7)], + [(2, 6, 4), (3, 7, 5)], + [(0, 3, 5, 1, 2, 4)], + [(0, 7), (1, 6), (2, 3), (4, 5)], + ], + # [[8, 3, 2]] Z check automorphism group + "AutomorphismGroup(CheckMatCode([[1,0,1,0,1,0,1,0],[0,1,0,1,0,1,0,1]],GF(2)))": [ + [(1, 7), (3, 5), (4, 6)], + [(1, 7, 5, 3)], + [(5, 7)], + [(3, 5, 7)], + [(1, 3, 5), (2, 4, 6)], + [(1, 5), (2, 6), (3, 7)], + [(0, 5, 6, 7), (1, 4), (2, 3)], + [(0, 7), (1, 2), (3, 6, 5, 4)], + ], } KNOWN_PRIMITIVE_CENTRAL_IDEMPOTENTS: dict[tuple[str, int], IdempotentsList] = { From 9c62151fedf49c4375f24efc8936e220413d9998 Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Sat, 20 Jun 2026 11:01:01 -0400 Subject: [PATCH 4/4] nit --- src/qldpc/circuits/transversal.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/qldpc/circuits/transversal.py b/src/qldpc/circuits/transversal.py index 3994e96e5..1b535e580 100644 --- a/src/qldpc/circuits/transversal.py +++ b/src/qldpc/circuits/transversal.py @@ -148,12 +148,8 @@ def get_transversal_automorphism_group( group_z = get_transversal_automorphism_group( fake_code_z, ["SWAP"], deform_code=False, with_magma=with_magma ) - return abstract.Group( - *map( - abstract.GroupMember, - _sympy_group_intersection_generators(group_x, group_z), - ) - ) + generators = _sympy_group_intersection_generators(group_x, group_z) + return abstract.Group(*map(abstract.GroupMember, generators)) else: # we are looking for transversal gates involving two-qubit SWAPs and single-qubit Cliffords