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,317 @@
1
+ """High-level event-processing workflow used by the top-level ``pytesprocess`` CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pytesprocess.cli.common import (
6
+ catalog_for,
7
+ output_base,
8
+ trigger_edge_parameters,
9
+ )
10
+ from pytesprocess.config import ProcessingConfig
11
+ from pytesprocess.process import Randoms, TriggerProcessing, FeatureProcessing
12
+ from pytesprocess.workflows.salting import generate_salting_dataframes
13
+
14
+
15
+ _CANONICAL_STEP_ORDER = ("randoms", "salting", "trigger", "feature")
16
+ _VALID_STEPS = set(_CANONICAL_STEP_ORDER)
17
+
18
+
19
+ def normalize_processing_steps(steps):
20
+ """Return unique requested steps in the canonical execution order.
21
+
22
+ ``--steps`` accepts whitespace-separated values, comma-separated values,
23
+ or a mixture of both. For example, these are equivalent::
24
+
25
+ --steps randoms trigger feature
26
+ --steps randoms,trigger,feature
27
+ --steps randoms,trigger feature
28
+ """
29
+ requested = []
30
+ for value in steps or []:
31
+ for token in str(value).split(","):
32
+ name = token.strip().lower()
33
+ if not name:
34
+ continue
35
+ if name not in _VALID_STEPS:
36
+ raise ValueError(
37
+ f'Unknown processing step "{name}". Valid steps are '
38
+ f'{list(_CANONICAL_STEP_ORDER)}.'
39
+ )
40
+ if name not in requested:
41
+ requested.append(name)
42
+ if not requested:
43
+ raise ValueError("pytesprocess requires at least one --steps value.")
44
+ ordered = [step for step in _CANONICAL_STEP_ORDER if step in requested]
45
+ return requested, ordered
46
+
47
+
48
+ def _validate_dependencies(args, ordered_steps):
49
+ steps = set(ordered_steps)
50
+ measurement = args.measurement_type
51
+
52
+ if args.restricted and measurement != "background":
53
+ raise ValueError(
54
+ "--restricted is only valid with --measurement-type background "
55
+ "and selects restricted background data only."
56
+ )
57
+
58
+ if measurement == "threshold":
59
+ if "trigger" in steps:
60
+ raise ValueError(
61
+ "The trigger step is not valid for threshold data: NI DAQ has "
62
+ "already produced finite triggered records. Use --steps feature."
63
+ )
64
+ if "randoms" in steps:
65
+ raise ValueError(
66
+ "The randoms step is not supported for threshold data in the "
67
+ "combined process workflow."
68
+ )
69
+ if "salting" in steps:
70
+ raise ValueError(
71
+ "Salting generation currently selects continuous background "
72
+ "data. For threshold feature processing, provide an existing "
73
+ "--salting-dataframe if salt injection is required."
74
+ )
75
+ if args.trigger_dataframe:
76
+ raise ValueError(
77
+ "Threshold feature processing uses native finite records and "
78
+ "must not be given --trigger-dataframe."
79
+ )
80
+ else:
81
+ if "feature" in steps and "trigger" not in steps and not args.trigger_dataframe:
82
+ raise ValueError(
83
+ f"Feature processing of continuous {measurement} data requires "
84
+ "either the trigger step in the same workflow or an existing "
85
+ "--trigger-dataframe."
86
+ )
87
+
88
+ if "trigger" in steps and args.trigger_dataframe:
89
+ raise ValueError(
90
+ "--trigger-dataframe cannot be supplied when the trigger step is "
91
+ "requested; feature processing will use the newly generated triggers."
92
+ )
93
+
94
+ if "salting" in steps and args.salting_dataframe:
95
+ raise ValueError(
96
+ "Choose either the salting generation step or --salting-dataframe, "
97
+ "not both."
98
+ )
99
+
100
+ if args.salting_dataframe and not ({"trigger", "feature"} & steps):
101
+ raise ValueError(
102
+ "--salting-dataframe is only meaningful when trigger and/or feature "
103
+ "processing is requested."
104
+ )
105
+
106
+ if "salting" in steps and measurement != "background":
107
+ raise ValueError(
108
+ "Salting generation currently supports background data only."
109
+ )
110
+
111
+ if "randoms" in steps:
112
+ if measurement != "background":
113
+ raise ValueError(
114
+ "The combined randoms step currently supports background data only."
115
+ )
116
+ if (args.nrandoms is None) == (args.random_rate is None):
117
+ raise ValueError(
118
+ "The randoms step requires exactly one of --nrandoms or "
119
+ "--random-rate."
120
+ )
121
+
122
+
123
+ def _build_config(args, ordered_steps):
124
+ """Build and validate all requested YAML-backed workflows up front."""
125
+ yaml_workflows = [
126
+ step for step in ordered_steps if step in {"salting", "trigger", "feature"}
127
+ ]
128
+ if not yaml_workflows:
129
+ return None, catalog_for(args, measurement_types=args.measurement_type)
130
+
131
+ catalog = catalog_for(args, measurement_types=args.measurement_type)
132
+ config = ProcessingConfig(
133
+ args.config,
134
+ catalog.record_channels,
135
+ sample_rate=catalog.sample_rate_hz,
136
+ verbose=not args.quiet,
137
+ )
138
+ config.validate(
139
+ yaml_workflows,
140
+ check_resources=True,
141
+ display=not args.quiet,
142
+ )
143
+
144
+ # Salt generation may need trigger-template information for edge exclusion
145
+ # even when trigger processing itself is not requested. Validate that input
146
+ # before any generation starts.
147
+ if "salting" in ordered_steps:
148
+ salting = config.get_config("salting")
149
+ if not bool(salting.get("overall", {}).get("do_salt_deadtime", False)):
150
+ if "trigger" not in yaml_workflows:
151
+ config.validate(
152
+ ["trigger"],
153
+ check_resources=True,
154
+ display=not args.quiet,
155
+ )
156
+
157
+ return config, catalog
158
+
159
+
160
+ def _run_randoms(args):
161
+ proc = Randoms(
162
+ args.acquisition,
163
+ streams=args.streams,
164
+ processing_label=args.processing_label,
165
+ data_type="background",
166
+ restricted=args.restricted,
167
+ verbose=not args.quiet,
168
+ )
169
+ proc.process(
170
+ nrandoms=args.nrandoms,
171
+ random_rate=args.random_rate,
172
+ min_separation_msec=args.min_separation_msec,
173
+ edge_exclusion_msec=args.edge_exclusion_msec,
174
+ random_seed=args.random_seed,
175
+ lgc_save=True,
176
+ lgc_output=False,
177
+ save_path=output_base(args),
178
+ )
179
+ return proc.get_output_path()
180
+
181
+
182
+ def _run_trigger(args, config, catalog, salting_dataframe=None):
183
+ edge_msec, livetime = trigger_edge_parameters(config, catalog)
184
+ proc = TriggerProcessing(
185
+ args.acquisition,
186
+ config,
187
+ streams=args.streams,
188
+ processing_label=args.processing_label,
189
+ restricted=args.restricted,
190
+ data_type=args.measurement_type,
191
+ salting_dataframe=salting_dataframe,
192
+ verbose=not args.quiet,
193
+ )
194
+ proc.process(
195
+ ntriggers=args.ntriggers,
196
+ lgc_save=True,
197
+ lgc_output=False,
198
+ save_path=output_base(args),
199
+ ncores=args.ncores,
200
+ edge_exclusion_msec=edge_msec,
201
+ livetime=livetime,
202
+ partition_target_duration_s=args.partition_duration,
203
+ )
204
+ return proc.get_output_path()
205
+
206
+
207
+ def _run_feature(args, config, trigger_dataframe=None, salting_dataframe=None):
208
+ proc = FeatureProcessing(
209
+ args.acquisition,
210
+ config,
211
+ streams=args.streams,
212
+ trigger_dataframe_path=trigger_dataframe,
213
+ external_file=args.external_features,
214
+ processing_label=args.processing_label,
215
+ restricted=args.restricted,
216
+ data_type=args.measurement_type,
217
+ salting_dataframe=salting_dataframe,
218
+ verbose=not args.quiet,
219
+ )
220
+ proc.process(
221
+ nevents=args.nevents,
222
+ lgc_save=True,
223
+ lgc_output=False,
224
+ save_path=output_base(args),
225
+ ncores=args.ncores,
226
+ )
227
+ return proc.get_output_path() if hasattr(proc, "get_output_path") else None
228
+
229
+
230
+ def run_processing_workflow(args):
231
+ requested, ordered = normalize_processing_steps(args.steps)
232
+ _validate_dependencies(args, ordered)
233
+
234
+ if not args.quiet:
235
+ print("Requested steps: " + ", ".join(requested))
236
+ print("Execution order: " + " -> ".join(ordered))
237
+
238
+ config, catalog = _build_config(args, ordered)
239
+
240
+ results = {
241
+ "steps": ordered,
242
+ "randoms": None,
243
+ "salting": [],
244
+ "trigger": [],
245
+ "feature": [],
246
+ }
247
+
248
+ if "randoms" in ordered:
249
+ results["randoms"] = _run_randoms(args)
250
+
251
+ salting_paths = []
252
+ if "salting" in ordered:
253
+ salting_paths = generate_salting_dataframes(
254
+ args.acquisition,
255
+ args.config,
256
+ streams=args.streams,
257
+ output=args.output,
258
+ processing_label=args.processing_label,
259
+ restricted=args.restricted,
260
+ verbose=not args.quiet,
261
+ )
262
+ results["salting"] = list(salting_paths)
263
+ elif args.salting_dataframe:
264
+ salting_paths = [args.salting_dataframe]
265
+
266
+ # One generated salting dataframe represents one independent injected-data
267
+ # workflow (for example one fixed energy). Without salting there is one
268
+ # normal trigger/feature workflow.
269
+ injection_contexts = salting_paths if salting_paths else [None]
270
+
271
+ if "trigger" in ordered:
272
+ for salt_path in injection_contexts:
273
+ trigger_path = _run_trigger(
274
+ args, config, catalog, salting_dataframe=salt_path
275
+ )
276
+ results["trigger"].append(trigger_path)
277
+
278
+ if "feature" in ordered:
279
+ if "trigger" in ordered:
280
+ trigger_paths = results["trigger"]
281
+ if len(trigger_paths) != len(injection_contexts):
282
+ raise RuntimeError(
283
+ "Internal trigger/salting workflow count mismatch."
284
+ )
285
+ for trigger_path, salt_path in zip(trigger_paths, injection_contexts):
286
+ results["feature"].append(
287
+ _run_feature(
288
+ args,
289
+ config,
290
+ trigger_dataframe=trigger_path,
291
+ salting_dataframe=salt_path,
292
+ )
293
+ )
294
+ else:
295
+ # Continuous feature-only processing uses the user-provided trigger
296
+ # dataframe. Threshold processing intentionally uses None so
297
+ # FeatureProcessing reads native finite records directly.
298
+ trigger_path = (
299
+ None if args.measurement_type == "threshold"
300
+ else args.trigger_dataframe
301
+ )
302
+ for salt_path in injection_contexts:
303
+ results["feature"].append(
304
+ _run_feature(
305
+ args,
306
+ config,
307
+ trigger_dataframe=trigger_path,
308
+ salting_dataframe=salt_path,
309
+ )
310
+ )
311
+
312
+ return results
313
+
314
+
315
+ # Backward source-level alias for the short-lived v12 implementation.
316
+ def run_trigger_feature(args):
317
+ return run_processing_workflow(args)
@@ -0,0 +1,133 @@
1
+ """Salt-metadata generation workflow used by the CLI and notebooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import vaex as vx
7
+
8
+ from pytesprocess.config import ProcessingConfig
9
+ from pytesprocess.core import FilterData
10
+ from pytesprocess.salting import SaltGenerator
11
+ from pytesprocess.utils import (
12
+ get_trigger_template_info, create_dataframe_group,
13
+ add_dataframe_group_columns,
14
+ )
15
+ from pytesdaqx.io import AcquisitionCatalog
16
+ from qetpy.utils import convert_channel_name_to_list
17
+
18
+
19
+ def generate_salting_dataframes(
20
+ acquisition, config_file, *, streams=None, output=None,
21
+ processing_label=None, restricted=False, verbose=True,
22
+ ):
23
+ catalog = AcquisitionCatalog(acquisition, verbose=verbose).filter(
24
+ streams=streams, measurement_types="background", restricted=restricted
25
+ )
26
+ config = ProcessingConfig(
27
+ config_file, catalog.record_channels,
28
+ sample_rate=catalog.sample_rate_hz, verbose=verbose,
29
+ )
30
+ config.validate(["salting"], check_resources=True, display=verbose)
31
+ salting_config = config.get_config("salting")
32
+ overall = salting_config["overall"]
33
+
34
+ filter_file = overall["filter_file"]
35
+ didv_file = overall.get("didv_file")
36
+ do_salt_deadtime = bool(overall.get("do_salt_deadtime", False))
37
+
38
+ template_info = {}
39
+ if not do_salt_deadtime:
40
+ # Trigger templates define the edge exclusion used to avoid placing
41
+ # salts where trigger processing cannot recover a full pulse.
42
+ trigger_config = config.get_config("trigger")
43
+ if not trigger_config.get("channels"):
44
+ raise ValueError(
45
+ "Salting with do_salt_deadtime=false requires trigger configuration "
46
+ "so the pulse-edge exclusion can be determined."
47
+ )
48
+ fd = FilterData()
49
+ fd.load_hdf5(filter_file, overwrite=True)
50
+ template_info = get_trigger_template_info(trigger_config, fd)
51
+
52
+ filter_data = FilterData(verbose=verbose)
53
+ filter_data.load_hdf5(filter_file, overwrite=False)
54
+ if didv_file is not None:
55
+ filter_data.load_hdf5(didv_file, overwrite=False)
56
+
57
+ generator = SaltGenerator(filter_data, verbose=verbose)
58
+ generator.set_raw_data(acquisition, streams=streams, restricted=restricted)
59
+
60
+ energies = overall.get("energies")
61
+ pdf_file = overall.get("dm_pdf_file")
62
+ if energies is None:
63
+ energies = [None]
64
+ elif not isinstance(energies, list):
65
+ energies = [energies]
66
+ if pdf_file is not None and energies != [None]:
67
+ raise ValueError("Use fixed energies or dm_pdf_file, not both")
68
+
69
+ nsalt = int(overall.get("nsalt", 100))
70
+ random_seed = overall.get("random_seed")
71
+ coincident = bool(overall.get("coincident_salts", False))
72
+
73
+ if output:
74
+ base = Path(output)
75
+ else:
76
+ parent = Path(acquisition).parent
77
+ base = (parent.parent / "processed") if parent.name == "raw" else (parent / "processed")
78
+ if catalog.acquisition_name not in str(base):
79
+ base = base / catalog.acquisition_name
80
+ facility = catalog.entries[0].get("facility")
81
+ results = []
82
+
83
+ for energy_index, energy in enumerate(energies):
84
+ channel_frames = []
85
+ shared_positions = None
86
+ for channel_index, (channel, chan_config) in enumerate(
87
+ salting_config["channels"].items()
88
+ ):
89
+ seed = random_seed
90
+ if seed is not None and not coincident:
91
+ seed = int(seed) + channel_index
92
+
93
+ chan_list = convert_channel_name_to_list(channel)
94
+ efficiency = chan_config.get("collection_efficiency", 1.0)
95
+ if len(chan_list) > 1 and not isinstance(efficiency, (list, tuple)):
96
+ efficiency = [efficiency] * len(chan_list)
97
+
98
+ edge_exclusion_msec = 0.0
99
+ if not do_salt_deadtime:
100
+ edge_exclusion_msec = float(
101
+ template_info.get("max_edge_exclusion", 0.0)
102
+ )
103
+
104
+ dataframe = generator.generate_metadata(
105
+ channel,
106
+ template_tag=chan_config["template_tag"],
107
+ dpdi_tag=chan_config.get("dpdi_tag"),
108
+ dpdi_poles=chan_config.get("dpdi_poles"),
109
+ collection_efficiency=efficiency,
110
+ energy_eV=None if energy is None else float(energy),
111
+ dm_pdf_file=pdf_file,
112
+ nsalt=nsalt,
113
+ positions=shared_positions if coincident else None,
114
+ edge_exclusion_msec=edge_exclusion_msec,
115
+ random_seed=seed,
116
+ )
117
+ if coincident and shared_positions is None:
118
+ shared_positions = generator.get_positions().copy()
119
+ channel_frames.append(dataframe)
120
+
121
+ final = channel_frames[0] if len(channel_frames) == 1 else vx.concat(channel_frames)
122
+ descriptor = "salting_dm" if energy is None else f"salting_{float(energy):g}eV"
123
+ group = create_dataframe_group(
124
+ base, dataframe_type="salting", facility=facility,
125
+ name_prefix=descriptor, processing_label=processing_label,
126
+ restricted=restricted,
127
+ )
128
+ add_dataframe_group_columns(final, group, 1)
129
+ file_name = group.file_path(1)
130
+ final.export_hdf5(file_name, mode="w")
131
+ results.append(file_name)
132
+
133
+ return results
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytesprocess
3
+ Version: 0.1.1
4
+ Summary: Detector data processing for pytesdaqx acquisitions
5
+ Author-email: Bruno Serfass <serfass@berkeley.edu>, Samuel Watkins <samwatkins@berkeley.edu>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2022 Samuel Watkins
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/spice-herald/pytesprocess
29
+ Project-URL: Repository, https://github.com/spice-herald/pytesprocess
30
+ Keywords: detector,TES,DAQ,signal processing
31
+ Requires-Python: >=3.11
32
+ Description-Content-Type: text/markdown
33
+ License-File: LICENSE
34
+ Requires-Dist: numpy>=1.26
35
+ Requires-Dist: scipy
36
+ Requires-Dist: matplotlib
37
+ Requires-Dist: PyYAML
38
+ Requires-Dist: qetpy>=1.8.6
39
+ Requires-Dist: pandas
40
+ Requires-Dist: pytesdaqx
41
+ Requires-Dist: humanfriendly
42
+ Requires-Dist: vaex
43
+ Requires-Dist: pyarrow
44
+ Requires-Dist: lmfit
45
+ Requires-Dist: cloudpickle
46
+ Provides-Extra: dev
47
+ Requires-Dist: build; extra == "dev"
48
+ Requires-Dist: pytest; extra == "dev"
49
+ Dynamic: license-file
50
+
51
+ # pytesprocess
52
+
53
+ `pytesprocess` is the detector-processing layer used with raw acquisitions written
54
+ by `pytesdaqx`. It provides random-event selection, software triggering,
55
+ feature extraction, salting, IV/dIdV processing, noise/filter generation, and
56
+ persistence of processed event products as Vaex HDF5 dataframes.
57
+
58
+ The current code targets Python 3.11+ and the modern `pytesdaqx` acquisition
59
+ model (`acquisition` + `stream`).
60
+
61
+ ## Installation
62
+
63
+ For a development checkout:
64
+
65
+ ```bash
66
+ pip install -e .
67
+ ```
68
+
69
+ Runtime dependencies and the installed CLI entry point are defined entirely in
70
+ `pyproject.toml`.
71
+
72
+ ## Command-line interface
73
+
74
+ Installing the package provides:
75
+
76
+ ```bash
77
+ pytesprocess --help
78
+ ```
79
+
80
+ The normal event-processing workflow is selected directly from the top-level
81
+ command:
82
+
83
+ ```bash
84
+ pytesprocess ACQUISITION \
85
+ --config processing.yaml \
86
+ --steps randoms trigger feature
87
+ ```
88
+
89
+ `--steps` is mandatory. It accepts either whitespace-separated or
90
+ comma-separated values, including mixed input:
91
+
92
+ ```bash
93
+ --steps randoms trigger feature
94
+ --steps randoms,trigger,feature
95
+ --steps randoms,trigger feature
96
+ ```
97
+
98
+ All are equivalent. The execution order is normalized internally to:
99
+
100
+ ```text
101
+ randoms -> salting -> trigger -> feature
102
+ ```
103
+
104
+ The separate detector-characterization/filter workflows remain explicit:
105
+
106
+ ```bash
107
+ pytesprocess filter ACQUISITION --config processing.yaml
108
+ pytesprocess ivsweep ACQUISITION
109
+ ```
110
+
111
+ Example:
112
+
113
+ ```bash
114
+ pytesprocess acquisition_I2_D20260807_T170432.zarr \
115
+ --config continuous_data_processing_v2.yaml \
116
+ --steps randoms,trigger,feature \
117
+ --nrandoms 300 \
118
+ --nevents 300 \
119
+ --ncores 4
120
+ ```
121
+
122
+ See [docs/user/cli.md](docs/user/cli.md) for the current CLI behavior.
123
+
124
+ ## Configuration
125
+
126
+ New processing configuration uses `config_version: 2` and separates workflow
127
+ sections explicitly:
128
+
129
+ ```yaml
130
+ config_version: 2
131
+
132
+ resources:
133
+ filter_file: /path/to/filterdata.hdf5
134
+
135
+ trigger:
136
+ global: {}
137
+ channels: {}
138
+
139
+ feature:
140
+ global:
141
+ trace_length_msec: 20
142
+ pretrigger_length_msec: 10
143
+ presets: {}
144
+ channels: {}
145
+
146
+ salting:
147
+ global: {}
148
+ channels: {}
149
+
150
+ filter: {}
151
+ ```
152
+
153
+ Feature trace lengths are specified in milliseconds in v2 configuration. The
154
+ resolver converts them to samples using the selected acquisition sample rate.
155
+ Channel selectors include `all`, shell-style globs such as `Z1P*`, comma groups
156
+ for applying one block to several independent channels, and explicit
157
+ multi-channel expressions such as `A|B`.
158
+
159
+ See [docs/user/configuration.md](docs/user/configuration.md).
160
+
161
+ ## Processed dataframe identity
162
+
163
+ Each Vaex processing product has one dataframe-group identity:
164
+
165
+ ```text
166
+ dataframe_group_name
167
+ dataframe_group_id
168
+ dataframe_group_number
169
+ dataframe_file_index
170
+ processing_label
171
+ ```
172
+
173
+ For example:
174
+
175
+ ```text
176
+ trigger_I2_D20260908_T123456/
177
+ trigger_I2_D20260908_T123456_F0001.hdf5
178
+ trigger_I2_D20260908_T123456_F0002.hdf5
179
+ ```
180
+
181
+ `F####` is a processing task/output shard index, not a second stream or series
182
+ identifier. Raw provenance uses canonical stream fields such as `stream_id`,
183
+ `stream_number`, and `stream_trigger_index`.
184
+
185
+ See [docs/user/outputs.md](docs/user/outputs.md).
186
+
187
+ ## Package structure
188
+
189
+ The current package layout intentionally separates reusable analysis objects
190
+ from acquisition-processing workflows:
191
+
192
+ ```text
193
+ pytesprocess/
194
+ core/ reusable analysis/data objects and algorithms
195
+ process/ raw/dataframe processing executors
196
+ config/ YAML loading, selection, resolution, validation
197
+ salting/ salt metadata generation and waveform injection
198
+ workflows/ multi-step orchestration used by the CLI
199
+ cli/ command-line parsing and dispatch
200
+ utils/ shared utilities and HDF5/dataframe helpers
201
+ ```
202
+
203
+ See [docs/developer/architecture.md](docs/developer/architecture.md) for the
204
+ current developer-oriented architecture.
205
+
206
+ ## Notes on multiprocessing
207
+
208
+ The existing Vaex/PyArrow and numerical-library thread limits are intentional.
209
+ They were introduced to avoid thread oversubscription and unstable/slower
210
+ multicore processing. They should not be removed or relocated without dedicated
211
+ multicore benchmarking.