pytesprocess 0.1.1__tar.gz → 0.1.2__tar.gz

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 (137) hide show
  1. {pytesprocess-0.1.1/pytesprocess.egg-info → pytesprocess-0.1.2}/PKG-INFO +3 -1
  2. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/README.md +1 -0
  3. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pyproject.toml +1 -0
  4. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/_version.py +1 -1
  5. pytesprocess-0.1.2/pytesprocess/cli/__init__.py +16 -0
  6. pytesprocess-0.1.2/pytesprocess/cli/commands/__init__.py +5 -0
  7. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/cli/commands/event.py +11 -0
  8. pytesprocess-0.1.2/pytesprocess/cli/commands/feature.py +144 -0
  9. pytesprocess-0.1.2/pytesprocess/cli/commands/randoms.py +88 -0
  10. pytesprocess-0.1.2/pytesprocess/cli/commands/trigger.py +118 -0
  11. pytesprocess-0.1.2/pytesprocess/cli/commands/vibration.py +18 -0
  12. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/cli/main.py +27 -14
  13. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/config/manager.py +20 -9
  14. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/config/resolvers/__init__.py +3 -1
  15. pytesprocess-0.1.2/pytesprocess/config/resolvers/common.py +127 -0
  16. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/config/resolvers/feature.py +57 -32
  17. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/config/resolvers/salting.py +5 -34
  18. pytesprocess-0.1.2/pytesprocess/config/resolvers/trigger.py +63 -0
  19. pytesprocess-0.1.2/pytesprocess/config/resolvers/vibration.py +230 -0
  20. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/config/selectors.py +30 -42
  21. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/config/validation.py +11 -2
  22. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/core/algorithms.py +281 -39
  23. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/core/eventbuilder.py +143 -42
  24. pytesprocess-0.1.2/pytesprocess/core/filter_metadata.py +166 -0
  25. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/core/filterbuilder.py +4 -3
  26. pytesprocess-0.1.2/pytesprocess/core/filterdata.py +1628 -0
  27. pytesprocess-0.1.2/pytesprocess/core/ivsweep.py +1675 -0
  28. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/core/noise.py +274 -352
  29. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/core/noisemodel.py +331 -584
  30. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/core/oftrigger.py +105 -57
  31. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/core/template.py +101 -30
  32. pytesprocess-0.1.2/pytesprocess/core/trigger_utils.py +18 -0
  33. pytesprocess-0.1.2/pytesprocess/process/data_source.py +419 -0
  34. pytesprocess-0.1.2/pytesprocess/process/feature_errors.py +113 -0
  35. pytesprocess-0.1.2/pytesprocess/process/feature_memory.py +40 -0
  36. pytesprocess-0.1.2/pytesprocess/process/feature_plan.py +300 -0
  37. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/process/feature_resources.py +49 -34
  38. pytesprocess-0.1.2/pytesprocess/process/feature_schema.py +80 -0
  39. pytesprocess-0.1.2/pytesprocess/process/feature_windows.py +26 -0
  40. pytesprocess-0.1.2/pytesprocess/process/feature_work.py +279 -0
  41. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/process/features.py +442 -144
  42. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/process/filterprocess.py +7 -1
  43. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/process/processing_data.py +147 -84
  44. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/process/randoms.py +223 -36
  45. pytesprocess-0.1.2/pytesprocess/process/randoms_work.py +110 -0
  46. pytesprocess-0.1.2/pytesprocess/process/trigger_partitions.py +152 -0
  47. pytesprocess-0.1.2/pytesprocess/process/trigger_work.py +108 -0
  48. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/process/triggers.py +366 -84
  49. pytesprocess-0.1.2/pytesprocess/process/work_plan.py +217 -0
  50. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/utils/__init__.py +2 -0
  51. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/utils/arg_utils.py +23 -12
  52. pytesprocess-0.1.2/pytesprocess/utils/channels.py +186 -0
  53. pytesprocess-0.1.2/pytesprocess/utils/dataframe_output.py +270 -0
  54. pytesprocess-0.1.2/pytesprocess/utils/downsampling.py +170 -0
  55. pytesprocess-0.1.2/pytesprocess/utils/event_metadata.py +155 -0
  56. pytesprocess-0.1.2/pytesprocess/utils/filter_hdf5.py +539 -0
  57. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/utils/utils.py +31 -127
  58. pytesprocess-0.1.2/pytesprocess/vibration/__init__.py +7 -0
  59. pytesprocess-0.1.2/pytesprocess/vibration/analyzer.py +407 -0
  60. pytesprocess-0.1.2/pytesprocess/vibration/estimators.py +342 -0
  61. pytesprocess-0.1.2/pytesprocess/vibration/plotting.py +315 -0
  62. pytesprocess-0.1.2/pytesprocess/vibration/processing.py +581 -0
  63. pytesprocess-0.1.2/pytesprocess/workflows/__init__.py +3 -0
  64. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/workflows/processing.py +5 -6
  65. {pytesprocess-0.1.1 → pytesprocess-0.1.2/pytesprocess.egg-info}/PKG-INFO +3 -1
  66. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess.egg-info/SOURCES.txt +60 -1
  67. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess.egg-info/requires.txt +1 -0
  68. pytesprocess-0.1.2/tests/test_channel_expressions.py +187 -0
  69. pytesprocess-0.1.2/tests/test_cli_exit_status.py +61 -0
  70. pytesprocess-0.1.2/tests/test_config_resolvers.py +204 -0
  71. pytesprocess-0.1.2/tests/test_dataframe_output.py +136 -0
  72. pytesprocess-0.1.2/tests/test_downsampled_filters.py +144 -0
  73. pytesprocess-0.1.2/tests/test_downsampling.py +132 -0
  74. pytesprocess-0.1.2/tests/test_empty_and_selected_features.py +226 -0
  75. pytesprocess-0.1.2/tests/test_eventbuilder_coincidence.py +115 -0
  76. pytesprocess-0.1.2/tests/test_feature_downsampling.py +156 -0
  77. pytesprocess-0.1.2/tests/test_feature_memory.py +176 -0
  78. pytesprocess-0.1.2/tests/test_feature_on_error.py +201 -0
  79. pytesprocess-0.1.2/tests/test_feature_schema.py +62 -0
  80. pytesprocess-0.1.2/tests/test_feature_tasks.py +89 -0
  81. pytesprocess-0.1.2/tests/test_feature_unit_execution.py +229 -0
  82. pytesprocess-0.1.2/tests/test_feature_window_errors.py +271 -0
  83. pytesprocess-0.1.2/tests/test_feature_work_units.py +368 -0
  84. pytesprocess-0.1.2/tests/test_fft_bin.py +280 -0
  85. pytesprocess-0.1.2/tests/test_filter_compatibility.py +143 -0
  86. pytesprocess-0.1.2/tests/test_filter_hdf5.py +193 -0
  87. pytesprocess-0.1.2/tests/test_filter_metadata.py +185 -0
  88. pytesprocess-0.1.2/tests/test_memory_limit_per_worker.py +92 -0
  89. pytesprocess-0.1.2/tests/test_metadata_roundtrip.py +275 -0
  90. pytesprocess-0.1.2/tests/test_native_features.py +195 -0
  91. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/tests/test_processing_data_v15.py +2 -1
  92. pytesprocess-0.1.2/tests/test_processing_guards.py +160 -0
  93. pytesprocess-0.1.2/tests/test_randoms_work_units.py +130 -0
  94. pytesprocess-0.1.2/tests/test_restricted_processing.py +159 -0
  95. pytesprocess-0.1.2/tests/test_signed_feature_windows.py +293 -0
  96. pytesprocess-0.1.2/tests/test_template_metadata.py +82 -0
  97. pytesprocess-0.1.2/tests/test_trigger_alignment.py +120 -0
  98. pytesprocess-0.1.2/tests/test_trigger_downsampling.py +311 -0
  99. pytesprocess-0.1.2/tests/test_trigger_filter_reuse.py +113 -0
  100. pytesprocess-0.1.2/tests/test_trigger_partition_groups.py +293 -0
  101. pytesprocess-0.1.2/tests/test_trigger_work_units.py +138 -0
  102. pytesprocess-0.1.2/tests/test_vibration_estimators.py +155 -0
  103. pytesprocess-0.1.2/tests/test_vibration_processing.py +319 -0
  104. pytesprocess-0.1.1/pytesprocess/cli/__init__.py +0 -1
  105. pytesprocess-0.1.1/pytesprocess/cli/commands/__init__.py +0 -5
  106. pytesprocess-0.1.1/pytesprocess/config/resolvers/common.py +0 -56
  107. pytesprocess-0.1.1/pytesprocess/config/resolvers/trigger.py +0 -84
  108. pytesprocess-0.1.1/pytesprocess/core/filterdata.py +0 -1849
  109. pytesprocess-0.1.1/pytesprocess/core/ivsweep.py +0 -2072
  110. pytesprocess-0.1.1/pytesprocess/process/data_source.py +0 -185
  111. pytesprocess-0.1.1/pytesprocess/process/feature_plan.py +0 -186
  112. pytesprocess-0.1.1/pytesprocess/utils/dataframe_output.py +0 -120
  113. pytesprocess-0.1.1/pytesprocess/utils/filter_hdf5.py +0 -594
  114. pytesprocess-0.1.1/pytesprocess/workflows/__init__.py +0 -3
  115. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/LICENSE +0 -0
  116. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/__init__.py +0 -0
  117. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/cli/commands/filter.py +0 -0
  118. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/cli/commands/ivsweep.py +0 -0
  119. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/cli/common.py +0 -0
  120. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/config/__init__.py +0 -0
  121. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/config/loader.py +0 -0
  122. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/config/warnings.py +0 -0
  123. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/core/__init__.py +0 -0
  124. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/core/didv.py +0 -0
  125. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/process/__init__.py +0 -0
  126. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/process/event_context.py +0 -0
  127. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/process/ivprocess.py +0 -0
  128. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/salting/__init__.py +0 -0
  129. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/salting/generator.py +0 -0
  130. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/salting/injector.py +0 -0
  131. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/salting/sampling.py +0 -0
  132. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess/workflows/salting.py +0 -0
  133. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess.egg-info/dependency_links.txt +0 -0
  134. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess.egg-info/entry_points.txt +0 -0
  135. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/pytesprocess.egg-info/top_level.txt +0 -0
  136. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/setup.cfg +0 -0
  137. {pytesprocess-0.1.1 → pytesprocess-0.1.2}/tests/test_cli_steps_v16.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytesprocess
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Detector data processing for pytesdaqx acquisitions
5
5
  Author-email: Bruno Serfass <serfass@berkeley.edu>, Samuel Watkins <samwatkins@berkeley.edu>
