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.
- pytesprocess/__init__.py +9 -0
- pytesprocess/_version.py +2 -0
- pytesprocess/cli/__init__.py +1 -0
- pytesprocess/cli/commands/__init__.py +5 -0
- pytesprocess/cli/commands/event.py +66 -0
- pytesprocess/cli/commands/filter.py +17 -0
- pytesprocess/cli/commands/ivsweep.py +29 -0
- pytesprocess/cli/common.py +86 -0
- pytesprocess/cli/main.py +81 -0
- pytesprocess/config/__init__.py +4 -0
- pytesprocess/config/loader.py +94 -0
- pytesprocess/config/manager.py +297 -0
- pytesprocess/config/resolvers/__init__.py +5 -0
- pytesprocess/config/resolvers/common.py +56 -0
- pytesprocess/config/resolvers/feature.py +293 -0
- pytesprocess/config/resolvers/salting.py +86 -0
- pytesprocess/config/resolvers/trigger.py +84 -0
- pytesprocess/config/selectors.py +108 -0
- pytesprocess/config/validation.py +314 -0
- pytesprocess/config/warnings.py +2 -0
- pytesprocess/core/__init__.py +10 -0
- pytesprocess/core/algorithms.py +1455 -0
- pytesprocess/core/didv.py +1648 -0
- pytesprocess/core/eventbuilder.py +495 -0
- pytesprocess/core/filterbuilder.py +81 -0
- pytesprocess/core/filterdata.py +1849 -0
- pytesprocess/core/ivsweep.py +2072 -0
- pytesprocess/core/noise.py +923 -0
- pytesprocess/core/noisemodel.py +1408 -0
- pytesprocess/core/oftrigger.py +1035 -0
- pytesprocess/core/template.py +450 -0
- pytesprocess/process/__init__.py +6 -0
- pytesprocess/process/data_source.py +185 -0
- pytesprocess/process/event_context.py +35 -0
- pytesprocess/process/feature_plan.py +186 -0
- pytesprocess/process/feature_resources.py +267 -0
- pytesprocess/process/features.py +1024 -0
- pytesprocess/process/filterprocess.py +1176 -0
- pytesprocess/process/ivprocess.py +1380 -0
- pytesprocess/process/processing_data.py +967 -0
- pytesprocess/process/randoms.py +921 -0
- pytesprocess/process/triggers.py +1011 -0
- pytesprocess/salting/__init__.py +7 -0
- pytesprocess/salting/generator.py +364 -0
- pytesprocess/salting/injector.py +329 -0
- pytesprocess/salting/sampling.py +84 -0
- pytesprocess/utils/__init__.py +5 -0
- pytesprocess/utils/arg_utils.py +122 -0
- pytesprocess/utils/dataframe_output.py +120 -0
- pytesprocess/utils/filter_hdf5.py +594 -0
- pytesprocess/utils/utils.py +701 -0
- pytesprocess/workflows/__init__.py +3 -0
- pytesprocess/workflows/processing.py +317 -0
- pytesprocess/workflows/salting.py +133 -0
- pytesprocess-0.1.1.dist-info/METADATA +211 -0
- pytesprocess-0.1.1.dist-info/RECORD +60 -0
- pytesprocess-0.1.1.dist-info/WHEEL +5 -0
- pytesprocess-0.1.1.dist-info/entry_points.txt +2 -0
- pytesprocess-0.1.1.dist-info/licenses/LICENSE +21 -0
- 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,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 *
|