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,1455 @@
1
+ import numpy as np
2
+ import qetpy as qp
3
+ from scipy import signal
4
+ from scipy.optimize import curve_fit
5
+
6
+ from pytesprocess.utils import utils
7
+
8
+ # NumPy 2.0 introduced ``np.trapezoid`` as the replacement for
9
+ # ``np.trapz``; ``np.trapz`` was later removed. Keep compatibility
10
+ # with both NumPy 1.x and newer NumPy releases.
11
+ try:
12
+ _trapezoid = np.trapezoid
13
+ except AttributeError: # NumPy < 2.0
14
+ _trapezoid = np.trapz
15
+
16
+
17
+ __all__ = [
18
+ 'FeatureExtractors',
19
+ ]
20
+
21
+
22
+ class FeatureExtractors:
23
+ """
24
+ A class that contains all of the possible feature extractors
25
+ for a given trace, assuming processing on a single channel.
26
+ Each feature extraction function is a staticmethod for processing
27
+ convenience.
28
+
29
+ """
30
+
31
+ @staticmethod
32
+ def ofnxmx2(channel,
33
+ of_base,
34
+ available_channels=None,
35
+ feature_base_name='ofnxmx2',
36
+ template_tag=None,
37
+ amplitude_names=None,
38
+ fit_window = None,
39
+ **kwargs):
40
+ """
41
+ Feature extraction for the nxmx2 Optimum Filter.
42
+
43
+ Parameters
44
+ ----------
45
+ channel : str
46
+ channel with format 'chan1|chan2|chan3'
47
+ (order matter)
48
+
49
+ of_base : OFBase object
50
+ OFBase QETpy object
51
+
52
+ lowchi2_fcutoff : float, optional
53
+ The frequency (in Hz) that we should cut off the chi^2 when
54
+ calculating the low frequency chi^2. Default is 10 kHz. (NOT IMPLEMENTED)
55
+
56
+ available_channels : list
57
+ list of available channels
58
+
59
+
60
+ Returns
61
+ -------
62
+ retdict : dict
63
+ Dictionary containing the various extracted features.
64
+
65
+
66
+ """
67
+
68
+ # split channel name into list (same order)
69
+ channel_list, separator = utils.split_channel_name(
70
+ channel,
71
+ available_channels=available_channels,
72
+ separator='|')
73
+
74
+ nchans = len(channel_list)
75
+
76
+
77
+ if template_tag is None:
78
+ raise ValueError(f'ERROR: Missing "template_tag" argument '
79
+ f'for channel {channel}, '
80
+ f'algorithm "{feature_base_name}"')
81
+
82
+ # time constraints tag is the name of algorithm
83
+ time_constraints_tag = feature_base_name
84
+
85
+ # check template available
86
+ template = of_base.template(channel,
87
+ template_tag=template_tag)
88
+ if template is None:
89
+ raise ValueError(f'ERROR: Missing template '
90
+ f'for channel {channel}, '
91
+ f'tag "{template_tag}", '
92
+ f'algorithm "{feature_base_name}"')
93
+
94
+
95
+ ntmps = template.shape[1]
96
+
97
+
98
+ # amplitude names
99
+ if amplitude_names is None:
100
+ amplitude_names = []
101
+ for itmp in range(ntmps):
102
+ amplitude_names.append(f'amp{itmp+1}')
103
+
104
+ else:
105
+
106
+ if isinstance(amplitude_names, str):
107
+ amplitude_names = [amplitude_names]
108
+
109
+ if len(amplitude_names) != ntmps:
110
+ raise ValueError(
111
+ f'ERROR: Wrong length for "amplitude_names" '
112
+ f'argument. Expecting {ntmps} name '
113
+ f'for channel {channel}, '
114
+ f'algorithm "{feature_base_name}"')
115
+
116
+
117
+
118
+ # initalize dictionary
119
+ retdict = dict()
120
+ retdict[f'chi2_{feature_base_name}'] = -999999.0,
121
+ retdict[f'delta_t_{feature_base_name}'] = -999999.0,
122
+ for iamp, amp_name in enumerate(amplitude_names):
123
+ retdict[f'{amp_name}_{feature_base_name}'] = -999999.0
124
+
125
+ # check if signal stored
126
+ if not of_base.is_signal_stored(channel):
127
+ return retdict
128
+
129
+ # instantiate OF NxM
130
+ OF = qp.OFnxmx2(of_base=of_base,
131
+ channels=channel,
132
+ template_tag=template_tag,
133
+ time_constraints_tag=time_constraints_tag,
134
+ verbose=False)
135
+
136
+ # calc
137
+ OF.calc()
138
+
139
+ amps, deltat, chi2 = OF.get_fit()
140
+
141
+ retdict[f'chi2_{feature_base_name}'] = chi2
142
+ retdict[f'delta_t_{feature_base_name}'] = deltat
143
+ for iamp, amp_name in enumerate(amplitude_names):
144
+ retdict[f'{amp_name}_{feature_base_name}'] = amps[iamp]
145
+
146
+ return retdict
147
+
148
+ @staticmethod
149
+ def ofnxm(channel, of_base,
150
+ available_channels=None,
151
+ feature_base_name='ofnxm',
152
+ template_tag=None,
153
+ amplitude_names=None,
154
+ window_min_from_trig_usec=None,
155
+ window_max_from_trig_usec=None,
156
+ window_min_index=None,
157
+ window_max_index=None,
158
+ lgc_outside_window=False,
159
+ lowchi2_fcutoff=10000,
160
+ interpolate_t0=False,
161
+ **kwargs):
162
+ """
163
+ Feature extraction for the NxM Optimum Filter.
164
+ Returns both constrained and nodelay paramaters.
165
+
166
+
167
+ Parameters
168
+ ----------
169
+ channel : str
170
+ channel with format 'chan1|chan2|chan3'
171
+ (order matter)
172
+
173
+ of_base : OFBase object
174
+ OFBase QETpy object
175
+
176
+ lowchi2_fcutoff : float, optional
177
+ The frequency (in Hz) that we should cut off the chi^2 when
178
+ calculating the low frequency chi^2. Default is 10 kHz.
179
+
180
+ available_channels : list
181
+ list of available channels
182
+
183
+ feature_base_name : str, option
184
+ output feature base name
185
+
186
+
187
+ Returns
188
+ -------
189
+ retdict : dict
190
+ Dictionary containing the various extracted features.
191
+
192
+ """
193
+
194
+ debug = False
195
+
196
+ # split channel name into list (same order)
197
+ channel_list, separator = utils.split_channel_name(
198
+ channel,
199
+ available_channels=available_channels,
200
+ separator='|')
201
+
202
+ nchans = len(channel_list)
203
+
204
+ # check data
205
+ if template_tag is None:
206
+ raise ValueError(f'ERROR: Missing "template_tag" argument '
207
+ f'for channel {channel}, '
208
+ f'algorithm "{feature_base_name}"')
209
+
210
+ template = of_base.template(channel,
211
+ template_tag=template_tag)
212
+ if template is None:
213
+ raise ValueError(f'ERROR: Missing template '
214
+ f'for channel {channel}, '
215
+ f'tag "{template_tag}", '
216
+ f'algorithm "{feature_base_name}"')
217
+
218
+
219
+ ntmps = template.shape[1]
220
+
221
+ if amplitude_names is None:
222
+ amplitude_names = []
223
+ for itmp in range(ntmps):
224
+ amplitude_names.append(f'amp{itmp+1}')
225
+ else:
226
+
227
+ if isinstance(amplitude_names, str):
228
+ amplitude_names = [amplitude_names]
229
+
230
+ if len(amplitude_names) != ntmps:
231
+ raise ValueError(
232
+ f'ERROR: Wrong length for "amplitude_names" '
233
+ f'argument. Expecting {ntmps} name '
234
+ f'for channel {channel}, '
235
+ f'algorithm "{feature_base_name}"')
236
+
237
+ # Initialize output
238
+ retdict = dict()
239
+ retdict[f'chi2_{feature_base_name}_constrained'] = -999999.0
240
+ retdict[f't0_{feature_base_name}_constrained'] = -999999.0
241
+ for iamp, amp_name in enumerate(amplitude_names):
242
+ retdict[f'{amp_name}_{feature_base_name}_constrained'] = -999999.0
243
+ retdict[f'chi2_{feature_base_name}_nodelay'] = -999999.0
244
+ for iamp, amp_name in enumerate(amplitude_names):
245
+ retdict[f'{amp_name}_{feature_base_name}_nodelay'] = -999999.0
246
+
247
+ # check if signal stored
248
+ if not of_base.is_signal_stored(channel):
249
+ return retdict
250
+
251
+
252
+ # instantiate OF NxM
253
+ OF = qp.OFnxm(of_base=of_base,
254
+ channels=channel,
255
+ template_tag=template_tag,
256
+ verbose=False)
257
+
258
+ # calc
259
+ OF.calc()
260
+
261
+ # get data
262
+ amps_constrained, t0_constrained, chi2_constrained = OF.get_fit_withdelay(
263
+ window_min_from_trig_usec=window_min_from_trig_usec,
264
+ window_max_from_trig_usec=window_max_from_trig_usec,
265
+ window_min_index=window_min_index,
266
+ window_max_index=window_max_index,
267
+ interpolate_t0=interpolate_t0,
268
+ lgc_outside_window=lgc_outside_window
269
+ )
270
+ amps_nodelay, t0_nodelay, chi2_nodelay = OF.get_fit_nodelay()
271
+
272
+ # store
273
+ retdict[f'chi2_{feature_base_name}_constrained'] = chi2_constrained
274
+ retdict[f't0_{feature_base_name}_constrained'] = t0_constrained
275
+ for iamp, amp_name in enumerate(amplitude_names):
276
+ retdict[f'{amp_name}_{feature_base_name}_constrained'] = amps_constrained[iamp]
277
+
278
+ retdict[f'chi2_{feature_base_name}_nodelay'] = chi2_nodelay
279
+ for iamp, amp_name in enumerate(amplitude_names):
280
+ retdict[f'{amp_name}_{feature_base_name}_nodelay'] = amps_nodelay[iamp]
281
+
282
+ return retdict
283
+
284
+
285
+ @staticmethod
286
+ def of1x1_nodelay(channel, of_base,
287
+ template_tag=None,
288
+ lowchi2_fcutoff=10000,
289
+ feature_base_name='of1x1_nodelay',
290
+ **kwargs):
291
+ """
292
+ Feature extraction for the no delay Optimum Filter.
293
+
294
+
295
+ Parameters
296
+ ----------
297
+ channel : str
298
+ channel name
299
+
300
+ of_base : OFBase object
301
+ OFBase QETpy object
302
+
303
+ template_tag : str
304
+ tag of the template to be used for OF calculation,
305
+
306
+
307
+ lowchi2_fcutoff : float, optional
308
+ The frequency (in Hz) that we should cut off the chi^2 when
309
+ calculating the low frequency chi^2. Default is 10 kHz.
310
+
311
+ feature_base_name : str, option
312
+ output feature base name
313
+
314
+ Returns
315
+ -------
316
+ retdict : dict
317
+ Dictionary containing the various extracted features.
318
+
319
+ """
320
+
321
+ # check tag
322
+ if template_tag is None:
323
+ raise ValueError('ERROR: Template tag required for OF 1x1')
324
+
325
+
326
+ # intialize variables
327
+ retdict = {
328
+ ('amp_' + feature_base_name): -999999.0,
329
+ ('chi2_' + feature_base_name): -999999.0,
330
+ ('lowchi2_' + feature_base_name): -999999.0
331
+ }
332
+
333
+ # check if signal stored
334
+ if not of_base.is_signal_stored(channel):
335
+ return retdict
336
+
337
+
338
+ # instantiate OF 1x1
339
+ OF = qp.OF1x1(of_base=of_base,
340
+ channel=channel,
341
+ template_tag=template_tag)
342
+
343
+ # calc
344
+ OF.calc(lgc_fit_withdelay=False,
345
+ lgc_fit_nodelay=True,
346
+ lowchi2_fcutoff=lowchi2_fcutoff)
347
+
348
+ # get results
349
+ amp, t0, chi2, lowchi2 = OF.get_result_nodelay()
350
+
351
+ # store features
352
+ retdict = {
353
+ ('amp_' + feature_base_name): amp,
354
+ ('chi2_' + feature_base_name): chi2,
355
+ ('lowchi2_' + feature_base_name): lowchi2
356
+ }
357
+
358
+ return retdict
359
+
360
+
361
+
362
+ @staticmethod
363
+ def of1x1_unconstrained(channel, of_base,
364
+ template_tag='default',
365
+ interpolate=False,
366
+ lowchi2_fcutoff=10000,
367
+ feature_base_name='of1x1_unconstrained',
368
+ **kwargs):
369
+ """
370
+ Feature extraction for the unconstrained Optimum Filter.
371
+
372
+
373
+ Parameters
374
+ ----------
375
+ channel : str
376
+ channel name
377
+
378
+ of_base : OFBase object
379
+ OFBase
380
+
381
+ template_tag : str, option
382
+ tag of the template to be used for OF calculation,
383
+ Default: 'default'
384
+
385
+ interpolate : bool, optional
386
+ if True, do delay interpolation
387
+ default: False
388
+
389
+ lowchi2_fcutoff : float, optional
390
+ The frequency (in Hz) that we should cut off the chi^2 when
391
+ calculating the low frequency chi^2. Default is 10 kHz.
392
+
393
+ feature_base_name : str, option
394
+ output feature base name
395
+
396
+
397
+
398
+ Returns
399
+ -------
400
+ retdict : dict
401
+ Dictionary containing the various extracted features.
402
+
403
+ """
404
+
405
+ # intialize features dict
406
+ retdict = {
407
+ ('amp_' + feature_base_name): -999999.0,
408
+ ('t0_' + feature_base_name): -999999.0,
409
+ ('chi2_' + feature_base_name): -999999.0,
410
+ ('lowchi2_' + feature_base_name): -999999.0
411
+ }
412
+
413
+ # check if signal stored
414
+ if not of_base.is_signal_stored(channel):
415
+ return retdict
416
+
417
+ # instantiate OF1x1
418
+ OF = qp.OF1x1(of_base=of_base,
419
+ channel=channel,
420
+ template_tag=template_tag)
421
+ # calc
422
+ OF.calc(lowchi2_fcutoff=lowchi2_fcutoff,
423
+ interpolate_t0=interpolate,
424
+ lgc_fit_withdelay=True,
425
+ lgc_fit_nodelay=False,
426
+ lgc_plot=False)
427
+
428
+ # get results
429
+ amp, t0, chi2, lowchi2 = OF.get_result_withdelay()
430
+
431
+
432
+ # store features
433
+ retdict = {
434
+ ('amp_' + feature_base_name): amp,
435
+ ('t0_' + feature_base_name): t0,
436
+ ('chi2_' + feature_base_name): chi2,
437
+ ('lowchi2_' + feature_base_name): lowchi2
438
+ }
439
+
440
+ return retdict
441
+
442
+
443
+ @staticmethod
444
+ def of1x1_constrained(channel, of_base,
445
+ template_tag='default',
446
+ window_min_from_trig_usec=None,
447
+ window_max_from_trig_usec=None,
448
+ window_min_index=None,
449
+ window_max_index=None,
450
+ lgc_outside_window=False,
451
+ interpolate=False,
452
+ lowchi2_fcutoff=10000,
453
+ feature_base_name='of1x1_constrained',
454
+ **kwargs):
455
+ """
456
+ Feature extraction for the constrained Optimum Filter.
457
+
458
+
459
+ Parameters
460
+ ----------
461
+ channel : str
462
+ channel name
463
+
464
+ of_base : OFBase object
465
+ OFBase QETpy object
466
+
467
+ template_tag : str, optional
468
+ tag of the template to be used for OF calculation,
469
+ Default: 'default'
470
+
471
+ window_min_from_trig_usec : float, optional
472
+ OF filter window start in micro seconds from
473
+ pre-trigger (can be negative if prior pre-trigger)
474
+ Default: use "window_min_index" or
475
+ or set to 0 if both parameters are None
476
+
477
+ window_max_from_trig_usec : float, optional
478
+ OF filter window end in micro seconds from
479
+ pre-trigger (can be negative if prior pre-trigger)
480
+ Default: use "window_max_index" or set to end trace
481
+ if "window_max_index" also None
482
+
483
+
484
+ window_min_index : int, optional
485
+ ADC index OF filter window start (alternative
486
+ to "window_min_from_trig_usec")
487
+
488
+ Default: use "window_min_from_trig_usec" or
489
+ set to 0 if both parameters are None
490
+
491
+ window_max_index : int, optional
492
+ ADC index OF filter window end (alternative
493
+ to "window_min_from_trig_usec")
494
+
495
+ Default: use "window_max_from_trig_usec" or
496
+ set to end of trace if both parameters
497
+ are None
498
+
499
+ lgc_outside_window : bool, optional
500
+ If True, define window to be outside [min:max]
501
+ Default: False
502
+
503
+ interpolate : bool, optional
504
+ if True, do delay interpolation
505
+ default: False
506
+
507
+ lowchi2_fcutoff : float, optional
508
+ The frequency (in Hz) that we should cut off the chi^2 when
509
+ calculating the low frequency chi^2. Default is 10 kHz.
510
+
511
+
512
+ feature_base_name : str, optional
513
+ output feature base name
514
+
515
+
516
+
517
+ Returns
518
+ -------
519
+ retdict : dict
520
+ Dictionary containing the various extracted features.
521
+
522
+ """
523
+
524
+ # intialize dictionary
525
+ retdict = {
526
+ ('amp_' + feature_base_name): -999999.0,
527
+ ('t0_' + feature_base_name): -999999.0,
528
+ ('chi2_' + feature_base_name): -999999.0,
529
+ ('lowchi2_' + feature_base_name): -999999.0,
530
+ ('chi2nopulse_' + feature_base_name): -999999.0,
531
+ ('ampres_' + feature_base_name): -999999.0,
532
+ ('timeres_' + feature_base_name): -999999.0
533
+ }
534
+
535
+ # check if signal stored
536
+ if not of_base.is_signal_stored(channel):
537
+ return retdict
538
+
539
+
540
+ # instantiate OF1x1
541
+ OF = qp.OF1x1(of_base=of_base,
542
+ channel=channel,
543
+ template_tag=template_tag)
544
+
545
+ # calc (signal needs to be None if set already)
546
+ OF.calc(window_min_from_trig_usec=window_min_from_trig_usec,
547
+ window_max_from_trig_usec=window_max_from_trig_usec,
548
+ window_min_index=window_min_index,
549
+ window_max_index=window_max_index,
550
+ lowchi2_fcutoff=lowchi2_fcutoff,
551
+ interpolate_t0=interpolate,
552
+ lgc_outside_window=lgc_outside_window,
553
+ lgc_fit_withdelay=True,
554
+ lgc_fit_nodelay=False,
555
+ lgc_plot=False)
556
+
557
+
558
+ # get results
559
+ amp, t0, chi2, lowchi2 = OF.get_result_withdelay()
560
+
561
+ # get chi2 no pulse
562
+ chi2_nopulse = OF.get_chisq_nopulse()
563
+
564
+ # get OF resolution
565
+ ampres = OF.get_energy_resolution()
566
+ timeres = OF.get_time_resolution()
567
+
568
+ retdict = {
569
+ ('amp_' + feature_base_name): amp,
570
+ ('t0_' + feature_base_name): t0,
571
+ ('chi2_' + feature_base_name): chi2,
572
+ ('lowchi2_' + feature_base_name): lowchi2,
573
+ ('chi2nopulse_' + feature_base_name): chi2_nopulse,
574
+ ('ampres_' + feature_base_name): ampres,
575
+ ('timeres_' + feature_base_name): timeres,
576
+ }
577
+
578
+ return retdict
579
+
580
+ @staticmethod
581
+ def of1x2x2(channel, of_base,
582
+ template_tag_1='Scintillation',
583
+ template_tag_2='Evaporation',
584
+ feature_base_name='of1x2x2',
585
+ **kwargs):
586
+ """
587
+ Feature extraction for the one channel, two template Optimum Filter.
588
+
589
+ Parameters
590
+ ----------
591
+ of_base : OFBase object, optional
592
+ OFBase if it has been instantiated independently
593
+
594
+ template_tag_1: str, option
595
+ tag of the template to be used for OF calculation of the scintillation part; please use Scintilation,
596
+ Default: 'Scintillation'
597
+
598
+ template_tag_2 : str, option
599
+ tag of the template to be used for OF calculation of the evaporation part; please use Evaporation,
600
+ Default: 'Evaporation'
601
+
602
+ feature_base_name : str, option
603
+ output feature base name
604
+
605
+
606
+
607
+ Returns
608
+ -------
609
+ retdict : dict
610
+ Dictionary containing the various extracted features.
611
+
612
+ """
613
+
614
+ # intialize dictionary
615
+ retdict = {
616
+ ('scintillation_amp_' + feature_base_name): -999999.0,
617
+ ('evaporation_amp_' + feature_base_name): -999999.0,
618
+ ('time_diff_' + feature_base_name): -999999.0,
619
+ ('scintillation_time_index' + feature_base_name): -999999.0,
620
+ ('evaporation_time_index' + feature_base_name): -999999.0
621
+ }
622
+
623
+ # check if signal stored
624
+ if not of_base.is_signal_stored(channel):
625
+ return retdict
626
+
627
+ # instantiate OF1x2
628
+ OF = qp.OF1x2(
629
+ of_base=of_base,
630
+ template_1_tag=template_tag_1,
631
+ template_2_tag=template_tag_1,
632
+ channel_name= channel,
633
+ )
634
+
635
+ # calc (signal needs to be None if set already)
636
+ OF.calc(lgc_plot=False)
637
+
638
+ # get results
639
+ scintillation_amp = OF._amplitude[OF._template_1_tag]
640
+ evaporation_amp = OF._amplitude[OF._template_2_tag]
641
+ time_diff= OF._time_diff_two_Pulses
642
+ Starting_time_first_pulse = OF._time_first_pulse
643
+ Starting_time_second_pulse = OF._time_second_pulse
644
+
645
+
646
+ # store features
647
+ retdict = {
648
+ ('scintillation_amp_' + feature_base_name): scintillation_amp,
649
+ ('evaporation_amp_' + feature_base_name): evaporation_amp,
650
+ ('time_diff_' + feature_base_name): time_diff,
651
+ ('scintillation_time_index' + feature_base_name): Starting_time_first_pulse ,
652
+ ('evaporation_time_index' + feature_base_name): Starting_time_second_pulse
653
+ }
654
+
655
+ return retdict
656
+
657
+
658
+ @staticmethod
659
+ def baseline(trace,
660
+ window_min_index=None, window_max_index=None,
661
+ feature_base_name='baseline',
662
+ **kwargs):
663
+ """
664
+ Feature extraction for the trace baseline.
665
+
666
+ Parameters
667
+ ----------
668
+ trace : ndarray
669
+ An ndarray containing the raw data to extract the feature
670
+ from.
671
+
672
+ window_min_index : int, optional
673
+ The minium index of the window used to average the trace.
674
+ Default: 0
675
+
676
+ window_max_index : int, optional
677
+ The maximum index of the window used to average the trace.
678
+ Default: end of trace
679
+
680
+ feature_base_name : str, optional
681
+ output feature base name
682
+
683
+ Returns
684
+ -------
685
+ retdict : dict
686
+ Dictionary containing the various extracted features.
687
+
688
+ """
689
+
690
+ # check if trace is empty or None
691
+ if (trace is None or trace.size==0):
692
+ retdict = {
693
+ feature_base_name: -999999.0,
694
+ }
695
+
696
+ return retdict
697
+
698
+
699
+ if window_min_index is None:
700
+ window_min_index = 0
701
+
702
+ if window_max_index is None:
703
+ window_max_index = trace.shape[-1] - 1
704
+
705
+
706
+ baseline = np.mean(trace[window_min_index:window_max_index])
707
+
708
+ retdict = {
709
+ feature_base_name: baseline,
710
+ }
711
+
712
+ return retdict
713
+
714
+
715
+
716
+ @staticmethod
717
+ def integral(trace, fs,
718
+ window_min_index=None, window_max_index=None,
719
+ feature_base_name='integral',
720
+ **kwargs):
721
+ """
722
+ Feature extraction for the pulse integral.
723
+
724
+ Parameters
725
+ ----------
726
+ trace : ndarray
727
+ An ndarray containing the raw data to extract the feature
728
+ from.
729
+
730
+ fs : float
731
+ The digitization rate of the data in trace.
732
+
733
+ window_min_index : int, optional
734
+ The minium index of the window used to integrate the trace.
735
+ Default: 0
736
+
737
+ window_max_index : int, optional
738
+ The maximum index of the window used to integrate the trace.
739
+ Default: end of trace
740
+
741
+ feature_base_name : str, optional
742
+ output feature base name
743
+
744
+ Returns
745
+ -------
746
+ retdict : dict
747
+ Dictionary containing the various extracted features.
748
+
749
+ """
750
+
751
+ # check if trace is empty or None
752
+ if (trace is None or trace.size==0):
753
+ retdict = {
754
+ feature_base_name: -999999.0,
755
+ }
756
+
757
+ return retdict
758
+
759
+
760
+ if window_min_index is None:
761
+ window_min_index = 0
762
+
763
+ if window_max_index is None:
764
+ window_max_index = trace.shape[-1] - 1
765
+
766
+
767
+ integral = _trapezoid(trace[window_min_index:window_max_index]) / fs
768
+
769
+ retdict = {
770
+ feature_base_name: integral,
771
+ }
772
+
773
+ return retdict
774
+
775
+
776
+
777
+
778
+ @staticmethod
779
+ def maximum(trace,
780
+ window_min_index=None, window_max_index=None,
781
+ feature_base_name='maximum',
782
+ **kwargs):
783
+ """
784
+ Feature extraction for the maximum pulse value.
785
+
786
+ Parameters
787
+ ----------
788
+ trace : ndarray
789
+ An ndarray containing the raw data to extract the feature
790
+ from.
791
+
792
+ window_min_index : int, optional
793
+ The minium index of the window used to find maximum
794
+ Default: 0
795
+
796
+ window_max_index : int, optional
797
+ The maximum index of the window used to find maximum
798
+ Default: end of trace
799
+
800
+ feature_base_name : str, optional
801
+ output feature base name
802
+
803
+
804
+ Returns
805
+ -------
806
+ retdict : dict
807
+ Dictionary containing the various extracted features.
808
+
809
+ """
810
+
811
+ # check if trace is empty or None
812
+ if (trace is None or trace.size==0):
813
+ retdict = {
814
+ feature_base_name: -999999.0,
815
+ }
816
+
817
+ return retdict
818
+
819
+
820
+ if window_min_index is None:
821
+ window_min_index = 0
822
+
823
+ if window_max_index is None:
824
+ window_max_index = trace.shape[-1] - 1
825
+
826
+ max_trace = np.amax(trace[window_min_index:window_max_index])
827
+
828
+ retdict = {
829
+ feature_base_name: max_trace,
830
+ }
831
+
832
+ return retdict
833
+
834
+
835
+
836
+
837
+ @staticmethod
838
+ def minimum(trace,
839
+ window_min_index=None, window_max_index=None,
840
+ feature_base_name='minimum',
841
+ **kwargs):
842
+ """
843
+ Feature extraction for the minimum pulse value.
844
+
845
+ Parameters
846
+ ----------
847
+ trace : ndarray
848
+ An ndarray containing the raw data to extract the feature
849
+ from.
850
+
851
+
852
+ window_min_index : int, optional
853
+ The minium index of the window used to find minimum
854
+ Default: 0
855
+
856
+ window_max_index : int, optional
857
+ The maximum index of the window used to find minimum
858
+ Default: end of trace
859
+
860
+ feature_base_name : str, optional
861
+ output feature base name
862
+
863
+
864
+ Returns
865
+ -------
866
+ retdict : dict
867
+ Dictionary containing the various extracted features.
868
+
869
+ """
870
+
871
+ # check if trace is empty or None
872
+ if (trace is None or trace.size==0):
873
+ retdict = {
874
+ feature_base_name: -999999.0,
875
+ }
876
+
877
+ return retdict
878
+
879
+
880
+ if window_min_index is None:
881
+ window_min_index = 0
882
+
883
+ if window_max_index is None:
884
+ window_max_index = trace.shape[-1] - 1
885
+
886
+
887
+ min_trace = np.amin(trace[window_min_index:window_max_index])
888
+
889
+ retdict = {
890
+ feature_base_name: min_trace,
891
+ }
892
+
893
+ return retdict
894
+
895
+
896
+
897
+ @staticmethod
898
+ def energyabsorbed(trace,
899
+ fs, vb, i0, rl,
900
+ window_min_index=None, window_max_index=None,
901
+ feature_base_name='energyabsorbed',
902
+ **kwargs):
903
+ """
904
+ Feature extraction for the minimum pulse value.
905
+
906
+ Parameters
907
+ ----------
908
+ trace : ndarray
909
+ An ndarray containing the raw data to extract the feature
910
+ from.
911
+ fs : float
912
+ The digitization rate of the data in trace.
913
+ vb : float
914
+ Bias voltage applied to the TES.
915
+ i0 : float
916
+ Quiescent operating current of the TES.
917
+ rl : float
918
+ Load resistance in the TES circuit.
919
+
920
+ window_min_index : int, optional
921
+ The index of the trace to start the integration.
922
+ Default: 0
923
+ window_max_index : int, optional
924
+ The index of the trace to end the integration.
925
+ Default: end of trace
926
+
927
+ feature_base_name : str, optional
928
+ output feature base name
929
+
930
+ Returns
931
+ -------
932
+ retdict : dict
933
+ Dictionary containing the various extracted features.
934
+
935
+ """
936
+
937
+ # check if trace is empty or None
938
+ if (trace is None or trace.size==0):
939
+ retdict = {
940
+ feature_base_name: -999999.0,
941
+ }
942
+
943
+ return retdict
944
+
945
+
946
+ baseline = trace[:window_min_index].mean()
947
+ i_trace = trace[window_min_index:window_max_index] - baseline
948
+
949
+ p0 = i_trace * (vb - 2*i0*rl) - i_trace**2 * rl
950
+
951
+ en_abs = _trapezoid(p0, dx=1/fs, axis=-1)
952
+
953
+ retdict = {
954
+ feature_base_name: en_abs,
955
+ }
956
+
957
+ return retdict
958
+
959
+
960
+ @staticmethod
961
+ def psd_amp(channel, fft_data=None,
962
+ of_base=None,
963
+ f_lims=[],
964
+ feature_base_name='psd_amp',
965
+ **kwargs):
966
+ """
967
+ Feature extraction for measuring the average amplitude of a
968
+ ffted trace in a range of frequencies. The FFT is supplied by the per-event FFT cache so multiple FFT-based
969
+ features can reuse the same transform.
970
+ The arguments "trace", "template", "psd"
971
+ (and associated parameters "fs", "nb_samples_pretrigger")
972
+ should only be used if not already added in OF base object.
973
+ Otherwise keep as None
974
+
975
+ Parameters
976
+ ----------
977
+ fft_data : EventFFTData-like object
978
+ Cached signal FFT/frequency view. ``of_base`` remains accepted as a
979
+ compatibility keyword for direct callers.
980
+
981
+ f_lims : list of list of floats
982
+ A list of [f_low, f_high]s between which the averaged PSD is
983
+ calculated. For example, [[45.0, 65.0], [120.0, 130.0]]
984
+
985
+ feature_base_name : str, option
986
+ output feature base name
987
+
988
+ Returns
989
+ -------
990
+ retdict : dict
991
+ Dictionary containing the various extracted features.
992
+
993
+ """
994
+
995
+ # ``of_base`` is retained as a compatibility keyword for direct callers.
996
+ # FeatureProcessing now passes a lightweight EventFFTData object.
997
+ if fft_data is None:
998
+ fft_data = of_base
999
+
1000
+ # check f_lims and define feature name
1001
+ if not f_lims:
1002
+ raise ValueError('ERROR: "f_lims" required for algorithm psd_amps')
1003
+
1004
+ freq_ranges, range_names = utils.cleanup_freq_ranges(f_lims)
1005
+
1006
+ # initialize output
1007
+ retdict = {}
1008
+ for var_base in range_names:
1009
+ var_name = f'{feature_base_name}_{var_base}'
1010
+ retdict[var_name] = -999999.0
1011
+
1012
+ # check if signal available
1013
+ if not fft_data.is_signal_stored(channel):
1014
+ return retdict
1015
+
1016
+ # get fft
1017
+ trace_fft = fft_data.signal_fft(channel, squeeze_array=True)
1018
+ trace_fft = trace_fft.copy()
1019
+ freqs = fft_data.fft_freqs().copy()
1020
+ nbins = fft_data.nb_samples()
1021
+
1022
+ # sample rate
1023
+ fs = utils.estimate_sampling_rate(freqs)
1024
+ if 'fs' in kwargs:
1025
+ fs = kwargs['fs']
1026
+
1027
+ # calculate psd
1028
+ psd = (np.abs(trace_fft)**2.0)*nbins/fs
1029
+
1030
+ # fold
1031
+ freqs_fold, psd_fold = qp.utils.fold_spectrum(psd, fs)
1032
+
1033
+ # remove DC
1034
+ psd_fold = psd_fold[1:]
1035
+ psd_fold = np.sqrt(psd_fold)
1036
+ freqs_fold = freqs_fold[1:]
1037
+
1038
+ retdict = {}
1039
+ ind_ranges = utils.get_ind_freq_ranges(freq_ranges, freqs_fold)
1040
+ for it, ind_range in enumerate(ind_ranges):
1041
+
1042
+ ind_low = ind_range[0]
1043
+ ind_high = ind_range[1]
1044
+
1045
+ # take median
1046
+ psd_chunk = psd_fold[ind_low:ind_high]
1047
+
1048
+ # smooth + max ?
1049
+ # nb_samples = len(psd_chunk)
1050
+ #psd_chunk_smooth = np.convolve(psd_chunk, np.ones(3)/3,
1051
+ # mode='valid')
1052
+ #
1053
+ psd_avg = np.average(psd_chunk)
1054
+
1055
+ # parameter name
1056
+ psd_amp_name = f'{feature_base_name}_{range_names[it]}'
1057
+ retdict[psd_amp_name] = psd_avg
1058
+
1059
+ return retdict
1060
+
1061
+
1062
+ @staticmethod
1063
+ def psd_peaks(channel, fft_data=None,
1064
+ of_base=None,
1065
+ f_lims=[],
1066
+ npeaks=1,
1067
+ min_separation_hz=0.0,
1068
+ average_range=False,
1069
+ feature_base_name='psd_peaks',
1070
+ **kwargs):
1071
+ """
1072
+ Feature extraction for finding peaks of a psd
1073
+ in a range of frequencies. The FFT is supplied by the per-event FFT cache so multiple FFT-based
1074
+ features can reuse the same transform.
1075
+
1076
+ Parameters
1077
+ ----------
1078
+ fft_data : EventFFTData-like object
1079
+ Cached signal FFT/frequency view. ``of_base`` remains accepted as a
1080
+ compatibility keyword for direct callers.
1081
+
1082
+ f_lims : list of list of floats
1083
+ A list of [f_low, f_high]s between which the PSD peak is
1084
+ calculated. For example, [[45.0, 65.0], [120.0, 130.0]]
1085
+
1086
+ npeaks : number of peaks to search. Default=1
1087
+
1088
+ min_separation_hz : minimum separation between peaks
1089
+
1090
+ feature_base_name : str, option
1091
+ output feature base name
1092
+
1093
+ Returns
1094
+ -------
1095
+ retdict : dict
1096
+ Dictionary containing the various extracted features.
1097
+
1098
+ """
1099
+ if fft_data is None:
1100
+ fft_data = of_base
1101
+
1102
+ # check f_lims and define feature name
1103
+ if not f_lims:
1104
+ raise ValueError('ERROR: "f_lims" required for algorithm psd_amps')
1105
+
1106
+ freq_ranges, range_names = utils.cleanup_freq_ranges(f_lims)
1107
+
1108
+ # initialize output
1109
+ retdict = {}
1110
+ for i in range(1, npeaks + 1):
1111
+ for var_base in range_names:
1112
+ var_name_amp = f'{feature_base_name}_{var_base}_amp_{i}'
1113
+ var_name_freq = f'{feature_base_name}_{var_base}_freq_{i}'
1114
+ retdict[var_name_amp] = -999999.0
1115
+ retdict[var_name_freq] = -999999.0
1116
+ # DC
1117
+ retdict[f'{feature_base_name}_dc_amp'] = -999999.0
1118
+
1119
+ # check if signal stored
1120
+ if not fft_data.is_signal_stored(channel):
1121
+ return retdict
1122
+
1123
+ trace_fft = fft_data.signal_fft(channel, squeeze_array=True)
1124
+ trace_fft = trace_fft.copy()
1125
+ freqs = fft_data.fft_freqs().copy()
1126
+ nbins = fft_data.nb_samples()
1127
+
1128
+ if trace_fft.ndim != 1:
1129
+ # multi-channels, not implemented
1130
+ raise ValueError(f'ERROR: "psd_peaks" not implemented for '
1131
+ f'multi-channel. Remove algorithm for '
1132
+ f'channel {channel}!')
1133
+
1134
+ # sample rate
1135
+ fs = utils.estimate_sampling_rate(freqs)
1136
+ if 'fs' in kwargs:
1137
+ fs = kwargs['fs']
1138
+
1139
+ # calculate psd
1140
+ psd = (np.abs(trace_fft)**2.0)*nbins/fs
1141
+
1142
+ # fold
1143
+ freqs_fold, psd_fold = qp.utils.fold_spectrum(psd, fs)
1144
+
1145
+ # store DC amp
1146
+ retdict[f'{feature_base_name}_dc_amp'] = np.sqrt(psd_fold[0])
1147
+
1148
+ # remove DC
1149
+ psd_fold = psd_fold[1:]
1150
+ psd_fold = np.sqrt(psd_fold)
1151
+ freqs_fold = freqs_fold[1:]
1152
+
1153
+ # loop range and find peaks
1154
+ ind_ranges = utils.get_ind_freq_ranges(freq_ranges, freqs_fold)
1155
+ for it, freq_range in enumerate(freq_ranges):
1156
+
1157
+ ind_range = ind_ranges[it]
1158
+ var_base = range_names[it]
1159
+
1160
+ # case single frequency or just take average range
1161
+ if ((ind_range[1] == ind_range[0]+1)
1162
+ or average_range):
1163
+
1164
+ ind_low = ind_range[0]
1165
+ ind_high = ind_range[1]
1166
+ psd_chunk = psd_fold[ind_low:ind_high]
1167
+ psd_avg = np.average(psd_chunk)
1168
+ freq_avg = np.average(freqs_fold[ind_low:ind_high])
1169
+ var_name_amp = f'{feature_base_name}_{var_base}_amp_1'
1170
+ var_name_freq = f'{feature_base_name}_{var_base}_freq_1'
1171
+
1172
+ retdict[var_name_amp] = psd_avg
1173
+ retdict[var_name_freq] = freq_avg
1174
+
1175
+ else:
1176
+
1177
+
1178
+ # find peaks within interval
1179
+ result_list = utils.find_psd_peaks(
1180
+ freqs_fold, psd_fold,
1181
+ fmin=freq_range[0], fmax=freq_range[1],
1182
+ npeaks=npeaks,
1183
+ min_separation_hz=min_separation_hz,
1184
+ min_prominence=None)
1185
+
1186
+
1187
+ # loop peaks
1188
+ for i in range(npeaks):
1189
+ var_name_amp = f'{feature_base_name}_{var_base}_amp_{i+1}'
1190
+ var_name_freq = f'{feature_base_name}_{var_base}_freq_{i+1}'
1191
+ if i < len(result_list):
1192
+ result = result_list[i]
1193
+ retdict[var_name_amp] = result['amplitude']
1194
+ retdict[var_name_freq] = result['freq']
1195
+ else:
1196
+ retdict[var_name_amp] = -999999.
1197
+ retdict[var_name_freq] = -999999.
1198
+
1199
+ # done
1200
+ return retdict
1201
+
1202
+ @staticmethod
1203
+ def phase(channel, fft_data=None,
1204
+ of_base=None,
1205
+ f_lims=[],
1206
+ npeaks=1,
1207
+ min_separation_hz=0.0,
1208
+ threshold_factor=1e-3,
1209
+ feature_base_name='phase',
1210
+ **kwargs):
1211
+ """
1212
+ Feature extraction for finding the phase signal for peaks of the psd
1213
+ in a range of frequencies. The FFT is supplied by the per-event FFT cache so multiple FFT-based
1214
+ features can reuse the same transform.
1215
+ Phase is returned in units of radians.
1216
+
1217
+ Parameters
1218
+ ----------
1219
+ fft_data : EventFFTData-like object
1220
+ Cached signal FFT/frequency view. ``of_base`` remains accepted as a
1221
+ compatibility keyword for direct callers.
1222
+
1223
+ f_lims : list of list of floats
1224
+ A list of [f_low, f_high]s that will be used to search for peaks in the PSD.
1225
+ The phase will be calculated and returned for frequencies
1226
+ associated with those peaks.
1227
+ Example: [[45.0, 65.0], [120.0, 130.0]].
1228
+
1229
+ npeaks : number of peaks to search. Default=1
1230
+
1231
+ min_separation_hz : minimum separation between peaks
1232
+
1233
+ threshold_factor : a threshold factor that will be applied in FFT units to avoid calculating the phase for noisy garbage.
1234
+
1235
+ feature_base_name : str, option
1236
+ output feature base name
1237
+
1238
+ Returns
1239
+ -------
1240
+ retdict : dict
1241
+ Dictionary containing the various extracted features.
1242
+
1243
+ Phase will be returned in radians.
1244
+ """
1245
+
1246
+ if fft_data is None:
1247
+ fft_data = of_base
1248
+
1249
+ # check f_lims and define feature name
1250
+ if not f_lims:
1251
+ raise ValueError('ERROR: "f_lims" required for algorithm psd_amps')
1252
+
1253
+ freq_ranges, range_names = utils.cleanup_freq_ranges(f_lims)
1254
+
1255
+ # initialize output
1256
+ retdict = {}
1257
+ for i in range(1, npeaks + 1):
1258
+ for var_base in range_names:
1259
+ var_name_amp = f'{feature_base_name}_{var_base}_phase_{i}'
1260
+ var_name_freq = f'{feature_base_name}_{var_base}_freq_{i}'
1261
+ retdict[var_name_amp] = -999999.0
1262
+ retdict[var_name_freq] = -999999.0
1263
+
1264
+
1265
+ # check if signal stored
1266
+ if not fft_data.is_signal_stored(channel):
1267
+ return retdict
1268
+
1269
+ trace_fft = fft_data.signal_fft(channel, squeeze_array=True)
1270
+ trace_fft = trace_fft.copy()
1271
+ freqs = fft_data.fft_freqs().copy()
1272
+ nbins = fft_data.nb_samples()
1273
+
1274
+ if trace_fft.ndim != 1:
1275
+ # multi-channels, not implemented
1276
+ raise ValueError(f'ERROR: "phase" not implemented for '
1277
+ f'multi-channel. Remove algorithm for '
1278
+ f'channel {channel}!')
1279
+
1280
+ # sample rate
1281
+ fs = utils.estimate_sampling_rate(freqs)
1282
+ if 'fs' in kwargs:
1283
+ fs = kwargs['fs']
1284
+
1285
+ # calculate psd
1286
+ psd = (np.abs(trace_fft)**2.0)*nbins/fs
1287
+
1288
+ # fold
1289
+ freqs_fold, psd_fold = qp.utils.fold_spectrum(psd, fs)
1290
+
1291
+ # remove DC
1292
+ psd_fold = psd_fold[1:]
1293
+ psd_fold = np.sqrt(psd_fold)
1294
+ freqs_fold = freqs_fold[1:]
1295
+
1296
+ fft_cpy = np.array(trace_fft, copy=True)
1297
+ mag = np.abs(fft_cpy)
1298
+
1299
+ # Phase reference shift for pretrigger
1300
+ nb_samples_pretrigger = kwargs.get('nb_samples_pretrigger', 0)
1301
+ t0 = nb_samples_pretrigger / fs # seconds
1302
+ fft_cpy *= np.exp(1j * 2.0 * np.pi * freqs * t0)
1303
+
1304
+ # Mask tiny-magnitude bins to avoid noisy phase
1305
+ thr = mag.max() * float(kwargs.get('threshold_factor', 0.0))
1306
+ phase = np.angle(fft_cpy)
1307
+ if thr > 0:
1308
+ phase = np.where(mag >= thr, phase, -999999.0)
1309
+
1310
+
1311
+ # Keep only positive frequencies (includes DC and Nyquist if N even)
1312
+ N = phase.shape[-1]
1313
+ pos_stop = N // 2 + 1
1314
+ phase_fold = phase[:pos_stop]
1315
+ freqs_fold = freqs[:pos_stop]
1316
+
1317
+ # Fix Nyquist if negative (happens for even N)
1318
+ if freqs_fold[-1] < 0:
1319
+ freqs_fold[-1] = abs(freqs_fold[-1])
1320
+
1321
+ # Remove DC
1322
+ phase_fold = phase_fold[1:]
1323
+ freqs_fold = freqs_fold[1:]
1324
+
1325
+ # loop ranges
1326
+ ind_ranges = utils.get_ind_freq_ranges(freq_ranges, freqs_fold)
1327
+ for it, freq_range in enumerate(freq_ranges):
1328
+
1329
+ ind_range = ind_ranges[it]
1330
+ var_base = range_names[it]
1331
+
1332
+ # case single frequency or just take average range
1333
+ if (ind_range[1] == ind_range[0]+1):
1334
+ ind_low = ind_range[0]
1335
+ ind_high = ind_range[1]
1336
+ var_name_phase = f'{feature_base_name}_{var_base}_phase_1'
1337
+ var_name_freq = f'{feature_base_name}_{var_base}_freq_1'
1338
+
1339
+ freq_val = float(freqs_fold[ind_low:ind_high].item())
1340
+ phase_val = float(phase_fold[ind_low:ind_high].item())
1341
+
1342
+ retdict[var_name_freq] = freq_val
1343
+ retdict[var_name_phase] = phase_val
1344
+
1345
+ else:
1346
+
1347
+ # find peaks within interval
1348
+ result_list = utils.find_psd_peaks(
1349
+ freqs_fold, psd_fold,
1350
+ fmin=freq_range[0], fmax=freq_range[1],
1351
+ npeaks=npeaks,
1352
+ min_separation_hz=min_separation_hz,
1353
+ min_prominence=None)
1354
+
1355
+ # loop peaks
1356
+ for i in range(npeaks):
1357
+ var_name_phase = f'{feature_base_name}_{var_base}_phase_{i+1}'
1358
+ var_name_freq = f'{feature_base_name}_{var_base}_freq_{i+1}'
1359
+ if i < len(result_list):
1360
+ result = result_list[i]
1361
+ retdict[var_name_freq] = result['freq']
1362
+ retdict[var_name_phase] = phase_fold[result['index']]
1363
+ else:
1364
+ retdict[var_name_phase] = -999999.
1365
+ retdict[var_name_freq] = -999999.
1366
+
1367
+ # done
1368
+ return retdict
1369
+
1370
+ @staticmethod
1371
+ def rftau(trace,
1372
+ rtau=None, ftau=None, amp0=None, t0=None,
1373
+ feature_base_name='rftau',
1374
+ **kwargs):
1375
+ """
1376
+ Feature extraction for the trace baseline.
1377
+
1378
+ Parameters
1379
+ ----------
1380
+ trace : ndarray
1381
+ An ndarray containing the raw data to extract the feature
1382
+ from.
1383
+
1384
+ rtau : int, optional
1385
+ The rise time of the pulse, in samples.
1386
+ Default: 30
1387
+
1388
+ ftau : int, optional
1389
+ The fall time of the pulse, in samples.
1390
+ Default: 100
1391
+
1392
+ amp0 : int, optional
1393
+ The amplitude of the pulse.
1394
+ Default: estimated from data
1395
+
1396
+ t0 : int, optional
1397
+ The start time of the pulse, in samples.
1398
+ Default: midpoint - 10 samples
1399
+
1400
+ feature_base_name : str, optional
1401
+ output feature base name
1402
+
1403
+ Returns
1404
+ -------
1405
+ retdict : dict
1406
+ Dictionary containing the various extracted features.
1407
+
1408
+ """
1409
+
1410
+ # check if trace is empty or None
1411
+ if (trace is None or trace.size==0):
1412
+ retdict = {
1413
+ feature_base_name: -999999.0,
1414
+ }
1415
+
1416
+ return retdict
1417
+
1418
+ # set default parameters
1419
+ if rtau is None:
1420
+ rtau = 30 # samples
1421
+ if ftau is None:
1422
+ ftau = 100 # samples
1423
+ if t0 is None:
1424
+ t0 = int(trace.shape[-1]/2) - 10 # midpoint-10
1425
+
1426
+ lw = 800; uw = 1600 # could make these parameters; hard-coded for now. beware changing them, long windows eat CPU time with little added benefit
1427
+ tt = np.arange(0,trace.shape[0],1)
1428
+ baseline = np.mean(trace[t0-lw:t0])
1429
+ traceb = (trace-baseline) # get a good baseline in precisely the window we care about
1430
+
1431
+ # RC filter the trace -- I wish this were done in hardware
1432
+ fs = 1.25e6 # Sampling frequency (Hz)
1433
+ fc = 50e3 # Desired cutoff frequency (Hz)
1434
+ order = 1 # RC filter order
1435
+ b, a = signal.butter(order, fc / (0.5 * fs), btype='low')
1436
+ tracerc = signal.lfilter(b,a,traceb)
1437
+
1438
+ if amp0 is None:
1439
+ amp0 = np.max(tracerc[t0-lw:t0+uw])
1440
+
1441
+ try: # this is necessary for corner cases in which the template fit barfs
1442
+ (opt, cov) = curve_fit(utils.twopole, tt[t0-lw:t0+uw], tracerc[t0-lw:t0+uw], p0 = [rtau, ftau, amp0, t0])#, bounds=(0,[100,1000,1e-6,t0+100]))
1443
+ except:
1444
+ opt = [-1,-1,-1,-1]
1445
+ amp = np.max(utils.twopole(tt, *opt))
1446
+ chisq = np.sum((tracerc[t0-lw:t0+uw]-utils.twopole(tt[t0-lw:t0+uw], *opt))**2)/(uw+lw)
1447
+
1448
+ retdict = {
1449
+ ('risetime_' + feature_base_name): opt[0],
1450
+ ('falltime_' + feature_base_name): opt[1],
1451
+ ('amplitud_' + feature_base_name): amp,
1452
+ ('chisq_' + feature_base_name): chisq,
1453
+ }
1454
+
1455
+ return retdict