6
6
  License: MIT License
@@ -37,6 +37,7 @@ Requires-Dist: matplotlib
37
37
  Requires-Dist: PyYAML
38
38
  Requires-Dist: qetpy>=1.8.6
39
39
  Requires-Dist: pandas
40
+ Requires-Dist: tables
40
41
  Requires-Dist: pytesdaqx
41
42
  Requires-Dist: humanfriendly
42
43
  Requires-Dist: vaex
@@ -106,6 +107,7 @@ The separate detector-characterization/filter workflows remain explicit:
106
107
  ```bash
107
108
  pytesprocess filter ACQUISITION --config processing.yaml
108
109
  pytesprocess ivsweep ACQUISITION
110
+ pytesprocess vibration ACQUISITION --config vibration.yaml
109
111
  ```
110
112
 
111
113
  Example:
@@ -56,6 +56,7 @@ The separate detector-characterization/filter workflows remain explicit:
56
56
  ```bash
57
57
  pytesprocess filter ACQUISITION --config processing.yaml
58
58
  pytesprocess ivsweep ACQUISITION
59
+ pytesprocess vibration ACQUISITION --config vibration.yaml
59
60
  ```
60
61
 
61
62
  Example:
@@ -21,6 +21,7 @@ dependencies = [
21
21
  "PyYAML",
22
22
  "qetpy>=1.8.6",
23
23
  "pandas",
24
+ "tables",
24
25
  "pytesdaqx",
25
26
  "humanfriendly",
26
27
  "vaex",
@@ -1,2 +1,2 @@
1
1
  # special file for defining the current version of the package
2
- __version__ = "0.1.1"
2
+ __version__ = "0.1.2"
@@ -0,0 +1,16 @@
1
+ """Command-line interface for pytesprocess.
2
+
3
+ ``main`` is re-exported lazily: ``pytesprocess.workflows`` imports helpers from
4
+ ``pytesprocess.cli.common``, so eagerly pulling in the command modules here
5
+ would make that a circular import.
6
+ """
7
+
8
+ __all__ = ["main"]
9
+
10
+
11
+ def __getattr__(name):
12
+ if name == "main":
13
+ from .main import main
14
+
15
+ return main
16
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,5 @@
1
+ """Command-specific parser/run helpers for the pytesprocess CLI."""
2
+
3
+ from . import event, feature, filter, ivsweep, randoms, trigger, vibration
4
+
5
+ __all__ = ["event", "feature", "filter", "ivsweep", "randoms", "trigger", "vibration"]
@@ -51,6 +51,17 @@ def configure(parser):
51
51
  default=10.0,
52
52
  dest="partition_duration",
53
53
  )
