spectre-core 0.0.12__py3-none-any.whl → 0.0.14__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 +163 -58
- spectre_core/batches/__init__.py +10 -11
- spectre_core/batches/_base.py +170 -78
- spectre_core/batches/_batches.py +149 -99
- spectre_core/batches/_factory.py +56 -14
- spectre_core/batches/_register.py +23 -8
- 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 +115 -42
- 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 +117 -73
- spectre_core/config/__init__.py +6 -8
- spectre_core/config/_paths.py +65 -25
- spectre_core/config/_time_formats.py +15 -10
- 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 +70 -44
- spectre_core/post_processing/_factory.py +42 -12
- spectre_core/post_processing/_post_processor.py +24 -26
- 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/{library → plugins}/_swept_center_frequency.py +215 -143
- 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 +42 -52
- spectre_core/receivers/{gr → plugins/gr}/_rspduo.py +61 -74
- spectre_core/receivers/{gr → plugins/gr}/_test.py +33 -31
- spectre_core/spectrograms/__init__.py +5 -3
- spectre_core/spectrograms/_analytical.py +121 -66
- spectre_core/spectrograms/_array_operations.py +103 -36
- spectre_core/spectrograms/_spectrogram.py +380 -207
- spectre_core/spectrograms/_transform.py +197 -169
- spectre_core/wgetting/__init__.py +4 -2
- spectre_core/wgetting/_callisto.py +173 -118
- {spectre_core-0.0.12.dist-info → spectre_core-0.0.14.dist-info}/METADATA +14 -7
- spectre_core-0.0.14.dist-info/RECORD +75 -0
- {spectre_core-0.0.12.dist-info → spectre_core-0.0.14.dist-info}/WHEEL +1 -1
- spectre_core/batches/library/_callisto.py +0 -96
- spectre_core/batches/library/_fixed_center_frequency.py +0 -133
- spectre_core/batches/library/_swept_center_frequency.py +0 -105
- 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 -114
- 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.12.dist-info/RECORD +0 -64
- /spectre_core/receivers/{gr → plugins/gr}/__init__.py +0 -0
- {spectre_core-0.0.12.dist-info → spectre_core-0.0.14.dist-info}/LICENSE +0 -0
- {spectre_core-0.0.12.dist-info → spectre_core-0.0.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,354 @@
|
|
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 datetime import datetime
|
6
|
+
from dataclasses import dataclass
|
7
|
+
from typing import Optional
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
import numpy.typing as npt
|
11
|
+
from astropy.io import fits
|
12
|
+
from astropy.io.fits.hdu.image import PrimaryHDU
|
13
|
+
from astropy.io.fits.hdu.table import BinTableHDU
|
14
|
+
from astropy.io.fits.hdu.hdulist import HDUList
|
15
|
+
|
16
|
+
from spectre_core.exceptions import InvalidSweepMetadataError
|
17
|
+
from spectre_core.config import TimeFormat
|
18
|
+
from spectre_core.spectrograms import Spectrogram, SpectrumUnit
|
19
|
+
from ._batch_keys import BatchKey
|
20
|
+
from .._base import BaseBatch, BatchFile
|
21
|
+
from .._register import register_batch
|
22
|
+
|
23
|
+
|
24
|
+
@dataclass(frozen=True)
|
25
|
+
class _BatchExtension:
|
26
|
+
"""Supported extensions for a `IQStreamBatch`.
|
27
|
+
|
28
|
+
:ivar FITS: Corresponds to the `.fits` file extension.
|
29
|
+
:ivar BIN: Corresponds to the `.bin` file extension.
|
30
|
+
:ivar HDR: Corresponds to the `.hdr` file extension.
|
31
|
+
"""
|
32
|
+
FITS: str = "fits"
|
33
|
+
BIN : str = "bin"
|
34
|
+
HDR : str = "hdr"
|
35
|
+
|
36
|
+
|
37
|
+
class _BinFile(BatchFile[npt.NDArray[np.complex64]]):
|
38
|
+
"""Stores complex IQ samples in the binary format, as produced by the `gr-spectre`
|
39
|
+
OOT module block `batched_file_sink`.
|
40
|
+
"""
|
41
|
+
def __init__(
|
42
|
+
self,
|
43
|
+
batch_parent_dir_path: str,
|
44
|
+
batch_name: str
|
45
|
+
) -> None:
|
46
|
+
"""Initialise a `_BinFile` instance.
|
47
|
+
|
48
|
+
:param batch_parent_dir_path: The parent directory for the batch.
|
49
|
+
:param batch_name: The batch name.
|
50
|
+
"""
|
51
|
+
super().__init__(batch_parent_dir_path, batch_name, _BatchExtension.BIN)
|
52
|
+
|
53
|
+
|
54
|
+
def _read(
|
55
|
+
self
|
56
|
+
) -> npt.NDArray[np.complex64]:
|
57
|
+
"""Reads the binary file and returns the stored complex IQ samples.
|
58
|
+
|
59
|
+
:return: The raw 32-bit floats in the binary file, interpreted as 64-bit complex IQ samples.
|
60
|
+
"""
|
61
|
+
with open(self.file_path, "rb") as fh:
|
62
|
+
return np.fromfile(fh, dtype=np.complex64)
|
63
|
+
|
64
|
+
|
65
|
+
@dataclass
|
66
|
+
class IQMetadata:
|
67
|
+
"""Represents metadata for IQ samples produced by the `gr-spectre` OOT module block `batched_file_sink`.
|
68
|
+
|
69
|
+
:ivar millisecond_correction: The millisecond component of the batch start time.
|
70
|
+
:ivar center_frequencies: Center frequencies for each IQ sample, if the stream was frequency tagged.
|
71
|
+
None otherwise.
|
72
|
+
:ivar num_samples: Number of samples collected at each center frequency, if the stream was frequency
|
73
|
+
tagged. None otherwise.
|
74
|
+
"""
|
75
|
+
millisecond_correction: int
|
76
|
+
center_frequencies: Optional[npt.NDArray[np.float32]] = None
|
77
|
+
num_samples: Optional[npt.NDArray[np.int32]] = None
|
78
|
+
|
79
|
+
def is_frequency_tagged(self) -> bool:
|
80
|
+
"""Check if the IQ metadata contains frequency tagging information.
|
81
|
+
|
82
|
+
:return: True if frequency tagging information is present; False otherwise.
|
83
|
+
"""
|
84
|
+
return (self.center_frequencies is not None) and (self.num_samples is not None)
|
85
|
+
|
86
|
+
|
87
|
+
class _HdrFile(BatchFile[IQMetadata]):
|
88
|
+
"""Stores IQ sample metadata produced by the `gr-spectre` OOT module block `batched_file_sink`, used
|
89
|
+
to help parse the corresponding `.bin` file.
|
90
|
+
|
91
|
+
File Structure:
|
92
|
+
- If frequency tagged:
|
93
|
+
(`<millisecond_correction>`, `<freq_0>`, `<num_samples_0>`, `<freq_1>`, `<num_samples_1>`, ...)
|
94
|
+
All values are stored as 32-bit floats.
|
95
|
+
- The first value is the millisecond component for the batch start time.
|
96
|
+
- Subsequent tuples (`<freq_n>`, `<num_samples_n>`) indicate that `<num_samples_n>` samples were collected at `<freq_n>`.
|
97
|
+
- If not frequency tagged:
|
98
|
+
(`<millisecond_correction>`)
|
99
|
+
Only the millisecond correction is present, with no frequency information.
|
100
|
+
|
101
|
+
This format enables mapping IQ samples in the binary file to their corresponding center frequencies, if applicable.
|
102
|
+
"""
|
103
|
+
def __init__(
|
104
|
+
self,
|
105
|
+
parent_dir_path: str,
|
106
|
+
base_file_name: str
|
107
|
+
) -> None:
|
108
|
+
"""Initialise a `_HdrFile` instance.
|
109
|
+
|
110
|
+
:param parent_dir_path: The parent directory for the batch.
|
111
|
+
:param base_file_name: The batch name.
|
112
|
+
"""
|
113
|
+
super().__init__(parent_dir_path, base_file_name, _BatchExtension.HDR)
|
114
|
+
|
115
|
+
|
116
|
+
def _read(
|
117
|
+
self
|
118
|
+
) -> IQMetadata:
|
119
|
+
"""Parses the binary contents of the `.hdr` file to extract IQ sample metadata.
|
120
|
+
|
121
|
+
:return: An instance of `IQMetadata` containing the parsed metadata, including the millisecond correction
|
122
|
+
and, if applicable, frequency tagging details.
|
123
|
+
"""
|
124
|
+
hdr_contents = self._extract_raw_contents()
|
125
|
+
millisecond_correction = self._get_millisecond_correction(hdr_contents)
|
126
|
+
if hdr_contents.size == 1:
|
127
|
+
return IQMetadata(millisecond_correction)
|
128
|
+
else:
|
129
|
+
center_frequencies = self._get_center_frequencies(hdr_contents)
|
130
|
+
num_samples = self._get_num_samples(hdr_contents)
|
131
|
+
self._validate_frequencies_and_samples(center_frequencies,
|
132
|
+
num_samples)
|
133
|
+
return IQMetadata(millisecond_correction,
|
134
|
+
center_frequencies,
|
135
|
+
num_samples)
|
136
|
+
|
137
|
+
|
138
|
+
|
139
|
+
def _extract_raw_contents(
|
140
|
+
self
|
141
|
+
) -> npt.NDArray[np.float32]:
|
142
|
+
"""Reads the raw contents of the `.hdr` file."""
|
143
|
+
with open(self.file_path, "rb") as fh:
|
144
|
+
# the `batched_file_sink` block in the `gr-spectre` GNU Radio OOT module stores
|
145
|
+
# the integral millisecond component as a 32-bit float.
|
146
|
+
return np.fromfile(fh, dtype=np.float32)
|
147
|
+
|
148
|
+
|
149
|
+
def _get_millisecond_correction(
|
150
|
+
self,
|
151
|
+
hdr_contents: npt.NDArray[np.float32]
|
152
|
+
) -> int:
|
153
|
+
"""Extracts and validates the millisecond component of the batch start time.
|
154
|
+
|
155
|
+
The value is stored as a 32-bit float but interpreted as an integer.
|
156
|
+
"""
|
157
|
+
millisecond_correction = float(hdr_contents[0])
|
158
|
+
|
159
|
+
if not millisecond_correction.is_integer():
|
160
|
+
raise TypeError(f"Expected integer value for millisecond correction, but got {millisecond_correction}")
|
161
|
+
|
162
|
+
return int(millisecond_correction)
|
163
|
+
|
164
|
+
|
165
|
+
def _get_center_frequencies(
|
166
|
+
self,
|
167
|
+
hdr_contents: npt.NDArray[np.float32]
|
168
|
+
) -> npt.NDArray[np.float32]:
|
169
|
+
"""Extracts the center frequencies from the `.hdr` file contents.
|
170
|
+
|
171
|
+
Center frequencies are stored at every second entry, starting from the first index.
|
172
|
+
"""
|
173
|
+
return hdr_contents[1::2]
|
174
|
+
|
175
|
+
|
176
|
+
def _get_num_samples(
|
177
|
+
self,
|
178
|
+
hdr_contents: npt.NDArray[np.float32]
|
179
|
+
) -> npt.NDArray[np.int32]:
|
180
|
+
"""Extracts the number of samples per frequency from the `.hdr` file contents.
|
181
|
+
|
182
|
+
The values are stored as 32-bit floats but are interpreted as integers.
|
183
|
+
Sample counts are located at every second entry, starting from the second index.
|
184
|
+
"""
|
185
|
+
num_samples_as_float = hdr_contents[2::2]
|
186
|
+
if not all(num_samples_as_float == num_samples_as_float.astype(int)):
|
187
|
+
raise InvalidSweepMetadataError("Number of samples per frequency is expected to describe an integer")
|
188
|
+
return num_samples_as_float.astype(np.int32)
|
189
|
+
|
190
|
+
|
191
|
+
def _validate_frequencies_and_samples(
|
192
|
+
self,
|
193
|
+
center_frequencies: npt.NDArray[np.float32],
|
194
|
+
num_samples: npt.NDArray[np.int32]
|
195
|
+
) -> None:
|
196
|
+
"""Ensures that each center frequency has a corresponding sample count."""
|
197
|
+
if len(center_frequencies) != len(num_samples):
|
198
|
+
raise InvalidSweepMetadataError("Center frequencies and number of samples arrays are not the same length")
|
199
|
+
|
200
|
+
|
201
|
+
class _FitsFile(BatchFile[Spectrogram]):
|
202
|
+
"""Stores spectrogram data in the FITS file format, as generated by `spectre` from a stream of IQ samples."""
|
203
|
+
def __init__(
|
204
|
+
self,
|
205
|
+
parent_dir_path: str,
|
206
|
+
base_file_name: str
|
207
|
+
) -> None:
|
208
|
+
"""Initialise a `_FitsFile` instance.
|
209
|
+
|
210
|
+
:param parent_dir_path: The parent directory for the batch.
|
211
|
+
:param base_file_name: The batch name.
|
212
|
+
"""
|
213
|
+
super().__init__(parent_dir_path, base_file_name, _BatchExtension.FITS)
|
214
|
+
|
215
|
+
|
216
|
+
def _read(
|
217
|
+
self
|
218
|
+
) -> Spectrogram:
|
219
|
+
"""Read the FITS file and create a spectrogram.
|
220
|
+
|
221
|
+
:return: A `Spectrogram` instance containing the parsed FITS file data.
|
222
|
+
"""
|
223
|
+
with fits.open(self.file_path, mode='readonly') as hdulist:
|
224
|
+
primary_hdu = self._get_primary_hdu(hdulist)
|
225
|
+
dynamic_spectra = self._get_dynamic_spectra(primary_hdu)
|
226
|
+
bunit = self._get_bunit(primary_hdu)
|
227
|
+
spectrogram_start_datetime = self._get_spectrogram_start_datetime(primary_hdu)
|
228
|
+
bintable_hdu = self._get_bintable_hdu(hdulist)
|
229
|
+
times = self._get_times(bintable_hdu)
|
230
|
+
frequencies = self._get_frequencies(bintable_hdu)
|
231
|
+
|
232
|
+
# bunit is interpreted as a SpectrumUnit.
|
233
|
+
spectrum_unit = SpectrumUnit(bunit)
|
234
|
+
return Spectrogram(dynamic_spectra,
|
235
|
+
times,
|
236
|
+
frequencies,
|
237
|
+
self.tag,
|
238
|
+
spectrum_unit,
|
239
|
+
spectrogram_start_datetime)
|
240
|
+
|
241
|
+
|
242
|
+
def _get_primary_hdu(
|
243
|
+
self,
|
244
|
+
hdulist: HDUList
|
245
|
+
) -> PrimaryHDU:
|
246
|
+
return hdulist['PRIMARY']
|
247
|
+
|
248
|
+
|
249
|
+
def _get_bintable_hdu(
|
250
|
+
self,
|
251
|
+
hdulist: HDUList
|
252
|
+
) -> BinTableHDU:
|
253
|
+
return hdulist[1]
|
254
|
+
|
255
|
+
|
256
|
+
def _get_bunit(
|
257
|
+
self,
|
258
|
+
primary_hdu: PrimaryHDU
|
259
|
+
) -> str:
|
260
|
+
"""Get the units corresponding to the elements of the dynamic spectra."""
|
261
|
+
return primary_hdu.header['BUNIT']
|
262
|
+
|
263
|
+
|
264
|
+
def _get_dynamic_spectra(
|
265
|
+
self,
|
266
|
+
primary_hdu: PrimaryHDU
|
267
|
+
) -> npt.NDArray[np.float32]:
|
268
|
+
return primary_hdu.data
|
269
|
+
|
270
|
+
|
271
|
+
def _get_spectrogram_start_datetime(
|
272
|
+
self,
|
273
|
+
primary_hdu: PrimaryHDU
|
274
|
+
) -> datetime:
|
275
|
+
"""Get the start time of the spectrogram, up to the full precision available."""
|
276
|
+
date_obs = primary_hdu.header['DATE-OBS']
|
277
|
+
time_obs = primary_hdu.header['TIME-OBS']
|
278
|
+
return datetime.strptime(f"{date_obs}T{time_obs}", TimeFormat.PRECISE_DATETIME)
|
279
|
+
|
280
|
+
|
281
|
+
def _get_times(
|
282
|
+
self,
|
283
|
+
bintable_hdu: BinTableHDU
|
284
|
+
) -> npt.NDArray[np.float32]:
|
285
|
+
"""Extracts the elapsed times for each spectrum in seconds, with the first spectrum set to t=0
|
286
|
+
by convention.
|
287
|
+
"""
|
288
|
+
return bintable_hdu.data['TIME'][0] # already in seconds
|
289
|
+
|
290
|
+
|
291
|
+
def _get_frequencies(
|
292
|
+
self,
|
293
|
+
bintable_hdu: BinTableHDU
|
294
|
+
) -> npt.NDArray[np.float32]:
|
295
|
+
"""Extracts the frequencies for each spectral component."""
|
296
|
+
frequencies_MHz = bintable_hdu.data['FREQUENCY'][0]
|
297
|
+
return frequencies_MHz * 1e6 # convert to Hz
|
298
|
+
|
299
|
+
|
300
|
+
@register_batch(BatchKey.IQ_STREAM)
|
301
|
+
class IQStreamBatch(BaseBatch):
|
302
|
+
"""A batch of data derived from a stream of IQ samples from some receiver.
|
303
|
+
|
304
|
+
Supports the following extensions:
|
305
|
+
- `.fits` (via the `spectrogram_file` attribute)
|
306
|
+
- `.bin` (via the `bin_file` attribute)
|
307
|
+
- `.hdr` (via the `hdr_file` attribute)
|
308
|
+
"""
|
309
|
+
def __init__(
|
310
|
+
self,
|
311
|
+
start_time: str,
|
312
|
+
tag: str
|
313
|
+
) -> None:
|
314
|
+
"""Initialise a `IQStreamBatch` instance.
|
315
|
+
|
316
|
+
:param start_time: The start time of the batch.
|
317
|
+
:param tag: The batch name tag.
|
318
|
+
"""
|
319
|
+
super().__init__(start_time, tag)
|
320
|
+
self._batch_files = {}
|
321
|
+
self._fits_file = _FitsFile(self.parent_dir_path, self.name)
|
322
|
+
self._bin_file = _BinFile (self.parent_dir_path, self.name)
|
323
|
+
self._hdr_file = _HdrFile (self.parent_dir_path, self.name)
|
324
|
+
|
325
|
+
# add files formally to the batch
|
326
|
+
self.add_file( self.spectrogram_file )
|
327
|
+
self.add_file( self.bin_file )
|
328
|
+
self.add_file( self.hdr_file )
|
329
|
+
|
330
|
+
|
331
|
+
@property
|
332
|
+
def spectrogram_file(
|
333
|
+
self
|
334
|
+
) -> _FitsFile:
|
335
|
+
"""The batch file corresponding to the `.fits` extension."""
|
336
|
+
return self._fits_file
|
337
|
+
|
338
|
+
|
339
|
+
@property
|
340
|
+
def bin_file(
|
341
|
+
self
|
342
|
+
) -> _BinFile:
|
343
|
+
"""The batch file corresponding to the `.bin` extension."""
|
344
|
+
return self._bin_file
|
345
|
+
|
346
|
+
|
347
|
+
@property
|
348
|
+
def hdr_file(
|
349
|
+
self
|
350
|
+
) -> _HdrFile:
|
351
|
+
"""The batch file corresponding to the `.hdr` extension."""
|
352
|
+
return self._hdr_file
|
353
|
+
|
354
|
+
|
@@ -2,28 +2,32 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
-
"""
|
6
|
-
Capture configuration files.
|
7
|
-
"""
|
5
|
+
"""Create, template and read capture config files."""
|
8
6
|
|
9
|
-
from .
|
7
|
+
from ._pnames import PName
|
8
|
+
from ._capture_modes import CaptureMode
|
9
|
+
from ._pvalidators import (
|
10
|
+
validate_fixed_center_frequency, validate_non_overlapping_steps, validate_num_samples_per_step,
|
11
|
+
validate_num_steps_per_sweep, validate_nyquist_criterion, validate_step_interval, validate_sweep_interval,
|
12
|
+
validate_swept_center_frequency, validate_window
|
13
|
+
)
|
10
14
|
from ._capture_config import CaptureConfig
|
15
|
+
from ._ptemplates import PTemplate, get_base_ptemplate
|
11
16
|
from ._parameters import (
|
12
17
|
Parameter, Parameters, parse_string_parameters, make_parameters
|
13
18
|
)
|
14
19
|
from ._capture_templates import (
|
15
|
-
CaptureTemplate,
|
20
|
+
CaptureTemplate, get_base_capture_template, make_base_capture_template
|
16
21
|
)
|
17
22
|
from ._pconstraints import (
|
18
|
-
|
19
|
-
)
|
20
|
-
from ._ptemplates import (
|
21
|
-
PTemplate, PNames, get_base_ptemplate,
|
23
|
+
BasePConstraint, EnforceSign, Bound, OneOf, PowerOfTwo
|
22
24
|
)
|
23
25
|
|
24
26
|
__all__ = [
|
25
|
-
"
|
26
|
-
"
|
27
|
-
"
|
28
|
-
"
|
27
|
+
"PTemplate", "PValidator", "CaptureConfig", "Parameter", "Parameters", "parse_string_parameters",
|
28
|
+
"make_parameters", "CaptureTemplate", "CaptureMode", "get_base_capture_template", "make_base_capture_template"
|
29
|
+
"PConstraint", "PConstraint", "Bound", "OneOf", "EnforceSign", "PowerOfTwo", "make_base_capture_template", "PName",
|
30
|
+
"get_base_ptemplate", "BasePConstraint", "validate_fixed_center_frequency", "validate_non_overlapping_steps",
|
31
|
+
"validate_num_samples_per_step", "validate_num_steps_per_sweep", "validate_nyquist_criterion", "validate_step_interval",
|
32
|
+
"validate_sweep_interval", "validate_swept_center_frequency", "validate_window"
|
29
33
|
]
|
@@ -3,83 +3,142 @@
|
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
5
|
from dataclasses import dataclass
|
6
|
+
from typing import Optional, Any
|
7
|
+
from functools import cached_property
|
6
8
|
|
7
9
|
from spectre_core._file_io import JsonHandler
|
8
10
|
from spectre_core.config import get_configs_dir_path
|
9
11
|
from spectre_core.exceptions import InvalidTagError
|
12
|
+
from ._ptemplates import PName
|
10
13
|
from ._parameters import (
|
11
14
|
Parameter,
|
12
15
|
Parameters,
|
13
16
|
make_parameters
|
14
17
|
)
|
15
18
|
|
16
|
-
@dataclass
|
17
|
-
class
|
19
|
+
@dataclass(frozen=True)
|
20
|
+
class _CaptureConfigKey:
|
21
|
+
"""Defined JSON keys for capture configs.
|
22
|
+
|
23
|
+
:ivar RECEIVER_NAME: The name of the receiver used for capture.
|
24
|
+
:ivar RECEIVER_MODE: The operating mode for the receiver to be used for capture.
|
25
|
+
:ivar PARAMETERS: The user configured parameters given to the receiver at the time of capture.
|
26
|
+
"""
|
18
27
|
RECEIVER_NAME = "receiver_name"
|
19
28
|
RECEIVER_MODE = "receiver_mode"
|
20
29
|
PARAMETERS = "parameters"
|
21
30
|
|
22
31
|
|
23
32
|
class CaptureConfig(JsonHandler):
|
24
|
-
|
25
|
-
|
33
|
+
"""Simple IO interface for a capture configs under a particular tag."""
|
34
|
+
def __init__(
|
35
|
+
self,
|
36
|
+
tag: str
|
37
|
+
) -> None:
|
38
|
+
"""Initialise an instance of `CaptureConfig`.
|
39
|
+
|
40
|
+
:param tag: The tag identifier for the capture config.
|
41
|
+
"""
|
26
42
|
self._validate_tag(tag)
|
27
43
|
self._tag = tag
|
28
44
|
super().__init__(get_configs_dir_path(),
|
29
45
|
f"capture_{tag}")
|
30
46
|
|
31
47
|
@property
|
32
|
-
def tag(
|
33
|
-
|
48
|
+
def tag(
|
49
|
+
self
|
50
|
+
) -> str:
|
51
|
+
"""The tag identifier for the capture config."""
|
34
52
|
return self._tag
|
35
53
|
|
36
54
|
|
37
|
-
def _validate_tag(
|
38
|
-
|
55
|
+
def _validate_tag(
|
56
|
+
self,
|
57
|
+
tag: str
|
58
|
+
) -> None:
|
59
|
+
"""Validate the tag of the capture config.
|
60
|
+
|
61
|
+
Some substrings are reserved for third-party spectrogram data.
|
62
|
+
"""
|
39
63
|
if "_" in tag:
|
40
64
|
raise InvalidTagError(f"Tags cannot contain an underscore. Received {tag}")
|
65
|
+
|
41
66
|
if "callisto" in tag:
|
42
|
-
raise
|
67
|
+
raise ValueError(f"The substring `callisto` is not allowed in a capture config tag.")
|
43
68
|
|
44
69
|
|
45
70
|
@property
|
46
|
-
def receiver_name(
|
47
|
-
|
48
|
-
|
71
|
+
def receiver_name(
|
72
|
+
self
|
73
|
+
) -> str:
|
74
|
+
"""The name of the receiver to be used for capture."""
|
75
|
+
d = self.read(cache=True)
|
76
|
+
return d[_CaptureConfigKey.RECEIVER_NAME]
|
49
77
|
|
50
78
|
|
51
79
|
@property
|
52
|
-
def receiver_mode(
|
53
|
-
|
54
|
-
|
80
|
+
def receiver_mode(
|
81
|
+
self
|
82
|
+
) -> str:
|
83
|
+
"""The operating mode for the receiver to be used for capture."""
|
84
|
+
d = self.read(cache=True)
|
85
|
+
return d[_CaptureConfigKey.RECEIVER_MODE]
|
55
86
|
|
56
87
|
|
57
|
-
@
|
58
|
-
def parameters(
|
59
|
-
|
60
|
-
|
88
|
+
@cached_property
|
89
|
+
def parameters(
|
90
|
+
self
|
91
|
+
) -> Parameters:
|
92
|
+
"""The user-configured parameters provided to the receiver at the time of capture."""
|
93
|
+
d = self.read(cache=True)
|
94
|
+
return make_parameters( d[_CaptureConfigKey.PARAMETERS] )
|
61
95
|
|
62
96
|
|
63
|
-
def get_parameter(
|
64
|
-
|
97
|
+
def get_parameter(
|
98
|
+
self,
|
99
|
+
name: PName
|
100
|
+
) -> Parameter:
|
101
|
+
"""Get a parameter stored by the capture config.
|
102
|
+
|
103
|
+
:param name: The name of the parameter.
|
104
|
+
:return: A `Parameter` instance with `name` and `value` retrieved from the capture
|
105
|
+
configuration file.
|
106
|
+
"""
|
65
107
|
return self.parameters.get_parameter(name)
|
66
108
|
|
67
109
|
|
68
|
-
def get_parameter_value(
|
69
|
-
|
110
|
+
def get_parameter_value(
|
111
|
+
self,
|
112
|
+
name: PName
|
113
|
+
) -> Optional[Any]:
|
114
|
+
"""Get the value of a parameter stored by the capture config.
|
115
|
+
|
116
|
+
For static typing, should be `cast` after return according to the corresponding `PTemplate`.
|
117
|
+
|
118
|
+
:param name: The name of the parameter.
|
119
|
+
:return: The value of the parameter corresponding to `name`. If the JSON value is
|
120
|
+
`null`, this method will return `None`.
|
121
|
+
"""
|
70
122
|
return self.parameters.get_parameter_value(name)
|
71
123
|
|
72
124
|
|
73
|
-
def save_parameters(
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
125
|
+
def save_parameters(
|
126
|
+
self,
|
127
|
+
receiver_name: str,
|
128
|
+
receiver_mode: str,
|
129
|
+
parameters: Parameters,
|
130
|
+
force: bool = False
|
131
|
+
):
|
132
|
+
"""Write the input parameters to a capture config.
|
133
|
+
|
134
|
+
:param receiver_name: The name of the receiver to be used for capture.
|
135
|
+
:param receiver_mode: The operating mode for the receiver to be used for capture.
|
136
|
+
:param parameters: The user-configured parameters provided to the receiver at the time of capture.
|
137
|
+
:param force: If true, force the write if the file already exists in the file system. Defaults to False.
|
138
|
+
"""
|
79
139
|
d = {
|
80
|
-
|
81
|
-
|
82
|
-
|
140
|
+
_CaptureConfigKey.RECEIVER_MODE: receiver_mode,
|
141
|
+
_CaptureConfigKey.RECEIVER_NAME: receiver_name,
|
142
|
+
_CaptureConfigKey.PARAMETERS : parameters.to_dict()
|
83
143
|
}
|
84
|
-
self.save(d,
|
85
|
-
force=force)
|
144
|
+
self.save(d, force=force)
|
@@ -0,0 +1,22 @@
|
|
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 enum import Enum
|
6
|
+
|
7
|
+
class CaptureMode(Enum):
|
8
|
+
"""A default capture mode for `spectre`.
|
9
|
+
|
10
|
+
Each `CaptureMode` has an associated base capture template, which can be fetched using:
|
11
|
+
|
12
|
+
`get_base_capture_template`
|
13
|
+
|
14
|
+
All base capture templates must be registered by one of `CaptureMode`. To introduce a new
|
15
|
+
base capture template, you need to create a new `CaptureMode` constant.
|
16
|
+
|
17
|
+
:ivar FIXED_CENTER_FREQUENCY: Indicates data capture at a fixed center frequency.
|
18
|
+
:ivar SWEPT_CENTER_FREQUENCY: Indicates data capture where the center frequency is continually sweeping
|
19
|
+
in fixed increments.
|
20
|
+
"""
|
21
|
+
FIXED_CENTER_FREQUENCY = "fixed_center_frequency"
|
22
|
+
SWEPT_CENTER_FREQUENCY = "swept_center_frequency"
|