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,967 @@
1
+ import numpy as np
2
+ import vaex as vx
3
+ from pytesprocess.utils import utils
4
+ from pytesprocess.core import FilterData
5
+ from pytesprocess.salting import SaltInjector
6
+ import copy
7
+ import pyarrow as pa
8
+ from pytesdaqx.io import StreamReader
9
+ from pytesprocess.utils import extract_stream_id
10
+ from .data_source import RawDataSource, TriggerDataframeSource
11
+ from .event_context import EventContext
12
+ vx.settings.main.thread_count = 1
13
+ vx.settings.main.thread_count_io = 1
14
+ pa.set_cpu_count(1)
15
+
16
+ __all__ = [
17
+ 'ProcessingData'
18
+ ]
19
+
20
+ class ProcessingData:
21
+ """
22
+ Class to manage data used for processing,
23
+ in particular
24
+ - raw data files, event traces/windows and metadata
25
+ - lightweight filter-template access needed by trigger processing
26
+
27
+ """
28
+
29
+ def __init__(self, raw_path, raw_files,
30
+ acquisition_name=None,
31
+ trigger_files=None,
32
+ trigger_group_name=None,
33
+ filter_file=None,
34
+ available_channels=None,
35
+ salting_dataframe=None,
36
+ verbose=True,
37
+ catalog=None):
38
+ """
39
+ Intialize data processing
40
+
41
+ Parameters
42
+ -----------
43
+
44
+ input_files : list
45
+ list of raw data files to be processed
46
+
47
+ acquisition_name : str, optional
48
+ raw data group name
49
+ default: None
50
+
51
+ filter_file : str, optional
52
+ full path to filter file
53
+ default: None
54
+
55
+ verbose : bool, optional
56
+ if True, display info
57
+ Default: True
58
+
59
+ """
60
+
61
+ # verbose
62
+ self._verbose = verbose
63
+
64
+ # input raw files/path
65
+ self._raw_path = raw_path
66
+ self._raw_files = raw_files
67
+ self._acquisition_name = acquisition_name
68
+ self._catalog = catalog
69
+
70
+ # trigger dataframe
71
+ self._trigger_files = trigger_files
72
+ self._trigger_group_name = trigger_group_name
73
+
74
+ # filter data
75
+ self._filter_data = None
76
+ self._filter_file = filter_file
77
+ if filter_file is not None:
78
+ self._filter_data = FilterData()
79
+ self._filter_data.load_hdf5(filter_file, overwrite=True)
80
+
81
+ # trigger-dataframe mode
82
+ self._is_trigger_dataframe = False
83
+ if self._trigger_files is not None:
84
+ self._is_trigger_dataframe = True
85
+
86
+
87
+ # Data sources and current event/window state. ProcessingData remains
88
+ # the public facade while source iteration and per-event state live in
89
+ # dedicated objects.
90
+ self._raw_source = RawDataSource(self._raw_files, catalog=self._catalog)
91
+ self._trigger_source = (
92
+ TriggerDataframeSource(self._trigger_files)
93
+ if self._is_trigger_dataframe else None
94
+ )
95
+ self._event = EventContext()
96
+
97
+ # get ADC and file info
98
+ self._data_info = self._extract_data_info()
99
+
100
+ # available channels
101
+ self._available_channels = available_channels
102
+
103
+ # salting
104
+ self._salting_dataframe = salting_dataframe
105
+ self._salting_inst = None
106
+ if salting_dataframe is not None:
107
+ if self._filter_data is None:
108
+ raise ValueError(
109
+ 'ERROR: A filter_file is required when salting injection is enabled.'
110
+ )
111
+ self._salting_inst = SaltInjector(
112
+ self._filter_data, verbose=verbose
113
+ )
114
+
115
+
116
+ @property
117
+ def verbose(self):
118
+ return self._verbose
119
+
120
+ def get_filter_data_inst(self):
121
+ return self._filter_data
122
+
123
+ def load_salting_dataframe(self):
124
+ """
125
+ Load salting dataframe
126
+ """
127
+
128
+ if self._salting_dataframe is None:
129
+ return
130
+
131
+ # disable vaex multi-threading
132
+ vx.settings.main.thread_count = 1
133
+ vx.settings.main.thread_count_io = 1
134
+ pa.set_cpu_count(1)
135
+
136
+ # SaltInjector accepts either a Vaex dataframe or a path to one.
137
+ self._salting_inst.set_dataframe(self._salting_dataframe)
138
+
139
+ def configure_trigger_partitions(self, partition_target_duration_s=10.0,
140
+ overlap_left_samples=0,
141
+ overlap_right_samples=0):
142
+ """Configure logical partitions used for continuous Zarr triggering."""
143
+ self._raw_source.configure_partitions(
144
+ partition_target_duration_s=partition_target_duration_s,
145
+ overlap_left_samples=overlap_left_samples,
146
+ overlap_right_samples=overlap_right_samples,
147
+ )
148
+
149
+ def get_partition_context(self):
150
+ """Return current logical-partition coordinates, or ``None``."""
151
+ return self._raw_source.partition_context()
152
+
153
+ def set_stream(self, stream):
154
+ """Select an input trigger-dataframe task or raw-data stream."""
155
+ vx.settings.main.thread_count_io = 1
156
+ self._event.reset()
157
+
158
+ if self._is_trigger_dataframe:
159
+ task_id = str(stream)
160
+ if self._verbose:
161
+ print('INFO: Loading dataframe trigger task ' + task_id)
162
+ self._trigger_source.select_task(task_id)
163
+ self._raw_source.close()
164
+ return
165
+
166
+ self._select_raw_stream(stream, force=True)
167
+
168
+ def _select_raw_stream(self, stream, force=False):
169
+ """Select one canonical raw stream."""
170
+ self._raw_source.select_stream(stream, force=force)
171
+ self._event.stream_id = self._raw_source.stream_id
172
+
173
+ def get_raw_path(self):
174
+ return self._raw_path
175
+
176
+
177
+ def read_next_event(self, channels=None, traces_config=None):
178
+ """Read the next native record or dataframe-selected event."""
179
+ # Keep the established per-worker Vaex/PyArrow limits. These settings
180
+ # are intentionally unchanged in v14 because they avoid known
181
+ # multicore oversubscription/file-reading problems.
182
+ vx.settings.main.thread_count = 1
183
+ vx.settings.main.thread_count_io = 1
184
+ pa.set_cpu_count(1)
185
+
186
+ if not self._is_trigger_dataframe:
187
+ if self._raw_source.uses_continuous_partitions:
188
+ return self._read_next_continuous_partition(channels)
189
+
190
+ try:
191
+ traces, info = self._raw_source.read_next_native(channels)
192
+ except StopIteration:
193
+ self._event.full_traces = None
194
+ return False
195
+
196
+ self._event.reset(stream_id=self._raw_source.stream_id)
197
+ self._event.full_traces = traces
198
+ self._event.admin_info = self._normalize_reader_info(info, channels)
199
+ self._event.stream_number = int(
200
+ self._event.admin_info.get('stream_num', -1)
201
+ )
202
+ global_segment = self._event.admin_info.get('global_segment_num')
203
+ if global_segment is not None and int(global_segment) >= 0:
204
+ self._event.global_segment_number = int(global_segment)
205
+
206
+ if self._salting_inst is not None:
207
+ if self._event.global_segment_number is None:
208
+ raise NotImplementedError(
209
+ 'Salting for native Zarr records is not implemented yet.'
210
+ )
211
+ chans = self._event.admin_info['detector_chans']
212
+ self._event.full_traces, self._event.salting_info = (
213
+ self._salting_inst.inject_record(
214
+ chans, self._event.full_traces,
215
+ stream_id=self._event.stream_id,
216
+ stream_number=self._event.stream_number,
217
+ global_segment_number=self._event.global_segment_number,
218
+ include_metadata=True,
219
+ )
220
+ )
221
+ return True
222
+
223
+ if traces_config is None:
224
+ raise ValueError(
225
+ 'ERROR: No trace info available. Something went wrong...'
226
+ )
227
+
228
+ row = self._trigger_source.next_row()
229
+ if row is None:
230
+ self._event.full_traces = None
231
+ return False
232
+
233
+ self._event.reset()
234
+ self._event.dataframe_info = row
235
+
236
+ def row_get(*names, default=None):
237
+ for name in names:
238
+ try:
239
+ if name in row and row[name] is not None:
240
+ return row[name]
241
+ except TypeError:
242
+ pass
243
+ return default
244
+
245
+ raw_stream = row_get('stream_id', 'stream_number')
246
+ if raw_stream is None:
247
+ raise ValueError(
248
+ 'ERROR: Trigger dataframe does not identify the raw stream.'
249
+ )
250
+ self._select_raw_stream(raw_stream)
251
+ self._event.stream_id = self._raw_source.stream_id
252
+
253
+ stream_trigger_index = row_get('stream_trigger_index')
254
+ global_segment_number = row_get(
255
+ 'global_segment_num', 'global_segment_number'
256
+ )
257
+ segment_trigger_index = row_get('segment_trigger_index')
258
+
259
+ if stream_trigger_index is None and global_segment_number is None:
260
+ raise ValueError(
261
+ 'ERROR: Trigger dataframe needs stream_trigger_index (Zarr) or '
262
+ 'global_segment_number (HDF5).'
263
+ )
264
+
265
+ self._event.truncated_traces = {}
266
+
267
+ # HDF5 salts are injected into the full native segment;
268
+ # stream-global salts are injected directly into each window.
269
+ if self._salting_inst is not None and stream_trigger_index is None:
270
+ full_record = {'global_segment_num': int(global_segment_number)}
271
+ traces, info = self._raw_source.read_record(
272
+ full_record, channels=channels
273
+ )
274
+ self._event.full_traces = traces
275
+ self._event.admin_info = self._normalize_reader_info(info, channels)
276
+ self._event.global_segment_number = int(global_segment_number)
277
+ self._event.stream_number = int(
278
+ row_get('stream_number', default=-1)
279
+ )
280
+ self._event.segment_trigger_index = int(segment_trigger_index)
281
+ chans = self._event.admin_info['detector_chans']
282
+ self._event.full_traces, self._event.salting_info = (
283
+ self._salting_inst.inject_record(
284
+ chans, self._event.full_traces,
285
+ stream_id=self._event.stream_id,
286
+ stream_number=self._event.stream_number,
287
+ global_segment_number=self._event.global_segment_number,
288
+ include_metadata=True,
289
+ )
290
+ )
291
+ for key_tuple in traces_config:
292
+ nb_samples = int(key_tuple[0])
293
+ nb_pretrigger_samples = int(key_tuple[1])
294
+ min_idx = self._event.segment_trigger_index - nb_pretrigger_samples
295
+ max_idx = min_idx + nb_samples
296
+ window = None
297
+ if (min_idx >= 0
298
+ and max_idx <= self._event.full_traces.shape[-1]):
299
+ window = self._event.full_traces[:, min_idx:max_idx].copy()
300
+ self._event.truncated_traces[key_tuple] = {
301
+ 'traces': window, 'channels': chans
302
+ }
303
+ else:
304
+ for key_tuple in traces_config:
305
+ nb_samples = int(key_tuple[0])
306
+ nb_pretrigger_samples = int(key_tuple[1])
307
+ record = {}
308
+ if stream_trigger_index is not None:
309
+ record['stream_trigger_index'] = int(stream_trigger_index)
310
+ else:
311
+ record['global_segment_num'] = int(global_segment_number)
312
+ record['segment_trigger_index'] = int(segment_trigger_index)
313
+
314
+ try:
315
+ traces, info = self._raw_source.read_record(
316
+ record,
317
+ channels=channels,
318
+ trace_length_samples=nb_samples,
319
+ pretrigger_length_samples=nb_pretrigger_samples,
320
+ )
321
+ except (ValueError, IndexError):
322
+ traces, info = None, None
323
+
324
+ if info is not None:
325
+ info = self._normalize_reader_info(info, channels)
326
+ self._event.admin_info = info
327
+ trace_channels = info['detector_chans']
328
+ else:
329
+ trace_channels = list(
330
+ channels or self._available_channels or []
331
+ )
332
+
333
+ if (self._salting_inst is not None
334
+ and traces is not None
335
+ and stream_trigger_index is not None):
336
+ trace_start_index = (
337
+ int(stream_trigger_index) - nb_pretrigger_samples
338
+ )
339
+ traces, salting_info = self._salting_inst.inject_window(
340
+ trace_channels, traces,
341
+ stream_id=self._event.stream_id,
342
+ trace_start_index=trace_start_index,
343
+ include_metadata=True,
344
+ )
345
+ if salting_info:
346
+ self._event.salting_info = salting_info
347
+
348
+ self._event.truncated_traces[key_tuple] = {
349
+ 'traces': traces, 'channels': trace_channels
350
+ }
351
+
352
+ if self._event.admin_info is None:
353
+ self._event.admin_info = {'detector_config': {}}
354
+ if global_segment_number is not None:
355
+ self._event.global_segment_number = int(global_segment_number)
356
+ self._event.stream_number = int(
357
+ row_get('stream_number', default=-1)
358
+ )
359
+ if stream_trigger_index is not None:
360
+ self._event.stream_trigger_index = int(stream_trigger_index)
361
+ elif segment_trigger_index is not None:
362
+ self._event.segment_trigger_index = int(segment_trigger_index)
363
+ return True
364
+
365
+ def _read_next_continuous_partition(self, channels=None):
366
+ """Read one expanded logical partition from a continuous Zarr stream."""
367
+ result = self._raw_source.read_next_partition(
368
+ channels=channels, sample_rate_hz=self.get_sample_rate()
369
+ )
370
+ if result is None:
371
+ self._event.full_traces = None
372
+ return False
373
+
374
+ traces, info, read_start = result
375
+ self._event.reset(stream_id=self._raw_source.stream_id)
376
+ self._event.full_traces = traces
377
+ self._event.admin_info = self._normalize_reader_info(info, channels)
378
+ self._event.stream_number = int(
379
+ self._event.admin_info.get('stream_num', -1)
380
+ )
381
+
382
+ stream_start = self._event.admin_info.get('stream_start')
383
+ if stream_start is not None:
384
+ self._event.admin_info['event_time'] = (
385
+ float(stream_start) + read_start / float(self.get_sample_rate())
386
+ )
387
+
388
+ if self._salting_inst is not None:
389
+ trace_channels = self._event.admin_info['detector_chans']
390
+ self._event.full_traces, self._event.salting_info = (
391
+ self._salting_inst.inject_window(
392
+ trace_channels, self._event.full_traces,
393
+ stream_id=self._event.stream_id,
394
+ trace_start_index=read_start,
395
+ include_metadata=True,
396
+ )
397
+ )
398
+ return True
399
+
400
+ def _normalize_reader_info(self, info, channels=None):
401
+ """Normalize reader metadata to canonical stream/acquisition names."""
402
+ out = dict(info or {})
403
+ detector_chans = out.get('detector_channels', out.get('detector_chans'))
404
+ if detector_chans is None:
405
+ detector_chans = list(channels or self._available_channels or [])
406
+ elif isinstance(detector_chans, str):
407
+ detector_chans = [detector_chans]
408
+ else:
409
+ detector_chans = list(detector_chans)
410
+ out['detector_channels'] = detector_chans
411
+ # Internal compatibility for existing trace/channel accessors. This is
412
+ # not written as a dataframe provenance field.
413
+ out['detector_chans'] = detector_chans
414
+
415
+ out.setdefault('stream_num', -1)
416
+ out.setdefault('stream_id', self._raw_source.stream_id)
417
+ out.setdefault('global_segment_num', -1)
418
+ out.setdefault('event_id', -1)
419
+ out.setdefault('event_time', 0)
420
+
421
+ measurement_type = out.get(
422
+ 'measurement_type', out.get('run_type', out.get('data_type', 'unknown'))
423
+ )
424
+ out['measurement_type'] = measurement_type
425
+ out.pop('run_type', None)
426
+ out.setdefault('fridge_run', -1)
427
+ out['fridge_run_start'] = out.get(
428
+ 'fridge_run_start', out.get('fridge_run_start_time', 0)
429
+ )
430
+ out.setdefault('stream_start', 0)
431
+ out.setdefault('acquisition_start', 0)
432
+
433
+ if 'detector_config' not in out:
434
+ try:
435
+ detector_config = copy.deepcopy(
436
+ self._raw_source.get_detector_settings()
437
+ )
438
+ except Exception:
439
+ detector_config = {}
440
+ for config in detector_config.values():
441
+ if ('tes_bias' not in config
442
+ and config.get('tes_bias_dc_amps') is not None):
443
+ config['tes_bias'] = config['tes_bias_dc_amps']
444
+ out['detector_config'] = detector_config
445
+ return out
446
+
447
+ @staticmethod
448
+ def _get_pytesprocess_trigger_type(info):
449
+ """Return the legacy numeric pytesprocess trigger category.
450
+
451
+ Current pytesdaqx uses ``trigger_type`` for the DAQ trigger
452
+ configuration and stores string values such as ``"none"`` or
453
+ ``"digital_edge"``. pytesprocess historically stores a different
454
+ quantity in its output dataframe:
455
+
456
+ 1 = continuous/background
457
+ 2 = external trigger
458
+ 3 = random
459
+ 4 = threshold/software trigger
460
+
461
+ Numeric values from legacy raw files are preserved. Otherwise the
462
+ value is derived from the current ``measurement_type``.
463
+ """
464
+
465
+ if not info:
466
+ return np.nan
467
+
468
+ raw_trigger_type = info.get('trigger_type')
469
+
470
+ # Legacy pytesdaq/pytesdaqx files may already contain the numeric
471
+ # pytesprocess category. Preserve it when unambiguous.
472
+ if isinstance(raw_trigger_type, (int, np.integer)):
473
+ return np.int16(raw_trigger_type)
474
+ if isinstance(raw_trigger_type, (float, np.floating)):
475
+ if np.isfinite(raw_trigger_type):
476
+ return np.int16(raw_trigger_type)
477
+ if isinstance(raw_trigger_type, str):
478
+ value = raw_trigger_type.strip()
479
+ if value.isdigit():
480
+ return np.int16(int(value))
481
+
482
+ measurement_type = str(
483
+ info.get('measurement_type', info.get('run_type', ''))
484
+ ).strip().lower()
485
+
486
+ measurement_map = {
487
+ # Current canonical pytesdaqx names
488
+ 'background': 1,
489
+ 'external_trigger': 2,
490
+ 'threshold': 4,
491
+
492
+ # Legacy aliases that may still occur in old metadata
493
+ 'cont': 1,
494
+ 'continuous': 1,
495
+ 'open_cont': 1,
496
+ 'restricted_cont': 1,
497
+ 'exttrig': 2,
498
+ 'thresh': 4,
499
+ }
500
+ if measurement_type in measurement_map:
501
+ return np.int16(measurement_map[measurement_type])
502
+
503
+ # Legacy files may provide data_mode even when measurement_type is
504
+ # absent or too generic. Do not synthesize data_mode for new data.
505
+ data_mode = str(info.get('data_mode', '')).strip().lower()
506
+ data_mode_map = {
507
+ 'cont': 1,
508
+ 'trig-ext': 2,
509
+ 'rand': 3,
510
+ 'threshold': 4,
511
+ }
512
+ if data_mode in data_mode_map:
513
+ return np.int16(data_mode_map[data_mode])
514
+
515
+ # A continuous ADC acquisition is safely equivalent to the legacy
516
+ # continuous category even for measurement types such as calibration.
517
+ adc_mode = str(info.get('adc_mode', '')).strip().lower()
518
+ if adc_mode == 'continuous':
519
+ return np.int16(1)
520
+
521
+ return np.nan
522
+
523
+ def get_event_admin(self, return_all=False):
524
+ """Return canonical metadata for the current event/window."""
525
+ info = self._event.admin_info
526
+ if info is None:
527
+ return {}
528
+
529
+ if self._is_trigger_dataframe:
530
+ row = self._event.dataframe_info
531
+ if row is None:
532
+ return {}
533
+ admin = {}
534
+ for key, val in row.items():
535
+ admin[key] = np.nan if val is None else val
536
+ return admin
537
+
538
+ if return_all:
539
+ return info
540
+
541
+ admin = {}
542
+ global_segment = info.get('global_segment_num')
543
+ if global_segment is not None and int(global_segment) >= 0:
544
+ admin['global_segment_number'] = np.int64(global_segment)
545
+
546
+ admin['stream_number'] = np.int64(info.get('stream_num', -1))
547
+ if info.get('stream_id') is not None:
548
+ admin['stream_id'] = str(info['stream_id'])
549
+ if info.get('event_id') is not None:
550
+ admin['event_id'] = np.int32(info['event_id'])
551
+ if info.get('event_time') is not None:
552
+ admin['event_time'] = np.float64(info['event_time'])
553
+ admin['measurement_type'] = str(
554
+ info.get('measurement_type', 'unknown')
555
+ )
556
+
557
+ if self._acquisition_name is not None:
558
+ admin['acquisition_name'] = self._acquisition_name
559
+ else:
560
+ admin['acquisition_name'] = np.nan
561
+
562
+ admin['trigger_type'] = self._get_pytesprocess_trigger_type(info)
563
+ admin['trigger_amplitude'] = info.get('trigger_amplitude', np.nan)
564
+ admin['trigger_time'] = info.get('trigger_time', np.nan)
565
+
566
+ if info.get('fridge_run') is not None:
567
+ admin['fridge_run_number'] = np.int64(info['fridge_run'])
568
+
569
+ # Absolute start timestamps. EventBuilder derives per-trigger elapsed
570
+ # times from these when needed.
571
+ for source, dest in (
572
+ ('fridge_run_start', 'fridge_run_start_time'),
573
+ ('stream_start', 'stream_start_time'),
574
+ ('acquisition_start', 'acquisition_start_time'),
575
+ ):
576
+ value = info.get(source)
577
+ if value is not None:
578
+ admin[dest] = np.float64(value)
579
+
580
+ if self._event.salting_info is not None:
581
+ if 'salting_type' in self._event.salting_info:
582
+ admin['salting_type'] = self._event.salting_info['salting_type']
583
+ for key in (
584
+ 'salting_dataframe_group_name',
585
+ 'salting_dataframe_group_id',
586
+ 'salting_dataframe_group_number',
587
+ ):
588
+ if key in self._event.salting_info:
589
+ admin[key] = self._event.salting_info[key]
590
+
591
+ return admin
592
+
593
+
594
+ def get_channel_settings(self, channel):
595
+ """
596
+ Get channel settings dictionary
597
+
598
+ Parameters
599
+ ---------
600
+
601
+ channel : str
602
+ channel can be a single channel
603
+ or sum of channels "chan1+chan2"
604
+ or multiple channels "chan1|chan2"
605
+
606
+
607
+ Return
608
+ ------
609
+ settings_dict : dict
610
+ dictionnary with various detector settings variable
611
+
612
+ """
613
+
614
+ # initialize output
615
+ settings_dict = dict()
616
+
617
+
618
+ # check info filled
619
+ if (self._event.admin_info is None or
620
+ 'detector_config' not in self._event.admin_info):
621
+ return settings_dict
622
+
623
+ # get channels list
624
+ available_channels = self._available_channels
625
+ if available_channels is None:
626
+ available_channels = self._event.admin_info['detector_chans']
627
+
628
+ channels, separator = utils.split_channel_name(
629
+ channel,
630
+ available_channels=available_channels
631
+ )
632
+
633
+ # fill dictionary
634
+ for chan in channels:
635
+ settings_dict['tes_bias_' + chan] = (
636
+ self._event.admin_info['detector_config'][chan]['tes_bias'])
637
+ settings_dict['output_gain_' + chan] = (
638
+ self._event.admin_info['detector_config'][chan]['output_gain'])
639
+
640
+ return settings_dict
641
+
642
+
643
+
644
+ def get_channel_trace(self, channel,
645
+ nb_samples=None,
646
+ nb_pretrigger_samples=None,
647
+ weights=None):
648
+ """
649
+ Get trace (s)
650
+
651
+ Parameters
652
+ ----------
653
+
654
+ channel : str
655
+ channel can be a single channel
656
+ or sum of channels "chan1+chan2"
657
+ or multiple channels "chan1|chan2"
658
+
659
+ Return:
660
+ -------
661
+ array : ndarray
662
+ array with trace values
663
+
664
+ """
665
+
666
+ array = None
667
+
668
+ # get channels for case + or | used
669
+ available_channels = self._available_channels
670
+ if available_channels is None:
671
+ available_channels = self._event.admin_info['detector_chans']
672
+
673
+ channels, separator = utils.split_channel_name(
674
+ channel,
675
+ available_channels=available_channels
676
+ )
677
+
678
+ weights_array = None
679
+ if weights is not None:
680
+
681
+ weights_array = np.ones(len(channels))
682
+
683
+ for ichan, chan in enumerate(channels):
684
+ param = f'weight_{chan}'
685
+ if param not in weights:
686
+ raise ValueError(
687
+ f'ERROR: Missing parameter weight {param} '
688
+ f'for channel {channel}!'
689
+ )
690
+ val = weights[param]
691
+ weights_array[ichan] = val
692
+
693
+ # case full trace
694
+ if nb_samples is None:
695
+
696
+ # get array indices
697
+ channel_indices = list()
698
+ for chan in channels:
699
+ channel_indices.append(
700
+ self._event.admin_info['detector_chans'].index(chan)
701
+ )
702
+
703
+ if not channel_indices:
704
+ raise ValueError('Unable to get event traces for '
705
+ + channel)
706
+ # get array
707
+ array = self._event.full_traces[channel_indices,:]
708
+
709
+ else:
710
+
711
+ if nb_pretrigger_samples is None:
712
+ raise ValueError(
713
+ 'ERROR: "nb_pretrigger_samples" required!')
714
+
715
+ key = (nb_samples, nb_pretrigger_samples)
716
+
717
+ if (self._event.truncated_traces is None
718
+ or key not in self._event.truncated_traces.keys()):
719
+ raise ValueError('ERROR: Traces not available!')
720
+
721
+ channel_indices = list()
722
+ for chan in channels:
723
+ channel_indices.append(
724
+ self._event.truncated_traces[key]['channels'].index(chan)
725
+ )
726
+
727
+ # get array
728
+ if (self._event.truncated_traces[key]['traces'] is not None):
729
+ array = self._event.truncated_traces[key]['traces'][channel_indices,:]
730
+
731
+ # Build output
732
+ if array is None:
733
+ return array
734
+
735
+
736
+ if separator == '+':
737
+ if weights is not None:
738
+ weights_array = weights_array[:, np.newaxis]
739
+ array = array * weights_array
740
+ array = np.sum(array,axis=0)
741
+
742
+ elif separator == '-':
743
+ if weights is not None:
744
+ array = (array[0,:]*weights_array[0]
745
+ - array[1,:]*weights_array[1])
746
+ else:
747
+ array = array[0,:] - array[1,:]
748
+
749
+ elif separator is None:
750
+ array = array[0,:]
751
+
752
+ return array
753
+
754
+
755
+
756
+ def get_template(self, channel, tag='default'):
757
+ """Return a template and metadata from the loaded FilterData.
758
+
759
+ TriggerProcessing still uses this lightweight accessor. Feature OF
760
+ resource management is handled separately by FeatureResources.
761
+ """
762
+ if self._filter_data is None:
763
+ raise ValueError('ERROR: No filter_file is loaded.')
764
+ template, _time, metadata = self._filter_data.get_template(
765
+ channel, tag=tag, return_metadata=True
766
+ )
767
+ return template, metadata
768
+
769
+
770
+ def get_noise(self, channel, tag='default'):
771
+ """Return PSD/CSD data from the loaded FilterData.
772
+
773
+ Kept as a lightweight compatibility accessor; feature OF resource
774
+ preparation no longer lives in ProcessingData.
775
+ """
776
+ if self._filter_data is None:
777
+ raise ValueError('ERROR: No filter_file is loaded.')
778
+ channel_list = str(channel).replace(' ', '').split('|')
779
+ if len(channel_list) == 1:
780
+ return self._filter_data.get_psd(
781
+ channel, tag=tag, fold=False, return_metadata=True
782
+ )
783
+ return self._filter_data.get_csd(
784
+ channel, tag=tag, fold=False, return_metadata=True
785
+ )
786
+
787
+
788
+ def get_psd(self, channel, tag='default'):
789
+ """Return an unfolded PSD and metadata from the loaded FilterData."""
790
+ if self._filter_data is None:
791
+ raise ValueError('ERROR: No filter_file is loaded.')
792
+ return self._filter_data.get_psd(
793
+ channel, tag=tag, fold=False, return_metadata=True
794
+ )
795
+
796
+
797
+ def get_facility(self):
798
+ """
799
+ Function to extract facility # from
800
+ metadata
801
+
802
+ Parameters
803
+ ----------
804
+ None
805
+
806
+ Return
807
+ ------
808
+
809
+ facility : int
810
+ facility number
811
+
812
+ """
813
+ facility = None
814
+ if 'facility' in self._data_info.keys():
815
+ facility = self._data_info['facility']
816
+
817
+ return facility
818
+
819
+ def get_sample_rate(self):
820
+ """
821
+ Function to extract sample rate from
822
+ metadata
823
+
824
+ Parameters
825
+ ----------
826
+ None
827
+
828
+ Return
829
+ ------
830
+
831
+ sample_rate : float
832
+ ADC sample rate used to take data
833
+
834
+
835
+ """
836
+ sample_rate = None
837
+ if 'sample_rate_hz' in self._data_info:
838
+ sample_rate = self._data_info['sample_rate_hz']
839
+ elif 'sample_rate' in self._data_info:
840
+ sample_rate = self._data_info['sample_rate']
841
+
842
+ return sample_rate
843
+
844
+ def get_nb_samples(self):
845
+ """
846
+ Function to extract number of samples information
847
+ from metadata
848
+
849
+ Parameters
850
+ ----------
851
+ None
852
+
853
+ Return
854
+ ------
855
+
856
+ nb_samples : int
857
+ number of samples of the traces
858
+
859
+ """
860
+ nb_samples = None
861
+ if 'nb_samples' in self._data_info.keys():
862
+ nb_samples = self._data_info['nb_samples']
863
+
864
+ return nb_samples
865
+
866
+
867
+ def get_nb_pretrigger_samples(self):
868
+ """
869
+ Function to extract number of pretrigger samples
870
+ information from metadata
871
+
872
+ Parameters
873
+ ----------
874
+ None
875
+
876
+ Return
877
+ ------
878
+
879
+ nb_samples : int
880
+ number of pretrigger samples
881
+
882
+
883
+
884
+ """
885
+ nb_pretrigger_samples = None
886
+ if 'nb_pretrigger_samples' in self._data_info.keys():
887
+ nb_pretrigger_samples = self._data_info['nb_pretrigger_samples']
888
+
889
+ return nb_pretrigger_samples
890
+
891
+
892
+
893
+ def _extract_data_info(self, stream=None):
894
+ """
895
+ Function to extract all metadata
896
+ from raw data
897
+
898
+ Parameters
899
+ ----------
900
+ None
901
+
902
+ Return
903
+ ------
904
+
905
+ data_info : dict
906
+ dictionary with ADC/data information
907
+
908
+
909
+ """
910
+
911
+ # convert to stream id
912
+ stream_id = None
913
+ if stream is not None:
914
+ stream_id = extract_stream_id(stream)
915
+
916
+
917
+ data_info = None
918
+
919
+ if not self._raw_files:
920
+ raise ValueError('No file available to get sample rate!')
921
+
922
+ # StreamReader is the storage-independent metadata boundary for current
923
+ # pytesdaqx. Prefer the acquisition catalog when available; retain a
924
+ # resource-level fallback for older/direct ProcessingData callers.
925
+ reader_kwargs = {}
926
+ if stream_id is not None:
927
+ reader_kwargs['streams'] = stream_id
928
+
929
+ if self._catalog is not None:
930
+ reader_source = self._catalog.acquisition_path
931
+ reader_kwargs['catalog'] = self._catalog
932
+ elif stream_id is None:
933
+ reader_source = next(iter(self._raw_files.values()))[0]
934
+ else:
935
+ reader_source = self._raw_files[stream_id][0]
936
+
937
+ with StreamReader(reader_source, **reader_kwargs) as reader:
938
+ metadata = reader.get_metadata()
939
+
940
+ data_info = copy.deepcopy(metadata.get('groups', {}).get('adc', {}))
941
+ data_info['comment'] = metadata.get('comment')
942
+ data_info['facility'] = metadata.get('facility')
943
+ data_info['run_purpose'] = metadata.get('run_purpose', 'unknown')
944
+
945
+ # AcquisitionCatalog is the canonical source for storage/sample-rate
946
+ # information in current pytesdaqx. Keep the legacy names used by this
947
+ # class as aliases for now.
948
+ if self._catalog is not None:
949
+ view = self._catalog if stream_id is None else self._catalog.filter(streams=stream_id)
950
+ if view.sample_rate_hz is not None:
951
+ data_info['sample_rate_hz'] = float(view.sample_rate_hz)
952
+ data_info['sample_rate'] = float(view.sample_rate_hz)
953
+ if view.storage_format is not None:
954
+ data_info['storage_format'] = view.storage_format
955
+ if view.entries:
956
+ entry = view.entries[0]
957
+ if entry.get('facility') is not None:
958
+ data_info['facility'] = entry['facility']
959
+ native_length = entry.get('trace_length_samples')
960
+ if native_length is None:
961
+ native_length = entry.get('segment_length_samples')
962
+ if native_length is not None:
963
+ data_info.setdefault('nb_samples', int(native_length))
964
+
965
+ return data_info
966
+
967
+