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,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
|