spectre-core 0.0.11__py3-none-any.whl → 0.0.13__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.
- spectre_core/_file_io/__init__.py +1 -3
- spectre_core/_file_io/file_handlers.py +170 -65
- spectre_core/batches/__init__.py +21 -0
- spectre_core/batches/_base.py +238 -0
- spectre_core/batches/_batches.py +247 -0
- spectre_core/batches/_factory.py +69 -0
- spectre_core/batches/_register.py +30 -0
- spectre_core/batches/plugins/_batch_keys.py +16 -0
- spectre_core/batches/plugins/_callisto.py +183 -0
- spectre_core/batches/plugins/_iq_stream.py +354 -0
- spectre_core/capture_configs/__init__.py +17 -13
- spectre_core/capture_configs/_capture_config.py +93 -34
- spectre_core/capture_configs/_capture_modes.py +22 -0
- spectre_core/capture_configs/_capture_templates.py +207 -122
- spectre_core/capture_configs/_parameters.py +116 -46
- spectre_core/capture_configs/_pconstraints.py +86 -35
- spectre_core/capture_configs/_pnames.py +49 -0
- spectre_core/capture_configs/_ptemplates.py +389 -346
- spectre_core/capture_configs/_pvalidators.py +121 -77
- spectre_core/config/__init__.py +7 -9
- spectre_core/config/_paths.py +66 -26
- spectre_core/config/_time_formats.py +15 -8
- spectre_core/exceptions.py +2 -4
- spectre_core/jobs/__init__.py +14 -0
- spectre_core/jobs/_jobs.py +111 -0
- spectre_core/jobs/_workers.py +171 -0
- spectre_core/logs/__init__.py +17 -0
- spectre_core/logs/_configure.py +67 -0
- spectre_core/logs/_decorators.py +33 -0
- spectre_core/logs/_logs.py +228 -0
- spectre_core/logs/_process_types.py +14 -0
- spectre_core/plotting/__init__.py +4 -2
- spectre_core/plotting/_base.py +204 -102
- spectre_core/plotting/_format.py +17 -4
- spectre_core/plotting/_panel_names.py +18 -0
- spectre_core/plotting/_panel_stack.py +167 -53
- spectre_core/plotting/_panels.py +341 -141
- spectre_core/post_processing/__init__.py +8 -6
- spectre_core/post_processing/_base.py +71 -45
- spectre_core/post_processing/_factory.py +42 -12
- spectre_core/post_processing/_post_processor.py +27 -29
- spectre_core/post_processing/_register.py +22 -6
- spectre_core/post_processing/plugins/_event_handler_keys.py +16 -0
- spectre_core/post_processing/plugins/_fixed_center_frequency.py +129 -0
- spectre_core/post_processing/plugins/_swept_center_frequency.py +439 -0
- spectre_core/py.typed +0 -0
- spectre_core/receivers/__init__.py +10 -7
- spectre_core/receivers/_base.py +220 -69
- spectre_core/receivers/_factory.py +53 -7
- spectre_core/receivers/_register.py +30 -9
- spectre_core/receivers/_spec_names.py +26 -15
- spectre_core/receivers/plugins/__init__.py +0 -0
- spectre_core/receivers/plugins/_receiver_names.py +16 -0
- spectre_core/receivers/plugins/_rsp1a.py +59 -0
- spectre_core/receivers/plugins/_rspduo.py +67 -0
- spectre_core/receivers/plugins/_sdrplay_receiver.py +190 -0
- spectre_core/receivers/plugins/_test.py +218 -0
- spectre_core/receivers/plugins/gr/_base.py +80 -0
- spectre_core/receivers/{gr → plugins/gr}/_rsp1a.py +45 -55
- spectre_core/receivers/{gr → plugins/gr}/_rspduo.py +65 -78
- spectre_core/receivers/{gr → plugins/gr}/_test.py +36 -34
- spectre_core/spectrograms/__init__.py +5 -3
- spectre_core/spectrograms/_analytical.py +121 -72
- spectre_core/spectrograms/_array_operations.py +103 -36
- spectre_core/spectrograms/_spectrogram.py +410 -203
- spectre_core/spectrograms/_transform.py +199 -188
- spectre_core/wgetting/__init__.py +4 -2
- spectre_core/wgetting/_callisto.py +178 -127
- {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/METADATA +14 -7
- spectre_core-0.0.13.dist-info/RECORD +75 -0
- {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/WHEEL +1 -1
- spectre_core/chunks/__init__.py +0 -22
- spectre_core/chunks/_base.py +0 -116
- spectre_core/chunks/_chunks.py +0 -200
- spectre_core/chunks/_factory.py +0 -25
- spectre_core/chunks/_register.py +0 -15
- spectre_core/chunks/library/_callisto.py +0 -98
- spectre_core/chunks/library/_fixed_center_frequency.py +0 -128
- spectre_core/chunks/library/_swept_center_frequency.py +0 -103
- spectre_core/logging/__init__.py +0 -11
- spectre_core/logging/_configure.py +0 -35
- spectre_core/logging/_decorators.py +0 -19
- spectre_core/logging/_log_handlers.py +0 -176
- spectre_core/post_processing/library/_fixed_center_frequency.py +0 -115
- spectre_core/post_processing/library/_swept_center_frequency.py +0 -382
- spectre_core/receivers/gr/_base.py +0 -33
- spectre_core/receivers/library/_rsp1a.py +0 -61
- spectre_core/receivers/library/_rspduo.py +0 -69
- spectre_core/receivers/library/_sdrplay_receiver.py +0 -185
- spectre_core/receivers/library/_test.py +0 -221
- spectre_core-0.0.11.dist-info/RECORD +0 -64
- /spectre_core/receivers/{gr → plugins/gr}/__init__.py +0 -0
- {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/LICENSE +0 -0
- {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/top_level.txt +0 -0
@@ -2,46 +2,64 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
-
from logging import getLogger
|
6
|
-
_LOGGER = getLogger(__name__)
|
7
|
-
|
8
|
-
from dataclasses import dataclass
|
9
5
|
from math import floor
|
6
|
+
from typing import Optional, cast
|
7
|
+
|
10
8
|
from scipy.signal import get_window
|
11
|
-
from numbers import Number
|
12
|
-
from warnings import warn
|
13
|
-
from typing import Optional
|
14
9
|
|
15
10
|
from ._parameters import Parameters
|
16
|
-
from .
|
11
|
+
from ._pnames import PName
|
17
12
|
|
13
|
+
# ----------------------------------------------------------------------- #
|
14
|
+
# Throughout this module, repeated calls to `cast` will be seen on using the
|
15
|
+
# `get_parameter_value` method. This is purely to signify intent and keep
|
16
|
+
# the static type checkers happy.
|
17
|
+
#
|
18
|
+
# This is okay, as whenever the validators are called, the parameter values
|
19
|
+
# have already been individually casted and constrained according to the
|
20
|
+
# parameter templates. There is negligible runtime impact. Though a bit
|
21
|
+
# of a pain for maintenance.
|
22
|
+
# ----------------------------------------------------------------------- #
|
18
23
|
|
19
|
-
|
24
|
+
|
25
|
+
def validate_window(
|
20
26
|
parameters: Parameters
|
21
27
|
) -> None:
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
28
|
+
"""Ensure that the capture config describes a valid window.
|
29
|
+
|
30
|
+
:param parameters: The parameters to be validated.
|
31
|
+
:raises ValueError: If the window interval is greater than the batch size.
|
32
|
+
:raises ValueError: If the specified window type cannot be fetched using
|
33
|
+
the SciPy `get_window` function.
|
34
|
+
"""
|
35
|
+
window_size = cast(int, parameters.get_parameter_value(PName.WINDOW_SIZE))
|
36
|
+
sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
|
37
|
+
batch_size = cast(int, parameters.get_parameter_value(PName.BATCH_SIZE))
|
38
|
+
window_type = cast(str, parameters.get_parameter_value(PName.WINDOW_TYPE))
|
26
39
|
|
27
40
|
window_interval = window_size*(1 / sample_rate)
|
28
41
|
if window_interval > batch_size:
|
29
|
-
raise ValueError((f"The windowing interval must be strictly less than the
|
42
|
+
raise ValueError((f"The windowing interval must be strictly less than the batch size. "
|
30
43
|
f"Computed the windowing interval to be {window_interval} [s], "
|
31
|
-
f"but the
|
44
|
+
f"but the batch size is {batch_size} [s]"))
|
32
45
|
|
33
46
|
try:
|
34
47
|
_ = get_window(window_type, window_size)
|
35
48
|
except Exception as e:
|
36
|
-
raise
|
37
|
-
|
49
|
+
raise ValueError((f"An error has occurred while validating the window. "
|
50
|
+
f"Got {str(e)}"))
|
38
51
|
|
39
52
|
|
40
|
-
def
|
53
|
+
def validate_nyquist_criterion(
|
41
54
|
parameters: Parameters
|
42
55
|
) -> None:
|
43
|
-
|
44
|
-
|
56
|
+
"""Ensure that the Nyquist criterion is satisfied.
|
57
|
+
|
58
|
+
:param parameters: The parameters to be validated.
|
59
|
+
:raises ValueError: If the sample rate is less than the bandwidth.
|
60
|
+
"""
|
61
|
+
sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
|
62
|
+
bandwidth = cast(float, parameters.get_parameter_value(PName.BANDWIDTH))
|
45
63
|
|
46
64
|
if sample_rate < bandwidth:
|
47
65
|
raise ValueError((f"Nyquist criterion has not been satisfied. "
|
@@ -51,56 +69,76 @@ def _validate_nyquist_criterion(
|
|
51
69
|
|
52
70
|
def _compute_num_steps_per_sweep(min_freq: float,
|
53
71
|
max_freq: float,
|
54
|
-
samp_rate: int,
|
55
72
|
freq_step: float) -> int:
|
56
|
-
|
73
|
+
"""Compute the number of steps in one frequency sweep.
|
57
74
|
|
75
|
+
The center frequency starts at `min_freq` and increments in steps of `freq_step`
|
76
|
+
until the next step would exceed `max_freq`.
|
58
77
|
|
59
|
-
|
78
|
+
:param min_freq: The minimum frequency of the sweep.
|
79
|
+
:param max_freq: The maximum frequency of the sweep.
|
80
|
+
:param freq_step: The frequency step size.
|
81
|
+
:return: The number of steps in one frequency sweep.
|
82
|
+
"""
|
83
|
+
return floor((max_freq - min_freq) / freq_step)
|
84
|
+
|
85
|
+
|
86
|
+
def validate_num_steps_per_sweep(
|
60
87
|
parameters: Parameters
|
61
88
|
) -> None:
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
89
|
+
"""Ensure that there are at least two steps in frequency per sweep.
|
90
|
+
|
91
|
+
:param parameters: The parameters to be validated.
|
92
|
+
:raises ValueError: If the number of steps per sweep is less than or equal to one.
|
93
|
+
"""
|
94
|
+
min_freq = cast(float, parameters.get_parameter_value(PName.MIN_FREQUENCY))
|
95
|
+
max_freq = cast(float, parameters.get_parameter_value(PName.MAX_FREQUENCY))
|
96
|
+
freq_step = cast(float, parameters.get_parameter_value(PName.FREQUENCY_STEP))
|
66
97
|
|
67
98
|
num_steps_per_sweep = _compute_num_steps_per_sweep(min_freq,
|
68
99
|
max_freq,
|
69
|
-
sample_rate,
|
70
100
|
freq_step)
|
71
101
|
if num_steps_per_sweep <= 1:
|
72
102
|
raise ValueError((f"We need strictly greater than one step per sweep. "
|
73
103
|
f"Computed {num_steps_per_sweep} step per sweep"))
|
74
104
|
|
75
105
|
|
76
|
-
def
|
106
|
+
def validate_sweep_interval(
|
77
107
|
parameters: Parameters
|
78
108
|
) -> None:
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
109
|
+
"""Ensure that the sweep interval is greater than the batch size.
|
110
|
+
|
111
|
+
:param parameters: The parameters to be validated.
|
112
|
+
:raises ValueError: If the sweep interval is greater than the batch size.
|
113
|
+
"""
|
114
|
+
min_freq = cast(float, parameters.get_parameter_value(PName.MIN_FREQUENCY))
|
115
|
+
max_freq = cast(float, parameters.get_parameter_value(PName.MAX_FREQUENCY))
|
116
|
+
freq_step = cast(float, parameters.get_parameter_value(PName.FREQUENCY_STEP))
|
117
|
+
samples_per_step = cast(int, parameters.get_parameter_value(PName.SAMPLES_PER_STEP))
|
118
|
+
batch_size = cast(int, parameters.get_parameter_value(PName.BATCH_SIZE))
|
119
|
+
sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
|
85
120
|
|
86
121
|
num_steps_per_sweep = _compute_num_steps_per_sweep(min_freq,
|
87
122
|
max_freq,
|
88
|
-
sample_rate,
|
89
123
|
freq_step)
|
90
124
|
num_samples_per_sweep = num_steps_per_sweep * samples_per_step
|
91
125
|
sweep_interval = num_samples_per_sweep * 1/sample_rate
|
92
126
|
if sweep_interval > batch_size:
|
93
|
-
raise ValueError((f"Sweep interval must be less than the
|
127
|
+
raise ValueError((f"Sweep interval must be less than the batch size. "
|
94
128
|
f"The computed sweep interval is {sweep_interval} [s], "
|
95
|
-
f"but the given
|
129
|
+
f"but the given batch size is {batch_size} [s]"))
|
96
130
|
|
97
131
|
|
98
|
-
def
|
132
|
+
def validate_num_samples_per_step(
|
99
133
|
parameters: Parameters
|
100
134
|
) -> None:
|
135
|
+
"""Ensure that the number of samples per step is greater than the window size.
|
101
136
|
|
102
|
-
|
103
|
-
|
137
|
+
:param parameters: The parameters to be validated.
|
138
|
+
:raises ValueError: If the window size is greater than the number of samples per step.
|
139
|
+
"""
|
140
|
+
window_size = cast(int, parameters.get_parameter_value(PName.WINDOW_SIZE))
|
141
|
+
samples_per_step = cast(int, parameters.get_parameter_value(PName.SAMPLES_PER_STEP))
|
104
142
|
|
105
143
|
if window_size >= samples_per_step:
|
106
144
|
raise ValueError((f"Window size must be strictly less than the number of samples per step. "
|
@@ -108,12 +146,17 @@ def _validate_num_samples_per_step(
|
|
108
146
|
f"to the number of samples per step {samples_per_step}"))
|
109
147
|
|
110
148
|
|
111
|
-
def
|
149
|
+
def validate_non_overlapping_steps(
|
112
150
|
parameters: Parameters
|
113
151
|
) -> None:
|
152
|
+
"""Ensure that the stepped spectrograms are non-overlapping in the frequency domain.
|
153
|
+
|
154
|
+
:param parameters: The parameters to be validated.
|
155
|
+
:raises NotImplementedError: If the spectrograms overlap in the frequency domain.
|
156
|
+
"""
|
114
157
|
|
115
|
-
freq_step
|
116
|
-
sample_rate = parameters.get_parameter_value(
|
158
|
+
freq_step = cast(float, parameters.get_parameter_value(PName.FREQUENCY_STEP))
|
159
|
+
sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
|
117
160
|
|
118
161
|
if freq_step < sample_rate:
|
119
162
|
raise NotImplementedError(f"SPECTRE does not yet support spectral steps overlapping in frequency. "
|
@@ -121,51 +164,52 @@ def _validate_non_overlapping_steps(
|
|
121
164
|
f"rate {sample_rate * 1e-6} [MHz]")
|
122
165
|
|
123
166
|
|
124
|
-
def
|
167
|
+
def validate_step_interval(
|
125
168
|
parameters: Parameters,
|
126
|
-
|
169
|
+
api_retuning_latency: float
|
127
170
|
) -> None:
|
128
|
-
|
129
|
-
|
130
|
-
|
171
|
+
"""Ensure that the time elapsed collecting samples at a fixed frequency is greater
|
172
|
+
than the empirically derived API retuning latency.
|
173
|
+
|
174
|
+
:param parameters: The parameters to be validated.
|
175
|
+
:param api_retuning_latency: The empirically derived API retuning latency (in seconds).
|
176
|
+
:raises ValueError: If the time elapsed for a step is less than the API retuning latency.
|
177
|
+
"""
|
178
|
+
samples_per_step = cast(int, parameters.get_parameter_value(PName.SAMPLES_PER_STEP))
|
179
|
+
sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
|
131
180
|
|
132
181
|
step_interval = samples_per_step * 1/ sample_rate # [s]
|
133
|
-
if step_interval <
|
182
|
+
if step_interval < api_retuning_latency:
|
134
183
|
raise ValueError(f"The computed step interval of {step_interval} [s] is of the order of empirically "
|
135
|
-
f"derived api latency {
|
184
|
+
f"derived api latency {api_retuning_latency} [s]; you may experience undefined behaviour!")
|
136
185
|
|
137
186
|
|
138
|
-
def
|
187
|
+
def validate_fixed_center_frequency(
|
139
188
|
parameters: Parameters
|
140
189
|
) -> None:
|
141
|
-
|
142
|
-
_validate_window(parameters)
|
190
|
+
"""Apply validators for capture config parameters describing fixed center frequency capture.
|
143
191
|
|
192
|
+
:param parameters: The parameters to be validated.
|
193
|
+
"""
|
194
|
+
validate_nyquist_criterion(parameters)
|
195
|
+
validate_window(parameters)
|
144
196
|
|
145
|
-
|
197
|
+
|
198
|
+
def validate_swept_center_frequency(
|
146
199
|
parameters: Parameters,
|
147
|
-
api_retuning_latency: Optional[
|
200
|
+
api_retuning_latency: Optional[float] = None,
|
148
201
|
) -> None:
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
202
|
+
"""Apply validators for capture config parameters describing swept center frequency capture.
|
203
|
+
|
204
|
+
:param parameters: The parameters to be validated.
|
205
|
+
:param api_retuning_latency: The empirically derived API retuning latency. Defaults to None.
|
206
|
+
"""
|
207
|
+
validate_nyquist_criterion(parameters)
|
208
|
+
validate_window(parameters)
|
209
|
+
validate_non_overlapping_steps(parameters)
|
210
|
+
validate_num_steps_per_sweep(parameters)
|
211
|
+
validate_num_samples_per_step(parameters)
|
212
|
+
validate_sweep_interval(parameters)
|
155
213
|
|
156
214
|
if api_retuning_latency is not None:
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
@dataclass(frozen=True)
|
161
|
-
class PValidators:
|
162
|
-
window = _validate_window
|
163
|
-
nyquist_criterion = _validate_nyquist_criterion
|
164
|
-
step_interval = _validate_step_interval
|
165
|
-
non_overlapping_steps = _validate_non_overlapping_steps
|
166
|
-
num_steps_per_sweep = _validate_num_steps_per_sweep
|
167
|
-
num_samples_per_step = _validate_num_samples_per_step
|
168
|
-
sweep_interval = _validate_sweep_interval
|
169
|
-
step_interval = _validate_step_interval
|
170
|
-
fixed_center_frequency = _validate_fixed_center_frequency_parameters
|
171
|
-
swept_center_frequency = _validate_swept_center_frequency_parameters
|
215
|
+
validate_step_interval(parameters, api_retuning_latency)
|
spectre_core/config/__init__.py
CHANGED
@@ -2,19 +2,17 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
+
|
6
|
+
"""General `spectre` package configurations."""
|
7
|
+
|
5
8
|
from ._paths import (
|
6
|
-
get_spectre_data_dir_path,
|
9
|
+
get_spectre_data_dir_path, get_batches_dir_path, get_configs_dir_path, get_logs_dir_path
|
7
10
|
)
|
8
11
|
from ._time_formats import (
|
9
|
-
|
12
|
+
TimeFormat
|
10
13
|
)
|
11
14
|
|
12
15
|
__all__ = [
|
13
|
-
"get_spectre_data_dir_path",
|
14
|
-
"
|
15
|
-
"get_configs_dir_path",
|
16
|
-
"get_logs_dir_path",
|
17
|
-
"DEFAULT_DATE_FORMAT",
|
18
|
-
"DEFAULT_TIME_FORMAT",
|
19
|
-
"DEFAULT_DATETIME_FORMAT"
|
16
|
+
"get_spectre_data_dir_path", "get_batches_dir_path", "get_configs_dir_path", "get_logs_dir_path",
|
17
|
+
"TimeFormat"
|
20
18
|
]
|
spectre_core/config/_paths.py
CHANGED
@@ -3,39 +3,57 @@
|
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
5
|
"""
|
6
|
-
|
6
|
+
File system path definitions.
|
7
|
+
|
8
|
+
`spectre` uses the required environment variable `SPECTRE_DATA_DIR_PATH`
|
9
|
+
and creates three directories inside it:
|
10
|
+
|
11
|
+
- `batches`: To hold the batched data files.
|
12
|
+
- `logs`: To hold log files generated at runtime.
|
13
|
+
- `configs`: To hold the capture config files.
|
7
14
|
"""
|
8
15
|
|
9
16
|
import os
|
17
|
+
from typing import Optional
|
10
18
|
|
11
|
-
_SPECTRE_DATA_DIR_PATH = os.environ.get("SPECTRE_DATA_DIR_PATH")
|
12
|
-
if _SPECTRE_DATA_DIR_PATH
|
13
|
-
raise ValueError("The environment variable SPECTRE_DATA_DIR_PATH
|
14
|
-
|
15
|
-
_CHUNKS_DIR_PATH = os.environ.get("SPECTRE_CHUNKS_DIR_PATH",
|
16
|
-
os.path.join(_SPECTRE_DATA_DIR_PATH, 'chunks'))
|
17
|
-
os.makedirs(_CHUNKS_DIR_PATH,
|
18
|
-
exist_ok=True)
|
19
|
+
_SPECTRE_DATA_DIR_PATH = os.environ.get("SPECTRE_DATA_DIR_PATH", "NOTSET")
|
20
|
+
if _SPECTRE_DATA_DIR_PATH == "NOTSET":
|
21
|
+
raise ValueError("The environment variable `SPECTRE_DATA_DIR_PATH` must be set.")
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
os.
|
23
|
-
exist_ok=True)
|
23
|
+
_BATCHES_DIR_PATH = os.path.join(_SPECTRE_DATA_DIR_PATH, 'batches')
|
24
|
+
_LOGS_DIR_PATH = os.path.join(_SPECTRE_DATA_DIR_PATH, 'logs')
|
25
|
+
_CONFIGS_DIR_PATH = os.path.join(_SPECTRE_DATA_DIR_PATH, "configs")
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
-
os.makedirs(_CONFIGS_DIR_PATH,
|
28
|
-
exist_ok=True)
|
27
|
+
os.makedirs(_BATCHES_DIR_PATH, exist_ok=True)
|
28
|
+
os.makedirs(_LOGS_DIR_PATH, exist_ok=True)
|
29
|
+
os.makedirs(_CONFIGS_DIR_PATH, exist_ok=True)
|
29
30
|
|
30
31
|
|
31
32
|
def get_spectre_data_dir_path(
|
32
33
|
) -> str:
|
34
|
+
"""The default ancestral path for all `spectre` file system data.
|
35
|
+
|
36
|
+
:return: The value stored by the `SPECTRE_DATA_DIR_PATH` environment variable.
|
37
|
+
"""
|
33
38
|
return _SPECTRE_DATA_DIR_PATH
|
34
39
|
|
35
40
|
|
36
|
-
def _get_date_based_dir_path(
|
37
|
-
|
41
|
+
def _get_date_based_dir_path(
|
42
|
+
base_dir: str,
|
43
|
+
year: Optional[int] = None,
|
44
|
+
month: Optional[int] = None,
|
45
|
+
day: Optional[int] = None
|
38
46
|
) -> str:
|
47
|
+
"""Append a date-based directory onto the base directory.
|
48
|
+
|
49
|
+
:param base_dir: The base directory to have the date directory appended to.
|
50
|
+
:param year: Numeric year. Defaults to None.
|
51
|
+
:param month: Numeric month. Defaults to None.
|
52
|
+
:param day: Numeric day. Defaults to None.
|
53
|
+
:raises ValueError: If a day is specified without the year or month.
|
54
|
+
:raises ValueError: If a month is specified without the year.
|
55
|
+
:return: The base directory with optional year, month, and day subdirectories appended.
|
56
|
+
"""
|
39
57
|
if day and not (year and month):
|
40
58
|
raise ValueError("A day requires both a month and a year")
|
41
59
|
if month and not year:
|
@@ -52,20 +70,38 @@ def _get_date_based_dir_path(base_dir: str, year: int = None,
|
|
52
70
|
return os.path.join(base_dir, *date_dir_components)
|
53
71
|
|
54
72
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
73
|
+
def get_batches_dir_path(
|
74
|
+
year: Optional[int] = None,
|
75
|
+
month: Optional[int] = None,
|
76
|
+
day: Optional[int] = None
|
58
77
|
) -> str:
|
59
|
-
|
78
|
+
"""The directory in the file system containing the batched data files. Optionally, append
|
79
|
+
a date-based directory to the end of the path.
|
80
|
+
|
81
|
+
:param year: The numeric year. Defaults to None.
|
82
|
+
:param month: The numeric month. Defaults to None.
|
83
|
+
:param day: The numeric day. Defaults to None.
|
84
|
+
:return: The directory path for batched data files, optionally with a date-based subdirectory.
|
85
|
+
"""
|
86
|
+
return _get_date_based_dir_path(_BATCHES_DIR_PATH,
|
60
87
|
year,
|
61
88
|
month,
|
62
89
|
day)
|
63
90
|
|
64
91
|
|
65
|
-
def get_logs_dir_path(
|
66
|
-
|
67
|
-
|
92
|
+
def get_logs_dir_path(
|
93
|
+
year: Optional[int] = None,
|
94
|
+
month: Optional[int] = None,
|
95
|
+
day: Optional[int] = None
|
68
96
|
) -> str:
|
97
|
+
"""The directory in the file system containing the log files generated at runtime. Optionally, append
|
98
|
+
a date-based directory to the end of the path.
|
99
|
+
|
100
|
+
:param year: The numeric year. Defaults to None.
|
101
|
+
:param month: The numeric month. Defaults to None.
|
102
|
+
:param day: The numeric day. Defaults to None.
|
103
|
+
:return: The directory path for log files, optionally with a date-based subdirectory.
|
104
|
+
"""
|
69
105
|
return _get_date_based_dir_path(_LOGS_DIR_PATH,
|
70
106
|
year,
|
71
107
|
month,
|
@@ -74,4 +110,8 @@ def get_logs_dir_path(year: int = None,
|
|
74
110
|
|
75
111
|
def get_configs_dir_path(
|
76
112
|
) -> str:
|
113
|
+
"""The directory in the file system containing the capture configs.
|
114
|
+
|
115
|
+
:return: The directory path for configuration files.
|
116
|
+
"""
|
77
117
|
return _CONFIGS_DIR_PATH
|
@@ -2,14 +2,21 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
-
"""
|
6
|
-
Package-wide default datetime formats.
|
7
|
-
"""
|
8
|
-
|
9
5
|
from dataclasses import dataclass
|
10
6
|
|
7
|
+
|
11
8
|
@dataclass(frozen=True)
|
12
|
-
class
|
13
|
-
|
14
|
-
|
15
|
-
|
9
|
+
class TimeFormat:
|
10
|
+
"""Package-wide datetime formats.
|
11
|
+
|
12
|
+
:ivar DATE: Format for dates (e.g., '2025-01-11').
|
13
|
+
:ivar TIME: Format for times (e.g., '23:59:59').
|
14
|
+
:ivar DATETIME: Combined date and time format (e.g., '2025-01-11T23:59:59').
|
15
|
+
:ivar PRECISE_TIME: Format for times with microseconds (e.g., '23:59:59.123456').
|
16
|
+
:ivar PRECISE_DATETIME: Combined date and precise time format (e.g., '2025-01-11T23:59:59.123456').
|
17
|
+
"""
|
18
|
+
DATE = "%Y-%m-%d"
|
19
|
+
TIME = "%H:%M:%S"
|
20
|
+
DATETIME = f"{DATE}T{TIME}"
|
21
|
+
PRECISE_TIME = "%H:%M:%S.%f"
|
22
|
+
PRECISE_DATETIME = f"{DATE}T{PRECISE_TIME}"
|
spectre_core/exceptions.py
CHANGED
@@ -3,12 +3,10 @@
|
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
5
|
"""
|
6
|
-
|
6
|
+
`spectre` custom exceptions.
|
7
7
|
"""
|
8
8
|
|
9
|
-
class
|
10
|
-
class ChunkFileNotFoundError(FileNotFoundError): ...
|
11
|
-
class SpectrogramNotFoundError(FileNotFoundError): ...
|
9
|
+
class BatchNotFoundError(KeyError): ...
|
12
10
|
class ModeNotFoundError(KeyError): ...
|
13
11
|
class EventHandlerNotFoundError(KeyError): ...
|
14
12
|
class ReceiverNotFoundError(KeyError): ...
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
"""Manage `spectre` jobs and workers."""
|
6
|
+
|
7
|
+
from ._jobs import Job, start_job
|
8
|
+
from ._workers import (
|
9
|
+
Worker, make_worker, do_capture, do_post_processing
|
10
|
+
)
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
"Job", "Worker", "make_worker", "start_job", "do_capture", "do_post_processing"
|
14
|
+
]
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from logging import getLogger
|
6
|
+
_LOGGER = getLogger(__name__)
|
7
|
+
|
8
|
+
import time
|
9
|
+
|
10
|
+
from ._workers import Worker
|
11
|
+
|
12
|
+
class Job:
|
13
|
+
"""Represents a collection of workers that run long-running tasks as
|
14
|
+
multiprocessing processes.
|
15
|
+
|
16
|
+
A `Job` manages the lifecycle of its workers, including starting,
|
17
|
+
monitoring, and terminating them.
|
18
|
+
"""
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
workers: list[Worker]
|
22
|
+
) -> None:
|
23
|
+
"""Initialise a `Job` with a list of workers.
|
24
|
+
|
25
|
+
:param workers: A list of `Worker` instances to manage as part of the job.
|
26
|
+
"""
|
27
|
+
self._workers = workers
|
28
|
+
|
29
|
+
|
30
|
+
def start(
|
31
|
+
self,
|
32
|
+
) -> None:
|
33
|
+
"""Tell each worker to call their functions in the background as multiprocessing processes."""
|
34
|
+
for worker in self._workers:
|
35
|
+
worker.start()
|
36
|
+
|
37
|
+
|
38
|
+
def terminate(
|
39
|
+
self,
|
40
|
+
) -> None:
|
41
|
+
"""Tell each worker to terminate their processes, if the processes are still running."""
|
42
|
+
_LOGGER.info("Terminating workers...")
|
43
|
+
for worker in self._workers:
|
44
|
+
if worker.process.is_alive():
|
45
|
+
worker.process.terminate()
|
46
|
+
worker.process.join()
|
47
|
+
_LOGGER.info("All workers successfully terminated")
|
48
|
+
|
49
|
+
|
50
|
+
def monitor(
|
51
|
+
self,
|
52
|
+
total_runtime: float,
|
53
|
+
force_restart: bool = False
|
54
|
+
) -> None:
|
55
|
+
"""
|
56
|
+
Monitor the workers during execution and handle unexpected exits.
|
57
|
+
|
58
|
+
Periodically checks worker processes within the specified runtime duration.
|
59
|
+
If a worker exits unexpectedly:
|
60
|
+
- Restarts all workers if `force_restart` is True.
|
61
|
+
- Terminates all workers and raises an exception if `force_restart` is False.
|
62
|
+
|
63
|
+
:param total_runtime: Total time to monitor the workers, in seconds.
|
64
|
+
:param force_restart: Whether to restart all workers if one exits unexpectedly.
|
65
|
+
:raises RuntimeError: If a worker exits and `force_restart` is False.
|
66
|
+
"""
|
67
|
+
_LOGGER.info("Monitoring workers...")
|
68
|
+
start_time = time.time()
|
69
|
+
|
70
|
+
try:
|
71
|
+
while time.time() - start_time < total_runtime:
|
72
|
+
for worker in self._workers:
|
73
|
+
if not worker.process.is_alive():
|
74
|
+
error_message = f"Worker with name `{worker.name}` unexpectedly exited."
|
75
|
+
_LOGGER.error(error_message)
|
76
|
+
if force_restart:
|
77
|
+
# Restart all workers
|
78
|
+
for worker in self._workers:
|
79
|
+
worker.restart()
|
80
|
+
else:
|
81
|
+
self.terminate()
|
82
|
+
raise RuntimeError(error_message)
|
83
|
+
time.sleep(1) # Poll every second
|
84
|
+
|
85
|
+
_LOGGER.info("Session duration reached. Terminating workers...")
|
86
|
+
self.terminate()
|
87
|
+
|
88
|
+
except KeyboardInterrupt:
|
89
|
+
_LOGGER.info("Keyboard interrupt detected. Terminating workers...")
|
90
|
+
self.terminate()
|
91
|
+
|
92
|
+
|
93
|
+
def start_job(
|
94
|
+
workers: list[Worker],
|
95
|
+
total_runtime: float,
|
96
|
+
force_restart: bool = False
|
97
|
+
) -> None:
|
98
|
+
"""Create and run a job with the specified workers.
|
99
|
+
|
100
|
+
Starts the workers, monitors them for the specified runtime, and handles
|
101
|
+
unexpected exits according to the `force_restart` policy.
|
102
|
+
|
103
|
+
:param workers: A list of `Worker` instances to include in the job.
|
104
|
+
:param total_runtime: Total time to monitor the job, in seconds.
|
105
|
+
:param force_restart: Whether to restart all workers if one exits unexpectedly.
|
106
|
+
Defaults to False.
|
107
|
+
"""
|
108
|
+
job = Job(workers)
|
109
|
+
job.start()
|
110
|
+
job.monitor(total_runtime, force_restart)
|
111
|
+
|