Skip to content

qubic.state_disc

Tools for classifying demodulated and integrated IQ data (i.e. one complex value per shot).

TODO: maybe add an abstract state disc class/interface.

GMMManager

Class for managing multi-qubit GMM classifiers.

Attributes:

Name Type Description
chan_to_qubit dict

map from hardware channel (usually core_ind) to qubitid

gmm_dict dict

dictionary of GMMStateDiscriminator objects. keys are qubitid

Source code in qubic/state_disc.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
class GMMManager:
    """
    Class for managing multi-qubit GMM classifiers. 

    Attributes
    ----------
    chan_to_qubit: dict
        map from hardware channel (usually core_ind) to qubitid
    gmm_dict: dict
        dictionary of GMMStateDiscriminator objects. keys are qubitid

    """

    def __new__(cls,
                load_file: str = None, 
                gmm_dict: Dict[str, GMMStateDiscriminator] = None, 
                chanmap_or_chan_cfgs: Dict[int, str] | Dict[str, ChannelConfig] = None,
                load_json: str = None,
                n_states: int = 2):
        """
        Must specify either load_file, or chanmap_or_chan_cfgs. If load_file is NOT
        specified, can specify gmm_dict to load in existing set of GMM models.

        Parameters
        ----------
        load_file: str
            If provided, loads GMM manager object from pkl filename
        gmm_dict: dict
            Existing GMM dictionary, indexed by qubit. Loads this into
            the object
        chanmap_or_chan_cfgs: dict
            dict of ChannelConfig objects, or dictionary mapping 
            channels to qubits. 
        load_json: str
            If provided, loads GMM manager object from json filename
        n_states: int
            Number of states to classify

        """
        if load_file is not None:
            assert gmm_dict is None
            with open(load_file, 'rb') as f:
                inst = pkl.load(f)
            if chanmap_or_chan_cfgs is not None:
                inst._resolve_chanmap(chanmap_or_chan_cfgs)
            return inst
        else:
            return super(GMMManager, cls).__new__(cls)

    def __init__(self, 
                 load_file: str = None, 
                 gmm_dict: Dict[str, GMMStateDiscriminator] = None, 
                 chanmap_or_chan_cfgs: Dict[int, str] | Dict[str, ChannelConfig] = None,
                 load_json: str = None,
                 n_states: int = 2):
        """
        Must specify either load_file, or chanmap_or_chan_cfgs. If load_file is NOT
        specified, can specify gmm_dict to load in existing set of GMM models.

        Parameters
        ----------
        load_file: str
            If provided, loads GMM manager object from pkl filename
        gmm_dict: dict
            Existing GMM dictionary, indexed by qubit. Loads this into
            the object
        chanmap_or_chan_cfgs: dict
            dict of ChannelConfig objects, or dictionary mapping 
            channels to qubits. 
        load_json: str
            If provided, loads GMM manager object from json filename
        n_states: int
            Number of states to classify
        """
        self.n_states=n_states
        if gmm_dict is not None:
            assert isinstance(gmm_dict, dict)
            assert load_file is None
            self.gmm_dict = gmm_dict
            assert chanmap_or_chan_cfgs is not None
            self._resolve_chanmap(chanmap_or_chan_cfgs)
        elif load_file is not None: #object was loaded from pkl
            assert gmm_dict is None
            if chanmap_or_chan_cfgs is not None:
                self._resolve_chanmap(chanmap_or_chan_cfgs)
        elif load_json is not None:
            self.gmm_dict = {}
            with open(load_json) as jfile:
                jdict=json.load(jfile)
                for k,v in jdict.items():
                    self.gmm_dict[k]=GMMStateDiscriminator(load_dict=v)
        else:
            self.gmm_dict = {}
            assert chanmap_or_chan_cfgs is not None
            self._resolve_chanmap(chanmap_or_chan_cfgs)

    def update(self, gmm_manager):
        assert isinstance(gmm_manager, GMMManager)
        self.gmm_dict.update(gmm_manager.gmm_dict)

    def _resolve_chanmap(self, chanmap_or_chan_cfgs):
        if isinstance(list(chanmap_or_chan_cfgs.values())[1], str):
            # this is a channel to qubit map
            self.chan_to_qubit = chanmap_or_chan_cfgs
        else:
            # this is a chan cfg dict
            self.chan_to_qubit = {str(chanmap_or_chan_cfgs[dest].core_ind): dest.split('.')[0] 
                                  for dest, channel in chanmap_or_chan_cfgs.items() 
                                  if isinstance(channel, ChannelConfig) and dest.split('.')[1] == 'rdlo'}

    def fit(self, iq_shot_dict: Dict[str, np.ndarray]):
        """
        Fit GMM models based on input data in iq_shot_dict. If model doesn't exist, create it,
        if so, update existing model with new data.

        Parameters
        ----------
        iq_shot_dict: Dict[str, np.ndarray]
            dictionary of IQ data, keyed by str(channel_number), or qubit

        """
        for chan, iq_shots in iq_shot_dict.items():
            if self._get_gmm_key(chan) in self.gmm_dict.keys():
                self.gmm_dict[self._get_gmm_key(chan)].fit(iq_shots)
            else:
                self.gmm_dict[self._get_gmm_key(chan)] = GMMStateDiscriminator(fit_iqdata=iq_shots, n_states=self.n_states)

    def _get_gmm_key(self, chan: str) -> str:
        """
        Checks if `chan` is a qubit or numbered channel (as a string). If qubit,
        returns `chan`, else converts to corresponding qubit.
        """
        if chan[0].lower() == 'q':
            return chan
        else:
            return self.chan_to_qubit[chan]

    def get_threshold_angle(self, qubit: str, label0: int| str = 0, label1: int | str = 1) -> float:
        """
        Get the threshold angle for a particular qubit; wrapper around 
        GMMStateDiscriminator.get_threshold_angle

        Parameters
        ----------
        qubit: str
        label0: int | str
        label1: int | str

        Returns
        -------
        float:
            angle in radians

        """
        return self.gmm_dict[qubit].get_threshold_angle(label0, label1)

    def predict(self, iq_shot_dict: dict, output_keys: str = 'qubit') -> Dict[str, np.ndarray]:
        """
        Assign labels to IQ shots.

        Parameters
        ----------
        iq_shot_dict : dict
            keys: channel no. or qubitid
            values: complex array of shots to predict
        output_keys : str
            either 'qubit' or 'channel'

        Returns
        -------
        Dict[str, np.ndarray]
            Dictionary containing arrays of labeled data, corresponding to self.labels; same
            shape as iqdata; keyed by qubit or channel, depending on `output_keys`
        """
        result_dict = {}
        for chan, iq_shots in iq_shot_dict.items():
            if chan[0].lower() == 'q':
                result = self.gmm_dict[chan].predict(iq_shots)
                if output_keys == 'qubit':
                    result_dict[chan] = result
                elif output_keys == 'channel':
                    raise NotImplementedError
                else:
                    raise ValueError('output_keys must be qubit or channel')

            else:
                result = self.gmm_dict[self.chan_to_qubit[chan]].predict(iq_shots)
                if output_keys == 'qubit':
                    result_dict[self.chan_to_qubit[chan]] = result
                elif output_keys == 'channel':
                    result_dict[chan] = result
                else:
                    raise ValueError('output_keys must be qubit or channel')

        return result_dict

    def set_labels_maxtomin(self, iq_shot_dict: Dict[str, np.ndarray], labels_maxtomin: list):
        """
        Batched version of GMMStateDiscriminator.set_labels_maxtomin

        Parameters
        ----------
        iq_shot_data : dict
            Set of complex IQ values
        labels_maxtomin : list
            Labels to assign in descending order of prevelance
        """
        for chan, iq_shots in iq_shot_dict.items():
            self.gmm_dict[self._get_gmm_key(chan)].set_labels_maxtomin(iq_shots, labels_maxtomin)

    def save(self, filename: str):
        with open(filename, 'wb') as f:
            pkl.dump(self, f)

    def savejson(self, filename: str,update: bool = True, indent: int = 4):
        """
        Serialize into dictionary and save as json.
        """
        newdict={k:v.dict_serialize() for k,v in self.gmm_dict.items()}
        if update:            
            if os.path.isfile(filename):
                with open(filename) as f:
                    serdict=json.load(f)
            else:
                serdict={}
            serdict.update(newdict) 
        else:
            serdict=newdict
        with open(filename, 'w') as f:
            json.dump(serdict,f,indent=indent)