54
+ parser.add_argument(
55
+ "--memory-limit",
56
+ default="2GB",
57
+ help=(
58
+ "Memory safety limit per worker, not shared between them. For "
59
+ "continuous triggering it bounds the retained waveforms of one "
60
+ "connected pileup/coincidence group, whose size is set by the "
61
+ "data rather than by how much is processed at a time; raise it "
62
+ "if triggering reports the buffer limit."
63
+ ),
64
+ )
54
65
 
55
66
  # Random-generation options. They are validated only when randoms is
56
67
  # requested, so the same top-level command works for other step sets.
@@ -0,0 +1,144 @@
1
+ """Feature work-unit planning and execution.
2
+
3
+ Two commands that between them let feature processing be divided across any
4
+ number of workers: ``plan`` writes the division once, ``run`` executes one
5
+ piece of it. A unit is a plain command line, so the same division can be
6
+ driven by a shell loop, a job array, or anything else.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ from pytesprocess.cli.common import add_common_acquisition_args, output_base
12
+ from pytesprocess.process import FeatureProcessing
13
+ from pytesprocess.process.feature_work import load_plan
14
+
15
+
16
+ # This command builds its own sub-parsers, so the shared acquisition
17
+ # arguments are added per action rather than once at the top level.
18
+ COMMON_ARGS = False
19
+
20
+ _MEASUREMENT_TYPES = ["background", "calibration"]
21
+
22
+
23
+ def _add_shared(parser):
24
+ add_common_acquisition_args(parser, config=True, ncores=False)
25
+ parser.add_argument(
26
+ "--measurement-type",
27
+ default="background",
28
+ choices=_MEASUREMENT_TYPES,
29
+ help="Raw measurement type of the data behind the trigger dataframe.",
30
+ )
31
+ parser.add_argument("--external-features")
32
+ parser.add_argument(
33
+ "--salting-dataframe",
34
+ help="Existing salting dataframe to inject during feature processing.",
35
+ )
36
+
37
+
38
+ def configure(parser):
39
+ actions = parser.add_subparsers(dest="action", required=True)
40
+
41
+ plan = actions.add_parser(
42
+ "plan",
43
+ help="divide a trigger dataframe into feature work units",
44
+ )
45
+ _add_shared(plan)
46
+ plan.add_argument(
47
+ "--trigger-dataframe",
48
+ required=True,
49
+ help="Trigger dataframe group or shard(s) to divide.",
50
+ )
51
+ size = plan.add_mutually_exclusive_group()
52
+ size.add_argument(
53
+ "--units",
54
+ type=int,
55
+ help="Number of work units. Sizes then differ by at most one row.",
56
+ )
57
+ size.add_argument(
58
+ "--rows-per-unit",
59
+ type=int,
60
+ help="Fixed rows per unit, which bounds the memory one worker needs.",
61
+ )
62
+ plan.add_argument(
63
+ "--plan-file",
64
+ help="Where to write the plan. Default: plan.json in the new group.",
65
+ )
66
+ plan.add_argument("--campaign", help="Campaign name, recorded for provenance.")
67
+
68
+ run = actions.add_parser(
69
+ "run",
70
+ help="process exactly one work unit of a plan",
71
+ )
72
+ _add_shared(run)
73
+ run.add_argument("--plan", required=True, help="Plan file to read.")
74
+ run.add_argument(
75
+ "--unit", type=int, required=True, help="1-based unit index to process."
76
+ )
77
+ run.add_argument(
78
+ "--trigger-dataframe",
79
+ help="Trigger dataframe group. Default: derived from the plan itself.",
80
+ )
81
+ run.add_argument(
82
+ "--product-group",
83
+ help="Override the product group recorded in the plan.",
84
+ )
85
+ run.add_argument("--memory-limit", default="2GB")
86
+
87
+
88
+ def run(args):
89
+ if args.action == "plan":
90
+ return _plan(args)
91
+ return _run_unit(args)
92
+
93
+
94
+ def _processor(args, trigger_dataframe):
95
+ return FeatureProcessing(
96
+ args.acquisition,
97
+ args.config,
98
+ streams=args.streams,
99
+ trigger_dataframe_path=trigger_dataframe,
100
+ external_file=args.external_features,
101
+ processing_label=args.processing_label,
102
+ restricted=args.restricted,
103
+ data_type=args.measurement_type,
104
+ salting_dataframe=args.salting_dataframe,
105
+ verbose=not args.quiet,
106
+ )
107
+
108
+
109
+ def _plan(args):
110
+ proc = _processor(args, args.trigger_dataframe)
111
+ return proc.plan(
112
+ n_units=args.units,
113
+ rows_per_unit=args.rows_per_unit,
114
+ save_path=output_base(args),
115
+ plan_path=args.plan_file,
116
+ campaign=args.campaign,
117
+ )
118
+
119
+
120
+ def _run_unit(args):
121
+ plan = load_plan(args.plan)
122
+ trigger_dataframe = args.trigger_dataframe or _trigger_path_from_plan(plan)
123
+ proc = _processor(args, trigger_dataframe)
124
+ return proc.process_unit(
125
+ plan,
126
+ args.unit,
127
+ product_group=args.product_group,
128
+ memory_limit=args.memory_limit,
129
+ )
130
+
131
+
132
+ def _trigger_path_from_plan(plan):
133
+ """Locate the input trigger dataframe from the plan's own segments.
134
+
135
+ A unit job is told a plan and an index; it should not also have to be
136
+ told where the input lives, since the plan already says.
137
+ """
138
+ for unit in plan.get("units", []):
139
+ for segment in unit.get("selector", {}).get("segments", []):
140
+ return str(Path(segment["file"]).parent)
141
+ raise ValueError(
142
+ "ERROR: This plan has no row segments, so the input trigger "
143
+ "dataframe cannot be located. Pass --trigger-dataframe explicitly."
144
+ )
@@ -0,0 +1,88 @@
1
+ """Random-trigger work-unit planning and execution.
2
+
3
+ ``plan`` describes the selection as one unit and records the seed; ``run``
4
+ generates it. The step is one unit by design: randoms are drawn across the
5
+ whole selection at once, so dividing it would change the product rather than
6
+ distribute the work.
7
+ """
8
+
9
+ from pytesprocess.cli.common import add_common_acquisition_args, output_base
10
+ from pytesprocess.process import Randoms
11
+ from pytesprocess.process.work_plan import load_plan
12
+
13
+
14
+ # This command builds its own sub-parsers, so the shared acquisition
15
+ # arguments are added per action rather than once at the top level.
16
+ COMMON_ARGS = False
17
+
18
+
19
+ def _add_shared(parser):
20
+ add_common_acquisition_args(parser, config=False, ncores=False)
21
+
22
+
23
+ def configure(parser):
24
+ actions = parser.add_subparsers(dest="action", required=True)
25
+
26
+ plan = actions.add_parser(
27
+ "plan", help="describe the random selection as a work unit"
28
+ )
29
+ _add_shared(plan)
30
+ count = plan.add_mutually_exclusive_group(required=True)
31
+ count.add_argument("--nrandoms", type=int, help="Exact number to generate.")
32
+ count.add_argument(
33
+ "--random-rate", type=float,
34
+ help="Average rate in Hz; the count follows from the live time.",
35
+ )
36
+ plan.add_argument("--min-separation-msec", type=float, default=0.0)
37
+ plan.add_argument("--edge-exclusion-msec", type=float, default=0.0)
38
+ plan.add_argument("--partition-duration", type=float, dest="partition_duration")
39
+ plan.add_argument(
40
+ "--random-seed", type=int,
41
+ help="Seed to record. One is generated and recorded if omitted.",
42
+ )
43
+ plan.add_argument("--plan-file")
44
+ plan.add_argument("--campaign")
45
+
46
+ run = actions.add_parser("run", help="generate the randoms of a plan")
47
+ _add_shared(run)
48
+ run.add_argument("--plan", required=True)
49
+ run.add_argument("--unit", type=int, default=1)
50
+ run.add_argument("--product-group")
51
+
52
+
53
+ def run(args):
54
+ if args.action == "plan":
55
+ return _plan(args)
56
+ return _run_unit(args)
57
+
58
+
59
+ def _processor(args):
60
+ return Randoms(
61
+ args.acquisition,
62
+ streams=args.streams,
63
+ processing_label=args.processing_label,
64
+ data_type="background",
65
+ restricted=args.restricted,
66
+ verbose=not args.quiet,
67
+ )
68
+
69
+
70
+ def _plan(args):
71
+ return _processor(args).plan(
72
+ nrandoms=args.nrandoms,
73
+ random_rate=args.random_rate,
74
+ min_separation_msec=args.min_separation_msec,
75
+ edge_exclusion_msec=args.edge_exclusion_msec,
76
+ partition_target_duration_s=args.partition_duration,
77
+ random_seed=args.random_seed,
78
+ save_path=output_base(args),
79
+ plan_path=args.plan_file,
80
+ campaign=args.campaign,
81
+ )
82
+
83
+
84
+ def _run_unit(args):
85
+ plan = load_plan(args.plan, step="randoms")
86
+ return _processor(args).process_unit(
87
+ plan, args.unit, product_group=args.product_group
88
+ )
@@ -0,0 +1,118 @@
1
+ """Trigger work-unit planning and execution.
2
+
3
+ ``plan`` divides the selected streams into units, one per stream, and records
4
+ the live time and edge exclusion once so that every unit stamps the same
5
+ values. ``run`` executes one unit of that plan.
6
+ """
7
+
8
+ from pytesprocess.cli.common import (
9
+ add_common_acquisition_args,
10
+ output_base,
11
+ processing_config,
12
+ trigger_edge_parameters,
13
+ )
14
+ from pytesprocess.process import TriggerProcessing
15
+ from pytesprocess.process.work_plan import load_plan
16
+
17
+
18
+ # This command builds its own sub-parsers, so the shared acquisition
19
+ # arguments are added per action rather than once at the top level.
20
+ COMMON_ARGS = False
21
+
22
+ _MEASUREMENT_TYPES = ["background", "calibration"]
23
+
24
+
25
+ def _add_shared(parser):
26
+ add_common_acquisition_args(parser, config=True, ncores=False)
27
+ parser.add_argument(
28
+ "--measurement-type",
29
+ default="background",
30
+ choices=_MEASUREMENT_TYPES,
31
+ )
32
+ parser.add_argument(
33
+ "--salting-dataframe",
34
+ help="Existing salting dataframe to inject while triggering.",
35
+ )
36
+ parser.add_argument(
37
+ "--partition-duration", type=float, default=10.0,
38
+ dest="partition_duration",
39
+ )
40
+
41
+
42
+ def configure(parser):
43
+ actions = parser.add_subparsers(dest="action", required=True)
44
+
45
+ plan = actions.add_parser(
46
+ "plan", help="divide the selected streams into trigger work units"
47
+ )
48
+ _add_shared(plan)
49
+ plan.add_argument(
50
+ "--plan-file",
51
+ help="Where to write the plan. Default: plan.json in the new group.",
52
+ )
53
+ plan.add_argument("--campaign", help="Campaign name, recorded for provenance.")
54
+
55
+ run = actions.add_parser("run", help="process exactly one work unit of a plan")
56
+ _add_shared(run)
57
+ run.add_argument("--plan", required=True, help="Plan file to read.")
58
+ run.add_argument(
59
+ "--unit", type=int, required=True, help="1-based unit index to process."
60
+ )
61
+ run.add_argument(
62
+ "--product-group", help="Override the product group recorded in the plan."
63
+ )
64
+ run.add_argument("--ntriggers", type=int, default=-1)
65
+ run.add_argument("--memory-limit", default="2GB")
66
+
67
+
68
+ def run(args):
69
+ if args.action == "plan":
70
+ return _plan(args)
71
+ return _run_unit(args)
72
+
73
+
74
+ def _processor(args, config):
75
+ return TriggerProcessing(
76
+ args.acquisition,
77
+ config,
78
+ streams=args.streams,
79
+ processing_label=args.processing_label,
80
+ restricted=args.restricted,
81
+ data_type=args.measurement_type,
82
+ salting_dataframe=args.salting_dataframe,
83
+ verbose=not args.quiet,
84
+ )
85
+
86
+
87
+ def _plan(args):
88
+ config, catalog = processing_config(
89
+ args, workflows=["trigger"], measurement_types=args.measurement_type
90
+ )
91
+ # Computed once, over the whole selection, and carried in the plan: live
92
+ # time is not a property any single unit can work out for itself.
93
+ edge_msec, livetime = trigger_edge_parameters(config, catalog)
94
+
95
+ proc = _processor(args, config)
96
+ return proc.plan(
97
+ save_path=output_base(args),
98
+ plan_path=args.plan_file,
99
+ campaign=args.campaign,
100
+ edge_exclusion_msec=edge_msec,
101
+ livetime=livetime,
102
+ partition_target_duration_s=args.partition_duration,
103
+ )
104
+
105
+
106
+ def _run_unit(args):
107
+ plan = load_plan(args.plan, step="trigger")
108
+ config, _ = processing_config(
109
+ args, workflows=["trigger"], measurement_types=args.measurement_type
110
+ )
111
+ proc = _processor(args, config)
112
+ return proc.process_unit(
113
+ plan,
114
+ args.unit,
115
+ product_group=args.product_group,
116
+ ntriggers=args.ntriggers,
117
+ memory_limit=args.memory_limit,
118
+ )
@@ -0,0 +1,18 @@
1
+ from pytesprocess.vibration import VibrationProcessing
2
+
3
+
4
+ def configure(parser):
5
+ parser.add_argument("--config", help="Processing YAML file with a 'vibration' section")
6
+ parser.add_argument("--channels", nargs="+")
7
+
8
+
9
+ def run(args):
10
+ proc = VibrationProcessing(
11
+ args.acquisition, config_file=args.config, streams=args.streams,
12
+ restricted=args.restricted, verbose=not args.quiet,
13
+ )
14
+ proc.process(
15
+ channels=args.channels, ncores=args.ncores,
16
+ lgc_output=False, lgc_save=True,
17
+ save_path=args.output, processing_label=args.processing_label,
18
+ )
@@ -7,11 +7,18 @@ import multiprocessing
7
7
  import sys
8
8
 
9
9
  from .common import add_common_acquisition_args
10
- from .commands import event, filter as filter_command, ivsweep
10
+ from .commands import (
11
+ event, feature, filter as filter_command, ivsweep, randoms, trigger,
12
+ vibration,
13
+ )
11
14
 
12
15
  _SECONDARY_COMMANDS = {
16
+ "feature": feature,
13
17
  "filter": filter_command,
14
18
  "ivsweep": ivsweep,
19
+ "randoms": randoms,
20
+ "trigger": trigger,
21
+ "vibration": vibration,
15
22
  }
16
23
 
17
24
 
@@ -20,9 +27,9 @@ def _event_parser():
20
27
  prog="pytesprocess",
21
28
  description=(
22
29
  "TES event-processing workflow. Provide an acquisition and the "
23
- "requested --steps. Separate filter and ivsweep workflows are "
24
- "available as 'pytesprocess filter ...' and "
25
- "'pytesprocess ivsweep ...'."
30
+ "requested --steps. Separate filter, ivsweep and vibration "
31
+ "workflows are available as 'pytesprocess filter ...', "
32
+ "'pytesprocess ivsweep ...' and 'pytesprocess vibration ...'."
26
33
  ),
27
34
  )
28
35
  add_common_acquisition_args(parser, config=True, ncores=True)
@@ -35,11 +42,14 @@ def _secondary_parser(name, module):
35
42
  prog=f"pytesprocess {name}",
36
43
  description=f"pytesprocess {name} workflow",
37
44
  )
38
- add_common_acquisition_args(
39
- parser,
40
- config=(name == "filter"),
41
- ncores=True,
42
- )
45
+ # A command with its own sub-actions adds the shared acquisition
46
+ # arguments to each of them instead of once here.
47
+ if getattr(module, "COMMON_ARGS", True):
48
+ add_common_acquisition_args(
49
+ parser,
50
+ config=(name == "filter"),
51
+ ncores=True,
52
+ )
43
53
  module.configure(parser)
44
54
  return parser
45
55
 
@@ -47,8 +57,8 @@ def _secondary_parser(name, module):
47
57
  def build_parser(command=None):
48
58
  """Build one CLI parser.
