From 6e3c345b6dd4e814b8b69d226146fa436bcf9544 Mon Sep 17 00:00:00 2001 From: "Stefan K. Seritan" Date: Tue, 21 May 2024 14:10:04 -0700 Subject: [PATCH 1/9] Fix #408. This derives truncation functions for ByDepthDesign and BenchmarkingDesign such that paired list information (depths, circuits, idealouts) are maintained through truncation. --- pygsti/protocols/vb.py | 72 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/pygsti/protocols/vb.py b/pygsti/protocols/vb.py index 7c52e81a6..e99f8fdd7 100644 --- a/pygsti/protocols/vb.py +++ b/pygsti/protocols/vb.py @@ -11,11 +11,12 @@ #*************************************************************************************************** import numpy as _np +import copy as _copy -from pygsti.protocols import protocol as _proto -from pygsti.models.oplessmodel import SuccessFailModel as _SuccessFailModel from pygsti import tools as _tools from pygsti.algorithms import randomcircuit as _rc +from pygsti.protocols import protocol as _proto +from pygsti.models.oplessmodel import SuccessFailModel as _SuccessFailModel class ByDepthDesign(_proto.CircuitListsDesign): @@ -67,6 +68,25 @@ def map_qubit_labels(self, mapper): mapped_qubit_labels = self._mapped_qubit_labels(mapper) return ByDepthDesign(self.depths, mapped_circuit_lists, mapped_qubit_labels, remove_duplicates=False) + def truncate_to_lists(self, list_indices_to_keep): + """ + Truncates this experiment design by only keeping a subset of its circuit lists. + + Parameters + ---------- + list_indices_to_keep : iterable + A list of the (integer) list indices to keep. + + Returns + ------- + ByDepthDesign + The truncated experiment design. + """ + ret = _copy.deepcopy(self) # Works for derived classes too + ret.depths = [self.depths[i] for i in list_indices_to_keep] + ret.circuit_lists = [self.circuit_lists[i] for i in list_indices_to_keep] + return ret + class BenchmarkingDesign(ByDepthDesign): """ @@ -133,6 +153,54 @@ def map_qubit_labels(self, mapper): mapped_qubit_labels = self._mapped_qubit_labels(mapper) return BenchmarkingDesign(self.depths, mapped_circuit_lists, list(self.idealout_lists), mapped_qubit_labels, remove_duplicates=False) + + def truncate_to_lists(self, list_indices_to_keep): + """ + Truncates this experiment design by only keeping a subset of its circuit lists. + + Parameters + ---------- + list_indices_to_keep : iterable + A list of the (integer) list indices to keep. + + Returns + ------- + BenchmarkingDesign + The truncated experiment design. + """ + ret = _copy.deepcopy(self) # Works for derived classes too + ret.depths = [self.depths[i] for i in list_indices_to_keep] + ret.circuit_lists = [self.circuit_lists[i] for i in list_indices_to_keep] + ret.idealout_lists = [self.idealout_lists[i] for i in list_indices_to_keep] + return ret + + def _truncate_to_circuits_inplace(self, circuits_to_keep): + truncated_circuit_lists = [] + truncated_idealout_lists = [] + for circuits, idealouts in zip(self.circuit_lists, self.idealout_lists): + new_circuits, new_idealouts = zip(*filter(lambda ci: ci[0] in set(circuits_to_keep), zip(circuits, idealouts))) + truncated_circuit_lists.append(new_circuits) + truncated_idealout_lists.append(new_idealouts) + + self.circuit_lists = truncated_circuit_lists + self.idealout_lists = truncated_idealout_lists + self.nested = False # we're not sure whether the truncated lists are nested + super()._truncate_to_circuits_inplace(circuits_to_keep) + + def _truncate_to_design_inplace(self, other_design): + truncated_circuit_lists = [] + truncated_idealout_lists = [] + for circuits, idealouts, other_circuits in zip(self.circuit_lists, self.idealout_lists, other_design.circuit_lists): + new_circuits, new_idealouts = zip(*filter(lambda ci: ci[0] in set(other_circuits), zip(circuits, idealouts))) + truncated_circuit_lists.append(new_circuits) + truncated_idealout_lists.append(new_idealouts) + + self.circuit_lists = truncated_circuit_lists + self.idealout_lists = truncated_idealout_lists + super()._truncate_to_design_inplace(other_design) + + def _truncate_to_available_data_inplace(self, dataset): + self._truncate_to_circuits_inplace(set(dataset.keys())) class PeriodicMirrorCircuitDesign(BenchmarkingDesign): From 78576639b4ddf7b2d532f66b4e45c3fe3575ecc6 Mon Sep 17 00:00:00 2001 From: "Stefan K. Seritan" Date: Tue, 21 May 2024 15:43:39 -0700 Subject: [PATCH 2/9] Fixes #315. Allows optional return of number of native gates per Clifford during random circuit compilation, and stores these in the CliffordRBDesign. Adds utility function for computing average native gates per Clifford, and ensures this works through design truncation. Also adds a missing utility to compute total number of gates to Circuits. --- pygsti/algorithms/randomcircuit.py | 16 +++- pygsti/circuits/circuit.py | 24 +++++ pygsti/protocols/rb.py | 139 ++++++++++++++++++++++++++--- 3 files changed, 164 insertions(+), 15 deletions(-) diff --git a/pygsti/algorithms/randomcircuit.py b/pygsti/algorithms/randomcircuit.py index c45f1d76c..e68e5996c 100644 --- a/pygsti/algorithms/randomcircuit.py +++ b/pygsti/algorithms/randomcircuit.py @@ -2139,7 +2139,8 @@ def create_direct_rb_circuit(pspec, clifford_compilations, length, qubit_labels= def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_labels=None, randomizeout=False, - citerations=20, compilerargs=None, interleaved_circuit=None, seed=None): + citerations=20, compilerargs=None, interleaved_circuit=None, seed=None, + return_num_native_gates=False): """ Generates a "Clifford randomized benchmarking" (CRB) circuit. @@ -2223,6 +2224,9 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label seed : int, optional A seed to initialize the random number generator used for creating random clifford circuits. + + return_num_native_gates: bool, optional + Whether to return the number of native gates in the first `length`+1 compiled Cliffords Returns ------- @@ -2236,6 +2240,10 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label `qubit_labels`, if `qubit_labels` is not None; the ith element of `pspec.qubit_labels`, otherwise. In both cases, the ith element of the tuple corresponds to the error-free outcome for the qubit on the ith wire of the output circuit. + + num_native_gates: int + Total number of native gates in the first `length`+1 compiled Cliffords. + Only returned when `return_num_native_gates` is True """ if compilerargs is None: compilerargs = [] @@ -2255,6 +2263,7 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label # Sample length+1 uniformly random Cliffords (we want a circuit of length+2 Cliffords, in total), compile # them, and append them to the current circuit. + num_native_gates = 0 for i in range(0, length + 1): s, p = _symp.random_clifford(n, rand_state=rand_state) @@ -2263,6 +2272,8 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label clifford_compilations.get('paulieq', None), qubit_labels=qubit_labels, iterations=citerations, *compilerargs, rand_state=rand_state) + num_native_gates += circuit.num_gates + # Keeps track of the current composite Clifford s_composite, p_composite = _symp.compose_cliffords(s_composite, p_composite, s, p) full_circuit.append_circuit_inplace(circuit) @@ -2306,6 +2317,9 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label idealout = tuple(idealout) full_circuit.done_editing() + + if return_num_native_gates: + return full_circuit, idealout, num_native_gates return full_circuit, idealout diff --git a/pygsti/circuits/circuit.py b/pygsti/circuits/circuit.py index 8e629031a..25cbdd923 100644 --- a/pygsti/circuits/circuit.py +++ b/pygsti/circuits/circuit.py @@ -3405,6 +3405,30 @@ def two_q_gate_count(self): """ return self.num_nq_gates(2) + @property + def num_gates(self): + """ + The number of gates in the circuit. + + Returns + ------- + int + """ + if self._static: + def cnt(lbl): # obj a Label, perhaps compound + if lbl.is_simple(): # a simple label + return 1 if (lbl.sslbls is not None) else 0 + else: + return sum([cnt(sublbl) for sublbl in lbl.components]) + else: + def cnt(obj): # obj is either a simple label or a list + if isinstance(obj, _Label): # all Labels are simple labels + return 1 if (obj.sslbls is not None) else 0 + else: + return sum([cnt(sub) for sub in obj]) + + return sum([cnt(layer_lbl) for layer_lbl in self._labels]) + def num_nq_gates(self, nq): """ The number of `nq`-qubit gates in the circuit. diff --git a/pygsti/protocols/rb.py b/pygsti/protocols/rb.py index 484e5fb68..d4a5c7c21 100644 --- a/pygsti/protocols/rb.py +++ b/pygsti/protocols/rb.py @@ -111,7 +111,7 @@ class CliffordRBDesign(_vb.BenchmarkingDesign): """ @classmethod - def from_existing_circuits(cls, circuits_and_idealouts_by_depth, qubit_labels=None, + def from_existing_circuits(cls, data_by_depth, qubit_labels=None, randomizeout=False, citerations=20, compilerargs=(), interleaved_circuit=None, descriptor='A Clifford RB experiment', add_default_protocol=False): """ @@ -123,10 +123,12 @@ def from_existing_circuits(cls, circuits_and_idealouts_by_depth, qubit_labels=No Parameters ---------- - circuits_and_idealouts_by_depth : dict - A dictionary whose keys are integer depths and whose values are lists of `(circuit, ideal_outcome)` - 2-tuples giving each RB circuit and its - ideal (correct) outcome. + data_by_depth : dict + A dictionary whose keys are integer depths and whose values are lists of + `(circuit, ideal_outcome, num_native_gates)` tuples giving each RB circuit, its + ideal (correct) outcome, and (optionally) the number of native gates in the compiled Cliffords. + If only a 2-tuple is passed, i.e. number of native gates is not included, + the :meth:`average_gates_per_clifford()` function will not work. qubit_labels : list, optional If not None, a list of the qubits that the RB circuits are to be sampled for. This should @@ -183,14 +185,18 @@ def from_existing_circuits(cls, circuits_and_idealouts_by_depth, qubit_labels=No ------- CliffordRBDesign """ - depths = sorted(list(circuits_and_idealouts_by_depth.keys())) - circuit_lists = [[x[0] for x in circuits_and_idealouts_by_depth[d]] for d in depths] - ideal_outs = [[x[1] for x in circuits_and_idealouts_by_depth[d]] for d in depths] - circuits_per_depth = [len(circuits_and_idealouts_by_depth[d]) for d in depths] + depths = sorted(list(data_by_depth.keys())) + circuit_lists = [[x[0] for x in data_by_depth[d]] for d in depths] + ideal_outs = [[x[1] for x in data_by_depth[d]] for d in depths] + try: + num_native_gates = [[x[2] for x in data_by_depth[d]] for d in depths] + except KeyError: + num_native_gates = None + circuits_per_depth = [len(data_by_depth[d]) for d in depths] self = cls.__new__(cls) self._init_foundation(depths, circuit_lists, ideal_outs, circuits_per_depth, qubit_labels, randomizeout, citerations, compilerargs, descriptor, add_default_protocol, - interleaved_circuit) + interleaved_circuit, num_native_gates=num_native_gates) return self def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qubit_labels=None, randomizeout=False, @@ -199,6 +205,7 @@ def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qub if qubit_labels is None: qubit_labels = tuple(pspec.qubit_labels) circuit_lists = [] ideal_outs = [] + num_native_gates = [] if seed is None: self.seed = _np.random.randint(1, 1e6) # Pick a random seed @@ -214,27 +221,31 @@ def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qub args_list = [(pspec, clifford_compilations, l)] * circuits_per_depth kwargs_list = [dict(qubit_labels=qubit_labels, randomizeout=randomizeout, citerations=citerations, compilerargs=compilerargs, interleaved_circuit=interleaved_circuit, - seed=lseed + i) for i in range(circuits_per_depth)] + seed=lseed + i, return_num_native_gates=True) for i in range(circuits_per_depth)] results = _tools.mptools.starmap_with_kwargs(_rc.create_clifford_rb_circuit, circuits_per_depth, num_processes, args_list, kwargs_list) circuits_at_depth = [] idealouts_at_depth = [] - for c, iout in results: + num_native_gates_at_depth = [] + for c, iout, nng in results: circuits_at_depth.append(c) idealouts_at_depth.append((''.join(map(str, iout)),)) + num_native_gates_at_depth.append(nng) circuit_lists.append(circuits_at_depth) ideal_outs.append(idealouts_at_depth) + num_native_gates.append(num_native_gates_at_depth) self._init_foundation(depths, circuit_lists, ideal_outs, circuits_per_depth, qubit_labels, randomizeout, citerations, compilerargs, descriptor, add_default_protocol, - interleaved_circuit) + interleaved_circuit, num_native_gates=num_native_gates) def _init_foundation(self, depths, circuit_lists, ideal_outs, circuits_per_depth, qubit_labels, randomizeout, citerations, compilerargs, descriptor, add_default_protocol, - interleaved_circuit): + interleaved_circuit, num_native_gates=None): super().__init__(depths, circuit_lists, ideal_outs, qubit_labels, remove_duplicates=False) + self.num_native_gate_lists = num_native_gates self.circuits_per_depth = circuits_per_depth self.randomizeout = randomizeout self.citerations = citerations @@ -248,6 +259,70 @@ def _init_foundation(self, depths, circuit_lists, ideal_outs, circuits_per_depth defaultfit = 'full' self.add_default_protocol(RB(name='RB', defaultfit=defaultfit)) + def average_native_gates_per_clifford_for_circuit(self, list_idx, circ_idx): + """The average number of native gates per Clifford for a specific circuit + + Parameters + ---------- + list_idx: int + The index of the circuit list (for a given depth) + + circ_idx: int + The index of the circuit within the circuit list + + Returns + ------- + float + The average number of native gates per Clifford + """ + if self.num_native_gate_lists is None: + raise ValueError("Number of native gates not available, cannot compute average gates per Clifford") + num_native_gates = self.num_native_gate_lists[list_idx][circ_idx] + num_clifford_gates = self.depths[list_idx] + 1 + return num_native_gates / num_clifford_gates + + def average_native_gates_per_clifford_for_circuit_list(self, list_idx): + """The average number of gates per Clifford for a circuit list + + This essentially gives the average number of native gates per Clifford + for a given depth (indexed by list index, not depth). + + Parameters + ---------- + list_idx: int + The index of the circuit list (for a given depth) + + circ_idx: int + The index of the circuit within the circuit list + + Returns + ------- + float + The average number of native gates per Clifford + """ + if self.num_native_gate_lists is None: + raise ValueError("Number of native gates not available, cannot compute average gates per Clifford") + num_native_gates = sum(self.num_native_gate_lists[list_idx]) + num_clifford_gates = len(self.num_native_gate_lists[list_idx]) * (self.depths[list_idx] + 1) + return num_native_gates / num_clifford_gates + + def average_native_gates_per_clifford(self): + """The average number of native gates per Clifford for all circuits + + Returns + ------- + float + The average number of native gates per Clifford + """ + if self.num_native_gate_lists is None: + raise ValueError("Number of native gates not available, cannot compute average gates per Clifford") + num_native_gates = 0 + num_clifford_gates = 0 + for list_idx in range(len(self.depths)): + num_native_gates = sum(self.num_native_gate_lists[list_idx]) + num_clifford_gates = len(self.num_native_gate_lists[list_idx]) * (self.depths[list_idx] + 1) + return num_native_gates / num_clifford_gates + def map_qubit_labels(self, mapper): """ Creates a new experiment design whose circuits' qubit labels are updated according to a given mapping. @@ -273,6 +348,42 @@ def map_qubit_labels(self, mapper): self.interleaved_circuit, self.descriptor, add_default_protocol=False) + def _truncate_to_circuits_inplace(self, circuits_to_keep): + if self.num_native_gate_lists is not None: + truncated_circuit_lists = [] + truncated_idealout_lists = [] + truncated_num_native_gate_lists = [] + for circuits, idealouts, nngs in zip(self.circuit_lists, self.idealout_lists, self.num_native_gate_lists): + new_circuits, new_idealouts, new_nngs = zip(*filter(lambda ci: ci[0] in set(circuits_to_keep), zip(circuits, idealouts, nngs))) + truncated_circuit_lists.append(new_circuits) + truncated_idealout_lists.append(new_idealouts) + truncated_num_native_gate_lists.append(new_nngs) + + self.circuit_lists = truncated_circuit_lists + self.idealout_lists = truncated_idealout_lists + self.num_native_gate_lists = truncated_num_native_gate_lists + # TODO: What to do about circuit_per_depth, which may no longer be correct, or even uniform + # Should it be a list of ints instead of a single int? + super()._truncate_to_circuits_inplace(circuits_to_keep) + + def _truncate_to_design_inplace(self, other_design): + if self.num_native_gate_lists is not None: + truncated_circuit_lists = [] + truncated_idealout_lists = [] + truncated_num_native_gate_lists = [] + for circuits, idealouts, nngs, other_circuits in zip(self.circuit_lists, self.idealout_lists, self.num_native_gate_lists, other_design.circuit_lists): + new_circuits, new_idealouts, new_nngs = zip(*filter(lambda ci: ci[0] in set(other_circuits), zip(circuits, idealouts, nngs))) + truncated_circuit_lists.append(new_circuits) + truncated_idealout_lists.append(new_idealouts) + truncated_num_native_gate_lists.append(new_nngs) + + self.circuit_lists = truncated_circuit_lists + self.idealout_lists = truncated_idealout_lists + self.num_native_gate_lists = truncated_num_native_gate_lists + # TODO: What to do about circuit_per_depth, which may no longer be correct, or even uniform + # Should it be a list of ints instead of a single int? + super()._truncate_to_design_inplace(other_design) + class DirectRBDesign(_vb.BenchmarkingDesign): """ From 9d6339c6436ba222ca29b715026733408d9c80d7 Mon Sep 17 00:00:00 2001 From: "Stefan K. Seritan" Date: Tue, 21 May 2024 16:32:07 -0700 Subject: [PATCH 3/9] Updated fix for #408. Generalizes serialization/truncation for attributes that are "paired" with the circuit lists in BenchmarkingDesigns. This removes the code duplication in inherited classes with more "paired" attributes, such as CliffordRBDesign (with the new native gate info) and BinaryRBDesign (with measurements/signs). --- pygsti/protocols/rb.py | 51 +++++++------------------------------ pygsti/protocols/vb.py | 57 +++++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 57 deletions(-) diff --git a/pygsti/protocols/rb.py b/pygsti/protocols/rb.py index d4a5c7c21..d90c26759 100644 --- a/pygsti/protocols/rb.py +++ b/pygsti/protocols/rb.py @@ -190,7 +190,7 @@ def from_existing_circuits(cls, data_by_depth, qubit_labels=None, ideal_outs = [[x[1] for x in data_by_depth[d]] for d in depths] try: num_native_gates = [[x[2] for x in data_by_depth[d]] for d in depths] - except KeyError: + except IndexError: num_native_gates = None circuits_per_depth = [len(data_by_depth[d]) for d in depths] self = cls.__new__(cls) @@ -244,8 +244,12 @@ def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qub def _init_foundation(self, depths, circuit_lists, ideal_outs, circuits_per_depth, qubit_labels, randomizeout, citerations, compilerargs, descriptor, add_default_protocol, interleaved_circuit, num_native_gates=None): - super().__init__(depths, circuit_lists, ideal_outs, qubit_labels, remove_duplicates=False) self.num_native_gate_lists = num_native_gates + if self.num_native_gate_lists is not None: + # If we have native gate information, pair this with circuit data so that we serialize/truncate properly + self.paired_with_circuit_attrs = ["num_native_gate_lists"] + + super().__init__(depths, circuit_lists, ideal_outs, qubit_labels, remove_duplicates=False) self.circuits_per_depth = circuits_per_depth self.randomizeout = randomizeout self.citerations = citerations @@ -348,42 +352,6 @@ def map_qubit_labels(self, mapper): self.interleaved_circuit, self.descriptor, add_default_protocol=False) - def _truncate_to_circuits_inplace(self, circuits_to_keep): - if self.num_native_gate_lists is not None: - truncated_circuit_lists = [] - truncated_idealout_lists = [] - truncated_num_native_gate_lists = [] - for circuits, idealouts, nngs in zip(self.circuit_lists, self.idealout_lists, self.num_native_gate_lists): - new_circuits, new_idealouts, new_nngs = zip(*filter(lambda ci: ci[0] in set(circuits_to_keep), zip(circuits, idealouts, nngs))) - truncated_circuit_lists.append(new_circuits) - truncated_idealout_lists.append(new_idealouts) - truncated_num_native_gate_lists.append(new_nngs) - - self.circuit_lists = truncated_circuit_lists - self.idealout_lists = truncated_idealout_lists - self.num_native_gate_lists = truncated_num_native_gate_lists - # TODO: What to do about circuit_per_depth, which may no longer be correct, or even uniform - # Should it be a list of ints instead of a single int? - super()._truncate_to_circuits_inplace(circuits_to_keep) - - def _truncate_to_design_inplace(self, other_design): - if self.num_native_gate_lists is not None: - truncated_circuit_lists = [] - truncated_idealout_lists = [] - truncated_num_native_gate_lists = [] - for circuits, idealouts, nngs, other_circuits in zip(self.circuit_lists, self.idealout_lists, self.num_native_gate_lists, other_design.circuit_lists): - new_circuits, new_idealouts, new_nngs = zip(*filter(lambda ci: ci[0] in set(other_circuits), zip(circuits, idealouts, nngs))) - truncated_circuit_lists.append(new_circuits) - truncated_idealout_lists.append(new_idealouts) - truncated_num_native_gate_lists.append(new_nngs) - - self.circuit_lists = truncated_circuit_lists - self.idealout_lists = truncated_idealout_lists - self.num_native_gate_lists = truncated_num_native_gate_lists - # TODO: What to do about circuit_per_depth, which may no longer be correct, or even uniform - # Should it be a list of ints instead of a single int? - super()._truncate_to_design_inplace(other_design) - class DirectRBDesign(_vb.BenchmarkingDesign): """ @@ -1081,6 +1049,9 @@ def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qub def _init_foundation(self, depths, circuit_lists, measurements, signs, circuits_per_depth, qubit_labels, layer_sampling, sampler, samplerargs, addlocal, lsargs, descriptor, add_default_protocol): + # Pair these attributes with circuit data so that we serialize/truncate properly + self.paired_with_circuit_attrs = ["measurements", "signs"] + super().__init__(depths, circuit_lists, signs, qubit_labels, remove_duplicates=False) self.measurements = measurements self.signs = signs @@ -1097,10 +1068,6 @@ def _init_foundation(self, depths, circuit_lists, measurements, signs, circuits_ defaultfit = 'A-fixed' self.add_default_protocol(RB(name='RB', defaultfit=defaultfit)) - - self.auxfile_types['signs'] = 'json' # Makes sure that signs and measurements are saved seperately - self.auxfile_types['measurements'] = 'json' - class RandomizedBenchmarking(_vb.SummaryStatistics): """ diff --git a/pygsti/protocols/vb.py b/pygsti/protocols/vb.py index e99f8fdd7..9a734d310 100644 --- a/pygsti/protocols/vb.py +++ b/pygsti/protocols/vb.py @@ -118,12 +118,27 @@ class BenchmarkingDesign(ByDepthDesign): Whether to remove duplicates when automatically creating all the circuits that need data. """ + + paired_with_circuit_attrs = None + """List of attributes which are paired up with circuit lists + + These will be saved as external files during serialization, + and are truncated when circuit lists are truncated. + """ def __init__(self, depths, circuit_lists, ideal_outs, qubit_labels=None, remove_duplicates=False): assert(len(depths) == len(ideal_outs)) super().__init__(depths, circuit_lists, qubit_labels, remove_duplicates) + self.idealout_lists = ideal_outs - self.auxfile_types['idealout_lists'] = 'json' + + if self.paired_with_circuit_attrs is None: + self.paired_with_circuit_attrs = ['idealout_lists'] + else: + self.paired_with_circuit_attrs.insert(0, 'idealout_lists') + + for paired_attr in self.paired_with_circuit_attrs: + self.auxfile_types[paired_attr] = 'json' def _mapped_circuits_and_idealouts_by_depth(self, mapper): """ Used in derived classes """ @@ -171,32 +186,44 @@ def truncate_to_lists(self, list_indices_to_keep): ret = _copy.deepcopy(self) # Works for derived classes too ret.depths = [self.depths[i] for i in list_indices_to_keep] ret.circuit_lists = [self.circuit_lists[i] for i in list_indices_to_keep] - ret.idealout_lists = [self.idealout_lists[i] for i in list_indices_to_keep] + for paired_attr in self.paired_with_circuit_attrs: + val = getattr(self, paired_attr) + new_val = [val[i] for i in list_indices_to_keep] + setattr(ret, paired_attr, new_val) return ret def _truncate_to_circuits_inplace(self, circuits_to_keep): truncated_circuit_lists = [] - truncated_idealout_lists = [] - for circuits, idealouts in zip(self.circuit_lists, self.idealout_lists): - new_circuits, new_idealouts = zip(*filter(lambda ci: ci[0] in set(circuits_to_keep), zip(circuits, idealouts))) - truncated_circuit_lists.append(new_circuits) - truncated_idealout_lists.append(new_idealouts) + paired_attr_lists_list = [getattr(self, paired_attr) for paired_attr in self.paired_with_circuit_attrs] + truncated_paired_attr_lists_list = [[] for _ in range(len(self.paired_with_circuit_attrs))] + for list_idx, circuits in enumerate(self.circuit_lists): + paired_attrs = [pal[list_idx] for pal in paired_attr_lists_list] + # Do the same filtering as CircuitList.truncate, but drag along any paired attributes + new_data = list(zip(*filter(lambda ci: ci[0] in set(circuits_to_keep), zip(circuits, *paired_attrs)))) + truncated_circuit_lists.append(new_data[0]) + for i, attr_data in enumerate(new_data[1:]): + truncated_paired_attr_lists_list[i].append(attr_data) self.circuit_lists = truncated_circuit_lists - self.idealout_lists = truncated_idealout_lists - self.nested = False # we're not sure whether the truncated lists are nested + for paired_attr, paired_attr_lists in zip(self.paired_with_circuit_attrs, truncated_paired_attr_lists_list): + setattr(self, paired_attr, paired_attr_lists) super()._truncate_to_circuits_inplace(circuits_to_keep) def _truncate_to_design_inplace(self, other_design): truncated_circuit_lists = [] - truncated_idealout_lists = [] - for circuits, idealouts, other_circuits in zip(self.circuit_lists, self.idealout_lists, other_design.circuit_lists): - new_circuits, new_idealouts = zip(*filter(lambda ci: ci[0] in set(other_circuits), zip(circuits, idealouts))) - truncated_circuit_lists.append(new_circuits) - truncated_idealout_lists.append(new_idealouts) + paired_attr_lists_list = [getattr(self, paired_attr) for paired_attr in self.paired_with_circuit_attrs] + truncated_paired_attr_lists_list = [[] for _ in range(len(self.paired_with_circuit_attrs))] + for list_idx, circuits in enumerate(self.circuit_lists): + paired_attrs = [pal[list_idx] for pal in paired_attr_lists_list] + # Do the same filtering as CircuitList.truncate, but drag along any paired attributes + new_data = list(zip(*filter(lambda ci: ci[0] in set(other_design.circuit_lists[list_idx]), zip(circuits, *paired_attrs)))) + truncated_circuit_lists.append(new_data[0]) + for i, attr_data in enumerate(new_data[1:]): + truncated_paired_attr_lists_list[i].append(attr_data) self.circuit_lists = truncated_circuit_lists - self.idealout_lists = truncated_idealout_lists + for paired_attr, paired_attr_lists in zip(self.paired_with_circuit_attrs, truncated_paired_attr_lists_list): + setattr(self, paired_attr, paired_attr_lists) super()._truncate_to_design_inplace(other_design) def _truncate_to_available_data_inplace(self, dataset): From a1957628f479482189e41edeb75758dbe2657622 Mon Sep 17 00:00:00 2001 From: "Stefan K. Seritan" Date: Wed, 22 May 2024 14:37:41 -0700 Subject: [PATCH 4/9] Fixes #314. Adds an option `exact_compilation_key` that allows the user to specify one of the CompilationRules to use for deterministic Clifford compilation in CliffordRBDesign generation. --- pygsti/algorithms/randomcircuit.py | 59 ++++++++++++++++++++++++------ pygsti/protocols/rb.py | 10 +++-- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/pygsti/algorithms/randomcircuit.py b/pygsti/algorithms/randomcircuit.py index e68e5996c..f0ce28884 100644 --- a/pygsti/algorithms/randomcircuit.py +++ b/pygsti/algorithms/randomcircuit.py @@ -2140,7 +2140,7 @@ def create_direct_rb_circuit(pspec, clifford_compilations, length, qubit_labels= def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_labels=None, randomizeout=False, citerations=20, compilerargs=None, interleaved_circuit=None, seed=None, - return_num_native_gates=False): + return_num_native_gates=False, exact_compilation_key=None): """ Generates a "Clifford randomized benchmarking" (CRB) circuit. @@ -2166,9 +2166,10 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label the gates in `pspec`, and will respect the connectivity encoded by `pspec`. clifford_compilations : dict - A dictionary with the potential keys `'absolute'` and `'paulieq'` and corresponding + A dictionary with at least the potential keys `'absolute'` and `'paulieq'` and corresponding :class:`CompilationRules` values. These compilation rules specify how to compile the - "native" gates of `pspec` into Clifford gates. + "native" gates of `pspec` into Clifford gates. Additional :class:`CompilationRules` can be + provided, particularly for use with `exact_compilation_key`. length : int The "CRB length" of the circuit -- an integer >= 0 -- which is the number of Cliffords in the @@ -2227,6 +2228,15 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label return_num_native_gates: bool, optional Whether to return the number of native gates in the first `length`+1 compiled Cliffords + + exact_compilation_key: str, optional + The key into `clifford_compilations` to use for exact deterministic complation of Cliffords. + The underlying :class:`CompilationRules` object must provide compilations for all possible + n-qubit Cliffords that will be generated. This also requires the pspec is able to generate the + symplectic representations for all n-qubit Cliffords in :meth:`compute_clifford_symplectic_reps`. + This is currently generally intended for use out-of-the-box with 1-qubit Clifford RB; + however, larger number of qubits can be used so long as the user specifies the processor spec and + compilation rules properly. Returns ------- @@ -2253,6 +2263,11 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label # The number of qubits the circuit is over. n = len(qubits) + if exact_compilation_key is not None: + # Precompute some of the symplectic reps if we are doing exact compilation + srep_cache = _symp.compute_internal_gate_symplectic_representations() + srep_cache.update(pspec.compute_clifford_symplectic_reps()) + rand_state = _np.random.RandomState(seed) # OK if seed is None # Initialize the identity circuit rep. @@ -2264,14 +2279,36 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label # Sample length+1 uniformly random Cliffords (we want a circuit of length+2 Cliffords, in total), compile # them, and append them to the current circuit. num_native_gates = 0 - for i in range(0, length + 1): - - s, p = _symp.random_clifford(n, rand_state=rand_state) - circuit = _cmpl.compile_clifford(s, p, pspec, - clifford_compilations.get('absolute', None), - clifford_compilations.get('paulieq', None), - qubit_labels=qubit_labels, iterations=citerations, *compilerargs, - rand_state=rand_state) + for _ in range(0, length + 1): + if exact_compilation_key is not None: + # Deterministic compilation based on a provided clifford compilation + assert exact_compilation_key in clifford_compilations, \ + f"{exact_compilation_key} not provided in `clifford_compilations`" + + # Pick clifford + cidx = rand_state.randint(24**n) + lbl = _lbl.Label(f'C{cidx}', qubits) + + # Try to do deterministic compilation + try: + circuit = clifford_compilations[exact_compilation_key].retrieve_compilation_of(lbl) + except AssertionError: + raise ValueError( + f"Failed to compile n-qubit Clifford 'C{cidx}'. Ensure this is provided in the " + \ + "compilation rules, or use a compilation algorithm to synthesize it by not " + \ + "specifying `exact_compilation_key`." + ) + + # compute the symplectic rep of the chosen clifford + s, p = _symp.symplectic_rep_of_clifford_circuit(circuit, srep_cache) + else: + # Random compilation + s, p = _symp.random_clifford(n, rand_state=rand_state) + circuit = _cmpl.compile_clifford(s, p, pspec, + clifford_compilations.get('absolute', None), + clifford_compilations.get('paulieq', None), + qubit_labels=qubit_labels, iterations=citerations, *compilerargs, + rand_state=rand_state) num_native_gates += circuit.num_gates # Keeps track of the current composite Clifford diff --git a/pygsti/protocols/rb.py b/pygsti/protocols/rb.py index d90c26759..42f2649b7 100644 --- a/pygsti/protocols/rb.py +++ b/pygsti/protocols/rb.py @@ -200,8 +200,8 @@ def from_existing_circuits(cls, data_by_depth, qubit_labels=None, return self def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qubit_labels=None, randomizeout=False, - interleaved_circuit=None, citerations=20, compilerargs=(), descriptor='A Clifford RB experiment', - add_default_protocol=False, seed=None, verbosity=1, num_processes=1): + interleaved_circuit=None, citerations=20, compilerargs=(), exact_compilation_key=None, + descriptor='A Clifford RB experiment', add_default_protocol=False, seed=None, verbosity=1, num_processes=1): if qubit_labels is None: qubit_labels = tuple(pspec.qubit_labels) circuit_lists = [] ideal_outs = [] @@ -221,7 +221,8 @@ def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qub args_list = [(pspec, clifford_compilations, l)] * circuits_per_depth kwargs_list = [dict(qubit_labels=qubit_labels, randomizeout=randomizeout, citerations=citerations, compilerargs=compilerargs, interleaved_circuit=interleaved_circuit, - seed=lseed + i, return_num_native_gates=True) for i in range(circuits_per_depth)] + seed=lseed + i, return_num_native_gates=True, exact_compilation_key=exact_compilation_key) + for i in range(circuits_per_depth)] results = _tools.mptools.starmap_with_kwargs(_rc.create_clifford_rb_circuit, circuits_per_depth, num_processes, args_list, kwargs_list) @@ -243,7 +244,7 @@ def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qub def _init_foundation(self, depths, circuit_lists, ideal_outs, circuits_per_depth, qubit_labels, randomizeout, citerations, compilerargs, descriptor, add_default_protocol, - interleaved_circuit, num_native_gates=None): + interleaved_circuit, num_native_gates=None, exact_compilation_key=None): self.num_native_gate_lists = num_native_gates if self.num_native_gate_lists is not None: # If we have native gate information, pair this with circuit data so that we serialize/truncate properly @@ -256,6 +257,7 @@ def _init_foundation(self, depths, circuit_lists, ideal_outs, circuits_per_depth self.compilerargs = compilerargs self.descriptor = descriptor self.interleaved_circuit = interleaved_circuit + self.exact_compilation_key = exact_compilation_key if add_default_protocol: if randomizeout: defaultfit = 'A-fixed' From 8d586c5d63cdd365659aa3497991eefac8f3f830 Mon Sep 17 00:00:00 2001 From: "Stefan K. Seritan" Date: Thu, 23 May 2024 11:12:00 -0700 Subject: [PATCH 5/9] Add tests for RB fixes/upgrades. --- pygsti/protocols/rb.py | 1 + pygsti/protocols/vb.py | 22 +++-- test/unit/protocols/test_protocols.py | 117 ++++++++++++++++---------- test/unit/protocols/test_rb.py | 43 +++++++++- 4 files changed, 130 insertions(+), 53 deletions(-) diff --git a/pygsti/protocols/rb.py b/pygsti/protocols/rb.py index 42f2649b7..f3d09d472 100644 --- a/pygsti/protocols/rb.py +++ b/pygsti/protocols/rb.py @@ -203,6 +203,7 @@ def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qub interleaved_circuit=None, citerations=20, compilerargs=(), exact_compilation_key=None, descriptor='A Clifford RB experiment', add_default_protocol=False, seed=None, verbosity=1, num_processes=1): if qubit_labels is None: qubit_labels = tuple(pspec.qubit_labels) + assert len(qubit_labels) == len(pspec.qubit_labels), "Must provide qubit labels that match number of qubits in pspec" circuit_lists = [] ideal_outs = [] num_native_gates = [] diff --git a/pygsti/protocols/vb.py b/pygsti/protocols/vb.py index 9a734d310..ea20b64a6 100644 --- a/pygsti/protocols/vb.py +++ b/pygsti/protocols/vb.py @@ -200,9 +200,14 @@ def _truncate_to_circuits_inplace(self, circuits_to_keep): paired_attrs = [pal[list_idx] for pal in paired_attr_lists_list] # Do the same filtering as CircuitList.truncate, but drag along any paired attributes new_data = list(zip(*filter(lambda ci: ci[0] in set(circuits_to_keep), zip(circuits, *paired_attrs)))) - truncated_circuit_lists.append(new_data[0]) - for i, attr_data in enumerate(new_data[1:]): - truncated_paired_attr_lists_list[i].append(attr_data) + if len(new_data): + truncated_circuit_lists.append(new_data[0]) + for i, attr_data in enumerate(new_data[1:]): + truncated_paired_attr_lists_list[i].append(attr_data) + else: + # If we have truncated all circuits, append empty lists + truncated_circuit_lists.append([]) + truncated_paired_attr_lists_list.append([[] for _ in range(len(self.paired_with_circuit_attrs))]) self.circuit_lists = truncated_circuit_lists for paired_attr, paired_attr_lists in zip(self.paired_with_circuit_attrs, truncated_paired_attr_lists_list): @@ -217,9 +222,14 @@ def _truncate_to_design_inplace(self, other_design): paired_attrs = [pal[list_idx] for pal in paired_attr_lists_list] # Do the same filtering as CircuitList.truncate, but drag along any paired attributes new_data = list(zip(*filter(lambda ci: ci[0] in set(other_design.circuit_lists[list_idx]), zip(circuits, *paired_attrs)))) - truncated_circuit_lists.append(new_data[0]) - for i, attr_data in enumerate(new_data[1:]): - truncated_paired_attr_lists_list[i].append(attr_data) + if len(new_data): + truncated_circuit_lists.append(new_data[0]) + for i, attr_data in enumerate(new_data[1:]): + truncated_paired_attr_lists_list[i].append(attr_data) + else: + # If we have truncated all circuits, append empty lists + truncated_circuit_lists.append([]) + truncated_paired_attr_lists_list.append([[] for _ in range(len(self.paired_with_circuit_attrs))]) self.circuit_lists = truncated_circuit_lists for paired_attr, paired_attr_lists in zip(self.paired_with_circuit_attrs, truncated_paired_attr_lists_list): diff --git a/test/unit/protocols/test_protocols.py b/test/unit/protocols/test_protocols.py index 1385707b6..1a5d3f4bc 100644 --- a/test/unit/protocols/test_protocols.py +++ b/test/unit/protocols/test_protocols.py @@ -11,6 +11,51 @@ class ExperimentDesignTester(BaseCase): def setUpClass(cls): cls.gst_design = std.create_gst_experiment_design(4) + #Create a bunch of experiment designs: + from pygsti.protocols import ExperimentDesign, CircuitListsDesign, CombinedExperimentDesign, \ + SimultaneousExperimentDesign, FreeformDesign, StandardGSTDesign, GateSetTomographyDesign, \ + CliffordRBDesign, DirectRBDesign, MirrorRBDesign + from pygsti.processors import CliffordCompilationRules as CCR + + circuits_on0 = pygsti.circuits.to_circuits(["{}@(0)", "Gxpi2:0", "Gypi2:0"], line_labels=(0,)) + circuits_on0b = pygsti.circuits.to_circuits(["Gxpi2:0^2", "Gypi2:0^2"], line_labels=(0,)) + circuits_on1 = pygsti.circuits.to_circuits(["Gxpi2:1^2", "Gypi2:1^2"], line_labels=(1,)) + circuits_on01 = pygsti.circuits.to_circuits(["Gcnot:0:1", "Gxpi2:0Gypi2:1^2Gcnot:0:1Gxpi:0"], + line_labels=(0,1)) + + #For GST edesigns + mdl = std.target_model() + gst_pspec = mdl.create_processor_spec() + + #For RB edesigns + pspec = pygsti.processors.QubitProcessorSpec(2, ["Gxpi2", "Gypi2","Gxx"], + geometry='line', qubit_labels=(0,1)) + compilations = {"absolute": CCR.create_standard(pspec, "absolute", ("paulis", "1Qcliffords"), verbosity=0), + "paulieq": CCR.create_standard(pspec, "paulieq", ("1Qcliffords", "allcnots"), verbosity=0), + } + + pspec1Q = pygsti.processors.QubitProcessorSpec(1, ["Gxpi2", "Gypi2","Gxmpi2", "Gympi2"], + geometry='line', qubit_labels=(0,)) + compilations1Q = {"absolute": CCR.create_standard(pspec1Q, "absolute", ("paulis", "1Qcliffords"), verbosity=0), + "paulieq": CCR.create_standard(pspec1Q, "paulieq", ("1Qcliffords", "allcnots"), verbosity=0), + } + + edesigns = [] + edesigns.append(ExperimentDesign(circuits_on0)) + edesigns.append(CircuitListsDesign([circuits_on0, circuits_on0b])) + edesigns.append(CombinedExperimentDesign({'one': ExperimentDesign(circuits_on0), + 'two': ExperimentDesign(circuits_on1), + 'three': ExperimentDesign(circuits_on01)}, qubit_labels=(0,1))) + edesigns.append(SimultaneousExperimentDesign([ExperimentDesign(circuits_on0), ExperimentDesign(circuits_on1)])) + edesigns.append(FreeformDesign(circuits_on01)) + edesigns.append(std.create_gst_experiment_design(2)) + edesigns.append(GateSetTomographyDesign(gst_pspec, [circuits_on0, circuits_on0b])) + edesigns.append(CliffordRBDesign(pspec, compilations, depths=[0,2,5], circuits_per_depth=4)) + edesigns.append(DirectRBDesign(pspec, compilations, depths=[0,2,5], circuits_per_depth=4)) + edesigns.append(MirrorRBDesign(pspec1Q, depths=[0,2,4], circuits_per_depth=4, + clifford_compilations=compilations1Q)) + cls.edesigns = edesigns + def test_promotion(self): circuits = pygsti.circuits.to_circuits(["{}@(0)", "Gxpi2:0", "Gypi2:0"]) edesign1 = pygsti.protocols.ExperimentDesign(circuits) @@ -89,52 +134,7 @@ def test_create_edesign_fromdir_subdirs(self, root_path): self.assertTrue(all([a == b for a,b in zip(edesign3['subdir2'].all_circuits_needing_data, self.gst_design.circuit_lists[1])])) def test_map_edesign_sslbls(self): - #Create a bunch of experiment designs: - from pygsti.protocols import ExperimentDesign, CircuitListsDesign, CombinedExperimentDesign, \ - SimultaneousExperimentDesign, FreeformDesign, StandardGSTDesign, GateSetTomographyDesign, \ - CliffordRBDesign, DirectRBDesign, MirrorRBDesign - from pygsti.processors import CliffordCompilationRules as CCR - - circuits_on0 = pygsti.circuits.to_circuits(["{}@(0)", "Gxpi2:0", "Gypi2:0"], line_labels=(0,)) - circuits_on0b = pygsti.circuits.to_circuits(["Gxpi2:0^2", "Gypi2:0^2"], line_labels=(0,)) - circuits_on1 = pygsti.circuits.to_circuits(["Gxpi2:1^2", "Gypi2:1^2"], line_labels=(1,)) - circuits_on01 = pygsti.circuits.to_circuits(["Gcnot:0:1", "Gxpi2:0Gypi2:1^2Gcnot:0:1Gxpi:0"], - line_labels=(0,1)) - - #For GST edesigns - mdl = std.target_model() - gst_pspec = mdl.create_processor_spec() - - #For RB edesigns - pspec = pygsti.processors.QubitProcessorSpec(2, ["Gxpi2", "Gypi2","Gxx"], - geometry='line', qubit_labels=(0,1)) - compilations = {"absolute": CCR.create_standard(pspec, "absolute", ("paulis", "1Qcliffords"), verbosity=0), - "paulieq": CCR.create_standard(pspec, "paulieq", ("1Qcliffords", "allcnots"), verbosity=0), - } - - pspec1Q = pygsti.processors.QubitProcessorSpec(1, ["Gxpi2", "Gypi2","Gxmpi2", "Gympi2"], - geometry='line', qubit_labels=(0,)) - compilations1Q = {"absolute": CCR.create_standard(pspec1Q, "absolute", ("paulis", "1Qcliffords"), verbosity=0), - "paulieq": CCR.create_standard(pspec1Q, "paulieq", ("1Qcliffords", "allcnots"), verbosity=0), - } - - - edesigns = [] - edesigns.append(ExperimentDesign(circuits_on0)) - edesigns.append(CircuitListsDesign([circuits_on0, circuits_on0b])) - edesigns.append(CombinedExperimentDesign({'one': ExperimentDesign(circuits_on0), - 'two': ExperimentDesign(circuits_on1), - 'three': ExperimentDesign(circuits_on01)}, qubit_labels=(0,1))) - edesigns.append(SimultaneousExperimentDesign([ExperimentDesign(circuits_on0), ExperimentDesign(circuits_on1)])) - edesigns.append(FreeformDesign(circuits_on01)) - edesigns.append(std.create_gst_experiment_design(2)) - edesigns.append(GateSetTomographyDesign(gst_pspec, [circuits_on0, circuits_on0b])) - edesigns.append(CliffordRBDesign(pspec, compilations, depths=[0,2,5], circuits_per_depth=4)) - edesigns.append(DirectRBDesign(pspec, compilations, depths=[0,2,5], circuits_per_depth=4)) - edesigns.append(MirrorRBDesign(pspec1Q, depths=[0,2,4], circuits_per_depth=4, - clifford_compilations=compilations1Q)) - - for edesign in edesigns: + for edesign in self.edesigns: print("Testing edesign of type: ", str(type(edesign))) orig_qubits = edesign.qubit_labels for c in edesign.all_circuits_needing_data: @@ -150,3 +150,28 @@ def test_map_edesign_sslbls(self): self.assertEqual(mapped_edesign.qubit_labels, mapped_qubits) for c in mapped_edesign.all_circuits_needing_data: self.assertTrue(set(c.line_labels).issubset(mapped_qubits)) + + def test_truncation(self): + from pygsti.protocols import BenchmarkingDesign + + for edesign in self.edesigns: + print("Testing edesign of type: ", str(type(edesign))) + + truncated_circuits = edesign.all_circuits_needing_data[:2] + truncated_edesign = edesign.truncate_to_circuits(truncated_circuits) + self.assertTrue(set(truncated_circuits) == set(truncated_edesign.all_circuits_needing_data)) + + if isinstance(edesign, BenchmarkingDesign): + # Check that the paired attributes were also truncated properly + # These will be lists of lists + for attr in edesign.paired_with_circuit_attrs: + attr_lists = getattr(edesign, attr) + truncated_attr_lists = getattr(truncated_edesign, attr) + for a_list, ta_list, c_list, tc_list in zip(attr_lists, truncated_attr_lists, + edesign.circuit_lists, truncated_edesign.circuit_lists): + self.assertTrue(len(ta_list) == len(tc_list)) + + # Ensure that the paired attribute data is correct + for ta_data, tc_circ in zip(ta_list, tc_list): + untruncated_idx = c_list.index(tc_circ) + self.assertTrue(a_list[untruncated_idx] == ta_data) diff --git a/test/unit/protocols/test_rb.py b/test/unit/protocols/test_rb.py index 7cf601b7c..bb9422603 100644 --- a/test/unit/protocols/test_rb.py +++ b/test/unit/protocols/test_rb.py @@ -1,5 +1,7 @@ from ..util import BaseCase +import numpy as _np + import pygsti from pygsti.protocols import rb as _rb from pygsti.processors import CliffordCompilationRules as CCR @@ -11,7 +13,7 @@ def setUp(self): self.num_qubits = 2 self.qubit_labels = ['Q'+str(i) for i in range(self.num_qubits)] - gate_names = ['Gxpi2', 'Gxmpi2', 'Gypi2', 'Gympi2', 'Gcphase'] + gate_names = ['Gi', 'Gxpi2', 'Gxmpi2', 'Gypi2', 'Gympi2', 'Gcphase'] availability = {'Gcphase':[('Q'+str(i),'Q'+str((i+1) % self.num_qubits)) for i in range(self.num_qubits)]} self.pspec = pygsti.processors.QubitProcessorSpec(self.num_qubits, gate_names, availability=availability, @@ -21,7 +23,16 @@ def setUp(self): 'paulieq': CCR.create_standard(self.pspec, 'paulieq', ('1Qcliffords', 'allcnots'), verbosity=0) } + gate_names_1Q = gate_names[:-1] + self.qubit_labels1Q = ['Q0'] + self.pspec1Q = pygsti.processors.QubitProcessorSpec(1, gate_names_1Q, qubit_labels=self.qubit_labels1Q) + self.compilations1Q = { + 'absolute': CCR.create_standard(self.pspec1Q, 'absolute', ('paulis', '1Qcliffords'), verbosity=0), + 'paulieq': CCR.create_standard(self.pspec1Q, 'paulieq', ('1Qcliffords', 'allcnots'), verbosity=0) + } + # TODO: Test a lot of these, currently just the default from the tutorial + # Probably as pytest mark parameterize for randomizeout, compilerargs? self.depths = [0, 2]#, 4, 8] self.circuits_per_depth = 5 self.qubits = ['Q0', 'Q1'] @@ -61,7 +72,37 @@ def test_design_construction(self): [[self.assertAlmostEqual(c.simulate(tmodel)[bs],1.) for c, bs in zip(cl, bsl)] for cl, bsl in zip(mp_design.circuit_lists, mp_design.idealout_lists)] + def test_deterministic_compilation(self): + # TODO: Figure out good test for this. Full circuit is a synthetic idle, we need to somehow check the non-inverted + # Clifford is the same as the random case? + abs_design = _rb.CliffordRBDesign( + self.pspec1Q, self.compilations1Q, self.depths, self.circuits_per_depth, qubit_labels=self.qubit_labels1Q, + randomizeout=self.randomizeout, interleaved_circuit=self.interleaved_circuit, + citerations=self.citerations, compilerargs=self.compiler_args, seed=self.seed, + verbosity=self.verbosity, exact_compilation_key='absolute') + + peq_design = _rb.CliffordRBDesign( + self.pspec1Q, self.compilations1Q, self.depths, self.circuits_per_depth, qubit_labels=self.qubit_labels1Q, + randomizeout=self.randomizeout, interleaved_circuit=self.interleaved_circuit, + citerations=self.citerations, compilerargs=self.compiler_args, seed=self.seed, + verbosity=self.verbosity, exact_compilation_key='paulieq') + + # Testing a non-standard (but unrealistic) compilation + rule_dict = {f'C{i}': (_np.eye(2), pygsti.circuits.Circuit([], (0,))) for i in range(24)} + compilations = self.compilations1Q.copy() + compilations["idle"] = pygsti.processors.CompilationRules(rule_dict) + idle_design = _rb.CliffordRBDesign( + self.pspec1Q, compilations, self.depths, self.circuits_per_depth, qubit_labels=self.qubit_labels1Q, + randomizeout=False, interleaved_circuit=self.interleaved_circuit, + citerations=self.citerations, compilerargs=self.compiler_args, seed=self.seed, + verbosity=self.verbosity, exact_compilation_key='idle') + + # All circuits should be the empty circuit (since we've turned off randomizeout) + for clist in idle_design.circuit_lists: + self.assertTrue(set(clist) == set([pygsti.circuits.Circuit([], self.qubit_labels1Q)])) + # Also a handy place to test native gate counts since it should be 0 + self.assertTrue(idle_design.average_native_gates_per_clifford() == 0) class TestDirectRBDesign(BaseCase): From e18457e28cf0d25d34d818b582b9201b5705e0b7 Mon Sep 17 00:00:00 2001 From: "Stefan K. Seritan" Date: Thu, 30 May 2024 13:02:39 -0600 Subject: [PATCH 6/9] PR updates and bugfixes --- pygsti/algorithms/randomcircuit.py | 176 +++++++++++++++++++++++------ pygsti/protocols/rb.py | 81 ++++++++----- test/unit/protocols/test_rb.py | 4 +- 3 files changed, 195 insertions(+), 66 deletions(-) diff --git a/pygsti/algorithms/randomcircuit.py b/pygsti/algorithms/randomcircuit.py index f0ce28884..ee7140e01 100644 --- a/pygsti/algorithms/randomcircuit.py +++ b/pygsti/algorithms/randomcircuit.py @@ -2137,10 +2137,129 @@ def create_direct_rb_circuit(pspec, clifford_compilations, length, qubit_labels= # return experiment_dict +def _sample_clifford_circuit(pspec, clifford_compilations, qubit_labels, citerations, + compilerargs, exact_compilation_key, srep_cache, rand_state): + """Helper function to compile a random Clifford circuit. + + Parameters + ---------- + pspec : QubitProcessorSpec + The QubitProcessorSpec for the device that the circuit is being sampled for, which defines the + "native" gate-set and the connectivity of the device. The returned CRB circuit will be over + the gates in `pspec`, and will respect the connectivity encoded by `pspec`. + + clifford_compilations : dict + A dictionary with at least the potential keys `'absolute'` and `'paulieq'` and corresponding + :class:`CompilationRules` values. These compilation rules specify how to compile the + "native" gates of `pspec` into Clifford gates. Additional :class:`CompilationRules` can be + provided, particularly for use with `exact_compilation_key`. + + qubit_labels : list + A list of the qubits that the RB circuit is to be sampled for. + + citerations : int + Some of the Clifford compilation algorithms in pyGSTi (including the default algorithm) are + randomized, and the lowest-cost circuit is chosen from all the circuit generated in the + iterations of the algorithm. This is the number of iterations used. The time required to + generate a CRB circuit is linear in `citerations` * (`length` + 2). Lower-depth / lower 2-qubit + gate count compilations of the Cliffords are important in order to successfully implement + CRB on more qubits. + + compilerargs : list + A list of arguments that are handed to compile_clifford() function, which includes all the + optional arguments of compile_clifford() *after* the `iterations` option (set by `citerations`). + In order, this list should be values for: + + algorithm : str. A string that specifies the compilation algorithm. The default in + compile_clifford() will always be whatever we consider to be the 'best' all-round + algorithm + + aargs : list. A list of optional arguments for the particular compilation algorithm. + + costfunction : 'str' or function. The cost-function from which the "best" compilation + for a Clifford is chosen from all `citerations` compilations. The default costs a + circuit as 10x the num. of 2-qubit gates in the circuit + 1x the depth of the circuit. + + prefixpaulis : bool. Whether to prefix or append the Paulis on each Clifford. + + paulirandomize : bool. Whether to follow each layer in the Clifford circuit with a + random Pauli on each qubit (compiled into native gates). I.e., if this is True the + native gates are Pauli-randomized. When True, this prevents any coherent errors adding + (on average) inside the layers of each compiled Clifford, at the cost of increased + circuit depth. Defaults to False. + + For more information on these options, see the `:func:compile_clifford()` docstring. + + exact_compilation_key: str, optional + The key into `clifford_compilations` to use for exact deterministic complation of Cliffords. + The underlying :class:`CompilationRules` object must provide compilations for all possible + n-qubit Cliffords that will be generated. This also requires the pspec is able to generate the + symplectic representations for all n-qubit Cliffords in :meth:`compute_clifford_symplectic_reps`. + This is currently generally intended for use out-of-the-box with 1-qubit Clifford RB; + however, larger number of qubits can be used so long as the user specifies the processor spec and + compilation rules properly. + + srep_cache: dict + Keys are gate labels and values are precomputed symplectic representations. + + rand_state: np.random.RandomState + A RandomState to use for RNG + + Returns + ------- + clifford_circuit : Circuit + The compiled Clifford circuit + + s: + The symplectic matrix of the Clifford + + p: + The symplectic phase vector of the Clifford + """ + # Find the labels of the qubits to create the circuit for. + if qubit_labels is not None: qubits = qubit_labels[:] # copy this list + else: qubits = pspec.qubit_labels[:] # copy this list + # The number of qubits the circuit is over. + n = len(qubits) + + if exact_compilation_key is not None: + # Deterministic compilation based on a provided clifford compilation + assert exact_compilation_key in clifford_compilations, \ + f"{exact_compilation_key} not provided in `clifford_compilations`" + + # Pick clifford + cidx = rand_state.randint(_symp.compute_num_cliffords(n)) + lbl = _lbl.Label(f'C{cidx}', qubits) + + # Try to do deterministic compilation + try: + circuit = clifford_compilations[exact_compilation_key].retrieve_compilation_of(lbl) + except AssertionError: + raise ValueError( + f"Failed to compile n-qubit Clifford 'C{cidx}'. Ensure this is provided in the " + \ + "compilation rules, or use a compilation algorithm to synthesize it by not " + \ + "specifying `exact_compilation_key`." + ) + + # compute the symplectic rep of the chosen clifford + # TODO: Note that this is inefficient. For speed, we could implement the pair to + # _symp.compute_symplectic_matrix and just calculate s and p directly + s, p = _symp.symplectic_rep_of_clifford_circuit(circuit, srep_cache) + else: + # Random compilation + s, p = _symp.random_clifford(n, rand_state=rand_state) + circuit = _cmpl.compile_clifford(s, p, pspec, + clifford_compilations.get('absolute', None), + clifford_compilations.get('paulieq', None), + qubit_labels=qubit_labels, iterations=citerations, *compilerargs, + rand_state=rand_state) + + return circuit, s, p + def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_labels=None, randomizeout=False, citerations=20, compilerargs=None, interleaved_circuit=None, seed=None, - return_num_native_gates=False, exact_compilation_key=None): + return_native_gate_counts=False, exact_compilation_key=None): """ Generates a "Clifford randomized benchmarking" (CRB) circuit. @@ -2226,7 +2345,7 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label A seed to initialize the random number generator used for creating random clifford circuits. - return_num_native_gates: bool, optional + return_native_gate_counts: bool, optional Whether to return the number of native gates in the first `length`+1 compiled Cliffords exact_compilation_key: str, optional @@ -2251,9 +2370,9 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label In both cases, the ith element of the tuple corresponds to the error-free outcome for the qubit on the ith wire of the output circuit. - num_native_gates: int - Total number of native gates in the first `length`+1 compiled Cliffords. - Only returned when `return_num_native_gates` is True + native_gate_counts: dict + Total number of native gates, native 2q gates, and native circuit size in the + first `length`+1 compiled Cliffords. Only returned when `return_num_native_gates` is True """ if compilerargs is None: compilerargs = [] @@ -2263,6 +2382,7 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label # The number of qubits the circuit is over. n = len(qubits) + srep_cache = {} if exact_compilation_key is not None: # Precompute some of the symplectic reps if we are doing exact compilation srep_cache = _symp.compute_internal_gate_symplectic_representations() @@ -2279,37 +2399,15 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label # Sample length+1 uniformly random Cliffords (we want a circuit of length+2 Cliffords, in total), compile # them, and append them to the current circuit. num_native_gates = 0 + num_native_2q_gates = 0 + native_size = 0 for _ in range(0, length + 1): - if exact_compilation_key is not None: - # Deterministic compilation based on a provided clifford compilation - assert exact_compilation_key in clifford_compilations, \ - f"{exact_compilation_key} not provided in `clifford_compilations`" - - # Pick clifford - cidx = rand_state.randint(24**n) - lbl = _lbl.Label(f'C{cidx}', qubits) - - # Try to do deterministic compilation - try: - circuit = clifford_compilations[exact_compilation_key].retrieve_compilation_of(lbl) - except AssertionError: - raise ValueError( - f"Failed to compile n-qubit Clifford 'C{cidx}'. Ensure this is provided in the " + \ - "compilation rules, or use a compilation algorithm to synthesize it by not " + \ - "specifying `exact_compilation_key`." - ) - - # compute the symplectic rep of the chosen clifford - s, p = _symp.symplectic_rep_of_clifford_circuit(circuit, srep_cache) - else: - # Random compilation - s, p = _symp.random_clifford(n, rand_state=rand_state) - circuit = _cmpl.compile_clifford(s, p, pspec, - clifford_compilations.get('absolute', None), - clifford_compilations.get('paulieq', None), - qubit_labels=qubit_labels, iterations=citerations, *compilerargs, - rand_state=rand_state) + # Perform sampling + circuit, s, p = _sample_clifford_circuit(pspec, clifford_compilations, qubit_labels, citerations, + compilerargs, exact_compilation_key, srep_cache, rand_state) num_native_gates += circuit.num_gates + num_native_2q_gates += circuit.num_nq_gates(2) + native_size += circuit.size # Keeps track of the current composite Clifford s_composite, p_composite = _symp.compose_cliffords(s_composite, p_composite, s, p) @@ -2355,8 +2453,14 @@ def create_clifford_rb_circuit(pspec, clifford_compilations, length, qubit_label full_circuit.done_editing() - if return_num_native_gates: - return full_circuit, idealout, num_native_gates + native_gate_counts = { + "native_gate_count": num_native_gates, + "native_2q_gate_count": num_native_2q_gates, + "native_size": native_size + } + + if return_native_gate_counts: + return full_circuit, idealout, native_gate_counts return full_circuit, idealout diff --git a/pygsti/protocols/rb.py b/pygsti/protocols/rb.py index f3d09d472..ae5a98e1e 100644 --- a/pygsti/protocols/rb.py +++ b/pygsti/protocols/rb.py @@ -10,6 +10,7 @@ # http://www.apache.org/licenses/LICENSE-2.0 or in the LICENSE file in the root pyGSTi directory. #*************************************************************************************************** +from collections import defaultdict import numpy as _np from pygsti.protocols import protocol as _proto @@ -189,14 +190,14 @@ def from_existing_circuits(cls, data_by_depth, qubit_labels=None, circuit_lists = [[x[0] for x in data_by_depth[d]] for d in depths] ideal_outs = [[x[1] for x in data_by_depth[d]] for d in depths] try: - num_native_gates = [[x[2] for x in data_by_depth[d]] for d in depths] + native_gate_counts = [[x[2] for x in data_by_depth[d]] for d in depths] except IndexError: - num_native_gates = None + native_gate_counts = None circuits_per_depth = [len(data_by_depth[d]) for d in depths] self = cls.__new__(cls) self._init_foundation(depths, circuit_lists, ideal_outs, circuits_per_depth, qubit_labels, randomizeout, citerations, compilerargs, descriptor, add_default_protocol, - interleaved_circuit, num_native_gates=num_native_gates) + interleaved_circuit, native_gate_counts=native_gate_counts) return self def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qubit_labels=None, randomizeout=False, @@ -206,7 +207,7 @@ def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qub assert len(qubit_labels) == len(pspec.qubit_labels), "Must provide qubit labels that match number of qubits in pspec" circuit_lists = [] ideal_outs = [] - num_native_gates = [] + native_gate_counts = [] if seed is None: self.seed = _np.random.randint(1, 1e6) # Pick a random seed @@ -222,34 +223,34 @@ def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qub args_list = [(pspec, clifford_compilations, l)] * circuits_per_depth kwargs_list = [dict(qubit_labels=qubit_labels, randomizeout=randomizeout, citerations=citerations, compilerargs=compilerargs, interleaved_circuit=interleaved_circuit, - seed=lseed + i, return_num_native_gates=True, exact_compilation_key=exact_compilation_key) + seed=lseed + i, return_native_gate_counts=True, exact_compilation_key=exact_compilation_key) for i in range(circuits_per_depth)] results = _tools.mptools.starmap_with_kwargs(_rc.create_clifford_rb_circuit, circuits_per_depth, num_processes, args_list, kwargs_list) circuits_at_depth = [] idealouts_at_depth = [] - num_native_gates_at_depth = [] + native_gate_counts_at_depth = [] for c, iout, nng in results: circuits_at_depth.append(c) idealouts_at_depth.append((''.join(map(str, iout)),)) - num_native_gates_at_depth.append(nng) + native_gate_counts_at_depth.append(nng) circuit_lists.append(circuits_at_depth) ideal_outs.append(idealouts_at_depth) - num_native_gates.append(num_native_gates_at_depth) + native_gate_counts.append(native_gate_counts_at_depth) self._init_foundation(depths, circuit_lists, ideal_outs, circuits_per_depth, qubit_labels, randomizeout, citerations, compilerargs, descriptor, add_default_protocol, - interleaved_circuit, num_native_gates=num_native_gates) + interleaved_circuit, native_gate_counts=native_gate_counts) def _init_foundation(self, depths, circuit_lists, ideal_outs, circuits_per_depth, qubit_labels, randomizeout, citerations, compilerargs, descriptor, add_default_protocol, - interleaved_circuit, num_native_gates=None, exact_compilation_key=None): - self.num_native_gate_lists = num_native_gates - if self.num_native_gate_lists is not None: + interleaved_circuit, native_gate_counts=None, exact_compilation_key=None): + self.native_gate_count_lists = native_gate_counts + if self.native_gate_count_lists is not None: # If we have native gate information, pair this with circuit data so that we serialize/truncate properly - self.paired_with_circuit_attrs = ["num_native_gate_lists"] + self.paired_with_circuit_attrs = ["native_gate_count_lists"] super().__init__(depths, circuit_lists, ideal_outs, qubit_labels, remove_duplicates=False) self.circuits_per_depth = circuits_per_depth @@ -279,14 +280,19 @@ def average_native_gates_per_clifford_for_circuit(self, list_idx, circ_idx): Returns ------- - float - The average number of native gates per Clifford + avg_gate_counts: dict + The average number of native gates, native 2Q gates, and native size + per Clifford as values with respective label keys """ - if self.num_native_gate_lists is None: - raise ValueError("Number of native gates not available, cannot compute average gates per Clifford") - num_native_gates = self.num_native_gate_lists[list_idx][circ_idx] + if self.native_gate_counts_lists is None: + raise ValueError("Native gate counts not available, cannot compute average gates per Clifford") + num_clifford_gates = self.depths[list_idx] + 1 - return num_native_gates / num_clifford_gates + avg_gate_counts = {} + for key, native_gate_count in self.native_gate_count_lists[list_idx][circ_idx].items(): + avg_gate_counts[key.replace('native', 'avg_native_per_clifford')] = native_gate_count / num_clifford_gates + + return avg_gate_counts def average_native_gates_per_clifford_for_circuit_list(self, list_idx): """The average number of gates per Clifford for a circuit list @@ -307,11 +313,20 @@ def average_native_gates_per_clifford_for_circuit_list(self, list_idx): float The average number of native gates per Clifford """ - if self.num_native_gate_lists is None: - raise ValueError("Number of native gates not available, cannot compute average gates per Clifford") - num_native_gates = sum(self.num_native_gate_lists[list_idx]) - num_clifford_gates = len(self.num_native_gate_lists[list_idx]) * (self.depths[list_idx] + 1) - return num_native_gates / num_clifford_gates + if self.native_gate_count_lists is None: + raise ValueError("Native gate counts not available, cannot compute average gates per Clifford") + + gate_counts = defaultdict(int) + for native_gate_counts in self.native_gate_count_lists[list_idx]: + for k, v in native_gate_counts.items(): + gate_counts[k] += v + + num_clifford_gates = len(self.native_gate_count_lists[list_idx]) * (self.depths[list_idx] + 1) + avg_gate_counts = {} + for key, total_native_gate_counts in gate_counts.items(): + avg_gate_counts[key.replace('native', 'avg_native_per_clifford')] = total_native_gate_counts / num_clifford_gates + + return avg_gate_counts def average_native_gates_per_clifford(self): """The average number of native gates per Clifford for all circuits @@ -321,14 +336,22 @@ def average_native_gates_per_clifford(self): float The average number of native gates per Clifford """ - if self.num_native_gate_lists is None: + if self.native_gate_count_lists is None: raise ValueError("Number of native gates not available, cannot compute average gates per Clifford") - num_native_gates = 0 + + gate_counts = defaultdict(int) num_clifford_gates = 0 for list_idx in range(len(self.depths)): - num_native_gates = sum(self.num_native_gate_lists[list_idx]) - num_clifford_gates = len(self.num_native_gate_lists[list_idx]) * (self.depths[list_idx] + 1) - return num_native_gates / num_clifford_gates + for native_gate_counts in self.native_gate_count_lists[list_idx]: + for k, v in native_gate_counts.items(): + gate_counts[k] += v + num_clifford_gates += len(self.native_gate_count_lists[list_idx]) * (self.depths[list_idx] + 1) + + avg_gate_counts = {} + for key, total_native_gate_counts in gate_counts.items(): + avg_gate_counts[key.replace('native', 'avg_native_per_clifford')] = total_native_gate_counts / num_clifford_gates + + return avg_gate_counts def map_qubit_labels(self, mapper): """ diff --git a/test/unit/protocols/test_rb.py b/test/unit/protocols/test_rb.py index bb9422603..19259e5a2 100644 --- a/test/unit/protocols/test_rb.py +++ b/test/unit/protocols/test_rb.py @@ -102,7 +102,9 @@ def test_deterministic_compilation(self): self.assertTrue(set(clist) == set([pygsti.circuits.Circuit([], self.qubit_labels1Q)])) # Also a handy place to test native gate counts since it should be 0 - self.assertTrue(idle_design.average_native_gates_per_clifford() == 0) + avg_gate_counts = idle_design.average_native_gates_per_clifford() + for v in avg_gate_counts.values(): + self.assertTrue(v == 0) class TestDirectRBDesign(BaseCase): From 596c9e43a2b5086944ca6d104353064f12d2dd5d Mon Sep 17 00:00:00 2001 From: "Stefan K. Seritan" Date: Mon, 3 Jun 2024 10:17:22 -0600 Subject: [PATCH 7/9] Fix bug where repeat circuits were unexpectedly removed in truncate. Previous logic would remove duplicate circuits if `circuits_to_keep` was not a set. New logic never removes repeats. --- pygsti/circuits/circuitlist.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pygsti/circuits/circuitlist.py b/pygsti/circuits/circuitlist.py index 3c9345269..666802382 100644 --- a/pygsti/circuits/circuitlist.py +++ b/pygsti/circuits/circuitlist.py @@ -158,11 +158,7 @@ def truncate(self, circuits_to_keep): ------- CircuitList """ - if isinstance(circuits_to_keep, set): - new_circuits = list(filter(lambda c: c in circuits_to_keep, self._circuits)) - else: - current_circuits = set(self._circuits) - new_circuits = list(filter(lambda c: c in current_circuits, circuits_to_keep)) + new_circuits = list(filter(lambda c: c in set(circuits_to_keep), self._circuits)) return CircuitList(new_circuits, self.op_label_aliases) # don't transfer weights or name def truncate_to_dataset(self, dataset): From 90cadf48e32bd8696f77ea1c01aff0c5e264c1c5 Mon Sep 17 00:00:00 2001 From: "Stefan K. Seritan" Date: Mon, 3 Jun 2024 10:19:40 -0600 Subject: [PATCH 8/9] Updated changelog for 0.9.12.3 --- CHANGELOG | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index af238b94b..e482e892f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,17 @@ # CHANGELOG +## [0.9.12.3] - 2024-06-03 + +### Added +* Deterministic Clifford compilation and native gate count statistics for `CliffordRBDesign` (#314, #315, #443) + + +### Fixed +* Truncation bugfix in `BenchmarkingDesign` objects with "paired" lists to `circuit_list` attribute (#408, #443) +* Fixes and efficiency improvements for various linear algebra calls (#432) +* `densitymx_slow` evotype hotfix (#438, #439) + + ## [0.9.12.2] - 2024-04-16 ### Added From 723cd24aec3b90d28b0fcd9b31145b920c256acf Mon Sep 17 00:00:00 2001 From: "Stefan K. Seritan" Date: Mon, 3 Jun 2024 12:33:42 -0700 Subject: [PATCH 9/9] Bugfix for RB tutorials. Reverts an added check that (unintentionally) prevented generating RB designs on subsets of pspecs. --- pygsti/protocols/rb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pygsti/protocols/rb.py b/pygsti/protocols/rb.py index ae5a98e1e..9929752fe 100644 --- a/pygsti/protocols/rb.py +++ b/pygsti/protocols/rb.py @@ -204,7 +204,6 @@ def __init__(self, pspec, clifford_compilations, depths, circuits_per_depth, qub interleaved_circuit=None, citerations=20, compilerargs=(), exact_compilation_key=None, descriptor='A Clifford RB experiment', add_default_protocol=False, seed=None, verbosity=1, num_processes=1): if qubit_labels is None: qubit_labels = tuple(pspec.qubit_labels) - assert len(qubit_labels) == len(pspec.qubit_labels), "Must provide qubit labels that match number of qubits in pspec" circuit_lists = [] ideal_outs = [] native_gate_counts = []