__init__(load_file=None, gmm_dict=None, chanmap_or_chan_cfgs=None, load_json=None, n_states=2)

Must specify either load_file, or chanmap_or_chan_cfgs. If load_file is NOT specified, can specify gmm_dict to load in existing set of GMM models.

Parameters:

Name Type Description Default
load_file str

If provided, loads GMM manager object from pkl filename

None
gmm_dict Dict[str, GMMStateDiscriminator]

Existing GMM dictionary, indexed by qubit. Loads this into the object

None
chanmap_or_chan_cfgs Dict[int, str] | Dict[str, ChannelConfig]

dict of ChannelConfig objects, or dictionary mapping channels to qubits.

None
load_json str

If provided, loads GMM manager object from json filename

None
n_states int

Number of states to classify

2
Source code in qubic/state_disc.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
def __init__(self, 
             load_file: str = None, 
             gmm_dict: Dict[str, GMMStateDiscriminator] = None, 
             chanmap_or_chan_cfgs: Dict[int, str] | Dict[str, ChannelConfig] = None,
             load_json: str = None,
             n_states: int = 2):
    """
    Must specify either load_file, or chanmap_or_chan_cfgs. If load_file is NOT
    specified, can specify gmm_dict to load in existing set of GMM models.

    Parameters
    ----------
    load_file: str
        If provided, loads GMM manager object from pkl filename
    gmm_dict: dict
        Existing GMM dictionary, indexed by qubit. Loads this into
        the object
    chanmap_or_chan_cfgs: dict
        dict of ChannelConfig objects, or dictionary mapping 
        channels to qubits. 
    load_json: str
        If provided, loads GMM manager object from json filename
    n_states: int
        Number of states to classify
    """
    self.n_states=n_states
    if gmm_dict is not None:
        assert isinstance(gmm_dict, dict)
        assert load_file is None
        self.gmm_dict = gmm_dict
        assert chanmap_or_chan_cfgs is not None
        self._resolve_chanmap(chanmap_or_chan_cfgs)
    elif load_file is not None: #object was loaded from pkl
        assert gmm_dict is None
        if chanmap_or_chan_cfgs is not None:
            self._resolve_chanmap(chanmap_or_chan_cfgs)
    elif load_json is not None:
        self.gmm_dict = {}
        with open(load_json) as jfile:
            jdict=json.load(jfile)
            for k,v in jdict.items():
                self.gmm_dict[k]=GMMStateDiscriminator(load_dict=v)
    else:
        self.gmm_dict = {}
        assert chanmap_or_chan_cfgs is not None
        self._resolve_chanmap(chanmap_or_chan_cfgs)

