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/py.typed ADDED
File without changes
@@ -0,0 +1,415 @@
1
+ import sodetlib as sdl
2
+ import numpy as np
3
+ import time
4
+ from scipy import signal
5
+ from tqdm.auto import trange
6
+ from pysmurf.client.base.smurf_control import SmurfControl
7
+ import matplotlib.pyplot as plt
8
+
9
+ def check_packet_loss(Ss, cfgs, dur=10, fr_khz=4, nchans=2000, slots=None, downsample_factor=1):
10
+ """
11
+ Takes a short G3 Stream on multiple slots simultaneously and checks for
12
+ dropped samples. This function is strange since it requires simultaneous
13
+ streaming on multiple slots to properly test, so it doesn't follow the
14
+ standard sodetlib function / data format.
15
+
16
+ Args
17
+ -----
18
+ Ss : dict[SmurfController]1
19
+ Dict of pysmurf instances where the key is the slot-number
20
+ cfgs : dict[DetConfig]
21
+ Dict of DetConfigs where the key is the slot number
22
+ dur : float
23
+ Duration of data stream (sec)
24
+ fr_khz : float
25
+ Frequency of FR rate (khz)
26
+ nchans : int
27
+ Number of channels to stream
28
+ slots : list
29
+ Which slots to stream data on. If None, will stream on all slots in the
30
+ Ss object.
31
+ downsample_factor : int
32
+ Downsample factor to use
33
+
34
+ Returns
35
+ --------
36
+ ams : dict[AxisManagers]
37
+ Dict of axis managers indexed by slot-number
38
+ res : dict
39
+ Dict where the key is the slot number, and the values are dicts
40
+ containing frame counters, number of dropped frames, etc.
41
+ """
42
+ if slots is None:
43
+ slots = Ss.keys()
44
+
45
+ for s in slots:
46
+ S = Ss[s]
47
+ S.flux_ramp_setup(fr_khz, 0.4, band=0)
48
+ sdl.stream_g3_on(
49
+ S, channel_mask=np.arange(nchans), downsample_factor=downsample_factor,
50
+ subtype='check_packet_loss'
51
+ )
52
+
53
+ time.sleep(dur)
54
+
55
+ sids = {}
56
+ for s in slots:
57
+ sids[s] = sdl.stream_g3_off(Ss[s])
58
+
59
+ ams = {}
60
+ for s, sid in sids.items():
61
+ ams[s] = sdl.load_session(cfgs[s].stream_id, sid, no_signal=True)
62
+
63
+ res = {}
64
+ for s, am in ams.items():
65
+ dropped_samps = np.sum(np.diff(am.primary['FrameCounter']) - 1)
66
+ total_samps = len(am.primary['FrameCounter'])
67
+ res[s] = {
68
+ 'sid': sids[s],
69
+ 'meta': sdl.get_metadata(Ss[s], cfgs[s]),
70
+ 'frame_counter': am.primary['FrameCounter'],
71
+ 'dropped_samples': dropped_samps,
72
+ 'dropped_frac': dropped_samps / total_samps,
73
+ }
74
+
75
+ return ams, res
76
+
77
+ @sdl.set_action()
78
+ def measure_bias_line_resistances(
79
+ S: SmurfControl, cfg, vstep=0.001, bgs=None, sleep_time=2.0):
80
+ """
81
+ Function to measure the bias line resistance and high-low-current-ratio for
82
+ each bias group. This needs to be run with the smurf hooked up to the
83
+ cryostat and the detectors superconducting, and a bgmap should exist.
84
+
85
+ Args
86
+ -------
87
+ S : SmurfControl
88
+ Pysmurf instance
89
+ cfg : DetConfig
90
+ Det Config instance
91
+ vstep : float
92
+ Voltage step size (in low-current-mode volts)
93
+ bgs : list
94
+ Bias lines to measure. Will default to active bias lines
95
+ sleep_time : float
96
+ Time to wait at each step.
97
+ """
98
+ if bgs is None:
99
+ bgs = cfg.dev.exp['active_bgs']
100
+ bgs = np.atleast_1d(bgs)
101
+ bgmap = np.load(cfg.dev.exp['bgmap_file'], allow_pickle=True).item()
102
+ bg_m = np.asarray(sum([bgmap['bgmap'] == bg for bg in bgs]), dtype=bool)
103
+
104
+ vbias = S.get_tes_bias_bipolar_array()
105
+ vb_low = vbias.copy()
106
+ vb_low[bgs] = 0
107
+ vb_high = vbias.copy()
108
+ vb_high[bgs] = vstep
109
+ segs = []
110
+
111
+ S.set_tes_bias_bipolar_array(vb_low)
112
+ sdl.set_current_mode(S, bgs, 0, const_current=False)
113
+
114
+ def take_step(bias_arr, sleep_time, wait_time=0.2):
115
+ S.set_tes_bias_bipolar_array(bias_arr)
116
+ time.sleep(wait_time)
117
+ t0 = time.time()
118
+ time.sleep(sleep_time)
119
+ t1 = time.time()
120
+ return (t0, t1)
121
+
122
+ sdl.stream_g3_on(S, subtype='measure_bias_line_resistance')
123
+ time.sleep(0.5)
124
+
125
+ segs.append(take_step(vb_low, sleep_time, wait_time=0.5))
126
+ segs.append(take_step(vb_high, sleep_time, wait_time=0.5))
127
+
128
+ S.set_tes_bias_bipolar_array(vb_low)
129
+ time.sleep(0.5)
130
+ sdl.set_current_mode(S, bgs, 1, const_current=False)
131
+
132
+ segs.append(take_step(vb_low, sleep_time, wait_time=0.05))
133
+ segs.append(take_step(vb_high, sleep_time, wait_time=0.05))
134
+
135
+ sid = sdl.stream_g3_off(S)
136
+
137
+ am = sdl.load_session(cfg.stream_id, sid)
138
+ ts = am.timestamps
139
+ sigs = []
140
+ for (t0, t1) in segs:
141
+ m = (t0 < ts) & (ts < t1)
142
+ sigs.append(np.mean(am.signal[bg_m][:, m], axis=1) * S.pA_per_phi0 / (2*np.pi))
143
+
144
+ Rbl_low = vstep / (np.abs(sigs[1] - sigs[0]) * 1e-12)
145
+ Rbl_high = vstep / (np.abs(sigs[3] - sigs[2]) * 1e-12)
146
+ high_low_ratio = Rbl_low / Rbl_high
147
+
148
+ cfg.dev.exp['bias_line_resistance'] = np.nanmedian(Rbl_low)
149
+ cfg.dev.exp['high_low_current_ratio'] = np.nanmedian(high_low_ratio)
150
+ cfg.dev.update_file()
151
+
152
+ path = sdl.make_filename(S, 'measure_bias_line_info')
153
+ data = {
154
+ 'Rbl_low_all': Rbl_low,
155
+ 'Rbl_high_all': Rbl_high,
156
+ 'high_low_ratio_all': high_low_ratio,
157
+ 'bias_line_resistance': np.nanmedian(Rbl_low),
158
+ 'high_current_mode_resistance': np.nanmedian(Rbl_high),
159
+ 'high_low_ratio': np.nanmedian(high_low_ratio),
160
+ 'sid': sid,
161
+ 'vstep': vstep,
162
+ 'bgs': bgs,
163
+ 'meta': sdl.get_metadata(S, cfg),
164
+ 'segs': segs,
165
+ 'sigs': sigs,
166
+ 'path': path,
167
+ }
168
+ np.save(path, data, allow_pickle=True)
169
+ S.pub.register_file(path, 'bias_line_resistances', format='npy')
170
+
171
+ return am, data
172
+
173
+ def setup_fixed_tones(S, cfg, tones_per_band=256, bands=None, jitter=0.5,
174
+ tone_power=None):
175
+ """
176
+ Enables many fixed tones across a selection of bands.
177
+
178
+ Args
179
+ ----------
180
+ S : SmurfControl
181
+ Pysmurf instance
182
+ cfg : DetConfig
183
+ Det config instance
184
+ tones_per_band : int
185
+ Number of fixed tones to create in each band
186
+ bands : int, list[int]
187
+ Bands to set fixed tones in. Defaults to all 8.
188
+ jitter : float
189
+ Noise [Mhz] to add to the center freq so that fixed tones are not
190
+ equispaced.
191
+ tone_power : int
192
+ Tone power of fixed tones.
193
+ """
194
+ if bands is None:
195
+ bands = np.arange(8)
196
+ bands = np.atleast_1d(bands)
197
+
198
+ chans_per_band = S.get_number_channels()
199
+ for band in bands:
200
+ S.log(f"Setting fixed tones for band {band}")
201
+ if tone_power is None:
202
+ tone_power = cfg.dev.bands[band]['tone_power']
203
+
204
+ sbs = np.linspace(0, chans_per_band, tones_per_band, dtype=int, endpoint=False)
205
+ asa = np.zeros_like(S.get_amplitude_scale_array(band))
206
+ asa[sbs] = tone_power
207
+ S.set_amplitude_scale_array(band, asa)
208
+ S.set_center_frequency_array(band, np.random.uniform(-jitter/2, jitter/2, chans_per_band))
209
+ S.set_feedback_enable_array(band, np.zeros(chans_per_band, dtype=int))
210
+
211
+ def get_noise_dBcHz(S, band, chan, nsamp=2**20, nperseg=2**16,
212
+ noise_freq=30e3, noise_bw=100):
213
+ """
214
+ Takes debug data and measures I/Q noise in dBc/Hz.
215
+
216
+ Args
217
+ -----
218
+ S : SmurfControl
219
+ Pysmurf instance
220
+ band : int
221
+ Smurf band
222
+ chan : int
223
+ Smurf chan
224
+ nsamp : int
225
+ Number of samples to take
226
+ nperseg : int
227
+ Nperseg to use when creating the psd
228
+ noise_freq : float
229
+ Freq to measure the readout noise at.
230
+ noise_bw : float
231
+ Frequency bandwidth over which the noise median will be taken
232
+
233
+ Returns
234
+ ---------
235
+ noise_i : float
236
+ Noise of the I data stream in dBc/Hz
237
+ noise_q : float
238
+ Noise of the Q data stream in dBc/Hz
239
+ """
240
+ fsamp = S.get_channel_frequency_mhz() * 1e6
241
+
242
+ sig_i, sig_q, _ = S.take_debug_data(band, channel=chan, rf_iq=True, nsamp=nsamp)
243
+ datfile = S.get_streamdatawriter_datafile()
244
+
245
+ fs, pxxi = signal.welch(sig_i, fs=fsamp, nperseg=nperseg)
246
+ fs, pxxq = signal.welch(sig_q, fs=fsamp, nperseg=nperseg)
247
+
248
+ magfac = np.mean(sig_q)**2 + np.mean(sig_i)**2
249
+ pxxi_dbc = 10. * np.log10(pxxi/magfac)
250
+ pxxq_dbc = 10. * np.log10(pxxq/magfac)
251
+
252
+ f0, f1 = noise_freq - noise_bw/2, noise_freq + noise_bw/2
253
+ m = (f0 < fs) & (fs < f1)
254
+ noise_i = np.nanmedian(pxxi_dbc[m])
255
+ noise_q = np.nanmedian(pxxq_dbc[m])
256
+
257
+ return noise_i, noise_q, datfile
258
+
259
+
260
+ def plot_fixed_tone_loopback(res):
261
+ """Plot results from fixed_tone_loopback"""
262
+ fig, ax = plt.subplots()
263
+ fig.patch.set_facecolor('white')
264
+
265
+ ax.plot(res['freqs'], res['noise_q'], '.', alpha=0.8)
266
+ ax.plot(res['freqs'], res['noise_i'], '.', alpha=0.8)
267
+ txt = '\n'.join([
268
+ f"num tones: {len(res['ft_freqs_all'])}",
269
+ f"tone power: {res['tone_power']}",
270
+ f"UC att: {res['att_uc']}",
271
+ f"DC att: {res['att_dc']}",
272
+ ])
273
+ ax.text(0.05, 0.8, txt, transform=ax.transAxes,
274
+ bbox=dict(fc='white', alpha=0.6))
275
+ ax.set_xlabel("Freq [MHz]")
276
+ ax.set_ylabel("Noise [dBc/Hz]")
277
+ crate_id = res['meta']['crate_id']
278
+ slot = res['meta']['slot']
279
+ ax.set_title(f"Crate {crate_id}, Slot {slot}")
280
+
281
+ return fig, ax
282
+
283
+
284
+ def fixed_tone_loopback(
285
+ S, cfg, bands=None, tones_per_band=256, meas_chans_per_band=5,
286
+ setup_tones=False, tone_power=12, show_pb=True, noise_freq=30e3,
287
+ noise_bw=100, att_uc=None, att_dc=None):
288
+ """
289
+ Runs QC test to check noise levels across band with many fixed tones enabled.
290
+
291
+ Args
292
+ ------
293
+ S : SmurfControl
294
+ Pysmurf instance
295
+ cfg : DetConfig
296
+ Det config instance
297
+ bands : int, list[int]
298
+ Bands to run on. Default is all 8
299
+ tones_per_band : int
300
+ Number of fixed tones to enable per band. This defaults to 256, which
301
+ is 2048 tones total with all 8 bands enabled.
302
+ meas_chans_per_band : int
303
+ Number of channels per band to measure readout noise
304
+ setup_fixed_tones : bool
305
+ If true, will set up fixed tones across specified bands. If false,
306
+ this will skip the setup and assume tones are already set up.
307
+ tone_power : int
308
+ Tone power to use
309
+ show_pb : bool
310
+ If True will show a progress bar
311
+ noise_freq : float
312
+ Target frequency to measure the readout noise
313
+ noise_bw : float
314
+ Frequency bandwidth over which the noise median will be taken
315
+ att_uc : int
316
+ UC atten. If not set, will use current uc atten.
317
+ att_dc : int
318
+ DC atten. If not set, will use current dc atten
319
+ """
320
+ if bands is None:
321
+ bands = np.arange(8)
322
+ bands = np.atleast_1d(bands)
323
+
324
+ if att_uc is None:
325
+ att_uc = S.get_att_uc(bands[0])
326
+ else:
327
+ for b in bands:
328
+ S.set_att_uc(b, att_uc)
329
+
330
+ if att_dc is None:
331
+ att_dc = S.get_att_dc(bands[0])
332
+ else:
333
+ for b in bands:
334
+ S.set_att_dc(b, att_dc)
335
+
336
+ if setup_tones:
337
+ setup_fixed_tones(S, cfg, tones_per_band=tones_per_band, bands=bands,
338
+ tone_power=tone_power)
339
+ else: # Just update the tone power
340
+ S.log(f"Setting tone power to {tone_power} for bands {bands}...")
341
+ for band in bands:
342
+ asa = S.get_amplitude_scale_array(band)
343
+ if tone_power in np.unique(asa):
344
+ continue
345
+ asa[asa != 0] = tone_power
346
+ S.set_amplitude_scale_array(band, asa)
347
+
348
+ meas_bands = []
349
+ meas_chans = []
350
+ meas_freqs = []
351
+ ft_chans_all = []
352
+ ft_freqs_all = []
353
+ ft_bands_all = []
354
+ S.log("Finding fixed tones and meas_channels")
355
+ for band in bands:
356
+ freqs = S.get_center_frequency_array(band) \
357
+ + S.get_tone_frequency_offset_mhz(band) \
358
+ + S.get_band_center_mhz(band)
359
+
360
+ ft_chans = np.where(S.get_amplitude_scale_array(band))[0]
361
+ freqs = freqs[ft_chans]
362
+ sort_idx = np.argsort(freqs)
363
+ meas_idx = np.unique(np.round(np.linspace(
364
+ 0, len(ft_chans) - 1, meas_chans_per_band)).astype(int))
365
+ # We want to sort first so meas_chans are evenly distributed across
366
+ # freq space
367
+ meas_chans.append(ft_chans[sort_idx][meas_idx])
368
+ meas_bands.append([band for _ in meas_idx])
369
+ meas_freqs.append(freqs[sort_idx][meas_idx])
370
+ ft_chans_all.append(ft_chans)
371
+ ft_bands_all.append([band for _ in ft_chans])
372
+ ft_freqs_all.append(freqs)
373
+ meas_bands = np.hstack(meas_bands)
374
+ meas_chans = np.hstack(meas_chans)
375
+ meas_freqs = np.hstack(meas_freqs)
376
+ ft_chans_all = np.hstack(ft_chans_all)
377
+ ft_bands_all = np.hstack(ft_freqs_all)
378
+ ft_freqs_all = np.hstack(ft_freqs_all)
379
+
380
+ noise_i = np.full_like(meas_freqs, np.nan)
381
+ noise_q = np.full_like(meas_freqs, np.nan)
382
+ datfiles = []
383
+ for i in trange(len(meas_bands), disable=not(show_pb)):
384
+ b, c = meas_bands[i], meas_chans[i]
385
+ S.log(f"Band {b}, Chan {c}")
386
+ try:
387
+ noise_i[i], noise_q[i], _ = get_noise_dBcHz(
388
+ S, b, c, noise_freq=noise_freq,
389
+ noise_bw=noise_bw)
390
+ except IndexError as e:
391
+ S.log(f"Take Data failed...\n{e}")
392
+ S.log("Skipping channel")
393
+ datfiles.append(S.get_streamdatawriter_datafile())
394
+
395
+ fname = sdl.make_filename(S, 'fixed_tone_loopback.npy')
396
+ res = dict(
397
+ meta=sdl.get_metadata(S, cfg),
398
+ bands=meas_bands, channels=meas_chans, freqs=meas_freqs,
399
+ noise_i=noise_i, noise_q=noise_q,
400
+ ft_bands_all=ft_bands_all, ft_channels_all=ft_chans_all,
401
+ ft_freqs_all=ft_freqs_all, noise_freq=noise_freq,
402
+ att_uc=att_uc, att_dc=att_dc, tone_power=tone_power,
403
+ datfiles=datfiles
404
+ )
405
+ np.save(fname, res, allow_pickle=True)
406
+ S.pub.register_file(fname, 'loopback', format='npy')
407
+
408
+
409
+ fname = sdl.make_filename(S, 'fixed_tone_loopback.png', plot=True)
410
+ fig, _ = plot_fixed_tone_loopback(res)
411
+ fig.savefig(fname)
412
+ S.pub.register_file(fname, 'loopback', format='png', plot=True)
413
+
414
+ return res
415
+