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,651 @@
1
+ import numpy as np
2
+ from tqdm.auto import tqdm, trange
3
+ import time
4
+ import numpy as np
5
+ import sodetlib as sdl
6
+ from sodetlib.constants import *
7
+ from scipy.signal import welch, hilbert
8
+ from scipy.optimize import minimize
9
+ import matplotlib.pyplot as plt
10
+ from sotodlib.tod_ops.filters import gaussian_filter, fourier_filter
11
+ from sotodlib.core import IndexAxis, AxisManager, OffsetAxis, LabelAxis
12
+
13
+
14
+ def new_ci_dset(S, cfg, bands, chans, freqs, run_kwargs=None, ob_path=None,
15
+ sc_path=None):
16
+ """
17
+ Creates a new CIData AxisManager. If ob_path and sc_path are set, they
18
+ will be loaded with a remapped "dets" axis.
19
+ """
20
+ ndets = len(chans)
21
+ nsteps = len(freqs)
22
+
23
+ ds = AxisManager(
24
+ LabelAxis('dets', vals=[f"r{x:0>4}" for x in range(ndets)]),
25
+ IndexAxis('steps', count=nsteps),
26
+ IndexAxis('biaslines', count=NBGS),
27
+ )
28
+ ds.wrap('meta', sdl.dict_to_am(sdl.get_metadata(S, cfg), skip_bad_types=True))
29
+ ds.meta.wrap('g3_dir', cfg.sys['g3_dir'])
30
+ if run_kwargs is not None:
31
+ ds.wrap('run_kwargs', sdl.dict_to_am(run_kwargs))
32
+
33
+ ds.wrap('bands', bands, [(0, 'dets')])
34
+ ds.wrap('channels', chans, [(0, 'dets')])
35
+ ds.wrap('freqs', freqs, [(0, 'steps')])
36
+
37
+ ds.wrap_new('start_times', ('biaslines', 'steps'))
38
+ ds.wrap_new('stop_times', ('biaslines', 'steps'))
39
+ ds.wrap_new('sids', ('biaslines',), dtype=int)
40
+
41
+ # Load bgmap stuff
42
+ bgmap, polarity = sdl.load_bgmap(
43
+ bands, chans, cfg.dev.exp['bgmap_file']
44
+ )
45
+ ds.wrap('bgmap', bgmap, [(0, 'dets')])
46
+ ds.wrap('polarity', polarity, [(0, 'dets')])
47
+
48
+
49
+ ob_path = cfg.dev.exp.get('complex_impedance_ob_path')
50
+ sc_path = cfg.dev.exp.get('complex_impedance_sc_path')
51
+ if ds.run_kwargs.state == 'transition':
52
+ if ob_path is None:
53
+ raise ValueError("No ob CI path found in the device cfg")
54
+ if sc_path is None:
55
+ raise ValueError("No sc CI path found in the device cfg")
56
+
57
+ ob = sdl.remap_dets(AxisManager.load(ob_path), ds, load_axes=False)
58
+ ds.wrap('ob', ob)
59
+
60
+ sc = sdl.remap_dets(AxisManager.load(sc_path), ds, load_axes=False)
61
+ ds.wrap('sc', sc)
62
+
63
+ return ds
64
+
65
+ def A_per_bit(ds):
66
+ return 2 * ds.meta['rtm_bit_to_volt'] \
67
+ / ds.meta['bias_line_resistance'] \
68
+ * ds.meta['high_low_current_ratio']
69
+
70
+ def load_tod(ds, bg, arc=None):
71
+ """
72
+ Loads a TOD for a biasgroup given a CI dset.
73
+
74
+ Args
75
+ -----
76
+ ds : AxisManager
77
+ CI Dataset
78
+ bg : int
79
+ Bias group to load data for
80
+ arc : G3tSmurf, optional
81
+ If a G3tSmurf archive is passed this will be used to load data.
82
+ """
83
+ if arc is not None:
84
+ start = ds.start_times[bg, 0]
85
+ stop = ds.stop_times[bg, -1]
86
+ seg = arc.load_data(start, stop, show_pb=False)
87
+ else:
88
+ sid = ds.sids[bg]
89
+ seg = sdl.load_session(ds.meta.stream_id, sid, base_dir=ds.meta.g3_dir)
90
+ return seg
91
+
92
+ ################################################################################
93
+ # CI Analysis
94
+ ################################################################################
95
+ def analyze_seg(ds, tod, bg, i):
96
+ """
97
+ Analyze segment of CI data. The main goal of this is to calculate
98
+ the Ites phasor, containing the amplitude (A) and phase (relative
99
+ tot he commanded) of the TES response to an incoming sine wave.
100
+ This performs the following steps:
101
+ 1. Restrict full TOD to a single excitation frequency. This will put
102
+ everything units of A, correct for channel polarity. This will also
103
+ correct timestamps based on the FrameCounter.
104
+ 2. Takes PSD of the bias data to obtain the reference freq. Filters signal
105
+ using gaussian filter around reference freq.
106
+ 3. Use lock-in amplification with bias as reference to extract
107
+ amplitude and phase of the filtered signal with respect to the
108
+ commanded bias.
109
+
110
+ Args
111
+ -----
112
+ ds : AxisManager
113
+ CI Dataset
114
+ tod : AxisManger
115
+ axis-manager containing tod for a given bias group
116
+ bg : int
117
+ Bias group that is being analyzed
118
+ i : int
119
+ Freq index. 0 will analyze the first freq segment taken.
120
+
121
+ Returns
122
+ ---------
123
+ am : AxisManager
124
+ Returns an souped-up tod axis-manager corresponding to this freq with
125
+ the following fields:
126
+ - timestmaps, biases, signal, ch_info. Standard tod axismanager stuff
127
+ but with units converted into A, timestamps fixed, and offsets
128
+ subtracted out.
129
+ - sample_rate, cmd_freq: Floats with the sample rate and commanded
130
+ freq
131
+ - filt_sig: Filtered signal (using gaussian filter around commanded
132
+ freq)
133
+ - lockin_x, lockin_y: Lockin x and y signals, used to calc amp and phase
134
+ across the tod
135
+ - Ites: Phasor for Ites. Amplitude is the amp of the sine wave response,
136
+ and angle is the phase relative to the commanded bias.
137
+ """
138
+ t0, t1 = ds.start_times[bg, i], ds.stop_times[bg, i]
139
+ am = sdl.restrict_to_times(tod, t0, t1, in_place=False)
140
+
141
+ sample_rate = 1./np.median(np.diff(am.timestamps))
142
+
143
+ # Convert everything to A
144
+ am.signal = am.signal * ds.meta['pA_per_phi0'] / (2*np.pi) * 1e-12
145
+
146
+ # Index mapping from am readout channel to sweep chan index.
147
+ chan_idxs = sdl.map_band_chans(
148
+ am.ch_info.band, am.ch_info.channel,
149
+ ds.bands, ds.channels
150
+ )
151
+ am.signal *= ds.polarity[chan_idxs, None]
152
+
153
+ am.biases = am.biases * A_per_bit(ds)
154
+
155
+ # Remove offset from signal
156
+ am.signal -= np.mean(am.signal, axis=1)[:, None]
157
+ am.biases -= np.mean(am.biases, axis=1)[:, None]
158
+
159
+ # Fix up timestamps based on frame-counter
160
+ t0, t1 = am.timestamps[0], am.timestamps[-1]
161
+ fc = am.primary["FrameCounter"]
162
+ fc = fc - fc[0]
163
+ ts = t0 + fc/fc[-1] * (t1 - t0)
164
+ am.timestamps = ts
165
+ am.wrap('sample_rate', sample_rate)
166
+ am.wrap('cmd_freq', ds.freqs[i])
167
+
168
+ # Get psds cause we'll want that
169
+ nsamp = len(am.signal[0])
170
+ fxx, bias_pxx = welch(am.biases[bg], fs=sample_rate, nperseg=nsamp)
171
+
172
+ # Gaussian filter around peak freq of bias asd.
173
+ f0, f1 = .9 * am.cmd_freq, 1.1*am.cmd_freq
174
+ m = (f0 < fxx) & (fxx < f1)
175
+
176
+ idx = np.argmax(bias_pxx[m])
177
+ f = fxx[m][idx]
178
+
179
+ filt = gaussian_filter(f, f_sigma=f / 5)
180
+ filt_sig = fourier_filter(am, filt)
181
+ am.wrap('filt_sig', filt_sig, [(0, 'dets'), (1, 'samps')])
182
+
183
+ # Lock in amplification!
184
+ # To get ref + ref offset by 90-deg, take Hilbert transform, and then
185
+ # real part gives you ref and imag part is offset by 90 deg.
186
+ sig = filt_sig
187
+ ref = hilbert(am.biases[bg] / np.max(am.biases[bg]))
188
+ X = sig * ref.real
189
+ Y = sig * ref.imag
190
+ # We're averaging over enough periods where we don't really need to
191
+ # restrict to an int number of periods...
192
+ xmean = np.mean(X, axis=1)
193
+ ymean = np.mean(Y, axis=1)
194
+ phase = -np.arctan2(ymean, xmean)
195
+ amp = 2*np.sqrt(xmean**2 + ymean**2)
196
+ Ites = amp * np.exp(1.0j * phase)
197
+ am.wrap('lockin_x', X)
198
+ am.wrap('lockin_y', Y)
199
+ am.wrap('Ites', Ites)
200
+ return am
201
+
202
+ def analyze_tods(ds, bgs=None, tod=None, arc=None, show_pb=True):
203
+ """
204
+ Analyzes TODS for a CIData set. This will add the following fields to the
205
+ dataset:
206
+ - Ites(dets, steps): Ites phasor for each detector / frequency
207
+ combination. Amplitude is the amp of the current response (A), and angle
208
+ is the phase relative to commanded bias.
209
+ - Ibias(biaslines): Amplitude (A) of the sinewave used for each
210
+ biasline.
211
+ - Ibias_dc(biaslines): DC bias current (A) for each biasline
212
+ - res_freqs(dets): Resonance frequency of each channel detectors.
213
+ """
214
+ if bgs is None:
215
+ bgs = ds.run_kwargs.bgs
216
+ bgs = np.atleast_1d(bgs)
217
+
218
+ # Delete temp fields if they exist
219
+ for f in ['_Ites', '_Ibias', '_Ibias_dc', '_res_freqs']:
220
+ if f in ds._fields:
221
+ ds.move(f, None)
222
+
223
+ nsteps = len(ds.freqs)
224
+ ds.wrap_new('_Ites', ('dets', nsteps), cls=np.full,
225
+ fill_value=np.nan, dtype=np.complex128)
226
+ ds.wrap_new('_Ibias', (NBGS,), cls=np.full, fill_value=np.nan)
227
+ ds.wrap_new('_Ibias_dc', (NBGS,), cls=np.full, fill_value=np.nan)
228
+ ds.wrap_new('_res_freqs', ('dets',), cls=np.full, fill_value=np.nan)
229
+
230
+ ntot = len(bgs) * len(ds.freqs)
231
+ pb = tqdm(total=ntot, disable=(not show_pb))
232
+ for bg in bgs:
233
+ if tod is None:
234
+ pb.set_description(f"Loading tod for bg {bg}")
235
+ _tod = load_tod(ds, bg, arc=arc)
236
+ else:
237
+ _tod = tod
238
+ chmap = sdl.map_band_chans(
239
+ _tod.ch_info.band, _tod.ch_info.channel,
240
+ ds.bands, ds.channels
241
+ )
242
+
243
+ pb.set_description(f"Analyzing segments for bg {bg}")
244
+ for i in range(len(ds.freqs)):
245
+ try:
246
+ seg = analyze_seg(ds, _tod, bg, i)
247
+ except sdl.RestrictionException:
248
+ # Means there's no data at the specified time
249
+ pb.update()
250
+ continue
251
+ if i == 0:
252
+ ds._Ibias_dc[bg] = np.mean(seg.biases[bg])
253
+ ds._Ibias[bg] = 0.5 * np.ptp(seg.biases[bg])
254
+ ds._res_freqs[chmap] = seg.ch_info.res_frequency
255
+
256
+ ds._Ites[chmap, i] = seg.Ites
257
+ pb.update()
258
+ del _tod
259
+
260
+ for f in ['_Ites', '_Ibias', '_Ibias_dc', '_res_freqs']:
261
+ ds.move(f, f[1:])
262
+
263
+ return ds
264
+
265
+ def get_ztes(ds):
266
+ """
267
+ Calculates Ztes for in-transition CIData. Adds the following fields to the
268
+ CI dataset:
269
+ - Rn (dets): Normal resistances based off of low-f overbiased data points.
270
+ - Rtes (dets): TES Resistance, based off of low-f in-transition segment
271
+ - Vth (dets): Thevenin equiv voltage (V)
272
+ - Zeq (dets): Equiv impedance
273
+ - Ztes (dets): TES complex impedance
274
+ """
275
+ ob, sc = ds.ob, ds.sc
276
+
277
+ fields = ['_Rn', '_Rtes', '_Vth', '_Zeq', '_Ztes']
278
+ for f in fields:
279
+ if f in ds._fields:
280
+ ds.move(f, None)
281
+
282
+ ds.wrap_new('_Rn', ('dets',))
283
+ ds.wrap_new('_Rtes', ('dets',))
284
+ ds.wrap_new('_Vth', ('dets',))
285
+ ds.wrap_new('_Zeq', ('dets',))
286
+ ds.wrap_new('_Ztes', ('dets',))
287
+
288
+ # Calculates Rn
289
+ Ib_ob = ob.Ibias[ob.bgmap][:, None]
290
+ ds._Rn = ob.meta.R_sh * (np.abs(Ib_ob / ob.Ites) - 1)[:, 0]
291
+
292
+ # Calculate Rtes for in-transition dets
293
+ Ib = ds.Ibias_dc[ds.bgmap]
294
+
295
+ dIrat = np.real(ds.Ites[:, 0]) / np.abs(ds.Ibias[ds.bgmap])
296
+ I0 = Ib * dIrat / (2 * dIrat - 1)
297
+ Pj = I0 * ds.meta.R_sh * (Ib - I0)
298
+ ds._Rtes = np.abs(Pj / I0**2)
299
+
300
+ Ites_ob = np.zeros_like(ds.Ites)
301
+ Ites_sc = np.zeros_like(ds.Ites)
302
+ for rc in range(len(ob.channels)):
303
+ Ites_ob[rc, :] = np.interp(ds.freqs, ob.freqs, ob.Ites[rc])
304
+ Ites_sc[rc, :] = np.interp(ds.freqs, sc.freqs, sc.Ites[rc])
305
+
306
+ ds._Vth = 1./((1./Ites_ob - 1./Ites_sc) / ds._Rn[:, None])
307
+ ds._Zeq = ds._Vth / Ites_sc
308
+
309
+ ds._Ztes = ds._Vth / ds.Ites - ds._Zeq
310
+
311
+ for f in fields:
312
+ ds.move(f, f[1:])
313
+
314
+ return ds
315
+
316
+
317
+ def Ztes_fit(f, R, beta, L, tau):
318
+ """
319
+ Ztes equation from Irwin/Shaw eq 42
320
+ """
321
+ return R * (1 + beta) \
322
+ + R * L / (1 - L) * (2 + beta) / (1 + 2j * np.pi * f * tau)
323
+
324
+ def guess_fit_params(ds, idx):
325
+ """
326
+ Gets initial params for fit at a particular freq idx
327
+ """
328
+ R = ds.Rtes[idx]
329
+
330
+ min_idx = np.argmin(np.imag(ds.Ztes[idx]))
331
+ tau_guess = -1./(2*np.pi*ds.freqs[min_idx])
332
+
333
+ beta_guess = np.abs(ds.Ztes[idx, -1]) / R - 1
334
+
335
+ L_guess = 1000
336
+
337
+ return (R, beta_guess, L_guess, tau_guess)
338
+
339
+
340
+ def fit_single_det_params(ds, idx, x0=None, weights=None, fmax=None):
341
+ """
342
+ Fits detector parameters for a single channel
343
+ """
344
+ R = ds.Rtes[idx]
345
+ if x0 is None:
346
+ x0 = guess_fit_params(ds, idx)
347
+
348
+ if weights is None:
349
+ weights = np.ones_like(ds.freqs)
350
+
351
+ if fmax is not None:
352
+ weights[ds.freqs > fmax] = 0
353
+
354
+ def chi2(x):
355
+ zfit = Ztes_fit(ds.freqs, *x)
356
+ c2 = np.nansum(weights * np.abs(ds.Ztes[idx] - zfit)**2)
357
+ return c2
358
+
359
+ res = minimize(chi2, x0)
360
+ if list(res.x) == list(x0):
361
+ res.success = False
362
+
363
+ return res
364
+
365
+ def fit_det_params(ds, pb=False, fmax=None):
366
+ """
367
+ Fits detector params for a sweep.
368
+
369
+ Args
370
+ -----
371
+ ds : AxisManager
372
+ CIData
373
+ pb : bool
374
+ If True, wil display progressbar.
375
+ fmax : optional, float
376
+ If set, will only fit using freq values less than fmax.
377
+ """
378
+ fields = ['_fit_x', '_fit_labels', '_tau_eff', '_Rfit', '_beta_I', '_L_I',
379
+ '_tau_I']
380
+
381
+ for f in fields:
382
+ if f in ds._fields:
383
+ ds.move(f, None)
384
+
385
+ for f in ['_tau_eff', '_Rfit', '_beta_I', '_L_I', '_tau_I']:
386
+ ds.wrap_new(f, ('dets', ), cls=np.full, fill_value=np.nan)
387
+ ds.wrap('_fit_labels', np.array(['R', 'beta_I', 'L_I', 'tau_I']))
388
+ ds.wrap_new('_fit_x', ('dets', 4), cls=np.full, fill_value=np.nan)
389
+
390
+ for i in trange(len(ds.channels), disable=(not pb)):
391
+ if ds.bgmap[i] == -1: continue
392
+
393
+ res = fit_single_det_params(ds, i, fmax=fmax)
394
+ ds._fit_x[i, :] = res.x
395
+
396
+ # Compute tau_eff
397
+ RL = ds.meta.R_sh
398
+ ds._Rfit, ds._beta_I, ds._L_I, ds._tau_I = ds._fit_x.T
399
+ ds._tau_eff = ds._tau_I * (1 - ds._L_I) * (1 + ds._beta_I + RL / ds._Rfit) \
400
+ / (1 + ds._beta_I + RL / ds._Rfit + ds._L_I * (1 - RL / ds._Rfit))
401
+
402
+ for f in fields:
403
+ ds.move(f, f[1:])
404
+
405
+ return True
406
+
407
+ def analyze_full(ds, bgs=None):
408
+ """
409
+ Performs the full CI analysis on a dataset.
410
+ """
411
+ analyze_tods(ds, bgs=bgs)
412
+ if ds.run_kwargs.state == 'transition':
413
+ get_ztes(ds)
414
+ fit_det_params(ds)
415
+ return ds
416
+
417
+
418
+ ###########################################################################
419
+ # Plotting functions
420
+ ###########################################################################
421
+ def plot_transfers(d, rc):
422
+ """
423
+ Plot the SC, OB, and in-transition transfer functions for a channel
424
+ """
425
+ bg = d.bgmap[rc]
426
+
427
+ fig, axes = plt.subplots(1, 2, figsize=(16, 6))
428
+ labels = ['Transition', 'superconducting', 'overbiased']
429
+
430
+ for i, s in enumerate([d, d.sc, d.ob]):
431
+ mag = np.abs(s.Ites[rc])/s.Ibias[bg]
432
+ phase = np.unwrap(np.angle(s.Ites[rc]))
433
+ axes[0].plot(s.freqs, mag, '.', label=labels[i])
434
+ axes[1].plot(s.freqs, phase, '.', label=labels[i])
435
+ axes[0].set(xscale='log', yscale='log')
436
+ for ax in axes:
437
+ ax.legend()
438
+ ax.set_xlabel('Freq (Hz)', fontsize=16)
439
+ axes[0].set_ylabel(r'$I_\mathrm{TES}$ / $I_\mathrm{bias}$', fontsize=16)
440
+ axes[1].set_ylabel(r'$\phi_{I_\mathrm{TES}}$', fontsize=16)
441
+ return fig, axes
442
+
443
+ def plot_ztes(ds, rc, x=None, write_text=True):
444
+ """
445
+ plots Ztes data
446
+
447
+ Args
448
+ -----
449
+ ds : AxisManager
450
+ analyzed CIData object
451
+ rc : int
452
+ Channel whose data to plot
453
+ x : optional, list
454
+ Fit params to plot instead of the ones stored in the CIData object.
455
+ write_text : bool
456
+ If textbox with param data should be written.
457
+ """
458
+ dims = np.array([2.5, 1])
459
+ fig, axes = plt.subplots(1, 2, figsize=5 * dims)
460
+
461
+ ztes = 1000 * ds.Ztes[rc]
462
+ fs = np.linspace(0, np.max(ds.freqs), 1000)
463
+ if x is None:
464
+ x = ds.fit_x[rc]
465
+ zfit = 1000 * Ztes_fit(fs, *x)
466
+
467
+ # Circ plot
468
+ ax = axes[0]
469
+ ax.scatter(np.real(ztes), np.imag(ztes), c=np.log(ds.freqs), marker='.')
470
+ ax.plot(np.real(zfit), np.imag(zfit), color='black', ls='--', alpha=0.6)
471
+ ax.set_xlabel(r'Re[$Z_\mathrm{TES}$] (m$\Omega$)', fontsize=16)
472
+ ax.set_ylabel(r'Im[$Z_\mathrm{TES}$] (m$\Omega$)', fontsize=16)
473
+
474
+ ## Param summary
475
+ txt = '\n'.join([
476
+ r'$\tau_\mathrm{eff}$ = ' + f'{ds.tau_eff[rc]*1000:.2f} ms',
477
+ r'$R_\mathrm{fit}$ = ' + f'{x[0]*1000:.2f} '+ r'm$\Omega$',
478
+ r'$\beta_I$ = ' + f'{x[1]:.2f}',
479
+ r'$\mathcal{L}_I$ = ' + f'{x[2]:.2f}',
480
+ r'$\tau_I$ = ' + f'{x[3]*1000:.2f} ms',
481
+ # r'$r^2$ = ' + f'{ds.ztes_rsquared[rc]:.2f}',
482
+ ])
483
+ if write_text:
484
+ ax.text(0.35, 0.35, txt, transform=ax.transAxes, fontsize=12,
485
+ bbox=dict(facecolor='white', alpha=0.8, edgecolor='black'))
486
+
487
+ # Im / Re plot
488
+ ax = axes[1]
489
+ ax.plot(ds.freqs, np.real(ztes), '.', color='C0', label='Re')
490
+ ax.plot(ds.freqs, np.imag(ztes), '.', color='C1', label='Im')
491
+ ax.plot(fs, np.real(zfit), color='C0', alpha=0.8, ls='--')
492
+ ax.plot(fs, np.imag(zfit), color='C1', alpha=0.8, ls='--')
493
+ ax.set(xscale='log')
494
+ ax.set_xlabel("Freq (Hz)", fontsize=16)
495
+ ax.set_ylabel(r"$Z_\mathrm{TES}$ (m$\Omega$)", fontsize=16)
496
+ ax.legend(fontsize=12)
497
+
498
+ return fig, ax
499
+
500
+ ###########################################################################
501
+ # Data Taking Functions
502
+ ###########################################################################
503
+ @sdl.set_action()
504
+ def take_complex_impedance(
505
+ S, cfg, bgs, freqs=None, state='transition', nperiods=500,
506
+ max_meas_time=20., tickle_voltage=0.005, run_analysis=False):
507
+ """
508
+ Takes a complex impedance sweep. This will play sine waves on specified
509
+ bias-groups over the current DC bias voltage. This returns a CISweep object.
510
+
511
+ Args
512
+ ----
513
+ S : SmurfControl
514
+ Pysmurf Instance
515
+ cfg : DetConfig
516
+ Det config instance
517
+ bgs : array, int
518
+ List of bias groups to run on
519
+ freqs : array, optional
520
+ List of frequencies to sweep over.
521
+ state : str
522
+ Current detector state. Must be 'ob', 'sc', or 'transition'
523
+ nperiods : float
524
+ Number of periods to measure for at each frequency. If the meas_time
525
+ ends up larger than ``max_meas_time``, ``max_meas_time`` will be used
526
+ instead. This makes it so we don't spend unreasonably long amounts of
527
+ time at higher freqs.
528
+ max_meas_time : float
529
+ Maximum amount of time to wait at any given frequency
530
+ tickle_voltage : float
531
+ Tickle amplitude in low-current-mode volts.
532
+ run_analysis : bool
533
+ Perform the full CI analysis and save the results.
534
+ """
535
+ if state not in ['ob', 'sc', 'transition']:
536
+ raise ValueError("State must be 'ob', 'sc', or 'transition'")
537
+
538
+ bgs = np.atleast_1d(bgs)
539
+
540
+ if freqs is None:
541
+ freqs = np.logspace(0, np.log10(2e3), 20)
542
+ freqs = np.atleast_1d(freqs)
543
+
544
+ run_kwargs = {k: v for k, v in locals().items() if k not in ['S', 'cfg']}
545
+
546
+ # First, determine which bands and channels we'll try to run on.
547
+ scale_array = np.array([S.get_amplitude_scale_array(b) for b in range(8)])
548
+ bands, channels = np.where(scale_array > 0)
549
+
550
+ # Main dataset
551
+ ds = new_ci_dset(S, cfg, bands, channels, freqs, run_kwargs=run_kwargs)
552
+
553
+ initial_ds_factor = S.get_downsample_factor()
554
+ initial_filter_disable = S.get_filter_disable()
555
+
556
+ pb = tqdm(total=len(freqs)*len(bgs), disable=False)
557
+ try:
558
+ sdl.set_current_mode(S, bgs, 1)
559
+ tickle_voltage /= S.high_low_current_ratio
560
+
561
+ init_biases = S.get_tes_bias_bipolar_array()
562
+ for bg in bgs:
563
+ m = ds.bgmap == bg
564
+ channel_mask = ds.bands[m] * S.get_number_channels() + ds.channels[m]
565
+
566
+ ds.sids[bg] = sdl.stream_g3_on(
567
+ S, channel_mask=channel_mask, subtype='complex_impedance',
568
+ downsample_factor=1, filter_disable=True
569
+ )
570
+ for j, freq in enumerate(freqs):
571
+ meas_time = min(1./freq * nperiods, max_meas_time)
572
+ S.log(f"Tickle with bg={bg}, freq={freq}")
573
+ S.play_sine_tes(bg, tickle_voltage, freq)
574
+ ds.start_times[bg, j] = time.time()
575
+ time.sleep(meas_time)
576
+ ds.stop_times[bg, j] = time.time()
577
+ S.set_rtm_arb_waveform_enable(0)
578
+ S.set_tes_bias_bipolar(bg, init_biases[bg])
579
+ pb.update()
580
+ sdl.stream_g3_off(S)
581
+ finally:
582
+ sdl.set_current_mode(S, bgs, 0)
583
+ S.set_downsample_factor(initial_ds_factor)
584
+ S.set_filter_disable(initial_filter_disable)
585
+ sdl.stream_g3_off(S)
586
+
587
+ fname = sdl.make_filename(S, f'ci_sweep_{state}.h5')
588
+ ds.wrap('filepath', fname)
589
+ ds.save(fname)
590
+ S.pub.register_file(fname, 'ci', format='h5', plot=False)
591
+ S.log(f"Saved unanalyzed datafile to {fname}")
592
+
593
+ if run_analysis:
594
+ analyze_full(ds, bgs=bgs)
595
+ fname = sdl.make_filename(S, f'ci_sweep_{state}.h5')
596
+ ds.filepath = fname
597
+ ds.save(fname)
598
+ S.pub.register_file(fname, 'ci', format='h5', plot=False)
599
+ S.log(f"Saved analyzed datafile to {fname}")
600
+
601
+ return ds
602
+
603
+ def take_complex_impedance_ob_sc(S, cfg, bgs, overbias_voltage=19.9,
604
+ tes_bias=15.0, overbias_wait=5.0,
605
+ cool_wait=30., run_analysis=True, **ci_kwargs):
606
+ """
607
+ Takes overbiased and superconducting complex impedance sweeps. These are
608
+ required to analyze any in-transition sweeps.
609
+
610
+ Args
611
+ -----
612
+ S : SmurfControl
613
+ Pysmurf Instance
614
+ cfg : DetConfig
615
+ Det config instance
616
+ bgs : array, int
617
+ List of bias groups to run on
618
+ overbias_voltage : float
619
+ Voltage to use to overbias detectors
620
+ tes_bias : float
621
+ Voltage to set detectors to after overbiasing
622
+ overbias_wait : float
623
+ Time to wait at the overbias_voltage
624
+ cool_wait : float
625
+ Time to wait at the tes_bias after overbiasing
626
+ run_analysis : bool
627
+ Perform the full CI analysis and save the results.
628
+ **ci_kwargs :
629
+ Any additional kwargs will be passed directly to the
630
+ ``take_complex_impedance`` function.
631
+ """
632
+ bgs = np.atleast_1d(bgs)
633
+
634
+ # Takes SC sweep
635
+ for bg in bgs:
636
+ S.set_tes_bias_bipolar(bg, 0)
637
+ sc = take_complex_impedance(S, cfg, bgs, state='sc',
638
+ run_analysis=run_analysis, **ci_kwargs)
639
+
640
+ S.overbias_tes_all(bias_groups=bgs, overbias_voltage=overbias_voltage,
641
+ tes_bias=tes_bias, overbias_wait=overbias_wait,
642
+ cool_wait=cool_wait)
643
+ ob = take_complex_impedance(S, cfg, bgs, state='ob',
644
+ run_analysis=run_analysis, **ci_kwargs)
645
+
646
+ cfg.dev.update_experiment({
647
+ 'complex_impedance_sc_path': sc.filepath,
648
+ 'complex_impedance_ob_path': ob.filepath
649
+ }, update_file=True)
650
+
651
+ return sc, ob