__new__(load_file=None, gmm_dict=None, chanmap_or_chan_cfgs=None, load_json=None, n_states=2)

Must specify either load_file, or chanmap_or_chan_cfgs. If load_file is NOT specified, can specify gmm_dict to load in existing set of GMM models.

Parameters:

Name Type Description Default
load_file str

If provided, loads GMM manager object from pkl filename

None
gmm_dict Dict[str, GMMStateDiscriminator]

Existing GMM dictionary, indexed by qubit. Loads this into the object

None
chanmap_or_chan_cfgs Dict[int, str] | Dict[str, ChannelConfig]

dict of ChannelConfig objects, or dictionary mapping channels to qubits.

None
load_json str

If provided, loads GMM manager object from json filename

None
n_states int

Number of states to classify

2
Source code in qubic/state_disc.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def __new__(cls,
            load_file: str = None, 
            gmm_dict: Dict[str, GMMStateDiscriminator] = None, 
            chanmap_or_chan_cfgs: Dict[int, str] | Dict[str, ChannelConfig] = None,
            load_json: str = None,
            n_states: int = 2):
    """
    Must specify either load_file, or chanmap_or_chan_cfgs. If load_file is NOT
    specified, can specify gmm_dict to load in existing set of GMM models.

    Parameters
    ----------
    load_file: str
        If provided, loads GMM manager object from pkl filename
    gmm_dict: dict
        Existing GMM dictionary, indexed by qubit. Loads this into
        the object
    chanmap_or_chan_cfgs: dict
        dict of ChannelConfig objects, or dictionary mapping 
        channels to qubits. 
    load_json: str
        If provided, loads GMM manager object from json filename
    n_states: int
        Number of states to classify

    """
    if load_file is not None:
        assert gmm_dict is None
        with open(load_file, 'rb') as f:
            inst = pkl.load(f)
        if chanmap_or_chan_cfgs is not None:
            inst._resolve_chanmap(chanmap_or_chan_cfgs)
        return inst
    else:
        return super(GMMManager, cls).__new__(cls)

