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,1024 @@
1
+ import copy
2
+ import gc
3
+ import importlib
4
+ from itertools import repeat
5
+ import os
6
+ from pathlib import Path
7
+ import sys
8
+ import time
9
+ import warnings
10
+
11
+ from humanfriendly import parse_size
12
+ import numpy as np
13
+ import pandas as pd
14
+ import pyarrow as pa
15
+ import vaex as vx
16
+ import yaml
17
+ from pytesdaqx.io import AcquisitionCatalog
18
+
19
+ from pytesprocess.config import ProcessingConfig
20
+ from pytesprocess.core.algorithms import FeatureExtractors
21
+ from pytesprocess.process.feature_plan import FeaturePlan
22
+ from pytesprocess.process.feature_resources import EventTraceCache, FeatureResources
23
+ from pytesprocess.process.processing_data import ProcessingData
24
+ from pytesprocess.utils import create_dataframe_group
25
+
26
+ warnings.filterwarnings("ignore")
27
+
28
+
29
+ vx.settings.main.thread_count = 1
30
+ vx.settings.main.thread_count_io = 1
31
+ pa.set_cpu_count(1)
32
+
33
+ os.environ["OMP_NUM_THREADS"] = "1"
34
+ os.environ["MKL_NUM_THREADS"] = "1"
35
+ os.environ["NUMEXPR_NUM_THREADS"] = "1"
36
+ os.environ["OPENBLAS_NUM_THREADS"] = "1"
37
+
38
+
39
+ __all__ = [
40
+ 'FeatureProcessing'
41
+ ]
42
+
43
+ class FeatureProcessing:
44
+ """
45
+ Class to manage data processing and
46
+ extract features, dataframe can be saved
47
+ in hdf5 using vaex framework and returned
48
+ as a pandas dataframe (not both)
49
+
50
+ Multiple nodes can be used if data splitted in
51
+ different streams/tasks
52
+
53
+ """
54
+
55
+ def __init__(self, data_path, config_data,
56
+ streams=None,
57
+ trigger_dataframe_path=None,
58
+ external_file=None,
59
+ processing_label=None,
60
+ restricted=False,
61
+ data_type='background',
62
+ salting_dataframe=None,
63
+ verbose=True):
64
+ """
65
+ Intialize data processing
66
+
67
+ Parameters
68
+ ---------
69
+
70
+ data_path : str or list of str
71
+ data group directory containing data OR full path to HDF5 file
72
+ (or list of files). Data can be either raw data or
73
+ vaex dataframes
74
+
75
+ config_file : str
76
+ Full path and file name to the YAML settings for the
77
+ processing.
78
+
79
+ output_path : str, optional (default=No saved file)
80
+ base directory where output feature file will be saved
81
+
82
+
83
+ external_file : str, optional (default=no external files)
84
+ The path to a .py file with a FeatureExtractors class
85
+ to add algorithms developed by users. The name of the static
86
+ functions cannot be same as in _features (no duplicate). This is
87
+ meant for rapid development of features without
88
+ needing to rebuild the package.
89
+
90
+ streams : str or list of str, optional
91
+ Streams to process from the acquisition.
92
+
93
+
94
+ processing_label : str, optional
95
+ an optional processing name. This is used to be build output subdirectory name
96
+ and is saved as a feature in DetaFrame so it can then be used later during
97
+ analysis to make a cut on a specific processing when mutliple
98
+ datasets/processing are added together.
99
+
100
+ restricted : boolean
101
+ if True, use restricted data
102
+ if False (default), exclude restricted data
103
+
104
+ data_type : string
105
+ data type: "background", "calibration", "iv", ...
106
+
107
+ salting_dataframe : str or vaex dataframe
108
+ str if path to vaex hdf5 file or directly a dataframe
109
+
110
+
111
+ verbose : bool, optional
112
+ if True, display info
113
+
114
+ Return
115
+ ------
116
+ None
117
+ """
118
+
119
+ # processing id
120
+ self._processing_label = processing_label
121
+
122
+ # streams
123
+ self._streams = streams
124
+
125
+ # restricted data
126
+ self._restricted = restricted
127
+ if data_type != 'background': # only background data are restricted
128
+ self._restricted = False
129
+
130
+ # display
131
+ self._verbose = verbose
132
+
133
+ # if no dataframe, the data type needs to be threshold
134
+ if trigger_dataframe_path is None:
135
+ data_type = 'threshold'
136
+
137
+ # data type
138
+ self._data_type = data_type
139
+
140
+
141
+ # catalog
142
+ catalog_full = AcquisitionCatalog(data_path, verbose=verbose)
143
+
144
+ # filter based on user input
145
+ self._catalog = catalog_full.filter(
146
+ measurement_types=self._data_type,
147
+ streams=self._streams,
148
+ restricted=self._restricted)
149
+
150
+ self._acquisition_name = self._catalog.acquisition_name
151
+ self._acquisition_path = self._catalog.acquisition_path
152
+ path_obj = Path(self._acquisition_path)
153
+ self._acquisition_base_path = str(path_obj.parent)
154
+ self._available_channels = self._catalog.record_channels
155
+ self._sample_rate_hz = self._catalog.sample_rate_hz
156
+
157
+
158
+ rawdata_files = copy.deepcopy(
159
+ self._catalog.select_files_by_stream(
160
+ stream_key='stream_id',
161
+ )
162
+ )
163
+
164
+ if not rawdata_files:
165
+ raise ValueError('ERROR: No files were found! Check configuration...')
166
+
167
+ # list of streams
168
+ self._stream_list = list(rawdata_files.keys())
169
+
170
+ # config file
171
+ config_dict = {}
172
+ if isinstance(config_data, str):
173
+
174
+ if not os.path.isfile(config_data):
175
+ raise ValueError(f'ERROR: argument "{config_data}" '
176
+ f'should be a file or ProcessingConfig object!')
177
+
178
+ yaml = ProcessingConfig(config_data, self._available_channels,
179
+ sample_rate=self._sample_rate_hz)
180
+
181
+ config_dict = copy.deepcopy(yaml.get_config('feature'))
182
+
183
+ else:
184
+
185
+ if not isinstance(config_data, ProcessingConfig):
186
+ raise ValueError(
187
+ 'ERROR: raw data argument should be either '
188
+ 'a directory or ProcessingConfig object'
189
+ )
190
+
191
+ config_dict = copy.deepcopy(config_data.get_config('feature'))
192
+
193
+ # save info
194
+ self._processing_config = config_dict['channels']
195
+ self._selected_channels = config_dict['channel_list']
196
+ self._weights = config_dict['weights']
197
+ self._feature_plan = FeaturePlan(
198
+ self._processing_config, self._available_channels, self._sample_rate_hz
199
+ )
200
+ self._traces_config = self._feature_plan.traces_config
201
+ self._filter_file = config_dict['overall'].get('filter_file')
202
+
203
+ # check channels to be processed
204
+ if not self._selected_channels:
205
+ raise ValueError('ERROR: No channels to be processed! ' +
206
+ 'Check configuration...')
207
+
208
+ # Dataframe file list
209
+ self._dataframe_task_list = None
210
+ trigger_files = None
211
+ trigger_path = None
212
+ trigger_group_name = None
213
+
214
+ if trigger_dataframe_path is not None:
215
+
216
+ trigger_files, trigger_path, trigger_group_name = (
217
+ self._get_file_list(trigger_dataframe_path)
218
+ )
219
+
220
+ if not trigger_files:
221
+ raise ValueError(f'No dataframe files were found! '
222
+ f'Check configuration...')
223
+
224
+ self._dataframe_task_list = list(trigger_files.keys())
225
+
226
+ # External feature extractors
227
+ self._external_file = None
228
+ if external_file is not None:
229
+ if not os.path.exists(external_file):
230
+ raise ValueError('External feature extractors file "'
231
+ + external_file
232
+ + '" not found!')
233
+ self._external_file = external_file
234
+ if self._verbose:
235
+ print('INFO: External feature extractor = '
236
+ + self._external_file)
237
+
238
+ # get list of available features and check for duplicate
239
+ self._algorithm_list, self._ext_algorithm_list = (
240
+ self._extract_algorithm_list()
241
+ )
242
+
243
+ # instantiate processing data
244
+ self._output_group_path = None
245
+
246
+ self._processing_data_inst = ProcessingData(
247
+ self._acquisition_base_path,
248
+ rawdata_files,
249
+ acquisition_name=self._acquisition_name,
250
+ trigger_files=trigger_files,
251
+ trigger_group_name=trigger_group_name,
252
+ filter_file=self._filter_file,
253
+ available_channels=self._available_channels,
254
+ salting_dataframe=salting_dataframe,
255
+ verbose=verbose,
256
+ catalog=self._catalog)
257
+
258
+
259
+
260
+ # unique id list
261
+ self._id_list = self._stream_list
262
+ if self._dataframe_task_list is not None:
263
+ self._id_list = self._dataframe_task_list
264
+
265
+
266
+ def get_output_path(self):
267
+ """Return the current feature dataframe-group path."""
268
+ return self._output_group_path
269
+
270
+
271
+ def process(self,
272
+ nevents=-1,
273
+ lgc_save=False, save_path=None,
274
+ lgc_output=False,
275
+ ncores=1,
276
+ memory_limit='2GB'):
277
+
278
+ """
279
+ Process data
280
+
281
+ Parameters
282
+ ---------
283
+
284
+ nevents : int, optional
285
+ number of events to be processed
286
+ if not all events, requires ncores = 1
287
+ Default: all available (=-1).
288
+
289
+ lgc_save : bool, optional
290
+ if True, save dataframe in hdf5 files
291
+ Default: False
292
+
293
+ lgc_output : bool, optional
294
+ if True, return dataframe
295
+ Default: False
296
+
297
+ save_path : str, optional
298
+ base directory where output group will be saved
299
+ default: same base path as input data
300
+
301
+ ncores: int, optional
302
+ number of cores that will be used for processing
303
+ default: 1
304
+
305
+ memory_limit : str or float, optional
306
+ memory safety limit when returning a dataframe in memory.
307
+ Saved output is not split by this value; each worker writes one F#### shard.
308
+
309
+ """
310
+
311
+
312
+ # check input
313
+ if (ncores>1 and nevents>-1):
314
+ raise ValueError('ERROR: Multi cores processing only allowed when '
315
+ + 'processing ALL events!')
316
+
317
+ if lgc_output and lgc_save:
318
+ raise ValueError('ERROR: Unable to save and output datafame '
319
+ + 'at the same time. Set either lgc_output '
320
+ + 'or lgc_save to False.')
321
+
322
+ # check number cores allowed
323
+ if ncores>len(self._id_list):
324
+ ncores = len(self._id_list)
325
+ if self._verbose:
326
+ print('INFO: Changing number cores to '
327
+ + str(ncores) + ' (maximum allowed)')
328
+
329
+ # Create one dataframe-group identity for the complete feature call.
330
+ output_group_path = None
331
+ dataframe_group = None
332
+
333
+ if lgc_save:
334
+ if save_path is None:
335
+ save_path = self._processing_data_inst.get_raw_path()
336
+ if '/raw' in save_path:
337
+ save_path = save_path.replace('/raw', '/processed')
338
+ if 'processed' not in save_path:
339
+ save_path += '/processed'
340
+
341
+ if self._acquisition_name not in save_path:
342
+ save_path = save_path + '/' + self._acquisition_name
343
+
344
+ dataframe_group = create_dataframe_group(
345
+ save_path, dataframe_type='feature',
346
+ facility=self._processing_data_inst.get_facility(),
347
+ processing_label=self._processing_label,
348
+ restricted=self._restricted, data_type=self._data_type,
349
+ )
350
+ output_group_path = dataframe_group.path
351
+ if self._verbose:
352
+ print(f'INFO: Processing output group path: {output_group_path}')
353
+
354
+ self._output_group_path = output_group_path
355
+
356
+ # Prepare static feature resources once: templates, CSDs, template
357
+ # FFTs, phi/weights and NxMx2 matrices. Event signals are loaded lazily.
358
+ if self._verbose:
359
+ print('INFO: Preparing feature resources!')
360
+ self._feature_resources = FeatureResources(
361
+ self._feature_plan,
362
+ self._processing_data_inst.get_filter_data_inst(),
363
+ self._sample_rate_hz,
364
+ available_channels=self._available_channels,
365
+ verbose=self._verbose,
366
+ )
367
+
368
+ # convert memory usage in bytes
369
+ if isinstance(memory_limit, str):
370
+ memory_limit = parse_size(memory_limit)
371
+
372
+ # initialize output
373
+ output_df = None
374
+
375
+ # case only 1 node used for processing
376
+ if ncores == 1:
377
+ output_df = self._process(1,
378
+ self._id_list,
379
+ nevents,
380
+ lgc_save,
381
+ lgc_output,
382
+ dataframe_group,
383
+ output_group_path,
384
+ memory_limit)
385
+
386
+ else:
387
+
388
+ # split data
389
+ work_item_splits = self._split_work_items(ncores)
390
+
391
+ # for multi-core processing, we need to decrease the
392
+ # max memory so it fits in RAM
393
+ memory_limit /= ncores
394
+
395
+
396
+ # lauch pool processing
397
+ if self._verbose:
398
+ print(f'INFO: Processing with be split between {ncores} cores!')
399
+
400
+ node_nums = list(range(ncores+1))[1:]
401
+ pool = Pool(processes=ncores)
402
+ output_df_list = pool.starmap(self._process,
403
+ zip(node_nums,
404
+ work_item_splits,
405
+ repeat(nevents),
406
+ repeat(lgc_save),
407
+ repeat(lgc_output),
408
+ repeat(dataframe_group),
409
+ repeat(output_group_path),
410
+ repeat(memory_limit)))
411
+ pool.close()
412
+ pool.join()
413
+
414
+ # concatenate
415
+ output_df = pd.concat(output_df_list)
416
+
417
+ # processing done
418
+ if self._verbose:
419
+ print('INFO: Feature processing done!')
420
+
421
+
422
+ if lgc_output:
423
+ return output_df
424
+
425
+
426
+ def _process(self, node_num,
427
+ work_items, nevents,
428
+ lgc_save,
429
+ lgc_output,
430
+ dataframe_group,
431
+ output_group_path,
432
+ memory_limit):
433
+ """
434
+ Process data
435
+
436
+ Parameters
437
+ ---------
438
+
439
+ node_num : int
440
+ node id number, used for display
441
+
442
+ work_items : list of str
443
+ Raw stream IDs or trigger-dataframe task IDs assigned to this worker
444
+
445
+ nevents : int, optional
446
+ number of events to be processed
447
+ if not all events, requires ncores = 1
448
+ Default: all available (=-1)
449
+
450
+ lgc_save : bool, optional
451
+ if True, save dataframe in hdf5 files
452
+ (dataframe not returned)
453
+ if False, return dataframe (memory limit applies
454
+ so not all events may be processed)
455
+ Default: True
456
+
457
+ save_path : str, optional
458
+ base directory where output feature file will be saved
459
+ default: same base path as input data
460
+
461
+ ncores: int, optional
462
+ number of cores that will be used for processing
463
+ default: 1
464
+
465
+ memory_limit : float, optionl
466
+ memory safety limit when returning data in memory. Saved output
467
+ is one F#### shard per worker/task and is not split by this value.
468
+
469
+ """
470
+
471
+ # node string (for display)
472
+ node_num_str = str()
473
+ if node_num>-1:
474
+ node_num_str = ' Node #' + str(node_num)
475
+
476
+ # load salting dataframe
477
+ self._processing_data_inst.load_salting_dataframe()
478
+
479
+ # feature extractors
480
+ FE = FeatureExtractors
481
+ FE_ext = None
482
+ if self._external_file is not None:
483
+ FE_ext = self._load_external_extractors(self._external_file)
484
+
485
+ output_file = None
486
+ dataframe_metadata = {}
487
+ if lgc_save:
488
+ output_file = dataframe_group.file_path(node_num)
489
+ dataframe_metadata = dataframe_group.row_metadata(node_num)
490
+
491
+ event_counter = 0
492
+
493
+ # initialize data frame
494
+ feature_df = pd.DataFrame()
495
+ event_rows = []
496
+
497
+ # Loop raw streams or trigger-dataframe tasks assigned to this worker.
498
+ for item_index, work_item in enumerate(work_items):
499
+ final_stream = (item_index == len(work_items) - 1)
500
+
501
+ if self._verbose:
502
+ print('INFO' + node_num_str
503
+ + ': starting processing work item '
504
+ + work_item)
505
+
506
+
507
+ # set file list
508
+ self._processing_data_inst.set_stream(work_item)
509
+
510
+ # loop events
511
+ do_stop = False
512
+ start_time = time.time()
513
+ while (not do_stop):
514
+
515
+ # -----------------------
516
+ # Check number events
517
+ # and memory usage
518
+ # -----------------------
519
+
520
+ nevents_limit_reached = (nevents>0 and event_counter>=nevents)
521
+
522
+ # flag memory limit reached
523
+ memory_limit_reached = False
524
+ if event_rows:
525
+ memory_usage = sum(sys.getsizeof(row) for row in event_rows)
526
+ memory_limit_reached = (not lgc_save and memory_usage >= memory_limit)
527
+
528
+ # display
529
+ if self._verbose:
530
+
531
+ if (event_counter%100==0 and event_counter!=0):
532
+ print('INFO' + node_num_str
533
+ + ': Local number of events = '
534
+ + str(event_counter)
535
+ + ' (memory = ' + str(memory_usage/1e6) + ' MB)')
536
+
537
+ time_running = time.time() - start_time
538
+ speed = event_counter/time_running
539
+ print('INFO' + node_num_str
540
+ + ': Speed = '
541
+ + f'{speed:.4f}' + ' events per second')
542
+
543
+ # -----------------------
544
+ # Read next event
545
+ # -----------------------
546
+
547
+ success = self._processing_data_inst.read_next_event(
548
+ channels=self._selected_channels,
549
+ traces_config=self._traces_config
550
+ )
551
+
552
+ # end of file
553
+ if not success:
554
+ do_stop = True
555
+
556
+ # -----------------------
557
+ # Handle stop or
558
+ # memory limit
559
+ # -----------------------
560
+
561
+ # now let's handle case we need to stop
562
+ # memory/nb events limit reached
563
+
564
+ if ((do_stop and (final_stream or not lgc_save))
565
+ or nevents_limit_reached
566
+ or memory_limit_reached):
567
+
568
+ # convert to pandas
569
+ feature_df = pd.DataFrame(event_rows)
570
+ event_rows = []
571
+
572
+ # save one final F#### shard for this processing task
573
+ if lgc_save:
574
+ feature_df = feature_df.reset_index(drop=True)
575
+ vx.settings.main.thread_count = 1
576
+ vx.settings.main.thread_count_io = 1
577
+ pa.set_cpu_count(1)
578
+ feature_vx = vx.from_pandas(feature_df, copy_index=False)
579
+ feature_vx.export_hdf5(output_file, mode='w')
580
+ feature_vx.close()
581
+ del feature_df, feature_vx
582
+ gc.collect()
583
+ feature_df = pd.DataFrame()
584
+
585
+ # case maximum number of events reached
586
+ # -> processing done!
587
+ if nevents_limit_reached:
588
+ if self._verbose:
589
+ print('INFO' + node_num_str
590
+ + ': Requested nb events reached. '
591
+ + 'Stopping processing!')
592
+
593
+ return feature_df
594
+
595
+ # case memory limit reached and not saving file
596
+ # -> processing done
597
+ # case memory limit reached
598
+ # -> processing needs to stop!
599
+ if lgc_output and memory_limit_reached:
600
+ raise ValueError(
601
+ 'ERROR: memory limit reached! '
602
+ + 'Change memory limit or only save hdf5 files '
603
+ +'(lgc_save=True AND lgc_output=False) '
604
+ )
605
+
606
+ # Now let's stop or increment event counter....
607
+ if do_stop:
608
+ break
609
+ else:
610
+ event_counter += 1
611
+
612
+ # -----------------------
613
+ # Features calculation
614
+ # -----------------------
615
+
616
+ # initialize event features dictionary
617
+ event_features = dict()
618
+
619
+ # Per-event trace/FFT cache. Static OF resources remain intact
620
+ # across events; QETpy signal state is populated only on demand.
621
+ event_cache = EventTraceCache(
622
+ self._processing_data_inst,
623
+ self._sample_rate_hz,
624
+ weights=self._weights,
625
+ )
626
+ self._feature_resources.begin_event()
627
+
628
+ # Preserve input trigger-product provenance before assigning
629
+ # this feature product's own dataframe_group_* identity.
630
+ admin_data = self._processing_data_inst.get_event_admin()
631
+ if 'dataframe_group_id' in admin_data:
632
+ for key in ('name', 'id', 'number'):
633
+ source = f'dataframe_group_{key}'
634
+ if source in admin_data:
635
+ event_features[f'trigger_dataframe_group_{key}'] = admin_data[source]
636
+ if 'processing_label' in admin_data:
637
+ event_features['trigger_processing_label'] = admin_data['processing_label']
638
+ if 'dataframe_file_index' in admin_data:
639
+ event_features['trigger_dataframe_file_index'] = admin_data['dataframe_file_index']
640
+ event_features.update(admin_data)
641
+ event_features.update(dataframe_metadata)
642
+
643
+ # Detector settings
644
+ for channel in self._processing_config.keys():
645
+ event_features.update(
646
+ self._processing_data_inst.get_channel_settings(channel)
647
+ )
648
+
649
+
650
+ # Pulse features: execute the precompiled feature plan.
651
+ for plan in self._feature_plan.algorithms:
652
+ if plan.base_algorithm in self._algorithm_list:
653
+ extractor = getattr(FE, plan.base_algorithm)
654
+ elif plan.base_algorithm in self._ext_algorithm_list:
655
+ extractor = getattr(FE_ext, plan.base_algorithm)
656
+ else:
657
+ raise ValueError(
658
+ f'ERROR: Cannot find algorithm "{plan.base_algorithm}" anywhere. '
659
+ 'Check feature extractor exists!'
660
+ )
661
+
662
+ kwargs = self._build_algorithm_kwargs(plan)
663
+
664
+ if plan.resource_kind == 'optimal_filter':
665
+ resource = self._feature_resources.get_of_base(
666
+ plan, event_cache
667
+ )
668
+ extracted_features = extractor(
669
+ plan.channel, resource, **kwargs
670
+ )
671
+ elif plan.resource_kind == 'fft':
672
+ resource = event_cache.get_fft_data(
673
+ plan.channel, plan.trace_spec
674
+ )
675
+ extracted_features = extractor(
676
+ plan.channel, resource, **kwargs
677
+ )
678
+ else:
679
+ trace = event_cache.get_trace(
680
+ plan.channel, plan.trace_spec
681
+ )
682
+ extracted_features = extractor(trace, **kwargs)
683
+
684
+ for feature_base_name, value in extracted_features.items():
685
+ feature_name = f'{feature_base_name}_{plan.feature_channel}'
686
+ event_features[feature_name] = value
687
+
688
+ # done processing event!
689
+ # append event dictionary to list
690
+ event_rows.append(event_features)
691
+
692
+ # return features
693
+ print(f'INFO {node_num_str}: Processing done. Returning dataframe!')
694
+ return feature_df
695
+
696
+
697
+ def _get_file_list(self, dataframe_path):
698
+ """Return trigger dataframe shards as independent feature tasks.
699
+
700
+ Parameters
701
+ ----------
702
+ dataframe_path : str or list of str
703
+ A dataframe-group directory, one HDF5 shard, or a list of HDF5
704
+ shards. A directory cannot be mixed with explicit files.
705
+
706
+ Returns
707
+ -------
708
+ task_dict : dict
709
+ Mapping of task IDs to one input dataframe shard each.
710
+ base_path : str
711
+ Parent directory containing the dataframe group.
712
+ group_name : str
713
+ Input dataframe-group directory name.
714
+ """
715
+ paths = [dataframe_path] if isinstance(dataframe_path, str) else list(dataframe_path)
716
+
717
+ file_list = []
718
+ base_path = None
719
+ group_name = None
720
+
721
+ for item in paths:
722
+ path = Path(item)
723
+ if path.is_dir():
724
+ if len(paths) != 1:
725
+ raise ValueError(
726
+ "Only one dataframe-group directory may be supplied; "
727
+ "do not mix a directory with explicit files."
728
+ )
729
+ base_path = str(path.parent)
730
+ group_name = path.name
731
+ file_list = [str(file_path) for file_path in path.glob("*.hdf5")]
732
+ elif path.is_file():
733
+ if path.suffix != ".hdf5":
734
+ continue
735
+ if base_path is None:
736
+ base_path = str(path.parent.parent)
737
+ group_name = path.parent.name
738
+ file_list.append(str(path))
739
+ else:
740
+ raise ValueError(f'File or directory "{item}" does not exist!')
741
+
742
+ file_list = sorted(set(file_list))
743
+ if not file_list:
744
+ raise ValueError(
745
+ "ERROR: No input Vaex HDF5 dataframe files were found. "
746
+ "Check the trigger dataframe path."
747
+ )
748
+
749
+ task_dict = {
750
+ f"task_{file_index:06d}": [file_name]
751
+ for file_index, file_name in enumerate(file_list, start=1)
752
+ }
753
+
754
+ if self._verbose:
755
+ print(
756
+ f"INFO: Found {len(file_list)} dataframe file(s); each file "
757
+ "is available as an independent feature task."
758
+ )
759
+
760
+ return task_dict, base_path, group_name
761
+
762
+
763
+ def _load_external_extractors(self, external_file):
764
+ """
765
+ Helper function for loading an alternative SingleChannelExtractors
766
+ class.
767
+
768
+ Parameters
769
+ ----------
770
+
771
+ external_file : str
772
+ external feature file name
773
+
774
+ Return
775
+ ------
776
+
777
+ module : object
778
+ FeatureExtractors module
779
+
780
+ """
781
+
782
+ module_name = 'pytesprocess.process'
783
+
784
+ spec = importlib.util.spec_from_file_location(module_name,
785
+ external_file)
786
+ module = importlib.util.module_from_spec(spec)
787
+ sys.modules[module_name] = module
788
+ spec.loader.exec_module(module)
789
+
790
+ return module.FeatureExtractors
791
+
792
+
793
+
794
+ def _create_output_directory(self, base_path, facility,
795
+ restricted=False,
796
+ data_type='background'):
797
+ """Compatibility wrapper around the common dataframe-group helper."""
798
+ group = create_dataframe_group(
799
+ base_path, dataframe_type='feature', facility=facility,
800
+ processing_label=self._processing_label, restricted=restricted,
801
+ data_type=data_type,
802
+ )
803
+ return group.path, group.group_number
804
+
805
+
806
+ def _build_algorithm_kwargs(self, plan):
807
+ """Return runtime extractor kwargs from a compiled feature plan."""
808
+ kwargs = {
809
+ key: value for key, value in plan.params.items()
810
+ if key != 'run'
811
+ }
812
+ kwargs['fs'] = self._sample_rate_hz
813
+
814
+ if plan.trace_spec is None:
815
+ nb_samples = self._processing_data_inst.get_nb_samples()
816
+ nb_pretrigger_samples = (
817
+ self._processing_data_inst.get_nb_pretrigger_samples()
818
+ )
819
+ else:
820
+ nb_samples = plan.trace_spec.nb_samples
821
+ nb_pretrigger_samples = plan.trace_spec.nb_pretrigger_samples
822
+
823
+ kwargs['nb_samples'] = nb_samples
824
+ kwargs['nb_pretrigger_samples'] = nb_pretrigger_samples
825
+
826
+ window_min, window_max = self._get_window_indices(**kwargs)
827
+ kwargs['window_min_index'] = window_min
828
+ kwargs['window_max_index'] = window_max
829
+ kwargs['feature_base_name'] = plan.name
830
+ return kwargs
831
+
832
+
833
+ def _extract_algorithm_list(self):
834
+ """
835
+ Extract list for algorithms, check for duplicates
836
+
837
+ Parameters
838
+ ----------
839
+ None
840
+
841
+
842
+ Return
843
+ ------
844
+ algorithm_list : list
845
+ list of available algorithm from _features.py FeatureExtractors class
846
+
847
+ ext_algorithm_list : list
848
+ list of availble algorithms from external files
849
+
850
+ """
851
+
852
+ algorithm_list = list()
853
+ ext_algorithm_list = list()
854
+
855
+
856
+ # internal feature extractor
857
+ FE = FeatureExtractors
858
+ for attribute in dir(FE):
859
+ if attribute[0] == '_':
860
+ continue
861
+ algorithm_list.append(attribute)
862
+
863
+ # external feature extractor
864
+ if self._external_file is not None:
865
+ FE_ext = self._load_external_extractors(self._external_file)
866
+ for attribute in dir(FE_ext):
867
+ if attribute[0] == '_':
868
+ continue
869
+ # check for duplicate
870
+ if attribute in algorithm_list:
871
+ raise ValueError('External algorithm ' + attribute
872
+ + ' is a duplicate from internal feature extractor!'
873
+ + ' This is nto allowed. You need to change name...')
874
+
875
+ else:
876
+ ext_algorithm_list.append(attribute)
877
+
878
+
879
+ return algorithm_list, ext_algorithm_list
880
+
881
+
882
+ def _split_work_items(self, ncores):
883
+ """
884
+ Split feature work items between workers
885
+
886
+
887
+ Parameters
888
+ ----------
889
+
890
+ ncores : int
891
+ number of cores
892
+
893
+ Return
894
+ ------
895
+
896
+ output_list : list
897
+ list of dictionaries (length=ncores) containing
898
+ data
899
+
900
+
901
+ """
902
+
903
+
904
+ output_list = list()
905
+
906
+ # Split independent raw-stream/dataframe tasks.
907
+ work_item_split = np.array_split(self._id_list, ncores)
908
+
909
+ # remove empty array
910
+ for work_item_sublist in work_item_split:
911
+ if work_item_sublist.size == 0:
912
+ continue
913
+ output_list.append(list(work_item_sublist))
914
+
915
+ return output_list
916
+
917
+
918
+ def _get_window_indices(self, nb_samples,
919
+ nb_pretrigger_samples, fs,
920
+ window_min_from_start_usec=None,
921
+ window_min_to_end_usec=None,
922
+ window_min_from_trig_usec=None,
923
+ window_max_from_start_usec=None,
924
+ window_max_to_end_usec=None,
925
+ window_max_from_trig_usec=None,
926
+ **kwargs):
927
+ """
928
+ Calculate window index min and max from various types
929
+ of window definition
930
+
931
+ Parameters
932
+ ---------
933
+
934
+ nb_samples : int
935
+ total number of samples
936
+
937
+ nb_pretrigger_samples : int
938
+ number of pretrigger samples
939
+
940
+ window_min_from_start_usec : float, optional
941
+ OF filter window start in micro seconds defined
942
+ from beginning of trace
943
+
944
+ window_min_to_end_usec : float, optional
945
+ OF filter window start in micro seconds defined
946
+ as length to end of trace
947
+
948
+ window_min_from_trig_usec : float, optional
949
+ OF filter window start in micro seconds from
950
+ pre-trigger (can be negative if prior pre-trigger)
951
+
952
+
953
+ window_max_from_start_usec : float, optional
954
+ OF filter window max in micro seconds defined
955
+ from beginning of trace
956
+
957
+ window_max_to_end_usec : float, optional
958
+ OF filter window max in micro seconds defined
959
+ as length to end of trace
960
+
961
+
962
+ window_max_from_trig_usec : float, optional
963
+ OF filter window end in micro seconds from
964
+ pre-trigger (can be negative if prior pre-trigger)
965
+
966
+ Return:
967
+ ------
968
+
969
+ min_index : int
970
+ trace index window min
971
+
972
+ max_index : int
973
+ trace index window max
974
+ """
975
+
976
+
977
+ # min window
978
+ min_index = 0
979
+ if window_min_from_start_usec is not None:
980
+ min_index = int(window_min_from_start_usec*fs*1e-6)
981
+ elif window_min_to_end_usec is not None:
982
+ min_index = (nb_samples
983
+ - abs(int(window_min_to_end_usec*fs*1e-6))
984
+ - 1)
985
+ elif window_min_from_trig_usec is not None:
986
+ min_index = (nb_pretrigger_samples
987
+ + int(window_min_from_trig_usec*fs*1e-6))
988
+
989
+ # check
990
+ if min_index<0:
991
+ min_index=0
992
+ elif min_index>nb_samples-1:
993
+ min_index=nb_samples-1
994
+
995
+ # max index
996
+ max_index = nb_samples -1
997
+ if window_max_from_start_usec is not None:
998
+ max_index = int(window_max_from_start_usec*fs*1e-6)
999
+ elif window_max_to_end_usec is not None:
1000
+ max_index = (nb_samples
1001
+ - abs(int(window_max_to_end_usec*fs*1e-6))
1002
+ - 1)
1003
+ elif window_max_from_trig_usec is not None:
1004
+ max_index = (nb_pretrigger_samples
1005
+ + int(window_max_from_trig_usec*fs*1e-6))
1006
+
1007
+ # check
1008
+ if max_index<0:
1009
+ max_index=0
1010
+ elif max_index>nb_samples-1:
1011
+ max_index=nb_samples-1
1012
+
1013
+
1014
+ if max_index<min_index:
1015
+ raise ValueError('ERROR window calculation: '
1016
+ + 'max index smaller than min!'
1017
+ + 'Check configuration!')
1018
+
1019
+ return min_index, max_index
1020
+
1021
+
1022
+
1023
+
1024
+