From ae6a174156ee9f68b49d47506f668f077bf3d72d Mon Sep 17 00:00:00 2001 From: armanda Date: Wed, 9 Jul 2025 12:52:30 +0200 Subject: [PATCH 1/2] Add a class for Quantum Tanner Codes https://arxiv.org/abs/2202.13641 --- quantum_tanner_codes/__init__.py | 0 quantum_tanner_codes/auxiliary/__init__.py | 0 .../auxiliary/binary_utils.py | 47 ++++ .../auxiliary/group_tables.py | 205 ++++++++++++++++ quantum_tanner_codes/auxiliary/groups.py | 198 +++++++++++++++ quantum_tanner_codes/qtc.py | 230 ++++++++++++++++++ 6 files changed, 680 insertions(+) create mode 100644 quantum_tanner_codes/__init__.py create mode 100644 quantum_tanner_codes/auxiliary/__init__.py create mode 100644 quantum_tanner_codes/auxiliary/binary_utils.py create mode 100644 quantum_tanner_codes/auxiliary/group_tables.py create mode 100644 quantum_tanner_codes/auxiliary/groups.py create mode 100644 quantum_tanner_codes/qtc.py diff --git a/quantum_tanner_codes/__init__.py b/quantum_tanner_codes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/quantum_tanner_codes/auxiliary/__init__.py b/quantum_tanner_codes/auxiliary/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/quantum_tanner_codes/auxiliary/binary_utils.py b/quantum_tanner_codes/auxiliary/binary_utils.py new file mode 100644 index 00000000..b3c1bcbd --- /dev/null +++ b/quantum_tanner_codes/auxiliary/binary_utils.py @@ -0,0 +1,47 @@ +import numpy as np +def gaussian_reduction(b): + """Uses gauss elimination to compute the spaces generated by a matrix over GF(2)\ + outputs: rank, a matrix whose rows are a basis of the kernel\ + a matrix of independent rows of b, which generate its image\ + a matrix of independent rows, that complete the rows of gauss['im']\ + to a basis of the space\ + the set of pivot row: indices of maximal set of independent row for b""" + a = np.copy(b) + m, n = a.shape + ker = np.eye(n).astype(int) + pivot = set() + pivot_row = set() + for j in range(n): + i = 0 + while a[i][j] != 1 and i < m - 1: + i += 1 + if a[i][j] == 1: + pivot.add(j) + pivot_row.add(i) + for k in range(j + 1, n): + if a[i][k] == 1: + a[:, k] = (a[:, j] + a[:, k]) % 2 + ker[:, k] = (ker[:, j] + ker[:, k]) % 2 + indices = [j for j in range(n) if j not in pivot] + ker = (ker[:, indices]).T + rk = len(pivot) + image = b[list(pivot_row), :] + complete_to_basis = np.eye(n).astype(int)[indices, :] + return {'rank': rk, 'ker': ker, 'im': image, 'complete_to_basis': complete_to_basis, 'pivot_row': pivot_row, + 'indices': indices} + + +def get_support(m): + """ + A matrix, print support as pair + :param m: + :return: + """ + indices = m.nonzero() + if len(indices) == 2: + rows, cols = indices + out = [(rows[i], cols[i]) for i in range(len(rows))] + else: + rows = indices[0] + out = [rows[i] for i in range(len(rows))] + return out \ No newline at end of file diff --git a/quantum_tanner_codes/auxiliary/group_tables.py b/quantum_tanner_codes/auxiliary/group_tables.py new file mode 100644 index 00000000..e4824ae4 --- /dev/null +++ b/quantum_tanner_codes/auxiliary/group_tables.py @@ -0,0 +1,205 @@ +""" + Here we save some group tables for testing. + Group tables are as per GAP output MultiplicationTable(G), where G is a group. + The identity here is element 0, while in gap is element 1, so all the tables are shifted for consistence + i.e. table = gap_table - 1 +""" + +import numpy as np + + +klein_table = np.array([[1, 2, 3, 4], + [2, 1, 4, 3], + [3, 4, 1, 2], + [4, 3, 2, 1]]) + +klein_table = klein_table - 1 + +quaternion = np.array([[1, 2, 3, 4, 5, 6, 7, 8], + [2, 4, 5, 6, 7, 1, 8, 3], + [3, 8, 4, 7, 2, 5, 1, 6], + [4, 6, 7, 1, 8, 2, 3, 5], + [5, 3, 6, 8, 4, 7, 2, 1], + [6, 1, 8, 2, 3, 4, 5, 7], + [7, 5, 1, 3, 6, 8, 4, 2], + [8, 7, 2, 5, 1, 3, 6, 4]]) + +quaternion = quaternion - 1 + +quaternion_3 = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + [2, 3, 5, 6, 1, 7, 9, 10, 4, 11, 12, 8], + [3, 5, 1, 7, 2, 9, 4, 11, 6, 12, 8, 10], + [4, 10, 7, 8, 12, 2, 11, 1, 5, 6, 3, 9], + [5, 1, 2, 9, 3, 4, 6, 12, 7, 8, 10, 11], + [6, 11, 9, 10, 8, 3, 12, 2, 1, 7, 5, 4], + [7, 12, 4, 11, 10, 5, 8, 3, 2, 9, 1, 6], + [8, 6, 11, 1, 9, 10, 3, 4, 12, 2, 7, 5], + [9, 8, 6, 12, 11, 1, 10, 5, 3, 4, 2, 7], + [10, 7, 12, 2, 4, 11, 5, 6, 8, 3, 9, 1], + [11, 9, 8, 3, 6, 12, 1, 7, 10, 5, 4, 2], + [12, 4, 10, 5, 7, 8, 2, 9, 11, 1, 6, 3]]) + +quaternion_3 = quaternion_3 - 1 + +quaternion_4 = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + [2, 5, 6, 7, 8, 10, 11, 1, 12, 13, 14, 15, 3, 4, 16, 9], + [3, 12, 5, 9, 10, 7, 13, 16, 11, 1, 15, 8, 14, 6, 4, 2], + [4, 14, 15, 5, 11, 12, 2, 7, 3, 9, 1, 13, 16, 8, 10, 6], + [5, 8, 10, 11, 1, 13, 14, 2, 15, 3, 4, 16, 6, 7, 9, 12], + [6, 15, 8, 12, 13, 11, 3, 9, 14, 2, 16, 1, 4, 10, 7, 5], + [7, 4, 16, 8, 14, 15, 5, 11, 6, 12, 2, 3, 9, 1, 13, 10], + [8, 1, 13, 14, 2, 3, 4, 5, 16, 6, 7, 9, 10, 11, 12, 15], + [9, 6, 4, 10, 15, 8, 12, 13, 5, 11, 3, 14, 2, 16, 1, 7], + [10, 16, 1, 15, 3, 14, 6, 12, 4, 5, 9, 2, 7, 13, 11, 8], + [11, 7, 9, 1, 4, 16, 8, 14, 10, 15, 5, 6, 12, 2, 3, 13], + [12, 10, 7, 13, 16, 1, 15, 3, 8, 14, 6, 4, 5, 9, 2, 11], + [13, 9, 2, 16, 6, 4, 10, 15, 7, 8, 12, 5, 11, 3, 14, 1], + [14, 11, 12, 2, 7, 9, 1, 4, 13, 16, 8, 10, 15, 5, 6, 3], + [15, 13, 11, 3, 9, 2, 16, 6, 1, 4, 10, 7, 8, 12, 5, 14], + [16, 3, 14, 6, 12, 5, 9, 10, 2, 7, 13, 11, 1, 15, 8, 4]]) + +quaternion_4 = quaternion_4 - 1 + +quaternion_5 = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + [2, 3, 5, 6, 1, 7, 9, 10, 4, 11, 13, 14, 8, 15, 17, 18, 12, 19, 20, 16], + [3, 5, 1, 7, 2, 9, 4, 11, 6, 13, 8, 15, 10, 17, 12, 19, 14, 20, 16, 18], + [4, 18, 7, 8, 20, 2, 11, 12, 5, 6, 15, 16, 9, 10, 19, 1, 13, 14, 3, 17], + [5, 1, 2, 9, 3, 4, 6, 13, 7, 8, 10, 17, 11, 12, 14, 20, 15, 16, 18, 19], + [6, 19, 9, 10, 16, 3, 13, 14, 1, 7, 17, 18, 4, 11, 20, 2, 8, 15, 5, 12], + [7, 20, 4, 11, 18, 5, 8, 15, 2, 9, 12, 19, 6, 13, 16, 3, 10, 17, 1, 14], + [8, 14, 11, 12, 17, 18, 15, 16, 20, 2, 19, 1, 5, 6, 3, 4, 9, 10, 7, 13], + [9, 16, 6, 13, 19, 1, 10, 17, 3, 4, 14, 20, 7, 8, 18, 5, 11, 12, 2, 15], + [10, 15, 13, 14, 12, 19, 17, 18, 16, 3, 20, 2, 1, 7, 5, 6, 4, 11, 9, 8], + [11, 17, 8, 15, 14, 20, 12, 19, 18, 5, 16, 3, 2, 9, 1, 7, 6, 13, 4, 10], + [12, 10, 15, 16, 13, 14, 19, 1, 17, 18, 3, 4, 20, 2, 7, 8, 5, 6, 11, 9], + [13, 12, 10, 17, 15, 16, 14, 20, 19, 1, 18, 5, 3, 4, 2, 9, 7, 8, 6, 11], + [14, 11, 17, 18, 8, 15, 20, 2, 12, 19, 5, 6, 16, 3, 9, 10, 1, 7, 13, 4], + [15, 13, 12, 19, 10, 17, 16, 3, 14, 20, 1, 7, 18, 5, 4, 11, 2, 9, 8, 6], + [16, 6, 19, 1, 9, 10, 3, 4, 13, 14, 7, 8, 17, 18, 11, 12, 20, 2, 15, 5], + [17, 8, 14, 20, 11, 12, 18, 5, 15, 16, 2, 9, 19, 1, 6, 13, 3, 4, 10, 7], + [18, 7, 20, 2, 4, 11, 5, 6, 8, 15, 9, 10, 12, 19, 13, 14, 16, 3, 17, 1], + [19, 9, 16, 3, 6, 13, 1, 7, 10, 17, 4, 11, 14, 20, 8, 15, 18, 5, 12, 2], + [20, 4, 18, 5, 7, 8, 2, 9, 11, 12, 6, 13, 15, 16, 10, 17, 19, 1, 14, 3]]) +quaternion_5 = quaternion_5 - 1 + +quaternion_6 = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], + [2, 4, 6, 7, 8, 9, 1, 11, 13, 14, 15, 16, 3, 17, 5, 19, 20, 21, 22, 10, 23, 12, 24, 18], + [3, 13, 4, 9, 10, 2, 6, 20, 1, 11, 17, 18, 7, 8, 14, 24, 5, 19, 23, 15, 16, 21, 12, 22], + [4, 7, 9, 1, 11, 13, 2, 15, 3, 17, 5, 19, 6, 20, 8, 22, 10, 23, 12, 14, 24, 16, 18, 21], + [5, 16, 10, 11, 12, 21, 22, 2, 17, 18, 19, 1, 24, 6, 7, 8, 23, 3, 4, 13, 14, 15, 9, 20], + [6, 3, 7, 13, 14, 4, 9, 10, 2, 15, 20, 21, 1, 11, 17, 18, 8, 22, 24, 5, 19, 23, 16, 12], + [7, 1, 13, 2, 15, 3, 4, 5, 6, 20, 8, 22, 9, 10, 11, 12, 14, 24, 16, 17, 18, 19, 21, 23], + [8, 19, 14, 15, 16, 23, 12, 4, 20, 21, 22, 2, 18, 9, 1, 11, 24, 6, 7, 3, 17, 5, 13, 10], + [9, 6, 1, 3, 17, 7, 13, 14, 4, 5, 10, 23, 2, 15, 20, 21, 11, 12, 18, 8, 22, 24, 19, 16], + [10, 24, 11, 17, 18, 16, 21, 13, 5, 19, 23, 3, 22, 2, 6, 20, 12, 4, 9, 7, 8, 14, 1, 15], + [11, 22, 17, 5, 19, 24, 16, 7, 10, 23, 12, 4, 21, 13, 2, 15, 18, 9, 1, 6, 20, 8, 3, 14], + [12, 8, 18, 19, 1, 14, 15, 16, 23, 3, 4, 5, 20, 21, 22, 2, 9, 10, 11, 24, 6, 7, 17, 13], + [13, 9, 2, 6, 20, 1, 3, 17, 7, 8, 14, 24, 4, 5, 10, 23, 15, 16, 21, 11, 12, 18, 22, 19], + [14, 18, 15, 20, 21, 19, 23, 3, 8, 22, 24, 6, 12, 4, 9, 10, 16, 7, 13, 1, 11, 17, 2, 5], + [15, 12, 20, 8, 22, 18, 19, 1, 14, 24, 16, 7, 23, 3, 4, 5, 21, 13, 2, 9, 10, 11, 6, 17], + [16, 11, 21, 22, 2, 17, 5, 19, 24, 6, 7, 8, 10, 23, 12, 4, 13, 14, 15, 18, 9, 1, 20, 3], + [17, 21, 5, 10, 23, 22, 24, 6, 11, 12, 18, 9, 16, 7, 13, 14, 19, 1, 3, 2, 15, 20, 4, 8], + [18, 20, 19, 23, 3, 8, 14, 24, 12, 4, 9, 10, 15, 16, 21, 13, 1, 11, 17, 22, 2, 6, 5, 7], + [19, 15, 23, 12, 4, 20, 8, 22, 18, 9, 1, 11, 14, 24, 16, 7, 3, 17, 5, 21, 13, 2, 10, 6], + [20, 23, 8, 14, 24, 12, 18, 9, 15, 16, 21, 13, 19, 1, 3, 17, 22, 2, 6, 4, 5, 10, 7, 11], + [21, 10, 22, 24, 6, 11, 17, 18, 16, 7, 13, 14, 5, 19, 23, 3, 2, 15, 20, 12, 4, 9, 8, 1], + [22, 5, 24, 16, 7, 10, 11, 12, 21, 13, 2, 15, 17, 18, 19, 1, 6, 20, 8, 23, 3, 4, 14, 9], + [23, 14, 12, 18, 9, 15, 20, 21, 19, 1, 3, 17, 8, 22, 24, 6, 4, 5, 10, 16, 7, 13, 11, 2], + [24, 17, 16, 21, 13, 5, 10, 23, 22, 2, 6, 20, 11, 12, 18, 9, 7, 8, 14, 19, 1, 3, 15, 4]]) + +quaternion_6 = quaternion_6 - 1 + +d12_2 = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], + [2, 4, 6, 7, 8, 9, 1, 11, 13, 14, 15, 16, 3, 17, 5, 19, 20, 21, 22, 10, 23, 12, 24, 18], + [3, 6, 1, 9, 10, 2, 13, 14, 4, 5, 17, 18, 7, 8, 20, 21, 11, 12, 23, 15, 16, 24, 19, 22], + [4, 7, 9, 1, 11, 13, 2, 15, 3, 17, 5, 19, 6, 20, 8, 22, 10, 23, 12, 14, 24, 16, 18, 21], + [5, 16, 10, 11, 12, 21, 22, 2, 17, 18, 19, 1, 24, 6, 7, 8, 23, 3, 4, 13, 14, 15, 9, 20], + [6, 9, 2, 13, 14, 4, 3, 17, 7, 8, 20, 21, 1, 11, 10, 23, 15, 16, 24, 5, 19, 18, 22, 12], + [7, 1, 13, 2, 15, 3, 4, 5, 6, 20, 8, 22, 9, 10, 11, 12, 14, 24, 16, 17, 18, 19, 21, 23], + [8, 19, 14, 15, 16, 23, 12, 4, 20, 21, 22, 2, 18, 9, 1, 11, 24, 6, 7, 3, 17, 5, 13, 10], + [9, 13, 4, 3, 17, 7, 6, 20, 1, 11, 10, 23, 2, 15, 14, 24, 5, 19, 18, 8, 22, 21, 12, 16], + [10, 21, 5, 17, 18, 16, 24, 6, 11, 12, 23, 3, 22, 2, 13, 14, 19, 1, 9, 7, 8, 20, 4, 15], + [11, 22, 17, 5, 19, 24, 16, 7, 10, 23, 12, 4, 21, 13, 2, 15, 18, 9, 1, 6, 20, 8, 3, 14], + [12, 8, 18, 19, 1, 14, 15, 16, 23, 3, 4, 5, 20, 21, 22, 2, 9, 10, 11, 24, 6, 7, 17, 13], + [13, 3, 7, 6, 20, 1, 9, 10, 2, 15, 14, 24, 4, 5, 17, 18, 8, 22, 21, 11, 12, 23, 16, 19], + [14, 23, 8, 20, 21, 19, 18, 9, 15, 16, 24, 6, 12, 4, 3, 17, 22, 2, 13, 1, 11, 10, 7, 5], + [15, 12, 20, 8, 22, 18, 19, 1, 14, 24, 16, 7, 23, 3, 4, 5, 21, 13, 2, 9, 10, 11, 6, 17], + [16, 11, 21, 22, 2, 17, 5, 19, 24, 6, 7, 8, 10, 23, 12, 4, 13, 14, 15, 18, 9, 1, 20, 3], + [17, 24, 11, 10, 23, 22, 21, 13, 5, 19, 18, 9, 16, 7, 6, 20, 12, 4, 3, 2, 15, 14, 1, 8], + [18, 14, 12, 23, 3, 8, 20, 21, 19, 1, 9, 10, 15, 16, 24, 6, 4, 5, 17, 22, 2, 13, 11, 7], + [19, 15, 23, 12, 4, 20, 8, 22, 18, 9, 1, 11, 14, 24, 16, 7, 3, 17, 5, 21, 13, 2, 10, 6], + [20, 18, 15, 14, 24, 12, 23, 3, 8, 22, 21, 13, 19, 1, 9, 10, 16, 7, 6, 4, 5, 17, 2, 11], + [21, 17, 16, 24, 6, 11, 10, 23, 22, 2, 13, 14, 5, 19, 18, 9, 7, 8, 20, 12, 4, 3, 15, 1], + [22, 5, 24, 16, 7, 10, 11, 12, 21, 13, 2, 15, 17, 18, 19, 1, 6, 20, 8, 23, 3, 4, 14, 9], + [23, 20, 19, 18, 9, 15, 14, 24, 12, 4, 3, 17, 8, 22, 21, 13, 1, 11, 10, 16, 7, 6, 5, 2], + [24, 10, 22, 21, 13, 5, 17, 18, 16, 7, 6, 20, 11, 12, 23, 3, 2, 15, 14, 19, 1, 9, 8, 4]]) + +d12_2 = d12_2 - 1 + + +table_512 = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32], + [2, 5, 7, 8, 9, 10, 12, 14, 1, 16, 17, 18, 19, 20, 21, 22, 23, 3, 25, 4, 26, 6, 27, 28, 29, 30, + 11, 31, 13, 15, 32, 24], + [3, 7, 6, 11, 12, 13, 10, 17, 18, 19, 15, 16, 1, 23, 24, 25, 21, 22, 2, 27, 28, 29, 26, 4, 5, 31, + 30, 8, 9, 32, 14, 20], + [4, 8, 11, 1, 14, 15, 17, 2, 20, 21, 3, 23, 24, 5, 6, 26, 7, 27, 28, 9, 10, 30, 12, 13, 31, 16, + 18, 19, 32, 22, 25, 29], + [5, 9, 12, 14, 1, 16, 18, 20, 2, 22, 23, 3, 25, 4, 26, 6, 27, 7, 29, 8, 30, 10, 11, 31, 13, 15, + 17, 32, 19, 21, 24, 28], + [6, 10, 13, 15, 16, 1, 19, 21, 22, 2, 24, 25, 3, 26, 4, 5, 28, 29, 7, 30, 8, 9, 31, 11, 12, 14, + 32, 17, 18, 20, 23, 27], + [7, 12, 10, 17, 18, 19, 16, 23, 3, 25, 21, 22, 2, 27, 28, 29, 26, 6, 5, 11, 31, 13, 30, 8, 9, 32, + 15, 14, 1, 24, 20, 4], + [8, 14, 17, 2, 20, 21, 23, 5, 4, 26, 7, 27, 28, 9, 10, 30, 12, 11, 31, 1, 16, 15, 18, 19, 32, 22, + 3, 25, 24, 6, 29, 13], + [9, 1, 18, 20, 2, 22, 3, 4, 5, 6, 27, 7, 29, 8, 30, 10, 11, 12, 13, 14, 15, 16, 17, 32, 19, 21, + 23, 24, 25, 26, 28, 31], + [10, 16, 19, 21, 22, 2, 25, 26, 6, 5, 28, 29, 7, 30, 8, 9, 31, 13, 12, 15, 14, 1, 32, 17, 18, 20, + 24, 23, 3, 4, 27, 11], + [11, 17, 15, 3, 23, 24, 21, 7, 27, 28, 6, 26, 4, 12, 13, 31, 10, 30, 8, 18, 19, 32, 16, 1, 14, 25, + 22, 2, 20, 29, 5, 9], + [12, 18, 16, 23, 3, 25, 22, 27, 7, 29, 26, 6, 5, 11, 31, 13, 30, 10, 9, 17, 32, 19, 15, 14, 1, 24, + 21, 20, 2, 28, 4, 8], + [13, 19, 1, 24, 25, 3, 2, 28, 29, 7, 4, 5, 6, 31, 11, 12, 8, 9, 10, 32, 17, 18, 14, 15, 16, 23, + 20, 21, 22, 27, 26, 30], + [14, 20, 23, 5, 4, 26, 27, 9, 8, 30, 12, 11, 31, 1, 16, 15, 18, 17, 32, 2, 22, 21, 3, 25, 24, 6, + 7, 29, 28, 10, 13, 19], + [15, 21, 24, 6, 26, 4, 28, 10, 30, 8, 13, 31, 11, 16, 1, 14, 19, 32, 17, 22, 2, 20, 25, 3, 23, 5, + 29, 7, 27, 9, 12, 18], + [16, 22, 25, 26, 6, 5, 29, 30, 10, 9, 31, 13, 12, 15, 14, 1, 32, 19, 18, 21, 20, 2, 24, 23, 3, 4, + 28, 27, 7, 8, 11, 17], + [17, 23, 21, 7, 27, 28, 26, 12, 11, 31, 10, 30, 8, 18, 19, 32, 16, 15, 14, 3, 25, 24, 22, 2, 20, + 29, 6, 5, 4, 13, 9, 1], + [18, 3, 22, 27, 7, 29, 6, 11, 12, 13, 30, 10, 9, 17, 32, 19, 15, 16, 1, 23, 24, 25, 21, 20, 2, 28, + 26, 4, 5, 31, 8, 14], + [19, 25, 2, 28, 29, 7, 5, 31, 13, 12, 8, 9, 10, 32, 17, 18, 14, 1, 16, 24, 23, 3, 20, 21, 22, 27, + 4, 26, 6, 11, 30, 15], + [20, 4, 27, 9, 8, 30, 11, 1, 14, 15, 18, 17, 32, 2, 22, 21, 3, 23, 24, 5, 6, 26, 7, 29, 28, 10, + 12, 13, 31, 16, 19, 25], + [21, 26, 28, 10, 30, 8, 31, 16, 15, 14, 19, 32, 17, 22, 2, 20, 25, 24, 23, 6, 5, 4, 29, 7, 27, 9, + 13, 12, 11, 1, 18, 3], + [22, 6, 29, 30, 10, 9, 13, 15, 16, 1, 32, 19, 18, 21, 20, 2, 24, 25, 3, 26, 4, 5, 28, 27, 7, 8, + 31, 11, 12, 14, 17, 23], + [23, 27, 26, 12, 11, 31, 30, 18, 17, 32, 16, 15, 14, 3, 25, 24, 22, 21, 20, 7, 29, 28, 6, 5, 4, + 13, 10, 9, 8, 19, 1, 2], + [24, 28, 4, 13, 31, 11, 8, 19, 32, 17, 1, 14, 15, 25, 3, 23, 2, 20, 21, 29, 7, 27, 5, 6, 26, 12, + 9, 10, 30, 18, 16, 22], + [25, 29, 5, 31, 13, 12, 9, 32, 19, 18, 14, 1, 16, 24, 23, 3, 20, 2, 22, 28, 27, 7, 4, 26, 6, 11, + 8, 30, 10, 17, 15, 21], + [26, 30, 31, 16, 15, 14, 32, 22, 21, 20, 25, 24, 23, 6, 5, 4, 29, 28, 27, 10, 9, 8, 13, 12, 11, 1, + 19, 18, 17, 2, 3, 7], + [27, 11, 30, 18, 17, 32, 15, 3, 23, 24, 22, 21, 20, 7, 29, 28, 6, 26, 4, 12, 13, 31, 10, 9, 8, 19, + 16, 1, 14, 25, 2, 5], + [28, 31, 8, 19, 32, 17, 14, 25, 24, 23, 2, 20, 21, 29, 7, 27, 5, 4, 26, 13, 12, 11, 9, 10, 30, 18, + 1, 16, 15, 3, 22, 6], + [29, 13, 9, 32, 19, 18, 1, 24, 25, 3, 20, 2, 22, 28, 27, 7, 4, 5, 6, 31, 11, 12, 8, 30, 10, 17, + 14, 15, 16, 23, 21, 26], + [30, 15, 32, 22, 21, 20, 24, 6, 26, 4, 29, 28, 27, 10, 9, 8, 13, 31, 11, 16, 1, 14, 19, 18, 17, 2, + 25, 3, 23, 5, 7, 12], + [31, 32, 14, 25, 24, 23, 20, 29, 28, 27, 5, 4, 26, 13, 12, 11, 9, 8, 30, 19, 18, 17, 1, 16, 15, 3, + 2, 22, 21, 7, 6, 10], + [32, 24, 20, 29, 28, 27, 4, 13, 31, 11, 9, 8, 30, 19, 18, 17, 1, 14, 15, 25, 3, 23, 2, 22, 21, 7, + 5, 6, 26, 12, 10, 16]]) + +table_512 = table_512 - 1 diff --git a/quantum_tanner_codes/auxiliary/groups.py b/quantum_tanner_codes/auxiliary/groups.py new file mode 100644 index 00000000..04fb4abe --- /dev/null +++ b/quantum_tanner_codes/auxiliary/groups.py @@ -0,0 +1,198 @@ +from collections import defaultdict +import numpy as np + + +class G: + """ + G is a group described in terms of its Cayley (multiplication) table. + + The group elements are represented by integers from 0 to n-1, where + `n` is the order of the group. The identity element is assumed to be 0. + """ + def __init__(self, table): + """ + Initialize the group. + + :param table: A square NumPy array representing the Cayley table + of the group. Entry (i, j) gives the product of + group elements i and j. The identity element must + be at index 0. + """ + self.table = table + self.n = table.shape[0] + self.pairs = self.get_pairs() + + def is_abelian(self): + """ + Check if the group is Abelian (i.e., commutative). + + :return: True if the Cayley table is symmetric, False otherwise. + """ + return np.array_equal(self.table, self.table.T) + + def inv(self, el): + """ + Find the inverse of a given group element. + + :param el: Integer index of the group element. + :return: Integer index of the inverse element, i.e., the unique element `x` + such that table[el, x] == 0 (the identity). + + """ + row = self.table[el] + return np.where(row == 0)[0][0] + + def op(self, i, j): + + """ + Perform the group operation on two elements. + + :param i: Integer index of the first element. + :param j: Integer index of the second element. + :return: Integer index of the product i * j. + """ + return self.table[i, j] + + def ord(self, el): + """ + :param el: Integer index of the element. + :return: Order of the element (smallest positive k such that el^k = 0). + """ + res = el + order = 1 + while res != 0: + res = self.op(el, res) + order += 1 + return order + + def right_cayley(self, set_a): + """ + :param set_a: Subset of group elements (by index) used as right generators. + :return: Set of undirected edges in the right Cayley graph. + """ + + edges = set() + # iterates all elements + for g in range(self.n): + for a in set_a: + edges.add((g, self.op(g, a))) + edges = {(min(ed), max(ed)) for ed in edges} + return edges + + def left_cayley(self, set_a): + """ + :param set_a: Subset of group elements (by index) used as left generators. + :return: Set of undirected edges in the left Cayley graph. + """ + edges = set() + # iterates all elements + for g in range(self.n): + for a in set_a: + edges.add((g, self.op(a, g))) + edges = {(min(ed), max(ed)) for ed in edges} + return edges + + def get_pairs(self): + """ + :return: Dictionary mapping element order k to list of inverse pairs (i, j). + """ + + pairs = set() + for i in range(self.n): + pair = tuple(sorted({i, self.inv(i)})) + pairs.add(pair) + order_and_pairs = defaultdict(list) + for pair in pairs: + k = self.ord(pair[0]) + order_and_pairs[k].append(pair) + return order_and_pairs + + +def cyclic_group(n): + """ + :param n: Order of the cyclic group. + :return: Cayley table of the cyclic group of order n (Z/nZ). + """ + + row = np.array([i for i in range(n)]) + table = [] + for i in range(n, 0, -1): + table.append(np.roll(row, i)) + return np.vstack(table) + + +def direct_product(table_1, table_2): + """ + :param table_1: Cayley table of the first group (NumPy array). + :param table_2: Cayley table of the second group (NumPy array). + :return: Cayley table of the direct product group. + + Table of Z3 x Z2 has [0, 1, 2, 3, 4, 5] --> [00, 01, 10, 11, 20, 21] + Table of (Z3 x Z2) X Z2 --> [000, 001, 010, 011, 100, 101, 110, 111, 200, 201, 210, 211] + [0 of first ]x i of second -- [1 of first ]x i of second -- .. + my_ord = [] + for i in range(4): + for j in range(4): + for k in range(2): + my_ord.append((i, j, k)) + + """ + + n_1 = table_1.shape[0] + n_2 = table_2.shape[1] + + def small_indices(el): + i_1 = el // n_2 + i_2 = el % n_2 + return i_1, i_2 + + def big_index(i_1, i_2): + return i_1 * n_2 + i_2 + + n = n_1 * n_2 + table = np.zeros([n, n]).astype(int) + for g_i in range(n): + for g_j in range(n): + g_i_1, g_i_2 = small_indices(g_i) + g_j_1, g_j_2 = small_indices(g_j) + one = table_1[g_i_1, g_j_1] + two = table_2[g_i_2, g_j_2] + table[g_i, g_j] = big_index(one, two) + + return table + + +def from_tuple_to_direct_product_index(table_1, table_2, el): + """ + :param table_1: Cayley table of the first group. + :param table_2: Cayley table of the second group. + :param el: Tuple (i, j) representing the element in the product group. + :return: Flat index in the Cayley table of the direct product. + """ + # n_1 = table_1.shape[0] + n_2 = table_2.shape[0] + return el[0] * n_2 + el[1] + + +if __name__ == '__main__': + from group_tables import klein_table, quaternion + # Create klein group + g_klein = G(klein_table) + # create edges of a right and a left cayley graph of g_klein + klein_right = g_klein.right_cayley({1}) + klein_left = g_klein.left_cayley({2}) + + # Create quaternion group and a left cayley + q8 = G(quaternion) + + quaternion_left = q8.left_cayley({1, 5}) + print(quaternion_left) + + # this is the ordering that is used in direct product + my_ord = [] + for i in range(4): + for j in range(4): + for k in range(2): + my_ord.append((i, j, k)) + + diff --git a/quantum_tanner_codes/qtc.py b/quantum_tanner_codes/qtc.py new file mode 100644 index 00000000..4e830d7b --- /dev/null +++ b/quantum_tanner_codes/qtc.py @@ -0,0 +1,230 @@ +import numpy as np +import networkx as nx +from collections import defaultdict +from auxiliary.groups import G, from_tuple_to_direct_product_index, cyclic_group, direct_product +from auxiliary.binary_utils import gaussian_reduction, get_support + + +def from_two_pc_to_x_and_z_gen(h_1, h_2, debug=False): + """ + + Constructs X and Z stabilizer generators for a product code from two parity-check matrices. + + :param h_1: Parity-check matrix of the first classical code. + :param h_2: Parity-check matrix of the second classical code. + :param debug: If True, prints intermediate matrices for debugging. + :return: Tuple (x, z), where x is the X-generator matrix and z is the Z-generator matrix. + + gen for X code is g_1 x g_2 + gen for Z code is h_1 x h_2 + + """ + g_1 = gaussian_reduction(h_1)['ker'] + g_2 = gaussian_reduction(h_2)['ker'] + x = np.kron(g_1, g_2) + z = np.kron(h_1, h_2) + if debug: + print('g_1\n', g_1) + print('g_2\n', g_2) + print('h_1\n', h_1) + print('h_2\n', h_2) + return x, z + + +class Qtc: + """ + A quantum tensor code (QTC) object constructed from a group G and two inverse-closed generating sets A and B. + It builds a Cayley graph whose vertices are labeled by G and edges come from left and right Cayley graphs. + + The graph must be bipartite and connected to allow X and Z vertex assignment. + + The number of "squares" (g, ag, gb, agb) is G * Delta^2 / 4. + """ + + def __init__(self, group, a_set=None, b_set=None, default_x_and_z=True): + """ + :param group: Instance of G class (group described by Cayley table), identity element is 0. + :param a_set: Inverse-closed subset of G used for right Cayley graph edges. + :param b_set: Inverse-closed subset of G used for left Cayley graph edges. + :param default_x_and_z: If True and the graph is bipartite and connected, assign vertex sets of + nx deafult bipartition to X and Z. + """ + + # a_set and b_set are ordered and inverse closed + self.group = group + self.a_set = a_set + self.b_set = b_set + + # length of set_a and set_b gives the vertex degree of the graph, also the length of the inner code + self.delta_a = len(a_set) + self.delta_b = len(b_set) + self.small_n = self.delta_a * self.delta_b + + # builds the right cayley graph with edges (g, ga); is a set of edges + self.cay_a = group.right_cayley(a_set) + self.cay_b = group.left_cayley(b_set) + # as the left and right cayley graphs are defined on the same vertices, we build the total graph as + # nx object + graph = nx.Graph() + graph.add_edges_from(self.cay_a.union(self.cay_b)) + self.graph = graph + # CHECK + assert self.group.n == graph.number_of_nodes() + self.n_of_vertices = graph.number_of_nodes() + # the graph needs to be bipartite, X and Z vertices are the vertices in the partition + # also, we want it connected + self.x_vertices = None + self.z_vertices = None + + if default_x_and_z and nx.is_bipartite(graph) and nx.is_connected(graph): + self.x_vertices, self.z_vertices = nx.bipartite.sets(graph) + + self.v_inner_ordering = None + + # squares are all the quadruple (g, ag, agb, gb) + # self.squares is a list of quadruple of type tuple(sorted([v, va, bv, bva])) + # self.four_corners is true iff all squares have 4 corners + self.squares, self.four_corners = self.find_squares() + # the number of squares should be G * Delta ^ 2 / 4 + self.n_of_squares = len(self.squares) + # indexed_squares is a dictionary where + # keys is a square (v, va, bv, bva) and value is a number 0, . . , n_of_squares + self.square_to_index, self.vertex_to_incident_squares = self.index_squares_and_map_vertices() + + def __repr__(self): + """ + :return: String summary of group size, number of squares, and generator set sizes. + """ + return (f'size of G: {self.graph.number_of_nodes()}, number of squares {self.n_of_squares}\n' + f'inner code: {self.small_n}, size A: {self.delta_a}, size B: {self.delta_b}') + + def find_squares(self): + """ + Finds all squares of the form (v, va, bv, bva) and assigns them to each vertex v in self.v_inner_ordering. + + :return: A tuple (squares, four_corners): + - squares: list of tuples, each a square as sorted 4-tuple of vertex indices. + - four_corners: True if all squares have 4 distinct corners. + """ + + # for each vertex v, its small_n edges needs to be ordered so that we can put the small code constraints on it + # so v_inner_ordering it's a dictionary with v, the vertex as a key, and value is a dictionary + # {0: sq_i, 1: sq_j, . . . , small_n - 1: sq_k} + v_inner_ordering = {v: {inner: None for inner in range(self.small_n)} for v in self.graph} + + four_corners = True + + set_of_squares = set() + + # iterate vertices + # update the v_inner_ordering[v]: from 0 to small_n -1, location is a square expressed as tupla + for v in self.graph: + # we create a square, given by a tuple (a, b) with a in set_a and b in set_b + + for i_a, a in enumerate(self.a_set): + for i_b, b in enumerate(self.b_set): + va = self.group.op(v, a) + bv = self.group.op(b, v) + bva = self.group.op(bv, a) + square = tuple(sorted([v, va, bv, bva])) + if len(set(square)) != 4: + four_corners = False + # print('square ', square) + # print(f'a {a}, b {b}, v {v}') + v_inner_ordering[v][self.delta_b * i_a + i_b] = square + # idx += 1 + set_of_squares.add(square) + # for k, c in v_inner_ordering.items(): + # print(k) + # print(c) + self.v_inner_ordering = v_inner_ordering + + return list(set_of_squares), four_corners + + def index_squares_and_map_vertices(self): + + """ + Indexes each square and maps vertices to their incident squares. + + Also modifies self.v_inner_ordering so that squares are represented by their index. + + :return: Tuple (square_to_index, vertex_to_incident_squares) + - square_to_index: dict mapping square (4-tuple) to unique integer index. + - vertex_to_incident_squares: dict mapping vertex to list of square indices it belongs to. + + """ + square_to_index = {sq: idx for idx, sq in enumerate(self.squares)} + # now each square has a unique index assigned to it + + # adj vertex to square + vertex_to_incident_squares = defaultdict(list) + + for sq, idx in square_to_index.items(): + for v in sq: + vertex_to_incident_squares[v].append(idx) + + # for ech vertex v + # temp[v] is a dictionary with k an integer from 0 . . . small_n-1 + temp = defaultdict() + for v, local_view in self.v_inner_ordering.items(): + temp[v] = {k: square_to_index[c] for k, c in local_view.items()} + + # for k, c in temp.items(): + # print(k, c) + # print(self.v_inner_ordering[k]) + # print('\n') + self.v_inner_ordering = temp + + return square_to_index, vertex_to_incident_squares + + def get_a_paritycheck(self, vertex_set, gen): + """ + Constructs a parity-check matrix from a subset of vertices and a list of generator vectors. + + :param vertex_set: Set of vertices to assign generator constraints to. + :param gen: List of generator vectors (each a binary array). + :return: A NumPy array representing the parity-check matrix. + """ + matrix = [] + for v in vertex_set: + # for each generator + for i, g in enumerate(gen): + row = np.zeros(self.n_of_squares).astype(int) + for inner_el in get_support(g): + row[self.v_inner_ordering[v][inner_el]] = 1 + matrix.append(row) + return np.vstack(matrix) + + +if __name__ == '__main__': + # Build the rotated toric code as a Quantum Tanner Code + # n_ = 4 gives error as the logical i.e. line is counted as square as well + n_ = 6 + c_6 = cyclic_group(n_) + c_6_times_c_6 = direct_product(c_6, c_6) + group_6_6 = G(c_6_times_c_6) + + # a_set is inverse close of {(0, 1)} + # b_set is inverse close of {(1, 0)} + + print(f'The group index of the element (0,1) is: {from_tuple_to_direct_product_index(c_6, c_6, (0,1))}' + f' and its inverse is: {group_6_6.inv(1)}') + + print(f'The group index of the element (1,0) is: {from_tuple_to_direct_product_index(c_6, c_6, (1,0))}' + f' and its inverse is: {group_6_6.inv(6)}') + + # a = (0, 1) = 1 w inv 5 and b = (1, 0) = 6 w inv 30 + print('Quantum Tanner Code') + qtc = Qtc(group_6_6, a_set={1, 5}, b_set={30, 6}) + print(qtc) + print('We now build the pc matrices of the inner codes') + # The pcm of the inner codes are A \otimes B and A dual \otimes B dual + # For the Rotated Toric Code, A = B = [1, 1] + code_a = np.array([[1, 1]]) + code_b = np.array([[1, 1]]) + x_code, z_code = from_two_pc_to_x_and_z_gen(code_a, code_b) + print(f'Inner code for X stabilisers: {x_code}\nInner code for Z stabilisers: {z_code}') + print('We now build the pc matrices for the Rotated Toric code as Quantum Tanner Code') + hx_ = qtc.get_a_paritycheck(qtc.x_vertices, x_code) + hz_ = qtc.get_a_paritycheck(qtc.z_vertices, z_code) + print(f'hx:\n{hx_}\nhz:\n{hz_}') From 4ddba717cbbe742d2c26591996f4aca9fc1af6b8 Mon Sep 17 00:00:00 2001 From: armanda Date: Wed, 9 Jul 2025 12:54:13 +0200 Subject: [PATCH 2/2] Add a class for Quantum Tanner Codes https://arxiv.org/abs/2202.13641 --- quantum_tanner_codes/auxiliary/groups.py | 14 +-- quantum_tanner_codes/graph_qtc.py | 114 +++++++++++++++++++++++ 2 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 quantum_tanner_codes/graph_qtc.py diff --git a/quantum_tanner_codes/auxiliary/groups.py b/quantum_tanner_codes/auxiliary/groups.py index 04fb4abe..d2825176 100644 --- a/quantum_tanner_codes/auxiliary/groups.py +++ b/quantum_tanner_codes/auxiliary/groups.py @@ -51,7 +51,7 @@ def op(self, i, j): :param j: Integer index of the second element. :return: Integer index of the product i * j. """ - return self.table[i, j] + return int(self.table[i, j]) def ord(self, el): """ @@ -129,7 +129,7 @@ def direct_product(table_1, table_2): Table of Z3 x Z2 has [0, 1, 2, 3, 4, 5] --> [00, 01, 10, 11, 20, 21] Table of (Z3 x Z2) X Z2 --> [000, 001, 010, 011, 100, 101, 110, 111, 200, 201, 210, 211] - [0 of first ]x i of second -- [1 of first ]x i of second -- .. + [0 of first ]x i of second -- [1 of first ]x i of second -- etc. my_ord = [] for i in range(4): for j in range(4): @@ -186,13 +186,13 @@ def from_tuple_to_direct_product_index(table_1, table_2, el): q8 = G(quaternion) quaternion_left = q8.left_cayley({1, 5}) + print('Print edges of a left Cayley graph for the Quaternion group') print(quaternion_left) # this is the ordering that is used in direct product my_ord = [] - for i in range(4): - for j in range(4): - for k in range(2): - my_ord.append((i, j, k)) - + for i_ in range(4): + for j_ in range(4): + for k_ in range(2): + my_ord.append((i_, j_, k_)) diff --git a/quantum_tanner_codes/graph_qtc.py b/quantum_tanner_codes/graph_qtc.py new file mode 100644 index 00000000..23757717 --- /dev/null +++ b/quantum_tanner_codes/graph_qtc.py @@ -0,0 +1,114 @@ +from collections import defaultdict +from itertools import combinations + +import networkx as nx +import numpy as np + +""" +This class is not used right now, but may be used in the future. +""" + + +class GraphQtc: + """ + A quantum tensor code (QTC) object built from a bipartite graph. + + The graph must be bipartite and connected to allow X and Z vertex assignment. + + The number of "squares" (4-cycles) is |G| * Δ² / 4, where |G| is the number of vertices and Δ is the degree. + """ + + def __init__(self, graph, default_x_z=True): + """ + :param graph: networkx.Graph, assumed bipartite and undirected. + :param default_x_z: If True and the graph is bipartite, assign nx default bipartition to X and Z. + """ + self.graph = graph + self.n_of_vertices = graph.number_of_nodes() + + self.x_vertices = None + self.z_vertices = None + + if default_x_z: + assert nx.is_bipartite(graph) + self.x_vertices, self.z_vertices = nx.bipartite.sets(graph) + + self.squares = self.find_squares() + self.n_of_squares = len(self.squares) + + self.indexed_squares, self.vertex_to_squares = self.index_squares_and_map_vertices() + + def __repr__(self): + return f'size of G: {self.graph.number_of_nodes()}, number of squares {self.n_of_squares}' + + def find_squares(self): + """ + :return: Set of 4-tuples representing all squares (4-cycles) in the graph. + """ + set_of_squares = set() + neighbors = {v: set(self.graph[v]) for v in self.graph} + + for v in self.graph: + for v1, v2 in combinations(neighbors[v], 2): + assert v2 not in neighbors[v1], 'bipartite problem' + common = (neighbors[v1] & neighbors[v2]) - {v} + for w in common: + square = tuple(sorted([v, v1, w, v2])) + set_of_squares.add(square) + + return set_of_squares + + def reset_squares(self, new_squares): + """ + :param new_squares: Set of 4-tuples representing squares to replace the current square set. + """ + self.squares = new_squares + self.n_of_squares = len(self.squares) + self.indexed_squares, self.vertex_to_squares = self.index_squares_and_map_vertices() + + def filter_squares(self, edge_set): + """ + :param edge_set: Set of undirected edges (as tuples). + :return: Subset of squares that contain exactly two edges in edge_set. + """ + good_ones = set() + for square in self.squares: + flag = 0 + subgraph = self.graph.subgraph(square) + for edge in list(subgraph.edges): + if edge in edge_set or (edge[1], edge[0]) in edge_set: + flag += 1 + if flag == 2: + good_ones.add(square) + return good_ones + + def index_squares_and_map_vertices(self): + """ + :return: + - Dictionary mapping frozen sets of square vertices to indices. + - Dictionary mapping each vertex to a list of square indices it appears in. + """ + square_to_index = {frozenset(sq): idx for idx, sq in enumerate(self.squares)} + vertex_to_squares_neigh = defaultdict(list) + + for sq_set, idx in square_to_index.items(): + for vertex in sq_set: + vertex_to_squares_neigh[vertex].append(idx) + + return square_to_index, vertex_to_squares_neigh + + def get_a_paritycheck(self, vertex_set, gen): + """ + :param vertex_set: Iterable of vertices (typically all X or all Z). + :param gen: List of binary lists representing generator vectors. + :return: Parity-check matrix as NumPy array with one row per (vertex, generator) pair. + """ + matrix = [] + for v in vertex_set: + current_support = self.vertex_to_squares[v] + for g in gen: + row = np.zeros(self.n_of_squares).astype(int) + for index, label in enumerate(current_support): + row[label] = g[index] + matrix.append(row) + return np.vstack(matrix)