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,688 @@
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 scipy.signal import welch
9
+ from sodetlib.operations import bias_steps, iv
10
+
11
+ np.seterr(all='ignore')
12
+
13
+ def play_bias_wave(S, cfg, bias_group, freqs_wave, amp_wave, duration,
14
+ dc_bias=None):
15
+ """
16
+ Play a sine wave on the bias group.
17
+
18
+ Args
19
+ ----
20
+ bias_group : int
21
+ The bias group
22
+ freq_wave : float array
23
+ List of frequencies to play. Unit = Hz.
24
+ amp_wave : float
25
+ Amplitude of sine wave to use. Unit = Volts.
26
+ duration : float
27
+ Duration each sine wave is played
28
+ dc_bias : float, optional
29
+ Offset voltage of sine wave. Unit = Volts.
30
+
31
+ Returns
32
+ -------
33
+ start_times : float list
34
+ unix timestamp for the beginning of each sine wave.
35
+ stop_times : float list
36
+ unix timestamp for the end of each sine wave.
37
+ """
38
+ start_times, stop_times = [], []
39
+
40
+ if dc_bias is None:
41
+ dc_bias = S.get_tes_bias_bipolar_array()[bias_group]
42
+
43
+ for freq in freqs_wave:
44
+ S.log(f"BL sine wave with bg={bias_group}, freq={freq}")
45
+ S.play_sine_tes(bias_group=bias_group,
46
+ tone_amp=amp_wave,
47
+ tone_freq=freq, dc_amp=dc_bias)
48
+ start_times.append(time.time())
49
+ time.sleep(duration)
50
+ stop_times.append(time.time())
51
+ S.set_rtm_arb_waveform_enable(0)
52
+ S.set_tes_bias_bipolar(bias_group, dc_bias)
53
+ return start_times, stop_times
54
+
55
+ def get_amplitudes(f_c, x, fs = 4000, N = 12000, window = 'hann'):
56
+ """
57
+ Function for calculating the amplitude of a sine wave.
58
+
59
+ Args
60
+ ----
61
+ f_c : float
62
+ Target frequency to calculate sine wave amplitude at.
63
+ x : np.array
64
+ Data to analyze. Either can be shape nsamp or ndet x nsamp.
65
+ fs : int
66
+ Sample rate. Unit = samples/second.
67
+ N : int
68
+ Number of samples to calculate FFT on.
69
+ window : str
70
+ Window function to use. See scipy.signal.get_window for a list of
71
+ valid window functions to use. Default is ``hann``.
72
+
73
+ Returns
74
+ -------
75
+ a_peak : float
76
+ Amplitudes of sine waves. Shape is len(x)
77
+ """
78
+ x = np.atleast_2d(x)
79
+ f, p = welch(x, fs = fs, nperseg = N,
80
+ scaling = 'spectrum',return_onesided=True,
81
+ window = window)
82
+ a = np.sqrt(p)
83
+ # Ran into problem with f == f_c when nperseg and len(x) are not right.
84
+ # Ben to add a function to check this or enforce correct choice somehow.
85
+ idx = np.argmin(np.abs(f-f_c))
86
+ a_rms = a[:, idx]
87
+ a_peak = np.sqrt(2)*a_rms
88
+ return a_peak
89
+
90
+ def get_amplitudes_deprojection(f_c, x, ts):
91
+ """
92
+ Function for calculating the amplitude and phase of a wave by deprojecting sine and cosine components of desired frequency.
93
+
94
+ Args
95
+ ----
96
+ f_c : float
97
+ Target frequency to calculate wave amplitude at.
98
+ x : np.array
99
+ Data to analyze. Either can be shape nsamp or ndet x nsamp.
100
+ ts : np.array
101
+ Timestamps of data to analyze. Shape is nsamp.
102
+
103
+ Returns
104
+ -------
105
+ a_peak : float
106
+ Amplitudes of sine wave of the desired frequency. Shape is len(x)
107
+ phase : float
108
+ Phase of sine wave of the desired frequency. Shape is len(x)
109
+ """
110
+
111
+ vects = np.zeros((2, len(ts)), dtype='float32')
112
+ vects[0, :] = np.sin(2*np.pi*f_c*ts)
113
+ vects[1, :] = np.cos(2*np.pi*f_c*ts)
114
+ I = np.linalg.inv(np.tensordot(vects, vects, (1, 1)))
115
+ coeffs = np.matmul(x, vects.T)
116
+ coeffs = np.dot(I, coeffs.T).T
117
+ coeffs = np.atleast_2d(coeffs)
118
+
119
+ a_peak = np.sqrt(coeffs[:,0]**2 + coeffs[:,1]**2)
120
+ phase = np.arctan2(coeffs[:,0], coeffs[:,1])
121
+
122
+ return a_peak, phase
123
+
124
+ class BiasWaveAnalysis:
125
+ """
126
+ Container to manage analysis of bias waves taken with the take_bias_waves
127
+ function. The main function is ``run_analysis`` and will do a series of
128
+ analysis procedures to calculate DC detector parameters from a single bias
129
+ wave frequency. The method will be extended to calculate tau_eff from multiple
130
+ frequencies. Procedure:
131
+
132
+ - Loads an axis manager with all the data
133
+ - Finds locations of bias wave frequencies for each bias group
134
+ - Gets detector response to each bias wave
135
+ - Computes DC params R0, I0, Pj, Si from step responses
136
+ - Later: fit multiple TES amplitudes with one-pole filter for tau_eff measurement.
137
+
138
+ Most analysis inputs and products will be saved to a npy file so they can
139
+ be loaded and re-analyzed easily on another computer like simons1.
140
+
141
+ To load data from an saved step file, you can run::
142
+
143
+ bwa = BiasWaveAnalysis.load(<path>)
144
+
145
+ Attributes:
146
+ bands (array (int) of shape (ndets)):
147
+ Smurf bands for each resonator.
148
+ channels (array (int) of shape (ndets)):
149
+ Smurf channel for each resonator.
150
+ meta (dict) :
151
+ Dictionary of smurf metadata.
152
+ sid (int) :
153
+ Session-id of streaming session
154
+ run_kwargs :
155
+ input kwargs
156
+ start, stop (float):
157
+ start and stop time of all steps
158
+ high_current_mode (Bool) :
159
+ If high-current-mode was used
160
+ start_times, stop_times (array (float) of shape (nbgs, nfreqs)) :
161
+ Arrays of start and stop times for the start of each frequency sine wave.
162
+ bgmap (array (int) of shape (nchans)):
163
+ Map from readout channel to assigned bias group. -1 means not
164
+ assigned (that the assignment threshold was not met for any of the
165
+ 12 bgs)
166
+ polarity (array (int) of shape (ndets)):
167
+ The sign of each channel.
168
+ resp_times (array (float) shape (nbgs, nfreqs, npts)):
169
+ Shared timestamps for each of the wave responses in <wave_resp> and
170
+ <mean_resp> with respect to the bg-wave location, with the wave
171
+ occuring at t=0.
172
+ mean_resp (array (float) shape (nchans, nfreqs, npts)):
173
+ Wave response averaged accross all bias steps for a given channel
174
+ in Amps.
175
+ wave_resp (array (float) shape (nchans, nfreqs, npts)):
176
+ Each individual wave response for a given channel in amps.
177
+ wave_biases (array (float) shape (nbgs, nfreqs, npts)):
178
+ Each individual bias wave for a given channel.
179
+ Ibias (array (float) shape (nbgs)):
180
+ DC bias current of each bias group (amps). DC Params are
181
+ only calculated off of the minimum frequency in the array of frequencies.
182
+ Vbias:
183
+ DC bias voltage of each bias group (volts in low-current mode)
184
+ dIbias (array (float) shape (nbgs)):
185
+ Wave current amplitude for each bias group (amps)
186
+ dVbias (array (float) shape (nbgs)):
187
+ Wave voltage amplitude for each bias group (volts in low-current mode)
188
+ dItes (array (float) shape (nchans)):
189
+ Array of tes wave response height for each channel (amps)
190
+ R0 (array (float) shape (nchans)):
191
+ Computed TES resistances for each channel (ohms).
192
+ I0 (array (float) shape (nchans)):
193
+ Computed TES currents for each channel (amps)
194
+ Pj (array (float) shape (nchans)):
195
+ Bias power computed for each channel
196
+ Si (array (float) shape (nchans)):
197
+ Responsivity computed for each channel
198
+ R_n_IV (array (float) shape (nchans)):
199
+ Array of normal resistances for each channel pulled from IV
200
+ in the device cfg.
201
+ Rfrac (array (float) shape (nchans)):
202
+ Rfrac of each channel, determined from R0 and the channel's normal
203
+ resistance.
204
+ """
205
+ def __init__(self, S=None, cfg=None, bgs=None, run_kwargs=None):
206
+ self._S = S
207
+ self._cfg = cfg
208
+
209
+ self.bgs = bgs
210
+ self.am = None
211
+
212
+ if S is not None:
213
+ self.meta = sdl.get_metadata(S, cfg)
214
+ self.stream_id = cfg.stream_id
215
+
216
+ if run_kwargs is None:
217
+ run_kwargs = {}
218
+ self.run_kwargs = run_kwargs
219
+ self.high_current_mode = run_kwargs.get("high_current_mode", True)
220
+
221
+ def save(self, path=None):
222
+ data = {}
223
+ saved_fields = [
224
+ # Run data and metadata
225
+ 'bands', 'channels', 'sid', 'meta', 'run_kwargs', 'start', 'stop',
226
+ 'high_current_mode', 'start_times', 'stop_times',
227
+ # Bgmap data
228
+ 'bgmap', 'polarity',
229
+ # Step data and fits, including chunked bias data
230
+ 'resp_times', 'mean_resp', 'wave_resp', 'wave_biases',
231
+ # Add in tau fit stuff here. The below commented out params are anticipated to be reported for tau analysis.
232
+ # 'step_fit_tmin', 'step_fit_popts', 'step_fit_pcovs',
233
+ # 'tau_eff',
234
+ # Det param data
235
+ 'Ibias', 'Vbias', 'dIbias', 'dVbias', 'dItes',
236
+ 'R0', 'I0', 'Pj', 'Si',
237
+ # From IV's
238
+ 'R_n_IV', 'Rfrac',
239
+ ]
240
+
241
+ for f in saved_fields:
242
+ if not hasattr(self, f):
243
+ print(f"WARNING: field {f} does not exist... "
244
+ "defaulting to None")
245
+ data[f] = getattr(self, f, None)
246
+
247
+ if path is not None:
248
+ np.save(path, data, allow_pickle=True)
249
+ self.filepath = path
250
+ else:
251
+ self.filepath = sdl.validate_and_save(
252
+ 'bias_wave_analysis.npy', data, S=self._S, cfg=self._cfg,
253
+ make_path=True
254
+ )
255
+
256
+ @classmethod
257
+ def load(cls, filepath):
258
+ self = cls()
259
+ data = np.load(filepath, allow_pickle=True).item()
260
+ for k, v in data.items():
261
+ setattr(self, k, v)
262
+ self.filepath = filepath
263
+ return self
264
+
265
+ def run_analysis(self, arc=None, base_dir='/data/so/timestreams',
266
+ R0_thresh=30e-3, save=False, bg_map_file=None):
267
+ """
268
+ Analyzes data taken with take_bias_waves.
269
+
270
+ Parameters
271
+ ----------
272
+ arc (optional, G3tSmurf):
273
+ G3tSmurf archive. If specified, will attempt to load
274
+ axis-manager using archive instead of sid.
275
+ base_dir (optiional, str):
276
+ Base directory where timestreams are stored. Defaults to
277
+ /data/so/timestreams.
278
+ R0_thresh (float):
279
+ Any channel with resistance greater than R0_thresh will be
280
+ unassigned from its bias group under the assumption that it's
281
+ crosstalk
282
+ save (bool):
283
+ If true will save the analysis to a npy file.
284
+ bg_map_file (optional, path):
285
+ If create_bg_map is false and this file is not None, use this file
286
+ to load the bg_map.
287
+
288
+ CURRENTLY ONLY INCLUDES CALCULATION OF DC PARAMETERS.
289
+ NO TIME CONSTANT FITS.
290
+ """
291
+ self._load_am(arc=arc, base_dir=base_dir)
292
+ self._split_frequencies()
293
+ if bg_map_file is not None:
294
+ self.bgmap, self.polarity = sdl.load_bgmap(
295
+ self.bands, self.channels, bg_map_file)
296
+ else:
297
+ self.bgmap, self.polarity = sdl.load_bgmap(
298
+ self.bands, self.channels, self.meta['bgmap_file'])
299
+
300
+ # GOT TO HERE
301
+ self._get_wave_response()
302
+ self._compute_dc_params(R0_thresh=R0_thresh)
303
+
304
+ # Load R_n from IV
305
+ self.R_n_IV = np.full(self.nchans, np.nan)
306
+ # Rfrac determined from R0 and R_n
307
+ self.Rfrac = np.full(self.nchans, np.nan)
308
+ if self.meta['iv_file'] is not None:
309
+ if os.path.exists(self.meta['iv_file']):
310
+ iva = iv.IVAnalysis.load(self.meta['iv_file'])
311
+ chmap = sdl.map_band_chans(
312
+ self.bands, self.channels, iva.bands, iva.channels
313
+ )
314
+ self.R_n_IV = iva.R_n[chmap]
315
+ self.R_n_IV[chmap == -1] = np.nan
316
+ self.Rfrac = self.R0 / self.R_n_IV
317
+
318
+ # Can add in function for fitting time constant from multifrequency data here.
319
+ # self._fit_tau_effs(tmin=fit_tmin)
320
+
321
+ if save:
322
+ self.save()
323
+
324
+ def _load_am(self, arc=None, base_dir='/data/so/timestreams', fix_timestamps=True):
325
+ """
326
+ Attempts to load the axis manager from the sid or return one that's
327
+ already loaded. Also sets the `abs_chans` array.
328
+ """
329
+ if self.am is None:
330
+ if arc:
331
+ self.am = arc.load_data(self.start, self.stop, stream_id=self.meta['stream_id'])
332
+ else:
333
+ self.am = sdl.load_session(self.meta['stream_id'], self.sid,
334
+ base_dir=base_dir)
335
+
336
+ # Fix up timestamp jitter from timestamping in software
337
+ if fix_timestamps:
338
+ fsamp, t0 = np.polyfit(self.am.primary['FrameCounter'],
339
+ self.am.timestamps, 1)
340
+ self.am.timestamps = t0 + self.am.primary['FrameCounter']*fsamp
341
+ if "det_info" in self.am:
342
+ self.bands = self.am.det_info.smurf.band
343
+ self.channels = self.am.det_info.smurf.channel
344
+ else:
345
+ self.bands = self.am.ch_info.band
346
+ self.channels = self.am.ch_info.channel
347
+ self.abs_chans = self.bands*512 + self.channels
348
+ self.nbgs = len(self.am.biases)
349
+ self.nchans = len(self.am.signal)
350
+ return self.am
351
+
352
+ def _split_frequencies(self, am=None):
353
+ """
354
+ Gets indices for each sine wave frequency on each bias group.
355
+
356
+ Returns
357
+ -------
358
+ start_idxs : float array
359
+ Array of indices for the start of each frequency sine wave.
360
+ Shape (n_bias_groups, n_frequencies).
361
+ stop_idxs : float array
362
+ Array of indices for the end of each frequency sine wave.
363
+ Shape (n_bias_groups, n_frequencies).
364
+ """
365
+ if am is None:
366
+ am = self.am
367
+
368
+ self.start_idxs = np.full(np.shape(self.start_times), -1)
369
+ self.stop_idxs = np.full(np.shape(self.stop_times), -1)
370
+
371
+ for bg, bias in enumerate(am.biases[:12]):
372
+ if np.all(np.isnan(self.start_times[bg,:])):
373
+ continue
374
+ for i, [start, stop] in enumerate(zip(self.start_times[bg,:], self.stop_times[bg,:])):
375
+ self.start_idxs[bg, i] = np.argmin(np.abs(am.timestamps - start))
376
+ self.stop_idxs[bg, i] = np.argmin(np.abs(am.timestamps - stop))
377
+
378
+ return self.start_idxs, self.stop_idxs
379
+
380
+ def _get_wave_response(self, am=None):
381
+ """
382
+ Splits up full axis manager into each sine wave.
383
+ Here we enforce the number of points in each sine wave to be equal.
384
+
385
+ Returns
386
+ -------
387
+ ts : float array
388
+ Array of timestamps for each sine wave period
389
+ Shape (n_bias_groups, n_frequencies, n_pts_in_sine_wave).
390
+ sigs : float array
391
+ Array of sine response data.
392
+ Shape (n_detectors, n_frequencies, n_pts_in_sine_wave).
393
+ UPDATE THE DOCSTRING
394
+ """
395
+ if am is None:
396
+ am = self.am
397
+
398
+ nchans = len(am.signal)
399
+ nbgs = 12
400
+ n_freqs = np.shape(self.start_idxs)[-1]
401
+ npts = np.nanmin(self.stop_idxs-self.start_idxs)
402
+
403
+ sigs = np.full((nchans, n_freqs, npts), np.nan)
404
+ biases = np.full((nbgs, n_freqs, npts), np.nan)
405
+ ts = np.full((nbgs, n_freqs, npts), np.nan)
406
+
407
+ A_per_rad = self.meta['pA_per_phi0'] / (2*np.pi) * 1e-12
408
+ for bg in np.unique(self.bgmap):
409
+ if bg == -1:
410
+ continue
411
+ rcs = np.where(self.bgmap == bg)[0]
412
+ for i, si in enumerate(self.start_idxs[bg]):
413
+ if np.isnan(ts[bg,i]).all():
414
+ ts[bg, i, :] = am.timestamps[si:si+npts] - am.timestamps[si]
415
+ sigs[rcs, i, :] = am.signal[rcs, si:si+npts] * A_per_rad
416
+ biases[bg, i, :] = am.biases[bg, si:si+npts]
417
+
418
+ self.resp_times = ts
419
+ self.wave_resp = (sigs.T * self.polarity).T
420
+ self.mean_resp = (np.nanmean(sigs, axis=1).T * self.polarity).T
421
+ self.wave_biases = biases
422
+
423
+ return ts, sigs, biases
424
+
425
+ def _compute_dc_params(self, R0_thresh=30e-3):
426
+ """
427
+ Calculates Ibias, dIbias, and dItes from axis manager, and then
428
+ runs the DC param calc to estimate R0, I0, Pj, etc.
429
+ If multiple frequency are taken, the DC Params are only calculated
430
+ off of the minimum frequency in the array of frequencies.
431
+
432
+ Args:
433
+ R0_thresh: (float)
434
+ Any channel with resistance greater than R0_thresh will be
435
+ unassigned from its bias group under the assumption that it's
436
+ crosstalk
437
+
438
+ Saves:
439
+ Ibias:
440
+ Array of shape (nbgs) containing the DC bias current for each
441
+ bias group
442
+ Vbias:
443
+ Array of shape (nbgs) containing the DC bias voltage for each
444
+ bias group
445
+ dIbias:
446
+ Array of shape (nbgs) containing the wave current amplitude for
447
+ each bias group
448
+ dVbias:
449
+ Array of shape (nbgs) containing the wave voltage amplitude for
450
+ each bias group
451
+ dItes:
452
+ Array of shape (nchans) containing the wave current amplitude for
453
+ each detector.
454
+ """
455
+ nbgs = 12
456
+ nchans = len(self.am.signal)
457
+ npts = np.nanmin(self.stop_idxs-self.start_idxs)
458
+
459
+ Ibias = np.full(nbgs, np.nan)
460
+ dIbias = np.full(nbgs, 0.0, dtype=float)
461
+ dItes = np.full(nchans, np.nan)
462
+
463
+ dIbias_phase = np.full(nbgs, 0.0, dtype=float)
464
+ dItes_phase = np.full(nchans, np.nan)
465
+
466
+ # Compute Ibias and dIbias
467
+ bias_line_resistance = self.meta['bias_line_resistance']
468
+ high_low_current_ratio = self.meta['high_low_current_ratio']
469
+ rtm_bit_to_volt = self.meta['rtm_bit_to_volt']
470
+ amp_per_bit = 2 * rtm_bit_to_volt / bias_line_resistance
471
+ if self.high_current_mode:
472
+ amp_per_bit *= high_low_current_ratio
473
+
474
+ for bg in range(nbgs):
475
+ if len(self.start_idxs[bg]) == 0:
476
+ continue
477
+ rcs = np.where(self.bgmap == bg)[0]
478
+ s = slice(int(self.start_idxs[bg][0]),
479
+ int(self.start_idxs[bg][0] + npts))
480
+ Ibias[bg] = np.nanmean(self.am.biases[bg, s]) * amp_per_bit
481
+
482
+ dIbias[bg], dIbias_phase[bg] = get_amplitudes_deprojection(self.run_kwargs['freqs_wave'][0],
483
+ self.am.biases[bg, s], self.resp_times[bg, 0, :])
484
+ dIbias[bg] = dIbias[bg] * amp_per_bit
485
+
486
+ dItes[rcs], dItes_phase[rcs] = get_amplitudes_deprojection(self.run_kwargs['freqs_wave'][0],
487
+ self.wave_resp[rcs,0,:], self.resp_times[bg, 0, :])
488
+
489
+ self.Ibias = Ibias
490
+ self.Vbias = Ibias * bias_line_resistance
491
+ self.dIbias = dIbias
492
+ self.dIbias_phase = dIbias_phase
493
+ self.dVbias = dIbias * bias_line_resistance
494
+ self.dItes = dItes
495
+ self.dItes_phase = dItes_phase
496
+
497
+ R0, I0, Pj = self._compute_R0_I0_Pj()
498
+
499
+ Si = -1./(I0 * (R0 - self.meta['R_sh']))
500
+
501
+ # If resistance is too high, most likely crosstalk so just reset
502
+ # bg mapping and det params
503
+ if R0_thresh is not None:
504
+ m = np.abs(R0) > R0_thresh
505
+ self.bgmap[m] = -1
506
+ for arr in [R0, I0, Pj, Si]:
507
+ arr[m] = np.nan
508
+
509
+ self.R0 = R0
510
+ self.I0 = I0
511
+ self.Pj = Pj
512
+ self.Si = Si
513
+
514
+ return R0, I0, Pj, Si
515
+
516
+ def _compute_R0_I0_Pj(self):
517
+ """
518
+ Computes the DC params R0 I0 and Pj
519
+
520
+ Carbon copy from BiasStepAnalysis
521
+ """
522
+ Ib = self.Ibias[self.bgmap]
523
+ dIb = self.dIbias[self.bgmap]
524
+ dItes = self.dItes
525
+
526
+ dIb_phase = self.dIbias_phase[self.bgmap]
527
+ dItes_phase = self.dItes_phase
528
+
529
+ Ib[self.bgmap == -1] = np.nan
530
+ dIb[self.bgmap == -1] = np.nan
531
+
532
+ #sign of phase response, is there a better way to do it?
533
+ rel_phase = dItes_phase - dIb_phase
534
+ rel_phase = np.isclose(abs(rel_phase), np.pi, atol=3e-1, rtol = 1e-1) #arbitrary cutoff?
535
+ rel_phase_sign = np.full(rel_phase.shape, 1.0)
536
+ rel_phase_sign[np.where(rel_phase == True)] = -1.0
537
+
538
+ dIrat = rel_phase_sign * (dItes / dIb)
539
+
540
+ R_sh = self.meta["R_sh"]
541
+
542
+ I0 = np.zeros_like(dIrat)
543
+ I0_nontransition = Ib * dIrat
544
+ I0_transition = Ib * dIrat / (2 * dIrat - 1)
545
+ I0[dIrat>0] = I0_nontransition[dIrat>0]
546
+ I0[dIrat<0] = I0_transition[dIrat<0]
547
+
548
+ Pj = I0 * R_sh * (Ib - I0)
549
+ R0 = Pj / I0**2
550
+ R0[I0 == 0] = 0
551
+
552
+ return R0, I0, Pj
553
+
554
+ @sdl.set_action()
555
+ def take_bias_waves(S, cfg, bgs=None, amp_wave=0.05, freqs_wave=[23.0],
556
+ duration=10, high_current_mode=True, hcm_wait_time=3,
557
+ run_analysis=True, analysis_kwargs=None, channel_mask=None,
558
+ g3_tag=None, stream_subtype='bias_waves',
559
+ enable_compression=False, plot_rfrac=True, show_plots=False):
560
+ """
561
+ Takes bias wave data at the current DC voltage. Assumes bias lines
562
+ are already in low-current mode (if they are in high-current this will
563
+ not run correction). This function runs bias waves and returns a
564
+ BiasWaveAnalysis object, which can be used to easily view and re-analyze
565
+ data.
566
+
567
+ Parameters:
568
+ S (SmurfControl):
569
+ Pysmurf control instance
570
+ cfg (DetConfig):
571
+ Detconfig instance
572
+ bgs ( int, list, optional):
573
+ Bias groups to run steps on, defaulting to all 12. Note that the
574
+ bias-group mapping generated by the bias step analysis will be
575
+ restricted to the bgs set here so if you only run with a small
576
+ subset of bias groups, the map might not be correct.
577
+ amp_wave (float):
578
+ Bias wave amplitude voltage in Low-current-mode units.
579
+ This will be divided by the high-low-ratio before running the steps
580
+ in high-current mode.
581
+ freqs_wave (float):
582
+ List of frequencies to take bias wave data at.
583
+ duration (float):
584
+ Duration in seconds of bias wave frequency
585
+ high_current_mode (bool):
586
+ If true, switches to high-current-mode. If False, leaves in LCM
587
+ which runs through the bias-line filter, so make sure you
588
+ extend the step duration to be like >2 sec or something
589
+ hcm_wait_time (float):
590
+ Time to wait after switching to high-current-mode.
591
+ run_analysis (bool):
592
+ If True, will attempt to run the analysis to calculate DC params
593
+ and tau_eff. If this fails, the analysis object will
594
+ still be returned but will not contain all analysis results.
595
+ analysis_kwargs (dict, optional):
596
+ Keyword arguments to be passed to the BiasWaveAnalysis run_analysis
597
+ function.
598
+ channel_mask : np.ndarray, optional
599
+ Mask containing absolute smurf-channels to write to disk
600
+ g3_tag: string, optional
601
+ Tag to attach to g3 stream.
602
+ stream_subtype : optional, string
603
+ Stream subtype for this operation. This will default to 'bias_waves'.
604
+ enable_compression: bool, optional
605
+ If True, will tell the smurf-streamer to compress G3Frames. Defaults
606
+ to False because this dominates frame-processing time for high
607
+ data-rate streams.
608
+ plot_rfrac : bool
609
+ Create rfrac plot, publish it, and save it. Default is True.
610
+ show_plots : bool
611
+ Show plot in addition to saving when running interactively. Default is False.
612
+ """
613
+ if bgs is None:
614
+ bgs = cfg.dev.exp['active_bgs']
615
+ bgs = np.atleast_1d(bgs)
616
+
617
+ # Dumb way to get all run kwargs, but we probably want to save these in
618
+ # data object
619
+ freqs_wave = np.sort(np.atleast_1d(freqs_wave)) # Enforces lowest frequency first.
620
+ run_kwargs = {
621
+ 'bgs': bgs, 'amp_wave': amp_wave,
622
+ 'freqs_wave': freqs_wave, 'duration': duration,
623
+ 'high_current_mode': high_current_mode,
624
+ 'hcm_wait_time': hcm_wait_time, 'run_analysis': run_analysis,
625
+ 'analysis_kwargs': analysis_kwargs, 'channel_mask': channel_mask,
626
+ }
627
+
628
+ initial_ds_factor = S.get_downsample_factor()
629
+ initial_filter_disable = S.get_filter_disable()
630
+ initial_dc_biases = S.get_tes_bias_bipolar_array()
631
+
632
+ try:
633
+ dc_biases = initial_dc_biases
634
+ init_current_mode = sdl.get_current_mode_array(S)
635
+ if high_current_mode:
636
+ dc_biases = dc_biases / S.high_low_current_ratio
637
+ amp_wave /= S.high_low_current_ratio
638
+ sdl.set_current_mode(S, bgs, 1)
639
+ S.log(f"Waiting {hcm_wait_time} sec after switching to hcm")
640
+ time.sleep(hcm_wait_time)
641
+
642
+ bwa = BiasWaveAnalysis(S, cfg, bgs, run_kwargs=run_kwargs)
643
+
644
+ bwa.sid = sdl.stream_g3_on(
645
+ S, tag=g3_tag, channel_mask=channel_mask, downsample_factor=1,
646
+ filter_disable=True, subtype=stream_subtype, enable_compression=enable_compression
647
+ )
648
+
649
+ bwa.start_times = np.full((12, len(freqs_wave)), np.nan)
650
+ bwa.stop_times = np.full((12, len(freqs_wave)), np.nan)
651
+
652
+ bwa.start = time.time()
653
+
654
+ for bg in bgs:
655
+ bwa.start_times[bg,:], bwa.stop_times[bg,:] = play_bias_wave(S, cfg, bg,
656
+ freqs_wave,
657
+ amp_wave, duration)
658
+
659
+ bwa.stop = time.time()
660
+
661
+ finally:
662
+ sdl.stream_g3_off(S)
663
+
664
+ # Restores current mode to initial values
665
+ sdl.set_current_mode(S, np.where(init_current_mode == 0)[0], 0)
666
+ sdl.set_current_mode(S, np.where(init_current_mode == 1)[0], 1)
667
+
668
+ S.set_downsample_factor(initial_ds_factor)
669
+ S.set_filter_disable(initial_filter_disable)
670
+
671
+ if run_analysis:
672
+ S.log("Running bias wave analysis")
673
+ try:
674
+ if analysis_kwargs is None:
675
+ analysis_kwargs = {}
676
+ bwa.run_analysis(save=True, **analysis_kwargs)
677
+ if plot_rfrac:
678
+ fig, _ = bias_steps.plot_Rfrac(bwa)
679
+ path = sdl.make_filename(S, 'bw_rfrac_summary.png', plot=True)
680
+ fig.savefig(path)
681
+ S.pub.register_file(path, 'bw_rfrac_summary', plot=True, format='png')
682
+ if not show_plots:
683
+ plt.close(fig)
684
+ except Exception:
685
+ print(f"Bias wave analysis failed with exception:")
686
+ print(traceback.format_exc())
687
+
688
+ return bwa