49
59
 
50
- ``command=None`` builds the default event-processing parser. ``filter``
51
- and ``ivsweep`` build the two explicit secondary workflow parsers.
60
+ ``command=None`` builds the default event-processing parser. ``filter``,
61
+ ``ivsweep`` and ``vibration`` build the explicit secondary workflow parsers.
52
62
  """
53
63
  if command is None:
54
64
  return _event_parser()
@@ -58,6 +68,7 @@ def build_parser(command=None):
58
68
 
59
69
 
60
70
  def main(argv=None):
71
+ """Return zero after a successful workflow; processing errors propagate."""
61
72
  try:
62
73
  multiprocessing.set_start_method("spawn")
63
74
  except RuntimeError:
@@ -70,12 +81,14 @@ def main(argv=None):
70
81
  module = _SECONDARY_COMMANDS[command]
71
82
  parser = build_parser(command)
72
83
  args = parser.parse_args(args_in)
73
- return module.run(args)
84
+ module.run(args)
85
+ return 0
74
86
 
75
87
  parser = build_parser()
76
88
  args = parser.parse_args(args_in)
77
- return event.run(args)
89
+ event.run(args)
90
+ return 0
78
91
 
79
92
 
80
93
  if __name__ == "__main__":
81
- main()
94
+ sys.exit(main())
@@ -5,7 +5,10 @@ import warnings
5
5
 
6
6
  from .loader import load_processing_yaml
7
7
  from .warnings import ConfigDeprecationWarning
8
- from .resolvers import FeatureConfigResolver, TriggerConfigResolver, SaltingConfigResolver
8
+ from .resolvers import (
9
+ FeatureConfigResolver, TriggerConfigResolver, SaltingConfigResolver,
10
+ VibrationConfigResolver,
11
+ )
9
12
  from .resolvers.common import rename_obsolete_keys
10
13
  from .validation import (
11
14
  normalize_workflows, validate_resolved_workflow,
@@ -20,7 +23,8 @@ _LEGACY_SALTING_OVERALL = {
20
23
  }
21
24
  _RESERVED_TOP_LEVEL = {
22
25
  'config_version', 'resources', 'trigger', 'feature', 'salting', 'filter',
23
- 'ivsweep', 'didv', 'noise', 'template', 'global', 'filter_file', 'didv_file'
26
+ 'ivsweep', 'didv', 'noise', 'template', 'vibration', 'global', 'filter_file',
27
+ 'didv_file'
24
28
  }
25
29
 
26
30
 
@@ -73,13 +77,13 @@ class ProcessingConfig:
73
77
  Returns
74
78
  -------
75
79
  dict
76
- Validation summary suitable for a future CLI startup report.
80
+ Validation summary (also displayed at CLI startup).
77
81
 
78
82
  Notes
79
83
  -----
80
- This method performs no processing and intentionally is not called by
81
- ``scripts/process.py`` yet. It lets a future multi-step CLI validate
82
- all requested workflows before starting the first processing stage.
84
+ This method performs no processing. The CLI workflows call it to
85
+ validate all requested workflows before starting the first
86
+ processing stage.
83
87
  """