fit(iq_shot_dict)

Fit GMM models based on input data in iq_shot_dict. If model doesn't exist, create it, if so, update existing model with new data.

Parameters:

Name Type Description Default
iq_shot_dict Dict[str, ndarray]

dictionary of IQ data, keyed by str(channel_number), or qubit

required
Source code in qubic/state_disc.py
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
def fit(self, iq_shot_dict: Dict[str, np.ndarray]):
    """
    Fit GMM models based on input data in iq_shot_dict. If model doesn't exist, create it,
    if so, update existing model with new data.

    Parameters
    ----------
    iq_shot_dict: Dict[str, np.ndarray]
        dictionary of IQ data, keyed by str(channel_number), or qubit

    """
    for chan, iq_shots in iq_shot_dict.items():
        if self._get_gmm_key(chan) in self.gmm_dict.keys():
            self.gmm_dict[self._get_gmm_key(chan)].fit(iq_shots)
        else:
            self.gmm_dict[self._get_gmm_key(chan)] = GMMStateDiscriminator(fit_iqdata=iq_shots, n_states=self.n_states)

get_threshold_angle(qubit, label0=0, label1=1)

Get the threshold angle for a particular qubit; wrapper around GMMStateDiscriminator.get_threshold_angle

Parameters:

Name Type Description Default
qubit str
required
label0 int | str
0
label1 int | str
1

Returns:

Name Type Description
float float

angle in radians

Source code in qubic/state_disc.py
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
def get_threshold_angle(self, qubit: str, label0: int| str = 0, label1: int | str = 1) -> float:
    """
    Get the threshold angle for a particular qubit; wrapper around 
    GMMStateDiscriminator.get_threshold_angle

    Parameters
    ----------
    qubit: str
    label0: int | str
    label1: int | str

    Returns
    -------
    float:
        angle in radians

    """
    return self.gmm_dict[qubit].get_threshold_angle(label0, label1)

predict(iq_shot_dict, output_keys='qubit')

Assign labels to IQ shots.

Parameters:

Name Type Description Default
iq_shot_dict dict

keys: channel no. or qubitid values: complex array of shots to predict

required
output_keys str

either 'qubit' or 'channel'

'qubit'

Returns:

Type Description
Dict[str, ndarray]

Dictionary containing arrays of labeled data, corresponding to self.labels; same shape as iqdata; keyed by qubit or channel, depending on output_keys

