Source code for scalerqec.Symbolic.symbolicLER

from sympy import symbols, binomial,  simplify, latex
from ..Clifford.clifford import *
from ..Clifford.stimparser import *
from ..Clifford.QEPGpython import *
import pymatching
from ..QEC.noisemodel import NoiseModel
from ..QEC.qeccircuit import StabCode


# ----------------------------------------------------------------------
# Physical-error model
p = symbols('p')
q = 1 - p                     #   probability of "no error"
Px = Py = Pz = p / 3          #   probabilities of  X  Y  Z



[docs] def vec_to_idx(vec): """ Converts a binary vector (A tuple or a list of 0s and 1s) to an integer index. The first element of the vector is treated as the most significant bit(MSB). Example: (1,0,1) -> 1*2^2 + 0*2^1 + 1*2^0 =5 Args: vec: A tuple or list of binary digits(0 or 1). Returns: An integer representation of the binary vector. Returns 0 for an empty vector. """ idx=0 for bit in vec: idx = (idx<<1) | bit return idx
[docs] def idx_to_vec(idx, dimension): """ Converts and integer index back to the binary vector of a given dimension. The first element of the vector is the most significant bit (MSB). Example: (5,3) - > (1,0,1) Args: idx: The non-negative integer index. dimension: The desired non-negative dimension(length) of the output binary vector Returns: A tuple of binary digits representing the index. """ bits = [] temp_idx = idx for _ in range(dimension): bits.append(temp_idx & 1) temp_idx >>= 1 return tuple(reversed(bits))
[docs] def idx_to_bool_list(idx, dimension): """ Converts and integer index back to the boolean vector of a given dimension. The first element of the vector is the most significant bit (MSB). Example: (5,3) - > [True,False,True] Args: idx: The non-negative integer index. dimension: The desired non-negative dimension(length) of the output binary vector Returns: A list of boolean representing the binary digits given the index. """ bool_list = [] temp_idx = idx for _ in range(dimension): bool_list.append(True if (temp_idx & 1) else False) temp_idx >>= 1 return list(reversed(bool_list))
[docs] def xor_vec(vec_a, vec_b): """ Performs element-wise XOR addition on two binary vectors. Args: vec_a: The first binary vector (tuple or list of 0s and 1s). vec_b: The second binary vector (tuple or list of 0s and 1s). Returns: A tuple representing the element-wise XOR of two vectors. """ return tuple(a^b for a,b in zip(vec_a,vec_b))
MAX_degree=100 MAX_weight=12 ''' Use symbolic algorithm to calculate the probability. Simply enumerate all possible cases '''
[docs] class symbolicLER: def __init__(self,error_rate=0): self._num_detector=0 self._num_noise=0 self._error_rate=error_rate self._dp=None self._cliffordcircuit=CliffordCircuit(4) self._graph=None self._all_predictions=None self._PROP_X = None self._PROP_Y = None self._PROP_Z = None self._error_row_indices = [] self._subspace_LER={}
[docs] def get_totalnoise(self): """ Get the total number of noise in the circuit """ return self._num_noise
[docs] def parse_from_file(self,filepath): """ Read the circuit, parse from the file """ stim_str="" with open(filepath, "r", encoding="utf-8") as f: stim_str = f.read() self._cliffordcircuit.error_rate = self._error_rate self._cliffordcircuit.compile_from_stim_circuit_str(stim_str) self._num_noise = self._cliffordcircuit.totalnoise self._num_detector=len(self._cliffordcircuit.parityMatchGroup) self._total_detector_outcome=(1<<(self._num_detector+1)) self._graph=QEPGpython(self._cliffordcircuit) self._graph.backword_graph_construction()
[docs] def generate_pymatching_table(self): """ For all detector result, generate the prediction through pymatching. _all_predictions store all prediction by matching """ # Configure a decoder using the circuit. stimcircuit=self._cliffordcircuit.stimcircuit detector_error_model = stimcircuit.detector_error_model(decompose_errors=False) matcher = pymatching.Matching.from_detector_error_model(detector_error_model) all_inputs = [] print("Total detector outcome: ", 1<<self._num_detector) for i in range(0,1<<self._num_detector): print("i=",i) print(1<<self._num_detector) # Convert the integer to a list of booleans bool_list = idx_to_bool_list(i, self._num_detector) # Print the list of booleans all_inputs.append(bool_list) #print(all_inputs) self._all_predictions = matcher.decode_batch(all_inputs)
[docs] def calc_error_row_indices(self): """ Based on the prediction result by pymatching of all possible input, build a list including all row indices that cause logical error """ self._error_row_indices = [] for row in range(0,self._total_detector_outcome): full_bool_list = idx_to_bool_list(row, self._num_detector+1) """ Get the current observable outcome represented by the row index. """ current_obs=full_bool_list[-1] ind_no_obs=vec_to_idx(full_bool_list[:-1]) """ Return the exact prediction result from the decoder, which is computed by function generate_pymatching_table(self) """ real_obs=self._all_predictions[ind_no_obs][0] if(current_obs!=real_obs): self._error_row_indices.append(row)
#print(self._error_row_indices)
[docs] def initialize_single_pauli_propagation(self): """ Calculate and store the table of the propagation result of single pauli error """ self._PROP_X = [] self._PROP_Y = [] self._PROP_Z = [] for noiseidx in range(self._num_noise): self._PROP_X.append(tuple(self._graph.sample_x_error(noiseidx))) self._PROP_Y.append(tuple(self._graph.sample_y_error(noiseidx))) self._PROP_Z.append(tuple(self._graph.sample_z_error(noiseidx)))
[docs] def initialize_dp(self): """ Given the circuit information, initialize the dp table for running the algorithm """ self._dp = [ [ [0]*self._total_detector_outcome for _ in range(self._num_noise+1) ] for _ in range(self._num_noise+1) ]
[docs] def verify_table(self,i): sum=0 for j in range(0,i+1): for vec_index in range(self._total_detector_outcome): sum+=self._dp[i][j][vec_index] # print(self._dp[i][j][vec_index]) sum=simplify(sum) # print(i,sum) assert sum==1
[docs] def dynamic_calculation_of_dp(self): # ---------------------------------------------------------------------- # DP tables MAX_I = self._num_noise self.initialize_dp() col_size=self._num_detector+1 # ---------------------------------------------------------------------- # Fill dp[i][j][·] using the recurrence in Eq. (1) for i in range(0, MAX_I+1): self._dp[i][0][0] = (1-p)**i for j in range(1, i+1): # j ≤ i if j > MAX_weight: continue #print("MAX_I=",MAX_I,"i=",i,"j=",j) for vec_idx in range(self._total_detector_outcome): vec = idx_to_vec(vec_idx,col_size) # 1) “no error’’ branch acc = q * self._dp[i-1][j][vec_idx] if j >= 1: for (prob, prop) in ((Px, self._PROP_X[i-1]), (Py, self._PROP_Y[i-1]), (Pz, self._PROP_Z[i-1])): prev_vec = xor_vec(prop, vec) acc += prob * self._dp[i-1][j-1][ vec_to_idx(prev_vec) ] self._dp[i][j][vec_idx] = simplify(acc) if MAX_weight>=self._num_noise: self.verify_table(i)
[docs] def calculate_LER_brute_force(self): """ Enumerate all possible noise input to get the exact LER polynomial """ pass
# ---------------------------------------------------------------------- # Calculate logical error rate # The input is a list of rows with logical errors
[docs] def calculate_LER(self): self._LER=0 for weight in range(1,self._num_noise+1): subLER=0 for rowindex in self._error_row_indices: subLER+=self._dp[self._num_noise][weight][rowindex] self._subspace_LER[weight]=simplify(subLER) self._LER+=simplify(subLER) self._LER=simplify(self._LER).expand() #LER=LER.series(p, 0, MAX_degree).removeO() # no .expand() print("LER polynomial: ", latex(self._LER)) return self._LER
[docs] def evaluate_LER(self,pval): return self._LER.evalf(subs={p:pval})
[docs] def evaluate_LER_subspace(self,pval,weight): return self._subspace_LER[weight].evalf(subs={p:pval})
[docs] def subspace_LER(self,weight): """ Get the subspace LER for a given weight """ if weight in self._subspace_LER: bernolli_coeff = binomial(self._num_noise, weight) * (p**weight) * ((1-p)**(self._num_noise-weight)) subspaceLER=simplify(self._subspace_LER[weight]/bernolli_coeff) return subspaceLER.expand() else: raise ValueError("Subspace LER for weight {} is not calculated.".format(weight))
[docs] def calculate_LER_from_file(self,filepath,pvalue) -> float: """ The most import function. Read the stim circuit, calculate the exact polynomial of LER, and evaluate with the value of p(physical error rate). Following steps are included: Step1: Parse the circuit from the file, inject depolarization noise Step2: Compile the STIM detector graph, generate the entire prediction table Stem3: Construct the QEPG graph Step4: Calculate all row indices in the table that will cause logical error Step5: Use dynamic algorithm to calculate the probability of measuring any possible outcomes Step6: Sum up all probability in the row with logical error Args: filepath: The path of the file with the STIM circuit pvalue: The physical error rate Returns: A floating value which store the final logical error rate """ self._error_rate=pvalue self.parse_from_file(filepath) print("---Step2: Generate the prediction table---") self.generate_pymatching_table() print("---Step2: construction QEPG--------------") self.initialize_single_pauli_propagation() print("---Step3: calculating error indices--------------") self.calc_error_row_indices() print("---Step4: dynamic algorithm--------------") self.dynamic_calculation_of_dp() self.calculate_LER() return self.evaluate_LER(pvalue)
[docs] def calculate_LER_from_StabCode(self,qeccirc:StabCode, noise_model: NoiseModel) -> float: """ Given a StabCode object, calculate the LER polynomial symbolically Args: qeccirc: A StabCode object error_rate: The physical error rate Returns: The symbolic polynomial of LER """ self._error_rate = noise_model.error_rate qeccirc.construct_IR_standard_scheme() qeccirc.compile_stim_circuit_from_IR_standard() self._cliffordcircuit = noise_model.reconstruct_clifford_circuit(qeccirc.circuit) self._num_noise = self._cliffordcircuit.totalnoise self._num_detector=len(self._cliffordcircuit.parityMatchGroup) self._total_detector_outcome=(1<<(self._num_detector+1)) self._graph=QEPGpython(self._cliffordcircuit) self._graph.backword_graph_construction() print("---Step2: Generate the prediction table---") self.generate_pymatching_table() print("---Step2: construction QEPG--------------") self.initialize_single_pauli_propagation() print("---Step3: calculating error indices--------------") self.calc_error_row_indices() print("---Step4: dynamic algorithm--------------") self.dynamic_calculation_of_dp() self.calculate_LER() self._LER=self.evaluate_LER(self._error_rate) print("Evaluated LER at p={} is {}".format(self._error_rate,self._LER)) return self._LER
if __name__=="__main__": tmp=symbolicLER(0.001) filepath="C:/Users/username/Documents/Sampling/stimprograms/small/simple" print(tmp.calculate_LER_from_file(filepath,0.001)) num_noise=tmp._num_noise # for weight in range(1,11): # print("LER in the subspace {} is {}".format(weight,tmp.evaluate_LER_subspace(0.001,weight))) for weight in range(1,12): print("SubspaceLER {} is {}".format(weight,tmp.subspace_LER(weight)))