Source code for ssip.pushouts

# Functions for constructing the two pushouts necessary
# for a fault-tolerant code merge between CSS codes.
import numpy as np

from ssip.basic_functions import (
    BinMatrix,
    CSScode,
    direct_sum_matrices,
    find_homology_basis,
    find_paired_basis,
    num_data_qubits,
    num_logical_qubits,
)
from ssip.lifted_product import tensor_product
from ssip.merge_result import MergeResult, find_logical_splitting
from ssip.monic_span_checker import monic_span, restricted_matrix


# Calculates the tensor product in the category of chain complexes
# of a logical operator subcomplex V and the complex P1 -> P0,
# with delP: P1 -> P0 = (1 1)^T.
# See p32 of https://arxiv.org/abs/2301.13738.
[docs] def sandwich_middle_code(V: BinMatrix, depth: int = 1) -> CSScode: def gen_classical_code(depth): parity_mat = np.zeros((depth + 1, depth)) for i in range(depth): parity_mat[i][i] = 1 parity_mat[i + 1][i] = 1 return parity_mat A = gen_classical_code(depth) return tensor_product(A, V)
# Calculates the pushout of a basis-preserving monic span with # logical operator subcomplex as # the apex. Assumes we have A, B differentials belonging # to one complex and C, D to another.
[docs] def pushout( A: BinMatrix, B: BinMatrix, C: BinMatrix, D: BinMatrix, qubit_map: dict, syndrome_map: dict, return_data: bool = False, ) -> CSScode | MergeResult: # the first differential direct_sumAC = direct_sum_matrices(A, C) # quotient rows together for item in qubit_map.items(): direct_sumAC[item[0]] = np.array( list( map( lambda x, y: bool(x + y), direct_sumAC[item[0]], direct_sumAC[item[1] + A.shape[0]], ) ) ) del2 = np.delete(direct_sumAC, [x + A.shape[0] for x in qubit_map.values()], 0) # the second differential direct_sumBD = direct_sum_matrices(B, D) # quotient rows together for item in syndrome_map.items(): direct_sumBD[item[0]] = np.array( list( map( lambda x, y: bool(x + y), direct_sumBD[item[0]], direct_sumBD[item[1] + B.shape[0]], ) ) ) del1 = np.delete(direct_sumBD, [x + B.shape[0] for x in syndrome_map.values()], 0) # quotient columns together for item in qubit_map.items(): del1[:, item[0]] = np.array( list( map( lambda x, y: bool(x + y), del1[:, item[0]], del1[:, item[1] + B.shape[1]], ) ) ) del1 = np.delete(del1, [x + B.shape[1] for x in qubit_map.values()], 1) new_code = CSScode(del2.T, del1) if return_data: # Calculate coequaliser at degree 1 n = A.shape[0] + C.shape[0] n2 = A.shape[0] coeq = np.eye(n) for key, val in qubit_map.items(): coeq[key] = np.array( list(map(lambda x, y: bool(x + y), coeq[key], coeq[val + n2])) ) coeq = np.delete(coeq, [x + n2 for x in qubit_map.values()], 0) # Calculate whether there are new logical qubits introduced by the merge num_before_logicals = len(find_homology_basis(A, B)) + len( find_homology_basis(C, D) ) num_after_logicals = num_logical_qubits(new_code) num_new_logicals = num_after_logicals - num_before_logicals + 1 new_Z_logicals = [] new_X_logicals = [] old_Z_logicals = [] old_X_logicals = [] if num_new_logicals < 0: raise ValueError("Pushout error, logical operators are not irreducible") # Calculate paired Z/X logicals if num_new_logicals: old_code = CSScode(direct_sum_matrices(A, C).T, direct_sum_matrices(B, D)) new_Z_logicals, new_X_logicals, old_Z_logicals, old_X_logicals = ( find_logical_splitting( old_code, new_code, coeq, num_before_logicals, num_after_logicals ) ) else: old_Z_logicals = find_homology_basis(del2, del1) trial_X_basis = find_homology_basis(del1.T, del2.T) old_X_logicals = find_paired_basis(old_Z_logicals, trial_X_basis) return MergeResult( new_code, coeq, [], [], [], new_Z_logicals, new_X_logicals, old_Z_logicals, old_X_logicals, ) return new_code
# Calculates the pushout based on qubits labelled by indices; if these indices do not # yield a monic span then return None. Assume C, D are CSScodes.
[docs] def pushout_by_indices( C: CSScode, D: CSScode, indices1: list[int], indices2: list[int], basis: str = "Z", return_data: bool = False, ) -> CSScode | MergeResult | None: restr1 = None restr2 = None if basis == "Z": restr1 = restricted_matrix(indices1, C.PX) restr2 = restricted_matrix(indices2, D.PX) elif basis == "X": restr1 = restricted_matrix(indices1, C.PZ) restr2 = restricted_matrix(indices2, D.PZ) else: raise ValueError("Must enter a valid Pauli string.") span = monic_span(restr1[0], restr2[0]) if span is None: return None qubit_map = {} for qb1, qb2 in span[0].items(): qubit_map[indices1[qb1]] = indices2[qb2] syndrome_map = {} for syndrome1, syndrome2 in span[1].items(): syndrome_map[restr1[1][syndrome1]] = restr2[1][syndrome2] if basis == "Z": return pushout( C.PZ.T.copy(), C.PX.copy(), D.PZ.T.copy(), D.PX.copy(), qubit_map, syndrome_map, return_data, ) elif basis == "X": temp_code = pushout( C.PX.T.copy(), C.PZ.copy(), D.PX.T.copy(), D.PZ.copy(), qubit_map, syndrome_map, return_data, ) if return_data: temp_new_Z_logicals = temp_code.NewZLogicals.copy() temp_code.NewZLogicals = temp_code.NewXLogicals.copy() temp_code.NewXLogicals = temp_new_Z_logicals.copy() temp_old_Z_logicals = temp_code.OldZLogicals.copy() temp_code.OldZLogicals = temp_code.OldXLogicals.copy() temp_code.OldXLogicals = temp_old_Z_logicals.copy() temp_code.Code = CSScode(temp_code.Code.PX, temp_code.Code.PZ) return temp_code return CSScode(temp_code.PX, temp_code.PZ)
# Given two CSS codes C2 -> C1 -> C0 and D2 -> D1 -> C0, with F: C2 -> C1, G: C1 -> 0, # J: D2 -> D1, K: D1 -> D0, and matching logical operators u in C1 and v in D1, # constructs the pushout of a pushout of chain complexes, giving the final code. # Assumes that the pushouts are of basis-preserving monic spans with logical operator # subcomplexes at the apexes.
[docs] def external_merge( V: BinMatrix, F: BinMatrix, G: BinMatrix, J: BinMatrix, K: BinMatrix, qubit_map: dict, syndrome_map: dict, depth: int = 1, return_data: str = False, ) -> CSScode | MergeResult: if depth < 1: raise ValueError("Must enter a pushout depth greater than 0.") tens = sandwich_middle_code(V, depth) qmap1 = {} qmap2 = {} for count, item in enumerate(qubit_map.items()): qmap1[count] = item[0] qmap2[count + depth * len(qubit_map)] = item[1] smap1 = {} smap2 = {} for count, item in enumerate(syndrome_map.items()): smap1[count] = item[0] smap2[count + depth * len(syndrome_map)] = item[1] first_pushout = pushout(tens.PZ.T, tens.PX, F, G, qmap1, smap1) second_pushout = pushout(first_pushout.PZ.T, first_pushout.PX, J, K, qmap2, smap2) if return_data: n_before = G.shape[1] + K.shape[1] n_after = num_data_qubits(second_pushout) if not n_after == G.shape[1] + K.shape[1] + (depth - 1) * len( qubit_map ) + depth * len(syndrome_map): raise RuntimeError("Dimensions do not match up when returning merge data") new_qubits = list(np.arange(len(qubit_map), depth * len(qubit_map))) + list( np.arange( (depth + 1) * len(qubit_map), (depth + 1) * len(qubit_map) + depth * len(syndrome_map), ) ) new_Z_stabilisers = list(range(depth * len(qubit_map))) new_X_stabilisers = list(range(len(syndrome_map), depth * len(syndrome_map))) merge_map = np.zeros((n_after, n_before)) qmap1_flipped = {v: k for k, v in qmap1.items()} qmap2_flipped = {v: k for k, v in qmap2.items()} count1 = 0 count2 = 0 for i in range(G.shape[1]): if i in qmap1_flipped: merge_map[qmap1_flipped[i]][i] = 1 count1 += 1 else: merge_map[ (depth + 1) * len(qubit_map) + depth * len(syndrome_map) + i - count1 ][i] = 1 for i in range(K.shape[1]): if i in qmap2_flipped: merge_map[qmap2_flipped[i]][i + G.shape[1]] = 1 count2 += 1 else: j = ( G.shape[1] + depth * len(qmap2) + depth * len(syndrome_map) + i - count2 ) merge_map[j][i + G.shape[1]] = 1 num_before_logicals = num_logical_qubits(CSScode(F.T, G)) + num_logical_qubits( CSScode(J.T, K) ) num_after_logicals = num_logical_qubits(second_pushout) num_new_logicals = num_after_logicals - num_before_logicals + 1 new_Z_logicals = [] new_X_logicals = [] old_Z_logicals = [] old_X_logicals = [] if num_new_logicals < 0: raise ValueError( "External merge error, logical operators are not irreducible" ) if num_new_logicals: old_code = CSScode(direct_sum_matrices(F, J).T, direct_sum_matrices(G, K)) new_Z_logicals, new_X_logicals, old_Z_logicals, old_X_logicals = ( find_logical_splitting( old_code, second_pushout, merge_map, num_before_logicals, num_after_logicals, ) ) else: old_Z_logicals = find_homology_basis(second_pushout.PZ.T, second_pushout.PX) trial_X_logicals = find_homology_basis( second_pushout.PX.T, second_pushout.PZ ) old_X_logicals = find_paired_basis(old_Z_logicals, trial_X_logicals) return MergeResult( second_pushout, merge_map, new_Z_stabilisers, new_X_stabilisers, new_qubits, new_Z_logicals, new_X_logicals, old_Z_logicals, old_X_logicals, ) return second_pushout
[docs] def external_merge_by_indices( C: CSScode, D: CSScode, indices1: list[int], indices2: list[int], basis: str = "Z", depth: int = 1, return_data: bool = False, ) -> CSScode | MergeResult | None: """Construct an external merge between two CSS codeblocks, along two logical operators. Args: C: The first CSS codeblock. D: The second CSS codeblock. indices1: The qubits in the support of the first logical operator. indices2: The qubits in the support of the second logical operator. basis: The basis, X or Z, to perform the logical measurement in. depth: The depth of the merge, i.e. how large to make the tensor product intermediate code. return_data: Whether to calculate a MergeResult rather than a CSScode output. The MergeResult includes substantially more data about the merge. Return: The merged CSScode, or the MergeResult object which contains the CSScode, or None if no merge could be found. """ restr1 = None restr2 = None if basis == "Z": restr1 = restricted_matrix(indices1, C.PX) restr2 = restricted_matrix(indices2, D.PX) elif basis == "X": restr1 = restricted_matrix(indices1, C.PZ) restr2 = restricted_matrix(indices2, D.PZ) else: raise ValueError("Must enter a valid Pauli string.") span = monic_span(restr1[0], restr2[0]) if span is None: return None qubit_map = {} for qb1, qb2 in span[0].items(): qubit_map[indices1[qb1]] = indices2[qb2] syndrome_map = {} for syndrome1, syndrome2 in span[1].items(): syndrome_map[restr1[1][syndrome1]] = restr2[1][syndrome2] qubit_map = dict(sorted(qubit_map.items())) syndrome_map = dict(sorted(syndrome_map.items())) if basis == "Z": return external_merge( restr1[0], C.PZ.T.copy(), C.PX.copy(), D.PZ.T.copy(), D.PX.copy(), qubit_map, syndrome_map, depth, return_data, ) if basis == "X": temp_code = external_merge( restr1[0], C.PX.T.copy(), C.PZ.copy(), D.PX.T.copy(), D.PZ.copy(), qubit_map, syndrome_map, depth, return_data, ) if return_data: temp_code.Code = CSScode(temp_code.Code.PX, temp_code.Code.PZ) temp_new_X_stabs = temp_code.NewXStabs.copy() temp_code.NewXStabs = temp_code.NewZStabs.copy() temp_code.NewZStabs = temp_new_X_stabs temp_new_Z_logicals = temp_code.NewZLogicals.copy() temp_code.NewZLogicals = temp_code.NewXLogicals.copy() temp_code.NewXLogicals = temp_new_Z_logicals.copy() temp_old_Z_logicals = temp_code.OldZLogicals.copy() temp_code.OldZLogicals = temp_code.OldXLogicals.copy() temp_code.OldXLogicals = temp_old_Z_logicals.copy() return temp_code return CSScode(temp_code.PX, temp_code.PZ)