Source code in qubic/state_disc.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
def predict(self, iq_shot_dict: dict, output_keys: str = 'qubit') -> Dict[str, np.ndarray]:
    """
    Assign labels to IQ shots.

    Parameters
    ----------
    iq_shot_dict : dict
        keys: channel no. or qubitid
        values: complex array of shots to predict
    output_keys : str
        either 'qubit' or 'channel'

    Returns
    -------
    Dict[str, np.ndarray]
        Dictionary containing arrays of labeled data, corresponding to self.labels; same
        shape as iqdata; keyed by qubit or channel, depending on `output_keys`
    """
    result_dict = {}
    for chan, iq_shots in iq_shot_dict.items():
        if chan[0].lower() == 'q':
            result = self.gmm_dict[chan].predict(iq_shots)
            if output_keys == 'qubit':
                result_dict[chan] = result
            elif output_keys == 'channel':
                raise NotImplementedError
            else:
                raise ValueError('output_keys must be qubit or channel')

        else:
            result = self.gmm_dict[self.chan_to_qubit[chan]].predict(iq_shots)
            if output_keys == 'qubit':
                result_dict[self.chan_to_qubit[chan]] = result
            elif output_keys == 'channel':
                result_dict[chan] = result
            else:
                raise ValueError('output_keys must be qubit or channel')

    return result_dict

savejson(filename, update=True, indent=4)

Serialize into dictionary and save as json.

Source code in qubic/state_disc.py
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def savejson(self, filename: str,update: bool = True, indent: int = 4):
    """
    Serialize into dictionary and save as json.
    """
    newdict={k:v.dict_serialize() for k,v in self.gmm_dict.items()}
    if update:            
        if os.path.isfile(filename):
            with open(filename) as f:
                serdict=json.load(f)
        else:
            serdict={}
        serdict.update(newdict) 
    else:
        serdict=newdict
    with open(filename, 'w') as f:
        json.dump(serdict,f,indent=indent)

set_labels_maxtomin(iq_shot_dict, labels_maxtomin)

Batched version of GMMStateDiscriminator.set_labels_maxtomin

Parameters:

Name Type Description Default
iq_shot_data dict

Set of complex IQ values

required
labels_maxtomin list

Labels to assign in descending order of prevelance

required
Source code in qubic/state_disc.py
390
391
392
393
394
395
396
397
398
399
400
401
402
def set_labels_maxtomin(self, iq_shot_dict: Dict[str, np.ndarray], labels_maxtomin: list):
    """
    Batched version of GMMStateDiscriminator.set_labels_maxtomin

    Parameters
    ----------
    iq_shot_data : dict
        Set of complex IQ values
    labels_maxtomin : list
        Labels to assign in descending order of prevelance
    """
    for chan, iq_shots in iq_shot_dict.items():
        self.gmm_dict[self._get_gmm_key(chan)].set_labels_maxtomin(iq_shots, labels_maxtomin)

GMMStateDiscriminator

Class for single-qudit state discrimination using a Gaussian-mixture model (GMM). Collections of state-discriminators (across multiple qubits) are managed using the GMMManager class

Source code in qubic/state_disc.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
class GMMStateDiscriminator:
    """
    Class for single-qudit state discrimination using a Gaussian-mixture model (GMM).
    Collections of state-discriminators (across multiple qubits) are managed using
    the GMMManager class
    """
    def __init__(self, n_states: int = 2, fit_iqdata: np.ndarray = None, load_dict: dict = None):
        self.labels = np.array(n_states*[np.nan])
        self.n_states = n_states
        self.fit_iqpoints = np.empty((0,), dtype=np.complex128)
        self.gmmfit = None
        if fit_iqdata is not None:
            self.fit(fit_iqdata)
