pytesprocess 0.1.1__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.
Files changed (60) hide show
  1. pytesprocess/__init__.py +9 -0
  2. pytesprocess/_version.py +2 -0
  3. pytesprocess/cli/__init__.py +1 -0
  4. pytesprocess/cli/commands/__init__.py +5 -0
  5. pytesprocess/cli/commands/event.py +66 -0
  6. pytesprocess/cli/commands/filter.py +17 -0
  7. pytesprocess/cli/commands/ivsweep.py +29 -0
  8. pytesprocess/cli/common.py +86 -0
  9. pytesprocess/cli/main.py +81 -0
  10. pytesprocess/config/__init__.py +4 -0
  11. pytesprocess/config/loader.py +94 -0
  12. pytesprocess/config/manager.py +297 -0
  13. pytesprocess/config/resolvers/__init__.py +5 -0
  14. pytesprocess/config/resolvers/common.py +56 -0
  15. pytesprocess/config/resolvers/feature.py +293 -0
  16. pytesprocess/config/resolvers/salting.py +86 -0
  17. pytesprocess/config/resolvers/trigger.py +84 -0
  18. pytesprocess/config/selectors.py +108 -0
  19. pytesprocess/config/validation.py +314 -0
  20. pytesprocess/config/warnings.py +2 -0
  21. pytesprocess/core/__init__.py +10 -0
  22. pytesprocess/core/algorithms.py +1455 -0
  23. pytesprocess/core/didv.py +1648 -0
  24. pytesprocess/core/eventbuilder.py +495 -0
  25. pytesprocess/core/filterbuilder.py +81 -0
  26. pytesprocess/core/filterdata.py +1849 -0
  27. pytesprocess/core/ivsweep.py +2072 -0
  28. pytesprocess/core/noise.py +923 -0
  29. pytesprocess/core/noisemodel.py +1408 -0
  30. pytesprocess/core/oftrigger.py +1035 -0
  31. pytesprocess/core/template.py +450 -0
  32. pytesprocess/process/__init__.py +6 -0
  33. pytesprocess/process/data_source.py +185 -0
  34. pytesprocess/process/event_context.py +35 -0
  35. pytesprocess/process/feature_plan.py +186 -0
  36. pytesprocess/process/feature_resources.py +267 -0
  37. pytesprocess/process/features.py +1024 -0
  38. pytesprocess/process/filterprocess.py +1176 -0
  39. pytesprocess/process/ivprocess.py +1380 -0
  40. pytesprocess/process/processing_data.py +967 -0
  41. pytesprocess/process/randoms.py +921 -0
  42. pytesprocess/process/triggers.py +1011 -0
  43. pytesprocess/salting/__init__.py +7 -0
  44. pytesprocess/salting/generator.py +364 -0
  45. pytesprocess/salting/injector.py +329 -0
  46. pytesprocess/salting/sampling.py +84 -0
  47. pytesprocess/utils/__init__.py +5 -0
  48. pytesprocess/utils/arg_utils.py +122 -0
  49. pytesprocess/utils/dataframe_output.py +120 -0
  50. pytesprocess/utils/filter_hdf5.py +594 -0
  51. pytesprocess/utils/utils.py +701 -0
  52. pytesprocess/workflows/__init__.py +3 -0
  53. pytesprocess/workflows/processing.py +317 -0
  54. pytesprocess/workflows/salting.py +133 -0
  55. pytesprocess-0.1.1.dist-info/METADATA +211 -0
  56. pytesprocess-0.1.1.dist-info/RECORD +60 -0
  57. pytesprocess-0.1.1.dist-info/WHEEL +5 -0
  58. pytesprocess-0.1.1.dist-info/entry_points.txt +2 -0
  59. pytesprocess-0.1.1.dist-info/licenses/LICENSE +21 -0
  60. pytesprocess-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1648 @@
