From 396c91b9873512a1237ae931cb099b9f3bf3c0b4 Mon Sep 17 00:00:00 2001 From: Corey Ostrove Date: Mon, 11 Nov 2024 22:05:16 -0700 Subject: [PATCH] Higher-order BCH Implementation Add in support for higher-order BCH up to fourth order. --- .../errorpropagator_dev.py | 101 ++++++------- pygsti/tools/errgenproptools.py | 142 +++++++++++++----- 2 files changed, 147 insertions(+), 96 deletions(-) diff --git a/pygsti/errorgenpropagation/errorpropagator_dev.py b/pygsti/errorgenpropagation/errorpropagator_dev.py index cc347e809..2936ac622 100644 --- a/pygsti/errorgenpropagation/errorpropagator_dev.py +++ b/pygsti/errorgenpropagation/errorpropagator_dev.py @@ -59,17 +59,19 @@ def eoc_error_channel(self, circuit, multi_gate_dict=None, include_spam=True, us """ if use_bch: - raise NotImplementedError('Still under development.') - propagated_error_generators = self.propagate_errorgens_bch(circuit, multi_gate_dict=multi_gate_dict, - *bch_kwargs) + #should return a single dictionary of error generator rates + propagated_error_generator = self.propagate_errorgens_bch(circuit, multi_gate_dict=multi_gate_dict, + **bch_kwargs) + #convert this to a process matrix + return _spl.expm(self.errorgen_layer_dict_to_errorgen(propagated_error_generator, mx_basis='pp')) else: propagated_error_generators = self.propagate_errorgens(circuit, multi_gate_dict, include_spam) #loop though the propagated error generator layers and construct their error generators. #Then exponentiate exp_error_generators = [] - for err_gen_layer_list in propagated_error_generators: - if err_gen_layer_list: #if not empty. Should be length one if not empty. + for err_gen_layer in propagated_error_generators: + if err_gen_layer: #if not empty. #Keep the error generator in the standard basis until after the end-of-circuit #channel is constructed so we can reduce the overhead of changing basis. exp_error_generators.append(_spl.expm(self.errorgen_layer_dict_to_errorgen(err_gen_layer_list[0], mx_basis='pp'))) @@ -229,14 +231,14 @@ def propagate_errorgens_bch(self, circuit, bch_order=1, bch_layerwise=False, mul with state preparation and measurement. """ - msg = 'When bch_layerwise is True this can take the values of either 1 or 2.'\ - +' Otherwise only a value of 1 is currently implemented.' - if not bch_layerwise: - assert bch_order==1, msg - else: - msg1 = 'When bch_layerwise is False only bch_order values of 1 and 2 are currently'\ - + ' supported.' - assert bch_order==1 or bch_order==2, msg1 + #msg = 'When bch_layerwise is True this can take the values of either 1 or 2.'\ + # +' Otherwise only a value of 1 is currently implemented.' + #if not bch_layerwise: + # assert bch_order==1, msg + #else: + # msg1 = 'When bch_layerwise is False only bch_order values of 1 and 2 are currently'\ + # + ' supported.' + # assert bch_order==1 or bch_order==2, msg1 #if not doing layerwise BCH then we can re-use `propagate_errorgens` fully. if not bch_layerwise: @@ -261,11 +263,11 @@ def propagate_errorgens_bch(self, circuit, bch_order=1, bch_layerwise=False, mul assert circuit.line_labels is not None and circuit.line_labels != ('*',) errorgen_layers = self.construct_errorgen_layers(circuit, len(circuit.line_labels), include_spam) - #propagate the errorgen_layers through the propagation_layers to get a list - #of end of circuit error generator dictionaries. - propagated_errorgen_layers = self._propagate_errorgen_layers_bch(errorgen_layers, propagation_layers, + #propagate the errorgen_layers through the propagation_layers to get the + #end of circuit error generator dictionary. + propagated_errorgen_layers = self._propagate_errorgen_layers_bch(errorgen_layers, propagation_layers, + bch_order=bch_order, include_spam = include_spam) - return propagated_errorgen_layers @@ -434,7 +436,7 @@ def construct_errorgen_layers(self, circuit, num_qubits, include_spam=True, incl value currently found in the model. Returns ------- - List of lists of dictionaries, each one containing the error generator coefficients and rates for a circuit layer, + List of dictionaries, each one containing the error generator coefficients and rates for a circuit layer, with the error generator coefficients now represented using LocalStimErrorgenLabel. """ @@ -471,7 +473,7 @@ def construct_errorgen_layers(self, circuit, num_qubits, include_spam=True, incl errorgen_layer[_LSE(errgen_coeff_lbl.errorgen_type, paulis, circuit_time=j)] = rate if fixed_rate is None else fixed_rate else: errorgen_layer[_LSE(errgen_coeff_lbl.errorgen_type, paulis)] = rate if fixed_rate is None else fixed_rate - error_gen_dicts_by_layer.append([errorgen_layer]) + error_gen_dicts_by_layer.append(errorgen_layer) return error_gen_dicts_by_layer def _propagate_errorgen_layers(self, errorgen_layers, propagation_layers, include_spam=True): @@ -519,16 +521,12 @@ def _propagate_errorgen_layers(self, errorgen_layers, propagation_layers, includ for i in range(stopping_idx): err_layer = errorgen_layers[i] prop_layer = propagation_layers[i] - new_err_layer = [] - #err_layer should be length 1 if using this method - for bch_level_list in err_layer: - new_error_dict=dict() - #iterate through dictionary of error generator coefficients and propagate each one. - for errgen_coeff_lbl in bch_level_list: - propagated_error_gen = errgen_coeff_lbl.propagate_error_gen_tableau(prop_layer, bch_level_list[errgen_coeff_lbl]) - new_error_dict[propagated_error_gen[0]] = propagated_error_gen[1] - new_err_layer.append(new_error_dict) - fully_propagated_layers.append(new_err_layer) + new_error_dict=dict() + #iterate through dictionary of error generator coefficients and propagate each one. + for errgen_coeff_lbl in err_layer: + propagated_error_gen = errgen_coeff_lbl.propagate_error_gen_tableau(prop_layer, err_layer[errgen_coeff_lbl]) + new_error_dict[propagated_error_gen[0]] = propagated_error_gen[1] + fully_propagated_layers.append(new_error_dict) #add the final layers which didn't require actual propagation (since they were already at the end). fully_propagated_layers.extend(errorgen_layers[stopping_idx:]) return fully_propagated_layers @@ -565,24 +563,15 @@ def _propagate_errorgen_layers_bch(self, errorgen_layers, propagation_layers, bc Returns ------- - fully_propagated_layers : list of lists of dicts - A list of list of dicts with the same structure as errorgen_layers corresponding - to the results of having propagated each of the error generator layers through - the circuit to the end while combining the layers in a layerwise fashion using the - BCH approximation. As a result of this combination, this list should have a length - of one. + fully_propagated_layer : dict + Dictionart corresponding to the results of having propagated each of the error generator + layers through the circuit to the end while combining the layers in a layerwise fashion + using the BCH approximation. """ - - #Add temporary errors when trying to do BCH beyond 1st order while the details of the 2nd order - #approximation's implementation are sorted out. - if bch_order != 1: - msg = 'The implementation of the 2nd order BCH approx is still under development. For now only 1st order is supported.' - raise NotImplementedError(msg) - assert all([len(layer)==1 for layer in errorgen_layers]), msg - - fully_propagated_layers=[] + #TODO: Refactor this and _propagate_errorgen_layers to reduce code repetition as their current + #implementations are very close to each other. #initialize a variable as temporary storage of the result - #of performing BCH on pairwise between a propagater errorgen + #of performing BCH on pairwise between a propagated errorgen #layer and an unpropagated layer for layerwise BCH. if len(errorgen_layers)>0: combined_err_layer = errorgen_layers[0] @@ -597,25 +586,19 @@ def _propagate_errorgen_layers_bch(self, errorgen_layers, propagation_layers, bc for i in range(stopping_idx): #err_layer = errorgen_layers[i] prop_layer = propagation_layers[i] - new_err_layer = [] - #err_layer should be length 1 if using this method - for bch_level_dict in combined_err_layer: - new_error_dict = dict() - #iterate through dictionary of error generator coefficients and propagate each one. - for errgen_coeff_lbl in bch_level_dict: - propagated_error_gen = errgen_coeff_lbl.propagate_error_gen_tableau(prop_layer, bch_level_dict[errgen_coeff_lbl]) - new_error_dict[propagated_error_gen[0]] = propagated_error_gen[1] - new_err_layer.append(new_error_dict) + new_error_dict = dict() + #iterate through dictionary of error generator coefficients and propagate each one. + for errgen_coeff_lbl in combined_err_layer: + propagated_error_gen = errgen_coeff_lbl.propagate_error_gen_tableau(prop_layer, combined_err_layer[errgen_coeff_lbl]) + new_error_dict[propagated_error_gen[0]] = propagated_error_gen[1] #next use BCH to combine new_err_layer with the now adjacent layer of errorgen_layers[i+1] - combined_err_layer = _eprop.bch_approximation(new_err_layer, errorgen_layers[i+1], bch_order=1) - + combined_err_layer = _eprop.bch_approximation(new_error_dict, errorgen_layers[i+1], bch_order=bch_order) #If we are including spam then there will be one last error generator which we doesn't have an associated propagation #which needs to be combined using BCH. if include_spam: - combined_err_layer = _eprop.bch_approximation(combined_err_layer, errorgen_layers[-1], bch_order=1) + combined_err_layer = _eprop.bch_approximation(combined_err_layer, errorgen_layers[-1], bch_order=bch_order) - fully_propagated_layers.append(combined_err_layer) - return fully_propagated_layers + return combined_err_layer def errorgen_layer_dict_to_errorgen(self, errorgen_layer, mx_basis='pp'): """ diff --git a/pygsti/tools/errgenproptools.py b/pygsti/tools/errgenproptools.py index 8fbb5c151..a178d3405 100644 --- a/pygsti/tools/errgenproptools.py +++ b/pygsti/tools/errgenproptools.py @@ -15,6 +15,7 @@ from pygsti.errorgenpropagation.localstimerrorgen import LocalStimErrorgenLabel as _LSE from pygsti.modelmembers.operations import LindbladErrorgen as _LinbladErrorgen from numpy import conjugate +from functools import reduce def errgen_coeff_label_to_stim_pauli_strs(err_gen_coeff_label, num_qubits): """ @@ -80,9 +81,8 @@ def bch_approximation(errgen_layer_1, errgen_layer_2, bch_order=1): Parameters ---------- - errgen_layer_1 : list of dicts - Each lists contains dictionaries of the error generator coefficients and rates for a circuit layer. - Each dictionary corresponds to a different order of the BCH approximation. + errgen_layer_1 : dict + Dictionary of the error generator coefficients and rates for a circuit layer. The error generator coefficients are represented using LocalStimErrorgenLabel. errgen_layer_2 : list of dicts @@ -90,62 +90,130 @@ def bch_approximation(errgen_layer_1, errgen_layer_2, bch_order=1): Returns ------- - combined_errgen_layer : list of dicts? - A list with the same general structure as `errgen_layer_1` and `errgen_layer_2`, but with the + combined_errgen_layer : dict + A dictionary with the same general structure as `errgen_layer_1` and `errgen_layer_2`, but with the rates combined according to the selected order of the BCH approximation. """ - if bch_order != 1: - msg = 'The implementation of the 2nd order BCH approx is still under development. For now only 1st order is supported.' - raise NotImplementedError(msg) - new_errorgen_layer=[] for curr_order in range(0,bch_order): - working_order_dict = dict() #add first order terms into new layer if curr_order == 0: - #get the dictionaries of error generator coefficient labels and rates - #for the current working BCH order. - current_errgen_dict_1 = errgen_layer_1[curr_order] - current_errgen_dict_2 = errgen_layer_2[curr_order] #Get a combined set of error generator coefficient labels for these two #dictionaries. - current_combined_coeff_lbls = set(current_errgen_dict_1.keys()) | set(current_errgen_dict_2.keys()) + current_combined_coeff_lbls = set(errgen_layer_1.keys()) | set(errgen_layer_2.keys()) + first_order_dict = dict() #loop through the combined set of coefficient labels and add them to the new dictionary for the current BCH #approximation order. If present in both we sum the rates. for coeff_lbl in current_combined_coeff_lbls: - working_order_dict[coeff_lbl] = current_errgen_dict_1.get(coeff_lbl, 0) + current_errgen_dict_2.get(coeff_lbl, 0) - new_errorgen_layer.append(working_order_dict) + first_order_dict[coeff_lbl] = errgen_layer_1.get(coeff_lbl, 0) + errgen_layer_2.get(coeff_lbl, 0) + + #allow short circuiting to avoid an expensive bunch of recombination logic when only using first order BCH + #which will likely be a common use case. + if bch_order==1: + return first_order_dict + new_errorgen_layer.append(first_order_dict) + #second order BCH terms. + # (1/2)*[X,Y] elif curr_order == 1: - current_errgen_dict_1 = errgen_layer_1[curr_order-1] - current_errgen_dict_2 = errgen_layer_2[curr_order-1] #calculate the pairwise commutators between each of the error generators in current_errgen_dict_1 and #current_errgen_dict_2. - for error1 in current_errgen_dict_1.keys(): - for error2 in current_errgen_dict_2.keys(): + commuted_errgen_list = [] + for error1 in errgen_layer_1.keys(): + for error2 in errgen_layer_2.keys(): #get the list of error generator labels - commuted_errgen_list = error_generator_commutator(error1, error2, - weight=1/2*current_errgen_dict_1[error1]*current_errgen_dict_1[error2]) - print(commuted_errgen_list) - #Add all of these error generators to the working dictionary of updated error generators and weights. - #There may be duplicates, which should be summed together. - for error_tuple in commuted_errgen_list: - working_order_dict[error_tuple[0]]=error_tuple[1] + commuted_errgen_sublist = error_generator_commutator(error1, error2, + weight= .5*errgen_layer_1[error1]*errgen_layer_2[error2]) + commuted_errgen_list.extend(commuted_errgen_sublist) + #print(f'{commuted_errgen_list=}') + #loop through all of the elements of commuted_errorgen_list and instantiate a dictionary with the requisite keys. + second_order_comm_dict = {error_tuple[0]:0 for error_tuple in commuted_errgen_list} + + #Add all of these error generators to the working dictionary of updated error generators and weights. + #There may be duplicates, which should be summed together. + for error_tuple in commuted_errgen_list: + second_order_comm_dict[error_tuple[0]] += error_tuple[1] + new_errorgen_layer.append(second_order_comm_dict) + #third order BCH terms + # (1/12)*([X,[X,Y]] - [Y,[X,Y]]) + elif curr_order == 2: + #we've already calculated (1/2)*[X,Y] in the previous order, so reuse this result. + #two different lists for the two different commutators so that we can more easily reuse + #this at higher order if needed. + commuted_errgen_list_1 = [] + commuted_errgen_list_2 = [] + first_order_comm = new_errorgen_layer[1] + + for error1a, error1b in zip(errgen_layer_1.keys(), errgen_layer_2.keys()): + for error2 in first_order_comm: + first_order_comm_rate = first_order_comm[error2] + #only need a factor of 1/6 because new_errorgen_layer[1] is 1/2 the commutator + commuted_errgen_sublist = error_generator_commutator(error1a, error2, + weight=(1/6)*errgen_layer_1[error1a]*first_order_comm_rate) + commuted_errgen_list_1.extend(commuted_errgen_sublist) + + #only need a factor of -1/6 because new_errorgen_layer[1] is 1/2 the commutator + commuted_errgen_sublist = error_generator_commutator(error1b, error2, + weight=-(1/6)*errgen_layer_2[error1b]*first_order_comm_rate) + commuted_errgen_list_2.extend(commuted_errgen_sublist) + + #turn the two new commuted error generator lists into dictionaries. + #loop through all of the elements of commuted_errorgen_list and instantiate a dictionary with the requisite keys. + third_order_comm_dict_1 = {error_tuple[0]:0 for error_tuple in commuted_errgen_list_1} + third_order_comm_dict_2 = {error_tuple[0]:0 for error_tuple in commuted_errgen_list_2} - if len(errgen_layer_1)==2: - for error_key in errgen_layer_1[1]: - working_order_dict[error_key]=errgen_layer_1[1][error_key] - if len(errgen_layer_2)==2: - for error_key in errgen_layer_2[1]: - working_order_dict[error_key]=errgen_layer_2[1][error_key] - new_errorgen_layer.append(working_order_dict) + #Add all of these error generators to the working dictionary of updated error generators and weights. + #There may be duplicates, which should be summed together. + for error_tuple in commuted_errgen_list_1: + third_order_comm_dict_1[error_tuple[0]] += error_tuple[1] + for error_tuple in commuted_errgen_list_2: + third_order_comm_dict_2[error_tuple[0]] += error_tuple[1] + + #finally sum these two dictionaries + third_order_comm_dict = {key: third_order_comm_dict_1.get(key, 0) + third_order_comm_dict_2.get(key, 0) + for key in set(third_order_comm_dict_1) | set(third_order_comm_dict_2)} + new_errorgen_layer.append(third_order_comm_dict) + + #fourth order BCH terms + # -(1/24)*[Y,[X,[X,Y]]] + elif curr_order == 3: + #we've already calculated (1/12)*[X,[X,Y]] so reuse this result. + #this is stored in third_order_comm_dict_1 + commuted_errgen_list = [] + for error1 in errgen_layer_2.keys(): + for error2 in third_order_comm_dict_1.keys(): + #only need a factor of -1/2 because third_order_comm_dict_1 is 1/12 the nested commutator + commuted_errgen_sublist = error_generator_commutator(error1, error2, + weight= -.5*errgen_layer_2[error1]*third_order_comm_dict_1[error2]) + commuted_errgen_list.extend(commuted_errgen_sublist) + + #loop through all of the elements of commuted_errorgen_list and instantiate a dictionary with the requisite keys. + fourth_order_comm_dict = {error_tuple[0]:0 for error_tuple in commuted_errgen_list} + + #Add all of these error generators to the working dictionary of updated error generators and weights. + #There may be duplicates, which should be summed together. + for error_tuple in commuted_errgen_list: + fourth_order_comm_dict[error_tuple[0]] += error_tuple[1] + new_errorgen_layer.append(fourth_order_comm_dict) else: - raise ValueError("Higher Orders are not Implemented Yet") - return new_errorgen_layer + raise NotImplementedError("Higher orders beyond fourth order are not implemented yet.") + + #Finally accumulate all of the dictionaries in new_errorgen_layer into a single one, summing overlapping terms. + errorgen_labels_by_order = [set(order_dict) for order_dict in new_errorgen_layer] + complete_errorgen_labels = reduce(lambda a, b: a|b, errorgen_labels_by_order) + + #initialize a dictionary with requisite keys + new_errorgen_layer_dict = {lbl: 0 for lbl in complete_errorgen_labels} + + for order_dict in new_errorgen_layer: + for lbl, rate in order_dict.items(): + new_errorgen_layer_dict[lbl] += rate + + return new_errorgen_layer_dict def error_generator_commutator(errorgen_1, errorgen_2, flip_weight=False, weight=1.0):