#        self.fitdatetime=None            
        if load_dict is not None:
            self.loadfromdict(load_dict)

    def fit(self, iqdata: np.ndarray, update: bool = True):
        """
        Fit GMM model (determine blob locations and uncertainties) based
        on input iqdata.

        Parameters
        ----------
        iqdata: np.ndarray
            array of complex-valued IQ shots
        update: bool
            if True (default), then update existing model with new 
            data, else re-create model using only new data for fit
        """
        if update:
            self.fit_iqpoints = np.append(self.fit_iqpoints, iqdata)
        else:
            self.fit_iqpionts = iqdata

        self.gmmfit = mixture.GaussianMixture(self.n_states
                #                ,means_init=((-60000,-60000),(100000,100000))
                )
        nanmask = np.isnan(self.fit_iqpoints)
        if np.any(nanmask):
            logging.getLogger(__name__).warning('GMM fit data contains NaNs, ignoring')
        fit_points = self.fit_iqpoints[~nanmask]
        self.gmmfit.fit(self._format_complex_data(fit_points))
        self.fitdatetime=time.strftime('%Y%m%d_%H%M%S_%Z')

    def predict(self, iqdata: np.ndarray, use_label=True) -> np.ndarray:
        """
        Label iqdata with qubit state as determined by 

        Parameters
        ----------
        iqdata: np.ndarray
            array of complex-valued IQ shots

        Returns
        -------
        np.ndarray
            array of labeled data, corresponding to self.labels; same
            shape as iqdata
        """
        nanmask = np.isnan(iqdata)
        pred_iqdata = iqdata.copy()
        pred_iqdata[nanmask] = 0
        predictions = self.gmmfit.predict(self._format_complex_data(pred_iqdata))
        #ipdb.set_trace()
        if use_label:
            predictions = self.labels[predictions]
        else:
            predictions = predictions.astype(np.float64)
        predictions = np.reshape(predictions, iqdata.shape)
        predictions[nanmask] = np.nan
        return predictions

    def _format_complex_data(self, data: np.ndarray):
        return np.vstack((np.real(data.flatten()), np.imag(data.flatten()))).T

    def set_labels(self, labels: list[int | str] | np.ndarray):
        """
        Set all labels according to provided list
        """
        if len(labels) != self.n_states:
            raise Exception('Must have {} labels!'.format(self.n_states))
        self.labels = np.asarray(labels)

    def get_threshold_angle(self, label0: str | int = 0, label1: str | int = 1):
        """
        Get the angle (wrt to horizontal) of the midpoint between two labels in the 
        IQ plane.

        Parameters
        ----------
        label0: str | int
        label1: str | int

        Returns
        -------
        float: threshold angle in radians
        """
        blob0_coords = self.gmmfit.means_[self.labels==label0][0]
        blob1_coords = self.gmmfit.means_[self.labels==label1][0]
        threshpoint = (blob0_coords + blob1_coords)/2
        return np.arctan2(threshpoint[1], threshpoint[0])

    def switch_labels(self):
        """
        Switch 1 and 0 labels. For higher energy states, reverse the order of
        the labels array.
        """
        self.labels = self.labels[::-1]

    def set_none_label(self, label: int | str):
        """
        If any single label is None, set it to `label`
        """
        nonesum=sum(self.labels==None)
        if nonesum==1:
            index=np.where(self.labels==None)[0][0]
            self.labels[index]=label

    def set_labels_maxtomin(self, iqdata: np.ndarray, labels_maxtomin: list | np.ndarray = [0,1]):
        """
        Set labels in descending order based on number of shots in a given blob.
        e.g. if labels_maxtomin = [0,1], this function will assign label 0
        to the GMM blob with the highest population in iqdata, and 1 to the next
        highest. If any rank-ordered blob should have unchanged assignment, set 
        to None. (e.g. labels_maxtomin=[None, 1] will only assign 1 to the lowest
        population blob)

        Parameters
        ----------
        iqdata: np.ndarray
            raw complex IQ shots
        labels_maxtomin: list or np.ndarray
            order of labels to assign, in descending order
            of prevelance in iqdata
        """
        assert len(labels_maxtomin) <= self.n_states
        pred = self.predict(iqdata, use_label=False)
        n_pred = [] #number of shots at label index 0, 1, 2, etc
        for i in range(self.n_states):
            n_pred.append(np.nansum(pred == i))

        blobinds_sorted = np.argsort(n_pred)[::-1] #sort blobinds in order of max prevelance
        for i, label in enumerate(labels_maxtomin):
            if label is not None:
                self.labels[blobinds_sorted[i]] = label
                print('set label ', blobinds_sorted[i], label)

    def dict_serialize(self):
        gmmdictser={k:v.tolist() if isinstance(v,np.ndarray) else v for k,v in self.gmmfit.__dict__.items()}
        dictout=dict(labels=self.labels.tolist())
        if hasattr(self,'fitdatetime'):
            dictout.update(dict(fitdatetime=self.fitdatetime))
        dictout.update(dict(gmm=gmmdictser))
        return dictout

    def loadfromdict(self, dictin: dict):
        """
        Load GMM model (labels + means for each state) from a dictionary
        """
        gmmdictser={k:np.array(v) if isinstance(v,list) else v for k,v in dictin.items()}
        if 'labels' in dictin:
            self.labels=np.array(dictin['labels'])
        else:
            self.labels=None
        if 'fitdatetime' in dictin:
            self.fitdatetime=dictin['fitdatetime']
        else:
            self.fitdatetime=None
        if 'gmm' in dictin:
            self.gmmfit = mixture.GaussianMixture()
            for k,v in dictin['gmm'].items():
                setattr(self.gmmfit,k,np.array(v) if isinstance(v,list) else v)
            self.n_states=self.gmmfit.n_components