84
88
  names = normalize_workflows(workflows)
85
89
  if names is None:
@@ -103,7 +107,7 @@ class ProcessingConfig:
103
107
  def _configured_workflows(self):
104
108
  names = []
105
109
  for name in ('trigger', 'feature', 'salting', 'filter', 'ivsweep',
106
- 'didv', 'noise', 'template'):
110
+ 'didv', 'noise', 'template', 'vibration'):
107
111
  section = self._canonical.get(name, {})
108
112
  if not isinstance(section, dict):
109
113
  continue
@@ -125,7 +129,7 @@ class ProcessingConfig:
125
129
  'global': copy.deepcopy(self._canonical['resources'])
126
130
  }
127
131
  for name in ('trigger', 'feature', 'salting', 'filter', 'ivsweep',
128
- 'didv', 'noise', 'template'):
132
+ 'didv', 'noise', 'template', 'vibration'):
129
133
  out[name] = self.get_config(name)
130
134
  return out
131
135
 
@@ -151,6 +155,10 @@ class ProcessingConfig:
151
155
  self._canonical['salting'], self._canonical['resources'],
152
156
  self._available_channels, version=self._version,
153
157
  ).resolve()
158
+ elif name == 'vibration':
159
+ resolved = VibrationConfigResolver(
160
+ self._canonical['vibration'], self._available_channels,
161
+ ).resolve()
154
162
  elif name in {'filter', 'ivsweep'}:
