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.
Files changed (94) hide show
  1. spectre_core/_file_io/__init__.py +1 -3
  2. spectre_core/_file_io/file_handlers.py +170 -65
  3. spectre_core/batches/__init__.py +21 -0
  4. spectre_core/batches/_base.py +238 -0
  5. spectre_core/batches/_batches.py +247 -0
  6. spectre_core/batches/_factory.py +69 -0
  7. spectre_core/batches/_register.py +30 -0
  8. spectre_core/batches/plugins/_batch_keys.py +16 -0
  9. spectre_core/batches/plugins/_callisto.py +183 -0
  10. spectre_core/batches/plugins/_iq_stream.py +354 -0
  11. spectre_core/capture_configs/__init__.py +17 -13
  12. spectre_core/capture_configs/_capture_config.py +93 -34
  13. spectre_core/capture_configs/_capture_modes.py +22 -0
  14. spectre_core/capture_configs/_capture_templates.py +207 -122
  15. spectre_core/capture_configs/_parameters.py +116 -46
  16. spectre_core/capture_configs/_pconstraints.py +86 -35
  17. spectre_core/capture_configs/_pnames.py +49 -0
  18. spectre_core/capture_configs/_ptemplates.py +389 -346
  19. spectre_core/capture_configs/_pvalidators.py +121 -77
  20. spectre_core/config/__init__.py +7 -9
  21. spectre_core/config/_paths.py +66 -26
  22. spectre_core/config/_time_formats.py +15 -8
  23. spectre_core/exceptions.py +2 -4
  24. spectre_core/jobs/__init__.py +14 -0
  25. spectre_core/jobs/_jobs.py +111 -0
  26. spectre_core/jobs/_workers.py +171 -0
  27. spectre_core/logs/__init__.py +17 -0
  28. spectre_core/logs/_configure.py +67 -0
  29. spectre_core/logs/_decorators.py +33 -0
  30. spectre_core/logs/_logs.py +228 -0
  31. spectre_core/logs/_process_types.py +14 -0
  32. spectre_core/plotting/__init__.py +4 -2
  33. spectre_core/plotting/_base.py +204 -102
  34. spectre_core/plotting/_format.py +17 -4
  35. spectre_core/plotting/_panel_names.py +18 -0
  36. spectre_core/plotting/_panel_stack.py +167 -53
  37. spectre_core/plotting/_panels.py +341 -141
  38. spectre_core/post_processing/__init__.py +8 -6
  39. spectre_core/post_processing/_base.py +71 -45
  40. spectre_core/post_processing/_factory.py +42 -12
  41. spectre_core/post_processing/_post_processor.py +27 -29
  42. spectre_core/post_processing/_register.py +22 -6
  43. spectre_core/post_processing/plugins/_event_handler_keys.py +16 -0
  44. spectre_core/post_processing/plugins/_fixed_center_frequency.py +129 -0
  45. spectre_core/post_processing/plugins/_swept_center_frequency.py +439 -0
  46. spectre_core/py.typed +0 -0
  47. spectre_core/receivers/__init__.py +10 -7
  48. spectre_core/receivers/_base.py +220 -69
  49. spectre_core/receivers/_factory.py +53 -7
  50. spectre_core/receivers/_register.py +30 -9
  51. spectre_core/receivers/_spec_names.py +26 -15
  52. spectre_core/receivers/plugins/__init__.py +0 -0
  53. spectre_core/receivers/plugins/_receiver_names.py +16 -0
  54. spectre_core/receivers/plugins/_rsp1a.py +59 -0
  55. spectre_core/receivers/plugins/_rspduo.py +67 -0
  56. spectre_core/receivers/plugins/_sdrplay_receiver.py +190 -0
  57. spectre_core/receivers/plugins/_test.py +218 -0
  58. spectre_core/receivers/plugins/gr/_base.py +80 -0
  59. spectre_core/receivers/{gr → plugins/gr}/_rsp1a.py +45 -55
  60. spectre_core/receivers/{gr → plugins/gr}/_rspduo.py +65 -78
  61. spectre_core/receivers/{gr → plugins/gr}/_test.py +36 -34
  62. spectre_core/spectrograms/__init__.py +5 -3
  63. spectre_core/spectrograms/_analytical.py +121 -72
  64. spectre_core/spectrograms/_array_operations.py +103 -36
  65. spectre_core/spectrograms/_spectrogram.py +410 -203
  66. spectre_core/spectrograms/_transform.py +199 -188
  67. spectre_core/wgetting/__init__.py +4 -2
  68. spectre_core/wgetting/_callisto.py +178 -127
  69. {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/METADATA +14 -7
  70. spectre_core-0.0.13.dist-info/RECORD +75 -0
  71. {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/WHEEL +1 -1
  72. spectre_core/chunks/__init__.py +0 -22
  73. spectre_core/chunks/_base.py +0 -116
  74. spectre_core/chunks/_chunks.py +0 -200
  75. spectre_core/chunks/_factory.py +0 -25
  76. spectre_core/chunks/_register.py +0 -15
  77. spectre_core/chunks/library/_callisto.py +0 -98
  78. spectre_core/chunks/library/_fixed_center_frequency.py +0 -128
  79. spectre_core/chunks/library/_swept_center_frequency.py +0 -103
  80. spectre_core/logging/__init__.py +0 -11
  81. spectre_core/logging/_configure.py +0 -35
  82. spectre_core/logging/_decorators.py +0 -19
  83. spectre_core/logging/_log_handlers.py +0 -176
  84. spectre_core/post_processing/library/_fixed_center_frequency.py +0 -115
  85. spectre_core/post_processing/library/_swept_center_frequency.py +0 -382
  86. spectre_core/receivers/gr/_base.py +0 -33
  87. spectre_core/receivers/library/_rsp1a.py +0 -61
  88. spectre_core/receivers/library/_rspduo.py +0 -69
  89. spectre_core/receivers/library/_sdrplay_receiver.py +0 -185
  90. spectre_core/receivers/library/_test.py +0 -221
  91. spectre_core-0.0.11.dist-info/RECORD +0 -64
  92. /spectre_core/receivers/{gr → plugins/gr}/__init__.py +0 -0
  93. {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/LICENSE +0 -0
  94. {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 ._ptemplates import PNames
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
- def _validate_window(
24
+
25
+ def validate_window(
20
26
  parameters: Parameters
21
27
  ) -> None:
22
- window_size = parameters.get_parameter_value(PNames.WINDOW_SIZE)
23
- window_type = parameters.get_parameter_value(PNames.WINDOW_TYPE)
24
- sample_rate = parameters.get_parameter_value(PNames.SAMPLE_RATE)
25
- batch_size = parameters.get_parameter_value(PNames.BATCH_SIZE)
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 chunk size. "
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 chunk size is {batch_size} [s]"))
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 Exception((f"An error has occurred while validating the window. "
37
- f"Got {str(e)}"))
49
+ raise ValueError((f"An error has occurred while validating the window. "
50
+ f"Got {str(e)}"))
38
51
 
39
52
 
40
- def _validate_nyquist_criterion(
53
+ def validate_nyquist_criterion(
41
54
  parameters: Parameters
42
55
  ) -> None:
43
- sample_rate = parameters.get_parameter_value(PNames.SAMPLE_RATE)
44
- bandwidth = parameters.get_parameter_value(PNames.BANDWIDTH)
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
- return floor((max_freq - min_freq + samp_rate/2) / freq_step)
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
- def _validate_num_steps_per_sweep(
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
- min_freq = parameters.get_parameter_value(PNames.MIN_FREQUENCY)
63
- max_freq = parameters.get_parameter_value(PNames.MAX_FREQUENCY)
64
- sample_rate = parameters.get_parameter_value(PNames.SAMPLE_RATE)
65
- freq_step = parameters.get_parameter_value(PNames.FREQUENCY_STEP)
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 _validate_sweep_interval(
106
+ def validate_sweep_interval(
77
107
  parameters: Parameters
78
108
  ) -> None:
79
- min_freq = parameters.get_parameter_value(PNames.MIN_FREQUENCY)
80
- max_freq = parameters.get_parameter_value(PNames.MAX_FREQUENCY)
81
- sample_rate = parameters.get_parameter_value(PNames.SAMPLE_RATE)
82
- freq_step = parameters.get_parameter_value(PNames.FREQUENCY_STEP)
83
- samples_per_step = parameters.get_parameter_value(PNames.SAMPLES_PER_STEP)
84
- batch_size = parameters.get_parameter_value(PNames.BATCH_SIZE)
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 chunk size. "
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 chunk size is {batch_size} [s]"))
129
+ f"but the given batch size is {batch_size} [s]"))
96
130
 
97
131
 
98
- def _validate_num_samples_per_step(
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
- window_size = parameters.get_parameter_value(PNames.WINDOW_SIZE)
103
- samples_per_step = parameters.get_parameter_value(PNames.SAMPLES_PER_STEP)
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 _validate_non_overlapping_steps(
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 = parameters.get_parameter_value(PNames.FREQUENCY_STEP)
116
- sample_rate = parameters.get_parameter_value(PNames.SAMPLE_RATE)
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 _validate_step_interval(
167
+ def validate_step_interval(
125
168
  parameters: Parameters,
126
- api_latency: Number
169
+ api_retuning_latency: float
127
170
  ) -> None:
128
-
129
- samples_per_step = parameters.get_parameter_value(PNames.SAMPLES_PER_STEP)
130
- sample_rate = parameters.get_parameter_value(PNames.SAMPLE_RATE)
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 < api_latency:
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 {api_latency} [s]; you may experience undefined behaviour!")
184
+ f"derived api latency {api_retuning_latency} [s]; you may experience undefined behaviour!")
136
185
 
137
186
 
138
- def _validate_fixed_center_frequency_parameters(
187
+ def validate_fixed_center_frequency(
139
188
  parameters: Parameters
140
189
  ) -> None:
141
- _validate_nyquist_criterion(parameters)
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
- def _validate_swept_center_frequency_parameters(
197
+
198
+ def validate_swept_center_frequency(
146
199
  parameters: Parameters,
147
- api_retuning_latency: Optional[Number] = None,
200
+ api_retuning_latency: Optional[float] = None,
148
201
  ) -> None:
149
- _validate_nyquist_criterion(parameters)
150
- _validate_window(parameters)
151
- _validate_non_overlapping_steps(parameters)
152
- _validate_num_steps_per_sweep(parameters)
153
- _validate_num_samples_per_step(parameters)
154
- _validate_sweep_interval(parameters)
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
- _validate_step_interval(parameters, api_retuning_latency)
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)
@@ -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, get_chunks_dir_path, get_configs_dir_path, get_logs_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
- TimeFormats
12
+ TimeFormat
10
13
  )
11
14
 
12
15
  __all__ = [
13
- "get_spectre_data_dir_path",
14
- "get_chunks_dir_path",
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
  ]
@@ -3,39 +3,57 @@
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  """
6
- SPECTRE data paths.
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 is None:
13
- raise ValueError("The environment variable SPECTRE_DATA_DIR_PATH has not been set")
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
- _LOGS_DIR_PATH = os.environ.get("SPECTRE_LOGS_DIR_PATH",
21
- os.path.join(_SPECTRE_DATA_DIR_PATH, 'logs'))
22
- os.makedirs(_LOGS_DIR_PATH,
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
- _CONFIGS_DIR_PATH = os.environ.get("SPECTRE_CONFIGS_DIR_PATH",
26
- os.path.join(_SPECTRE_DATA_DIR_PATH, "configs"))
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(base_dir: str, year: int = None,
37
- month: int = None, day: int = None
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 get_chunks_dir_path(year: int = None,
56
- month: int = None,
57
- day: int = None
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
- return _get_date_based_dir_path(_CHUNKS_DIR_PATH,
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(year: int = None,
66
- month: int = None,
67
- day: int = None
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 TimeFormats:
13
- TIME = "%H:%M:%S"
14
- DATE = "%Y-%m-%d"
15
- DATETIME = f"{DATE}T{TIME}"
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}"
@@ -3,12 +3,10 @@
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  """
6
- SPECTRE custom exceptions.
6
+ `spectre` custom exceptions.
7
7
  """
8
8
 
9
- class ChunkNotFoundError(FileNotFoundError): ...
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
+