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.
- pytesprocess/__init__.py +9 -0
- pytesprocess/_version.py +2 -0
- pytesprocess/cli/__init__.py +1 -0
- pytesprocess/cli/commands/__init__.py +5 -0
- pytesprocess/cli/commands/event.py +66 -0
- pytesprocess/cli/commands/filter.py +17 -0
- pytesprocess/cli/commands/ivsweep.py +29 -0
- pytesprocess/cli/common.py +86 -0
- pytesprocess/cli/main.py +81 -0
- pytesprocess/config/__init__.py +4 -0
- pytesprocess/config/loader.py +94 -0
- pytesprocess/config/manager.py +297 -0
- pytesprocess/config/resolvers/__init__.py +5 -0
- pytesprocess/config/resolvers/common.py +56 -0
- pytesprocess/config/resolvers/feature.py +293 -0
- pytesprocess/config/resolvers/salting.py +86 -0
- pytesprocess/config/resolvers/trigger.py +84 -0
- pytesprocess/config/selectors.py +108 -0
- pytesprocess/config/validation.py +314 -0
- pytesprocess/config/warnings.py +2 -0
- pytesprocess/core/__init__.py +10 -0
- pytesprocess/core/algorithms.py +1455 -0
- pytesprocess/core/didv.py +1648 -0
- pytesprocess/core/eventbuilder.py +495 -0
- pytesprocess/core/filterbuilder.py +81 -0
- pytesprocess/core/filterdata.py +1849 -0
- pytesprocess/core/ivsweep.py +2072 -0
- pytesprocess/core/noise.py +923 -0
- pytesprocess/core/noisemodel.py +1408 -0
- pytesprocess/core/oftrigger.py +1035 -0
- pytesprocess/core/template.py +450 -0
- pytesprocess/process/__init__.py +6 -0
- pytesprocess/process/data_source.py +185 -0
- pytesprocess/process/event_context.py +35 -0
- pytesprocess/process/feature_plan.py +186 -0
- pytesprocess/process/feature_resources.py +267 -0
- pytesprocess/process/features.py +1024 -0
- pytesprocess/process/filterprocess.py +1176 -0
- pytesprocess/process/ivprocess.py +1380 -0
- pytesprocess/process/processing_data.py +967 -0
- pytesprocess/process/randoms.py +921 -0
- pytesprocess/process/triggers.py +1011 -0
- pytesprocess/salting/__init__.py +7 -0
- pytesprocess/salting/generator.py +364 -0
- pytesprocess/salting/injector.py +329 -0
- pytesprocess/salting/sampling.py +84 -0
- pytesprocess/utils/__init__.py +5 -0
- pytesprocess/utils/arg_utils.py +122 -0
- pytesprocess/utils/dataframe_output.py +120 -0
- pytesprocess/utils/filter_hdf5.py +594 -0
- pytesprocess/utils/utils.py +701 -0
- pytesprocess/workflows/__init__.py +3 -0
- pytesprocess/workflows/processing.py +317 -0
- pytesprocess/workflows/salting.py +133 -0
- pytesprocess-0.1.1.dist-info/METADATA +211 -0
- pytesprocess-0.1.1.dist-info/RECORD +60 -0
- pytesprocess-0.1.1.dist-info/WHEEL +5 -0
- pytesprocess-0.1.1.dist-info/entry_points.txt +2 -0
- pytesprocess-0.1.1.dist-info/licenses/LICENSE +21 -0
- 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))
|