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
sodetlib/util.py
ADDED
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for miscellaneous functions and classes that are useful in many sodetlib
|
|
3
|
+
scripts.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy import signal
|
|
7
|
+
import time
|
|
8
|
+
import os
|
|
9
|
+
from collections import namedtuple
|
|
10
|
+
import sodetlib
|
|
11
|
+
from sodetlib import det_config
|
|
12
|
+
from sodetlib.constants import *
|
|
13
|
+
from sotodlib.tod_ops.fft_ops import calc_psd
|
|
14
|
+
from sotodlib.core import AxisManager
|
|
15
|
+
|
|
16
|
+
if not os.environ.get('NO_PYSMURF', False):
|
|
17
|
+
try:
|
|
18
|
+
import epics
|
|
19
|
+
import pysmurf
|
|
20
|
+
from pysmurf.client.command.cryo_card import cmd_make
|
|
21
|
+
except Exception:
|
|
22
|
+
os.environ['NO_PYSMURF'] = True
|
|
23
|
+
|
|
24
|
+
StreamSeg = namedtuple("StreamSeg", "times sig mask")
|
|
25
|
+
|
|
26
|
+
class TermColors:
|
|
27
|
+
HEADER = '\n\033[95m'
|
|
28
|
+
OKBLUE = '\033[94m'
|
|
29
|
+
OKGREEN = '\033[92m'
|
|
30
|
+
WARNING = '\033[93m'
|
|
31
|
+
FAIL = '\033[91m'
|
|
32
|
+
ENDC = '\033[0m'
|
|
33
|
+
BOLD = '\033[1m'
|
|
34
|
+
UNDERLINE = '\033[4m'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cprint(msg, style=TermColors.OKBLUE):
|
|
38
|
+
if style is True:
|
|
39
|
+
style = TermColors.OKGREEN
|
|
40
|
+
elif style is False:
|
|
41
|
+
style = TermColors.FAIL
|
|
42
|
+
print(f"{style}{msg}{TermColors.ENDC}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def make_filename(S, name, ctime=None, plot=False):
|
|
46
|
+
"""
|
|
47
|
+
Creates a timestamped filename in the pysmurf outputs or plot directory.
|
|
48
|
+
"""
|
|
49
|
+
if ctime is None:
|
|
50
|
+
ctime = S.get_timestamp()
|
|
51
|
+
|
|
52
|
+
if plot:
|
|
53
|
+
ddir = S.plot_dir
|
|
54
|
+
else:
|
|
55
|
+
ddir = S.output_dir
|
|
56
|
+
|
|
57
|
+
return os.path.join(ddir, f'{ctime}_{name}')
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _encode_data(data):
|
|
61
|
+
"""
|
|
62
|
+
Encodes a data object into one that is serializable with
|
|
63
|
+
json so that it can be sent over UDP.
|
|
64
|
+
"""
|
|
65
|
+
if isinstance(data, list):
|
|
66
|
+
return [_encode_data(d) for d in data]
|
|
67
|
+
elif isinstance(data, dict):
|
|
68
|
+
return {str(k): _encode_data(d) for k, d in data.items()}
|
|
69
|
+
elif isinstance(data, np.ndarray):
|
|
70
|
+
return data.tolist()
|
|
71
|
+
else:
|
|
72
|
+
return data
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_session_data(S, key, val):
|
|
76
|
+
"""
|
|
77
|
+
Adds data to the OCS Session data object. If this is being run directly
|
|
78
|
+
from an OCS PysmurfController operation, the _ocs_session object will be
|
|
79
|
+
set in the pysmurf instance. If not, publish message so pysmurf monitor can
|
|
80
|
+
relay data to the correct controller agent.
|
|
81
|
+
"""
|
|
82
|
+
session = getattr(S, '_ocs_session', None)
|
|
83
|
+
if session is not None:
|
|
84
|
+
session.data[key] = _encode_data(val)
|
|
85
|
+
else:
|
|
86
|
+
S.pub.publish(_encode_data({key: val}), msgtype='session_data')
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def pub_ocs_log(S, msg, log=True):
|
|
91
|
+
"""
|
|
92
|
+
Passes a string to the OCS pysmurf controller to be logged to be passed
|
|
93
|
+
around the OCS network.
|
|
94
|
+
"""
|
|
95
|
+
if log:
|
|
96
|
+
S.log(msg)
|
|
97
|
+
|
|
98
|
+
session = getattr(S, '_ocs_session', None)
|
|
99
|
+
if session is not None:
|
|
100
|
+
session.add_message(msg)
|
|
101
|
+
|
|
102
|
+
S.pub.publish(msg, msgtype='session_log')
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class OCSAbortError(Exception):
|
|
106
|
+
"""Error that's thrown when an operation is aborted through OCS"""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def stop_point(S):
|
|
110
|
+
"""
|
|
111
|
+
This function will throw an error if the OCS session status is 'stopping'.
|
|
112
|
+
This allows the pysmurf controller to gracefully tell long-running sodetlib
|
|
113
|
+
functions to stop when possible without killing the agent.
|
|
114
|
+
"""
|
|
115
|
+
session = getattr(S, '_ocs_session', None)
|
|
116
|
+
if session is not None:
|
|
117
|
+
if session.status == 'stopping':
|
|
118
|
+
raise OCSAbortError()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_bgmap(bands, channels, bgmap_file):
|
|
123
|
+
"""
|
|
124
|
+
Loads bias-group assignments and channel polarity for
|
|
125
|
+
set of channels.
|
|
126
|
+
|
|
127
|
+
Args
|
|
128
|
+
----
|
|
129
|
+
bands : np.ndarray
|
|
130
|
+
Array of len <nchans> containing the smurf-band of each channel
|
|
131
|
+
channels : np.ndarray
|
|
132
|
+
Array of len <nchans> containing the smurf-channel of each channel
|
|
133
|
+
bgmap_file : str
|
|
134
|
+
Path to the bgmap file.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
--------
|
|
138
|
+
bgmap : np.ndarray
|
|
139
|
+
Array of lenght <nchans> containing bias-group assignments of each
|
|
140
|
+
channel. Unassigned channels contain -1.
|
|
141
|
+
polarity : np.ndarray
|
|
142
|
+
Array of lenght <nchans> containing the polarity of each channel,
|
|
143
|
+
or whether the squid current steps up or down with bias-current.
|
|
144
|
+
"""
|
|
145
|
+
bgmap_full = np.load(bgmap_file, allow_pickle=True).item()
|
|
146
|
+
idxs = map_band_chans(
|
|
147
|
+
bands, channels, bgmap_full['bands'], bgmap_full['channels']
|
|
148
|
+
)
|
|
149
|
+
bgmap = bgmap_full['bgmap'][idxs]
|
|
150
|
+
polarity = bgmap_full['polarity'][idxs]
|
|
151
|
+
bgmap[idxs == -1] = -1
|
|
152
|
+
|
|
153
|
+
return bgmap, polarity
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def map_band_chans(b1, c1, b2, c2, chans_per_band=512):
|
|
157
|
+
"""
|
|
158
|
+
Returns an index mapping of length nchans1 from one set of bands and
|
|
159
|
+
channels to another. Note that unmapped indices are returned as -1, so this
|
|
160
|
+
must be handled before (or after) indexing or else you'll get weird
|
|
161
|
+
results.
|
|
162
|
+
|
|
163
|
+
Args
|
|
164
|
+
----
|
|
165
|
+
b1 (np.ndarray):
|
|
166
|
+
Array of length nchans1 containing the smurf band of each channel
|
|
167
|
+
c1 (np.ndarray):
|
|
168
|
+
Array of length nchans1 containing the smurf channel of each channel
|
|
169
|
+
b2 (np.ndarray):
|
|
170
|
+
Array of length nchans2 containing the smurf band of each channel
|
|
171
|
+
c2 (np.ndarray):
|
|
172
|
+
Array of length nchans2 containing the smurf channel of each channel
|
|
173
|
+
chans_per_band (int):
|
|
174
|
+
Lets just hope this never changes.
|
|
175
|
+
"""
|
|
176
|
+
acs1 = b1 * chans_per_band + c1
|
|
177
|
+
acs2 = b2 * chans_per_band + c2
|
|
178
|
+
|
|
179
|
+
mapping = np.full_like(acs1, -1, dtype=int)
|
|
180
|
+
for i, ac in enumerate(acs1):
|
|
181
|
+
x = np.where(acs2 == ac)[0]
|
|
182
|
+
if len(x > 0):
|
|
183
|
+
mapping[i] = x[0]
|
|
184
|
+
|
|
185
|
+
return mapping
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_metadata(S, cfg):
|
|
189
|
+
"""
|
|
190
|
+
Gets collection of metadata from smurf and detconfig instance that's
|
|
191
|
+
useful for pretty much everything. This should be included in all output
|
|
192
|
+
data files created by sodetlib.
|
|
193
|
+
"""
|
|
194
|
+
return {
|
|
195
|
+
'tunefile': getattr(S, 'tune_file', None),
|
|
196
|
+
'high_low_current_ratio': S.high_low_current_ratio,
|
|
197
|
+
'R_sh': S.R_sh,
|
|
198
|
+
'pA_per_phi0': S.pA_per_phi0,
|
|
199
|
+
'rtm_bit_to_volt': S._rtm_slow_dac_bit_to_volt,
|
|
200
|
+
'bias_line_resistance': S.bias_line_resistance,
|
|
201
|
+
'chans_per_band': S.get_number_channels(),
|
|
202
|
+
'high_current_mode': get_current_mode_array(S),
|
|
203
|
+
'timestamp': time.time(),
|
|
204
|
+
'stream_id': cfg.stream_id,
|
|
205
|
+
'action': S.pub._action,
|
|
206
|
+
'action_timestamp': S.pub._action_ts,
|
|
207
|
+
'bgmap_file': cfg.dev.exp.get('bgmap_file'),
|
|
208
|
+
'iv_file': cfg.dev.exp.get('iv_file'),
|
|
209
|
+
'v_bias': S.get_tes_bias_bipolar_array(),
|
|
210
|
+
'pysmurf_client_version': pysmurf.__version__,
|
|
211
|
+
'rogue_version': S._caget(f'{S.epics_root}:AMCc:RogueVersion'),
|
|
212
|
+
'smurf_core_version': S._caget(f'{S.epics_root}:AMCc:SmurfApplication:SmurfVersion'),
|
|
213
|
+
'sodetlib_version': sodetlib.__version__,
|
|
214
|
+
'fpga_git_hash': S.get_fpga_git_hash_short(),
|
|
215
|
+
'cryocard_fw_version': S.C.get_fw_version(),
|
|
216
|
+
'crate_id': cfg.sys['crate_id'],
|
|
217
|
+
'slot': cfg.slot,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def validate_and_save(fname, data, S=None, cfg=None, register=True,
|
|
222
|
+
make_path=True):
|
|
223
|
+
"""
|
|
224
|
+
Does some basic data validation for sodetlib data files to make sure
|
|
225
|
+
they are normalized based on the following rules:
|
|
226
|
+
1. The 'meta' key exists storing metadata from smurf and cfg instances
|
|
227
|
+
such as the stream-id, pysmurf constants like high-low-current-ratio
|
|
228
|
+
etc., action and timestamp and more. See the get_metadata function
|
|
229
|
+
definition for more.
|
|
230
|
+
2. 'bands', and 'channels' arrays are defined, specifying the band/
|
|
231
|
+
channel combinations for data taking / analysis products.
|
|
232
|
+
3. 'sid' field exists to contain one or more session-ids for the
|
|
233
|
+
analysis.
|
|
234
|
+
|
|
235
|
+
Args
|
|
236
|
+
----
|
|
237
|
+
fname (str):
|
|
238
|
+
Filename or full-path of datafile. If ``make_path`` is set to True,
|
|
239
|
+
this will be turned into a file-path using the ``make_filename``
|
|
240
|
+
function. If not, this will be treated as the save-file-path
|
|
241
|
+
data (dict):
|
|
242
|
+
Data to be written to disk. This should contain at least the keys
|
|
243
|
+
``bands``, ``channels``, ``sid``, along with any other data
|
|
244
|
+
products. If the key ``meta`` does not already exist, metadata
|
|
245
|
+
will be populated using the smurf and det-config instances.
|
|
246
|
+
S (SmurfControl):
|
|
247
|
+
Pysmurf instance. This must be set if registering data file,
|
|
248
|
+
``meta`` is not yet set, or ``make_path`` is True.
|
|
249
|
+
cfg (DetConfig):
|
|
250
|
+
det-config instance. This must be set if registering data file,
|
|
251
|
+
``meta`` is not yet set, or ``make_path`` is True.
|
|
252
|
+
register (bool):
|
|
253
|
+
If True, will register the file with the pysmurf publisher
|
|
254
|
+
make_path (bool):
|
|
255
|
+
If true, will create a path by passing fname to the function
|
|
256
|
+
make_filename (putting it in the pysmurf output directory)
|
|
257
|
+
"""
|
|
258
|
+
if register or make_path or 'meta' not in data:
|
|
259
|
+
if S is None or cfg is None:
|
|
260
|
+
raise ValueError("Pysmurf and cfg instances must be set")
|
|
261
|
+
|
|
262
|
+
_data = data.copy()
|
|
263
|
+
if 'meta' not in data:
|
|
264
|
+
meta = get_metadata(S, cfg)
|
|
265
|
+
_data['meta'] = meta
|
|
266
|
+
|
|
267
|
+
for k in ['channels', 'bands', 'sid']:
|
|
268
|
+
if k not in data:
|
|
269
|
+
raise ValueError(f"Key '{k}' is required in data")
|
|
270
|
+
|
|
271
|
+
if make_path and S is not None:
|
|
272
|
+
path = make_filename(S, fname)
|
|
273
|
+
else:
|
|
274
|
+
path = fname
|
|
275
|
+
|
|
276
|
+
np.save(path, _data, allow_pickle=True)
|
|
277
|
+
if S is not None and register:
|
|
278
|
+
S.pub.register_file(path, 'sodetlib_data', format='npy')
|
|
279
|
+
return path
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def get_tracking_kwargs(S, cfg, band, kwargs=None):
|
|
283
|
+
band_cfg = cfg.dev.bands[band]
|
|
284
|
+
tk = {
|
|
285
|
+
'reset_rate_khz': cfg.dev.exp['flux_ramp_rate_khz'],
|
|
286
|
+
'lms_freq_hz': band_cfg['lms_freq_hz'],
|
|
287
|
+
'lms_gain': band_cfg['lms_gain'],
|
|
288
|
+
'fraction_full_scale': band_cfg['frac_pp'],
|
|
289
|
+
'make_plot': True,
|
|
290
|
+
'show_plot': True,
|
|
291
|
+
'channel': [],
|
|
292
|
+
'nsamp': 2**18,
|
|
293
|
+
'return_data': True,
|
|
294
|
+
'feedback_start_frac': cfg.dev.exp['feedback_start_frac'],
|
|
295
|
+
'feedback_end_frac': cfg.dev.exp['feedback_end_frac'],
|
|
296
|
+
'feedback_gain': band_cfg['feedback_gain'],
|
|
297
|
+
}
|
|
298
|
+
if kwargs is not None:
|
|
299
|
+
tk.update(kwargs)
|
|
300
|
+
return tk
|
|
301
|
+
|
|
302
|
+
def get_psd(S, times, phases, detrend='constant', nperseg=2**12, fs=None):
|
|
303
|
+
"""
|
|
304
|
+
Returns PSD for all channels.
|
|
305
|
+
Args:
|
|
306
|
+
S:
|
|
307
|
+
pysmurf.SmurfControl object
|
|
308
|
+
times: np.ndarray
|
|
309
|
+
timestamps (in ns)
|
|
310
|
+
phases: np.ndarray
|
|
311
|
+
Array of phases
|
|
312
|
+
detrend: str
|
|
313
|
+
Detrend argument to pass to signal.welch
|
|
314
|
+
nperseg: int
|
|
315
|
+
nperseg arg for signal.welch
|
|
316
|
+
fs: float
|
|
317
|
+
sample frequency for signal.welch. If None will calculate using the
|
|
318
|
+
timestamp array.
|
|
319
|
+
Returns:
|
|
320
|
+
f: np.ndarray
|
|
321
|
+
Frequencies
|
|
322
|
+
Pxx: np.ndarray
|
|
323
|
+
PSD in pA/sqrt(Hz)
|
|
324
|
+
"""
|
|
325
|
+
if fs is None:
|
|
326
|
+
fs = 1/np.diff(times/1e9).mean()
|
|
327
|
+
current = phases * S.pA_per_phi0 / (2 * np.pi)
|
|
328
|
+
f, Pxx = signal.welch(current, detrend=detrend, nperseg=nperseg, fs=fs)
|
|
329
|
+
Pxx = np.sqrt(Pxx)
|
|
330
|
+
return f, Pxx
|
|
331
|
+
|
|
332
|
+
def get_asd(am, pA_per_phi0=9e6, **psd_kwargs):
|
|
333
|
+
"""
|
|
334
|
+
Returns ASD (sqrt(PSD)) for all channels.
|
|
335
|
+
|
|
336
|
+
Args
|
|
337
|
+
----
|
|
338
|
+
am: AxisManager
|
|
339
|
+
timestamps (in ns)
|
|
340
|
+
pA_per_phi0: float
|
|
341
|
+
Conversion from phi_0 to pA, defaults to 9e6 set by the mux chip
|
|
342
|
+
mutual inductance between TES to SQUID.
|
|
343
|
+
psd_kwargs: dictionary
|
|
344
|
+
keyword arguments taken by scipy.welch function.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
f: np.ndarray
|
|
348
|
+
Frequencies
|
|
349
|
+
Axx: np.ndarray
|
|
350
|
+
ASD in pA/sqrt(Hz)
|
|
351
|
+
"""
|
|
352
|
+
f, Pxx = calc_psd(am, **psd_kwargs)
|
|
353
|
+
Axx = np.sqrt(Pxx)*pA_per_phi0/(2 * np.pi)
|
|
354
|
+
return f, Axx
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class SectionTimer:
|
|
358
|
+
def __init__(self):
|
|
359
|
+
self.sections = []
|
|
360
|
+
self.start_time = None
|
|
361
|
+
self.stop_time = None
|
|
362
|
+
|
|
363
|
+
def start_section(self, name):
|
|
364
|
+
if self.start_time is None:
|
|
365
|
+
self.start_time = time.time()
|
|
366
|
+
self.sections.append((time.time(), name))
|
|
367
|
+
|
|
368
|
+
def stop(self):
|
|
369
|
+
self.stop_time = time.time()
|
|
370
|
+
self.sections.append((time.time(), 'STOP'))
|
|
371
|
+
|
|
372
|
+
def reset(self):
|
|
373
|
+
self.sections = []
|
|
374
|
+
self.start_time = None
|
|
375
|
+
self.stop_time = None
|
|
376
|
+
|
|
377
|
+
def summary(self):
|
|
378
|
+
out = "="*80 + '\nTiming Summary\n' + '-'*80 + '\n'
|
|
379
|
+
out += f"Total time: {self.stop_time - self.start_time} sec\n"
|
|
380
|
+
out += 'name\tdur\tstart\n' + '='*80 + '\n'
|
|
381
|
+
|
|
382
|
+
name_len = max([len(name) for t, name in self.sections])
|
|
383
|
+
|
|
384
|
+
for i in range(len(self.sections) - 1):
|
|
385
|
+
t, name = self.sections[i]
|
|
386
|
+
dur = self.sections[i+1][0] - t
|
|
387
|
+
out += f'{name:{name_len}s}\t{dur:.2f}\t{t:.0f}\n'
|
|
388
|
+
|
|
389
|
+
return out
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def dev_cfg_from_pysmurf(S, save_file=None, clobber=True):
|
|
393
|
+
"""
|
|
394
|
+
Creates a populated device cfg object from a fully tuned pysmurf instance.
|
|
395
|
+
If a save-file is specifed, the device config file will be written there.
|
|
396
|
+
By default this will not save the device config to a file!! If you want
|
|
397
|
+
overwrite the currently used device cfg, you can run::
|
|
398
|
+
|
|
399
|
+
dev_cfg_from_pysmurf(S, save_file=cfg.dev_file, clobber=True)
|
|
400
|
+
|
|
401
|
+
Args
|
|
402
|
+
----
|
|
403
|
+
S : SmurfControl object
|
|
404
|
+
The pysmurf instance should be in a state where a tunefile is loaded,
|
|
405
|
+
attenuations, biases, and any other parameter are already set
|
|
406
|
+
correctly.
|
|
407
|
+
save_file : path
|
|
408
|
+
Path to save-file location. Remember that if you are running in a
|
|
409
|
+
docker container, you have to give the path as it is inside the
|
|
410
|
+
container. For example, the OCS_CONFIG_DIR is mapped to /config inside
|
|
411
|
+
the docker.
|
|
412
|
+
clobber : bool
|
|
413
|
+
If true, will overwrite the save_file if one already exists at that
|
|
414
|
+
location.
|
|
415
|
+
"""
|
|
416
|
+
dev = det_config.DeviceConfig()
|
|
417
|
+
|
|
418
|
+
# Experiment setup
|
|
419
|
+
amp_biases = S.get_amplifier_biases()
|
|
420
|
+
if hasattr(S, 'tune_file'):
|
|
421
|
+
tunefile = S.tune_file
|
|
422
|
+
else:
|
|
423
|
+
cprint("No tunefile is loaded! Loading tunefile=None", False)
|
|
424
|
+
tunefile = None
|
|
425
|
+
dev.exp.update({
|
|
426
|
+
'amp_50k_Id': amp_biases['50K_Id'],
|
|
427
|
+
'amp_50k_Vg': amp_biases['50K_Vg'],
|
|
428
|
+
'amp_hemt_Id': amp_biases['hemt_Id'],
|
|
429
|
+
'amp_hemt_Vg': amp_biases['hemt_Vg'],
|
|
430
|
+
'tunefile': tunefile,
|
|
431
|
+
'bias_line_resistance': S._bias_line_resistance,
|
|
432
|
+
'high_low_current_ratio': S._high_low_current_ratio,
|
|
433
|
+
'pA_per_phi0': S._pA_per_phi0,
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
# Right now not getting any bias group info
|
|
437
|
+
for band in S._bands:
|
|
438
|
+
tone_powers = S.get_amplitude_scale_array(band)[S.which_on(band)]
|
|
439
|
+
if len(tone_powers) == 0:
|
|
440
|
+
drive = S._amplitude_scale[band]
|
|
441
|
+
cprint(f"No channels are on in band {band}. Setting drive to "
|
|
442
|
+
f"pysmurf-cfg value: {drive}", style=TermColors.WARNING)
|
|
443
|
+
else:
|
|
444
|
+
drives, counts = np.unique(tone_powers, return_counts=True)
|
|
445
|
+
drive = drives[np.nanargmax(counts)]
|
|
446
|
+
if len(drives) > 1:
|
|
447
|
+
print(f"Multiple drive powers exist for band {band} ({drives})!")
|
|
448
|
+
print(f"Using most common power: {drive}")
|
|
449
|
+
|
|
450
|
+
feedback_start_frac = S._feedback_to_feedback_frac(band, S.get_feedback_start(band))
|
|
451
|
+
feedback_end_frac = S._feedback_to_feedback_frac(band, S.get_feedback_end(band))
|
|
452
|
+
|
|
453
|
+
flux_ramp_rate_khz = S.get_flux_ramp_freq()
|
|
454
|
+
lms_freq_hz = S.get_lms_freq_hz(band)
|
|
455
|
+
nphi0 = np.round(lms_freq_hz / flux_ramp_rate_khz / 1e3)
|
|
456
|
+
|
|
457
|
+
dev.bands[band].update({
|
|
458
|
+
'uc_att': S.get_att_uc(band),
|
|
459
|
+
'dc_att': S.get_att_dc(band),
|
|
460
|
+
'drive': drive,
|
|
461
|
+
'feedback_start_frac': feedback_start_frac,
|
|
462
|
+
'feedback_end_frac': feedback_end_frac,
|
|
463
|
+
'lms_gain': S.get_lms_gain(band),
|
|
464
|
+
'frac_pp': S.get_fraction_full_scale(),
|
|
465
|
+
'flux_ramp_rate_khz': flux_ramp_rate_khz,
|
|
466
|
+
'lms_freq_hz': lms_freq_hz,
|
|
467
|
+
'nphi0': nphi0
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
if save_file is not None:
|
|
471
|
+
if clobber and os.path.exists(save_file):
|
|
472
|
+
print(f"Rewriting existing file: {save_file}")
|
|
473
|
+
dev.dump(save_file, clobber=clobber)
|
|
474
|
+
return dev
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def get_wls_from_am(am, nperseg=2**16, fmin=10., fmax=20., pA_per_phi0=9e6):
|
|
478
|
+
"""
|
|
479
|
+
Gets white-noise levels for each channel from the axis manager returned
|
|
480
|
+
by smurf_ops.load_session.
|
|
481
|
+
|
|
482
|
+
Args
|
|
483
|
+
----
|
|
484
|
+
am : AxisManager
|
|
485
|
+
Smurf data returned by so.load_session or the G3tSmurf class
|
|
486
|
+
nperseg : int
|
|
487
|
+
nperseg to be passed to welch
|
|
488
|
+
fmin : float
|
|
489
|
+
Min frequency to use for white noise mask
|
|
490
|
+
fmax : float
|
|
491
|
+
Max freq to use for white noise mask
|
|
492
|
+
pA_per_phi0 : float
|
|
493
|
+
S.pA_per_phi0 unit conversion. This will eventually make its way
|
|
494
|
+
into the axis manager, but for now I'm just hardcoding this here
|
|
495
|
+
as a keyword argument until we get there.
|
|
496
|
+
|
|
497
|
+
Returns
|
|
498
|
+
--------
|
|
499
|
+
wls : array of floats
|
|
500
|
+
Array of the white-noise level for each channel, indexed by readout-
|
|
501
|
+
channel number
|
|
502
|
+
band_medians : array of floats
|
|
503
|
+
Array of the median white noise level for each band.
|
|
504
|
+
"""
|
|
505
|
+
fsamp = 1./np.median(np.diff(am.timestamps))
|
|
506
|
+
fs, pxx = signal.welch(am.signal * pA_per_phi0 / (2*np.pi),
|
|
507
|
+
fs=fsamp, nperseg=nperseg)
|
|
508
|
+
pxx = np.sqrt(pxx)
|
|
509
|
+
fmask = (fmin < fs) & (fs < fmax)
|
|
510
|
+
wls = np.median(pxx[:, fmask], axis=1)
|
|
511
|
+
band_medians = np.zeros(8)
|
|
512
|
+
for i in range(8):
|
|
513
|
+
m = am.ch_info.band == i
|
|
514
|
+
band_medians[i] = np.median(wls[m])
|
|
515
|
+
return wls, band_medians
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# useful functions for analysis etc.
|
|
519
|
+
def invert_mask(mask):
|
|
520
|
+
"""
|
|
521
|
+
Converts a readout mask from (band, chan)->rchan form to rchan->abs_chan
|
|
522
|
+
form.
|
|
523
|
+
"""
|
|
524
|
+
bands, chans = np.where(mask != -1)
|
|
525
|
+
maskinv = np.zeros_like(bands, dtype=np.int16)
|
|
526
|
+
for b, c in zip(bands, chans):
|
|
527
|
+
maskinv[mask[b, c]] = b * CHANS_PER_BAND + c
|
|
528
|
+
return maskinv
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def get_r2(sig, sig_hat):
|
|
532
|
+
""" Gets r-squared value for a signal"""
|
|
533
|
+
sst = np.sum((sig - sig.mean())**2)
|
|
534
|
+
sse = np.sum((sig - sig_hat)**2)
|
|
535
|
+
r2 = 1 - sse / sst
|
|
536
|
+
if r2 < 0:
|
|
537
|
+
return 0
|
|
538
|
+
return r2
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
class _Register:
|
|
542
|
+
def __init__(self, S, addr):
|
|
543
|
+
self.S = S
|
|
544
|
+
self.addr = S.epics_root + ":" + addr
|
|
545
|
+
|
|
546
|
+
def get(self, **kw):
|
|
547
|
+
return self.S._caget(self.addr, **kw)
|
|
548
|
+
|
|
549
|
+
def set(self, val, **kw):
|
|
550
|
+
self.S._caput(self.addr, val, **kw)
|
|
551
|
+
|
|
552
|
+
class Registers:
|
|
553
|
+
"""
|
|
554
|
+
Utility class for storing, getting, and setting SO-rogue registers even if
|
|
555
|
+
they are not in the standard rogue tree, or settable by existing pysmurf
|
|
556
|
+
get/set functions
|
|
557
|
+
"""
|
|
558
|
+
_root = 'AMCc:'
|
|
559
|
+
_processor = _root + "SmurfProcessor:"
|
|
560
|
+
_sostream = _processor + "SOStream:"
|
|
561
|
+
_sofilewriter = _sostream + 'SOFileWriter:'
|
|
562
|
+
_source_root = _root + 'StreamDataSource:'
|
|
563
|
+
|
|
564
|
+
_registers = {
|
|
565
|
+
'pysmurf_action': _sostream + 'pysmurf_action',
|
|
566
|
+
'pysmurf_action_timestamp': _sostream + "pysmurf_action_timestamp",
|
|
567
|
+
'stream_tag': _sostream + "stream_tag",
|
|
568
|
+
'open_g3stream': _sostream + "open_g3stream",
|
|
569
|
+
'g3_session_id': _sofilewriter + "session_id",
|
|
570
|
+
'g3_filepath': _sofilewriter + "filepath",
|
|
571
|
+
'debug_data': _sostream + "DebugData",
|
|
572
|
+
'debug_meta': _sostream + "DebugMeta",
|
|
573
|
+
'debug_builder': _sostream + "DebugBuilder",
|
|
574
|
+
'flac_level': _sostream + "FlacLevel",
|
|
575
|
+
'source_enable': _source_root + 'SourceEnable',
|
|
576
|
+
'enable_compression': _sostream + 'EnableCompression',
|
|
577
|
+
'agg_time': _sostream + 'AggTime',
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
def __init__(self, S):
|
|
581
|
+
self.S = S
|
|
582
|
+
for name, reg in self._registers.items():
|
|
583
|
+
setattr(self, name, _Register(S, reg))
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def get_current_mode_array(S):
|
|
587
|
+
"""
|
|
588
|
+
Gets high-current-mode relay status for all bias groups
|
|
589
|
+
"""
|
|
590
|
+
relay = S.get_cryo_card_relays()
|
|
591
|
+
relay = S.get_cryo_card_relays() # querey twice to ensure update
|
|
592
|
+
bgs = np.arange(S._n_bias_groups)
|
|
593
|
+
hcms = np.zeros_like(bgs, dtype=bool)
|
|
594
|
+
for i, bg in enumerate(bgs):
|
|
595
|
+
r = np.ravel(S._pic_to_bias_group[np.where(
|
|
596
|
+
S._pic_to_bias_group[:, 1] == bg)])[0]
|
|
597
|
+
hcms[i] = (relay >> r) & 1
|
|
598
|
+
return hcms
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def set_current_mode(S, bgs, mode, const_current=True):
|
|
602
|
+
"""
|
|
603
|
+
Sets one or more bias lines to high current mode. If const_current is True,
|
|
604
|
+
will also update the DC bias voltages to try and preserve current based on
|
|
605
|
+
the set high_low_current_ratio. We need this function to replace the
|
|
606
|
+
existing pysmurf function so we can set both PV's in a single epics call,
|
|
607
|
+
minimizing heating on the cryostat.
|
|
608
|
+
|
|
609
|
+
This function will attempt to check the existing rogue relay state to
|
|
610
|
+
determine if the voltages need to be updated or not. This may not work all
|
|
611
|
+
of the time since there is no relay readback for the high-current-mode
|
|
612
|
+
relays. That means this will set the voltages incorrectly if there is
|
|
613
|
+
somehow an inconsistency between the rogue relay state and the real relay
|
|
614
|
+
state.
|
|
615
|
+
|
|
616
|
+
Args
|
|
617
|
+
----
|
|
618
|
+
S : SmurfControl
|
|
619
|
+
Pysmurf control instance
|
|
620
|
+
bgs : (int, list)
|
|
621
|
+
Bias groups to switch to high-current-mode
|
|
622
|
+
mode : int
|
|
623
|
+
1 for high-current, 0 for low-current
|
|
624
|
+
const_current : bool
|
|
625
|
+
If true, will adjust voltage values simultaneously based on
|
|
626
|
+
S.high_low_current_ratio
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
bgs = np.atleast_1d(bgs).astype(int)
|
|
630
|
+
|
|
631
|
+
# DO this twice bc pysmurf does for some reason
|
|
632
|
+
old_relay = S.get_cryo_card_relays()
|
|
633
|
+
old_relay = S.get_cryo_card_relays()
|
|
634
|
+
new_relay = np.copy(old_relay)
|
|
635
|
+
|
|
636
|
+
old_dac_volt_arr = S.get_rtm_slow_dac_volt_array()
|
|
637
|
+
new_dac_volt_arr = np.copy(old_dac_volt_arr)
|
|
638
|
+
|
|
639
|
+
for bg in bgs:
|
|
640
|
+
# Gets relay bit index for bg
|
|
641
|
+
idx = np.where(S._pic_to_bias_group[:, 1] == bg)[0][0]
|
|
642
|
+
# Bit in relay mask corresponding to this bg's high-current
|
|
643
|
+
r = S._pic_to_bias_group[idx, 0]
|
|
644
|
+
|
|
645
|
+
# Index of bg's DAC pair
|
|
646
|
+
pair_idx = np.where(S._bias_group_to_pair[:, 0] == bg)[0][0]
|
|
647
|
+
pos_idx = S._bias_group_to_pair[pair_idx, 1] - 1
|
|
648
|
+
neg_idx = S._bias_group_to_pair[pair_idx, 2] - 1
|
|
649
|
+
if mode:
|
|
650
|
+
# sets relay bit to 1
|
|
651
|
+
new_relay = new_relay | (1 << r)
|
|
652
|
+
|
|
653
|
+
# if const_current and the old_relay bit was zero, divide bias
|
|
654
|
+
# voltage by high_low_ratio
|
|
655
|
+
if const_current and not (old_relay >> r) & 1:
|
|
656
|
+
new_dac_volt_arr[pos_idx] /= S.high_low_current_ratio
|
|
657
|
+
new_dac_volt_arr[neg_idx] /= S.high_low_current_ratio
|
|
658
|
+
else:
|
|
659
|
+
# Sets relay bit to 0
|
|
660
|
+
new_relay = new_relay & ~(1 << r)
|
|
661
|
+
|
|
662
|
+
# if const_current and the old_relay bit was one, mult bias
|
|
663
|
+
# voltage by high_low_ratio
|
|
664
|
+
if const_current and (old_relay >> r) & 1:
|
|
665
|
+
new_dac_volt_arr[pos_idx] *= S.high_low_current_ratio
|
|
666
|
+
new_dac_volt_arr[neg_idx] *= S.high_low_current_ratio
|
|
667
|
+
|
|
668
|
+
relay_data = cmd_make(0, S.C.relay_address, new_relay)
|
|
669
|
+
|
|
670
|
+
# Sets DAC data values, clipping the data array to make sure they contain
|
|
671
|
+
# the correct no. of bits
|
|
672
|
+
dac_data = (new_dac_volt_arr / S._rtm_slow_dac_bit_to_volt).astype(int)
|
|
673
|
+
nbits = S._rtm_slow_dac_nbits
|
|
674
|
+
dac_data = np.clip(dac_data, -2**(nbits-1), 2**(nbits-1)-1)
|
|
675
|
+
|
|
676
|
+
dac_data_reg = S.rtm_spi_max_root + S._rtm_slow_dac_data_array_reg
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
if isinstance(S.C.writepv, str):
|
|
680
|
+
cryocard_writepv = S.C.writepv
|
|
681
|
+
else:
|
|
682
|
+
cryocard_writepv = S.C.writepv.pvname
|
|
683
|
+
|
|
684
|
+
# It takes longer for DC voltages to settle than it does to toggle the
|
|
685
|
+
# high-current relay, so we can set them at the same time when switchign
|
|
686
|
+
# to hcm, but when switching to lcm we need a sleep statement to prevent
|
|
687
|
+
# dets from latching.
|
|
688
|
+
if mode:
|
|
689
|
+
epics.caput_many([cryocard_writepv, dac_data_reg], [relay_data, dac_data],
|
|
690
|
+
wait=True)
|
|
691
|
+
else:
|
|
692
|
+
S._caput(dac_data_reg, dac_data)
|
|
693
|
+
time.sleep(0.04)
|
|
694
|
+
S._caput(cryocard_writepv, relay_data)
|
|
695
|
+
|
|
696
|
+
time.sleep(0.1) # Just to be safe
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def save_fig(S, fig, name, tag=''):
|
|
700
|
+
"""
|
|
701
|
+
Saves figure to pysmurf plots directory and publishes result.
|
|
702
|
+
|
|
703
|
+
Args
|
|
704
|
+
-----
|
|
705
|
+
S : SmurfControl
|
|
706
|
+
Pysmurf instantc
|
|
707
|
+
fig : Figure
|
|
708
|
+
Matplotlib figure to save
|
|
709
|
+
name : Figure name
|
|
710
|
+
This will be passed to ``make_filename`` so it is timestamped
|
|
711
|
+
and placed in the pysmurf plots directory.
|
|
712
|
+
"""
|
|
713
|
+
fname = make_filename(S, name, plot=True)
|
|
714
|
+
fig.savefig(fname)
|
|
715
|
+
S.pub.register_file(fname, tag, plot=True)
|
|
716
|
+
return fname
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
################################################################################
|
|
720
|
+
# AxisManager Utility Functions
|
|
721
|
+
################################################################################
|
|
722
|
+
|
|
723
|
+
class RestrictionException(Exception):
|
|
724
|
+
"""Exception for when cannot restrict AxisManger properly"""
|
|
725
|
+
|
|
726
|
+
def restrict_to_times(am, t0, t1, in_place=False):
|
|
727
|
+
"""
|
|
728
|
+
Restricts axis manager to a time range (t0, t1)
|
|
729
|
+
"""
|
|
730
|
+
m = (t0 < am.timestamps) & (am.timestamps < t1)
|
|
731
|
+
if not m.any():
|
|
732
|
+
raise RestrictionException
|
|
733
|
+
i0, i1 = np.where(m)[0][[0, -1]] + am.samps.offset
|
|
734
|
+
return am.restrict('samps', (i0, i1), in_place=in_place)
|
|
735
|
+
|
|
736
|
+
def dict_to_am(d, skip_bad_types=False):
|
|
737
|
+
"""
|
|
738
|
+
Attempts to convert a dictionary into an axis manager. This can be used on
|
|
739
|
+
dicts containing basic types such as (str, int, float) along with numpy
|
|
740
|
+
arrays. The AxisManager will not have any structure such as "axes", but
|
|
741
|
+
this is useful if you want to nest semi-arbitrary data such as the "meta"
|
|
742
|
+
dict into an axis manager.
|
|
743
|
+
|
|
744
|
+
Args
|
|
745
|
+
-----
|
|
746
|
+
d : dict
|
|
747
|
+
Dict to convert ot axismanager
|
|
748
|
+
skip_bad_types : bool
|
|
749
|
+
If True, will skip any value that is not a str, int, float, or
|
|
750
|
+
np.ndarray. If False, this will raise an error for invalid types.
|
|
751
|
+
"""
|
|
752
|
+
allowed_types = (str, int, float, np.ndarray)
|
|
753
|
+
am = AxisManager()
|
|
754
|
+
for k, v in d.items():
|
|
755
|
+
if isinstance(v, dict):
|
|
756
|
+
am.wrap(k, dict_to_am(v))
|
|
757
|
+
if isinstance(v, allowed_types):
|
|
758
|
+
am.wrap(k, v)
|
|
759
|
+
elif not skip_bad_types:
|
|
760
|
+
raise ValueError(
|
|
761
|
+
f"Key {k} is of type {type(v)} which cannot be wrapped by an "
|
|
762
|
+
"axismanager")
|
|
763
|
+
return am
|
|
764
|
+
|
|
765
|
+
def remap_dets(src, dst, load_axes=False, idxmap=None):
|
|
766
|
+
"""
|
|
767
|
+
This function takes in two axis-managers, and returns a copy of the 2nd
|
|
768
|
+
which is identical except all fields aligned with 'dets' are remapped so they
|
|
769
|
+
match up with the 'dets' field of the first array. This is very useful if you
|
|
770
|
+
want to compare data across tunes, or across cryostats. An additional field
|
|
771
|
+
"unmapped" of shape ``(dets, )`` will be added containing mask for which
|
|
772
|
+
channels were unmapped by the idxmap.
|
|
773
|
+
|
|
774
|
+
Args
|
|
775
|
+
----
|
|
776
|
+
src : AxisManager
|
|
777
|
+
"source" data. A copy of this data will be returned, with the 'dets'
|
|
778
|
+
axis remapped to match those of the dest AxisManager
|
|
779
|
+
dst : AxisManager
|
|
780
|
+
"destination" data. The 'dets' axis will be taken from this AxisManager.
|
|
781
|
+
load_axes : bool
|
|
782
|
+
If True, will add ``src`` axes into the copy, and copy the axis
|
|
783
|
+
assignments. Note that if you are trying to nest this in another
|
|
784
|
+
axismanager, this may cause trouble if 'src', and 'dst' have
|
|
785
|
+
axes with the same name but different values. If this is false,
|
|
786
|
+
only the 'dets' assignment will be used, allowing for safe nesting.
|
|
787
|
+
idxmap : optional, np.ndarry
|
|
788
|
+
If an idxmap is passed, that will be used to remap the dets axis.
|
|
789
|
+
If none is passed, an idxmap will be created to match up bands and
|
|
790
|
+
channels of the src and dst. This might be useful if you want to
|
|
791
|
+
map across cooldowns based on a detmap.
|
|
792
|
+
"""
|
|
793
|
+
if idxmap is None:
|
|
794
|
+
idxmap = map_band_chans(
|
|
795
|
+
dst.bands, dst.channels,
|
|
796
|
+
src.bands, src.channels,
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
axes = [dst.dets]
|
|
800
|
+
if load_axes:
|
|
801
|
+
axes.extend([v for k, v in src._axes.items() if k != 'dets'])
|
|
802
|
+
|
|
803
|
+
am_new = AxisManager(*axes)
|
|
804
|
+
for k, axes in src._assignments.items():
|
|
805
|
+
f = src._fields[k]
|
|
806
|
+
if isinstance(f, AxisManager):
|
|
807
|
+
am_new.wrap(k, remap_dets(f, dst, load_axes=load_axes, idxmap=idxmap))
|
|
808
|
+
elif isinstance(f, np.ndarray):
|
|
809
|
+
assignments = []
|
|
810
|
+
if load_axes:
|
|
811
|
+
assignments.extend([
|
|
812
|
+
(i, n) for i, n in enumerate(axes)
|
|
813
|
+
if n is not None and n!='dets'
|
|
814
|
+
])
|
|
815
|
+
if 'dets' in axes:
|
|
816
|
+
i = axes.index('dets')
|
|
817
|
+
f = np.rollaxis(np.rollaxis(f, i)[idxmap], -i)
|
|
818
|
+
am_new.wrap(k, f, assignments)
|
|
819
|
+
else: # Literals
|
|
820
|
+
am_new.wrap(k, f)
|
|
821
|
+
|
|
822
|
+
am_new.wrap('unmapped', idxmap == -1, [(0, 'dets')])
|
|
823
|
+
return am_new
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def overbias_dets(S, cfg, bias_groups=None, biases=None, cool_wait=None,
|
|
827
|
+
high_current_mode=False):
|
|
828
|
+
"""
|
|
829
|
+
Overbiases detectors based on parameters in the device cfg.
|
|
830
|
+
|
|
831
|
+
Args
|
|
832
|
+
------
|
|
833
|
+
S : SmurfControl
|
|
834
|
+
Pysmurf control instance
|
|
835
|
+
cfg : DetConfig
|
|
836
|
+
DetConfig instance
|
|
837
|
+
bias_groups : list
|
|
838
|
+
List of bias groups to overbias
|
|
839
|
+
biases: list
|
|
840
|
+
Array of final bias values to set after overbiasing. This must
|
|
841
|
+
be an array of length 12 where biases[bg] is the bias of bias group bg.
|
|
842
|
+
If this is not set, the ``cool_voltage`` set in the device cfg file
|
|
843
|
+
will be used for each bias group.
|
|
844
|
+
cool_wait : float
|
|
845
|
+
If set, time to wait after overbiasing dets. Defaults to no wait time.
|
|
846
|
+
high_current_mode : bool, List[bool]
|
|
847
|
+
Whether the bias groups should be left in high-current-mode. If a list is
|
|
848
|
+
passed, it should be an array of len 12 where high_current_mode[bg] is
|
|
849
|
+
the desired mode for bias line ``bg``.
|
|
850
|
+
"""
|
|
851
|
+
if bias_groups is None:
|
|
852
|
+
bias_groups = cfg.dev.exp['active_bgs']
|
|
853
|
+
|
|
854
|
+
if isinstance(high_current_mode, (bool, int)):
|
|
855
|
+
high_current_mode = [high_current_mode for _ in range(12)]
|
|
856
|
+
high_current_mode = np.atleast_1d(high_current_mode)
|
|
857
|
+
|
|
858
|
+
S.log("Overbiasing Detectors")
|
|
859
|
+
set_current_mode(S, bias_groups, 1, const_current=False)
|
|
860
|
+
for bg in bias_groups:
|
|
861
|
+
S.set_tes_bias_bipolar(bg, cfg.dev.bias_groups[bg]['overbias_voltage'])
|
|
862
|
+
|
|
863
|
+
wait_time = cfg.dev.exp['overbias_wait']
|
|
864
|
+
S.log(f"Waiting at ob volt for {wait_time} sec")
|
|
865
|
+
time.sleep(wait_time)
|
|
866
|
+
|
|
867
|
+
for bg in bias_groups:
|
|
868
|
+
if biases is not None:
|
|
869
|
+
bias = biases[bg]
|
|
870
|
+
else:
|
|
871
|
+
bias = cfg.dev.bias_groups[bg]['cool_voltage']
|
|
872
|
+
S.set_tes_bias_bipolar(bg, bias)
|
|
873
|
+
|
|
874
|
+
lcm_bgs = np.where(~high_current_mode)[0]
|
|
875
|
+
set_current_mode(S, lcm_bgs, 0, const_current=False)
|
|
876
|
+
|
|
877
|
+
if cool_wait is not None:
|
|
878
|
+
time.sleep(cool_wait)
|
|
879
|
+
|
|
880
|
+
return S.get_tes_bias_bipolar_array()
|