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,716 @@
1
+ import numpy as np
2
+ import time
3
+ from scipy.interpolate import interp1d
4
+ import sodetlib as sdl
5
+ import matplotlib.pyplot as plt
6
+ import os
7
+ from copy import deepcopy
8
+ from tqdm.auto import trange
9
+ from typing import Optional, Dict, Any
10
+ from dataclasses import dataclass, asdict
11
+
12
+ import warnings
13
+ warnings.filterwarnings('ignore')
14
+ np.seterr(all="ignore")
15
+
16
+ class IVAnalysis:
17
+ """
18
+ IVAnalysis is the object used to hold take_iv information and analysis
19
+ products. When instantiating from scratch, all keyword arguments must be
20
+ specified. (This is not true if you are loading from an existing file
21
+ using the load function).
22
+
23
+ Args
24
+ ----
25
+ S : SmurfControl
26
+ Pysmurf instance
27
+ cfg : DetConfig
28
+ DetConfig instance
29
+ run_kwargs : dict
30
+ Dictionary of keyword arguments passed to the take_iv function.
31
+ sid : int
32
+ Session id from the IV stream session
33
+ start_times : np.ndarray
34
+ Array of start_times of each bias point
35
+ stop_times : np.ndarray
36
+ Array of stop_times of each bias point
37
+
38
+ Attributes
39
+ -----------
40
+ meta : dict
41
+ Dictionary of pysmurf and sodetlib metadata during the IV.
42
+ biases_cmd : np.ndarray
43
+ Array of commanded biases voltages (in low-current-mode units)
44
+ nbiases : int
45
+ Number of bias points commanded throughout the IV
46
+ bias_groups : np.ndarray
47
+ Array containing the bias-groups included in this IV
48
+ am : AxisManager
49
+ AxisManager containing TOD from the IV (this is not saved to disk,
50
+ but can be loaded with the _load_am function)
51
+ nchans : int
52
+ Number of channels included in the IV
53
+ bands : np.ndarray
54
+ Array of shape (nchans) containing the smurf-band of each channel
55
+ channels : np.ndarray
56
+ Array of shape (nchans) containing the smurf-channel of each channel
57
+ v_bias : np.ndarray
58
+ Array of shape (nbiases) containing the bias voltage at each bias point
59
+ (in low-current-mode Volts)
60
+ i_bias : np.ndarray
61
+ Array of shape (nbiases) containing the commanded bias-current
62
+ at each step (Amps)
63
+ resp : np.ndarray
64
+ Array of shape (nchans, nbiases) containing the squid response (Amps)
65
+ at each bias point
66
+ R : np.ndarray
67
+ Array of shape (nchans, nbiases) containing the TES Resistance of each
68
+ channel at each bias point
69
+ p_tes : np.ndarray
70
+ Array of shape (nchans, nbiases) containing the electrical power on the
71
+ TES (W) of each channel at each bias point
72
+ v_tes : np.ndarray
73
+ Array of shape (nchans, nbiases) containing the voltage across the TES
74
+ for each channel at each bias point (V)
75
+ i_tes : np.ndarray
76
+ Array of shape (nchans, nbiases) containing the current across the TES
77
+ for each channel at each bias point (Amps)
78
+ R_n : np.ndarray
79
+ Array of shape (nchans) containing the normal resistance (Ohms) of the
80
+ TES
81
+ R_L : np.ndarray
82
+ Array of shape (nchans) containing the non-TES resistance (Ohms).
83
+ Should be shunt resistance + parasitic resistance
84
+ p_sat : np.ndarray
85
+ Array of shape (nchans) containing the electrical power (W) at which
86
+ Rfrac crosses the 90% threshold (or whatever arg is passed to the
87
+ analysis fn). Note that this is only truly the saturation power in
88
+ the absence of optical power.
89
+ si : np.ndarray
90
+ Array of shape (nchans, nbiases) containing the responsivity (1/V)
91
+ of the TES for each bias-step
92
+ idxs : np.ndarray
93
+ Array of shape (nchans, 3) containing:
94
+ 1. Last index of the SC branch
95
+ 2. First index off the normal branch
96
+ 3. Index at which p_tes crosses the 90% thresh
97
+ bgmap : np.ndarray
98
+ Array of shape (nchans) containing bias-group assignment of each
99
+ channel in the IV. This is not calculated with the IV but pulled in
100
+ from the device cfg, so it's important you run the take_bgmap function
101
+ to generate the bias-group map before running the IV.
102
+ polarity : np.ndarray
103
+ Array of shape (nchans) containing the polarity of each channel (also
104
+ from the bias-map file in the device cfg). This tells you if the TES
105
+ current changes in the same direction or opposite direction of the bias
106
+ current.
107
+ """
108
+ def __init__(self, S=None, cfg=None, run_kwargs=None, sid=None,
109
+ start_times=None, stop_times=None) -> None:
110
+
111
+ self._S = S
112
+ self._cfg = cfg
113
+ self.run_kwargs = run_kwargs
114
+ self.sid = sid
115
+ self.start_times = start_times
116
+ self.stop_times = stop_times
117
+ self.am = None
118
+
119
+ if self._S is not None:
120
+ # Initialization stuff on initial creation with a smurf instance
121
+ self.meta = sdl.get_metadata(S, cfg)
122
+ self.biases_cmd = np.sort(self.run_kwargs['biases'])
123
+ self.nbiases = len(self.biases_cmd)
124
+ self.bias_groups = self.run_kwargs['bias_groups']
125
+
126
+ am = self._load_am()
127
+ self.nchans= len(am.signal)
128
+
129
+ self.bands = am.ch_info.band
130
+ self.channels = am.ch_info.channel
131
+ self.v_bias = np.full((self.nbiases, ), np.nan)
132
+ self.i_bias = np.full((self.nbiases, ), np.nan)
133
+ self.v_thevenin = np.full((self.nbiases, ), np.nan)
134
+ self.resp = np.full((self.nchans, self.nbiases), np.nan)
135
+ self.R = np.full((self.nchans, self.nbiases), np.nan)
136
+ self.p_tes = np.full((self.nchans, self.nbiases), np.nan)
137
+ self.v_tes = np.full((self.nchans, self.nbiases), np.nan)
138
+ self.i_tes = np.full((self.nchans, self.nbiases), np.nan)
139
+ self.R_n = np.full((self.nchans, ), np.nan)
140
+ self.R_L = np.full((self.nchans, ), np.nan)
141
+ self.p_sat = np.full((self.nchans, ), np.nan)
142
+ self.si = np.full((self.nchans, self.nbiases), np.nan)
143
+ self.idxs = np.full((self.nchans, 3), -1, dtype=int)
144
+
145
+ self.bgmap, self.polarity = sdl.load_bgmap(
146
+ self.bands, self.channels, self.meta['bgmap_file']
147
+ )
148
+
149
+ def to_dict(self):
150
+ saved_fields = [
151
+ 'meta', 'bands', 'channels', 'sid', 'start_times', 'stop_times',
152
+ 'run_kwargs', 'biases_cmd', 'bias_groups', 'nbiases', 'nchans',
153
+ 'bgmap', 'polarity', 'resp', 'v_bias', 'i_bias', 'R', 'R_n', 'R_L',
154
+ 'idxs', 'p_tes', 'v_tes', 'i_tes', 'p_sat', 'si', 'v_thevenin'
155
+ ]
156
+ return {
157
+ field: getattr(self, field, None) for field in saved_fields
158
+ }
159
+
160
+ def save(self, path=None, update_cfg=False):
161
+ data = self.to_dict()
162
+ if path is not None:
163
+ np.save(path, data, allow_pickle=True)
164
+ else:
165
+ self.filepath = sdl.validate_and_save(
166
+ 'iv_analysis.npy', data, S=self._S, cfg=self._cfg, register=True,
167
+ make_path=True)
168
+ if update_cfg:
169
+ self._cfg.dev.update_experiment({'iv_file': self.filepath},
170
+ update_file=True)
171
+
172
+ @classmethod
173
+ def from_dict(cls, data: Dict[str, Any]) -> 'IVAnalysis':
174
+ iva = cls()
175
+ for key, val in data.items():
176
+ setattr(iva, key, val)
177
+
178
+ if len(iva.start_times) == 1:
179
+ # Data was taken before we started saving timestamps for each
180
+ # BG separately. Convert this to the 2d array that's now expected
181
+ # by analysis.
182
+ iva.start_times = np.vstack([iva.start_times for _ in range(15)])
183
+ iva.stop_times = np.vstack([iva.stop_times for _ in range(15)])
184
+
185
+ return iva
186
+
187
+ @classmethod
188
+ def load(cls, path) -> 'IVAnalysis':
189
+ return cls.from_dict(np.load(path, allow_pickle=True).item())
190
+
191
+
192
+ def _load_am(self, arc=None):
193
+ if self.am is None:
194
+ if arc:
195
+ self.am = arc.load_data(self.start_times[0],
196
+ self.stop_times[-1])
197
+ else:
198
+ self.am = sdl.load_session(self.meta['stream_id'], self.sid)
199
+ return self.am
200
+
201
+
202
+ def compute_psats(iva, psat_level=0.9):
203
+ """
204
+ Computes Psat for an IVAnalysis object. Will save results to iva.p_sat.
205
+ This assumes i_tes, v_tes, and r_tes have already been calculated.
206
+
207
+ Args
208
+ ----
209
+ iva : IVAnalysis
210
+ IV Analysis object
211
+ psat_level : float
212
+ R_frac level for which Psat is defined. If 0.9, Psat will be the
213
+ power on the TES when R_frac = 0.9.
214
+
215
+ Returns
216
+ -------
217
+ p_sat : np.ndarray
218
+ Array of length (nchan) with the p-sat computed for each channel (W)
219
+ """
220
+ # calculates P_sat as P_TES when Rfrac = psat_level
221
+ # if the TES is at 90% R_n more than once, just take the last crossing
222
+ for i in range(iva.nchans):
223
+ if np.isnan(iva.R_n[i]):
224
+ continue
225
+
226
+ level = psat_level
227
+ R = iva.R[i]
228
+ R_n = iva.R_n[i]
229
+ p_tes = iva.p_tes[i]
230
+ cross_idx = np.where(R/R_n < level)[0]
231
+
232
+ if len(cross_idx) == 0:
233
+ iva.p_sat[i] = np.nan
234
+ continue
235
+
236
+ # Takes cross-index to be the last time Rfrac crosses psat_level
237
+ cross_idx = cross_idx[-1]
238
+ if cross_idx == 0:
239
+ iva.p_sat[i] = np.nan
240
+ continue
241
+
242
+ iva.idxs[i, 2] = cross_idx
243
+ try:
244
+ iva.p_sat[i] = interp1d(
245
+ R[cross_idx:cross_idx+2]/R_n,
246
+ p_tes[cross_idx:cross_idx+2]
247
+ )(level)
248
+ except ValueError:
249
+ iva.p_sat[i] = np.nan
250
+
251
+ return iva.p_sat
252
+
253
+
254
+ def compute_si(iva):
255
+ """
256
+ Computes responsivity S_i for an IV analysis object. Will save results
257
+ to iva.si. This assumes i_tes, v_tes, and r_tes have already been
258
+ calculated.
259
+
260
+ Args
261
+ ----
262
+ iva : IVAnalysis
263
+ IV Analysis object
264
+
265
+ Returns
266
+ -------
267
+ si : np.ndarray
268
+ Array of length (nchan, nbiases) with the responsivity as a fn of bias
269
+ voltage for each channel (V^-1).
270
+ """
271
+ smooth_dist = 5
272
+ w_len = 2 * smooth_dist + 1
273
+ w = (1./float(w_len))*np.ones(w_len) # window
274
+
275
+ for i in range(iva.nchans):
276
+ if np.isnan(iva.R_n[i]):
277
+ continue
278
+
279
+ # Running average
280
+ i_tes_smooth = np.convolve(iva.i_tes[i], w, mode='same')
281
+ v_tes_smooth = np.convolve(iva.v_tes[i], w, mode='same')
282
+ r_tes_smooth = v_tes_smooth/i_tes_smooth
283
+
284
+ sc_idx = iva.idxs[i, 0]
285
+ if sc_idx == -1:
286
+ continue
287
+ R_L = iva.R_L[i]
288
+
289
+ # Take derivatives
290
+ di_tes = np.diff(i_tes_smooth)
291
+ dv_tes = np.diff(v_tes_smooth)
292
+ R_L_smooth = np.ones(len(r_tes_smooth-1)) * R_L
293
+ R_L_smooth[:sc_idx] = dv_tes[:sc_idx]/di_tes[:sc_idx]
294
+ r_tes_smooth_noStray = r_tes_smooth - R_L_smooth
295
+ i0 = i_tes_smooth[:-1]
296
+ r0 = r_tes_smooth_noStray[:-1]
297
+ rL = R_L_smooth[:-1]
298
+ beta = 0.
299
+
300
+ # artificially setting rL to 0 for now,
301
+ # to avoid issues in the SC branch
302
+ # don't expect a large change, given the
303
+ # relative size of rL to the other terms
304
+ rL = 0
305
+
306
+ # Responsivity estimate, derivation done here by MSF
307
+ # https://www.overleaf.com/project/613978cb38d9d22e8550d45c
308
+ si = -(1./(i0*r0*(2+beta)))*(1-((r0*(1+beta)+rL)/(dv_tes/di_tes)))
309
+ si[:sc_idx] = np.nan
310
+ iva.si[i, :-1] = si
311
+
312
+
313
+ def analyze_iv(iva, psat_level=0.9, save=False, update_cfg=False, show_pb=False):
314
+ """
315
+ Runs main analysis for an IVAnalysis object. This calculates the attributes
316
+ defined in the IVAnalysis class.
317
+
318
+ Args
319
+ ----
320
+ iva : IVAnalysis
321
+ IV analysis object
322
+ psat_level : float
323
+ R_frac for which P_sat is defined. Defaults to 0.9
324
+ save : bool
325
+ If true, will save IVAnalysis object after completing
326
+ update_cfg : bool
327
+ If true, will update the device config with the new IV analysis
328
+ filepath
329
+ show_pb : bool
330
+ If true, will display a progress bar for the IV analysis
331
+ """
332
+ am = iva._load_am()
333
+ R_sh = iva.meta['R_sh']
334
+
335
+ ts = am.primary.UnixTime / 1e9
336
+ # Calculate phase response and bias_voltages / currents
337
+ for i in trange(iva.nbiases, disable=(not show_pb)):
338
+ # Start from back because analysis is easier low->high voltages
339
+ for j, bg in enumerate(iva.bias_groups):
340
+ t0, t1 = iva.start_times[bg, -(i+1)], iva.stop_times[bg, -(i+1)]
341
+ chan_mask = iva.bgmap == bg
342
+ m = (t0 < ts) & (ts < t1)
343
+ iva.resp[chan_mask, i] = np.nanmean(am.signal[:, m][chan_mask], axis=1)
344
+ if j == 0:
345
+ bias_bits = np.median(am.biases[bg, m])
346
+ iva.v_bias[i] = bias_bits * 2 * iva.meta['rtm_bit_to_volt']
347
+
348
+ if iva.run_kwargs['high_current_mode']:
349
+ iva.v_bias *= iva.meta['high_low_current_ratio']
350
+
351
+ iva.i_bias = iva.v_bias / iva.meta['bias_line_resistance']
352
+
353
+ # Convert phase to Amps
354
+ A_per_rad = iva.meta['pA_per_phi0'] / (2*np.pi) * 1e-12
355
+ iva.resp = (iva.resp.T * iva.polarity).T * A_per_rad
356
+
357
+ for i in trange(iva.nchans, disable=(not show_pb)):
358
+ d_resp = np.diff(iva.resp[i])
359
+ dd_resp = np.diff(d_resp)
360
+ dd_resp_abs = np.abs(dd_resp)
361
+
362
+ # Find index of superconducting branch
363
+ try:
364
+ sc_idx = np.nanargmax(dd_resp_abs) + 1
365
+ except ValueError:
366
+ continue
367
+ if sc_idx == 1:
368
+ continue
369
+ iva.idxs[i, 0] = sc_idx
370
+
371
+ # Find index of normal branch by finding the min index after
372
+ # sc branch. (Skips a few indices after sc branch to avoid possible
373
+ # phase skipping)
374
+ try:
375
+ nb_idx = sc_idx + 1 + np.nanargmin(iva.resp[i, sc_idx+1:])
376
+ except ValueError:
377
+ continue
378
+ iva.idxs[i, 1] = nb_idx
379
+ nb_fit_idx = (iva.nbiases + nb_idx) // 2
380
+ fin_idx = (np.isfinite(iva.i_bias[nb_fit_idx:]) &
381
+ np.isfinite(iva.resp[i, nb_fit_idx:]))
382
+ try:
383
+ norm_fit = np.polyfit(iva.i_bias[nb_fit_idx:][fin_idx],
384
+ iva.resp[i, nb_fit_idx:][fin_idx], 1)
385
+ except:
386
+ continue
387
+ iva.resp[i] -= norm_fit[1] # Put resp in real current units
388
+
389
+ fin_idx = (np.isfinite(iva.i_bias[:sc_idx]) &
390
+ np.isfinite(iva.resp[i, :sc_idx]))
391
+ try:
392
+ sc_fit = np.polyfit(iva.i_bias[:sc_idx][fin_idx],
393
+ iva.resp[i, :sc_idx][fin_idx], 1)
394
+ except:
395
+ continue
396
+
397
+ # subtract off unphysical y-offset in superconducting branch; this
398
+ # is probably due to an undetected phase wrap at the kink between
399
+ # the superconducting branch and the transition, so it is
400
+ # *probably* legitimate to remove it by hand.
401
+ iva.resp[i, :sc_idx] -= sc_fit[1]
402
+ sc_fit[1] = 0 # now change s.c. fit offset to 0 for plotting
403
+
404
+ R = R_sh * (iva.i_bias/(iva.resp[i]) - 1)
405
+ R_n = np.nanmean(R[nb_fit_idx:])
406
+ R_L = np.nanmean(R[1:sc_idx])
407
+
408
+ iva.v_tes[i] = iva.i_bias * R_sh * R / (R + R_sh)
409
+ iva.i_tes[i] = iva.v_tes[i] / R
410
+ iva.p_tes[i] = (iva.v_tes[i]**2) / R
411
+ iva.R[i] = R
412
+ iva.R_n[i] = R_n
413
+ iva.R_L[i] = R_L
414
+
415
+ compute_psats(iva, psat_level)
416
+ compute_si(iva)
417
+
418
+ if save:
419
+ iva.save(update_cfg=update_cfg)
420
+
421
+ def plot_Rfracs(iva, Rn_range=(5e-3, 12e-3), bgs=None):
422
+ """
423
+ Plots Stacked Rfrac curves of each channel.
424
+ """
425
+ fig, ax = plt.subplots()
426
+ Rfrac = (iva.R.T / iva.R_n).T
427
+ if bgs is None:
428
+ bgs = np.arange(12)
429
+ bgs = np.atleast_1d(bgs)
430
+ for i, rf in enumerate(Rfrac):
431
+ bg = iva.bgmap[i]
432
+ if bgs is not None:
433
+ if bg not in bgs:
434
+ continue
435
+
436
+ if not Rn_range[0] < iva.R_n[i] < Rn_range[1]:
437
+ continue
438
+
439
+ if bg == -1:
440
+ continue
441
+
442
+ ax.plot(iva.v_bias, rf, alpha=0.1, color=f'C{bg}')
443
+ ax.set(ylim=(0, 1.1))
444
+ ax.set_xlabel("Bias Voltage (V)", fontsize=14)
445
+ ax.set_ylabel("$R_\\mathrm{frac}$", fontsize=14)
446
+ return fig, ax
447
+
448
+ def plot_Rn_hist(iva, range=(0, 10), text_loc=(0.05, 0.05), bbox_props=None):
449
+ """
450
+ Plots summary of channel normal resistances.
451
+
452
+ Args
453
+ -----
454
+ iva : IVAnalysis
455
+ IVAnalysis object
456
+ range : tuple
457
+ Range of histogram
458
+ text_loc : tuple
459
+ Location of text box in coordinate frame of axis transform
460
+ bbox_props : dict
461
+ Additional bbox properties to add to the textbox
462
+ """
463
+ fig, ax = plt.subplots()
464
+ hist = ax.hist(iva.R_n*1000, range=range, bins=40)
465
+ ax.axvline(np.nanmedian(iva.R_n)*1000, color='red', ls='--')
466
+
467
+ chans_pictured = int(np.sum(hist[0]))
468
+ txt = f"{chans_pictured} / {iva.nchans} channels pictured"
469
+ txt += '\n' + get_plot_text(iva)
470
+ txt += f'\nMedian: {np.nanmedian(iva.R_n)*1000:0.2f} mOhms'
471
+
472
+ bbox = dict(facecolor='wheat', alpha=0.8)
473
+ if bbox_props is not None:
474
+ bbox.update(bbox_props)
475
+
476
+ ax.text(*text_loc, txt, bbox=bbox, transform=ax.transAxes)
477
+ ax.set_xlabel("$R_n$ (m$\\Omega$)", fontsize=14)
478
+ return fig, ax
479
+
480
+
481
+ def plot_channel_iv(iva, rc):
482
+ """
483
+
484
+ Plots anlayzed IV results for a given channel.
485
+
486
+ Args
487
+ ----
488
+ iva : IVAnalysis
489
+ Analyzed IVAnalysis instance
490
+ rc : int
491
+ Readout channel to plot
492
+ """
493
+ fig, axes = plt.subplots(4, 1, figsize=(10, 10))
494
+ for ax in axes.ravel():
495
+ v_sc = iva.v_bias[iva.idxs[rc, 0]]
496
+ v_norm = iva.v_bias[iva.idxs[rc, 1]]
497
+ ax.axvspan(0, v_sc, alpha=0.1, color='C0')
498
+ ax.axvspan(v_sc, v_norm, alpha=0.1, color='C2')
499
+ ax.axvspan(v_norm, iva.v_bias[-1], alpha=0.1, color='C1')
500
+
501
+ axes[0].plot(iva.v_bias, iva.i_tes[rc], color='black')
502
+ axes[0].set_ylabel("Current (Amps)")
503
+ axes[1].plot(iva.v_bias, iva.R[rc]*1000, color='black')
504
+ axes[1].set_ylabel("R (mOhms)")
505
+ axes[2].plot(iva.v_bias, iva.p_tes[rc]*1e12, color='black')
506
+ axes[2].set_ylabel("Pbias (pW)")
507
+ axes[3].plot(iva.v_bias, iva.si[rc]*1e-6, color='black')
508
+ axes[3].set_ylabel("Si (1/uV)")
509
+
510
+ b, c, bg = iva.bands[rc], iva.channels[rc], iva.bgmap[rc]
511
+ axes[0].set_title(f"Band: {b}, Chan: {c}, BG: {bg}", fontsize=18)
512
+
513
+ axes[-1].set_xlabel("Voltage (V)")
514
+ return fig, axes
515
+
516
+ @dataclass
517
+ class IVConfig:
518
+ bias_groups: Optional[list] = None
519
+ overbias_voltage: float = 18.0
520
+ overbias_wait: float = 5.0
521
+ high_current_mode: bool = True
522
+ cool_wait: float = 30.
523
+ cool_voltage: Optional[float] = None
524
+ biases: Optional[np.ndarray] = None
525
+ bias_high: float = 18.0
526
+ bias_low: float = 0.0
527
+ bias_step: float = 0.025
528
+ show_plots: bool = True
529
+ wait_time: float = 0.1
530
+ run_analysis: bool = True
531
+ run_serially: bool = False
532
+ serial_wait_time: float = 10.
533
+ g3_tag: Optional[str] = None
534
+ analysis_kwargs: Optional[dict] = None
535
+
536
+ def __post_init__(self):
537
+ if self.biases is None:
538
+ self.biases = np.arange(self.bias_high, self.bias_low - self.bias_step, -self.bias_step)
539
+ self.biases = np.sort(self.biases)[::-1]
540
+
541
+ if self.analysis_kwargs is None:
542
+ self.analysis_kwargs = {}
543
+
544
+
545
+ @sdl.set_action()
546
+ def take_iv(S, cfg, **kwargs):
547
+ """
548
+ Takes an IV.
549
+
550
+ This function requires an accurate bias-group-map, so be sure to run
551
+ ``take_bgmap`` before this.
552
+
553
+ Args
554
+ ----
555
+ S : SmurfControl
556
+ pysmurf instance
557
+ cfg : DetConfig
558
+ detconfig instance
559
+ bias_groups : list, int
560
+ List of bias groups to run on. Defaults to all 12.
561
+ overbias_voltage : float
562
+ Voltage to use to overbias detectors
563
+ overbias_wait : float
564
+ Time (sec) to wait at overbiased voltage
565
+ high_current_mode : bool
566
+ If True, will run IV in high-current-mode. This is highly recommended
567
+ to avoid the bias-line filter.
568
+ cool_wait : float
569
+ Amout of time to wait at the first bias step after overbiasing.
570
+ cool_voltage : float
571
+ TES bias voltage to sit at during the cool_wait time while the cryostat
572
+ cools
573
+ biases : np.ndarray, optional
574
+ array of biases to use for IV.
575
+ This should be in units of Low-Current-Mode volts. If you are running
576
+ in high-current-mode this will automatically be adjusted for you!!
577
+ bias_high : float
578
+ Upper limit for biases if not manually set. (to be used in np.arange)
579
+ This should be in units of Low-Current-Mode volts. If you are running
580
+ in high-current-mode this will automatically be adjusted for you!!
581
+ bias_low : float
582
+ Lower limit for biases if not manually set. (to be used in np.arange)
583
+ This should be in units of Low-Current-Mode volts. If you are running
584
+ in high-current-mode this will automatically be adjusted for you!!
585
+ bias_step : float
586
+ Step size for biases if not manually set. (to be used in np.arange)
587
+ This should be in units of Low-Current-Mode volts. If you are running
588
+ in high-current-mode this will automatically be adjusted for you!!
589
+ wait_time : float
590
+ Amount of time to wait at each bias point.
591
+ run_analysis : bool
592
+ If True, will automatically run analysis, save it, and update device
593
+ cfg. (unless otherwise specified in analysis_kwargs)
594
+ show_plots : bool
595
+ If true, will show summary plots
596
+ run_serially : bool
597
+ If True, will run IV sweeps in serial, running each of the specified
598
+ bias groups independently instead of all together.
599
+ serial_wait_time : float
600
+ Time to sleep between serial IV sweeps (sec)
601
+ g3_tag : string (optional)
602
+ If not None, overrides default tag "oper,iv" sent to g3 file.
603
+ analysis_kwargs : dict
604
+ Keyword arguments to pass to analysis
605
+
606
+ Returns
607
+ -------
608
+ iva : IVAnalysis
609
+ IVAnalysis object. Will be already analyzed if run_analysis=True.
610
+ """
611
+ if not hasattr(S, 'tune_file'):
612
+ raise AttributeError('No tunefile loaded in current '
613
+ 'pysmurf session. Load active tunefile.')
614
+
615
+ kw = deepcopy(cfg.dev.exp['iv_defaults'])
616
+ kw.update(kwargs)
617
+ ivcfg = IVConfig(**kw)
618
+
619
+ if ivcfg.bias_groups is None:
620
+ ivcfg.bias_groups = cfg.dev.exp['active_bgs']
621
+ ivcfg.bias_groups = np.atleast_1d(ivcfg.bias_groups).astype(int)
622
+
623
+ run_kwargs = asdict(ivcfg)
624
+
625
+ start_times = np.zeros((S._n_bias_groups, len(ivcfg.biases)))
626
+ stop_times = np.zeros((S._n_bias_groups, len(ivcfg.biases)))
627
+ sdl.stop_point(S)
628
+ def overbias_and_sweep(bgs, cool_voltage=None):
629
+ """
630
+ Helper function to run IV sweep for a single bg or group of bgs.
631
+ """
632
+ bgs = np.atleast_1d(bgs).astype(int)
633
+ _bias_arr = S.get_tes_bias_bipolar_array()
634
+ _bias_arr[ivcfg.bias_groups] = 0
635
+ S.set_tes_bias_bipolar_array(_bias_arr)
636
+ if ivcfg.overbias_voltage > 0:
637
+ if cool_voltage is None:
638
+ cool_voltage = np.max(ivcfg.biases)
639
+ S.overbias_tes_all(
640
+ bias_groups=bgs, overbias_wait=ivcfg.overbias_wait,
641
+ tes_bias=cool_voltage, cool_wait=ivcfg.cool_wait,
642
+ high_current_mode=ivcfg.high_current_mode,
643
+ overbias_voltage=ivcfg.overbias_voltage
644
+ )
645
+
646
+ S.log(f"Starting TES Bias Ramp on bg {bgs}")
647
+ for i, bias in enumerate(ivcfg.biases):
648
+ sdl.stop_point(S)
649
+ S.log(f"Setting bias to {bias:4.3f}", S.LOG_INFO)
650
+ _bias_arr[bgs] = bias
651
+ S.set_tes_bias_bipolar_array(_bias_arr)
652
+ start_times[bgs, i] = time.time()
653
+ time.sleep(ivcfg.wait_time)
654
+ stop_times[bgs, i] = time.time()
655
+
656
+ if ivcfg.high_current_mode:
657
+ ivcfg.biases /= S.high_low_current_ratio
658
+ try:
659
+ sid = sdl.stream_g3_on(S, tag=ivcfg.g3_tag, subtype='iv')
660
+ if ivcfg.run_serially:
661
+ for bg in ivcfg.bias_groups:
662
+ overbias_and_sweep(bg, cool_voltage=ivcfg.cool_voltage)
663
+ time.sleep(ivcfg.serial_wait_time)
664
+ else:
665
+ overbias_and_sweep(ivcfg.bias_groups)
666
+ finally:
667
+ sdl.stream_g3_off(S)
668
+
669
+ # Turn off biases and streaming on error
670
+ for bg in ivcfg.bias_groups:
671
+ S.set_tes_bias_bipolar(bg, 0)
672
+
673
+
674
+ iva = IVAnalysis(S, cfg, run_kwargs, sid, start_times, stop_times)
675
+
676
+ if ivcfg.run_analysis:
677
+ sdl.stop_point(S)
678
+ _analysis_kwargs = {'save': True, 'update_cfg': True}
679
+ _analysis_kwargs.update(ivcfg.analysis_kwargs)
680
+ analyze_iv(iva, **_analysis_kwargs)
681
+
682
+ # Save and publish plots
683
+ is_interactive = plt.isinteractive()
684
+ try:
685
+ if not ivcfg.show_plots:
686
+ plt.ioff()
687
+ fig, ax = plot_Rfracs(iva)
688
+ fname = sdl.make_filename(S, 'iv_rfracs.png', plot=True)
689
+ fig.savefig(fname)
690
+ S.pub.register_file(fname, 'iv', format='png', plot=True)
691
+ if not ivcfg.show_plots:
692
+ plt.close(fig)
693
+
694
+ fig, ax = plot_Rn_hist(iva)
695
+ fname = sdl.make_filename(S, 'iv_rns.png', plot=True)
696
+ fig.savefig(fname)
697
+ S.pub.register_file(fname, 'iv', format='png', plot=True)
698
+ if not ivcfg.show_plots:
699
+ plt.close(fig)
700
+ finally:
701
+ if is_interactive:
702
+ plt.ion()
703
+
704
+ return iva
705
+
706
+
707
+ def get_plot_text(iva):
708
+ """
709
+ Gets text to add to text-box for IV plots.
710
+ """
711
+ return '\n'.join([
712
+ f"stream_id: {iva.meta['stream_id']}",
713
+ f"sid: {iva.sid}",
714
+ f"path: {os.path.basename(iva.filepath)}",
715
+ ])
716
+