sodetlib 0.6.1rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1248 @@
1
+ import time
2
+ import os
3
+ import traceback
4
+ import numpy as np
5
+ import sodetlib as sdl
6
+ import matplotlib.pyplot as plt
7
+ import scipy.optimize
8
+ from sodetlib.operations import iv
9
+ from typing import Dict, Any, Optional
10
+ from dataclasses import dataclass, asdict
11
+ from copy import deepcopy
12
+
13
+ np.seterr(all='ignore')
14
+
15
+
16
+ def _play_tes_bipolar_waveform(S, bias_group, waveform, do_enable=True,
17
+ continuous=True, **kwargs):
18
+ """
19
+ Play a bipolar waveform on the bias group.
20
+ Args
21
+ ----
22
+ bias_group : int
23
+ The bias group
24
+ waveform : float array
25
+ The waveform the play on the bias group.
26
+ do_enable : bool, optional, default True
27
+ Whether to enable the DACs (similar to what is required
28
+ for TES bias).
29
+ continuous : bool, optional, default True
30
+ Whether to play the TES waveform continuously.
31
+ """
32
+ bias_order = S.bias_group_to_pair[:,0]
33
+
34
+ dac_positives = S.bias_group_to_pair[:,1]
35
+ dac_negatives = S.bias_group_to_pair[:,2]
36
+
37
+ dac_idx = np.ravel(np.where(bias_order == bias_group))
38
+
39
+ dac_positive = dac_positives[dac_idx][0]
40
+ dac_negative = dac_negatives[dac_idx][0]
41
+
42
+ # https://confluence.slac.stanford.edu/display/SMuRF/SMuRF+firmware#SMuRFfirmware-RTMDACarbitrarywaveforms
43
+ # Target the two bipolar DACs assigned to this bias group:
44
+ S.set_dac_axil_addr(0, dac_positive)
45
+ S.set_dac_axil_addr(1, dac_negative)
46
+
47
+ # Must enable the DACs (if not enabled already)
48
+ if do_enable:
49
+ S.set_rtm_slow_dac_enable(dac_positive, 2, **kwargs)
50
+ S.set_rtm_slow_dac_enable(dac_negative, 2, **kwargs)
51
+
52
+ # Load waveform into each DAC's LUT table. Opposite sign so
53
+ # they combine coherenty
54
+ S.set_rtm_arb_waveform_lut_table(0, waveform)
55
+ S.set_rtm_arb_waveform_lut_table(1, -waveform)
56
+
57
+ # Enable waveform generation (1=on both DACs)
58
+ S.set_rtm_arb_waveform_enable(3)
59
+
60
+ # Continous mode to play the waveform continuously
61
+ if continuous:
62
+ S.set_rtm_arb_waveform_continuous(1)
63
+ else:
64
+ S.set_rtm_arb_waveform_continuous(0)
65
+
66
+ def _make_step_waveform(S, step_dur, step_voltage, dc_voltage):
67
+ """
68
+ Returns a waveform for a bias step. Waveform will contain step-up
69
+ to (dc_voltage + step_voltage) for `step_dur` seconds, and then a
70
+ step-down to (dc_voltage) for `step_dur` seconds.
71
+
72
+ Args
73
+ -----
74
+ S : SmurfControl
75
+ Pysmurf instance
76
+ step_dur : float
77
+ Duration of each step (sec)
78
+ step_voltage : float
79
+ Amplitude of step (V)
80
+ dc_voltage : float
81
+ DC voltage of step
82
+
83
+ Returns
84
+ ---------
85
+ sig : np.ndarray
86
+ Waveform to use
87
+ timer_size : float
88
+ Waveform timer size
89
+ """
90
+ # Setup waveform
91
+ sig = np.ones(2048)
92
+ sig *= dc_voltage / (2*S._rtm_slow_dac_bit_to_volt)
93
+ sig[1024:] += step_voltage / (2*S._rtm_slow_dac_bit_to_volt)
94
+ # Use twice the step dur because the signal contains two steps
95
+ timer_size = int(step_dur*2/(6.4e-9 * 2048))
96
+ return sig, timer_size
97
+
98
+ def play_bias_steps_waveform(S, cfg, bias_group, step_duration, step_voltage,
99
+ num_steps=5, dc_bias=None):
100
+ """
101
+ Plays bias steps on a single bias line using the Arb waveform generator
102
+
103
+ Args
104
+ ----
105
+ S:
106
+ Pysmurf control instance
107
+ cfg:
108
+ DetConfig instance
109
+ bias_group: int
110
+ Bias groups to play bias step on. Defaults to all 12
111
+ step_duration: float
112
+ Duration of each step in sec
113
+ step_voltage : float
114
+ Step size (volts)
115
+ num_steps: int
116
+ Number of bias steps
117
+ dacs : str
118
+ Which group of DACs to play bias-steps on. Can be 'pos',
119
+ 'neg', or 'both'
120
+ """
121
+ if dc_bias is None:
122
+ dc_bias = S.get_tes_bias_bipolar(bias_group)
123
+ sig, timer_size = _make_step_waveform(S, step_duration, step_voltage, dc_bias)
124
+ S.set_rtm_arb_waveform_timer_size(timer_size, wait_done=True)
125
+ _play_tes_bipolar_waveform(S, bias_group, sig)
126
+ start_time = time.time()
127
+ time.sleep(step_duration * (num_steps+1))
128
+ stop_time = time.time()
129
+ S.set_rtm_arb_waveform_enable(0)
130
+ S.set_tes_bias_bipolar(bias_group, dc_bias)
131
+ return start_time, stop_time
132
+
133
+
134
+ def play_bias_steps_dc(S, cfg, bias_groups, step_duration, step_voltage,
135
+ num_steps=5, dacs='pos'):
136
+ """
137
+ Plays bias steps on a group of bias groups stepping with only one DAC
138
+
139
+ Args:
140
+ S:
141
+ Pysmurf control instance
142
+ cfg:
143
+ DetConfig instance
144
+ bias_group: (int, list, optional)
145
+ Bias groups to play bias step on. Defaults to all 12
146
+ step_duration: float
147
+ Duration of each step in sec
148
+ step_voltage : float
149
+ Step size (volts)
150
+ num_steps: int
151
+ Number of bias steps
152
+ dacs : str
153
+ Which group of DACs to play bias-steps on. Can be 'pos',
154
+ 'neg', or 'both'
155
+ """
156
+ if bias_groups is None:
157
+ bias_groups = cfg.dev.exp['active_bgs']
158
+ bias_groups = np.atleast_1d(bias_groups)
159
+
160
+ if dacs not in ['pos', 'neg', 'both']:
161
+ raise ValueError("Arg dac must be in ['pos', 'neg', 'both']")
162
+
163
+ dac_volt_array_low = S.get_rtm_slow_dac_volt_array()
164
+ dac_volt_array_high = dac_volt_array_low.copy()
165
+
166
+ bias_order, dac_positives, dac_negatives = S.bias_group_to_pair.T
167
+
168
+ for bg in bias_groups:
169
+ bg_idx = np.ravel(np.where(bias_order == bg))
170
+ dac_positive = dac_positives[bg_idx][0] - 1
171
+ dac_negative = dac_negatives[bg_idx][0] - 1
172
+ if dacs == 'pos':
173
+ dac_volt_array_high[dac_positive] += step_voltage
174
+ elif dacs == 'neg':
175
+ dac_volt_array_high[dac_negative] -= step_voltage
176
+ elif dacs == 'both':
177
+ dac_volt_array_high[dac_positive] += step_voltage / 2
178
+ dac_volt_array_high[dac_negative] -= step_voltage / 2
179
+
180
+ start = time.time()
181
+ for _ in range(num_steps):
182
+ S.set_rtm_slow_dac_volt_array(dac_volt_array_high)
183
+ time.sleep(step_duration)
184
+ S.set_rtm_slow_dac_volt_array(dac_volt_array_low)
185
+ time.sleep(step_duration)
186
+ stop = time.time()
187
+
188
+ return start, stop
189
+
190
+
191
+ def exp_fit(t, A, tau, b):
192
+ """
193
+ Fit function for exponential falloff in bias steps
194
+ """
195
+ return A * np.exp(-t / tau) + b
196
+
197
+
198
+ class BiasStepAnalysis:
199
+ """
200
+ Container to manage analysis of bias steps taken with the take_bias_steps
201
+ function. The main function is ``run_analysis`` and will do a series of
202
+ analysis procedures to create a biasgroup map and calculate DC detector
203
+ parameters and tau_eff:
204
+
205
+ - Loads an axis manager with all the data
206
+ - Finds locations of step edges for each bias group
207
+ - Creates a bg map using the isolated bg step responses
208
+ - Gets detector responses of each step and aligns them based on the time
209
+ of the step
210
+ - Computes DC params R0, I0, Pj, Si from step responses
211
+ - Fits exponential to the average step response and estimates tau_eff.
212
+
213
+ Most analysis inputs and products will be saved to a npy file so they can
214
+ be loaded and re-analyzed easily on another computer like simons1.
215
+
216
+ To load data from an saved step file, you can run::
217
+
218
+ bsa = BiasStepAnalysis.load(<path>)
219
+
220
+ Attributes:
221
+ tunefile (path): Path of the tunefile loaded by the pysmurf instance
222
+ high_low_current_ratio (float):
223
+ Ratio of high to low current
224
+ R_sh (float):
225
+ Shunt resistance loaded into pysmurf at time of creation
226
+ pA_per_phi0 (float):
227
+ pA_per_phi0, as loaded in pysmurf at time of creation
228
+ rtm_bit_to_volt (float):
229
+ Conversion between bias dac bit and volt
230
+ bias_line_resistance (float):
231
+ Bias line resistance loaded in pysmurf at time of creation
232
+ high_current_mode (bool):
233
+ If high-current-mode was used
234
+ stream_id (string):
235
+ stream_id of the streamer this was run on.
236
+ sid (int):
237
+ Session-id of streaming session
238
+ start, stop (float):
239
+ start and stop time of all steps
240
+ edge_idxs (array(ints) of shape (nbgs, nsteps)):
241
+ Array containing indexes (wrt axis manager) of bias group steps
242
+ for each bg
243
+ edge_signs (array(+/-1) of shape (nbgs, nsteps)):
244
+ Array of signs of each step, denoting whether step is rising or
245
+ falling
246
+ bg_corr (array (float) of shape (nchans, nbgs)):
247
+ Bias group correlation array, stating likelihood that a given
248
+ channel belongs on a given bias group determined from the isolated
249
+ steps
250
+ bgmap (array (int) of shape (nchans)):
251
+ Map from readout channel to assigned bias group. -1 means not
252
+ assigned (that the assignment threshold was not met for any of the
253
+ 12 bgs)
254
+ abs_chans (array (int) of shape (nchans)):
255
+ Array of the absolute smurf channel number for each channel in the
256
+ axis manager.
257
+ resp_times (array (float) shape (nbgs, npts)):
258
+ Shared timestamps for each of the step responses in <step_resp> and
259
+ <mean_resp> with respect to the bg-step location, with the step
260
+ occuring at t=0.
261
+ mean_resp (array (float) shape (nchans, npts)):
262
+ Step response averaged accross all bias steps for a given channel
263
+ in Amps.
264
+ step_resp (array (float) shape (nchans, nsteps, npts)):
265
+ Each individual step response for a given channel in amps
266
+ Ibias (array (float) shape (nbgs)):
267
+ DC bias current of each bias group (amps)
268
+ Vbias:
269
+ DC bias voltage of each bias group (volts in low-current mode)
270
+ dIbias (array (float) shape (nbgs)):
271
+ Step current for each bias group (amps)
272
+ dVbias (array (float) shape (nbgs)):
273
+ Step voltage for each bias group (volts in low-current mode)
274
+ dItes (array (float) shape (nchans)):
275
+ Array of tes step heigh for each channel (amps)
276
+ R0 (array (float) shape (nchans)):
277
+ Computed TES resistances for each channel (ohms)
278
+ I0 (array (float) shape (nchans)):
279
+ Computed TES currents for each channel (amps)
280
+ Pj (array (float) shape (nchans)):
281
+ Bias power computed for each channel (W)
282
+ Si (array (float) shape (nchans)):
283
+ Responsivity computed for each channel (1/V)
284
+ step_fit_tmin (array (float) shape (nchans)):
285
+ Time after bias step to start fitting exponential (sec)
286
+ step_fit_popts (array (float) of shape (nchans, 3)):
287
+ Optimal fit parameters (A, tau, b) for the exponential fit of each
288
+ channel
289
+ step_fit_pcovs (array (float) shape (nchans, 3, 3)):
290
+ Fit covariances for each channel
291
+ tau_eff (array (float) shape (nchans)):
292
+ Tau_eff for each channel (sec). Same as step_fit_popts[:, 1].
293
+ R_n_IV (array (float) shape (nchans)):
294
+ Array of normal resistances for each channel pulled from IV
295
+ in the device cfg.
296
+ Rfrac (array (float) shape (nchans)):
297
+ Rfrac of each channel, determined from R0 and the channel's normal
298
+ resistance.
299
+ """
300
+
301
+ def __init__(self, S=None, cfg=None, run_kwargs=None):
302
+ self._S = S
303
+ self._cfg = cfg
304
+
305
+ self.am = None
306
+ self.edge_idxs = None
307
+ self.transition_range = None
308
+
309
+ if S is not None:
310
+ self.meta = sdl.get_metadata(S, cfg)
311
+ self.stream_id = cfg.stream_id
312
+
313
+ if run_kwargs is None:
314
+ run_kwargs = {}
315
+ self.run_kwargs = run_kwargs
316
+ self.high_current_mode = run_kwargs.get("high_current_mode", True)
317
+
318
+
319
+ @classmethod
320
+ def from_dict(cls, data) -> 'BiasStepAnalysis':
321
+ self = cls()
322
+ for k, v in data.items():
323
+ setattr(self, k, v)
324
+ return self
325
+
326
+ def to_dict(self) -> Dict[str, Any]:
327
+ data = {}
328
+ saved_fields = [
329
+ # Run data and metadata
330
+ 'bands', 'channels', 'sid', 'meta', 'run_kwargs', 'start', 'stop',
331
+ 'high_current_mode',
332
+ # Bgmap data
333
+ 'bgmap', 'polarity',
334
+ # Step data and fits
335
+ 'resp_times', 'mean_resp', 'step_resp',
336
+ # Step fit data
337
+ 'step_fit_tmin', 'step_fit_popts', 'step_fit_pcovs',
338
+ 'tau_eff',
339
+ # Det param data
340
+ 'transition_range', 'Ibias', 'Vbias', 'dIbias', 'dVbias', 'dItes',
341
+ 'R0', 'I0', 'Pj', 'Si',
342
+ # From IV's
343
+ 'R_n_IV', 'Rfrac',
344
+ ]
345
+
346
+ for f in saved_fields:
347
+ if not hasattr(self, f):
348
+ print(f"WARNING: field {f} does not exist... "
349
+ "defaulting to None")
350
+ data[f] = getattr(self, f, None)
351
+ return data
352
+
353
+ def save(self, path=None) -> None:
354
+ data = self.to_dict()
355
+ if path is not None:
356
+ np.save(path, data, allow_pickle=True)
357
+ self.filepath = path
358
+ else:
359
+ self.filepath = sdl.validate_and_save(
360
+ 'bias_step_analysis.npy', data, S=self._S, cfg=self._cfg,
361
+ make_path=True
362
+ )
363
+
364
+ @classmethod
365
+ def load(cls, filepath) -> 'BiasStepAnalysis':
366
+ self = cls.from_dict(np.load(filepath, allow_pickle=True).item())
367
+ self.filepath = filepath
368
+ return self
369
+
370
+ def run_analysis(
371
+ self, create_bg_map=False, assignment_thresh=0.3, save_bg_map=True,
372
+ arc=None, base_dir='/data/so/timestreams', step_window=0.03, fit_tmin=1.5e-3,
373
+ transition=None, R0_thresh=30e-3, save=False, bg_map_file=None, fit_tau=True,
374
+ ):
375
+ """
376
+ Runs the bias step analysis.
377
+
378
+ Parameters:
379
+ create_bg_map (bool):
380
+ If True, will create a bg map from the step data. If False,
381
+ will use the bgmap from the device cfg
382
+ assignment_thresh (float):
383
+ Correlation threshold for which channels should be assigned to
384
+ particular bias groups.
385
+ save_bg_map (bool):
386
+ If True, will save the created bgmap to disk and set it as
387
+ the bgmap path in the device cfg.
388
+ arc (optional, G3tSmurf):
389
+ G3tSmurf archive. If specified, will attempt to load
390
+ axis-manager using archive instead of sid.
391
+ base_dir (optiional, str):
392
+ Base directory where timestreams are stored. Defaults to
393
+ /data/so/timestreams.
394
+ step_window (float):
395
+ Time after the bias step (in seconds) to use for the analysis.
396
+ fit_tmin (float):
397
+ DEPRECATED!! do not use.
398
+ transition: (tuple, bool, optional)
399
+ DEPRECATED!! do not use.
400
+ R0_thresh (float):
401
+ Any channel with resistance greater than R0_thresh will be
402
+ unassigned from its bias group under the assumption that it's
403
+ crosstalk
404
+ save (bool):
405
+ If true will save the analysis to a npy file.
406
+ bg_map_file (optional, path):
407
+ If create_bg_map is false and this file is not None, use this file
408
+ to load the bg_map.
409
+ fit_tau (bool):
410
+ If True, perform the time constant fit. If False, skip this.
411
+ """
412
+ self._load_am(arc=arc, base_dir=base_dir)
413
+ self._find_bias_edges()
414
+ if create_bg_map:
415
+ self._create_bg_map(assignment_thresh=assignment_thresh,
416
+ save_bg_map=save_bg_map)
417
+ elif bg_map_file is not None:
418
+ self.bgmap, self.polarity = sdl.load_bgmap(
419
+ self.bands, self.channels, bg_map_file)
420
+ else:
421
+ self.bgmap, self.polarity = sdl.load_bgmap(
422
+ self.bands, self.channels, self.meta['bgmap_file'])
423
+
424
+ self._get_step_response(step_window=step_window)
425
+ self._compute_dc_params(R0_thresh=R0_thresh)
426
+
427
+ # Load R_n from IV
428
+ self.R_n_IV = np.full(self.nchans, np.nan)
429
+ # Rfrac determined from R0 and R_n
430
+ self.Rfrac = np.full(self.nchans, np.nan)
431
+ if self.meta['iv_file'] is not None:
432
+ if os.path.exists(self.meta['iv_file']):
433
+ iva = iv.IVAnalysis.load(self.meta['iv_file'])
434
+ chmap = sdl.map_band_chans(
435
+ self.bands, self.channels, iva.bands, iva.channels
436
+ )
437
+ self.R_n_IV = iva.R_n[chmap]
438
+ self.R_n_IV[chmap == -1] = np.nan
439
+ self.Rfrac = self.R0 / self.R_n_IV
440
+
441
+ if create_bg_map and save_bg_map and self._S is not None:
442
+ # Write bgmap after compute_dc_params because bg-assignment
443
+ # will be un-set if resistance estimation is too high.
444
+ ts = str(int(time.time()))
445
+ data = {
446
+ 'bands': self.bands,
447
+ 'channels': self.channels,
448
+ 'sid': self.sid,
449
+ 'meta': self.meta,
450
+ 'bgmap': self.bgmap,
451
+ 'polarity': self.polarity,
452
+ }
453
+ path = os.path.join('/data/smurf_data/bias_group_maps',
454
+ ts[:5],
455
+ self.meta['stream_id'],
456
+ f'{ts}_bg_map.npy')
457
+ os.makedirs(os.path.dirname(path), exist_ok=True)
458
+ sdl.validate_and_save(path, data, S=self._S, cfg=self._cfg,
459
+ register=True, make_path=False)
460
+ self._cfg.dev.update_experiment({'bgmap_file': path},
461
+ update_file=True)
462
+
463
+ if fit_tau:
464
+ self._fit_tau_effs()
465
+
466
+ if save:
467
+ self.save()
468
+
469
+ def _load_am(self, arc=None, base_dir='/data/so/timestreams', fix_timestamps=True):
470
+ """
471
+ Attempts to load the axis manager from the sid or return one that's
472
+ already loaded. Also sets the `abs_chans` array.
473
+
474
+ TODO: adapt this to work on with a general G3tSmurf archive if
475
+ supplied.
476
+ """
477
+ if self.am is None:
478
+ if arc:
479
+ self.am = arc.load_data(self.start, self.stop, stream_id=self.meta['stream_id'])
480
+ else:
481
+ self.am = sdl.load_session(self.meta['stream_id'], self.sid,
482
+ base_dir=base_dir)
483
+
484
+ # Fix up timestamp jitter from timestamping in software
485
+ if fix_timestamps:
486
+ fsamp, t0 = np.polyfit(self.am.primary['FrameCounter'],
487
+ self.am.timestamps, 1)
488
+ self.am.timestamps = t0 + self.am.primary['FrameCounter']*fsamp
489
+ if "det_info" in self.am:
490
+ self.bands = self.am.det_info.smurf.band
491
+ self.channels = self.am.det_info.smurf.channel
492
+ else:
493
+ self.bands = self.am.ch_info.band
494
+ self.channels = self.am.ch_info.channel
495
+ self.abs_chans = self.bands*512 + self.channels
496
+ self.nbgs = len(self.am.biases)
497
+ self.nchans = len(self.am.signal)
498
+ return self.am
499
+
500
+ def _find_bias_edges(self, am=None):
501
+ """
502
+ Finds sample indices and signs of bias steps in timestream.
503
+
504
+ Returns
505
+ --------
506
+ edge_idxs: list
507
+ List containing the edge sample indices for each bias group.
508
+ There are n_biaslines elements, each one a np.ndarray
509
+ contianing a sample idx for each edge found
510
+
511
+ edge_signs: list
512
+ List with the same shape as edge_idxs, containing +/-1
513
+ depending on whether the edge is rising or falling
514
+ """
515
+ if am is None:
516
+ am = self.am
517
+
518
+ edge_idxs = [[] for _ in am.biases]
519
+ edge_signs = [[] for _ in am.biases]
520
+
521
+ for bg, bias in enumerate(am.biases):
522
+ idxs = np.where(np.diff(bias) != 0)[0]
523
+ signs = np.sign(np.diff(bias)[idxs])
524
+
525
+ # Delete double steps
526
+ doubled_idxs = np.where(np.diff(signs) == 0)[0]
527
+ idxs = np.delete(idxs, doubled_idxs)
528
+ signs = np.delete(signs, doubled_idxs)
529
+
530
+ edge_idxs[bg] = idxs
531
+ edge_signs[bg] = signs
532
+
533
+ self.edge_idxs = edge_idxs
534
+ self.edge_signs = edge_signs
535
+
536
+ return edge_idxs, edge_signs
537
+
538
+ def _create_bg_map(self, step_window=0.03, assignment_thresh=0.3,
539
+ save_bg_map=True):
540
+ """
541
+ Creates a bias group mapping from the bg step sweep. The step sweep
542
+ goes down and steps each bias group one-by-one up and down twice. A
543
+ bias group correlation factor is computed by integrating the diff of
544
+ the TES signal times the sign of the step for each bg step. The
545
+ correlation factors are normalized such that the sum across bias groups
546
+ for each channel is 1, and an assignment is made if the normalized
547
+ bias-group correlation is greater than some threshold.
548
+
549
+ Saves:
550
+ bg_corr (np.ndarray):
551
+ array of shape (nchans, nbgs) that contain the correlation
552
+ factor for each chan/bg combo (normalized st the sum is 1).
553
+ bgmap (np.ndarray):
554
+ Array of shape (nchans) containing the assigned bg of each
555
+ channel, or -1 if no assigment could be determined
556
+ """
557
+ am = self._load_am()
558
+ if self.edge_idxs is None:
559
+ self._find_bias_edges()
560
+
561
+ fsamp = np.nanmean(1./np.diff(am.timestamps))
562
+ npts = int(fsamp * step_window)
563
+
564
+ nchans = len(am.signal)
565
+ nbgs = len(am.biases)
566
+ bgs = np.arange(nbgs)
567
+ bg_corr = np.zeros((nchans, nbgs))
568
+
569
+ for bg in bgs:
570
+ for i, ei in enumerate(self.edge_idxs[bg]):
571
+ s = slice(ei, ei+npts)
572
+ ts = am.timestamps[s]
573
+ if not (self.start < ts[0] < self.stop):
574
+ continue
575
+ sig = self.edge_signs[bg][i] * am.signal[:, s]
576
+ bg_corr[:, bg] += np.sum(np.diff(sig), axis=1)
577
+ abs_bg_corr = np.abs(bg_corr)
578
+ normalized_bg_corr = (abs_bg_corr.T / np.sum(abs_bg_corr, axis=1)).T
579
+ normalized_bg_corr[np.isnan(normalized_bg_corr)] = 0.
580
+ bgmap = np.nanargmax(normalized_bg_corr, axis=1)
581
+ m = np.max(normalized_bg_corr, axis=1) < assignment_thresh
582
+ bgmap[m] = -1
583
+
584
+ # Calculate the sign of each channel
585
+ self.polarity = np.ones(self.nchans, dtype=int)
586
+ for i in range(self.nchans):
587
+ self.polarity[i] = np.sign(bg_corr[i, bgmap[i]])
588
+
589
+ self.bg_corr = normalized_bg_corr
590
+ self.bgmap = bgmap
591
+
592
+ return self.bgmap
593
+
594
+ def _get_step_response(self, step_window=0.03, pts_before_step=20,
595
+ restrict_to_bg_sweep=False, am=None):
596
+ """
597
+ Finds each channel's response to the bias step by looking at the signal
598
+ in a small window of <npts> around each edge-index.
599
+
600
+ Saves:
601
+ resp_times:
602
+ Array of shape (nbgs, npts) containing the shared timestamps
603
+ for channels on a given bias group
604
+ step_resp:
605
+ Array of shape (nchans, nsteps, npts) containing the response
606
+ of each channel in a window around every step
607
+ mean_resp:
608
+ Array of (nchans, npts) containing the averaged step response
609
+ for each channel
610
+ """
611
+ if am is None:
612
+ am = self.am
613
+
614
+ fsamp = np.nanmean(1./np.diff(am.timestamps))
615
+ pts_after_step = int(fsamp * step_window)
616
+ nchans = len(am.signal)
617
+ nbgs = len(am.biases)
618
+ npts = pts_before_step + pts_after_step
619
+ n_edges = np.max([len(ei) for ei in self.edge_idxs])
620
+
621
+ sigs = np.full((nchans, n_edges, npts), np.nan)
622
+ ts = np.full((nbgs, npts), np.nan)
623
+
624
+ A_per_rad = self.meta['pA_per_phi0'] / (2*np.pi) * 1e-12
625
+ for bg in np.unique(self.bgmap):
626
+ if bg == -1:
627
+ continue
628
+ rcs = np.where(self.bgmap == bg)[0]
629
+ for i, ei in enumerate(self.edge_idxs[bg][:-2]):
630
+ if i < 2:
631
+ continue
632
+ s = slice(ei - pts_before_step, ei + pts_after_step)
633
+ if np.isnan(ts[bg]).all():
634
+ ts[bg, :] = am.timestamps[s] - am.timestamps[ei]
635
+ sig = self.edge_signs[bg][i] * am.signal[rcs, s] * A_per_rad
636
+ # Subtracts mean of last 10 pts such that step ends at 0
637
+ sigs[rcs, i, :] = (sig.T - np.nanmean(sig[:, -10:], axis=1)).T
638
+
639
+ self.resp_times = ts
640
+ self.step_resp = (sigs.T * self.polarity).T
641
+ self.mean_resp = (np.nanmean(sigs, axis=1).T * self.polarity).T
642
+
643
+ return ts, sigs
644
+
645
+ def _compute_R0_I0_Pj(self):
646
+ """
647
+ Computes the DC params R0 I0 and Pj
648
+ """
649
+ Ib = self.Ibias[self.bgmap]
650
+ dIb = self.dIbias[self.bgmap]
651
+ dItes = self.dItes
652
+ Ib[self.bgmap == -1] = np.nan
653
+ dIb[self.bgmap == -1] = np.nan
654
+ dIrat = dItes / dIb
655
+
656
+ R_sh = self.meta["R_sh"]
657
+
658
+ I0 = np.zeros_like(dIrat)
659
+ I0_nontransition = Ib * dIrat
660
+ I0_transition = Ib * dIrat / (2 * dIrat - 1)
661
+ I0[dIrat>0] = I0_nontransition[dIrat>0]
662
+ I0[dIrat<0] = I0_transition[dIrat<0]
663
+
664
+ Pj = I0 * R_sh * (Ib - I0)
665
+ R0 = Pj / I0**2
666
+ R0[I0 == 0] = 0
667
+
668
+ return R0, I0, Pj
669
+
670
+ def _compute_dc_params(self, R0_thresh=30e-3):
671
+ """
672
+ Calculates Ibias, dIbias, and dItes from axis manager, and then
673
+ runs the DC param calc to estimate R0, I0, Pj, etc. Here you must
674
+
675
+ Args:
676
+ transition: (tuple)
677
+ Range of voltage bias values (in low-cur units) where the
678
+ "in-transition" resistance calculation should be used. If True,
679
+ or False, will use in-transition or normal calc for all
680
+ channels. Will default to ``cfg.dev.exp['transition_range']`` or
681
+ (1, 8) if that does not exist or if self._cfg is not set.
682
+ R0_thresh: (float)
683
+ Any channel with resistance greater than R0_thresh will be
684
+ unassigned from its bias group under the assumption that it's
685
+ crosstalk
686
+
687
+ Saves:
688
+ Ibias:
689
+ Array of shape (nbgs) containing the DC bias current for each
690
+ bias group
691
+ Vbias:
692
+ Array of shape (nbgs) containing the DC bias voltage for each
693
+ bias group
694
+ dIbias:
695
+ Array of shape (nbgs) containing the step current for each
696
+ bias group
697
+ dVbias:
698
+ Array of shape (nbgs) containing the step voltage for each
699
+ bias group
700
+ """
701
+ nbgs = len(self.am.biases)
702
+
703
+ Ibias = np.full(nbgs, np.nan)
704
+ dIbias = np.full(nbgs, 0.0, dtype=float)
705
+
706
+ # Compute Ibias and dIbias
707
+ bias_line_resistance = self.meta['bias_line_resistance']
708
+ high_low_current_ratio = self.meta['high_low_current_ratio']
709
+ rtm_bit_to_volt = self.meta['rtm_bit_to_volt']
710
+ amp_per_bit = 2 * rtm_bit_to_volt / bias_line_resistance
711
+ if self.high_current_mode:
712
+ amp_per_bit *= high_low_current_ratio
713
+ for bg in range(nbgs):
714
+ if len(self.edge_idxs[bg]) == 0:
715
+ continue
716
+ b0 = self.am.biases[bg, self.edge_idxs[bg][0] - 3]
717
+ b1 = self.am.biases[bg, self.edge_idxs[bg][0] + 3]
718
+ Ibias[bg] = b0 * amp_per_bit
719
+ dIbias[bg] = (b1 - b0) * amp_per_bit
720
+
721
+ # Compute dItes
722
+ i0 = np.nanmean(self.mean_resp[:, :5], axis=1)
723
+ i1 = np.nanmean(self.mean_resp[:, -10:], axis=1)
724
+ dItes = i1 - i0
725
+
726
+ self.Ibias = Ibias
727
+ self.Vbias = Ibias * bias_line_resistance
728
+ self.dIbias = dIbias
729
+ self.dVbias = dIbias * bias_line_resistance
730
+ self.dItes = dItes
731
+
732
+ R0, I0, Pj = self._compute_R0_I0_Pj()
733
+
734
+ Si = -1./(I0 * (R0 - self.meta['R_sh']))
735
+
736
+ # If resistance is too high, most likely crosstalk so just reset
737
+ # bg mapping and det params
738
+ if R0_thresh is not None:
739
+ m = np.abs(R0) > R0_thresh
740
+ self.bgmap[m] = -1
741
+ for arr in [R0, I0, Pj, Si]:
742
+ arr[m] = np.nan
743
+
744
+ self.R0 = R0
745
+ self.I0 = I0
746
+ self.Pj = Pj
747
+ self.Si = Si
748
+
749
+ return R0, I0, Pj, Si
750
+
751
+ def _fit_tau_effs(self, tmin=1.5e-3, weight_exp=0.3):
752
+ """
753
+ Fits mean step responses to exponential
754
+
755
+ Args:
756
+ tmin: float
757
+ DEPRECATED!! do not use.
758
+
759
+ Saves:
760
+ step_fit_tmin: np.ndarray
761
+ Array of shape (nchans) containing tmin used for each chan.
762
+ step_fit_popts: np.ndarray
763
+ Array of shape (nchans, 3) containing popt for each chan.
764
+ step_fit_pcovs: np.ndarray
765
+ Array of shape (nchans, 3, 3) containing pcov matrices for each
766
+ channel
767
+ tau_eff: np.ndarray
768
+ Array of shape (nchans) contianing tau_eff for each chan.
769
+ """
770
+ nbgs = len(self.am.biases)
771
+ nchans = len(self.am.signal)
772
+ step_fit_popts = np.full((nchans, 3), np.nan)
773
+ step_fit_pcovs = np.full((nchans, 3, 3), np.nan)
774
+ step_fit_tmin = np.full(nchans, np.nan)
775
+
776
+ for bg in range(nbgs):
777
+ rcs = np.where(self.bgmap == bg)[0]
778
+ if not len(rcs):
779
+ continue
780
+ ts = self.resp_times[bg]
781
+ if not len(ts[~np.isnan(ts)]):
782
+ continue
783
+ for rc in rcs:
784
+ resp = self.mean_resp[rc]
785
+ tmin_m = np.abs(resp) > 0.9*np.nanmax(np.abs(resp))
786
+ if not tmin_m.any():
787
+ continue
788
+ tmin = np.max((0, ts[tmin_m][-1]))
789
+ m = (ts > tmin) & (~np.isnan(resp))
790
+ if not m.any():
791
+ continue
792
+ offset_guess = np.nanmean(resp[np.abs(ts - ts[-1]) < 0.01])
793
+ bounds = [
794
+ (-np.inf, 0, -np.inf),
795
+ (np.inf, 0.1, np.inf)
796
+ ]
797
+ p0 = (0.1, 0.001, offset_guess)
798
+ try:
799
+ popt, pcov = scipy.optimize.curve_fit(
800
+ exp_fit, ts[m], resp[m],
801
+ sigma=ts[m]**weight_exp, p0=p0, bounds=bounds
802
+ )
803
+ step_fit_popts[rc] = popt
804
+ step_fit_pcovs[rc] = pcov
805
+ step_fit_tmin[rc] = tmin
806
+ except RuntimeError:
807
+ pass
808
+
809
+ self.step_fit_tmin = step_fit_tmin
810
+ self.step_fit_popts = step_fit_popts
811
+ self.step_fit_pcovs = step_fit_pcovs
812
+ self.tau_eff = step_fit_popts[:, 1]
813
+
814
+ ####################################################################
815
+ # Plotting functions
816
+ ####################################################################
817
+
818
+ def plot_steps(bsa, rc, nsteps=5, offset=0, ax=None, **kw):
819
+ if ax is None:
820
+ fig, ax = plt.subplots()
821
+ else:
822
+ fig = ax.figure
823
+
824
+ bg = bsa.bgmap[rc]
825
+ i0 = bsa.edge_idxs[bg][2]
826
+ if 3 + nsteps < len(bsa.edge_idxs[bg]):
827
+ i1 = bsa.edge_idxs[bg][3 + nsteps]
828
+ else:
829
+ i1 = bsa.edge_idxs[bg][-1]
830
+
831
+ sl = slice(i0, i1)
832
+ ts = bsa.am.timestamps[sl]
833
+ ts = ts - ts[0]
834
+ sig = bsa.am.signal[rc, sl]
835
+ sig = sig - np.nanmean(sig) + offset
836
+ ax.plot(ts, sig, **kw)
837
+
838
+ return fig, ax
839
+
840
+
841
+ def plot_step_fit(bsa, rc, ax=None, plot_all_steps=True):
842
+ """
843
+ Plots the step response and fit of a given readout channel
844
+ """
845
+ if ax is None:
846
+ fig, ax = plt.subplots()
847
+ else:
848
+ fig = ax.figure
849
+
850
+ bg = bsa.bgmap[rc]
851
+ ts = bsa.resp_times[bg]
852
+ try:
853
+ m = ts > bsa.step_fit_tmin[rc]
854
+ except TypeError:
855
+ m = ts > bsa.step_fit_tmin
856
+ if plot_all_steps:
857
+ for sig in bsa.step_resp[rc]:
858
+ ax.plot(ts*1000, sig, alpha=0.1, color='grey')
859
+ ax.plot(ts*1000, bsa.mean_resp[rc], '.', label='avg step')
860
+ ax.plot(ts[m]*1000, exp_fit(ts[m], *bsa.step_fit_popts[rc]), label='fit')
861
+
862
+ text = '\n'.join([
863
+ r'$\tau_\mathrm{eff}$=' + f'{bsa.tau_eff[rc]*1000:0.2f} ms',
864
+ r'$R_\mathrm{TES}=$' + f'{bsa.R0[rc]* 1000:0.2f}' + r'm$\Omega$',
865
+ ])
866
+
867
+ ax.text(0.7, 0.1, text, transform=ax.transAxes,
868
+ bbox={'facecolor': 'wheat', 'alpha': 0.3}, fontsize=12)
869
+
870
+ ax.legend()
871
+ ax.set(xlabel="Time (ms)", ylabel="Current (Amps)")
872
+
873
+ return fig, ax
874
+
875
+ def plot_iv_res_comparison(bsa, lim=None, bgs=None, ax=None):
876
+ if bgs is None:
877
+ bgs = bsa.bgs
878
+ bgs = np.atleast_1d(bgs)
879
+
880
+ iva = iv.IVAnalysis.load(bsa.meta['iv_file'])
881
+ chmap = sdl.map_band_chans(bsa.bands, bsa.channels,
882
+ iva.bands, iva.channels)
883
+ iv_res = np.full(bsa.nchans, np.nan)
884
+
885
+ if ax is None:
886
+ fig, ax = plt.subplots()
887
+ fig.patch.set_facecolor('white')
888
+ else:
889
+ fig = ax.figure
890
+
891
+ for bg in bgs:
892
+ m = bsa.bgmap == bg
893
+ vb = bsa.Vbias[bg]
894
+ idx = np.nanargmin(np.abs(iva.v_bias - vb))
895
+ iv_res = iva.R[chmap[m], idx]
896
+ ax.scatter(bsa.R0[m]*1000, iv_res*1000, marker='.', alpha=0.2,
897
+ label=f'Bias Group {bg}')
898
+ if lim is None:
899
+ lim = (-0.1, 10)
900
+ ax.plot(lim, lim, ls='--', color='grey', alpha=0.4)
901
+ ax.set(xlim=lim, ylim=lim)
902
+ ax.set_xlabel("Bias Step Resistance (mOhm)")
903
+ ax.set_ylabel("IV Resistance (mOhm)")
904
+
905
+ return fig, ax
906
+
907
+ def get_plot_text(bsa):
908
+ """
909
+ Gets text to add to plot textboxes
910
+ """
911
+ return '\n'.join([
912
+ f"stream_id: {bsa.meta['stream_id']}",
913
+ f"sid: {bsa.sid}",
914
+ f"path: {os.path.basename(bsa.filepath)}",
915
+ ])
916
+
917
+ def plot_bg_assignment(bsa, text_loc=(0.05, 0.85), text_kw=None):
918
+ """
919
+ Plots bias group assignment summary
920
+ """
921
+ xs = np.arange(13)
922
+ ys = np.zeros(13)
923
+
924
+ xticklabels = []
925
+ for i in range(12):
926
+ ys[i] = np.sum(bsa.bgmap == i)
927
+ xticklabels.append(str(i))
928
+ ys[12] = np.sum(bsa.bgmap == -1)
929
+ xticklabels.append('None')
930
+
931
+ fig, ax = plt.subplots(figsize=(16, 10))
932
+ ax.bar(xs, ys)
933
+ ax.set_xticks(np.arange(13))
934
+ ax.set_xticklabels(xticklabels)
935
+ ax.set_xlabel("Bias Group", fontsize=16)
936
+ ax.set_ylabel("Num Channels", fontsize=16)
937
+ if text_kw is None:
938
+ text_kw = {}
939
+
940
+ ax.text(
941
+ *text_loc, get_plot_text(bsa), transform=ax.transAxes,
942
+ fontsize=18, bbox=dict(facecolor='white', alpha=0.8)
943
+ )
944
+ return fig, ax
945
+
946
+
947
+ def plot_Rtes(bsa):
948
+ """
949
+ Plots Rtes
950
+ """
951
+ fig, ax = plt.subplots()
952
+ ax.hist(bsa.R0 * 1000, range=(-1, 10), bins=40)
953
+ ax.set_xlabel(r"$R_\mathrm{TES}$ (m$\Omega$)", fontsize=12)
954
+ return fig, ax
955
+
956
+ def plot_Rfrac(bsa, text_loc=(0.6, 0.8)):
957
+ """
958
+ Plots Rfrac split out by bias group
959
+ """
960
+ iva = iv.IVAnalysis.load(bsa.meta['iv_file'])
961
+ chmap = sdl.map_band_chans(bsa.bands, bsa.channels,
962
+ iva.bands, iva.channels)
963
+
964
+ Rfracs = bsa.R0 / iva.R_n[chmap]
965
+ lim = (-0.1, 1.2)
966
+ nbins=30
967
+ bins = np.linspace(*lim, nbins)
968
+ bgs = np.arange(12)
969
+ hists = np.zeros((len(bgs), nbins-1))
970
+ for bg in bgs:
971
+ m = bsa.bgmap == bg
972
+ if not m.any():
973
+ continue
974
+ hists[bg, :] = np.histogram(Rfracs[m], bins=bins)[0]
975
+
976
+ offset = 0.2* np.max(hists)
977
+ fig, ax = plt.subplots(figsize=(18, 10))
978
+ fig.patch.set_facecolor('white')
979
+ for bg, h in enumerate(hists):
980
+ ax.plot(bins[1:], h + offset * bg, '-o', markersize=4)
981
+ ax.fill_between(bins[1:], h + offset * bg, y2=offset * bg, alpha=0.2)
982
+ txt = f"{int(np.sum(h))} Chans"
983
+ ax.text(1.1, offset*(bg + 0.15), txt, fontsize=20)
984
+
985
+ ax.set_xlabel(r"Rfrac", fontsize=24)
986
+ ax.set_yticks(offset * bgs)
987
+ ax.set_yticklabels([f'BG {bg}' for bg in bgs], fontsize=20)
988
+ ax.tick_params(axis='x', labelsize=24)
989
+ txt = get_plot_text(bsa)
990
+ ax.text(*text_loc, txt, bbox=dict(facecolor='white', alpha=0.8), fontsize=20,
991
+ transform=ax.transAxes)
992
+ return fig, ax
993
+
994
+
995
+ @dataclass
996
+ class BiasStepConfig:
997
+ bgs: Optional[float] = None
998
+ dc_voltage: float = None
999
+ step_voltage: float = 0.05
1000
+ step_duration: float = 0.05
1001
+ nsteps: int = 20
1002
+ high_current_mode: bool = True
1003
+ hcm_wait_time: float = 3.
1004
+ dacs: str = 'pos'
1005
+ use_waveform: bool = True
1006
+ plot_rfrac: bool = True
1007
+ show_plots: bool = True
1008
+ run_analysis: bool = True
1009
+ channel_mask: Optional[np.ndarray] = None
1010
+ g3_tag: Optional[str] = None
1011
+ stream_subtype: str = 'bias_steps'
1012
+ enable_compression: bool = False
1013
+ analysis_kwargs: Optional[dict] = None
1014
+
1015
+ def __post_init__(self):
1016
+ if self.analysis_kwargs is None:
1017
+ self.analysis_kwargs = {}
1018
+
1019
+ if self.dacs not in ['pos', 'neg', 'both']:
1020
+ raise ValueError(f'dacs={self.dacs} not in ["pos", "neg", "both"]')
1021
+
1022
+ @dataclass
1023
+ class BgMapConfig(BiasStepConfig):
1024
+ dc_voltage: float = 0.3
1025
+ step_voltage: float = 0.01
1026
+ hcm_wait_time: float = 0
1027
+ plot_rfrac: bool = False
1028
+ stream_subtype: str = 'bgmap'
1029
+
1030
+ def __post_init__(self):
1031
+ super().__post_init__()
1032
+
1033
+ # Makes sure these kwargs are set by default unless otherwise
1034
+ # specified
1035
+ kw = {
1036
+ 'assignment_thresh': 0.3, 'create_bg_map': True,
1037
+ 'save_bg_map': True
1038
+ }
1039
+ kw.update(self.analysis_kwargs)
1040
+ self.analysis_kwargs = kw
1041
+
1042
+ def get_bias_step_cfg(self):
1043
+ return BiasStepConfig(
1044
+ **{f.name: getattr(self, f.name) for f in fields(BiasStepConfig)}
1045
+ )
1046
+
1047
+
1048
+ @sdl.set_action()
1049
+ def take_bgmap(S, cfg, **bgmap_pars):
1050
+ """
1051
+ Function to easily create a bgmap. This will set all bias group voltages
1052
+ to 0 (since this is best for generating the bg map), and run bias-steps
1053
+ with default parameters optimal for creating a bgmap.
1054
+
1055
+ See also:
1056
+ ---------
1057
+ take_bias_steps
1058
+ """
1059
+ kw = deepcopy(cfg.dev.exp['bgmap_defaults'])
1060
+ kw.update(bgmap_pars)
1061
+ bgmap_cfg = BgMapConfig(**kw)
1062
+
1063
+ if bgmap_cfg.bgs is None:
1064
+ bgmap_cfg.bgs = cfg.dev.exp['active_bgs']
1065
+ bgmap_cfg.bgs = np.atleast_1d(bgmap_cfg.bgs)
1066
+
1067
+ bsa = take_bias_steps(S, cfg, **asdict(bgmap_cfg))
1068
+
1069
+ if hasattr(bsa, 'bgmap'):
1070
+ fig, ax = plot_bg_assignment(bsa)
1071
+ sdl.save_fig(S, fig, 'bg_assignments.png')
1072
+ if bgmap_cfg.show_plots:
1073
+ plt.show()
1074
+ else:
1075
+ plt.close()
1076
+
1077
+ for bg in bgmap_cfg.bgs:
1078
+ S.set_tes_bias_bipolar(bg, 0.)
1079
+
1080
+ return bsa
1081
+
1082
+
1083
+ @sdl.set_action()
1084
+ def take_bias_steps(S, cfg, **bscfg_pars):
1085
+ """
1086
+ Takes bias step data at the specified DC voltage. Assumes bias lines
1087
+ are already in low-current mode (if they are in high-current this will
1088
+ not run correction). This function runs bias steps and returns a
1089
+ BiasStepAnalysis object, which can be used to easily view and re-analyze
1090
+ data.
1091
+
1092
+ This function will first run a "bias group sweep", running multiple steps
1093
+ on each bias-line one at a time. This data is used to generate a bgmap.
1094
+ After, <nsteps> bias steps are played on all channels simultaneously.
1095
+
1096
+ Parameters:
1097
+ S (SmurfControl):
1098
+ Pysmurf control instance
1099
+ cfg (DetConfig):
1100
+ Detconfig instance
1101
+ bgs ( int, list, optional):
1102
+ Bias groups to run steps on.
1103
+ dc_voltage : float, optional
1104
+ DC bias used at low end of bias step to avoid divide by zeros in
1105
+ the analysis. If unspecified, uses the current value.
1106
+ step_voltage (float):
1107
+ Step voltage in Low-current-mode units. (i.e. this will be divided
1108
+ by the high-low-ratio before running the steps in high-current
1109
+ mode)
1110
+ step_duration (float):
1111
+ Duration in seconds of each step
1112
+ nsteps (int):
1113
+ Number of steps to run
1114
+ high_current_mode (bool):
1115
+ If true, switches to high-current-mode. If False, leaves in LCM
1116
+ which runs through the bias-line filter, so make sure you
1117
+ extend the step duration to be like >2 sec or something
1118
+ hcm_wait_time (float):
1119
+ Time to wait after switching to high-current-mode.
1120
+ dacs : {'pos', 'neg', 'both'}
1121
+ Which group of DACs to play bias-steps on.
1122
+ run_analysis (bool):
1123
+ If True, will attempt to run the analysis to calculate DC params
1124
+ and tau_eff. If this fails, the analysis object will
1125
+ still be returned but will not contain all analysis results.
1126
+ analysis_kwargs (dict, optional):
1127
+ Keyword arguments to be passed to the BiasStepAnalysis run_analysis
1128
+ function.
1129
+ channel_mask : np.ndarray, optional
1130
+ Mask containing absolute smurf-channels to write to disk
1131
+ g3_tag: string, optional
1132
+ Tag to attach to g3 stream.
1133
+ stream_subtype : optional, string
1134
+ Stream subtype for this operation. This will default to 'bias_steps'.
1135
+ enable_compression: bool, optional
1136
+ If True, will tell the smurf-streamer to compress G3Frames. Defaults
1137
+ to False because this dominates frame-processing time for high
1138
+ data-rate streams.
1139
+ plot_rfrac : bool
1140
+ Create rfrac plot, publish it, and save it. Default is True.
1141
+ show_plots : bool
1142
+ Show plot in addition to saving when running interactively. Default is False.
1143
+ """
1144
+ kw = deepcopy(cfg.dev.exp['biasstep_defaults'])
1145
+ kw.update(bscfg_pars)
1146
+ bscfg = BiasStepConfig(**kw)
1147
+
1148
+ if bscfg.bgs is None:
1149
+ bscfg.bgs = cfg.dev.exp['active_bgs']
1150
+ bscfg.bgs = np.atleast_1d(bscfg.bgs)
1151
+
1152
+ # Adds to account for steps that may be cut in analysis
1153
+ bscfg.nsteps += 4
1154
+
1155
+ if bscfg.dc_voltage is not None:
1156
+ for bg in bscfg.bgs:
1157
+ S.set_tes_bias_bipolar(bg, bscfg.dc_voltage)
1158
+
1159
+ initial_ds_factor = S.get_downsample_factor()
1160
+ initial_filter_disable = S.get_filter_disable()
1161
+ initial_dc_biases = S.get_tes_bias_bipolar_array()
1162
+ init_current_mode = sdl.get_current_mode_array(S)
1163
+
1164
+ try:
1165
+ dc_biases = initial_dc_biases
1166
+ if bscfg.high_current_mode:
1167
+ dc_biases = dc_biases / S.high_low_current_ratio
1168
+ bscfg.step_voltage /= S.high_low_current_ratio
1169
+ sdl.set_current_mode(S, bscfg.bgs, 1)
1170
+ S.log(f"Waiting {bscfg.hcm_wait_time} sec after switching to hcm")
1171
+ time.sleep(bscfg.hcm_wait_time)
1172
+
1173
+ bsa = BiasStepAnalysis(S, cfg, run_kwargs=asdict(bscfg))
1174
+
1175
+ bsa.sid = sdl.stream_g3_on(
1176
+ S, tag=bscfg.g3_tag, channel_mask=bscfg.channel_mask, downsample_factor=1,
1177
+ filter_disable=True, subtype=bscfg.stream_subtype,
1178
+ enable_compression=bscfg.enable_compression
1179
+ )
1180
+
1181
+ bsa.start = time.time()
1182
+
1183
+ for bg in bscfg.bgs:
1184
+ if bscfg.use_waveform:
1185
+ play_bias_steps_waveform(
1186
+ S, cfg, bg, bscfg.step_duration, bscfg.step_voltage,
1187
+ num_steps=bscfg.nsteps
1188
+ )
1189
+ else:
1190
+ play_bias_steps_dc(
1191
+ S, cfg, bg, bscfg.step_duration, bscfg.step_voltage,
1192
+ num_steps=bscfg.nsteps, dacs=bscfg.dacs,
1193
+ )
1194
+ bsa.stop = time.time()
1195
+
1196
+ finally:
1197
+ sdl.stream_g3_off(S)
1198
+
1199
+ # Restores current mode to initial values
1200
+ sdl.set_current_mode(S, np.where(init_current_mode == 0)[0], 0)
1201
+ sdl.set_current_mode(S, np.where(init_current_mode == 1)[0], 1)
1202
+
1203
+ S.set_downsample_factor(initial_ds_factor)
1204
+ S.set_filter_disable(initial_filter_disable)
1205
+
1206
+ if bscfg.run_analysis:
1207
+ S.log("Running bias step analysis")
1208
+ try:
1209
+ bsa.run_analysis(save=True, **bscfg.analysis_kwargs)
1210
+ if bscfg.plot_rfrac:
1211
+ fig, ax = plot_Rfrac(bsa)
1212
+ path = sdl.make_filename(S, 'bs_rfrac_summary.png', plot=True)
1213
+ fig.savefig(path)
1214
+ S.pub.register_file(path, 'bs_rfrac_summary', plot=True, format='png')
1215
+ if not bscfg.show_plots:
1216
+ plt.close(fig)
1217
+ except Exception:
1218
+ print(f"Bias step analysis failed with exception:")
1219
+ print(traceback.format_exc())
1220
+
1221
+ return bsa
1222
+
1223
+
1224
+ def plot_taueff_hist(bsa, text_loc=(0.4, 0.8), fontsize=15, figsize=(10, 6),
1225
+ range=(0, 3)):
1226
+ """
1227
+ Plots a histogram of the tau_eff measurements for all bias lines.
1228
+
1229
+ Args
1230
+ -----
1231
+ bsa : BiasStepAnalysis
1232
+ analyzed BiasStepAnalysis object
1233
+ text_loc : tuple[float]
1234
+ Location of textbox in the axis coordinate system.
1235
+ fontsize : int
1236
+ font size of text in textbox
1237
+ figsize : tuple
1238
+ Figure size
1239
+ range : tuple
1240
+ histogram range
1241
+ """
1242
+ fig, ax = plt.subplots(figsize=figsize)
1243
+ ax.hist(bsa.tau_eff * 1000, range=range, bins=30)
1244
+ ax.set_xlabel("Tau eff (ms)", fontsize=18)
1245
+ txt = get_plot_text(bsa)
1246
+ ax.text(*text_loc, txt, bbox=dict(facecolor='white', alpha=0.8),
1247
+ fontsize=fontsize, transform=ax.transAxes)
1248
+ return fig, ax