fit(iqdata, update=True)

Fit GMM model (determine blob locations and uncertainties) based on input iqdata.

Parameters:

Name Type Description Default
iqdata ndarray

array of complex-valued IQ shots

required
update bool

if True (default), then update existing model with new data, else re-create model using only new data for fit

True
Source code in qubic/state_disc.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def fit(self, iqdata: np.ndarray, update: bool = True):
    """
    Fit GMM model (determine blob locations and uncertainties) based
    on input iqdata.

    Parameters
    ----------
    iqdata: np.ndarray
        array of complex-valued IQ shots
    update: bool
        if True (default), then update existing model with new 
        data, else re-create model using only new data for fit
    """
    if update:
        self.fit_iqpoints = np.append(self.fit_iqpoints, iqdata)
    else:
        self.fit_iqpionts = iqdata

    self.gmmfit = mixture.GaussianMixture(self.n_states
            #                ,means_init=((-60000,-60000),(100000,100000))
            )
    nanmask = np.isnan(self.fit_iqpoints)
    if np.any(nanmask):
        logging.getLogger(__name__).warning('GMM fit data contains NaNs, ignoring')
    fit_points = self.fit_iqpoints[~nanmask]
    self.gmmfit.fit(self._format_complex_data(fit_points))
    self.fitdatetime=time.strftime('%Y%m%d_%H%M%S_%Z')

get_threshold_angle(label0=0, label1=1)

Get the angle (wrt to horizontal) of the midpoint between two labels in the IQ plane.

Parameters:

Name Type Description Default
label0 str | int
0
label1 str | int
1

Returns:

Name Type Description
float threshold angle in radians
Source code in qubic/state_disc.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def get_threshold_angle(self, label0: str | int = 0, label1: str | int = 1):
    """
    Get the angle (wrt to horizontal) of the midpoint between two labels in the 
    IQ plane.

    Parameters
    ----------
    label0: str | int
    label1: str | int

    Returns
    -------
    float: threshold angle in radians
    """
    blob0_coords = self.gmmfit.means_[self.labels==label0][0]
    blob1_coords = self.gmmfit.means_[self.labels==label1][0]
    threshpoint = (blob0_coords + blob1_coords)/2
    return np.arctan2(threshpoint[1], threshpoint[0])

loadfromdict(dictin)

Load GMM model (labels + means for each state) from a dictionary

Source code in qubic/state_disc.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def loadfromdict(self, dictin: dict):
    """
    Load GMM model (labels + means for each state) from a dictionary
    """
    gmmdictser={k:np.array(v) if isinstance(v,list) else v for k,v in dictin.items()}
    if 'labels' in dictin:
        self.labels=np.array(dictin['labels'])
    else:
        self.labels=None
    if 'fitdatetime' in dictin:
        self.fitdatetime=dictin['fitdatetime']
    else:
        self.fitdatetime=None
    if 'gmm' in dictin:
        self.gmmfit = mixture.GaussianMixture()
        for k,v in dictin['gmm'].items():
            setattr(self.gmmfit,k,np.array(v) if isinstance(v,list) else v)
        self.n_states=self.gmmfit.n_components

predict(iqdata, use_label=True)

Label iqdata with qubit state as determined by

Parameters:

