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,86 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import warnings
5
+
6
+ from ..selectors import expand_channel_selector
7
+ from ..warnings import ConfigDeprecationWarning
8
+ from .common import deep_merge
9
+
10
+
11
+ class SaltingConfigResolver:
12
+ """Resolve salting configuration into the legacy runtime shape.
13
+
14
+ Salt pulse construction depends on a template and, optionally, dPdI. PSD
15
+ and CSD tags were historically threaded through the API but never used by
16
+ the salting calculation; they are now accepted only as deprecated no-ops.
17
+ """
18
+
19
+ def __init__(self, config, resources, available_channels, version=1):
20
+ self._config = copy.deepcopy(config or {})
21
+ self._resources = copy.deepcopy(resources or {})
22
+ self._available_channels = list(available_channels or [])
23
+ self._version = int(version)
24
+
25
+ def resolve(self):
26
+ overall = copy.deepcopy(self._config.get('global', {}))
27
+ for key, value in self._resources.items():
28
+ overall.setdefault(key, value)
29
+ channels = self._config.get('channels', {})
30
+ if not isinstance(channels, dict):
31
+ raise ValueError('ERROR: salting.channels must be a mapping.')
32
+
33
+ assignments = {}
34
+ physical_list = []
35
+ for selector, block in channels.items():
36
+ if not isinstance(block, dict):
37
+ raise ValueError(
38
+ f'ERROR: Salting configuration for "{selector}" must be a mapping.'
39
+ )
40
+ for match in expand_channel_selector(selector, self._available_channels):
41
+ assignments.setdefault(match.target, []).append(
42
+ (match.specificity, selector, copy.deepcopy(block),
43
+ match.physical_channels)
44
+ )
45
+
46
+ resolved = {}
47
+ for target, items in assignments.items():
48
+ items.sort(key=lambda item: item[0])
49
+ merged = {}
50
+ seen = {}
51
+ physical = ()
52
+ for specificity, selector, block, physical_channels in items:
53
+ if specificity in seen:
54
+ raise ValueError(
55
+ f'ERROR: Salting selectors "{seen[specificity]}" and "{selector}" '
56
+ f'both match "{target}" with equal precedence.'
57
+ )
58
+ seen[specificity] = selector
59
+ merged = deep_merge(merged, block)
60
+ physical = physical_channels
61
+ if merged.get('disable', False) or merged.get('run') is False:
62
+ continue
63
+
64
+ # ``noise_tag`` is normalized to ``csd_tag`` by the compatibility
65
+ # loader. Neither was ever consumed by salting. Remove it from the
66
+ # resolved runtime configuration so new code cannot accidentally
67
+ # imply that CSD participates in pulse generation.
68
+ if 'csd_tag' in merged:
69
+ warnings.warn(
70
+ f'Salting configuration "{target}.csd_tag" is deprecated '
71
+ 'and ignored. Salting uses template_tag and optional dPdI.',
72
+ ConfigDeprecationWarning,
73
+ stacklevel=3,
74
+ )
75
+ merged.pop('csd_tag', None)
76
+
77
+ merged.pop('disable', None)
78
+ merged['run'] = True
79
+ resolved[target] = merged
80
+ physical_list.extend(physical)
81
+
82
+ return {
83
+ 'overall': overall,
84
+ 'channels': resolved,
85
+ 'channel_list': list(dict.fromkeys(physical_list)),
86
+ }
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ from ..selectors import expand_channel_selector
5
+ from .common import deep_merge
6
+
7
+
8
+ class TriggerConfigResolver:
9
+ def __init__(self, config, resources, available_channels, version=1):
10
+ self._config = copy.deepcopy(config or {})
11
+ self._resources = copy.deepcopy(resources or {})
12
+ self._available_channels = list(available_channels or [])
13
+ self._version = int(version)
14
+
15
+ def resolve(self):
16
+ overall = copy.deepcopy(self._config.get('global', {}))
17
+ for key, value in self._resources.items():
18
+ overall.setdefault(key, value)
19
+ channels = self._config.get('channels', {})
20
+ if not isinstance(channels, dict):
21
+ raise ValueError('ERROR: trigger.channels must be a mapping.')
22
+
23
+ assignments = {}
24
+ physical_list = []
25
+ for selector, block in channels.items():
26
+ if not isinstance(block, dict):
27
+ raise ValueError(f'ERROR: Trigger configuration for "{selector}" must be a mapping.')
28
+ for match in expand_channel_selector(selector, self._available_channels):
29
+ assignments.setdefault(match.target, []).append((match.specificity, selector,
30
+ copy.deepcopy(block),
31
+ match.physical_channels))
32
+
33
+ merged_channels = {}
34
+ for target, items in assignments.items():
35
+ items.sort(key=lambda item: item[0])
36
+ merged = {}
37
+ seen = {}
38
+ physical = ()
39
+ for specificity, selector, block, physical_channels in items:
40
+ if specificity in seen:
41
+ raise ValueError(
42
+ f'ERROR: Trigger selectors "{seen[specificity]}" and "{selector}" '
43
+ f'both match "{target}" with equal precedence.'
44
+ )
45
+ seen[specificity] = selector
46
+ merged = deep_merge(merged, block)
47
+ physical = physical_channels
48
+ if merged.get('disable', False) or merged.get('run') is False:
49
+ continue
50
+ physical_list.extend(physical)
51
+ merged_channels[target] = merged
52
+
53
+ trigger_channel_dict = {}
54
+ for channel, channel_cfg in merged_channels.items():
55
+ cfg = copy.deepcopy(channel_cfg)
56
+ trigger_channel = cfg.pop('trigger_channel', channel)
57
+
58
+ # One direct trigger config, or named trigger algorithms under channel.
59
+ if 'run' in cfg or not any(isinstance(v, dict) for v in cfg.values()):
60
+ run = cfg.pop('run', True)
61
+ if run:
62
+ # Runtime trigger code still expects an explicit ``run``
63
+ # flag. Public YAML semantics are simpler: omitted means
64
+ # enabled, while only ``run: false`` disables the block.
65
+ cfg['run'] = True
66
+ cfg['channel_name'] = channel
67
+ trigger_channel_dict[trigger_channel] = cfg
68
+ continue
69
+
70
+ for algo, algo_cfg in cfg.items():
71
+ if not isinstance(algo_cfg, dict):
72
+ continue
73
+ algo_cfg = copy.deepcopy(algo_cfg)
74
+ if not algo_cfg.pop('run', True):
75
+ continue
76
+ algo_cfg['run'] = True
77
+ algo_cfg['channel_name'] = channel
78
+ trigger_channel_dict[f'{algo}_{trigger_channel}'] = algo_cfg
79
+
80
+ return {
81
+ 'overall': overall,
82
+ 'channels': trigger_channel_dict,
83
+ 'channel_list': list(dict.fromkeys(physical_list)),
84
+ }
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from fnmatch import fnmatchcase
5
+
6
+
7
+ _COMPOSITE_SEPARATORS = ('|', '+', '-')
8
+ _GLOB_CHARS = ('*', '?', '[')
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class SelectorMatch:
13
+ target: str
14
+ physical_channels: tuple[str, ...]
15
+ specificity: int
16
+ selector: str
17
+
18
+
19
+ def _has_glob(text: str) -> bool:
20
+ return any(char in text for char in _GLOB_CHARS)
21
+
22
+
23
+ def _split_composite(expr: str) -> tuple[list[str], str | None]:
24
+ separators = [sep for sep in _COMPOSITE_SEPARATORS if sep in expr]
25
+ if not separators:
26
+ return [expr], None
27
+ if len(separators) > 1:
28
+ raise ValueError(
29
+ f'ERROR: Channel expression "{expr}" mixes separators {separators}. '
30
+ 'Use one of "|", "+", or "-" per multi-channel expression.'
31
+ )
32
+ sep = separators[0]
33
+ return expr.split(sep), sep
34
+
35
+
36
+ def expand_channel_selector(selector: str, available_channels) -> list[SelectorMatch]:
37
+ """Expand one config selector while preserving composite-channel semantics.
38
+
39
+ Rules:
40
+ * ``all`` selects all physical channels.
41
+ * glob patterns select physical channels only.
42
+ * comma separates independent physical selectors.
43
+ * ``|``, ``+`` and ``-`` denote one exact composite expression and may
44
+ not contain globs or commas.
45
+ """
46
+ if not isinstance(selector, str) or not selector.strip():
47
+ raise ValueError('ERROR: Channel selector must be a non-empty string.')
48
+
49
+ available = [str(chan) for chan in available_channels]
50
+ selector = selector.replace(' ', '')
51
+
52
+ # Multi-channel expressions are exact and never wildcard-expanded.
53
+ parts, sep = _split_composite(selector)
54
+ if sep is not None:
55
+ if ',' in selector:
56
+ raise ValueError(
57
+ f'ERROR: Composite channel expression "{selector}" cannot contain comma.'
58
+ )
59
+ if any(_has_glob(part) for part in parts):
60
+ raise ValueError(
61
+ f'ERROR: Wildcards are not allowed inside composite channel expression '
62
+ f'"{selector}". List the physical channels explicitly.'
63
+ )
64
+ missing = [part for part in parts if part not in available]
65
+ if missing:
66
+ raise ValueError(
67
+ f'ERROR: Unknown channel(s) {missing} in expression "{selector}". '
68
+ f'Available channels = {available}'
69
+ )
70
+ return [SelectorMatch(selector, tuple(parts), 40, selector)]
71
+
72
+ tokens = selector.split(',')
73
+ is_comma_group = len(tokens) > 1
74
+ results: list[SelectorMatch] = []
75
+ seen = set()
76
+
77
+ for token in tokens:
78
+ if not token:
79
+ raise ValueError(f'ERROR: Invalid empty channel in selector "{selector}".')
80
+
81
+ if token == 'all':
82
+ matched = available
83
+ specificity = 0 if not is_comma_group else 20
84
+ elif _has_glob(token):
85
+ matched = [chan for chan in available if fnmatchcase(chan, token)]
86
+ if not matched:
87
+ raise ValueError(
88
+ f'ERROR: Channel wildcard "{token}" did not match any raw channel. '
89
+ f'Available channels = {available}'
90
+ )
91
+ specificity = 10 if not is_comma_group else 20
92
+ else:
93
+ if token not in available:
94
+ raise ValueError(
95
+ f'ERROR: Channel "{token}" not found in raw data. '
96
+ f'Available channels = {available}'
97
+ )
98
+ matched = [token]
99
+ specificity = 30 if not is_comma_group else 20
100
+
101
+ for chan in matched:
102
+ key = (chan, specificity)
103
+ if key in seen:
104
+ continue
105
+ seen.add(key)
106
+ results.append(SelectorMatch(chan, (chan,), specificity, selector))
107
+
108
+ return results
@@ -0,0 +1,314 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+
7
+ _FEATURE_OF_PREFIXES = ('of1x1', 'of1x2x2', 'of1x3x3', 'ofnxm', 'ofnxmx2')
8
+
9
+ _SUPPORTED_WORKFLOWS = {
10
+ 'trigger', 'feature', 'salting', 'filter', 'ivsweep',
11
+ 'didv', 'noise', 'template',
12
+ }
13
+
14
+
15
+ def normalize_workflows(workflows):
16
+ if workflows is None:
17
+ return None
18
+ if isinstance(workflows, str):
19
+ workflows = [workflows]
20
+ names = []
21
+ for workflow in workflows:
22
+ name = str(workflow).strip().lower()
23
+ if name not in _SUPPORTED_WORKFLOWS:
24
+ raise ValueError(
25
+ f'ERROR: Unknown processing workflow "{workflow}". '
26
+ f'Available workflows = {sorted(_SUPPORTED_WORKFLOWS)}'
27
+ )
28
+ if name not in names:
29
+ names.append(name)
30
+ return names
31
+
32
+
33
+ def validate_resolved_workflow(name, config, check_resources=False):
34
+ """Validate one already-resolved workflow configuration.
35
+
36
+ Validation here checks the contract between the configuration layer and
37
+ processing classes. It intentionally does not read raw records, open
38
+ FilterData contents, or run algorithms; those remain runtime checks.
39
+ """
40
+ if not isinstance(config, dict):
41
+ raise ValueError(
42
+ f'ERROR: Resolved {name} configuration must be a mapping, '
43
+ f'got {type(config).__name__}.'
44
+ )
45
+
46
+ if name == 'trigger':
47
+ return _validate_trigger(config, check_resources=check_resources)
48
+ if name == 'feature':
49
+ return _validate_feature(config, check_resources=check_resources)
50
+ if name == 'salting':
51
+ return _validate_salting(config, check_resources=check_resources)
52
+
53
+ # FilterDataProcessing and IVSweepProcessing currently own their more
54
+ # specialized validation. The configuration manager still verifies that
55
+ # the section has the expected mapping type so a future CLI can validate
56
+ # all requested workflow sections through one entry point.
57
+ return {
58
+ 'configured_items': len(config),
59
+ }
60
+
61
+
62
+ def _require_mapping(config, key, workflow):
63
+ value = config.get(key, {})
64
+ if not isinstance(value, dict):
65
+ raise ValueError(
66
+ f'ERROR: Resolved {workflow}.{key} configuration must be a mapping.'
67
+ )
68
+ return value
69
+
70
+
71
+ def _require_filter_file(overall, workflow, check_resources=False):
72
+ filter_file = overall.get('filter_file')
73
+ if filter_file is None or not str(filter_file).strip():
74
+ raise ValueError(
75
+ f'ERROR: Filter file missing for {workflow} processing. '
76
+ 'Define resources.filter_file (or legacy filter_file).'
77
+ )
78
+ if check_resources:
79
+ path = Path(str(filter_file)).expanduser()
80
+ if not path.is_file():
81
+ raise ValueError(
82
+ f'ERROR: Filter file "{filter_file}" required by {workflow} '
83
+ 'processing does not exist.'
84
+ )
85
+ return str(filter_file)
86
+
87
+
88
+ def _validate_trigger(config, check_resources=False):
89
+ overall = _require_mapping(config, 'overall', 'trigger')
90
+ channels = _require_mapping(config, 'channels', 'trigger')
91
+ channel_list = config.get('channel_list', [])
92
+ if not isinstance(channel_list, list):
93
+ raise ValueError('ERROR: Resolved trigger.channel_list must be a list.')
94
+ if not channels:
95
+ raise ValueError(
96
+ 'ERROR: Trigger processing was requested but no trigger channels '
97
+ 'are enabled in the configuration.'
98
+ )
99
+ if not channel_list:
100
+ raise ValueError(
101
+ 'ERROR: Trigger processing was requested but no physical raw '
102
+ 'channels were selected.'
103
+ )
104
+
105
+ for name, block in channels.items():
106
+ if not isinstance(block, dict):
107
+ raise ValueError(
108
+ f'ERROR: Resolved trigger configuration "{name}" must be a mapping.'
109
+ )
110
+ if block.get('run') is not True:
111
+ raise ValueError(
112
+ f'ERROR: Resolved trigger configuration "{name}" does not have '
113
+ 'run: true. Disabled trigger blocks should have been removed by '
114
+ 'the resolver.'
115
+ )
116
+ if not block.get('channel_name'):
117
+ raise ValueError(
118
+ f'ERROR: Resolved trigger configuration "{name}" is missing '
119
+ 'channel_name.'
120
+ )
121
+
122
+ filter_file = _require_filter_file(
123
+ overall, 'trigger', check_resources=check_resources)
124
+ return {
125
+ 'configured_channels': len(channels),
126
+ 'physical_channels': len(channel_list),
127
+ 'filter_file': filter_file,
128
+ }
129
+
130
+
131
+ def _validate_feature(config, check_resources=False):
132
+ overall = _require_mapping(config, 'overall', 'feature')
133
+ channels = _require_mapping(config, 'channels', 'feature')
134
+ channel_list = config.get('channel_list', [])
135
+ if not isinstance(channel_list, list):
136
+ raise ValueError('ERROR: Resolved feature.channel_list must be a list.')
137
+ if not channels:
138
+ raise ValueError(
139
+ 'ERROR: Feature processing was requested but no feature channels '
140
+ 'or channel expressions are enabled in the configuration.'
141
+ )
142
+ if not channel_list:
143
+ raise ValueError(
144
+ 'ERROR: Feature processing was requested but no physical raw '
145
+ 'channels were selected.'
146
+ )
147
+
148
+ algorithm_count = 0
149
+ trace_configs = set()
150
+ requires_filter_file = False
151
+ for channel, block in channels.items():
152
+ if not isinstance(block, dict):
153
+ raise ValueError(
154
+ f'ERROR: Resolved feature configuration for "{channel}" '
155
+ 'must be a mapping.'
156
+ )
157
+ for algorithm, params in block.items():
158
+ if not isinstance(params, dict):
159
+ # feature_channel, weight_*, and other channel-level metadata
160
+ # intentionally remain non-dict entries.
161
+ continue
162
+ algorithm_count += 1
163
+ base_algorithm = params.get('base_algorithm', algorithm)
164
+ if str(base_algorithm).startswith(_FEATURE_OF_PREFIXES):
165
+ requires_filter_file = True
166
+ if params.get('run') is not True:
167
+ raise ValueError(
168
+ f'ERROR: Resolved feature algorithm "{channel}/{algorithm}" '
169
+ 'does not have run: true. Disabled algorithms should have '
170
+ 'been removed by the resolver.'
171
+ )
172
+ if 'nb_samples' not in params or 'nb_pretrigger_samples' not in params:
173
+ raise ValueError(
174
+ f'ERROR: Resolved feature algorithm "{channel}/{algorithm}" '
175
+ 'is missing trace-length fields.'
176
+ )
177
+ nb_samples = params['nb_samples']
178
+ nb_pre = params['nb_pretrigger_samples']
179
+ if (nb_samples is None) != (nb_pre is None):
180
+ raise ValueError(
181
+ f'ERROR: Trace length and pretrigger length must either both '
182
+ f'be set or both be omitted for "{channel}/{algorithm}".'
183
+ )
184
+ if nb_samples is not None:
185
+ if nb_samples <= 0 or nb_pre < 0 or nb_pre > nb_samples:
186
+ raise ValueError(
187
+ f'ERROR: Invalid resolved trace/pretrigger lengths for '
188
+ f'"{channel}/{algorithm}": {nb_samples}/{nb_pre} samples.'
189
+ )
190
+ trace_configs.add((int(nb_samples), int(nb_pre)))
191
+
192
+ if algorithm_count == 0:
193
+ raise ValueError(
194
+ 'ERROR: Feature processing was requested but no feature algorithms '
195
+ 'are enabled in the configuration.'
196
+ )
197
+
198
+ filter_file = overall.get('filter_file')
199
+ if requires_filter_file:
200
+ filter_file = _require_filter_file(
201
+ overall, 'feature', check_resources=check_resources)
202
+ elif check_resources and filter_file:
203
+ path = Path(str(filter_file)).expanduser()
204
+ if not path.is_file():
205
+ raise ValueError(
206
+ f'ERROR: Optional feature filter file "{filter_file}" does not exist.'
207
+ )
208
+
209
+ return {
210
+ 'configured_channels': len(channels),
211
+ 'physical_channels': len(channel_list),
212
+ 'algorithms': algorithm_count,
213
+ 'trace_configurations': len(trace_configs),
214
+ 'filter_file': filter_file,
215
+ 'requires_filter_file': requires_filter_file,
216
+ }
217
+
218
+
219
+ def _validate_salting(config, check_resources=False):
220
+ overall = _require_mapping(config, 'overall', 'salting')
221
+ channels = _require_mapping(config, 'channels', 'salting')
222
+ channel_list = config.get('channel_list', [])
223
+ if not isinstance(channel_list, list):
224
+ raise ValueError('ERROR: Resolved salting.channel_list must be a list.')
225
+ if not channels:
226
+ raise ValueError(
227
+ 'ERROR: Salting was requested but no salting channels or channel '
228
+ 'expressions are enabled in the configuration.'
229
+ )
230
+ if not channel_list:
231
+ raise ValueError(
232
+ 'ERROR: Salting was requested but no physical raw channels were selected.'
233
+ )
234
+ for channel, block in channels.items():
235
+ if not isinstance(block, dict):
236
+ raise ValueError(
237
+ f'ERROR: Resolved salting configuration for "{channel}" '
238
+ 'must be a mapping.'
239
+ )
240
+ if block.get('run') is not True:
241
+ raise ValueError(
242
+ f'ERROR: Resolved salting configuration "{channel}" does not '
243
+ 'have run: true. Disabled blocks should have been removed by '
244
+ 'the resolver.'
245
+ )
246
+ if not block.get('template_tag'):
247
+ raise ValueError(
248
+ f'ERROR: Salting channel "{channel}" requires template_tag.'
249
+ )
250
+ dpdi_tag = block.get('dpdi_tag')
251
+ dpdi_poles = block.get('dpdi_poles')
252
+ if (dpdi_tag is None) != (dpdi_poles is None):
253
+ raise ValueError(
254
+ f'ERROR: Salting channel "{channel}" requires dpdi_tag and '
255
+ 'dpdi_poles to be both set or both omitted.'
256
+ )
257
+ if 'csd_tag' in block or 'noise_tag' in block:
258
+ raise ValueError(
259
+ f'ERROR: Resolved salting channel "{channel}" still contains '
260
+ 'a noise/CSD tag. These are not used by salting.'
261
+ )
262
+
263
+ if overall.get('energies') is not None and overall.get('dm_pdf_file') is not None:
264
+ raise ValueError(
265
+ 'ERROR: Salting configuration must use fixed energies or dm_pdf_file, not both.'
266
+ )
267
+ if overall.get('nsalt') is not None and int(overall['nsalt']) <= 0:
268
+ raise ValueError('ERROR: salting.global.nsalt must be positive.')
269
+ if overall.get('random_seed') is not None:
270
+ try:
271
+ int(overall['random_seed'])
272
+ except (TypeError, ValueError):
273
+ raise ValueError('ERROR: salting.global.random_seed must be an integer or null.')
274
+
275
+ filter_file = _require_filter_file(
276
+ overall, 'salting', check_resources=check_resources)
277
+ if check_resources and overall.get('didv_file'):
278
+ didv_file = Path(str(overall['didv_file'])).expanduser()
279
+ if not didv_file.is_file():
280
+ raise ValueError(
281
+ f'ERROR: dIdV file "{overall["didv_file"]}" required by salting '
282
+ 'does not exist.'
283
+ )
284
+ if check_resources and overall.get('dm_pdf_file'):
285
+ pdf_file = Path(str(overall['dm_pdf_file'])).expanduser()
286
+ if not pdf_file.is_file():
287
+ raise ValueError(
288
+ f'ERROR: DM PDF file "{overall["dm_pdf_file"]}" required by '
289
+ 'salting does not exist.'
290
+ )
291
+ return {
292
+ 'configured_channels': len(channels),
293
+ 'physical_channels': len(channel_list),
294
+ 'filter_file': filter_file,
295
+ }
296
+
297
+
298
+ def format_validation_summary(summary):
299
+ lines = ['Configuration validation:']
300
+ for workflow, info in summary.get('workflows', {}).items():
301
+ details = []
302
+ if 'configured_channels' in info:
303
+ details.append(f'{info["configured_channels"]} configured channel(s)')
304
+ if 'physical_channels' in info:
305
+ details.append(f'{info["physical_channels"]} physical channel(s)')
306
+ if 'algorithms' in info:
307
+ details.append(f'{info["algorithms"]} algorithm(s)')
308
+ if 'trace_configurations' in info:
309
+ details.append(f'{info["trace_configurations"]} trace configuration(s)')
310
+ suffix = f' ({", ".join(details)})' if details else ''
311
+ lines.append(f' {workflow:<10} OK{suffix}')
312
+ if not summary.get('workflows'):
313
+ lines.append(' no workflows requested')
314
+ return '\n'.join(lines)
@@ -0,0 +1,2 @@
1
+ class ConfigDeprecationWarning(FutureWarning):
2
+ """Visible warning for deprecated processing-configuration syntax."""
@@ -0,0 +1,10 @@
1
+ from .algorithms import *
2
+ from .oftrigger import *
3
+ from .eventbuilder import *
4
+ from .filterdata import *
5
+ from .noise import *
6
+ from .template import *
7
+ from .ivsweep import *
8
+ from .didv import *
9
+ from .noisemodel import *
10
+ from .filterbuilder import *