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,701 @@
1
+ import os
2
+ import numpy as np
3
+ from scipy.signal import find_peaks
4
+ import stat
5
+
6
+
7
+ __all__ = ['split_channel_name', 'extract_window_indices',
8
+ 'find_linear_segment', 'create_directory', 'get_ind_freq_ranges',
9
+ 'is_empty','unique_list','estimate_sampling_rate' ,'find_psd_peaks',
10
+ 'get_trigger_template_info','convert_length_msec_to_samples']
11
+
12
+
13
+
14
+ def create_directory(directory_path):
15
+ """
16
+ create (sub) directory
17
+ """
18
+
19
+ if not os.path.isdir(directory_path):
20
+ try:
21
+ os.makedirs(directory_path)
22
+ os.chmod(directory_path, stat.S_IRWXG | stat.S_IRWXU | stat.S_IROTH | stat.S_IXOTH)
23
+ except OSError:
24
+ raise ValueError('\nERROR: Unable to create directory "'+ directory_path + '"!\n')
25
+
26
+
27
+
28
+
29
+ def split_channel_name(channel_name,
30
+ available_channels=None,
31
+ separator=None,
32
+ label=None):
33
+ """
34
+ Split channel name after various checks and return
35
+ list of individual channels and separator(s)
36
+ """
37
+
38
+ # allowed separators
39
+ allowed_separators = [',', '|', '+' ,'-']
40
+
41
+ # strip whitespace
42
+ channel_name = channel_name.replace(' ','')
43
+
44
+ # check if separator allowed
45
+ if (separator is not None
46
+ and separator not in allowed_separators):
47
+ raise ValueError(
48
+ f'ERROR: separator "{separator}" not '
49
+ f'recognized. Allowed separator '
50
+ f'{allowed_separators} ')
51
+
52
+ # check if channel_name has any separators
53
+ has_separator = False
54
+ for sep in allowed_separators:
55
+ if sep in channel_name:
56
+ has_separator = True
57
+ break
58
+ if not has_separator:
59
+ return [channel_name], None
60
+
61
+
62
+ # case available_channels is None
63
+ if available_channels is None:
64
+
65
+ if separator is None:
66
+ raise ValueError(
67
+ 'ERROR: separator required when '
68
+ '"available_channels" not provided! ')
69
+
70
+ if separator == '-':
71
+ raise ValueError(
72
+ 'ERROR: "available_channels" required '
73
+ 'when using separator "-"')
74
+
75
+ if ((separator == '-' or separator == '+')
76
+ and (',' in channel_name or '|' in channel_name)):
77
+ raise ValueError(
78
+ f'ERROR: Channels cannot be split with '
79
+ f'{separator} before channels split with '
80
+ f'"," and "|"')
81
+
82
+ channel_list = channel_name.split(separator)
83
+ return channel_list, separator
84
+
85
+ # from this point available_channels has been provided
86
+
87
+ # case already an individual channel
88
+ # or no separator found
89
+ if (channel_name in available_channels
90
+ or channel_name == 'all'):
91
+ return [channel_name], None
92
+
93
+ # get list of separators
94
+ channel_check = channel_name
95
+ channel_list = []
96
+ for chan in available_channels:
97
+ if chan in channel_check:
98
+ channel_check = channel_check.replace(chan, '')
99
+ channel_list.append(chan)
100
+
101
+ separator_list = [x for x in channel_check]
102
+ separator_list = list(set(separator_list))
103
+
104
+ # check if any channels are unavailable
105
+ non_separator_list = []
106
+ for sep in separator_list:
107
+ if sep not in allowed_separators:
108
+ non_separator_list.append(sep)
109
+ if non_separator_list:
110
+ raise ValueError(
111
+ f'ERROR: Unidentified channel "{channel_name}" in yaml file! '
112
+ f'Perhaps not in raw data? Available channels = {available_channels}')
113
+
114
+ # if no separator
115
+ if separator is None:
116
+
117
+ if len(separator_list) == 1:
118
+ separator_list = separator_list[0]
119
+ if separator_list != '-':
120
+ channel_list = channel_name.split(separator_list)
121
+
122
+ return channel_list, separator_list
123
+
124
+ # case separator provided
125
+
126
+ # check if separator in channe_name
127
+ if separator not in channel_name:
128
+ return [channel_name], None
129
+
130
+ # case not '-'
131
+ if separator != '-':
132
+ channel_list = channel_name.split(separator)
133
+ return channel_list.copy(), separator
134
+
135
+
136
+ if ('|' in channel_name
137
+ or '+' in channel_name
138
+ or ',' in channel_name):
139
+
140
+ raise ValueError(f'Multiple separators available, split first '
141
+ f'with other separators before "-"')
142
+ else:
143
+ return channel_list.copy(), separator
144
+
145
+
146
+
147
+
148
+ def extract_window_indices(nb_samples,
149
+ nb_samples_pretrigger, fs,
150
+ window_min_from_start_usec=None,
151
+ window_min_to_end_usec=None,
152
+ window_min_from_trig_usec=None,
153
+ window_max_from_start_usec=None,
154
+ window_max_to_end_usec=None,
155
+ window_max_from_trig_usec=None):
156
+ """
157
+ Calculate window index min and max from various types
158
+ of window definition
159
+
160
+ Parameters
161
+ ---------
162
+
163
+ nb_samples : int
164
+ total number of samples
165
+
166
+ nb_samples_pretrigger : int
167
+ number of pretrigger samples
168
+
169
+ fs: float
170
+ sample rate
171
+
172
+ window_min_from_start_usec : float, optional
173
+ OF filter window start in micro seconds defined
174
+ from beginning of trace
175
+
176
+ window_min_to_end_usec : float, optional
177
+ OF filter window start in micro seconds defined
178
+ as length to end of trace
179
+
180
+ window_min_from_trig_usec : float, optional
181
+ OF filter window start in micro seconds from
182
+ pre-trigger (can be negative if prior pre-trigger)
183
+
184
+
185
+ window_max_from_start_usec : float, optional
186
+ OF filter window max in micro seconds defined
187
+ from beginning of trace
188
+
189
+ window_max_to_end_usec : float, optional
190
+ OF filter window max in micro seconds defined
191
+ as length to end of trace
192
+
193
+
194
+ window_max_from_trig_usec : float, optional
195
+ OF filter window end in micro seconds from
196
+ pre-trigger (can be negative if prior pre-trigger)
197
+
198
+
199
+
200
+
201
+ Return:
202
+ ------
203
+
204
+ min_index : int
205
+ trace index window min
206
+
207
+ max_index : int
208
+ trace index window max
209
+ """
210
+
211
+ # ------------
212
+ # min window
213
+ # ------------
214
+ min_index = 0
215
+ if window_min_from_start_usec is not None:
216
+ min_index = int(window_min_from_start_usec*fs*1e-6)
217
+ elif window_min_to_end_usec is not None:
218
+ min_index = (nb_samples
219
+ - abs(int(window_min_to_end_usec*fs*1e-6))
220
+ - 1)
221
+ elif window_min_from_trig_usec is not None:
222
+ min_index = (nb_samples_pretrigger
223
+ + int(window_min_from_trig_usec*fs*1e-6))
224
+
225
+ # check
226
+ if min_index<0:
227
+ min_index=0
228
+ elif min_index>nb_samples-1:
229
+ min_index=nb_samples-1
230
+
231
+
232
+ # -------------
233
+ # max index
234
+ # -------------
235
+ max_index = nb_samples -1
236
+ if window_max_from_start_usec is not None:
237
+ max_index = int(window_max_from_start_usec*fs*1e-6)
238
+ elif window_max_to_end_usec is not None:
239
+ max_index = (nb_samples
240
+ - abs(int(window_max_to_end_usec*fs*1e-6))
241
+ - 1)
242
+ elif window_max_from_trig_usec is not None:
243
+ max_index = (nb_samples_pretrigger
244
+ + int(window_max_from_trig_usec*fs*1e-6))
245
+
246
+ # check
247
+ if max_index<0:
248
+ max_index=0
249
+ elif max_index>nb_samples-1:
250
+ max_index=nb_samples-1
251
+
252
+
253
+ if max_index<min_index:
254
+ raise ValueError('ERROR window calculation: '
255
+ + 'max index smaller than min!'
256
+ + 'Check configuration!')
257
+
258
+
259
+
260
+ return min_index, max_index
261
+
262
+
263
+ def find_linear_segment(x, y, tolerance=0.05):
264
+ """
265
+ Find linear segment within tolerance using first 3 points
266
+ fit (distance based on standardized X and Y using
267
+ first 3 points mean/std).
268
+ """
269
+ # check length
270
+ if len(x)<3:
271
+ print('WARNING: Not enough points to check linearity!')
272
+ return []
273
+
274
+ if len(x) != len(y):
275
+ raise ValueError('ERROR: X and Y arrays should have same length!')
276
+
277
+ # standardize data using mean/std first 3 points
278
+ xmean = np.mean(x[:3])
279
+ xstd = np.std(x[:3])
280
+ x = (x - xmean) / xstd
281
+
282
+ ymean = np.mean(y[:3])
283
+ ystd = np.std(y[:3])
284
+ y = (y - ymean) / ystd
285
+
286
+ # Use only the first three points to fit a linear
287
+ # regression line
288
+ slope, intercept = np.polyfit(x[:3], y[:3], 1)
289
+
290
+ # Calculate fitted values for all points
291
+ y_fit = slope * x + intercept
292
+
293
+ # Compute deviations for all points
294
+ deviations = np.abs(y - y_fit)
295
+
296
+
297
+ # get linear index list
298
+ # the deviation for the first 3 points used for the fit
299
+ # should be very small. Will use tolerance/10
300
+ index_list = list()
301
+ nb_points = len(deviations)
302
+ for idx in range(nb_points):
303
+ deviation = deviations[idx]
304
+ if (idx<3 and deviation>tolerance/10):
305
+ return []
306
+ if deviation>tolerance:
307
+ if nb_points>idx+1:
308
+ if deviations[idx+1]>tolerance:
309
+ break
310
+ else:
311
+ break
312
+ else:
313
+ index_list.append(idx)
314
+
315
+ return index_list
316
+
317
+ def is_empty(param):
318
+ """
319
+ check if empty
320
+ """
321
+
322
+ if param is None:
323
+ return True
324
+ try:
325
+ return len(param) == 0
326
+ except TypeError:
327
+ return False
328
+
329
+
330
+ def unique_list(alist):
331
+ """
332
+ make list unique
333
+ """
334
+
335
+ if not isinstance(alist, (list, np.ndarray)):
336
+ alist = [alist]
337
+
338
+ seen = set()
339
+ unique_items = []
340
+
341
+ for item in alist:
342
+ if item not in seen:
343
+ unique_items.append(item)
344
+ seen.add(item)
345
+
346
+ return unique_items
347
+
348
+
349
+ def cleanup_freq_ranges(f_lims):
350
+ """
351
+ cleanup frequency range for psd_peaks, also return feature
352
+ base names
353
+ """
354
+ if not isinstance(f_lims, list):
355
+ f_lims = [f_lims]
356
+
357
+ # loop
358
+ freq_ranges = list()
359
+ range_names = list()
360
+ for freq_range in f_lims:
361
+
362
+ # case single number
363
+ if (isinstance(freq_range, float)
364
+ or isinstance(freq_range, int)):
365
+ freq_range = [freq_range]
366
+
367
+ f_low = abs(freq_range[0])
368
+ if len(freq_range) == 2:
369
+ f_high = abs(freq_range[1])
370
+ if f_low > f_high:
371
+ f_low, f_high = f_high, f_low
372
+ name = f'{round(f_low)}_{round(f_high)}'
373
+ if name not in range_names:
374
+ freq_ranges.append([f_low, f_high])
375
+ range_names.append(f'{round(f_low)}_{round(f_high)}')
376
+ else:
377
+ name = f'{round(f_low)}'
378
+ if name not in range_names:
379
+ freq_ranges.append([f_low])
380
+ range_names.append(f'{round(f_low)}')
381
+
382
+ return freq_ranges, range_names
383
+
384
+
385
+
386
+
387
+ def get_ind_freq_ranges(freq_ranges, freqs):
388
+ """
389
+ Return index list
390
+ """
391
+
392
+ idx_ranges = list()
393
+ for freq_range in freq_ranges:
394
+ f_low = abs(freq_range[0])
395
+ ind_low = int(np.argmin(np.abs(freqs - f_low)))
396
+ ind_high = ind_low + 1
397
+ if len(freq_range) == 2:
398
+ f_high = abs(freq_range[1])
399
+ ind_high = int(np.argmin(np.abs(freqs - f_high)))
400
+
401
+ # Ensure order
402
+ if ind_low > ind_high:
403
+ ind_low, ind_high = ind_high, ind_low
404
+
405
+ # Handle identical indices
406
+ if ind_low == ind_high:
407
+ if ind_high < len(freqs) - 1:
408
+ ind_high += 1
409
+ elif ind_low > 0:
410
+ ind_low -= 1
411
+ else:
412
+ raise ValueError("Frequency range too narrow or outside bounds.")
413
+ idx_ranges.append([ind_low, ind_high])
414
+
415
+
416
+ return idx_ranges
417
+
418
+
419
+ def estimate_sampling_rate(freq_array):
420
+ """
421
+ Estimate the sampling rate from a frequency array that may be:
422
+ - Double-sided (fftfreq)
423
+ - Single-sided (rfftfreq)
424
+
425
+ Parameters
426
+ ----------
427
+ freq_array : array-like
428
+ Array of frequencies (e.g., as returned by fftfreq or rfftfreq).
429
+
430
+ Returns
431
+ -------
432
+ fs : float
433
+ Estimated sampling rate.
434
+
435
+ Notes
436
+ -----
437
+ - For a double-sided array of length N, the bin spacing is fs/N,
438
+ and there are negative frequencies in the array.
439
+ - For a single-sided array of length M = (N//2) + 1, the bin spacing
440
+ is still fs/N, but the array contains only [0, fs/N, 2fs/N, ..., fs/2].
441
+
442
+ """
443
+ freq_array = np.asarray(freq_array)
444
+
445
+ # Sort (in case the input is not sorted) and remove duplicates
446
+ freq_sorted = np.unique(np.sort(freq_array))
447
+
448
+ # Find the smallest positive frequency (this is our bin spacing, df)
449
+ positive_mask = freq_sorted > 0
450
+ if not np.any(positive_mask):
451
+ raise ValueError("No positive frequencies found; cannot infer sampling rate.")
452
+
453
+ df = freq_sorted[positive_mask][0] # first positive frequency
454
+
455
+ # Check if we have negative frequencies (i.e. double-sided)
456
+ if freq_sorted[0] < 0:
457
+ # Double-sided array (e.g., fftfreq)
458
+ N = len(freq_array)
459
+ else:
460
+ # Single-sided array (e.g., rfftfreq)
461
+ # For real-valued time-domain signals,
462
+ # length of rfftfreq array = N//2 + 1.
463
+ # => N = 2 * (len(freq_array) - 1) if N is even
464
+ # (also works for odd N in practice because rfftfreq definition.)
465
+ N = 2 * (len(freq_array) - 1)
466
+
467
+ fs = N * df
468
+ return fs
469
+
470
+
471
+ def find_psd_peaks(
472
+ f, psd,
473
+ fmin=100.0, fmax=1000.0,
474
+ npeaks=1,
475
+ min_separation_hz=0.0,
476
+ min_prominence=None):
477
+
478
+ """
479
+ Find up to `npeaks` highest peaks in a PSD between fmin and fmax.
480
+
481
+ Parameters
482
+ ----------
483
+ f : array-like
484
+ Frequency array (Hz).
485
+ psd : array-like
486
+ PSD values. Linear units by default. If `use_db=True`, provide in dB.
487
+ fmin, fmax : float
488
+ Frequency search band (Hz).
489
+ npeaks : int
490
+ Number of peaks to return (top-N by amplitude within band).
491
+ min_separation_hz : float
492
+ Enforce a minimum separation between detected peaks (in Hz).
493
+ min_prominence : float or None
494
+ Minimum prominence (same units as `Pxx`, or dB if `use_db=True`).
495
+ Leave None to let `find_peaks` decide automatically.
496
+
497
+ Returns
498
+ -------
499
+ peaks : list of dict
500
+ Each dict has: 'freq', 'amplitude', 'prominence', 'left_base_freq', 'right_base_freq', 'index'
501
+ """
502
+
503
+ f = np.asarray(f)
504
+ y = np.asarray(psd)
505
+
506
+ # Restrict to search band
507
+ band_mask = (f >= fmin) & (f <= fmax)
508
+
509
+ if not np.any(band_mask):
510
+ # No bins fall strictly inside the band: choose the closest bin
511
+ # Strategy: compute distance to the interval [fmin, fmax]
512
+ # If f[i] < fmin -> distance = fmin - f[i]
513
+ # If f[i] > fmax -> distance = f[i] - fmax
514
+ # If f[i] inside -> distance = 0 (but this case is already excluded here)
515
+ distances = np.where(f < fmin, fmin - f,
516
+ np.where(f > fmax, f - fmax, 0.0))
517
+ idx = np.argmin(distances)
518
+ f_band = np.array([f[idx]])
519
+ y_band = np.array([y[idx]])
520
+ return [{
521
+ "freq": float(f_band[0]),
522
+ "amplitude": float(y_band[0]),
523
+ "prominence": None,
524
+ "left_base_freq": None,
525
+ "right_base_freq": None,
526
+ "index": int(idx),
527
+ }]
528
+
529
+
530
+ f_band = f[band_mask]
531
+ y_band = y[band_mask]
532
+ base = int(np.where(band_mask)[0][0])
533
+
534
+ # Case only one bin in range
535
+ if len(f_band) == 1:
536
+ return [{
537
+ "freq": float(f_band[0]),
538
+ "amplitude": float(y_band[0]),
539
+ "prominence": None,
540
+ "left_base_freq": None,
541
+ "right_base_freq": None,
542
+ "index": int(base),
543
+ }]
544
+
545
+ # Frequency resolution -> convert min separation (Hz) to bins
546
+ df = np.median(np.diff(f_band)) if len(f_band) > 1 else np.inf
547
+ if np.isfinite(df) and min_separation_hz > 0:
548
+ distance_bins = max(1, int(np.ceil(min_separation_hz / df)))
549
+ else:
550
+ distance_bins = 0 # safe default
551
+
552
+
553
+ # Peak finding
554
+ # Prominence is given in the same units as y_band (linear or dB)
555
+ distance_arg = distance_bins if distance_bins >= 1 else None
556
+ peaks_idx, props = find_peaks(y_band, prominence=min_prominence, distance=distance_arg)
557
+
558
+ if peaks_idx.size == 0:
559
+
560
+ # Fallback: take up to `npeaks` largest bins, optionally separated by `distance_bins`
561
+ y_work = y_band.copy()
562
+ picked = []
563
+
564
+ for _ in range(min(npeaks, len(y_work))):
565
+ j = int(np.nanargmax(y_work))
566
+ if not np.isfinite(y_work[j]):
567
+ break
568
+ picked.append(j)
569
+
570
+ # Suppress neighbors so we don;t pick adjacent bins of the same line
571
+ if distance_bins > 0:
572
+ lo = max(0, j - distance_bins)
573
+ hi = min(len(y_work), j + distance_bins + 1)
574
+ y_work[lo:hi] = -np.inf
575
+ else:
576
+ y_work[j] = -np.inf # just exclude the chosen bin
577
+
578
+ # Build results, ordered by amplitude (desc)
579
+ results = [{
580
+ "freq": float(f_band[j]),
581
+ "amplitude": float(y_band[j]),
582
+ "prominence": None,
583
+ "left_base_freq": None,
584
+ "right_base_freq": None,
585
+ "index": int(base + j),
586
+ } for j in picked]
587
+
588
+ results.sort(key=lambda d: d["amplitude"], reverse=True)
589
+ return results
590
+
591
+
592
+ # Sort by height (amplitude) descending and take top-N
593
+ sort_order = np.argsort(y_band[peaks_idx])[::-1]
594
+ top = sort_order[:npeaks]
595
+
596
+ results = []
597
+ for i in top:
598
+ idx = peaks_idx[i]
599
+ amp = float(y_band[idx])
600
+ freq = float(f_band[idx])
601
+ prom = None
602
+ left_base_freq = None
603
+ right_base_freq = None
604
+ if props:
605
+ if 'prominence' in props:
606
+ prom = float(props['prominence'][i])
607
+ if 'left_bases' in props:
608
+ lb = props['left_bases'][i]
609
+ left_base_freq = float(f_band[lb])
610
+ if 'right_bases' in props:
611
+ rb = props['right_bases'][i]
612
+ right_base_freq = float(f_band[rb])
613
+ results.append({
614
+ 'freq': freq,
615
+ 'amplitude': amp,
616
+ 'prominence': prom,
617
+ 'left_base_freq':left_base_freq,
618
+ 'right_base_freq': right_base_freq,
619
+ 'index': int(base + idx) # index in original arrays
620
+ })
621
+ # Sort the returned list by frequency
622
+ # results.sort(key=lambda d: d['freq'])
623
+ return results
624
+
625
+
626
+
627
+ def get_trigger_template_info(trigger_config, filter_data_inst):
628
+ """
629
+ Check template length/pretrigger
630
+ for deadtime estimate
631
+ """
632
+
633
+ trigger_info = dict()
634
+ posttrigger_list = list()
635
+ pretrigger_list = list()
636
+
637
+ # loop channels
638
+
639
+ for trigger_chan, trigger_dict in trigger_config['channels'].items():
640
+
641
+ if not trigger_dict['run']:
642
+ continue
643
+
644
+ chan = trigger_dict['channel_name']
645
+ template_tag = trigger_dict['template_tag']
646
+ template, _, template_metadata = filter_data_inst.get_template(
647
+ chan,
648
+ tag=template_tag,
649
+ return_metadata=True)
650
+
651
+ # get info
652
+ sample_rate = template_metadata['sample_rate']
653
+ nb_pretrigger_samples = template_metadata['nb_pretrigger_samples']
654
+ nb_samples = template_metadata['nb_samples']
655
+ nb_posttrigger_samples = nb_samples - nb_pretrigger_samples
656
+
657
+ # convert to msec
658
+ pretrigger_length_msec = 1e3*nb_pretrigger_samples/sample_rate
659
+ posttrigger_length_msec = 1e3*nb_posttrigger_samples/sample_rate
660
+ trace_length_msec = 1e3*nb_samples/sample_rate
661
+
662
+
663
+ # save
664
+ trigger_info[trigger_chan] = {
665
+ 'nb_pretrigger_samples': nb_pretrigger_samples,
666
+ 'nb_posttrigger_samples': nb_posttrigger_samples,
667
+ 'nb_samples': nb_samples,
668
+ 'pretrigger_length_msec': pretrigger_length_msec,
669
+ 'posttrigger_length_msec': posttrigger_length_msec,
670
+ 'trace_length_msec': trace_length_msec
671
+ }
672
+
673
+
674
+ posttrigger_list.append(posttrigger_length_msec)
675
+ pretrigger_list.append(pretrigger_length_msec)
676
+
677
+
678
+
679
+ # find min/max
680
+ trigger_info['min_posttrigger_length_msec'] = min(posttrigger_list)
681
+ trigger_info['max_posttrigger_length_msec'] = max(posttrigger_list)
682
+ trigger_info['min_pretrigger_length_msec'] = min(pretrigger_list)
683
+ trigger_info['max_pretrigger_length_msec'] = max(pretrigger_list)
684
+ trigger_info['min_edge_exclusion'] = min(trigger_info['min_posttrigger_length_msec'],
685
+ trigger_info['min_pretrigger_length_msec'])
686
+ trigger_info['max_edge_exclusion'] = max(trigger_info['max_posttrigger_length_msec'],
687
+ trigger_info['max_pretrigger_length_msec'])
688
+
689
+
690
+ return trigger_info
691
+
692
+ def twopole(t,rtau, ftau, amp0, t0):
693
+ pulse = amp0*(np.exp(-(t-t0)/ftau) - np.exp(-(t-t0)/rtau))
694
+ if (ftau<=rtau):
695
+ ftau=rtau*1.001 # error-trap
696
+ twopoletrace = np.where(pulse > 0 , pulse, 0)
697
+ return twopoletrace
698
+
699
+ def convert_length_msec_to_samples(length_msec: float, sample_rate_hz: float) -> int:
700
+ """Convert a relative duration in milliseconds to the nearest sample count."""
701
+ return int(round(float(sample_rate_hz) * float(length_msec) / 1000.0))
@@ -0,0 +1,3 @@
1
+ from .processing import run_processing_workflow, run_trigger_feature
2
+
3
+ __all__ = ["run_processing_workflow", "run_trigger_feature"]