Name Type Description Default
iqdata ndarray

array of complex-valued IQ shots

required

Returns:

Type Description
ndarray

array of labeled data, corresponding to self.labels; same shape as iqdata

Source code in qubic/state_disc.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def predict(self, iqdata: np.ndarray, use_label=True) -> np.ndarray:
    """
    Label iqdata with qubit state as determined by 

    Parameters
    ----------
    iqdata: np.ndarray
        array of complex-valued IQ shots

    Returns
    -------
    np.ndarray
        array of labeled data, corresponding to self.labels; same
        shape as iqdata
    """
    nanmask = np.isnan(iqdata)
    pred_iqdata = iqdata.copy()
    pred_iqdata[nanmask] = 0
    predictions = self.gmmfit.predict(self._format_complex_data(pred_iqdata))
    #ipdb.set_trace()
    if use_label:
        predictions = self.labels[predictions]
    else:
        predictions = predictions.astype(np.float64)
    predictions = np.reshape(predictions, iqdata.shape)
    predictions[nanmask] = np.nan
    return predictions

set_labels(labels)

Set all labels according to provided list

Source code in qubic/state_disc.py
 95
 96
 97
 98
 99
100
101
def set_labels(self, labels: list[int | str] | np.ndarray):
    """
    Set all labels according to provided list
    """
    if len(labels) != self.n_states:
        raise Exception('Must have {} labels!'.format(self.n_states))
    self.labels = np.asarray(labels)

set_labels_maxtomin(iqdata, labels_maxtomin=[0, 1])

Set labels in descending order based on number of shots in a given blob. e.g. if labels_maxtomin = [0,1], this function will assign label 0 to the GMM blob with the highest population in iqdata, and 1 to the next highest. If any rank-ordered blob should have unchanged assignment, set to None. (e.g. labels_maxtomin=[None, 1] will only assign 1 to the lowest population blob)

Parameters:

Name Type Description Default
iqdata ndarray

raw complex IQ shots

required
labels_maxtomin list | ndarray

order of labels to assign, in descending order of prevelance in iqdata

[0, 1]
Source code in qubic/state_disc.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def set_labels_maxtomin(self, iqdata: np.ndarray, labels_maxtomin: list | np.ndarray = [0,1]):
    """
    Set labels in descending order based on number of shots in a given blob.
    e.g. if labels_maxtomin = [0,1], this function will assign label 0
    to the GMM blob with the highest population in iqdata, and 1 to the next
    highest. If any rank-ordered blob should have unchanged assignment, set 
    to None. (e.g. labels_maxtomin=[None, 1] will only assign 1 to the lowest
    population blob)

    Parameters
    ----------
    iqdata: np.ndarray
        raw complex IQ shots
    labels_maxtomin: list or np.ndarray
        order of labels to assign, in descending order
        of prevelance in iqdata
    """
    assert len(labels_maxtomin) <= self.n_states
    pred = self.predict(iqdata, use_label=False)
    n_pred = [] #number of shots at label index 0, 1, 2, etc
    for i in range(self.n_states):
        n_pred.append(np.nansum(pred == i))

    blobinds_sorted = np.argsort(n_pred)[::-1] #sort blobinds in order of max prevelance
    for i, label in enumerate(labels_maxtomin):
        if label is not None:
            self.labels[blobinds_sorted[i]] = label
            print('set label ', blobinds_sorted[i], label)

set_none_label(label)

If any single label is None, set it to label

Source code in qubic/state_disc.py
129
130
131
132
133
134
135
136
def set_none_label(self, label: int | str):
    """
    If any single label is None, set it to `label`
    """
    nonesum=sum(self.labels==None)
    if nonesum==1:
        index=np.where(self.labels==None)[0][0]
        self.labels[index]=label

switch_labels()

Switch 1 and 0 labels. For higher energy states, reverse the order of the labels array.

Source code in qubic/state_disc.py
122
123
124
125
126
127
def switch_labels(self):
    """
    Switch 1 and 0 labels. For higher energy states, reverse the order of
    the labels array.
    """
    self.labels = self.labels[::-1]