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,186 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Mapping
5
+
6
+ from pytesprocess.utils import utils
7
+
8
+
9
+ __all__ = [
10
+ 'TraceSpec',
11
+ 'OFModelSpec',
12
+ 'FeatureAlgorithmPlan',
13
+ 'FeaturePlan',
14
+ ]
15
+
16
+
17
+ # Keep the resource declaration explicit. Algorithms not listed here are
18
+ # ordinary trace algorithms (including user/external extractors).
19
+ _FFT_ALGORITHMS = {'psd_amp', 'psd_peaks', 'phase'}
20
+ _OF_PREFIXES = ('of1x1', 'of1x2x2', 'of1x3x3', 'ofnxm', 'ofnxmx2')
21
+
22
+
23
+ @dataclass(frozen=True, order=True)
24
+ class TraceSpec:
25
+ """Raw event-window definition used by one or more feature algorithms."""
26
+
27
+ nb_samples: int
28
+ nb_pretrigger_samples: int
29
+
30
+ @classmethod
31
+ def from_config(cls, config: Mapping[str, Any]):
32
+ nb_samples = config.get('nb_samples')
33
+ nb_pre = config.get('nb_pretrigger_samples')
34
+ if nb_samples is None and nb_pre is None:
35
+ return None
36
+ if nb_samples is None or nb_pre is None:
37
+ raise ValueError(
38
+ 'ERROR: Feature trace length and pretrigger length must both be defined.'
39
+ )
40
+ return cls(int(nb_samples), int(nb_pre))
41
+
42
+ def as_tuple(self):
43
+ return (self.nb_samples, self.nb_pretrigger_samples)
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class OFModelSpec:
48
+ """Identity of static QETpy OF state.
49
+
50
+ Event pretrigger is intentionally absent: it selects a raw event window but
51
+ does not change the template/noise optimal-filter model.
52
+ """
53
+
54
+ nb_samples: int
55
+ csd_tag: str = 'default'
56
+ coupling: str = 'AC'
57
+ ignored_frequency_peaks: tuple = ()
58
+ ignore_harmonics: bool = False
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class FeatureAlgorithmPlan:
63
+ channel: str
64
+ feature_channel: str
65
+ name: str
66
+ base_algorithm: str
67
+ resource_kind: str
68
+ trace_spec: TraceSpec | None
69
+ of_model_spec: OFModelSpec | None
70
+ params: Mapping[str, Any]
71
+ physical_channels: tuple[str, ...]
72
+ of_signal_channels: tuple[str, ...]
73
+
74
+
75
+ class FeaturePlan:
76
+ """Compile normalized feature YAML into an execution/resource plan once."""
77
+
78
+ def __init__(self, processing_config, available_channels, sample_rate_hz):
79
+ self._processing_config = processing_config
80
+ self._available_channels = list(available_channels or [])
81
+ self._sample_rate_hz = float(sample_rate_hz)
82
+ self._algorithms = []
83
+ self._trace_channels = {}
84
+ self._compile()
85
+
86
+ @property
87
+ def algorithms(self):
88
+ return tuple(self._algorithms)
89
+
90
+ @property
91
+ def of_algorithms(self):
92
+ return tuple(item for item in self._algorithms
93
+ if item.resource_kind == 'optimal_filter')
94
+
95
+ @property
96
+ def traces_config(self):
97
+ if not self._trace_channels:
98
+ return None
99
+ return {
100
+ spec.as_tuple(): list(channels)
101
+ for spec, channels in self._trace_channels.items()
102
+ }
103
+
104
+ @staticmethod
105
+ def _resource_kind(base_algorithm):
106
+ if base_algorithm in _FFT_ALGORITHMS:
107
+ return 'fft'
108
+ if base_algorithm.startswith(_OF_PREFIXES):
109
+ return 'optimal_filter'
110
+ return 'trace'
111
+
112
+ def _compile(self):
113
+ for channel, channel_cfg in self._processing_config.items():
114
+ if not isinstance(channel_cfg, dict):
115
+ continue
116
+
117
+ feature_channel = channel_cfg.get('feature_channel', channel)
118
+ physical_channels, _ = utils.split_channel_name(
119
+ channel, available_channels=self._available_channels
120
+ )
121
+ physical_channels = tuple(physical_channels)
122
+
123
+ for algorithm, raw_params in channel_cfg.items():
124
+ if not isinstance(raw_params, dict):
125
+ continue
126
+ if not raw_params.get('run', True):
127
+ continue
128
+
129
+ params = dict(raw_params)
130
+ base_algorithm = params.get('base_algorithm', algorithm)
131
+ resource_kind = self._resource_kind(base_algorithm)
132
+ trace_spec = TraceSpec.from_config(params)
133
+
134
+ if resource_kind in ('fft', 'optimal_filter') and trace_spec is None:
135
+ raise ValueError(
136
+ f'ERROR: Algorithm "{algorithm}" for channel "{channel}" '
137
+ 'requires an explicit trace length/pretrigger.'
138
+ )
139
+
140
+ of_spec = None
141
+ if resource_kind == 'optimal_filter':
142
+ ignored = params.get('ignored_frequency_peaks', ())
143
+ if ignored is None:
144
+ ignored = ()
145
+ elif not isinstance(ignored, (list, tuple)):
146
+ ignored = (ignored,)
147
+ else:
148
+ ignored = tuple(ignored)
149
+ of_spec = OFModelSpec(
150
+ nb_samples=trace_spec.nb_samples,
151
+ csd_tag=str(params.get('csd_tag', 'default')),
152
+ coupling=str(params.get('coupling', 'AC')),
153
+ ignored_frequency_peaks=tuple(ignored),
154
+ ignore_harmonics=bool(params.get('ignore_harmonics', False)),
155
+ )
156
+
157
+ of_signal_channels = physical_channels
158
+ if resource_kind == 'optimal_filter':
159
+ # Preserve the established OF convention: ``|`` means
160
+ # multiple OF channels, while derived ``+``/``-`` names
161
+ # are one composite OF signal stored under that expression.
162
+ of_signal_channels, _ = utils.split_channel_name(
163
+ channel, available_channels=self._available_channels,
164
+ separator='|'
165
+ )
166
+ of_signal_channels = tuple(of_signal_channels)
167
+
168
+ plan = FeatureAlgorithmPlan(
169
+ channel=channel,
170
+ feature_channel=feature_channel,
171
+ name=algorithm,
172
+ base_algorithm=base_algorithm,
173
+ resource_kind=resource_kind,
174
+ trace_spec=trace_spec,
175
+ of_model_spec=of_spec,
176
+ params=params,
177
+ physical_channels=physical_channels,
178
+ of_signal_channels=of_signal_channels,
179
+ )
180
+ self._algorithms.append(plan)
181
+
182
+ if trace_spec is not None:
183
+ existing = self._trace_channels.setdefault(trace_spec, [])
184
+ for chan in physical_channels:
185
+ if chan not in existing:
186
+ existing.append(chan)
@@ -0,0 +1,267 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ import numpy as np
6
+ import qetpy as qp
7
+
8
+ from .feature_plan import TraceSpec, OFModelSpec, FeatureAlgorithmPlan, FeaturePlan
9
+
10
+
11
+ __all__ = [
12
+ 'EventTraceCache',
13
+ 'EventFFTData',
14
+ 'FeatureResources',
15
+ ]
16
+
17
+
18
+ class EventFFTData:
19
+ """Small signal-FFT view used by non-OF frequency-domain features.
20
+
21
+ It intentionally exposes the subset of the old OFBase signal API used by
22
+ psd_amp/psd_peaks/phase, without constructing a QETpy OFBase.
23
+ """
24
+
25
+ def __init__(self, channel, trace, sample_rate_hz):
26
+ self._channel = channel
27
+ self._fs = float(sample_rate_hz)
28
+ self._nbins = None
29
+ self._freqs = None
30
+ self._fft = None
31
+ if trace is not None:
32
+ self._nbins = int(trace.shape[-1])
33
+ freqs, fft = qp.utils.fft(np.asarray(trace), self._fs, axis=-1)
34
+ self._freqs = np.asarray(freqs)
35
+ # Match QETpy OFBase.update_signal() normalization.
36
+ self._fft = np.asarray(fft) / self._nbins
37
+
38
+ def is_signal_stored(self, channel):
39
+ return channel == self._channel and self._fft is not None
40
+
41
+ def signal_fft(self, channel, squeeze_array=False):
42
+ if not self.is_signal_stored(channel):
43
+ raise ValueError(f'ERROR: FFT data are not stored for channel {channel}.')
44
+ out = self._fft
45
+ return np.squeeze(out) if squeeze_array else np.atleast_2d(out)
46
+
47
+ def fft_freqs(self):
48
+ return self._freqs
49
+
50
+ def nb_samples(self):
51
+ return self._nbins
52
+
53
+
54
+ class EventTraceCache:
55
+ """Per-event lazy cache for trace windows and their FFTs."""
56
+
57
+ def __init__(self, processing_data, sample_rate_hz, weights=None):
58
+ self._processing_data = processing_data
59
+ self._sample_rate_hz = float(sample_rate_hz)
60
+ self._weights = weights or {}
61
+ self._traces = {}
62
+ self._ffts = {}
63
+
64
+ def _weights_for(self, channel):
65
+ return self._weights.get(channel)
66
+
67
+ def get_trace(self, channel, trace_spec: TraceSpec | None):
68
+ key = (channel, trace_spec)
69
+ if key not in self._traces:
70
+ if trace_spec is None:
71
+ trace = self._processing_data.get_channel_trace(
72
+ channel, weights=self._weights_for(channel)
73
+ )
74
+ else:
75
+ trace = self._processing_data.get_channel_trace(
76
+ channel,
77
+ nb_samples=trace_spec.nb_samples,
78
+ nb_pretrigger_samples=trace_spec.nb_pretrigger_samples,
79
+ weights=self._weights_for(channel),
80
+ )
81
+ self._traces[key] = trace
82
+ return self._traces[key]
83
+
84
+ def get_fft_data(self, channel, trace_spec: TraceSpec):
85
+ key = (channel, trace_spec)
86
+ if key not in self._ffts:
87
+ trace = self.get_trace(channel, trace_spec)
88
+ self._ffts[key] = EventFFTData(
89
+ channel, trace, self._sample_rate_hz
90
+ )
91
+ return self._ffts[key]
92
+
93
+
94
+ @dataclass
95
+ class _OFModelState:
96
+ of_base: object
97
+ template_definitions: dict = field(default_factory=dict)
98
+ current_trace_spec: TraceSpec | None = None
99
+ loaded_channels: set = field(default_factory=set)
100
+
101
+
102
+ class FeatureResources:
103
+ """Static feature resources plus lazy per-event QETpy signal loading."""
104
+
105
+ def __init__(self, feature_plan: FeaturePlan, filter_data,
106
+ sample_rate_hz, available_channels=None, verbose=True):
107
+ self._plan = feature_plan
108
+ self._filter_data = filter_data
109
+ self._sample_rate_hz = float(sample_rate_hz)
110
+ self._available_channels = list(available_channels or [])
111
+ self._verbose = verbose
112
+ self._of_models = {}
113
+ self._prepare()
114
+
115
+ @property
116
+ def number_of_models(self):
117
+ return len(self._of_models)
118
+
119
+ def begin_event(self):
120
+ # Dynamic signal state is cleared once per event. Static templates,
121
+ # CSDs, phis, weights and p-matrices remain in the OFBase.
122
+ for state in self._of_models.values():
123
+ state.of_base.clear_signal()
124
+ state.current_trace_spec = None
125
+ state.loaded_channels.clear()
126
+
127
+ def _get_state(self, spec: OFModelSpec):
128
+ state = self._of_models.get(spec)
129
+ if state is None:
130
+ state = _OFModelState(
131
+ of_base=qp.OFBase(self._sample_rate_hz, verbose=self._verbose)
132
+ )
133
+ self._of_models[spec] = state
134
+ return state
135
+
136
+ def _prepare(self):
137
+ if not self._plan.of_algorithms:
138
+ return
139
+ if self._filter_data is None:
140
+ raise ValueError(
141
+ 'ERROR: Feature optimal-filter algorithms require a filter_file.'
142
+ )
143
+
144
+ for plan in self._plan.of_algorithms:
145
+ self._prepare_algorithm(plan)
146
+
147
+ def _prepare_algorithm(self, plan: FeatureAlgorithmPlan):
148
+ spec = plan.of_model_spec
149
+ state = self._get_state(spec)
150
+ of = state.of_base
151
+ params = plan.params
152
+ channel = plan.channel
153
+
154
+ # Static noise model.
155
+ if of.csd(channel) is None:
156
+ csd, _freqs, metadata = self._filter_data.get_csd(
157
+ channel, tag=spec.csd_tag, fold=False, return_metadata=True
158
+ )
159
+ fs_filter = metadata.get('sample_rate_hz', metadata.get('sample_rate'))
160
+ if fs_filter is not None and float(fs_filter) != self._sample_rate_hz:
161
+ raise ValueError(
162
+ f'ERROR: Sample rate is inconsistent between raw data '
163
+ f'({self._sample_rate_hz}) and CSD ({fs_filter}) for '
164
+ f'channel {channel}.'
165
+ )
166
+ if int(csd.shape[-1]) != spec.nb_samples:
167
+ raise ValueError(
168
+ f'ERROR: Number of samples is inconsistent between raw data '
169
+ f'({spec.nb_samples}) and CSD ({csd.shape[-1]}) for '
170
+ f'channel {channel}, algorithm {plan.name}.'
171
+ )
172
+ ignored = list(spec.ignored_frequency_peaks) or None
173
+ of.set_csd(
174
+ channel, csd,
175
+ coupling=spec.coupling,
176
+ ignored_frequency_peaks=ignored,
177
+ ignore_harmonics=spec.ignore_harmonics,
178
+ )
179
+
180
+ template_tag = params.get('template_tag')
181
+ if template_tag is None:
182
+ raise ValueError(
183
+ f'ERROR: a "template_tag" in yaml file is required for '
184
+ f'channel {channel}, algorithm "{plan.name}"!'
185
+ )
186
+
187
+ template, _time, metadata = self._filter_data.get_template(
188
+ channel, tag=template_tag, return_metadata=True
189
+ )
190
+ if int(template.shape[-1]) != spec.nb_samples:
191
+ raise ValueError(
192
+ f'ERROR: Number of samples is inconsistent between raw data '
193
+ f'({spec.nb_samples}) and template ("{template_tag}") for '
194
+ f'channel {channel}, algorithm {plan.name}.'
195
+ )
196
+
197
+ template_pre = metadata.get('nb_pretrigger_samples')
198
+ if template_pre is None:
199
+ template_pre = plan.trace_spec.nb_pretrigger_samples
200
+ template_pre = int(template_pre)
201
+ integralnorm = bool(params.get('integralnorm', False))
202
+ template_key = (channel, str(template_tag))
203
+ definition = (integralnorm, template_pre)
204
+ old_definition = state.template_definitions.get(template_key)
205
+ if old_definition is not None and old_definition != definition:
206
+ raise ValueError(
207
+ f'ERROR: Conflicting definitions for template tag "{template_tag}" '
208
+ f'on channel {channel}: {old_definition} vs {definition}. '
209
+ 'Use a distinct template tag for different normalization/pretrigger.'
210
+ )
211
+
212
+ if old_definition is None:
213
+ of.add_template(
214
+ channel, template,
215
+ template_tag=template_tag,
216
+ pretrigger_samples=template_pre,
217
+ integralnorm=integralnorm,
218
+ overwrite=False,
219
+ )
220
+ state.template_definitions[template_key] = definition
221
+
222
+ if of.phi(channel, template_tag) is None:
223
+ of.calc_phi(channel, template_tag)
224
+
225
+ if plan.base_algorithm == 'ofnxmx2':
226
+ if ('template_group_ids' not in params or 'fit_window' not in params):
227
+ raise ValueError(
228
+ f'ERROR: "template_group_ids" and "fit_window" required '
229
+ f'in yaml file for channel {channel}, algorithm {plan.name}!'
230
+ )
231
+ of.set_time_constraints(
232
+ channel,
233
+ template_group_ids=np.asarray(params['template_group_ids']),
234
+ fit_window=np.asarray(params['fit_window']),
235
+ restrict_time_flag=bool(params.get('restrict_time_flag', True)),
236
+ time_constraints_tag=plan.name,
237
+ )
238
+ of.calc_p_matrix(
239
+ channel,
240
+ template_tag=template_tag,
241
+ time_constraints_tag=plan.name,
242
+ )
243
+
244
+ def get_of_base(self, plan: FeatureAlgorithmPlan,
245
+ event_cache: EventTraceCache):
246
+ if plan.resource_kind != 'optimal_filter':
247
+ return None
248
+ state = self._of_models[plan.of_model_spec]
249
+
250
+ # One static OF model can be reused for several event pretriggers.
251
+ # Switching TraceSpec only replaces per-event signal state.
252
+ if state.current_trace_spec is None:
253
+ state.current_trace_spec = plan.trace_spec
254
+ elif state.current_trace_spec != plan.trace_spec:
255
+ state.of_base.clear_signal()
256
+ state.loaded_channels.clear()
257
+ state.current_trace_spec = plan.trace_spec
258
+
259
+ for channel in plan.of_signal_channels:
260
+ if channel in state.loaded_channels:
261
+ continue
262
+ trace = event_cache.get_trace(channel, plan.trace_spec)
263
+ if trace is not None:
264
+ state.of_base.update_signal(channel, trace, calc_fft=True)
265
+ state.loaded_channels.add(channel)
266
+
267
+ return state.of_base