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,1248 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import os
|
|
3
|
+
import traceback
|
|
4
|
+
import numpy as np
|
|
5
|
+
import sodetlib as sdl
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import scipy.optimize
|
|
8
|
+
from sodetlib.operations import iv
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
from dataclasses import dataclass, asdict
|
|
11
|
+
from copy import deepcopy
|
|
12
|
+
|
|
13
|
+
np.seterr(all='ignore')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _play_tes_bipolar_waveform(S, bias_group, waveform, do_enable=True,
|
|
17
|
+
continuous=True, **kwargs):
|
|
18
|
+
"""
|
|
19
|
+
Play a bipolar waveform on the bias group.
|
|
20
|
+
Args
|
|
21
|
+
----
|
|
22
|
+
bias_group : int
|
|
23
|
+
The bias group
|
|
24
|
+
waveform : float array
|
|
25
|
+
The waveform the play on the bias group.
|
|
26
|
+
do_enable : bool, optional, default True
|
|
27
|
+
Whether to enable the DACs (similar to what is required
|
|
28
|
+
for TES bias).
|
|
29
|
+
continuous : bool, optional, default True
|
|
30
|
+
Whether to play the TES waveform continuously.
|
|
31
|
+
"""
|
|
32
|
+
bias_order = S.bias_group_to_pair[:,0]
|
|
33
|
+
|
|
34
|
+
dac_positives = S.bias_group_to_pair[:,1]
|
|
35
|
+
dac_negatives = S.bias_group_to_pair[:,2]
|
|
36
|
+
|
|
37
|
+
dac_idx = np.ravel(np.where(bias_order == bias_group))
|
|
38
|
+
|
|
39
|
+
dac_positive = dac_positives[dac_idx][0]
|
|
40
|
+
dac_negative = dac_negatives[dac_idx][0]
|
|
41
|
+
|
|
42
|
+
# https://confluence.slac.stanford.edu/display/SMuRF/SMuRF+firmware#SMuRFfirmware-RTMDACarbitrarywaveforms
|
|
43
|
+
# Target the two bipolar DACs assigned to this bias group:
|
|
44
|
+
S.set_dac_axil_addr(0, dac_positive)
|
|
45
|
+
S.set_dac_axil_addr(1, dac_negative)
|
|
46
|
+
|
|
47
|
+
# Must enable the DACs (if not enabled already)
|
|
48
|
+
if do_enable:
|
|
49
|
+
S.set_rtm_slow_dac_enable(dac_positive, 2, **kwargs)
|
|
50
|
+
S.set_rtm_slow_dac_enable(dac_negative, 2, **kwargs)
|
|
51
|
+
|
|
52
|
+
# Load waveform into each DAC's LUT table. Opposite sign so
|
|
53
|
+
# they combine coherenty
|
|
54
|
+
S.set_rtm_arb_waveform_lut_table(0, waveform)
|
|
55
|
+
S.set_rtm_arb_waveform_lut_table(1, -waveform)
|
|
56
|
+
|
|
57
|
+
# Enable waveform generation (1=on both DACs)
|
|
58
|
+
S.set_rtm_arb_waveform_enable(3)
|
|
59
|
+
|
|
60
|
+
# Continous mode to play the waveform continuously
|
|
61
|
+
if continuous:
|
|
62
|
+
S.set_rtm_arb_waveform_continuous(1)
|
|
63
|
+
else:
|
|
64
|
+
S.set_rtm_arb_waveform_continuous(0)
|
|
65
|
+
|
|
66
|
+
def _make_step_waveform(S, step_dur, step_voltage, dc_voltage):
|
|
67
|
+
"""
|
|
68
|
+
Returns a waveform for a bias step. Waveform will contain step-up
|
|
69
|
+
to (dc_voltage + step_voltage) for `step_dur` seconds, and then a
|
|
70
|
+
step-down to (dc_voltage) for `step_dur` seconds.
|
|
71
|
+
|
|
72
|
+
Args
|
|
73
|
+
-----
|
|
74
|
+
S : SmurfControl
|
|
75
|
+
Pysmurf instance
|
|
76
|
+
step_dur : float
|
|
77
|
+
Duration of each step (sec)
|
|
78
|
+
step_voltage : float
|
|
79
|
+
Amplitude of step (V)
|
|
80
|
+
dc_voltage : float
|
|
81
|
+
DC voltage of step
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
---------
|
|
85
|
+
sig : np.ndarray
|
|
86
|
+
Waveform to use
|
|
87
|
+
timer_size : float
|
|
88
|
+
Waveform timer size
|
|
89
|
+
"""
|
|
90
|
+
# Setup waveform
|
|
91
|
+
sig = np.ones(2048)
|
|
92
|
+
sig *= dc_voltage / (2*S._rtm_slow_dac_bit_to_volt)
|
|
93
|
+
sig[1024:] += step_voltage / (2*S._rtm_slow_dac_bit_to_volt)
|
|
94
|
+
# Use twice the step dur because the signal contains two steps
|
|
95
|
+
timer_size = int(step_dur*2/(6.4e-9 * 2048))
|
|
96
|
+
return sig, timer_size
|
|
97
|
+
|
|
98
|
+
def play_bias_steps_waveform(S, cfg, bias_group, step_duration, step_voltage,
|
|
99
|
+
num_steps=5, dc_bias=None):
|
|
100
|
+
"""
|
|
101
|
+
Plays bias steps on a single bias line using the Arb waveform generator
|
|
102
|
+
|
|
103
|
+
Args
|
|
104
|
+
----
|
|
105
|
+
S:
|
|
106
|
+
Pysmurf control instance
|
|
107
|
+
cfg:
|
|
108
|
+
DetConfig instance
|
|
109
|
+
bias_group: int
|
|
110
|
+
Bias groups to play bias step on. Defaults to all 12
|
|
111
|
+
step_duration: float
|
|
112
|
+
Duration of each step in sec
|
|
113
|
+
step_voltage : float
|
|
114
|
+
Step size (volts)
|
|
115
|
+
num_steps: int
|
|
116
|
+
Number of bias steps
|
|
117
|
+
dacs : str
|
|
118
|
+
Which group of DACs to play bias-steps on. Can be 'pos',
|
|
119
|
+
'neg', or 'both'
|
|
120
|
+
"""
|
|
121
|
+
if dc_bias is None:
|
|
122
|
+
dc_bias = S.get_tes_bias_bipolar(bias_group)
|
|
123
|
+
sig, timer_size = _make_step_waveform(S, step_duration, step_voltage, dc_bias)
|
|
124
|
+
S.set_rtm_arb_waveform_timer_size(timer_size, wait_done=True)
|
|
125
|
+
_play_tes_bipolar_waveform(S, bias_group, sig)
|
|
126
|
+
start_time = time.time()
|
|
127
|
+
time.sleep(step_duration * (num_steps+1))
|
|
128
|
+
stop_time = time.time()
|
|
129
|
+
S.set_rtm_arb_waveform_enable(0)
|
|
130
|
+
S.set_tes_bias_bipolar(bias_group, dc_bias)
|
|
131
|
+
return start_time, stop_time
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def play_bias_steps_dc(S, cfg, bias_groups, step_duration, step_voltage,
|
|
135
|
+
num_steps=5, dacs='pos'):
|
|
136
|
+
"""
|
|
137
|
+
Plays bias steps on a group of bias groups stepping with only one DAC
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
S:
|
|
141
|
+
Pysmurf control instance
|
|
142
|
+
cfg:
|
|
143
|
+
DetConfig instance
|
|
144
|
+
bias_group: (int, list, optional)
|
|
145
|
+
Bias groups to play bias step on. Defaults to all 12
|
|
146
|
+
step_duration: float
|
|
147
|
+
Duration of each step in sec
|
|
148
|
+
step_voltage : float
|
|
149
|
+
Step size (volts)
|
|
150
|
+
num_steps: int
|
|
151
|
+
Number of bias steps
|
|
152
|
+
dacs : str
|
|
153
|
+
Which group of DACs to play bias-steps on. Can be 'pos',
|
|
154
|
+
'neg', or 'both'
|
|
155
|
+
"""
|
|
156
|
+
if bias_groups is None:
|
|
157
|
+
bias_groups = cfg.dev.exp['active_bgs']
|
|
158
|
+
bias_groups = np.atleast_1d(bias_groups)
|
|
159
|
+
|
|
160
|
+
if dacs not in ['pos', 'neg', 'both']:
|
|
161
|
+
raise ValueError("Arg dac must be in ['pos', 'neg', 'both']")
|
|
162
|
+
|
|
163
|
+
dac_volt_array_low = S.get_rtm_slow_dac_volt_array()
|
|
164
|
+
dac_volt_array_high = dac_volt_array_low.copy()
|
|
165
|
+
|
|
166
|
+
bias_order, dac_positives, dac_negatives = S.bias_group_to_pair.T
|
|
167
|
+
|
|
168
|
+
for bg in bias_groups:
|
|
169
|
+
bg_idx = np.ravel(np.where(bias_order == bg))
|
|
170
|
+
dac_positive = dac_positives[bg_idx][0] - 1
|
|
171
|
+
dac_negative = dac_negatives[bg_idx][0] - 1
|
|
172
|
+
if dacs == 'pos':
|
|
173
|
+
dac_volt_array_high[dac_positive] += step_voltage
|
|
174
|
+
elif dacs == 'neg':
|
|
175
|
+
dac_volt_array_high[dac_negative] -= step_voltage
|
|
176
|
+
elif dacs == 'both':
|
|
177
|
+
dac_volt_array_high[dac_positive] += step_voltage / 2
|
|
178
|
+
dac_volt_array_high[dac_negative] -= step_voltage / 2
|
|
179
|
+
|
|
180
|
+
start = time.time()
|
|
181
|
+
for _ in range(num_steps):
|
|
182
|
+
S.set_rtm_slow_dac_volt_array(dac_volt_array_high)
|
|
183
|
+
time.sleep(step_duration)
|
|
184
|
+
S.set_rtm_slow_dac_volt_array(dac_volt_array_low)
|
|
185
|
+
time.sleep(step_duration)
|
|
186
|
+
stop = time.time()
|
|
187
|
+
|
|
188
|
+
return start, stop
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def exp_fit(t, A, tau, b):
|
|
192
|
+
"""
|
|
193
|
+
Fit function for exponential falloff in bias steps
|
|
194
|
+
"""
|
|
195
|
+
return A * np.exp(-t / tau) + b
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class BiasStepAnalysis:
|
|
199
|
+
"""
|
|
200
|
+
Container to manage analysis of bias steps taken with the take_bias_steps
|
|
201
|
+
function. The main function is ``run_analysis`` and will do a series of
|
|
202
|
+
analysis procedures to create a biasgroup map and calculate DC detector
|
|
203
|
+
parameters and tau_eff:
|
|
204
|
+
|
|
205
|
+
- Loads an axis manager with all the data
|
|
206
|
+
- Finds locations of step edges for each bias group
|
|
207
|
+
- Creates a bg map using the isolated bg step responses
|
|
208
|
+
- Gets detector responses of each step and aligns them based on the time
|
|
209
|
+
of the step
|
|
210
|
+
- Computes DC params R0, I0, Pj, Si from step responses
|
|
211
|
+
- Fits exponential to the average step response and estimates tau_eff.
|
|
212
|
+
|
|
213
|
+
Most analysis inputs and products will be saved to a npy file so they can
|
|
214
|
+
be loaded and re-analyzed easily on another computer like simons1.
|
|
215
|
+
|
|
216
|
+
To load data from an saved step file, you can run::
|
|
217
|
+
|
|
218
|
+
bsa = BiasStepAnalysis.load(<path>)
|
|
219
|
+
|
|
220
|
+
Attributes:
|
|
221
|
+
tunefile (path): Path of the tunefile loaded by the pysmurf instance
|
|
222
|
+
high_low_current_ratio (float):
|
|
223
|
+
Ratio of high to low current
|
|
224
|
+
R_sh (float):
|
|
225
|
+
Shunt resistance loaded into pysmurf at time of creation
|
|
226
|
+
pA_per_phi0 (float):
|
|
227
|
+
pA_per_phi0, as loaded in pysmurf at time of creation
|
|
228
|
+
rtm_bit_to_volt (float):
|
|
229
|
+
Conversion between bias dac bit and volt
|
|
230
|
+
bias_line_resistance (float):
|
|
231
|
+
Bias line resistance loaded in pysmurf at time of creation
|
|
232
|
+
high_current_mode (bool):
|
|
233
|
+
If high-current-mode was used
|
|
234
|
+
stream_id (string):
|
|
235
|
+
stream_id of the streamer this was run on.
|
|
236
|
+
sid (int):
|
|
237
|
+
Session-id of streaming session
|
|
238
|
+
start, stop (float):
|
|
239
|
+
start and stop time of all steps
|
|
240
|
+
edge_idxs (array(ints) of shape (nbgs, nsteps)):
|
|
241
|
+
Array containing indexes (wrt axis manager) of bias group steps
|
|
242
|
+
for each bg
|
|
243
|
+
edge_signs (array(+/-1) of shape (nbgs, nsteps)):
|
|
244
|
+
Array of signs of each step, denoting whether step is rising or
|
|
245
|
+
falling
|
|
246
|
+
bg_corr (array (float) of shape (nchans, nbgs)):
|
|
247
|
+
Bias group correlation array, stating likelihood that a given
|
|
248
|
+
channel belongs on a given bias group determined from the isolated
|
|
249
|
+
steps
|
|
250
|
+
bgmap (array (int) of shape (nchans)):
|
|
251
|
+
Map from readout channel to assigned bias group. -1 means not
|
|
252
|
+
assigned (that the assignment threshold was not met for any of the
|
|
253
|
+
12 bgs)
|
|
254
|
+
abs_chans (array (int) of shape (nchans)):
|
|
255
|
+
Array of the absolute smurf channel number for each channel in the
|
|
256
|
+
axis manager.
|
|
257
|
+
resp_times (array (float) shape (nbgs, npts)):
|
|
258
|
+
Shared timestamps for each of the step responses in <step_resp> and
|
|
259
|
+
<mean_resp> with respect to the bg-step location, with the step
|
|
260
|
+
occuring at t=0.
|
|
261
|
+
mean_resp (array (float) shape (nchans, npts)):
|
|
262
|
+
Step response averaged accross all bias steps for a given channel
|
|
263
|
+
in Amps.
|
|
264
|
+
step_resp (array (float) shape (nchans, nsteps, npts)):
|
|
265
|
+
Each individual step response for a given channel in amps
|
|
266
|
+
Ibias (array (float) shape (nbgs)):
|
|
267
|
+
DC bias current of each bias group (amps)
|
|
268
|
+
Vbias:
|
|
269
|
+
DC bias voltage of each bias group (volts in low-current mode)
|
|
270
|
+
dIbias (array (float) shape (nbgs)):
|
|
271
|
+
Step current for each bias group (amps)
|
|
272
|
+
dVbias (array (float) shape (nbgs)):
|
|
273
|
+
Step voltage for each bias group (volts in low-current mode)
|
|
274
|
+
dItes (array (float) shape (nchans)):
|
|
275
|
+
Array of tes step heigh for each channel (amps)
|
|
276
|
+
R0 (array (float) shape (nchans)):
|
|
277
|
+
Computed TES resistances for each channel (ohms)
|
|
278
|
+
I0 (array (float) shape (nchans)):
|
|
279
|
+
Computed TES currents for each channel (amps)
|
|
280
|
+
Pj (array (float) shape (nchans)):
|
|
281
|
+
Bias power computed for each channel (W)
|
|
282
|
+
Si (array (float) shape (nchans)):
|
|
283
|
+
Responsivity computed for each channel (1/V)
|
|
284
|
+
step_fit_tmin (array (float) shape (nchans)):
|
|
285
|
+
Time after bias step to start fitting exponential (sec)
|
|
286
|
+
step_fit_popts (array (float) of shape (nchans, 3)):
|
|
287
|
+
Optimal fit parameters (A, tau, b) for the exponential fit of each
|
|
288
|
+
channel
|
|
289
|
+
step_fit_pcovs (array (float) shape (nchans, 3, 3)):
|
|
290
|
+
Fit covariances for each channel
|
|
291
|
+
tau_eff (array (float) shape (nchans)):
|
|
292
|
+
Tau_eff for each channel (sec). Same as step_fit_popts[:, 1].
|
|
293
|
+
R_n_IV (array (float) shape (nchans)):
|
|
294
|
+
Array of normal resistances for each channel pulled from IV
|
|
295
|
+
in the device cfg.
|
|
296
|
+
Rfrac (array (float) shape (nchans)):
|
|
297
|
+
Rfrac of each channel, determined from R0 and the channel's normal
|
|
298
|
+
resistance.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
def __init__(self, S=None, cfg=None, run_kwargs=None):
|
|
302
|
+
self._S = S
|
|
303
|
+
self._cfg = cfg
|
|
304
|
+
|
|
305
|
+
self.am = None
|
|
306
|
+
self.edge_idxs = None
|
|
307
|
+
self.transition_range = None
|
|
308
|
+
|
|
309
|
+
if S is not None:
|
|
310
|
+
self.meta = sdl.get_metadata(S, cfg)
|
|
311
|
+
self.stream_id = cfg.stream_id
|
|
312
|
+
|
|
313
|
+
if run_kwargs is None:
|
|
314
|
+
run_kwargs = {}
|
|
315
|
+
self.run_kwargs = run_kwargs
|
|
316
|
+
self.high_current_mode = run_kwargs.get("high_current_mode", True)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def from_dict(cls, data) -> 'BiasStepAnalysis':
|
|
321
|
+
self = cls()
|
|
322
|
+
for k, v in data.items():
|
|
323
|
+
setattr(self, k, v)
|
|
324
|
+
return self
|
|
325
|
+
|
|
326
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
327
|
+
data = {}
|
|
328
|
+
saved_fields = [
|
|
329
|
+
# Run data and metadata
|
|
330
|
+
'bands', 'channels', 'sid', 'meta', 'run_kwargs', 'start', 'stop',
|
|
331
|
+
'high_current_mode',
|
|
332
|
+
# Bgmap data
|
|
333
|
+
'bgmap', 'polarity',
|
|
334
|
+
# Step data and fits
|
|
335
|
+
'resp_times', 'mean_resp', 'step_resp',
|
|
336
|
+
# Step fit data
|
|
337
|
+
'step_fit_tmin', 'step_fit_popts', 'step_fit_pcovs',
|
|
338
|
+
'tau_eff',
|
|
339
|
+
# Det param data
|
|
340
|
+
'transition_range', 'Ibias', 'Vbias', 'dIbias', 'dVbias', 'dItes',
|
|
341
|
+
'R0', 'I0', 'Pj', 'Si',
|
|
342
|
+
# From IV's
|
|
343
|
+
'R_n_IV', 'Rfrac',
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
for f in saved_fields:
|
|
347
|
+
if not hasattr(self, f):
|
|
348
|
+
print(f"WARNING: field {f} does not exist... "
|
|
349
|
+
"defaulting to None")
|
|
350
|
+
data[f] = getattr(self, f, None)
|
|
351
|
+
return data
|
|
352
|
+
|
|
353
|
+
def save(self, path=None) -> None:
|
|
354
|
+
data = self.to_dict()
|
|
355
|
+
if path is not None:
|
|
356
|
+
np.save(path, data, allow_pickle=True)
|
|
357
|
+
self.filepath = path
|
|
358
|
+
else:
|
|
359
|
+
self.filepath = sdl.validate_and_save(
|
|
360
|
+
'bias_step_analysis.npy', data, S=self._S, cfg=self._cfg,
|
|
361
|
+
make_path=True
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def load(cls, filepath) -> 'BiasStepAnalysis':
|
|
366
|
+
self = cls.from_dict(np.load(filepath, allow_pickle=True).item())
|
|
367
|
+
self.filepath = filepath
|
|
368
|
+
return self
|
|
369
|
+
|
|
370
|
+
def run_analysis(
|
|
371
|
+
self, create_bg_map=False, assignment_thresh=0.3, save_bg_map=True,
|
|
372
|
+
arc=None, base_dir='/data/so/timestreams', step_window=0.03, fit_tmin=1.5e-3,
|
|
373
|
+
transition=None, R0_thresh=30e-3, save=False, bg_map_file=None, fit_tau=True,
|
|
374
|
+
):
|
|
375
|
+
"""
|
|
376
|
+
Runs the bias step analysis.
|
|
377
|
+
|
|
378
|
+
Parameters:
|
|
379
|
+
create_bg_map (bool):
|
|
380
|
+
If True, will create a bg map from the step data. If False,
|
|
381
|
+
will use the bgmap from the device cfg
|
|
382
|
+
assignment_thresh (float):
|
|
383
|
+
Correlation threshold for which channels should be assigned to
|
|
384
|
+
particular bias groups.
|
|
385
|
+
save_bg_map (bool):
|
|
386
|
+
If True, will save the created bgmap to disk and set it as
|
|
387
|
+
the bgmap path in the device cfg.
|
|
388
|
+
arc (optional, G3tSmurf):
|
|
389
|
+
G3tSmurf archive. If specified, will attempt to load
|
|
390
|
+
axis-manager using archive instead of sid.
|
|
391
|
+
base_dir (optiional, str):
|
|
392
|
+
Base directory where timestreams are stored. Defaults to
|
|
393
|
+
/data/so/timestreams.
|
|
394
|
+
step_window (float):
|
|
395
|
+
Time after the bias step (in seconds) to use for the analysis.
|
|
396
|
+
fit_tmin (float):
|
|
397
|
+
DEPRECATED!! do not use.
|
|
398
|
+
transition: (tuple, bool, optional)
|
|
399
|
+
DEPRECATED!! do not use.
|
|
400
|
+
R0_thresh (float):
|
|
401
|
+
Any channel with resistance greater than R0_thresh will be
|
|
402
|
+
unassigned from its bias group under the assumption that it's
|
|
403
|
+
crosstalk
|
|
404
|
+
save (bool):
|
|
405
|
+
If true will save the analysis to a npy file.
|
|
406
|
+
bg_map_file (optional, path):
|
|
407
|
+
If create_bg_map is false and this file is not None, use this file
|
|
408
|
+
to load the bg_map.
|
|
409
|
+
fit_tau (bool):
|
|
410
|
+
If True, perform the time constant fit. If False, skip this.
|
|
411
|
+
"""
|
|
412
|
+
self._load_am(arc=arc, base_dir=base_dir)
|
|
413
|
+
self._find_bias_edges()
|
|
414
|
+
if create_bg_map:
|
|
415
|
+
self._create_bg_map(assignment_thresh=assignment_thresh,
|
|
416
|
+
save_bg_map=save_bg_map)
|
|
417
|
+
elif bg_map_file is not None:
|
|
418
|
+
self.bgmap, self.polarity = sdl.load_bgmap(
|
|
419
|
+
self.bands, self.channels, bg_map_file)
|
|
420
|
+
else:
|
|
421
|
+
self.bgmap, self.polarity = sdl.load_bgmap(
|
|
422
|
+
self.bands, self.channels, self.meta['bgmap_file'])
|
|
423
|
+
|
|
424
|
+
self._get_step_response(step_window=step_window)
|
|
425
|
+
self._compute_dc_params(R0_thresh=R0_thresh)
|
|
426
|
+
|
|
427
|
+
# Load R_n from IV
|
|
428
|
+
self.R_n_IV = np.full(self.nchans, np.nan)
|
|
429
|
+
# Rfrac determined from R0 and R_n
|
|
430
|
+
self.Rfrac = np.full(self.nchans, np.nan)
|
|
431
|
+
if self.meta['iv_file'] is not None:
|
|
432
|
+
if os.path.exists(self.meta['iv_file']):
|
|
433
|
+
iva = iv.IVAnalysis.load(self.meta['iv_file'])
|
|
434
|
+
chmap = sdl.map_band_chans(
|
|
435
|
+
self.bands, self.channels, iva.bands, iva.channels
|
|
436
|
+
)
|
|
437
|
+
self.R_n_IV = iva.R_n[chmap]
|
|
438
|
+
self.R_n_IV[chmap == -1] = np.nan
|
|
439
|
+
self.Rfrac = self.R0 / self.R_n_IV
|
|
440
|
+
|
|
441
|
+
if create_bg_map and save_bg_map and self._S is not None:
|
|
442
|
+
# Write bgmap after compute_dc_params because bg-assignment
|
|
443
|
+
# will be un-set if resistance estimation is too high.
|
|
444
|
+
ts = str(int(time.time()))
|
|
445
|
+
data = {
|
|
446
|
+
'bands': self.bands,
|
|
447
|
+
'channels': self.channels,
|
|
448
|
+
'sid': self.sid,
|
|
449
|
+
'meta': self.meta,
|
|
450
|
+
'bgmap': self.bgmap,
|
|
451
|
+
'polarity': self.polarity,
|
|
452
|
+
}
|
|
453
|
+
path = os.path.join('/data/smurf_data/bias_group_maps',
|
|
454
|
+
ts[:5],
|
|
455
|
+
self.meta['stream_id'],
|
|
456
|
+
f'{ts}_bg_map.npy')
|
|
457
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
458
|
+
sdl.validate_and_save(path, data, S=self._S, cfg=self._cfg,
|
|
459
|
+
register=True, make_path=False)
|
|
460
|
+
self._cfg.dev.update_experiment({'bgmap_file': path},
|
|
461
|
+
update_file=True)
|
|
462
|
+
|
|
463
|
+
if fit_tau:
|
|
464
|
+
self._fit_tau_effs()
|
|
465
|
+
|
|
466
|
+
if save:
|
|
467
|
+
self.save()
|
|
468
|
+
|
|
469
|
+
def _load_am(self, arc=None, base_dir='/data/so/timestreams', fix_timestamps=True):
|
|
470
|
+
"""
|
|
471
|
+
Attempts to load the axis manager from the sid or return one that's
|
|
472
|
+
already loaded. Also sets the `abs_chans` array.
|
|
473
|
+
|
|
474
|
+
TODO: adapt this to work on with a general G3tSmurf archive if
|
|
475
|
+
supplied.
|
|
476
|
+
"""
|
|
477
|
+
if self.am is None:
|
|
478
|
+
if arc:
|
|
479
|
+
self.am = arc.load_data(self.start, self.stop, stream_id=self.meta['stream_id'])
|
|
480
|
+
else:
|
|
481
|
+
self.am = sdl.load_session(self.meta['stream_id'], self.sid,
|
|
482
|
+
base_dir=base_dir)
|
|
483
|
+
|
|
484
|
+
# Fix up timestamp jitter from timestamping in software
|
|
485
|
+
if fix_timestamps:
|
|
486
|
+
fsamp, t0 = np.polyfit(self.am.primary['FrameCounter'],
|
|
487
|
+
self.am.timestamps, 1)
|
|
488
|
+
self.am.timestamps = t0 + self.am.primary['FrameCounter']*fsamp
|
|
489
|
+
if "det_info" in self.am:
|
|
490
|
+
self.bands = self.am.det_info.smurf.band
|
|
491
|
+
self.channels = self.am.det_info.smurf.channel
|
|
492
|
+
else:
|
|
493
|
+
self.bands = self.am.ch_info.band
|
|
494
|
+
self.channels = self.am.ch_info.channel
|
|
495
|
+
self.abs_chans = self.bands*512 + self.channels
|
|
496
|
+
self.nbgs = len(self.am.biases)
|
|
497
|
+
self.nchans = len(self.am.signal)
|
|
498
|
+
return self.am
|
|
499
|
+
|
|
500
|
+
def _find_bias_edges(self, am=None):
|
|
501
|
+
"""
|
|
502
|
+
Finds sample indices and signs of bias steps in timestream.
|
|
503
|
+
|
|
504
|
+
Returns
|
|
505
|
+
--------
|
|
506
|
+
edge_idxs: list
|
|
507
|
+
List containing the edge sample indices for each bias group.
|
|
508
|
+
There are n_biaslines elements, each one a np.ndarray
|
|
509
|
+
contianing a sample idx for each edge found
|
|
510
|
+
|
|
511
|
+
edge_signs: list
|
|
512
|
+
List with the same shape as edge_idxs, containing +/-1
|
|
513
|
+
depending on whether the edge is rising or falling
|
|
514
|
+
"""
|
|
515
|
+
if am is None:
|
|
516
|
+
am = self.am
|
|
517
|
+
|
|
518
|
+
edge_idxs = [[] for _ in am.biases]
|
|
519
|
+
edge_signs = [[] for _ in am.biases]
|
|
520
|
+
|
|
521
|
+
for bg, bias in enumerate(am.biases):
|
|
522
|
+
idxs = np.where(np.diff(bias) != 0)[0]
|
|
523
|
+
signs = np.sign(np.diff(bias)[idxs])
|
|
524
|
+
|
|
525
|
+
# Delete double steps
|
|
526
|
+
doubled_idxs = np.where(np.diff(signs) == 0)[0]
|
|
527
|
+
idxs = np.delete(idxs, doubled_idxs)
|
|
528
|
+
signs = np.delete(signs, doubled_idxs)
|
|
529
|
+
|
|
530
|
+
edge_idxs[bg] = idxs
|
|
531
|
+
edge_signs[bg] = signs
|
|
532
|
+
|
|
533
|
+
self.edge_idxs = edge_idxs
|
|
534
|
+
self.edge_signs = edge_signs
|
|
535
|
+
|
|
536
|
+
return edge_idxs, edge_signs
|
|
537
|
+
|
|
538
|
+
def _create_bg_map(self, step_window=0.03, assignment_thresh=0.3,
|
|
539
|
+
save_bg_map=True):
|
|
540
|
+
"""
|
|
541
|
+
Creates a bias group mapping from the bg step sweep. The step sweep
|
|
542
|
+
goes down and steps each bias group one-by-one up and down twice. A
|
|
543
|
+
bias group correlation factor is computed by integrating the diff of
|
|
544
|
+
the TES signal times the sign of the step for each bg step. The
|
|
545
|
+
correlation factors are normalized such that the sum across bias groups
|
|
546
|
+
for each channel is 1, and an assignment is made if the normalized
|
|
547
|
+
bias-group correlation is greater than some threshold.
|
|
548
|
+
|
|
549
|
+
Saves:
|
|
550
|
+
bg_corr (np.ndarray):
|
|
551
|
+
array of shape (nchans, nbgs) that contain the correlation
|
|
552
|
+
factor for each chan/bg combo (normalized st the sum is 1).
|
|
553
|
+
bgmap (np.ndarray):
|
|
554
|
+
Array of shape (nchans) containing the assigned bg of each
|
|
555
|
+
channel, or -1 if no assigment could be determined
|
|
556
|
+
"""
|
|
557
|
+
am = self._load_am()
|
|
558
|
+
if self.edge_idxs is None:
|
|
559
|
+
self._find_bias_edges()
|
|
560
|
+
|
|
561
|
+
fsamp = np.nanmean(1./np.diff(am.timestamps))
|
|
562
|
+
npts = int(fsamp * step_window)
|
|
563
|
+
|
|
564
|
+
nchans = len(am.signal)
|
|
565
|
+
nbgs = len(am.biases)
|
|
566
|
+
bgs = np.arange(nbgs)
|
|
567
|
+
bg_corr = np.zeros((nchans, nbgs))
|
|
568
|
+
|
|
569
|
+
for bg in bgs:
|
|
570
|
+
for i, ei in enumerate(self.edge_idxs[bg]):
|
|
571
|
+
s = slice(ei, ei+npts)
|
|
572
|
+
ts = am.timestamps[s]
|
|
573
|
+
if not (self.start < ts[0] < self.stop):
|
|
574
|
+
continue
|
|
575
|
+
sig = self.edge_signs[bg][i] * am.signal[:, s]
|
|
576
|
+
bg_corr[:, bg] += np.sum(np.diff(sig), axis=1)
|
|
577
|
+
abs_bg_corr = np.abs(bg_corr)
|
|
578
|
+
normalized_bg_corr = (abs_bg_corr.T / np.sum(abs_bg_corr, axis=1)).T
|
|
579
|
+
normalized_bg_corr[np.isnan(normalized_bg_corr)] = 0.
|
|
580
|
+
bgmap = np.nanargmax(normalized_bg_corr, axis=1)
|
|
581
|
+
m = np.max(normalized_bg_corr, axis=1) < assignment_thresh
|
|
582
|
+
bgmap[m] = -1
|
|
583
|
+
|
|
584
|
+
# Calculate the sign of each channel
|
|
585
|
+
self.polarity = np.ones(self.nchans, dtype=int)
|
|
586
|
+
for i in range(self.nchans):
|
|
587
|
+
self.polarity[i] = np.sign(bg_corr[i, bgmap[i]])
|
|
588
|
+
|
|
589
|
+
self.bg_corr = normalized_bg_corr
|
|
590
|
+
self.bgmap = bgmap
|
|
591
|
+
|
|
592
|
+
return self.bgmap
|
|
593
|
+
|
|
594
|
+
def _get_step_response(self, step_window=0.03, pts_before_step=20,
|
|
595
|
+
restrict_to_bg_sweep=False, am=None):
|
|
596
|
+
"""
|
|
597
|
+
Finds each channel's response to the bias step by looking at the signal
|
|
598
|
+
in a small window of <npts> around each edge-index.
|
|
599
|
+
|
|
600
|
+
Saves:
|
|
601
|
+
resp_times:
|
|
602
|
+
Array of shape (nbgs, npts) containing the shared timestamps
|
|
603
|
+
for channels on a given bias group
|
|
604
|
+
step_resp:
|
|
605
|
+
Array of shape (nchans, nsteps, npts) containing the response
|
|
606
|
+
of each channel in a window around every step
|
|
607
|
+
mean_resp:
|
|
608
|
+
Array of (nchans, npts) containing the averaged step response
|
|
609
|
+
for each channel
|
|
610
|
+
"""
|
|
611
|
+
if am is None:
|
|
612
|
+
am = self.am
|
|
613
|
+
|
|
614
|
+
fsamp = np.nanmean(1./np.diff(am.timestamps))
|
|
615
|
+
pts_after_step = int(fsamp * step_window)
|
|
616
|
+
nchans = len(am.signal)
|
|
617
|
+
nbgs = len(am.biases)
|
|
618
|
+
npts = pts_before_step + pts_after_step
|
|
619
|
+
n_edges = np.max([len(ei) for ei in self.edge_idxs])
|
|
620
|
+
|
|
621
|
+
sigs = np.full((nchans, n_edges, npts), np.nan)
|
|
622
|
+
ts = np.full((nbgs, npts), np.nan)
|
|
623
|
+
|
|
624
|
+
A_per_rad = self.meta['pA_per_phi0'] / (2*np.pi) * 1e-12
|
|
625
|
+
for bg in np.unique(self.bgmap):
|
|
626
|
+
if bg == -1:
|
|
627
|
+
continue
|
|
628
|
+
rcs = np.where(self.bgmap == bg)[0]
|
|
629
|
+
for i, ei in enumerate(self.edge_idxs[bg][:-2]):
|
|
630
|
+
if i < 2:
|
|
631
|
+
continue
|
|
632
|
+
s = slice(ei - pts_before_step, ei + pts_after_step)
|
|
633
|
+
if np.isnan(ts[bg]).all():
|
|
634
|
+
ts[bg, :] = am.timestamps[s] - am.timestamps[ei]
|
|
635
|
+
sig = self.edge_signs[bg][i] * am.signal[rcs, s] * A_per_rad
|
|
636
|
+
# Subtracts mean of last 10 pts such that step ends at 0
|
|
637
|
+
sigs[rcs, i, :] = (sig.T - np.nanmean(sig[:, -10:], axis=1)).T
|
|
638
|
+
|
|
639
|
+
self.resp_times = ts
|
|
640
|
+
self.step_resp = (sigs.T * self.polarity).T
|
|
641
|
+
self.mean_resp = (np.nanmean(sigs, axis=1).T * self.polarity).T
|
|
642
|
+
|
|
643
|
+
return ts, sigs
|
|
644
|
+
|
|
645
|
+
def _compute_R0_I0_Pj(self):
|
|
646
|
+
"""
|
|
647
|
+
Computes the DC params R0 I0 and Pj
|
|
648
|
+
"""
|
|
649
|
+
Ib = self.Ibias[self.bgmap]
|
|
650
|
+
dIb = self.dIbias[self.bgmap]
|
|
651
|
+
dItes = self.dItes
|
|
652
|
+
Ib[self.bgmap == -1] = np.nan
|
|
653
|
+
dIb[self.bgmap == -1] = np.nan
|
|
654
|
+
dIrat = dItes / dIb
|
|
655
|
+
|
|
656
|
+
R_sh = self.meta["R_sh"]
|
|
657
|
+
|
|
658
|
+
I0 = np.zeros_like(dIrat)
|
|
659
|
+
I0_nontransition = Ib * dIrat
|
|
660
|
+
I0_transition = Ib * dIrat / (2 * dIrat - 1)
|
|
661
|
+
I0[dIrat>0] = I0_nontransition[dIrat>0]
|
|
662
|
+
I0[dIrat<0] = I0_transition[dIrat<0]
|
|
663
|
+
|
|
664
|
+
Pj = I0 * R_sh * (Ib - I0)
|
|
665
|
+
R0 = Pj / I0**2
|
|
666
|
+
R0[I0 == 0] = 0
|
|
667
|
+
|
|
668
|
+
return R0, I0, Pj
|
|
669
|
+
|
|
670
|
+
def _compute_dc_params(self, R0_thresh=30e-3):
|
|
671
|
+
"""
|
|
672
|
+
Calculates Ibias, dIbias, and dItes from axis manager, and then
|
|
673
|
+
runs the DC param calc to estimate R0, I0, Pj, etc. Here you must
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
transition: (tuple)
|
|
677
|
+
Range of voltage bias values (in low-cur units) where the
|
|
678
|
+
"in-transition" resistance calculation should be used. If True,
|
|
679
|
+
or False, will use in-transition or normal calc for all
|
|
680
|
+
channels. Will default to ``cfg.dev.exp['transition_range']`` or
|
|
681
|
+
(1, 8) if that does not exist or if self._cfg is not set.
|
|
682
|
+
R0_thresh: (float)
|
|
683
|
+
Any channel with resistance greater than R0_thresh will be
|
|
684
|
+
unassigned from its bias group under the assumption that it's
|
|
685
|
+
crosstalk
|
|
686
|
+
|
|
687
|
+
Saves:
|
|
688
|
+
Ibias:
|
|
689
|
+
Array of shape (nbgs) containing the DC bias current for each
|
|
690
|
+
bias group
|
|
691
|
+
Vbias:
|
|
692
|
+
Array of shape (nbgs) containing the DC bias voltage for each
|
|
693
|
+
bias group
|
|
694
|
+
dIbias:
|
|
695
|
+
Array of shape (nbgs) containing the step current for each
|
|
696
|
+
bias group
|
|
697
|
+
dVbias:
|
|
698
|
+
Array of shape (nbgs) containing the step voltage for each
|
|
699
|
+
bias group
|
|
700
|
+
"""
|
|
701
|
+
nbgs = len(self.am.biases)
|
|
702
|
+
|
|
703
|
+
Ibias = np.full(nbgs, np.nan)
|
|
704
|
+
dIbias = np.full(nbgs, 0.0, dtype=float)
|
|
705
|
+
|
|
706
|
+
# Compute Ibias and dIbias
|
|
707
|
+
bias_line_resistance = self.meta['bias_line_resistance']
|
|
708
|
+
high_low_current_ratio = self.meta['high_low_current_ratio']
|
|
709
|
+
rtm_bit_to_volt = self.meta['rtm_bit_to_volt']
|
|
710
|
+
amp_per_bit = 2 * rtm_bit_to_volt / bias_line_resistance
|
|
711
|
+
if self.high_current_mode:
|
|
712
|
+
amp_per_bit *= high_low_current_ratio
|
|
713
|
+
for bg in range(nbgs):
|
|
714
|
+
if len(self.edge_idxs[bg]) == 0:
|
|
715
|
+
continue
|
|
716
|
+
b0 = self.am.biases[bg, self.edge_idxs[bg][0] - 3]
|
|
717
|
+
b1 = self.am.biases[bg, self.edge_idxs[bg][0] + 3]
|
|
718
|
+
Ibias[bg] = b0 * amp_per_bit
|
|
719
|
+
dIbias[bg] = (b1 - b0) * amp_per_bit
|
|
720
|
+
|
|
721
|
+
# Compute dItes
|
|
722
|
+
i0 = np.nanmean(self.mean_resp[:, :5], axis=1)
|
|
723
|
+
i1 = np.nanmean(self.mean_resp[:, -10:], axis=1)
|
|
724
|
+
dItes = i1 - i0
|
|
725
|
+
|
|
726
|
+
self.Ibias = Ibias
|
|
727
|
+
self.Vbias = Ibias * bias_line_resistance
|
|
728
|
+
self.dIbias = dIbias
|
|
729
|
+
self.dVbias = dIbias * bias_line_resistance
|
|
730
|
+
self.dItes = dItes
|
|
731
|
+
|
|
732
|
+
R0, I0, Pj = self._compute_R0_I0_Pj()
|
|
733
|
+
|
|
734
|
+
Si = -1./(I0 * (R0 - self.meta['R_sh']))
|
|
735
|
+
|
|
736
|
+
# If resistance is too high, most likely crosstalk so just reset
|
|
737
|
+
# bg mapping and det params
|
|
738
|
+
if R0_thresh is not None:
|
|
739
|
+
m = np.abs(R0) > R0_thresh
|
|
740
|
+
self.bgmap[m] = -1
|
|
741
|
+
for arr in [R0, I0, Pj, Si]:
|
|
742
|
+
arr[m] = np.nan
|
|
743
|
+
|
|
744
|
+
self.R0 = R0
|
|
745
|
+
self.I0 = I0
|
|
746
|
+
self.Pj = Pj
|
|
747
|
+
self.Si = Si
|
|
748
|
+
|
|
749
|
+
return R0, I0, Pj, Si
|
|
750
|
+
|
|
751
|
+
def _fit_tau_effs(self, tmin=1.5e-3, weight_exp=0.3):
|
|
752
|
+
"""
|
|
753
|
+
Fits mean step responses to exponential
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
tmin: float
|
|
757
|
+
DEPRECATED!! do not use.
|
|
758
|
+
|
|
759
|
+
Saves:
|
|
760
|
+
step_fit_tmin: np.ndarray
|
|
761
|
+
Array of shape (nchans) containing tmin used for each chan.
|
|
762
|
+
step_fit_popts: np.ndarray
|
|
763
|
+
Array of shape (nchans, 3) containing popt for each chan.
|
|
764
|
+
step_fit_pcovs: np.ndarray
|
|
765
|
+
Array of shape (nchans, 3, 3) containing pcov matrices for each
|
|
766
|
+
channel
|
|
767
|
+
tau_eff: np.ndarray
|
|
768
|
+
Array of shape (nchans) contianing tau_eff for each chan.
|
|
769
|
+
"""
|
|
770
|
+
nbgs = len(self.am.biases)
|
|
771
|
+
nchans = len(self.am.signal)
|
|
772
|
+
step_fit_popts = np.full((nchans, 3), np.nan)
|
|
773
|
+
step_fit_pcovs = np.full((nchans, 3, 3), np.nan)
|
|
774
|
+
step_fit_tmin = np.full(nchans, np.nan)
|
|
775
|
+
|
|
776
|
+
for bg in range(nbgs):
|
|
777
|
+
rcs = np.where(self.bgmap == bg)[0]
|
|
778
|
+
if not len(rcs):
|
|
779
|
+
continue
|
|
780
|
+
ts = self.resp_times[bg]
|
|
781
|
+
if not len(ts[~np.isnan(ts)]):
|
|
782
|
+
continue
|
|
783
|
+
for rc in rcs:
|
|
784
|
+
resp = self.mean_resp[rc]
|
|
785
|
+
tmin_m = np.abs(resp) > 0.9*np.nanmax(np.abs(resp))
|
|
786
|
+
if not tmin_m.any():
|
|
787
|
+
continue
|
|
788
|
+
tmin = np.max((0, ts[tmin_m][-1]))
|
|
789
|
+
m = (ts > tmin) & (~np.isnan(resp))
|
|
790
|
+
if not m.any():
|
|
791
|
+
continue
|
|
792
|
+
offset_guess = np.nanmean(resp[np.abs(ts - ts[-1]) < 0.01])
|
|
793
|
+
bounds = [
|
|
794
|
+
(-np.inf, 0, -np.inf),
|
|
795
|
+
(np.inf, 0.1, np.inf)
|
|
796
|
+
]
|
|
797
|
+
p0 = (0.1, 0.001, offset_guess)
|
|
798
|
+
try:
|
|
799
|
+
popt, pcov = scipy.optimize.curve_fit(
|
|
800
|
+
exp_fit, ts[m], resp[m],
|
|
801
|
+
sigma=ts[m]**weight_exp, p0=p0, bounds=bounds
|
|
802
|
+
)
|
|
803
|
+
step_fit_popts[rc] = popt
|
|
804
|
+
step_fit_pcovs[rc] = pcov
|
|
805
|
+
step_fit_tmin[rc] = tmin
|
|
806
|
+
except RuntimeError:
|
|
807
|
+
pass
|
|
808
|
+
|
|
809
|
+
self.step_fit_tmin = step_fit_tmin
|
|
810
|
+
self.step_fit_popts = step_fit_popts
|
|
811
|
+
self.step_fit_pcovs = step_fit_pcovs
|
|
812
|
+
self.tau_eff = step_fit_popts[:, 1]
|
|
813
|
+
|
|
814
|
+
####################################################################
|
|
815
|
+
# Plotting functions
|
|
816
|
+
####################################################################
|
|
817
|
+
|
|
818
|
+
def plot_steps(bsa, rc, nsteps=5, offset=0, ax=None, **kw):
|
|
819
|
+
if ax is None:
|
|
820
|
+
fig, ax = plt.subplots()
|
|
821
|
+
else:
|
|
822
|
+
fig = ax.figure
|
|
823
|
+
|
|
824
|
+
bg = bsa.bgmap[rc]
|
|
825
|
+
i0 = bsa.edge_idxs[bg][2]
|
|
826
|
+
if 3 + nsteps < len(bsa.edge_idxs[bg]):
|
|
827
|
+
i1 = bsa.edge_idxs[bg][3 + nsteps]
|
|
828
|
+
else:
|
|
829
|
+
i1 = bsa.edge_idxs[bg][-1]
|
|
830
|
+
|
|
831
|
+
sl = slice(i0, i1)
|
|
832
|
+
ts = bsa.am.timestamps[sl]
|
|
833
|
+
ts = ts - ts[0]
|
|
834
|
+
sig = bsa.am.signal[rc, sl]
|
|
835
|
+
sig = sig - np.nanmean(sig) + offset
|
|
836
|
+
ax.plot(ts, sig, **kw)
|
|
837
|
+
|
|
838
|
+
return fig, ax
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def plot_step_fit(bsa, rc, ax=None, plot_all_steps=True):
|
|
842
|
+
"""
|
|
843
|
+
Plots the step response and fit of a given readout channel
|
|
844
|
+
"""
|
|
845
|
+
if ax is None:
|
|
846
|
+
fig, ax = plt.subplots()
|
|
847
|
+
else:
|
|
848
|
+
fig = ax.figure
|
|
849
|
+
|
|
850
|
+
bg = bsa.bgmap[rc]
|
|
851
|
+
ts = bsa.resp_times[bg]
|
|
852
|
+
try:
|
|
853
|
+
m = ts > bsa.step_fit_tmin[rc]
|
|
854
|
+
except TypeError:
|
|
855
|
+
m = ts > bsa.step_fit_tmin
|
|
856
|
+
if plot_all_steps:
|
|
857
|
+
for sig in bsa.step_resp[rc]:
|
|
858
|
+
ax.plot(ts*1000, sig, alpha=0.1, color='grey')
|
|
859
|
+
ax.plot(ts*1000, bsa.mean_resp[rc], '.', label='avg step')
|
|
860
|
+
ax.plot(ts[m]*1000, exp_fit(ts[m], *bsa.step_fit_popts[rc]), label='fit')
|
|
861
|
+
|
|
862
|
+
text = '\n'.join([
|
|
863
|
+
r'$\tau_\mathrm{eff}$=' + f'{bsa.tau_eff[rc]*1000:0.2f} ms',
|
|
864
|
+
r'$R_\mathrm{TES}=$' + f'{bsa.R0[rc]* 1000:0.2f}' + r'm$\Omega$',
|
|
865
|
+
])
|
|
866
|
+
|
|
867
|
+
ax.text(0.7, 0.1, text, transform=ax.transAxes,
|
|
868
|
+
bbox={'facecolor': 'wheat', 'alpha': 0.3}, fontsize=12)
|
|
869
|
+
|
|
870
|
+
ax.legend()
|
|
871
|
+
ax.set(xlabel="Time (ms)", ylabel="Current (Amps)")
|
|
872
|
+
|
|
873
|
+
return fig, ax
|
|
874
|
+
|
|
875
|
+
def plot_iv_res_comparison(bsa, lim=None, bgs=None, ax=None):
|
|
876
|
+
if bgs is None:
|
|
877
|
+
bgs = bsa.bgs
|
|
878
|
+
bgs = np.atleast_1d(bgs)
|
|
879
|
+
|
|
880
|
+
iva = iv.IVAnalysis.load(bsa.meta['iv_file'])
|
|
881
|
+
chmap = sdl.map_band_chans(bsa.bands, bsa.channels,
|
|
882
|
+
iva.bands, iva.channels)
|
|
883
|
+
iv_res = np.full(bsa.nchans, np.nan)
|
|
884
|
+
|
|
885
|
+
if ax is None:
|
|
886
|
+
fig, ax = plt.subplots()
|
|
887
|
+
fig.patch.set_facecolor('white')
|
|
888
|
+
else:
|
|
889
|
+
fig = ax.figure
|
|
890
|
+
|
|
891
|
+
for bg in bgs:
|
|
892
|
+
m = bsa.bgmap == bg
|
|
893
|
+
vb = bsa.Vbias[bg]
|
|
894
|
+
idx = np.nanargmin(np.abs(iva.v_bias - vb))
|
|
895
|
+
iv_res = iva.R[chmap[m], idx]
|
|
896
|
+
ax.scatter(bsa.R0[m]*1000, iv_res*1000, marker='.', alpha=0.2,
|
|
897
|
+
label=f'Bias Group {bg}')
|
|
898
|
+
if lim is None:
|
|
899
|
+
lim = (-0.1, 10)
|
|
900
|
+
ax.plot(lim, lim, ls='--', color='grey', alpha=0.4)
|
|
901
|
+
ax.set(xlim=lim, ylim=lim)
|
|
902
|
+
ax.set_xlabel("Bias Step Resistance (mOhm)")
|
|
903
|
+
ax.set_ylabel("IV Resistance (mOhm)")
|
|
904
|
+
|
|
905
|
+
return fig, ax
|
|
906
|
+
|
|
907
|
+
def get_plot_text(bsa):
|
|
908
|
+
"""
|
|
909
|
+
Gets text to add to plot textboxes
|
|
910
|
+
"""
|
|
911
|
+
return '\n'.join([
|
|
912
|
+
f"stream_id: {bsa.meta['stream_id']}",
|
|
913
|
+
f"sid: {bsa.sid}",
|
|
914
|
+
f"path: {os.path.basename(bsa.filepath)}",
|
|
915
|
+
])
|
|
916
|
+
|
|
917
|
+
def plot_bg_assignment(bsa, text_loc=(0.05, 0.85), text_kw=None):
|
|
918
|
+
"""
|
|
919
|
+
Plots bias group assignment summary
|
|
920
|
+
"""
|
|
921
|
+
xs = np.arange(13)
|
|
922
|
+
ys = np.zeros(13)
|
|
923
|
+
|
|
924
|
+
xticklabels = []
|
|
925
|
+
for i in range(12):
|
|
926
|
+
ys[i] = np.sum(bsa.bgmap == i)
|
|
927
|
+
xticklabels.append(str(i))
|
|
928
|
+
ys[12] = np.sum(bsa.bgmap == -1)
|
|
929
|
+
xticklabels.append('None')
|
|
930
|
+
|
|
931
|
+
fig, ax = plt.subplots(figsize=(16, 10))
|
|
932
|
+
ax.bar(xs, ys)
|
|
933
|
+
ax.set_xticks(np.arange(13))
|
|
934
|
+
ax.set_xticklabels(xticklabels)
|
|
935
|
+
ax.set_xlabel("Bias Group", fontsize=16)
|
|
936
|
+
ax.set_ylabel("Num Channels", fontsize=16)
|
|
937
|
+
if text_kw is None:
|
|
938
|
+
text_kw = {}
|
|
939
|
+
|
|
940
|
+
ax.text(
|
|
941
|
+
*text_loc, get_plot_text(bsa), transform=ax.transAxes,
|
|
942
|
+
fontsize=18, bbox=dict(facecolor='white', alpha=0.8)
|
|
943
|
+
)
|
|
944
|
+
return fig, ax
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def plot_Rtes(bsa):
|
|
948
|
+
"""
|
|
949
|
+
Plots Rtes
|
|
950
|
+
"""
|
|
951
|
+
fig, ax = plt.subplots()
|
|
952
|
+
ax.hist(bsa.R0 * 1000, range=(-1, 10), bins=40)
|
|
953
|
+
ax.set_xlabel(r"$R_\mathrm{TES}$ (m$\Omega$)", fontsize=12)
|
|
954
|
+
return fig, ax
|
|
955
|
+
|
|
956
|
+
def plot_Rfrac(bsa, text_loc=(0.6, 0.8)):
|
|
957
|
+
"""
|
|
958
|
+
Plots Rfrac split out by bias group
|
|
959
|
+
"""
|
|
960
|
+
iva = iv.IVAnalysis.load(bsa.meta['iv_file'])
|
|
961
|
+
chmap = sdl.map_band_chans(bsa.bands, bsa.channels,
|
|
962
|
+
iva.bands, iva.channels)
|
|
963
|
+
|
|
964
|
+
Rfracs = bsa.R0 / iva.R_n[chmap]
|
|
965
|
+
lim = (-0.1, 1.2)
|
|
966
|
+
nbins=30
|
|
967
|
+
bins = np.linspace(*lim, nbins)
|
|
968
|
+
bgs = np.arange(12)
|
|
969
|
+
hists = np.zeros((len(bgs), nbins-1))
|
|
970
|
+
for bg in bgs:
|
|
971
|
+
m = bsa.bgmap == bg
|
|
972
|
+
if not m.any():
|
|
973
|
+
continue
|
|
974
|
+
hists[bg, :] = np.histogram(Rfracs[m], bins=bins)[0]
|
|
975
|
+
|
|
976
|
+
offset = 0.2* np.max(hists)
|
|
977
|
+
fig, ax = plt.subplots(figsize=(18, 10))
|
|
978
|
+
fig.patch.set_facecolor('white')
|
|
979
|
+
for bg, h in enumerate(hists):
|
|
980
|
+
ax.plot(bins[1:], h + offset * bg, '-o', markersize=4)
|
|
981
|
+
ax.fill_between(bins[1:], h + offset * bg, y2=offset * bg, alpha=0.2)
|
|
982
|
+
txt = f"{int(np.sum(h))} Chans"
|
|
983
|
+
ax.text(1.1, offset*(bg + 0.15), txt, fontsize=20)
|
|
984
|
+
|
|
985
|
+
ax.set_xlabel(r"Rfrac", fontsize=24)
|
|
986
|
+
ax.set_yticks(offset * bgs)
|
|
987
|
+
ax.set_yticklabels([f'BG {bg}' for bg in bgs], fontsize=20)
|
|
988
|
+
ax.tick_params(axis='x', labelsize=24)
|
|
989
|
+
txt = get_plot_text(bsa)
|
|
990
|
+
ax.text(*text_loc, txt, bbox=dict(facecolor='white', alpha=0.8), fontsize=20,
|
|
991
|
+
transform=ax.transAxes)
|
|
992
|
+
return fig, ax
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
@dataclass
|
|
996
|
+
class BiasStepConfig:
|
|
997
|
+
bgs: Optional[float] = None
|
|
998
|
+
dc_voltage: float = None
|
|
999
|
+
step_voltage: float = 0.05
|
|
1000
|
+
step_duration: float = 0.05
|
|
1001
|
+
nsteps: int = 20
|
|
1002
|
+
high_current_mode: bool = True
|
|
1003
|
+
hcm_wait_time: float = 3.
|
|
1004
|
+
dacs: str = 'pos'
|
|
1005
|
+
use_waveform: bool = True
|
|
1006
|
+
plot_rfrac: bool = True
|
|
1007
|
+
show_plots: bool = True
|
|
1008
|
+
run_analysis: bool = True
|
|
1009
|
+
channel_mask: Optional[np.ndarray] = None
|
|
1010
|
+
g3_tag: Optional[str] = None
|
|
1011
|
+
stream_subtype: str = 'bias_steps'
|
|
1012
|
+
enable_compression: bool = False
|
|
1013
|
+
analysis_kwargs: Optional[dict] = None
|
|
1014
|
+
|
|
1015
|
+
def __post_init__(self):
|
|
1016
|
+
if self.analysis_kwargs is None:
|
|
1017
|
+
self.analysis_kwargs = {}
|
|
1018
|
+
|
|
1019
|
+
if self.dacs not in ['pos', 'neg', 'both']:
|
|
1020
|
+
raise ValueError(f'dacs={self.dacs} not in ["pos", "neg", "both"]')
|
|
1021
|
+
|
|
1022
|
+
@dataclass
|
|
1023
|
+
class BgMapConfig(BiasStepConfig):
|
|
1024
|
+
dc_voltage: float = 0.3
|
|
1025
|
+
step_voltage: float = 0.01
|
|
1026
|
+
hcm_wait_time: float = 0
|
|
1027
|
+
plot_rfrac: bool = False
|
|
1028
|
+
stream_subtype: str = 'bgmap'
|
|
1029
|
+
|
|
1030
|
+
def __post_init__(self):
|
|
1031
|
+
super().__post_init__()
|
|
1032
|
+
|
|
1033
|
+
# Makes sure these kwargs are set by default unless otherwise
|
|
1034
|
+
# specified
|
|
1035
|
+
kw = {
|
|
1036
|
+
'assignment_thresh': 0.3, 'create_bg_map': True,
|
|
1037
|
+
'save_bg_map': True
|
|
1038
|
+
}
|
|
1039
|
+
kw.update(self.analysis_kwargs)
|
|
1040
|
+
self.analysis_kwargs = kw
|
|
1041
|
+
|
|
1042
|
+
def get_bias_step_cfg(self):
|
|
1043
|
+
return BiasStepConfig(
|
|
1044
|
+
**{f.name: getattr(self, f.name) for f in fields(BiasStepConfig)}
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
@sdl.set_action()
|
|
1049
|
+
def take_bgmap(S, cfg, **bgmap_pars):
|
|
1050
|
+
"""
|
|
1051
|
+
Function to easily create a bgmap. This will set all bias group voltages
|
|
1052
|
+
to 0 (since this is best for generating the bg map), and run bias-steps
|
|
1053
|
+
with default parameters optimal for creating a bgmap.
|
|
1054
|
+
|
|
1055
|
+
See also:
|
|
1056
|
+
---------
|
|
1057
|
+
take_bias_steps
|
|
1058
|
+
"""
|
|
1059
|
+
kw = deepcopy(cfg.dev.exp['bgmap_defaults'])
|
|
1060
|
+
kw.update(bgmap_pars)
|
|
1061
|
+
bgmap_cfg = BgMapConfig(**kw)
|
|
1062
|
+
|
|
1063
|
+
if bgmap_cfg.bgs is None:
|
|
1064
|
+
bgmap_cfg.bgs = cfg.dev.exp['active_bgs']
|
|
1065
|
+
bgmap_cfg.bgs = np.atleast_1d(bgmap_cfg.bgs)
|
|
1066
|
+
|
|
1067
|
+
bsa = take_bias_steps(S, cfg, **asdict(bgmap_cfg))
|
|
1068
|
+
|
|
1069
|
+
if hasattr(bsa, 'bgmap'):
|
|
1070
|
+
fig, ax = plot_bg_assignment(bsa)
|
|
1071
|
+
sdl.save_fig(S, fig, 'bg_assignments.png')
|
|
1072
|
+
if bgmap_cfg.show_plots:
|
|
1073
|
+
plt.show()
|
|
1074
|
+
else:
|
|
1075
|
+
plt.close()
|
|
1076
|
+
|
|
1077
|
+
for bg in bgmap_cfg.bgs:
|
|
1078
|
+
S.set_tes_bias_bipolar(bg, 0.)
|
|
1079
|
+
|
|
1080
|
+
return bsa
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
@sdl.set_action()
|
|
1084
|
+
def take_bias_steps(S, cfg, **bscfg_pars):
|
|
1085
|
+
"""
|
|
1086
|
+
Takes bias step data at the specified DC voltage. Assumes bias lines
|
|
1087
|
+
are already in low-current mode (if they are in high-current this will
|
|
1088
|
+
not run correction). This function runs bias steps and returns a
|
|
1089
|
+
BiasStepAnalysis object, which can be used to easily view and re-analyze
|
|
1090
|
+
data.
|
|
1091
|
+
|
|
1092
|
+
This function will first run a "bias group sweep", running multiple steps
|
|
1093
|
+
on each bias-line one at a time. This data is used to generate a bgmap.
|
|
1094
|
+
After, <nsteps> bias steps are played on all channels simultaneously.
|
|
1095
|
+
|
|
1096
|
+
Parameters:
|
|
1097
|
+
S (SmurfControl):
|
|
1098
|
+
Pysmurf control instance
|
|
1099
|
+
cfg (DetConfig):
|
|
1100
|
+
Detconfig instance
|
|
1101
|
+
bgs ( int, list, optional):
|
|
1102
|
+
Bias groups to run steps on.
|
|
1103
|
+
dc_voltage : float, optional
|
|
1104
|
+
DC bias used at low end of bias step to avoid divide by zeros in
|
|
1105
|
+
the analysis. If unspecified, uses the current value.
|
|
1106
|
+
step_voltage (float):
|
|
1107
|
+
Step voltage in Low-current-mode units. (i.e. this will be divided
|
|
1108
|
+
by the high-low-ratio before running the steps in high-current
|
|
1109
|
+
mode)
|
|
1110
|
+
step_duration (float):
|
|
1111
|
+
Duration in seconds of each step
|
|
1112
|
+
nsteps (int):
|
|
1113
|
+
Number of steps to run
|
|
1114
|
+
high_current_mode (bool):
|
|
1115
|
+
If true, switches to high-current-mode. If False, leaves in LCM
|
|
1116
|
+
which runs through the bias-line filter, so make sure you
|
|
1117
|
+
extend the step duration to be like >2 sec or something
|
|
1118
|
+
hcm_wait_time (float):
|
|
1119
|
+
Time to wait after switching to high-current-mode.
|
|
1120
|
+
dacs : {'pos', 'neg', 'both'}
|
|
1121
|
+
Which group of DACs to play bias-steps on.
|
|
1122
|
+
run_analysis (bool):
|
|
1123
|
+
If True, will attempt to run the analysis to calculate DC params
|
|
1124
|
+
and tau_eff. If this fails, the analysis object will
|
|
1125
|
+
still be returned but will not contain all analysis results.
|
|
1126
|
+
analysis_kwargs (dict, optional):
|
|
1127
|
+
Keyword arguments to be passed to the BiasStepAnalysis run_analysis
|
|
1128
|
+
function.
|
|
1129
|
+
channel_mask : np.ndarray, optional
|
|
1130
|
+
Mask containing absolute smurf-channels to write to disk
|
|
1131
|
+
g3_tag: string, optional
|
|
1132
|
+
Tag to attach to g3 stream.
|
|
1133
|
+
stream_subtype : optional, string
|
|
1134
|
+
Stream subtype for this operation. This will default to 'bias_steps'.
|
|
1135
|
+
enable_compression: bool, optional
|
|
1136
|
+
If True, will tell the smurf-streamer to compress G3Frames. Defaults
|
|
1137
|
+
to False because this dominates frame-processing time for high
|
|
1138
|
+
data-rate streams.
|
|
1139
|
+
plot_rfrac : bool
|
|
1140
|
+
Create rfrac plot, publish it, and save it. Default is True.
|
|
1141
|
+
show_plots : bool
|
|
1142
|
+
Show plot in addition to saving when running interactively. Default is False.
|
|
1143
|
+
"""
|
|
1144
|
+
kw = deepcopy(cfg.dev.exp['biasstep_defaults'])
|
|
1145
|
+
kw.update(bscfg_pars)
|
|
1146
|
+
bscfg = BiasStepConfig(**kw)
|
|
1147
|
+
|
|
1148
|
+
if bscfg.bgs is None:
|
|
1149
|
+
bscfg.bgs = cfg.dev.exp['active_bgs']
|
|
1150
|
+
bscfg.bgs = np.atleast_1d(bscfg.bgs)
|
|
1151
|
+
|
|
1152
|
+
# Adds to account for steps that may be cut in analysis
|
|
1153
|
+
bscfg.nsteps += 4
|
|
1154
|
+
|
|
1155
|
+
if bscfg.dc_voltage is not None:
|
|
1156
|
+
for bg in bscfg.bgs:
|
|
1157
|
+
S.set_tes_bias_bipolar(bg, bscfg.dc_voltage)
|
|
1158
|
+
|
|
1159
|
+
initial_ds_factor = S.get_downsample_factor()
|
|
1160
|
+
initial_filter_disable = S.get_filter_disable()
|
|
1161
|
+
initial_dc_biases = S.get_tes_bias_bipolar_array()
|
|
1162
|
+
init_current_mode = sdl.get_current_mode_array(S)
|
|
1163
|
+
|
|
1164
|
+
try:
|
|
1165
|
+
dc_biases = initial_dc_biases
|
|
1166
|
+
if bscfg.high_current_mode:
|
|
1167
|
+
dc_biases = dc_biases / S.high_low_current_ratio
|
|
1168
|
+
bscfg.step_voltage /= S.high_low_current_ratio
|
|
1169
|
+
sdl.set_current_mode(S, bscfg.bgs, 1)
|
|
1170
|
+
S.log(f"Waiting {bscfg.hcm_wait_time} sec after switching to hcm")
|
|
1171
|
+
time.sleep(bscfg.hcm_wait_time)
|
|
1172
|
+
|
|
1173
|
+
bsa = BiasStepAnalysis(S, cfg, run_kwargs=asdict(bscfg))
|
|
1174
|
+
|
|
1175
|
+
bsa.sid = sdl.stream_g3_on(
|
|
1176
|
+
S, tag=bscfg.g3_tag, channel_mask=bscfg.channel_mask, downsample_factor=1,
|
|
1177
|
+
filter_disable=True, subtype=bscfg.stream_subtype,
|
|
1178
|
+
enable_compression=bscfg.enable_compression
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
bsa.start = time.time()
|
|
1182
|
+
|
|
1183
|
+
for bg in bscfg.bgs:
|
|
1184
|
+
if bscfg.use_waveform:
|
|
1185
|
+
play_bias_steps_waveform(
|
|
1186
|
+
S, cfg, bg, bscfg.step_duration, bscfg.step_voltage,
|
|
1187
|
+
num_steps=bscfg.nsteps
|
|
1188
|
+
)
|
|
1189
|
+
else:
|
|
1190
|
+
play_bias_steps_dc(
|
|
1191
|
+
S, cfg, bg, bscfg.step_duration, bscfg.step_voltage,
|
|
1192
|
+
num_steps=bscfg.nsteps, dacs=bscfg.dacs,
|
|
1193
|
+
)
|
|
1194
|
+
bsa.stop = time.time()
|
|
1195
|
+
|
|
1196
|
+
finally:
|
|
1197
|
+
sdl.stream_g3_off(S)
|
|
1198
|
+
|
|
1199
|
+
# Restores current mode to initial values
|
|
1200
|
+
sdl.set_current_mode(S, np.where(init_current_mode == 0)[0], 0)
|
|
1201
|
+
sdl.set_current_mode(S, np.where(init_current_mode == 1)[0], 1)
|
|
1202
|
+
|
|
1203
|
+
S.set_downsample_factor(initial_ds_factor)
|
|
1204
|
+
S.set_filter_disable(initial_filter_disable)
|
|
1205
|
+
|
|
1206
|
+
if bscfg.run_analysis:
|
|
1207
|
+
S.log("Running bias step analysis")
|
|
1208
|
+
try:
|
|
1209
|
+
bsa.run_analysis(save=True, **bscfg.analysis_kwargs)
|
|
1210
|
+
if bscfg.plot_rfrac:
|
|
1211
|
+
fig, ax = plot_Rfrac(bsa)
|
|
1212
|
+
path = sdl.make_filename(S, 'bs_rfrac_summary.png', plot=True)
|
|
1213
|
+
fig.savefig(path)
|
|
1214
|
+
S.pub.register_file(path, 'bs_rfrac_summary', plot=True, format='png')
|
|
1215
|
+
if not bscfg.show_plots:
|
|
1216
|
+
plt.close(fig)
|
|
1217
|
+
except Exception:
|
|
1218
|
+
print(f"Bias step analysis failed with exception:")
|
|
1219
|
+
print(traceback.format_exc())
|
|
1220
|
+
|
|
1221
|
+
return bsa
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def plot_taueff_hist(bsa, text_loc=(0.4, 0.8), fontsize=15, figsize=(10, 6),
|
|
1225
|
+
range=(0, 3)):
|
|
1226
|
+
"""
|
|
1227
|
+
Plots a histogram of the tau_eff measurements for all bias lines.
|
|
1228
|
+
|
|
1229
|
+
Args
|
|
1230
|
+
-----
|
|
1231
|
+
bsa : BiasStepAnalysis
|
|
1232
|
+
analyzed BiasStepAnalysis object
|
|
1233
|
+
text_loc : tuple[float]
|
|
1234
|
+
Location of textbox in the axis coordinate system.
|
|
1235
|
+
fontsize : int
|
|
1236
|
+
font size of text in textbox
|
|
1237
|
+
figsize : tuple
|
|
1238
|
+
Figure size
|
|
1239
|
+
range : tuple
|
|
1240
|
+
histogram range
|
|
1241
|
+
"""
|
|
1242
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
1243
|
+
ax.hist(bsa.tau_eff * 1000, range=range, bins=30)
|
|
1244
|
+
ax.set_xlabel("Tau eff (ms)", fontsize=18)
|
|
1245
|
+
txt = get_plot_text(bsa)
|
|
1246
|
+
ax.text(*text_loc, txt, bbox=dict(facecolor='white', alpha=0.8),
|
|
1247
|
+
fontsize=fontsize, transform=ax.transAxes)
|
|
1248
|
+
return fig, ax
|