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.
- sodetlib/__init__.py +22 -0
- sodetlib/_version.py +21 -0
- sodetlib/constants.py +13 -0
- sodetlib/det_config.py +709 -0
- sodetlib/noise.py +624 -0
- sodetlib/operations/__init__.py +5 -0
- sodetlib/operations/bias_dets.py +551 -0
- sodetlib/operations/bias_steps.py +1248 -0
- sodetlib/operations/bias_wave.py +688 -0
- sodetlib/operations/complex_impedance.py +651 -0
- sodetlib/operations/iv.py +716 -0
- sodetlib/operations/optimize.py +189 -0
- sodetlib/operations/squid_curves.py +641 -0
- sodetlib/operations/tracking.py +624 -0
- sodetlib/operations/uxm_relock.py +406 -0
- sodetlib/operations/uxm_setup.py +783 -0
- sodetlib/py.typed +0 -0
- sodetlib/quality_control.py +415 -0
- sodetlib/resonator_fitting.py +508 -0
- sodetlib/stream.py +291 -0
- sodetlib/tes_param_correction.py +579 -0
- sodetlib/util.py +880 -0
- sodetlib-0.6.1rc1.data/scripts/jackhammer +761 -0
- sodetlib-0.6.1rc1.dist-info/LICENSE +25 -0
- sodetlib-0.6.1rc1.dist-info/METADATA +6 -0
- sodetlib-0.6.1rc1.dist-info/RECORD +28 -0
- sodetlib-0.6.1rc1.dist-info/WHEEL +5 -0
- sodetlib-0.6.1rc1.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
|