1
+ from datetime import datetime
2
+ from pathlib import Path
3
+ import os
4
+ import stat
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import qetpy as qp
9
+ from scipy.signal import unit_impulse
10
+
11
+ from pytesdaqx.io import AcquisitionCatalog, StreamReader
12
+
13
+ from pytesprocess.core.filterdata import FilterData
14
+
15
+
16
+ class DIDVAnalysis(FilterData):
17
+ """
18
+ Class to manage didv calculations using
19
+ QETpy. DIDV data are stored
20
+ """
21
+ def __init__(self, verbose=True,
22
+ auto_save_hdf5=False,
23
+ save_path=None,
24
+ filter_data=None):
25
+
26
+ super().__init__(verbose=verbose, filter_data=filter_data)
27
+
28
+ self._didv_data = {}
29
+
30
+ # save results
31
+ self._save_hdf5 = auto_save_hdf5
32
+ self._save_path = None
33
+ self._file_name = self._set_file_name()
34
+
35
+
36
+ if save_path is not None:
37
+ path = Path(save_path)
38
+ if path.is_file():
39
+ self._save_path = str(path.parent)
40
+ self._file_name = path.name
41
+ elif path.is_dir():
42
+ self._save_path = str(path)
43
+ else:
44
+ raise ValueError('ERROR: "save_path" should be a '
45
+ 'file or path!')
46
+
47
+
48
+
49
+
50
+ # display
51
+ if self._save_hdf5:
52
+ full_path_file = self._file_name
53
+ if self._save_path is not None:
54
+ full_path_file = self._save_path + '/' + self._file_name
55
+ print(f'INFO: Results will be automatically saved '
56
+ f'in {full_path_file}')
57
+
58
+
59
+ def clear(self, channels=None):
60
+ """
61
+ Clear all data
62
+ """
63
+
64
+ # clear data
65
+ if channels is None:
66
+ self._didv_data.clear()
67
+ else:
68
+ if isinstance(channels, str):
69
+ channels = [channels]
70
+ for chan in channels:
71
+ if chan in self._didv_data:
72
+ self._didv_data.pop(chan)
73
+
74
+
75
+ def get_didv_data(self, channel):
76
+ """
77
+ Get dictionary with didv data
78
+ """
79
+
80
+ if channel in self._didv_data:
81
+ return self._didv_data[channel]
82
+ else:
83
+ raise ValueError(f'ERROR: No didv data available '
84
+ f'for channel "{channel}"!')
85
+
86
+
87
+ def get_dpdi(self, channel, poles=3):
88
+ """
89
+ Get dpdi data and frequencies
90
+ """
91
+
92
+
93
+ if channel not in self._didv_data:
94
+ raise ValueError(f'ERROR: No didv data available '
95
+ f'for channel "{channel}"!')
96
+
97
+ list_of_fitted_poles = (
98
+ self._didv_data[channel]['didvobj'].get_list_fitted_poles()
99
+ ) or []
100
+
101
+ if poles not in list_of_fitted_poles:
102
+ raise ValueError(f'ERROR: No {poles}-poles fit found for '
103
+ f'channel {channel}!')
104
+
105
+
106
+ # check dpdi available
107
+ poles_str = str(poles) + 'poles'
108
+ if 'dpdi_' + poles_str not in self._didv_data[channel]:
109
+ raise ValueError(f'ERROR: No dpdi found for '
110
+ f'channel {channel}!')
111
+
112
+ dpdi = self._didv_data[channel]['dpdi_' + poles_str]
113
+ dpdi_freqs = self._didv_data[channel]['dpdi_freqs_' + poles_str]
114
+ dpdi_err = self._didv_data[channel]['dpdi_err_' + poles_str]
115
+
116
+ return dpdi_freqs, dpdi, dpdi_err
117
+
118
+
119
+ def get_qetpy_object(self, channel):
120
+ """
121
+ Get QETpy object
122
+ """
123
+
124
+ didv_data = self.get_didv_data(channel)
125
+ if 'didvobj' in didv_data.keys():
126
+ return didv_data['didvobj']
127
+ else:
128
+ raise ValueError(f'ERROR: No dIdV QETpy object available '
129
+ f'for channel "{channel}"!')
130
+
131
+ def get_bias_params(self, channel, poles=3):
132
+ """
133
+ Get "bias" (I0, R0, P0) parameters
134
+ (can be either "true" current or set by user)
135
+ """
136
+
137
+ fit_result = self.get_fit_results(channel, poles=poles)
138
+ if 'biasparams' in fit_result.keys():
139
+ return fit_result['biasparams']
140
+ else:
141
+ raise ValueError(f'ERROR: No bias parameters available '
142
+ f'for channel "{channel}"!')
143
+
144
+
145
+ def get_bias_params_infinite_loop_gain(self, channel, poles=3):
146
+ """
147
+ Get "bias" (I0, R0, P0) parameters with infinite loop gain
148
+ (can be either "true" current or set by user)
149
+ """
150
+
151
+ if (channel in self._didv_data
152
+ and 'biasparams_ilg' in self._didv_data[channel]
153
+ and poles in self._didv_data[channel]['biasparams_ilg']):
154
+ return self._didv_data[channel]['biasparams_ilg'][poles]
155
+ else:
156
+ raise ValueError(f'ERROR: No bias parameters with infinite '
157
+ f'loop gain available for channel "{channel}"!')
158
+
159
+
160
+
161
+ def process_raw_data(self, channels, data_path,
162
+ stream=None,
163
+ overwrite=False):
164
+
165
+ """
166
+ Set dIdV data for a specific channel
167
+ form raw traces. Apply cuts and pre-process
168
+ data. The function can be called multiple
169
+ times for different channels
170
+ """
171
+
172
+ # loop channel and process data
173
+ if isinstance(channels, str):
174
+ channels = [channels]
175
+
176
+ for chan in channels:
177
+ self._process_raw_data(chan, data_path,
178
+ stream=stream,
179
+ overwrite=overwrite)
180
+
181
+ # update save path if not set already
182
+ if self._save_path is None:
183
+ self._save_path = self._default_save_path(data_path)
184
+
185
+ def set_processed_data(self, channels,
186
+ data,
187
+ overwrite=False):
188
+
189
+ """
190
+ Set dIdV data from already processed raw
191
+ data, in the form of pandas Series or
192
+ dictionary. The function can be called multiple
193
+ times for different channels
194
+ """
195
+
196
+
197
+ didv_data = data.copy()
198
+
199
+ # check channels and data
200
+ if isinstance(channels, str):
201
+ if channels not in data.keys():
202
+ didv_data = dict()
203
+ didv_data[channels] = data
204
+ channels = [channels]
205
+
206
+ for chan in channels:
207
+ if chan not in didv_data.keys():
208
+ raise ValueError(f'ERROR: Unable to find channel '
209
+ f' {chan} in data dictionary!')
210
+
211
+ # set data
212
+ for chan in channels:
213
+ self._set_processed_data(chan,
214
+ didv_data[chan],
215
+ overwrite=overwrite)
216
+
217
+ def set_bias_params_from_data(self, channel, results,
218
+ result_type='noise', tag='default',
219
+ metadata=None):
220
+ """Attach operating-point bias parameters to a processed dIdV.
221
+
222
+ The bias parameters may come from a full IV sweep or from a short
223
+ beginning-of-cycle normal-IV calibration. ``set_ivsweep_results_from_data``
224
+ remains as a compatibility wrapper around this more general method.
225
+ """
226
+ if isinstance(results, pd.Series):
227
+ results = results.to_dict()
228
+ elif not isinstance(results, dict):
229
+ raise ValueError(
230
+ 'ERROR: "results" should be a dictionary or pandas Series'
231
+ )
232
+ if not isinstance(channel, str):
233
+ raise ValueError('ERROR: This function is for a single channel!')
234
+ if channel not in self._didv_data:
235
+ raise ValueError(
236
+ f'ERROR: Channel {channel} not found. You first need to set dIdV data!'
237
+ )
238
+
239
+ biasparams = results.copy()
240
+ self.set_ivsweep_results(
241
+ channel,
242
+ pd.Series(biasparams),
243
+ result_type,
244
+ metadata=metadata,
245
+ tag=tag,
246
+ )
247
+ self._didv_data[channel]['ivsweep_results'] = biasparams.copy()
248
+
249
+ rp = biasparams.get('rp')
250
+ r0 = biasparams.get('r0')
251
+ rshunt = biasparams.get('rshunt', biasparams.get('rsh'))
252
+ if rp is None:
253
+ raise ValueError(
254
+ f'ERROR: "rp" is missing from bias parameters for channel {channel}!'
255
+ )
256
+ self._didv_data[channel]['data_config']['rp'] = rp
257
+ self._didv_data[channel]['didvobj']._rp = rp
258
+ if r0 is not None:
259
+ self._didv_data[channel]['didvobj']._r0 = r0
260
+ if rshunt is not None:
261
+ self._didv_data[channel]['data_config']['rshunt'] = rshunt
262
+ self._didv_data[channel]['didvobj']._rsh = rshunt
263
+
264
+
265
+ def set_ivsweep_results_from_data(self,
266
+ channel,
267
+ results,
268
+ iv_type='noise',
269
+ tag='default',
270
+ metadata=None):
271
+ """Compatibility wrapper for :meth:`set_bias_params_from_data`."""
272
+ return self.set_bias_params_from_data(
273
+ channel,
274
+ results,
275
+ result_type=iv_type,
276
+ tag=tag,
277
+ metadata=metadata,
278
+ )
279
+
280
+
281
+ def set_ivsweep_results_from_file(self,
282
+ channels,
283
+ file_name,
284
+ iv_type='noise',
285
+ include_bias_parameters=False):
286
+ """
287
+ Set results from IV sweeps, in particular
288
+ I0 offset, V0 offset
289
+ """
290
+
291
+ # check channels
292
+ if isinstance(channels, str):
293
+ channels = [channels]
294
+
295
+ for chan in channels:
296
+ if chan not in self._didv_data:
297
+ raise ValueError(f'ERROR: Channel {chan} not found. '
298
+ ' You first need to set data!' )
299
+
300
+
301
+ # load data from file
302
+ if file_name is not None:
303
+ self.load_hdf5(file_name)
304
+
305
+
306
+ # loop channels
307
+ for chan in channels:
308
+
309
+ # get tes bias
310
+ tes_bias = None
311
+ if include_bias_parameters:
312
+ data_config = self._didv_data[chan]['data_config']
313
+ tes_bias = data_config['tes_bias']
314
+
315
+ # get results
316
+ results = self.get_ivsweep_results(
317
+ chan,
318
+ iv_type=iv_type,
319
+ include_bias_parameters=include_bias_parameters,
320
+ tes_bias=tes_bias,
321
+ )
322
+
323
+ # save in include bias parameters
324
+ if include_bias_parameters:
325
+ pd_series = pd.Series(results.copy())
326
+ self.set_ivsweep_results(chan,
327
+ pd_series,
328
+ iv_type)
329
+
330
+ # keep a copy as part of didv data
331
+ self._didv_data[chan]['ivsweep_results'] = results.copy()
332
+
333
+ # check if Rp, Rshunt, R0 available
334
+ if 'rp' in results.keys():
335
+ rp = results['rp']
336
+ else:
337
+ raise ValueError(f'ERROR: "rp" is missing from IV results for '
338
+ f'channel {chan}. It needs to be added!')
339
+
340
+ r0 = None
341
+ if 'r0' in results.keys():
342
+ r0 = results['r0']
343
+
344
+ rshunt = None
345
+ if 'rshunt' in results.keys():
346
+ rshunt = results['rshunt']
347
+
348
+ # replace internal object data
349
+ if rp is not None:
350
+ self._didv_data[chan]['data_config']['rp'] = rp
351
+ self._didv_data[chan]['didvobj']._rp = rp
352
+
353
+ if r0 is not None:
354
+ self._didv_data[chan]['didvobj']._r0 = r0
355
+
356
+ if (rshunt is not None
357
+ and self._didv_data[chan]['data_config']['rshunt'] is None):
358
+ self._didv_data[chan]['data_config']['rshunt'] = rshunt
359
+ self._didv_data[chan]['didvobj']._rsh = rshunt
360
+
361
+
362
+
363
+ def dofit(self, list_of_poles, channels=None,
364
+ fcutoff=np.inf, bounds=None, guess_params=None,
365
+ guess_isloopgainsub1=None, lgc_fix=None,
366
+ add180phase=False, dt0=1.5e-6,
367
+ lgc_plot=False,
368
+ tag='default'):
369
+
370
+ """
371
+ Do dIdV fit for one or more channels. The function
372
+ "set_data" needs to be called prior "dofit" to store
373
+ didv data
374
+
375
+ """
376
+
377
+ # check if data available for each channel
378
+ if channels is None:
379
+ channels = self._didv_data.keys()
380
+ elif isinstance(channels, str):
381
+ channels = [channels]
382
+
383
+
384
+ for chan in channels:
385
+ if chan not in self._didv_data:
386
+ raise ValueError(
387
+ f'ERROR: dIdV data for channel {chan} not available. '
388
+ 'Set data first!'
389
+ )
390
+
391
+
392
+ # poles
393
+ if isinstance(list_of_poles, int):
394
+ list_of_poles = [list_of_poles]
395
+ else:
396
+ list_of_poles = list(list_of_poles)
397
+ list_of_poles.sort()
398
+
399
+
400
+ # loop channel and do fit
401
+ for chan in channels:
402
+
403
+ # get didv obj
404
+ didvobj = self._didv_data[chan]['didvobj']
405
+ didvobj._add180phase = add180phase
406
+ didvobj._dt0 = dt0
407
+
408
+ for poles in list_of_poles:
409
+ didvobj.dofit(
410
+ poles,
411
+ fcutoff=fcutoff,
412
+ bounds=bounds,
413
+ guess_params=guess_params,
414
+ guess_isloopgainsub1=guess_isloopgainsub1,
415
+ lgcfix=lgc_fix
416
+ )
417
+
418
+ # replace object with results included
419
+ self._didv_data[chan]['didvobj'] = didvobj
420
+
421
+
422
+ if lgc_plot:
423
+
424
+ if 2 in list_of_poles:
425
+ self.print_fit_result(chan, poles=2)
426
+
427
+ if 3 in list_of_poles:
428
+ self.print_fit_result(chan, poles=3)
429
+
430
+ self.plot_fit_result(chan)
431
+
432
+
433
+ # save data
434
+ self.save_didv_data(save_hdf5=self._save_hdf5, tag=tag)
435
+
436
+
437
+
438
+ def calc_smallsignal_params(self,
439
+ channels=None,
440
+ poles=None,
441
+ priors_fit_method=False,
442
+ lgc_diagnostics=False,
443
+ tag='default'):
444
+ """
445
+ Calculate small signal parameters with uncertainties
446
+ (optionally) using current from IV sweep.
447
+
448
+ """
449
+ print('Calculating small signal parameters...')
450
+ # check channels
451
+ if channels is None:
452
+ channels = self._didv_data.keys()
453
+ elif isinstance(channels, str):
454
+ channels = [channels]
455
+
456
+ for chan in channels:
457
+ if chan not in self._didv_data.keys():
458
+ raise ValueError(f'ERROR: dIdV fit not available for '
459
+ f'channel {chan}! Use "dofit" first.')
460
+
461
+ # loop channel
462
+ for chan in channels:
463
+
464
+ if self._verbose:
465
+ print(f'INFO: Calculating small signal parameters '
466
+ f'uncertainties for channel {chan}')
467
+
468
+ # data config
469
+ data_config = self._didv_data[chan]['data_config']
470
+ tes_bias = data_config['tes_bias']
471
+
472
+ # get didvobj
473
+ didvobj = self._didv_data[chan]['didvobj']
474
+
475
+ # get bias parameters from IV-sweep/normal-IV processing
476
+ biasparams = None
477
+ if ('ivsweep_results' in self._didv_data[chan]
478
+ and self._didv_data[chan]['ivsweep_results'] is not None):
479
+ biasparams = self._didv_data[chan]['ivsweep_results']
480
+
481
+ if 'ibias' in biasparams:
482
+ tes_bias = biasparams['ibias']
483
+ else:
484
+ biasparams['ibias'] = tes_bias
485
+
486
+ if self._verbose:
487
+ print(f'INFO: Using ibias={tes_bias:.3e} A from '
488
+ f'IV sweep for channel {chan}')
489
+
490
+
491
+ # calculate small signal params
492
+ didvobj.calc_smallsignal_params(
493
+ biasparams=biasparams,
494
+ poles=poles,
495
+ lgc_diagnostics=lgc_diagnostics
496
+ )
497
+
498
+ # replace
499
+ self._didv_data[chan]['didvobj'] = didvobj
500
+
501
+
502
+ # store data
503
+ self.save_didv_data(save_hdf5=self._save_hdf5, tag=tag)
504
+
505
+
506
+ def calc_bias_params_infinite_loop_gain(self, channels=None, tag='default'):
507
+
508
+ #
509
+ #Calculate I0,R0, and P0 with infinite loop gain
510
+ #approximation
511
+ #
512
+
513
+ # check channels
514
+ if channels is None:
515
+ channels = self._didv_data.keys()
516
+ elif isinstance(channels, str):
517
+ channels = [channels]
518
+ # get list of fitted poles
519
+ for chan in channels:
520
+ if chan not in self._didv_data.keys():
521
+ raise ValueError(f'ERROR: channel {chan} not available!')
522
+ didvobj = self._didv_data[chan]['didvobj']
523
+ list_of_fitted_poles = didvobj.get_list_fitted_poles()
524
+ if (list_of_fitted_poles is None
525
+ or (2 not in list_of_fitted_poles
526
+ and 3 not in list_of_fitted_poles)):
527
+ raise ValueError(f'ERROR: No fit available for '
528
+ f'channel {chan}!')
529
+
530
+ # loop channel
531
+ for chan in channels:
532
+
533
+ if self._verbose:
534
+ print(f'INFO: Calculating bias parameters with '
535
+ f'infinite loop gain approximation '
536
+ f'for channel {chan}')
537
+
538
+ biasparams_ilg = dict()
539
+
540
+ # get data config
541
+ data_config = self._didv_data[chan]['data_config']
542
+
543
+ # get didvobj
544
+ didvobj = self._didv_data[chan]['didvobj']
545
+ list_of_fitted_poles = didvobj.get_list_fitted_poles()
546
+
547
+ # loop poles
548
+ for poles in list_of_fitted_poles:
549
+
550
+ if (poles != 2 and poles != 3):
551
+ continue
552
+
553
+ fitresult = didvobj.fitresult(poles)
554
+
555
+ # check if result available
556
+ if (fitresult is None
557
+ or 'params' not in fitresult):
558
+ continue
559
+
560
+ ibias = None
561
+ ibias_err = 0
562
+ rp = None
563
+ rshunt = None
564
+ rn = None
565
+ if ('biasparams' in fitresult.keys() and
566
+ fitresult['biasparams'] is not None):
567
+
568
+ ibias = fitresult['biasparams']['ibias']
569
+ if 'ibias_err' in fitresult['biasparams']:
570
+ ibias_err = fitresult['biasparams']['ibias_err']
571
+ else:
572
+ ibias_err = 0
573
+ rp = fitresult['biasparams']['rp']
574
+ rshunt = fitresult['biasparams']['rshunt']
575
+ if 'rn' in fitresult['biasparams']:
576
+ rn = fitresult['biasparams']['rn']
577
+ else:
578
+ ibias = data_config['tes_bias']
579
+ if 'rp' not in data_config:
580
+ raise ValueError('ERROR: Unable to find rp!'
581
+ 'use "set_ivsweep_results()" first')
582
+ rp = data_config['rp']
583
+ rshunt = data_config['rshunt']
584
+ if 'rn' in data_config:
585
+ rn = data_config['rn']
586
+
587
+ biasparams_ilg[poles] = qp.get_biasparams_ilg(
588
+ fitresult['params'],
589
+ fitresult['cov'],
590
+ ibias,
591
+ ibias_err,
592
+ rshunt,
593
+ rp,
594
+ rn=rn)
595
+
596
+ self._didv_data[chan]['biasparams_ilg'] = biasparams_ilg
597
+
598
+ # store data
599
+ self.save_didv_data(save_hdf5=self._save_hdf5, tag=tag)
600
+
601
+
602
+ def calc_dpdi(self, freqs, channels=None, list_of_poles=None,
603
+ lgc_plot=False, tag='default'):
604
+ """
605
+ calculate dpdi in units of Volts
606
+ """
607
+
608
+ # check channels
609
+ if channels is None:
610
+ channels = self._didv_data.keys()
611
+ elif isinstance(channels, str):
612
+ channels = [channels]
613
+
614
+ for chan in channels:
615
+ if chan not in self._didv_data.keys():
616
+ raise ValueError(
617
+ f'ERROR: No data found for channel {chan}!'
618
+ )
619
+
620
+ for chan in channels:
621
+
622
+ # get didvobj
623
+ didvobj = self._didv_data[chan]['didvobj']
624
+
625
+ # list of poles
626
+ list_of_fitted_poles = didvobj.get_list_fitted_poles() or []
627
+ if list_of_poles is None:
628
+ poles_to_process = list(list_of_fitted_poles)
629
+ elif isinstance(list_of_poles, int):
630
+ poles_to_process = [list_of_poles]
631
+ else:
632
+ poles_to_process = list(list_of_poles)
633
+
634
+
635
+ # loop poles
636
+ for poles in poles_to_process:
637
+
638
+ if poles not in list_of_fitted_poles:
639
+ raise ValueError(f'ERROR: No fit available for poles '
640
+ f'{poles}. Use "dofit" function first!')
641
+
642
+ fitresult = didvobj.fitresult(poles)
643
+
644
+ dpdi, dpdi_err = qp.get_dPdI_with_uncertainties(
645
+ freqs, fitresult,
646
+ lgcplot=lgc_plot)
647
+
648
+ poles_str = str(poles) + 'poles'
649
+ self._didv_data[chan]['dpdi_' + poles_str] = dpdi
650
+ self._didv_data[chan]['dpdi_err_' + poles_str] = dpdi_err
651
+ self._didv_data[chan]['dpdi_freqs_' + poles_str] = freqs
652
+
653
+
654
+
655
+ # store data
656
+ self.save_didv_data(save_hdf5=self._save_hdf5, tag=tag)
657
+
658
+ def calc_energy_resolution(self, channel, psd,
659
+ poles=3, fs=None, template=None,
660
+ collection_eff=1,
661
+ lgc_power_template=False):
662
+ """
663
+ Get energy resolution based using calculated dpdi and input
664
+ psd and template (dirac delta power input if template is None)
665
+ Default is template in current.
666
+ """
667
+
668
+ # check data availability
669
+ if channel not in self._didv_data.keys():
670
+ raise ValueError(f'ERROR: No data found for channel {channel}!')
671
+
672
+ list_of_fitted_poles = (
673
+ self._didv_data[channel]['didvobj'].get_list_fitted_poles()
674
+ )
675
+ if poles not in list_of_fitted_poles:
676
+ raise ValueError(
677
+ f'ERROR: No {poles}-poles fit found for channel {channel}!'
678
+ )
679
+
680
+
681
+ # check dpdi available
682
+ poles_str = str(poles) + 'poles'
683
+ if 'dpdi_' + poles_str not in self._didv_data[channel]:
684
+ raise ValueError(f'ERROR: No dpdi found for channel {channel}!')
685
+
686
+ dpdi = self._didv_data[channel]['dpdi_' + poles_str]
687
+ dpdi_freqs = self._didv_data[channel]['dpdi_freqs_' + poles_str]
688
+
689
+ # sample rate
690
+ if fs is None:
691
+
692
+ if 'fs' not in self._didv_data[channel]['data_config']:
693
+ raise ValueError(f'ERROR: sample rate (fs) required!')
694
+
695
+ fs = self._didv_data[channel]['data_config']['fs']
696
+
697
+
698
+ if template is None:
699
+
700
+ # dirac delta input power
701
+ nbins = psd.shape[-1]
702
+ pretrigger_index = nbins//2
703
+ template = unit_impulse(nbins, idx=pretrigger_index)
704
+
705
+ elif not lgc_power_template:
706
+ template = qp.convert_template_to_power(template, dpdi=dpdi)
707
+
708
+
709
+ # calculate energy resolution
710
+ resolution = qp.utils.energy_resolution(
711
+ psd, template, dpdi, fs,
712
+ collection_eff=collection_eff)
713
+
714
+ # store
715
+ res_dict = dict()
716
+ res_dict['psd'] = psd
717
+ res_dict['template'] = template
718
+ res_dict['fs'] = fs
719
+ res_dict['collection_efficiency'] = collection_eff
720
+ res_dict['energy_resolution'] = resolution
721
+ self._didv_data[channel]['resolution'] = res_dict
722
+
723
+ return resolution
724
+
725
+
726
+ def dofit_prior(self, channels=None, list_of_poles=None,
727
+ use_ivsweep_current=True):
728
+ """Refit 2/3-pole models with priors derived from existing fits.
729
+
730
+ This is a legacy helper. The previous implementation referenced
731
+ several undefined analysis-local variables. Priors are now built
732
+ only from the selected channel's existing QETpy fit result, with
733
+ available bias-parameter uncertainties used for Rsh, Rp, and R0.
734
+ """
735
+
736
+ if channels is None:
737
+ channels = list(self._didv_data)
738
+ elif isinstance(channels, str):
739
+ channels = [channels]
740
+ else:
741
+ channels = list(channels)
742
+
743
+ for chan in channels:
744
+ if chan not in self._didv_data:
745
+ raise ValueError(
746
+ f'ERROR: dIdV fit result not found for channel {chan}! '
747
+ 'Use "dofit" first.'
748
+ )
749
+
750
+ didvobj = self._didv_data[chan]['didvobj']
751
+ fitted_poles = didvobj.get_list_fitted_poles() or []
752
+ if list_of_poles is None:
753
+ poles_to_fit = [p for p in fitted_poles if p in (2, 3)]
754
+ elif isinstance(list_of_poles, int):
755
+ poles_to_fit = [list_of_poles]
756
+ else:
757
+ poles_to_fit = list(list_of_poles)
758
+
759
+ if not poles_to_fit:
760
+ raise ValueError(
761
+ f'ERROR: No 2/3-pole fit available for channel {chan}'
762
+ )
763
+ missing = [p for p in poles_to_fit if p not in fitted_poles]
764
+ if missing:
765
+ raise ValueError(
766
+ f'ERROR: Missing existing fit(s) {missing} for channel {chan}'
767
+ )
768
+
769
+ if self._verbose:
770
+ print(f'INFO: Do prior fit(s) for channel {chan}')
771
+
772
+ didvobj_prior = qp.DIDVPriors(
773
+ rawtraces=None,
774
+ fs=didvobj._fs,
775
+ sgfreq=didvobj._sgfreq,
776
+ sgamp=didvobj._sgamp,
777
+ rsh=didvobj._rsh,
778
+ dutycycle=didvobj._dutycycle,
779
+ )
780
+ didvobj_prior._time = didvobj._time
781
+ didvobj_prior._freq = didvobj._freq
782
+ didvobj_prior._didvmean = didvobj._didvmean
783
+ didvobj_prior._didvstd = didvobj._didvstd
784
+ didvobj_prior._offset = didvobj._offset
785
+ didvobj_prior._offset_err = didvobj._offset_err
786
+ didvobj_prior._tmean = didvobj._tmean
787
+ didvobj_prior._dt0 = didvobj._dt0
788
+
789
+ for poles_select in poles_to_fit:
790
+ results = didvobj.fitresult(poles_select)
791
+ smallsignal = results.get('smallsignalparams') if results else None
792
+ if not smallsignal:
793
+ raise ValueError(
794
+ f'ERROR: Small-signal parameters are required before '
795
+ f'prior fitting {poles_select} poles for channel {chan}'
796
+ )
797
+
798
+ if poles_select == 2:
799
+ names = ['rsh', 'rp', 'r0', 'beta', 'l', 'L', 'tau0', 'dt']
800
+ elif poles_select == 3:
801
+ names = [
802
+ 'rsh', 'rp', 'r0', 'beta', 'l', 'L',
803
+ 'tau0', 'gratio', 'tau3', 'dt',
804
+ ]
805
+ else:
806
+ raise ValueError('ERROR: prior fitting supports 2 or 3 poles')
807
+
808
+ priors = np.asarray([smallsignal[name] for name in names], dtype=float)
809
+ priors_cov = np.diag((0.1 * np.abs(priors)) ** 2)
810
+
811
+ biasparams = results.get('biasparams') or {}
812
+ sigma_rsh = float(
813
+ biasparams.get('rshunt_err', biasparams.get('rsh_err', 0.1 * abs(priors[0])))
814
+ )
815
+ sigma_rp = float(
816
+ biasparams.get('rp_err', 0.1 * abs(priors[1]))
817
+ )
818
+ sigma_r0 = float(
819
+ biasparams.get('r0_err', 0.1 * abs(priors[2]))
820
+ )
821
+
822
+ priors_cov[0, 0] = sigma_rsh ** 2
823
+ priors_cov[1, 1] = sigma_rp ** 2
824
+ priors_cov[2, 2] = sigma_r0 ** 2
825
+ priors_cov[0, 1] = priors_cov[1, 0] = 0.5 * sigma_rsh * sigma_rp
826
+ priors_cov[0, 2] = priors_cov[2, 0] = 0.5 * sigma_rsh * sigma_r0
827
+ priors_cov[1, 2] = priors_cov[2, 1] = -0.2 * sigma_rp * sigma_r0
828
+
829
+ didvobj_prior.dofit(
830
+ poles=poles_select,
831
+ priors=priors,
832
+ priorscov=priors_cov,
833
+ )
834
+
835
+ self._didv_data[chan]['didvobj_prior'] = didvobj_prior
836
+
837
+ def get_fit_results(self, channel, poles, verbose=True):
838
+ """
839
+ Get fit result
840
+ """
841
+
842
+ # check if object available
843
+ if channel not in self._didv_data:
844
+ raise ValueError(f'ERROR: dIdV data not available for '
845
+ f'channel {channel}!')
846
+
847
+ # check if fit done
848
+ result = self._didv_data[channel]['didvobj'].fitresult(poles)
849
+ if not result:
850
+ if self._verbose and verbose:
851
+ print(f'WARNING: {channel}: No fit result found for poles {poles}! '
852
+ 'Returning empty dictionary.')
853
+ return result
854
+
855
+
856
+ def plot_fit_result(self, channels=None,
857
+ lgc_plot_fft=True, lgc_gray_mean=True,
858
+ lgc_didv_freq_filt=True,
859
+ zoom_factor=None, fcutoff=2e4,
860
+ lgc_save=False, save_path=None,
861
+ save_name=None):
862
+ """
863
+ Plot fit results for multiple channels
864
+ """
865
+
866
+
867
+ # check channels
868
+ if channels is None:
869
+ channels = self._didv_data.keys()
870
+ elif isinstance(channels, str):
871
+ channels = [channels]
872
+
873
+ for chan in channels:
874
+ if chan not in self._didv_data.keys():
875
+ raise ValueError(
876
+ f'ERROR: dIdV fit result not found for channel {chan}! '
877
+ 'Use "dofit" first.'
878
+ )
879
+
880
+ # look chanels
881
+ for chan in channels:
882
+
883
+ didvobj = self._didv_data[chan]['didvobj']
884
+
885
+ # check if any results
886
+ is_fitted = False
887
+ poles_list = [1,2,3]
888
+ for poles in poles_list:
889
+ if didvobj.fitresult(poles):
890
+ is_fitted = True
891
+ if not is_fitted:
892
+ print(f'WARNING: No fit result available for channel {chan}!')
893
+ continue
894
+
895
+ # display
896
+ if self._verbose:
897
+ print(f'\n{chan} dIdV Fit Plots:')
898
+
899
+ didvobj.plot_full_trace(
900
+ didv_freq_filt=lgc_didv_freq_filt,
901
+ gray_mean=lgc_gray_mean,
902
+ saveplot=lgc_save,
903
+ savepath=save_path,
904
+ savename=save_name,
905
+ lp_cutoff=fcutoff,
906
+ )
907
+
908
+ if zoom_factor is not None:
909
+ didvobj.plot_zoomed_in_trace(
910
+ didv_freq_filt=lgc_didv_freq_filt,
911
+ saveplot=lgc_save,
912
+ savepath=save_path,
913
+ savename=save_name,
914
+ lp_cutoff=fcutoff,
915
+ zoomfactor=zoom_factor,
916
+ )
917
+
918
+ if lgc_plot_fft:
919
+ didvobj.plot_re_im_didv(
920
+ poles='all',
921
+ saveplot=lgc_save,
922
+ savepath=save_path,
923
+ savename=save_name,
924
+ )
925
+
926
+
927
+ def print_fit_result(self, channels=None, poles=3):
928
+ """
929
+ Print fit information
930
+ """
931
+
932
+ # check channels
933
+ if channels is None:
934
+ channels = self._didv_data.keys()
935
+ elif isinstance(channels, str):
936
+ channels = [channels]
937
+
938
+ for chan in channels:
939
+ if chan not in self._didv_data.keys():
940
+ raise ValueError(
941
+ f'ERROR: dIdV fit result not found for channel {chan}! '
942
+ 'Use "dofit" first.'
943
+ )
944
+
945
+ # look chanels
946
+ for chan in channels:
947
+
948
+ didvobj = self._didv_data[chan]['didvobj']
949
+
950
+ # check if any results
951
+ results = didvobj.fitresult(poles)
952
+ if not results:
953
+ print(f'WARNING: No fit result available for channel {chan}!')
954
+ continue
955
+
956
+ print(f'\n{chan} dIdV {poles}-poles Fit Result:')
957
+
958
+
959
+ print('Fit chi2/Ndof = {:.3f}'.format(results['cost']))
960
+ print('\nFit time constants, NOT dIdV Poles: ')
961
+ print('Tau1: {:.3g} s'.format(np.abs(results['params']['tau1'])))
962
+ print('Tau2: {:.3g} s'.format(results['params']['tau2']))
963
+ print('Tau3: {:.4g} s'.format(results['params']['tau3']))
964
+ print(' ')
965
+ print('\nTrue dIdV Poles: ')
966
+ print('Tau_plus: {:.3g} s'.format(results['falltimes'][0]))
967
+ print('Tau_minus: {:.3g} s'.format(results['falltimes'][1]))
968
+ print('Tau_third: {:.4g} s'.format(results['falltimes'][2]))
969
+
970
+ if 'ssp_light' in results:
971
+ vals_vector = results['ssp_light']['vals']
972
+ sigmas_vector = results['ssp_light']['sigmas']
973
+ print('\nSmall Signal Parameters:')
974
+ print('l (loop gain) = {:.3f} +/- {:.4f}'.format(
975
+ vals_vector['l'], sigmas_vector['sigma_l']))
976
+ print('beta = {:.3f} +/- {:.4f}'.format(
977
+ vals_vector['beta'], sigmas_vector['sigma_beta']))
978
+ print('gratio = {:.3f} +/- {:.4f}'.format(
979
+ vals_vector['gratio'], sigmas_vector['sigma_gratio']))
980
+ print('tau0 = {:.3g} +/- {:.4g} ms'.format(
981
+ vals_vector['tau0']*1e3, sigmas_vector['sigma_tau0']*1e3))
982
+ print('L = {:.3f} +/- {:.4f} nH'.format(
983
+ vals_vector['L']*1e9, sigmas_vector['sigma_L']*1e9))
984
+
985
+
986
+ def compare_with_ivsweep(self, channel, poles=3):
987
+ """
988
+ Create a dataframe to compare results with
989
+ IV sweep
990
+ """
991
+
992
+ # check channel
993
+ if channel not in self._didv_data.keys():
994
+ raise ValueError(f'ERROR: channel {channel} not found!')
995
+
996
+
997
+ didv_results = self.get_fit_results(channel, poles=poles)
998
+ ivsweep_results = self.get_ivsweep_results(channel)
999
+
1000
+ data = dict()
1001
+
1002
+
1003
+ if ('biasparams' in didv_results
1004
+ and didv_results['biasparams'] is not None):
1005
+
1006
+ # define parameter list
1007
+ param_list = ['r0','i0','p0']
1008
+ norm_list = [1e3, 1e6, 1e15]
1009
+ label_list = ['R0 [mOhms]','I0 [muAmps]','P0 [fWatts]']
1010
+ label_inf_lgain_list = ['R0 Inf loop gain [mOhms]',
1011
+ 'I0 Inf loop gain [muAmps]',
1012
+ 'P0 Inf loop gain [fWatts]']
1013
+
1014
+ # results
1015
+ results = didv_results['biasparams']
1016
+ results_infinite_lgain = None
1017
+ if 'biasparams_infinite_lgain' in didv_results:
1018
+ results_infinite_lgain = (
1019
+ didv_results['biasparams_infinite_lgain']
1020
+ )
1021
+
1022
+ # loop parameters
1023
+ for iparam, param in enumerate(param_list):
1024
+
1025
+ if param not in ivsweep_results:
1026
+ continue
1027
+
1028
+ # normalization/label
1029
+ norm = norm_list[iparam]
1030
+ label = label_list[iparam]
1031
+ label_infinite_lgain = label_inf_lgain_list[iparam]
1032
+
1033
+
1034
+ # build string dIdV
1035
+ val = results[param]
1036
+ val_err = results[param + '_err']
1037
+ val_str = '{:.3g} +/- {:.4g}'.format(
1038
+ val*norm, val_err*norm
1039
+ )
1040
+
1041
+
1042
+ # build string IV
1043
+ iv_val = ivsweep_results[param]
1044
+ iv_val_err = ivsweep_results[param + '_err']
1045
+ iv_val_str = '{:.3g} +/- {:.4g}'.format(
1046
+ iv_val*norm, iv_val_err*norm
1047
+ )
1048
+
1049
+ data[label] = [val_str, iv_val_str]
1050
+
1051
+
1052
+ # infinite loop gain
1053
+ if (results_infinite_lgain is not None
1054
+ and 'r0_infinite_lgain' in ivsweep_results):
1055
+
1056
+ # build string dIdV
1057
+ val = results_infinite_lgain[param]
1058
+ val_err = results_infinite_lgain[param + '_err']
1059
+ val_str = '{:.3g} +/- {:.4g}'.format(
1060
+ val*norm, val_err*norm
1061
+ )
1062
+
1063
+ # build string IV
1064
+ iv_val = ivsweep_results[param + '_infinite_lgain']
1065
+ iv_val_err = ivsweep_results[param + '_err_infinite_lgain']
1066
+ iv_val_str = '{:.3g} +/- {:.4g}'.format(
1067
+ iv_val*norm, iv_val_err*norm
1068
+ )
1069
+
1070
+ data[label_infinite_lgain] = [val_str, iv_val_str]
1071
+
1072
+
1073
+ # small signal parmaters
1074
+ if ('ssp_light' in didv_results
1075
+ and 'didv_fit_l' in ivsweep_results):
1076
+
1077
+ # define parameter list
1078
+ param_list = ['l','beta','gratio', 'L', 'tau0']
1079
+ norm_list = [1, 1, 1, 1e9, 1e3]
1080
+ label_list = ['l (loop gain)','beta', 'gratio',
1081
+ 'L [nH]', 'tau0 [ms]']
1082
+
1083
+
1084
+ vals_vector = didv_results['ssp_light']['vals']
1085
+ sigmas_vector = didv_results['ssp_light']['sigmas']
1086
+
1087
+
1088
+ # loop parameters
1089
+ for iparam, param in enumerate(param_list):
1090
+
1091
+ param_iv = 'didv_fit_' + param
1092
+
1093
+ # normalization/label
1094
+ norm = norm_list[iparam]
1095
+ label = label_list[iparam]
1096
+
1097
+
1098
+ # build string dIdV
1099
+ val = vals_vector[param]
1100
+ val_err = sigmas_vector['sigma_' + param]
1101
+ val_str = '{:.3g} +/- {:.4g}'.format(
1102
+ val*norm, val_err*norm
1103
+ )
1104
+
1105
+ # build string IV
1106
+ iv_val = ivsweep_results[param_iv]
1107
+ iv_val_err = ivsweep_results[param_iv + '_err']
1108
+ iv_val_str = '{:.3g} +/- {:.4g}'.format(
1109
+ iv_val*norm, iv_val_err*norm
1110
+ )
1111
+
1112
+ # store data
1113
+ data[label] = [val_str, iv_val_str]
1114
+
1115
+ # fall times
1116
+ if ('falltimes' in didv_results
1117
+ and 'didv_fit_tau+' in ivsweep_results):
1118
+
1119
+ # define parameter list
1120
+ param_list = ['tau+','tau-','tau3']
1121
+ norm_list = [1e6, 1e6, 1e6]
1122
+ label_list = ['tau+ [usec]', 'tau- [usec]', 'tau3 [usec]']
1123
+
1124
+
1125
+ falltimes = didv_results['falltimes']
1126
+
1127
+
1128
+ # loop parameters
1129
+ for iparam, param in enumerate(param_list):
1130
+
1131
+ if (poles == 2 and iparam>1):
1132
+ break
1133
+
1134
+
1135
+ param_iv = 'didv_fit_' + param
1136
+
1137
+ # normalization/label
1138
+ norm = norm_list[iparam]
1139
+ label = label_list[iparam]
1140
+
1141
+
1142
+ # build string dIdV
1143
+ val = falltimes[iparam]
1144
+ val_str = '{:.3g}'.format(val*norm)
1145
+
1146
+
1147
+ # build string IV
1148
+ iv_val = ivsweep_results[param_iv]
1149
+ iv_val_str = '{:.3g}'.format(iv_val*norm)
1150
+
1151
+ # save
1152
+ data[label] = [val_str, iv_val_str]
1153
+
1154
+
1155
+
1156
+ df = None
1157
+ if data:
1158
+ df = pd.DataFrame.from_dict(data, orient='index', columns=['dIdV', 'IV Sweep'])
1159
+
1160
+ return df
1161
+
1162
+
1163
+ def save_didv_data(self, channels=None, save_path=None, save_hdf5=False, tag='default'):
1164
+ """
1165
+ Save didv data
1166
+ """
1167
+
1168
+ # channels
1169
+ if channels is None:
1170
+ channels = self._didv_data.keys()
1171
+ elif isinstance(channels, str):
1172
+ channels = [channels]
1173
+
1174
+ for chan in channels:
1175
+ if chan not in self._didv_data.keys():
1176
+ raise ValueError(
1177
+ f'ERROR: No data found for channel {chan}!'
1178
+ )
1179
+
1180
+
1181
+ # loop channels and set data
1182
+ save_data = False
1183
+ for chan in channels:
1184
+
1185
+ # metadata
1186
+ metadata = dict()
1187
+ metadata['channel'] = chan
1188
+
1189
+ data_config = self.get_didv_data(chan)['data_config']
1190
+ if data_config is not None:
1191
+ metadata.update(data_config.copy())
1192
+
1193
+ # 2-poles
1194
+ results = self.get_fit_results(chan, 2, verbose=False)
1195
+ if results:
1196
+ self.set_didv_results(chan, results, 2,
1197
+ metadata=metadata, tag=tag)
1198
+ save_data = True
1199
+
1200
+
1201
+ # 3-poles
1202
+ results = self.get_fit_results(chan, 3, verbose=False)
1203
+ if results:
1204
+ self.set_didv_results(chan, results, 3,
1205
+ metadata=metadata, tag=tag)
1206
+ save_data = True
1207
+
1208
+ # dpdi
1209
+ fs = None
1210
+ if ('data_config' in self._didv_data[chan]
1211
+ and 'fs' in self._didv_data[chan]['data_config']):
1212
+ fs = self._didv_data[chan]['data_config']['fs']
1213
+
1214
+ if 'dpdi_2poles' in self._didv_data[chan]:
1215
+ dpdi_freqs, dpdi, dpdi_err = self.get_dpdi(chan, 2)
1216
+ self.set_dpdi(chan, dpdi, dpdi_err, dpdi_freqs, 2,
1217
+ sample_rate=fs,
1218
+ metadata=metadata, tag=tag)
1219
+ save_data = True
1220
+
1221
+ if 'dpdi_3poles' in self._didv_data[chan]:
1222
+ dpdi_freqs, dpdi, dpdi_err = self.get_dpdi(chan, 3)
1223
+ self.set_dpdi(chan, dpdi, dpdi_err, dpdi_freqs, 3,
1224
+ sample_rate=fs,
1225
+ metadata=metadata, tag=tag)
1226
+ save_data = True
1227
+
1228
+ if not save_data:
1229
+ return
1230
+
1231
+ if save_hdf5:
1232
+
1233
+ # file_name
1234
+ file_path = self._save_path
1235
+ if file_path is None:
1236
+ file_path = './'
1237
+ file_name = self._file_name
1238
+
1239
+ if save_path is not None:
1240
+ if os.path.isfile(save_path):
1241
+ file_path = os.path.dirname(save_path)
1242
+ file_name = os.path.basename(save_path)
1243
+ elif os.path.isdir(save_path):
1244
+ file_path = save_path
1245
+ else:
1246
+ raise ValueError('ERROR: "save_path" should be a '
1247
+ 'file or path!')
1248
+ full_file_name = file_path + '/' + file_name
1249
+ if file_path[-1] == '/':
1250
+ full_file_name = file_path + file_name
1251
+
1252
+ self.save_hdf5(full_file_name, overwrite=True)
1253
+
1254
+
1255
+
1256
+ def _process_raw_data(self, channel,
1257
+ data_path, stream=None,
1258
+ overwrite=False):
1259
+
1260
+ """
1261
+ Set dIdV data for a specific channel
1262
+ form raw traces. Apply cuts and pre-process
1263
+ data. The function can be called multiple
1264
+ times for different channels
1265
+ """
1266
+
1267
+ if not overwrite and channel in self._didv_data:
1268
+ raise ValueError(
1269
+ f'ERROR: Data for channel {channel} already available. '
1270
+ 'Set overwrite=True'
1271
+ )
1272
+
1273
+ # Instantiate QETpy DIDV Object
1274
+ didvobj = None
1275
+ if self._verbose:
1276
+ print(f'INFO: Getting raw data and processing channel {channel}')
1277
+
1278
+
1279
+ with StreamReader(
1280
+ data_path,
1281
+ streams=stream,
1282
+ measurement_types='didv',
1283
+ ) as reader:
1284
+ selected_stream_ids = reader.selected_stream_ids
1285
+ if len(selected_stream_ids) > 1:
1286
+ raise ValueError(
1287
+ 'ERROR: DIDVAnalysis.process_raw_data requires one dIdV '
1288
+ 'stream at a time. Selected streams: '
1289
+ + ', '.join(selected_stream_ids)
1290
+ )
1291
+
1292
+ traces, info = reader.read_records(
1293
+ channels=channel,
1294
+ include_metadata=True,
1295
+ units='amps',
1296
+ stack=True,
1297
+ )
1298
+ detector_settings = reader.get_detector_settings()
1299
+
1300
+ acquisition_name = reader.acquisition_name
1301
+ stream_id = (
1302
+ selected_stream_ids[0]
1303
+ if selected_stream_ids
1304
+ else None
1305
+ )
1306
+
1307
+ traces = np.asarray(traces)
1308
+ if traces.ndim != 3 or traces.shape[1] != 1:
1309
+ raise ValueError(
1310
+ 'ERROR: Expected dIdV traces with shape '
1311
+ f'(n_traces, 1, n_samples), got {traces.shape}'
1312
+ )
1313
+ if traces.shape[0] == 0 or not info:
1314
+ raise ValueError(
1315
+ f'ERROR: No dIdV records found for channel {channel}'
1316
+ )
1317
+
1318
+ traces = traces[:, 0, :]
1319
+ fs = float(info[0]['sample_rate_hz'])
1320
+
1321
+ if channel not in detector_settings:
1322
+ raise ValueError(
1323
+ f'ERROR: Detector settings for channel {channel} not found'
1324
+ )
1325
+ settings = detector_settings[channel]
1326
+
1327
+ tes_bias = self._required_float(
1328
+ settings, 'tes_bias_dc_amps', channel
1329
+ )
1330
+ output_gain = self._optional_float(settings.get('output_gain'))
1331
+ close_loop_norm = self._optional_float(settings.get('close_loop_norm'))
1332
+ output_offset = self._optional_float(settings.get('output_offset_vdc'))
1333
+ sgamp = self._required_float(
1334
+ settings, 'tes_bias_ac_amplitude_amps', channel
1335
+ )
1336
+ sgfreq = self._required_float(
1337
+ settings, 'tes_bias_ac_frequency_hz', channel
1338
+ )
1339
+
1340
+ if sgfreq <= 0:
1341
+ raise ValueError(
1342
+ f'ERROR: Invalid dIdV frequency {sgfreq} Hz for {channel}'
1343
+ )
1344
+
1345
+ rshunt = self._first_optional_float(
1346
+ settings,
1347
+ 'shunt_resistance_ohm',
1348
+ 'rshunt',
1349
+ )
1350
+ if rshunt is None:
1351
+ raise ValueError(
1352
+ f'ERROR: Shunt resistance is required for channel {channel}'
1353
+ )
1354
+
1355
+ rp = self._first_optional_float(
1356
+ settings,
1357
+ 'parasitic_resistance_ohm',
1358
+ 'rp',
1359
+ )
1360
+
1361
+ dutycycle = self._first_optional_float(
1362
+ settings,
1363
+ 'dutycycle',
1364
+ 'duty_cycle',
1365
+ 'tes_bias_ac_dutycycle',
1366
+ )
1367
+ if dutycycle is None:
1368
+ dutycycle = 0.5
1369
+
1370
+ if acquisition_name is None:
1371
+ acquisition_name = info[0].get('acquisition_name')
1372
+ if stream_id is None:
1373
+ stream_id = info[0].get('stream_id')
1374
+ stream_name = info[0].get('stream_name', stream_id)
1375
+
1376
+
1377
+ # save relevant detector settings in dictionary
1378
+ data_config = dict()
1379
+ data_config['stream_id'] = stream_id
1380
+ data_config['stream_name'] = stream_name
1381
+ data_config['acquisition_name'] = acquisition_name
1382
+ data_config['fs'] = fs
1383
+ data_config['output_variable_gain'] = output_gain
1384
+ data_config['output_variable_offset'] = output_offset
1385
+ data_config['close_loop_norm'] = close_loop_norm
1386
+ data_config['rshunt'] = rshunt
1387
+ data_config['rp'] = rp
1388
+ data_config['tes_bias'] = tes_bias
1389
+ data_config['sgfreq'] = sgfreq
1390
+ data_config['sgamp'] = sgamp
1391
+ data_config['dutycycle'] = dutycycle
1392
+
1393
+
1394
+
1395
+ # add temperature
1396
+ temperature_list = ['cp','mc','still']
1397
+ for temp in temperature_list:
1398
+ temp_par = 'temperature_' + temp
1399
+ if temp_par in settings:
1400
+ data_config[temp_par] = (
1401
+ float(settings[temp_par])
1402
+ )
1403
+
1404
+
1405
+ # Apply cuts
1406
+ nonzero = ~np.all(traces == 0, axis=1)
1407
+ traces = traces[nonzero]
1408
+ if traces.shape[0] == 0:
1409
+ raise ValueError(
1410
+ f'ERROR: All dIdV traces are zero for channel {channel}'
1411
+ )
1412
+
1413
+ # cut pileup
1414
+ cut = qp.autocuts_didv(traces, fs=fs)
1415
+ traces = traces[cut]
1416
+ if traces.shape[0] == 0:
1417
+ raise ValueError(
1418
+ f'ERROR: No dIdV traces survive autocuts for channel {channel}'
1419
+ )
1420
+
1421
+ # Fit data
1422
+ didvobj = qp.DIDV(traces,
1423
+ fs,
1424
+ sgfreq,
1425
+ sgamp,
1426
+ rshunt,
1427
+ rp=rp,
1428
+ dutycycle=dutycycle,
1429
+ add180phase=False)
1430
+
1431
+ # process
1432
+ didvobj.processtraces()
1433
+
1434
+ # store
1435
+ self._didv_data[channel] = {'didvobj': didvobj,
1436
+ 'acquisition_name': acquisition_name,
1437
+ 'stream_id': stream_id,
1438
+ 'base_path': data_path,
1439
+ 'ivsweep_results': None,
1440
+ 'data_config': data_config,
1441
+ 'resolution': None}
1442
+
1443
+
1444
+
1445
+ def _set_processed_data(self, channel,
1446
+ didv_data,
1447
+ overwrite=False,
1448
+ **kwargs ):
1449
+
1450
+ """
1451
+ Set dIdV data from already processed raw
1452
+ data, in the form of pandas stream or
1453
+ dictionary. The function can be called multiple
1454
+ times for different channels
1455
+ """
1456
+
1457
+ if not overwrite and channel in self._didv_data:
1458
+ raise ValueError(
1459
+ f'ERROR: Data for channel {channel} already available. '
1460
+ 'Set overwrite=True'
1461
+ )
1462
+
1463
+ # check didv data
1464
+ if isinstance(didv_data, pd.Series):
1465
+ didv_data = didv_data.to_dict()
1466
+ elif not isinstance(didv_data, dict):
1467
+ raise ValueError(
1468
+ 'ERROR: "didv_data" should be a dictionary '
1469
+ ' or pandas Series')
1470
+
1471
+ # check variables
1472
+ available_vars = list(didv_data.keys())
1473
+ required_vars = ['avgtrace_didv', 'didvmean','didvstd',
1474
+ 'offset_didv','offset_err_didv',
1475
+ 'sgamp', 'sgfreq',
1476
+ 'fs_didv']
1477
+
1478
+ for var in required_vars:
1479
+ if var not in available_vars:
1480
+ raise ValueError(f'ERROR: Missing variable {var} '
1481
+ ' in didv_data')
1482
+
1483
+ # check if channel stored in didv_data
1484
+ if 'channel' in didv_data.keys():
1485
+ channel_didv = str(didv_data['channel'])
1486
+ if channel != channel_didv:
1487
+ raise ValueError(
1488
+ f'ERROR: Channel name in didv data "{channel_didv}" '
1489
+ 'is different than input channel!')
1490
+
1491
+ # check if other parameters are available
1492
+ rshunt = None
1493
+ if 'rshunt_noise' in didv_data.keys():
1494
+ rshunt = didv_data['rshunt_noise']
1495
+ elif 'rshunt_didv' in didv_data.keys():
1496
+ rshunt = didv_data['rshunt_didv']
1497
+ rshunt = self._first_finite_value(rshunt)
1498
+ if rshunt is None:
1499
+ raise ValueError(
1500
+ f'ERROR: Missing/invalid rshunt in processed dIdV data for '
1501
+ f'channel {channel}'
1502
+ )
1503
+
1504
+ # parasitic resistance
1505
+ rp = None
1506
+ if 'rp_noise' in didv_data.keys():
1507
+ rp = didv_data['rp_noise']
1508
+ elif 'rp_didv' in didv_data.keys():
1509
+ rp = didv_data['rp_didv']
1510
+ rp = self._first_finite_value(rp)
1511
+
1512
+
1513
+ # duty cycle
1514
+ dutycycle = 0.5
1515
+ if 'dutycycle' in didv_data.keys():
1516
+ dutycycle = didv_data['dutycycle']
1517
+
1518
+ # data config
1519
+ data_config_pars = ['stream_id', 'stream_name', 'acquisition_name',
1520
+ 'fs', 'output_variable_gain',
1521
+ 'output_variable_offset',
1522
+ 'close_loop_norm', 'tes_bias',
1523
+ 'sgfreq','sgamp']
1524
+
1525
+ data_config = dict()
1526
+ for config in data_config_pars:
1527
+ config_name = config + '_didv'
1528
+ if config_name in didv_data.keys():
1529
+ data_config[config] = didv_data[config_name]
1530
+ elif config in ('sgfreq', 'sgamp') and config in didv_data:
1531
+ data_config[config] = didv_data[config]
1532
+ else:
1533
+ data_config[config]= None
1534
+
1535
+ data_config['rshunt'] = rshunt
1536
+ data_config['rp'] = rp
1537
+ data_config['dutycycle'] = dutycycle
1538
+
1539
+
1540
+ # Instantiate QETpy DIDV Object
1541
+ didvobj = qp.didvinitfromdata(
1542
+ didv_data['avgtrace_didv'][:len(didv_data['didvmean'])],
1543
+ didv_data['didvmean'],
1544
+ didv_data['didvstd'],
1545
+ didv_data['offset_didv'],
1546
+ didv_data['offset_err_didv'],
1547
+ didv_data['fs_didv'],
1548
+ didv_data['sgfreq'],
1549
+ didv_data['sgamp'],
1550
+ rsh=rshunt,
1551
+ rp=rp,
1552
+ dutycycle=dutycycle,
1553
+ )
1554
+
1555
+
1556
+ # store
1557
+ self._didv_data[channel] = {'didvobj': didvobj,
1558
+ 'acquisition_name': data_config['acquisition_name'],
1559
+ 'stream_id': data_config['stream_id'],
1560
+ 'base_path': None,
1561
+ 'ivsweep_results': None,
1562
+ 'data_config': data_config,
1563
+ 'resolution': None}
1564
+
1565
+ @staticmethod
1566
+ def _optional_float(value):
1567
+ if value is None:
1568
+ return np.nan
1569
+ try:
1570
+ output = float(value)
1571
+ except (TypeError, ValueError):
1572
+ return np.nan
1573
+ return output
1574
+
1575
+ @classmethod
1576
+ def _first_finite_value(cls, value):
1577
+ output = cls._optional_float(value)
1578
+ if np.isfinite(output):
1579
+ return output
1580
+ return None
1581
+
1582
+ @classmethod
1583
+ def _required_float(cls, settings, key, channel):
1584
+ value = cls._optional_float(settings.get(key))
1585
+ if not np.isfinite(value):
1586
+ raise ValueError(
1587
+ f'ERROR: Invalid or missing "{key}" for channel {channel}'
1588
+ )
1589
+ return value
1590
+
1591
+ @classmethod
1592
+ def _first_optional_float(cls, settings, *keys):
1593
+ for key in keys:
1594
+ if key not in settings:
1595
+ continue
1596
+ value = cls._optional_float(settings.get(key))
1597
+ if np.isfinite(value):
1598
+ return value
1599
+ return None
1600
+
1601
+ def _default_save_path(self, data_path):
1602
+ """Choose the default filterdata directory for raw input."""
1603
+
1604
+ try:
1605
+ catalog = AcquisitionCatalog(data_path, verbose=False)
1606
+ source_path = Path(catalog.acquisition_path)
1607
+ except Exception:
1608
+ if isinstance(data_path, (list, tuple)):
1609
+ if not data_path:
1610
+ source_path = Path('.')
1611
+ else:
1612
+ source_path = Path(data_path[0])
1613
+ else:
1614
+ source_path = Path(data_path)
1615
+ if source_path.is_file():
1616
+ source_path = source_path.parent
1617
+
1618
+ path_text = str(source_path)
1619
+ if '/processed/' in path_text:
1620
+ path_text = path_text.replace('/processed/', '/filterdata/')
1621
+ elif '/raw/' in path_text:
1622
+ path_text = path_text.replace('/raw/', '/filterdata/')
1623
+
1624
+ output_path = Path(path_text)
1625
+ try:
1626
+ output_path.mkdir(parents=True, exist_ok=True)
1627
+ os.chmod(
1628
+ output_path,
1629
+ stat.S_IRWXG | stat.S_IRWXU | stat.S_IROTH | stat.S_IXOTH,
1630
+ )
1631
+ except OSError as exc:
1632
+ raise ValueError(
1633
+ f'ERROR: Unable to create directory "{output_path}"!'
1634
+ ) from exc
1635
+ return str(output_path)
1636
+
1637
+
1638
+ def _set_file_name(self):
1639
+ """
1640
+ Set file name
1641
+ """
1642
+
1643
+
1644
+ now = datetime.now()
1645
+ timestamp_id = now.strftime("D%Y%m%d_T%H%M%S")
1646
+ file_name = f"didv_analysis_{timestamp_id}.hdf5"
1647
+
1648
+ return file_name