155
163
  resolved = copy.deepcopy(self._canonical.get(name, {}))
156
164
  elif name in {'didv', 'noise', 'template'}:
@@ -169,7 +177,7 @@ class ProcessingConfig:
169
177
  def _normalize_v2(self, raw):
170
178
  allowed = {
171
179
  'config_version', 'resources', 'trigger', 'feature', 'salting',
172
- 'filter', 'ivsweep', 'didv', 'noise', 'template'
180
+ 'filter', 'ivsweep', 'didv', 'noise', 'template', 'vibration'
173
181
  }
174
182
  unknown = set(raw) - allowed
175
183
  if unknown:
@@ -190,6 +198,7 @@ class ProcessingConfig:
190
198
  'didv': copy.deepcopy(raw.get('didv', {})),
191
199
  'noise': copy.deepcopy(raw.get('noise', {})),
192
200
  'template': copy.deepcopy(raw.get('template', {})),
201
+ 'vibration': copy.deepcopy(raw.get('vibration', {})),
193
202
  }
194
203
 
195
204
  def _normalize_v2_workflow(self, section, name):
@@ -230,6 +239,7 @@ class ProcessingConfig:
230
239
  didv_cfg = work.pop('didv', {}) or {}
231
240
  noise_cfg = work.pop('noise', {}) or {}
232
241
  template_cfg = work.pop('template', {}) or {}
242
+ vibration_cfg = work.pop('vibration', {}) or {}
233
243
  global_feature = work.pop('global', {}) or {}
234
244
  work.pop('config_version', None)
235
245
  work.pop('resources', None)
@@ -262,6 +272,7 @@ class ProcessingConfig:
262
272
  'didv': didv_cfg,
263
273
  'noise': noise_cfg,
264
274
  'template': template_cfg,
275
+ 'vibration': vibration_cfg,
265
276
  }
266
277
 
267
278
  @staticmethod
@@ -1,5 +1,7 @@
1
1
  from .feature import FeatureConfigResolver
2
2
  from .trigger import TriggerConfigResolver
3
3
  from .salting import SaltingConfigResolver
4
+ from .vibration import VibrationConfigResolver
4
5
 
5
- __all__ = ['FeatureConfigResolver', 'TriggerConfigResolver', 'SaltingConfigResolver']
6
+ __all__ = ['FeatureConfigResolver', 'TriggerConfigResolver', 'SaltingConfigResolver',
7
+ 'VibrationConfigResolver']