spectre-core 0.0.8__py3-none-any.whl → 0.0.10__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/__init__.py +0 -3
- spectre_core/_file_io/__init__.py +15 -0
- spectre_core/_file_io/file_handlers.py +128 -0
- spectre_core/capture_configs/__init__.py +29 -0
- spectre_core/capture_configs/_capture_config.py +85 -0
- spectre_core/capture_configs/_capture_templates.py +222 -0
- spectre_core/capture_configs/_parameters.py +110 -0
- spectre_core/capture_configs/_pconstraints.py +82 -0
- spectre_core/capture_configs/_ptemplates.py +450 -0
- spectre_core/capture_configs/_pvalidators.py +173 -0
- spectre_core/chunks/__init__.py +17 -201
- spectre_core/chunks/{base.py → _base.py} +15 -60
- spectre_core/chunks/_chunks.py +200 -0
- spectre_core/chunks/{factory.py → _factory.py} +6 -7
- spectre_core/chunks/library/{callisto/chunk.py → _callisto.py} +4 -7
- spectre_core/chunks/library/{fixed/chunk.py → _fixed_center_frequency.py} +7 -64
- spectre_core/chunks/library/_swept_center_frequency.py +103 -0
- spectre_core/config/__init__.py +20 -0
- spectre_core/config/_paths.py +77 -0
- spectre_core/config/_time_formats.py +15 -0
- spectre_core/exceptions.py +4 -5
- spectre_core/logging/__init__.py +11 -0
- spectre_core/logging/_configure.py +35 -0
- spectre_core/logging/_decorators.py +19 -0
- spectre_core/{logging.py → logging/_log_handlers.py} +13 -58
- spectre_core/plotting/__init__.py +7 -1
- spectre_core/plotting/{base.py → _base.py} +40 -20
- spectre_core/plotting/_format.py +18 -0
- spectre_core/plotting/{panel_stack.py → _panel_stack.py} +50 -48
- spectre_core/plotting/_panels.py +234 -0
- spectre_core/post_processing/__init__.py +14 -0
- spectre_core/post_processing/_base.py +119 -0
- spectre_core/post_processing/_factory.py +23 -0
- spectre_core/post_processing/_post_processor.py +40 -0
- spectre_core/post_processing/library/_fixed_center_frequency.py +115 -0
- spectre_core/post_processing/library/_swept_center_frequency.py +382 -0
- spectre_core/receivers/__init__.py +12 -2
- spectre_core/receivers/_base.py +352 -0
- spectre_core/receivers/{factory.py → _factory.py} +2 -2
- spectre_core/receivers/_spec_names.py +20 -0
- spectre_core/receivers/gr/__init__.py +3 -0
- spectre_core/receivers/gr/_base.py +33 -0
- spectre_core/receivers/gr/_rsp1a.py +158 -0
- spectre_core/receivers/gr/_test.py +123 -0
- spectre_core/receivers/library/_rsp1a.py +61 -0
- spectre_core/receivers/library/_test.py +221 -0
- spectre_core/spectrograms/__init__.py +18 -0
- spectre_core/spectrograms/{analytical.py → _analytical.py} +29 -27
- spectre_core/spectrograms/{array_operations.py → _array_operations.py} +47 -1
- spectre_core/spectrograms/{spectrogram.py → _spectrogram.py} +62 -35
- spectre_core/spectrograms/{transform.py → _transform.py} +76 -89
- spectre_core/{receivers/library → wgetting}/__init__.py +4 -2
- spectre_core/wgetting/_callisto.py +155 -0
- {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/METADATA +1 -1
- spectre_core-0.0.10.dist-info/RECORD +63 -0
- spectre_core/cfg.py +0 -116
- spectre_core/chunks/library/__init__.py +0 -8
- spectre_core/chunks/library/sweep/__init__.py +0 -0
- spectre_core/chunks/library/sweep/chunk.py +0 -400
- spectre_core/dynamic_imports.py +0 -22
- spectre_core/file_handlers/base.py +0 -68
- spectre_core/file_handlers/configs.py +0 -271
- spectre_core/file_handlers/json.py +0 -40
- spectre_core/file_handlers/text.py +0 -21
- spectre_core/plotting/factory.py +0 -26
- spectre_core/plotting/format.py +0 -19
- spectre_core/plotting/library/__init__.py +0 -7
- spectre_core/plotting/library/frequency_cuts/panel.py +0 -74
- spectre_core/plotting/library/integral_over_frequency/panel.py +0 -34
- spectre_core/plotting/library/spectrogram/panel.py +0 -92
- spectre_core/plotting/library/time_cuts/panel.py +0 -77
- spectre_core/plotting/panel_register.py +0 -13
- spectre_core/receivers/base.py +0 -415
- spectre_core/receivers/library/rsp1a/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/fixed.py +0 -104
- spectre_core/receivers/library/rsp1a/gr/sweep.py +0 -129
- spectre_core/receivers/library/rsp1a/receiver.py +0 -68
- spectre_core/receivers/library/rspduo/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +0 -114
- spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +0 -131
- spectre_core/receivers/library/rspduo/gr/tuner_2_fixed.py +0 -120
- spectre_core/receivers/library/rspduo/gr/tuner_2_sweep.py +0 -119
- spectre_core/receivers/library/rspduo/receiver.py +0 -97
- spectre_core/receivers/library/test/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/cosine_signal_1.py +0 -83
- spectre_core/receivers/library/test/gr/tagged_staircase.py +0 -93
- spectre_core/receivers/library/test/receiver.py +0 -178
- spectre_core/receivers/validators.py +0 -193
- spectre_core/watchdog/__init__.py +0 -6
- spectre_core/watchdog/base.py +0 -105
- spectre_core/watchdog/factory.py +0 -22
- spectre_core/watchdog/library/__init__.py +0 -10
- spectre_core/watchdog/library/fixed/__init__.py +0 -0
- spectre_core/watchdog/library/fixed/event_handler.py +0 -41
- spectre_core/watchdog/library/sweep/event_handler.py +0 -55
- spectre_core/watchdog/post_processor.py +0 -50
- spectre_core/web_fetch/callisto.py +0 -101
- spectre_core-0.0.8.dist-info/RECORD +0 -74
- /spectre_core/chunks/{chunk_register.py → _register.py} +0 -0
- /spectre_core/{watchdog/event_handler_register.py → post_processing/_register.py} +0 -0
- /spectre_core/receivers/{receiver_register.py → _register.py} +0 -0
- /spectre_core/{chunks/library/callisto/__init__.py → receivers/gr/_rspduo.py} +0 -0
- /spectre_core/{chunks/library/fixed/__init__.py → receivers/library/_rspduo.py} +0 -0
- {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/LICENSE +0 -0
- {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/WHEEL +0 -0
- {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,450 @@
|
|
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 typing import Optional, TypeVar, Any
|
6
|
+
from copy import deepcopy
|
7
|
+
from textwrap import dedent
|
8
|
+
from dataclasses import dataclass
|
9
|
+
|
10
|
+
from ._pconstraints import PConstraint, PConstraints
|
11
|
+
from ._parameters import Parameter
|
12
|
+
|
13
|
+
T = TypeVar('T')
|
14
|
+
|
15
|
+
class PTemplate:
|
16
|
+
"""A parameter template.
|
17
|
+
|
18
|
+
Constrain the value and type that a parameter can take.
|
19
|
+
"""
|
20
|
+
def __init__(self,
|
21
|
+
name: str,
|
22
|
+
ptype: T,
|
23
|
+
default: Optional[T] = None,
|
24
|
+
nullable: bool = False,
|
25
|
+
enforce_default: Optional[bool] = False,
|
26
|
+
help: Optional[str] = None,
|
27
|
+
pconstraints: Optional[list[PConstraint]] = None):
|
28
|
+
if not callable(ptype):
|
29
|
+
raise TypeError("ptype must be callable.")
|
30
|
+
|
31
|
+
self._name = name
|
32
|
+
self._ptype = ptype
|
33
|
+
self._default = default
|
34
|
+
self._nullable = nullable
|
35
|
+
self._enforce_default = enforce_default
|
36
|
+
self._help = dedent(help).strip().replace("\n", " ") if help else "No help has been provided."
|
37
|
+
self._pconstraints: list[PConstraint] = pconstraints or []
|
38
|
+
|
39
|
+
|
40
|
+
@property
|
41
|
+
def name(self) -> str:
|
42
|
+
"""The name of the parameter."""
|
43
|
+
return self._name
|
44
|
+
|
45
|
+
|
46
|
+
@property
|
47
|
+
def ptype(self) -> T:
|
48
|
+
"""The parameter type."""
|
49
|
+
return self._ptype
|
50
|
+
|
51
|
+
|
52
|
+
@property
|
53
|
+
def default(self) -> Optional[T]:
|
54
|
+
"""The value of the parameter, if the value is unspecified."""
|
55
|
+
return self._default
|
56
|
+
|
57
|
+
|
58
|
+
@default.setter
|
59
|
+
def default(self, value: T) -> None:
|
60
|
+
"""Update the default of a ptemplate"""
|
61
|
+
self._default = value
|
62
|
+
|
63
|
+
|
64
|
+
@property
|
65
|
+
def nullable(self) -> bool:
|
66
|
+
"""Whether the value of the parameter is allowed to be of None type."""
|
67
|
+
return self._nullable
|
68
|
+
|
69
|
+
|
70
|
+
@property
|
71
|
+
def enforce_default(self) -> bool:
|
72
|
+
"""Whether the provided default value is enforced."""
|
73
|
+
return self._enforce_default
|
74
|
+
|
75
|
+
|
76
|
+
@enforce_default.setter
|
77
|
+
def enforce_default(self, value: bool) -> None:
|
78
|
+
"""Set whether the provided default value is enforced."""
|
79
|
+
self._enforce_default = value
|
80
|
+
|
81
|
+
|
82
|
+
@property
|
83
|
+
def help(self) -> str:
|
84
|
+
"""A description of what the parameter is, and the value it stores."""
|
85
|
+
return self._help
|
86
|
+
|
87
|
+
|
88
|
+
def add_pconstraint(self,
|
89
|
+
pconstraint: PConstraint) -> None:
|
90
|
+
self._pconstraints.append(pconstraint)
|
91
|
+
|
92
|
+
|
93
|
+
def _cast(self,
|
94
|
+
value: Any) -> T:
|
95
|
+
"""Cast the input value to the ptype of this template"""
|
96
|
+
try:
|
97
|
+
return self._ptype(value)
|
98
|
+
except (TypeError, ValueError) as e:
|
99
|
+
raise ValueError(f"Could not cast '{value}' to '{self._ptype.__name__}': {e}")
|
100
|
+
|
101
|
+
|
102
|
+
def _constrain(self,
|
103
|
+
value: T) -> T:
|
104
|
+
"""Constrain the input value according to constraints of the template."""
|
105
|
+
|
106
|
+
if self._enforce_default and value != self._default:
|
107
|
+
raise ValueError(f"The default value of '{self._default}' "
|
108
|
+
f"is required for the parameter '{self._name}'.")
|
109
|
+
|
110
|
+
# apply existing pconstraints
|
111
|
+
for constraint in self._pconstraints:
|
112
|
+
try:
|
113
|
+
constraint.constrain(value)
|
114
|
+
except ValueError as e:
|
115
|
+
raise ValueError(f"PConstraint '{constraint.__class__.__name__}' failed for the parameter '{self._name}': {e}")
|
116
|
+
except Exception as e:
|
117
|
+
raise RuntimeError(f"An unexpected error occurred while applying the pconstraint '{constraint.__class__.__name__}' to "
|
118
|
+
f"'{self.name}': {e}")
|
119
|
+
return value
|
120
|
+
|
121
|
+
|
122
|
+
def apply_template(self,
|
123
|
+
value: Optional[Any]) -> T:
|
124
|
+
"""Cast the value and constrain it according to this ptemplate."""
|
125
|
+
if value is None:
|
126
|
+
if self._default is not None:
|
127
|
+
value = self._default
|
128
|
+
elif not self._nullable:
|
129
|
+
raise ValueError(f"The parameter '{self._name}' is not nullable, "
|
130
|
+
f"but no value or default has been provided. "
|
131
|
+
f"Either provide a value, or provide a default.")
|
132
|
+
else:
|
133
|
+
return None
|
134
|
+
|
135
|
+
value = self._cast(value)
|
136
|
+
value = self._constrain(value)
|
137
|
+
return value
|
138
|
+
|
139
|
+
|
140
|
+
def make_parameter(self,
|
141
|
+
value: Optional[Any] = None) -> Parameter:
|
142
|
+
value = self.apply_template(value)
|
143
|
+
return Parameter(self._name, value)
|
144
|
+
|
145
|
+
|
146
|
+
def to_dict(self) -> dict:
|
147
|
+
"""Convert the template to a dictionary representation."""
|
148
|
+
return {
|
149
|
+
"name": self._name,
|
150
|
+
"type": str(self._ptype),
|
151
|
+
"default": self._default,
|
152
|
+
"enforce_default": self._enforce_default,
|
153
|
+
"help": self._help,
|
154
|
+
"constraints": [f"{constraint}" for constraint in self._pconstraints]
|
155
|
+
}
|
156
|
+
|
157
|
+
|
158
|
+
@dataclass(frozen=True)
|
159
|
+
class PNames:
|
160
|
+
"""A centralised store of default parameter template names"""
|
161
|
+
CENTER_FREQUENCY : str = "center_frequency"
|
162
|
+
MIN_FREQUENCY : str = "min_frequency"
|
163
|
+
MAX_FREQUENCY : str = "max_frequency"
|
164
|
+
FREQUENCY_STEP : str = "frequency_step"
|
165
|
+
FREQUENCY : str = "frequency"
|
166
|
+
BANDWIDTH : str = "bandwidth"
|
167
|
+
SAMPLE_RATE : str = "sample_rate"
|
168
|
+
IF_GAIN : str = "if_gain"
|
169
|
+
RF_GAIN : str = "rf_gain"
|
170
|
+
AMPLITUDE : str = "amplitude"
|
171
|
+
FREQUENCY : str = "frequency"
|
172
|
+
TIME_RESOLUTION : str = "time_resolution"
|
173
|
+
FREQUENCY_RESOLUTION : str = "frequency_resolution"
|
174
|
+
TIME_RANGE : str = "time_range"
|
175
|
+
BATCH_SIZE : str = "batch_size"
|
176
|
+
WINDOW_TYPE : str = "window_type"
|
177
|
+
WINDOW_HOP : str = "window_hop"
|
178
|
+
WINDOW_SIZE : str = "window_size"
|
179
|
+
EVENT_HANDLER_KEY : str = "event_handler_key"
|
180
|
+
WATCH_EXTENSION : str = "watch_extension"
|
181
|
+
CHUNK_KEY : str = "chunk_key"
|
182
|
+
SAMPLES_PER_STEP : str = "samples_per_step"
|
183
|
+
MIN_SAMPLES_PER_STEP : str = "min_samples_per_step"
|
184
|
+
MAX_SAMPLES_PER_STEP : str = "max_samples_per_step"
|
185
|
+
STEP_INCREMENT : str = "step_increment"
|
186
|
+
ORIGIN : str = "origin"
|
187
|
+
TELESCOPE : str = "telescope"
|
188
|
+
INSTRUMENT : str = "instrument"
|
189
|
+
OBJECT : str = "object"
|
190
|
+
OBS_LAT : str = "obs_lat"
|
191
|
+
OBS_LON : str = "obs_lon"
|
192
|
+
OBS_ALT : str = "obs_alt"
|
193
|
+
|
194
|
+
#
|
195
|
+
# All stored base ptemplates
|
196
|
+
#
|
197
|
+
_base_ptemplates = {
|
198
|
+
PNames.CENTER_FREQUENCY: PTemplate(PNames.CENTER_FREQUENCY,
|
199
|
+
float,
|
200
|
+
help = """
|
201
|
+
The center frequency of the SDR in Hz.
|
202
|
+
This value determines the midpoint of the frequency range
|
203
|
+
being processed.
|
204
|
+
""",
|
205
|
+
pconstraints=[
|
206
|
+
PConstraints.enforce_positive
|
207
|
+
]),
|
208
|
+
PNames.MIN_FREQUENCY: PTemplate(PNames.MIN_FREQUENCY,
|
209
|
+
float,
|
210
|
+
help = """
|
211
|
+
The minimum center frequency, in Hz, for the frequency sweep.
|
212
|
+
""",
|
213
|
+
pconstraints=[
|
214
|
+
PConstraints.enforce_positive
|
215
|
+
]),
|
216
|
+
PNames.MAX_FREQUENCY: PTemplate(PNames.MAX_FREQUENCY,
|
217
|
+
float,
|
218
|
+
help = """
|
219
|
+
The maximum center frequency, in Hz, for the frequency sweep.
|
220
|
+
""",
|
221
|
+
pconstraints=[
|
222
|
+
PConstraints.enforce_positive
|
223
|
+
]),
|
224
|
+
PNames.FREQUENCY_STEP: PTemplate(PNames.FREQUENCY_STEP,
|
225
|
+
float,
|
226
|
+
help = """
|
227
|
+
The amount, in Hz, by which the center frequency is incremented
|
228
|
+
for each step in the frequency sweep.
|
229
|
+
""",
|
230
|
+
pconstraints=[
|
231
|
+
PConstraints.enforce_positive
|
232
|
+
]),
|
233
|
+
PNames.BANDWIDTH: PTemplate(PNames.BANDWIDTH,
|
234
|
+
float,
|
235
|
+
help = """
|
236
|
+
The frequency range in Hz the signal will occupy without
|
237
|
+
significant attenutation.
|
238
|
+
""",
|
239
|
+
pconstraints=[
|
240
|
+
PConstraints.enforce_positive
|
241
|
+
]),
|
242
|
+
PNames.SAMPLE_RATE: PTemplate(PNames.SAMPLE_RATE,
|
243
|
+
int,
|
244
|
+
help = """
|
245
|
+
The number of samples per second in Hz.
|
246
|
+
""",
|
247
|
+
pconstraints=[
|
248
|
+
PConstraints.enforce_positive
|
249
|
+
]),
|
250
|
+
PNames.IF_GAIN: PTemplate(PNames.IF_GAIN,
|
251
|
+
float,
|
252
|
+
help = """
|
253
|
+
The intermediate frequency gain, in dB.
|
254
|
+
Negative value indicates attenuation.
|
255
|
+
""",
|
256
|
+
pconstraints=[
|
257
|
+
PConstraints.enforce_negative
|
258
|
+
]),
|
259
|
+
PNames.RF_GAIN: PTemplate(PNames.RF_GAIN,
|
260
|
+
float,
|
261
|
+
help = """
|
262
|
+
The radio frequency gain, in dB.
|
263
|
+
Negative value indicates attenuation.
|
264
|
+
""",
|
265
|
+
pconstraints=[
|
266
|
+
PConstraints.enforce_non_positive
|
267
|
+
]),
|
268
|
+
PNames.EVENT_HANDLER_KEY: PTemplate(PNames.EVENT_HANDLER_KEY,
|
269
|
+
str,
|
270
|
+
help = """
|
271
|
+
Identifies which post-processing functions to invoke
|
272
|
+
on newly created files.
|
273
|
+
"""),
|
274
|
+
PNames.CHUNK_KEY: PTemplate(PNames.CHUNK_KEY,
|
275
|
+
str,
|
276
|
+
help = """
|
277
|
+
Identifies the type of data is stored in each chunk.
|
278
|
+
""",
|
279
|
+
),
|
280
|
+
PNames.WINDOW_SIZE: PTemplate(PNames.WINDOW_SIZE,
|
281
|
+
int,
|
282
|
+
help = """
|
283
|
+
The size of the window, in samples, when performing the
|
284
|
+
Short Time FFT.
|
285
|
+
""",
|
286
|
+
pconstraints=[
|
287
|
+
PConstraints.enforce_positive,
|
288
|
+
PConstraints.power_of_two
|
289
|
+
]),
|
290
|
+
PNames.WINDOW_HOP: PTemplate(PNames.WINDOW_HOP,
|
291
|
+
int,
|
292
|
+
help = """
|
293
|
+
How much the window is shifted, in samples,
|
294
|
+
when performing the Short Time FFT.
|
295
|
+
""",
|
296
|
+
pconstraints=[
|
297
|
+
PConstraints.enforce_positive
|
298
|
+
]),
|
299
|
+
PNames.WINDOW_TYPE: PTemplate(PNames.WINDOW_TYPE,
|
300
|
+
str,
|
301
|
+
help = """
|
302
|
+
The type of window applied when performing the Short
|
303
|
+
Time FFT.
|
304
|
+
""",
|
305
|
+
),
|
306
|
+
PNames.WATCH_EXTENSION: PTemplate(PNames.WATCH_EXTENSION,
|
307
|
+
str,
|
308
|
+
help = """
|
309
|
+
Post-processing is triggered by newly created files with this extension.
|
310
|
+
Extensions are specified without the '.' character.
|
311
|
+
""",
|
312
|
+
),
|
313
|
+
PNames.TIME_RESOLUTION: PTemplate(PNames.TIME_RESOLUTION,
|
314
|
+
float,
|
315
|
+
nullable=True,
|
316
|
+
help = """
|
317
|
+
Batched spectrograms are smoothed by averaging up to the time resolution,
|
318
|
+
specified in seconds.
|
319
|
+
""",
|
320
|
+
pconstraints=[
|
321
|
+
PConstraints.enforce_non_negative
|
322
|
+
]),
|
323
|
+
PNames.FREQUENCY_RESOLUTION: PTemplate(PNames.FREQUENCY_RESOLUTION,
|
324
|
+
float,
|
325
|
+
nullable=True,
|
326
|
+
help = """
|
327
|
+
Batched spectrograms are smoothed by averaging up to the frequency resolution,
|
328
|
+
specified in Hz.
|
329
|
+
""",
|
330
|
+
pconstraints=[
|
331
|
+
PConstraints.enforce_non_negative
|
332
|
+
]),
|
333
|
+
PNames.TIME_RANGE: PTemplate(PNames.TIME_RANGE,
|
334
|
+
float,
|
335
|
+
nullable=True,
|
336
|
+
help = """
|
337
|
+
Batched spectrograms are stitched together until
|
338
|
+
the time range, in seconds, is surpassed.
|
339
|
+
""",
|
340
|
+
pconstraints=[
|
341
|
+
PConstraints.enforce_non_negative
|
342
|
+
]),
|
343
|
+
PNames.BATCH_SIZE: PTemplate(PNames.BATCH_SIZE,
|
344
|
+
int,
|
345
|
+
help = """
|
346
|
+
SDR data is collected in batches of this size, specified
|
347
|
+
in seconds.
|
348
|
+
""",
|
349
|
+
pconstraints=[
|
350
|
+
PConstraints.enforce_positive
|
351
|
+
]),
|
352
|
+
PNames.SAMPLES_PER_STEP: PTemplate(PNames.SAMPLES_PER_STEP,
|
353
|
+
int,
|
354
|
+
help = """
|
355
|
+
The number of samples taken at each center frequency in the sweep.
|
356
|
+
This may vary slightly from what is specified due to the nature of
|
357
|
+
GNU Radio runtime.
|
358
|
+
""",
|
359
|
+
pconstraints=[
|
360
|
+
PConstraints.enforce_positive
|
361
|
+
]),
|
362
|
+
PNames.ORIGIN: PTemplate(PNames.ORIGIN,
|
363
|
+
str,
|
364
|
+
nullable=True,
|
365
|
+
help="""
|
366
|
+
Corresponds to the FITS keyword ORIGIN.
|
367
|
+
"""),
|
368
|
+
PNames.TELESCOPE: PTemplate(PNames.TELESCOPE,
|
369
|
+
str,
|
370
|
+
nullable=True,
|
371
|
+
help="""
|
372
|
+
Corresponds to the FITS keyword TELESCOP.
|
373
|
+
"""),
|
374
|
+
PNames.INSTRUMENT: PTemplate(PNames.INSTRUMENT,
|
375
|
+
str,
|
376
|
+
nullable=True,
|
377
|
+
help="""
|
378
|
+
Corresponds to the FITS keyword INSTRUME.
|
379
|
+
"""),
|
380
|
+
PNames.OBJECT: PTemplate(PNames.OBJECT,
|
381
|
+
str,
|
382
|
+
nullable=True,
|
383
|
+
help="""
|
384
|
+
Corresponds to the FITS keyword OBJECT.
|
385
|
+
"""),
|
386
|
+
PNames.OBS_LAT: PTemplate(PNames.OBS_LAT,
|
387
|
+
float,
|
388
|
+
nullable=True,
|
389
|
+
help="""
|
390
|
+
Corresponds to the FITS keyword OBS_LAT.
|
391
|
+
"""),
|
392
|
+
PNames.OBS_LON: PTemplate(PNames.OBS_LON,
|
393
|
+
float,
|
394
|
+
nullable=True,
|
395
|
+
help="""
|
396
|
+
Corresponds to the FITS keyword OBS_LONG.
|
397
|
+
"""),
|
398
|
+
PNames.OBS_ALT: PTemplate(PNames.OBS_ALT,
|
399
|
+
float,
|
400
|
+
nullable=True,
|
401
|
+
help="""
|
402
|
+
Corresponds to the FITS keyword OBS_ALT.
|
403
|
+
"""),
|
404
|
+
PNames.AMPLITUDE: PTemplate(PNames.AMPLITUDE,
|
405
|
+
float,
|
406
|
+
help="""
|
407
|
+
The amplitude of the signal.
|
408
|
+
"""),
|
409
|
+
PNames.FREQUENCY: PTemplate(PNames.FREQUENCY,
|
410
|
+
float,
|
411
|
+
help="""
|
412
|
+
The frequency of the signal, in Hz.
|
413
|
+
"""),
|
414
|
+
PNames.MIN_SAMPLES_PER_STEP: PTemplate(PNames.MIN_SAMPLES_PER_STEP,
|
415
|
+
int,
|
416
|
+
help="""
|
417
|
+
The number of samples in the shortest step of the staircase.
|
418
|
+
""",
|
419
|
+
pconstraints=[
|
420
|
+
PConstraints.enforce_positive
|
421
|
+
]),
|
422
|
+
PNames.MAX_SAMPLES_PER_STEP: PTemplate(PNames.MAX_SAMPLES_PER_STEP,
|
423
|
+
int,
|
424
|
+
help="""
|
425
|
+
The number of samples in the longest step of the staircase.
|
426
|
+
""",
|
427
|
+
pconstraints=[
|
428
|
+
PConstraints.enforce_positive
|
429
|
+
]),
|
430
|
+
PNames.STEP_INCREMENT: PTemplate(PNames.STEP_INCREMENT,
|
431
|
+
int,
|
432
|
+
help="""
|
433
|
+
The length by which each step in the staircase is incremented.
|
434
|
+
""",
|
435
|
+
pconstraints=[
|
436
|
+
PConstraints.enforce_positive,
|
437
|
+
])
|
438
|
+
|
439
|
+
|
440
|
+
}
|
441
|
+
|
442
|
+
T = TypeVar('T')
|
443
|
+
def get_base_ptemplate(
|
444
|
+
parameter_name: str,
|
445
|
+
) -> PTemplate:
|
446
|
+
"""Create a fresh deep copy of a pre-defined ptemplate"""
|
447
|
+
if parameter_name not in _base_ptemplates:
|
448
|
+
raise KeyError(f"No ptemplate found for the parameter name '{parameter_name}'. "
|
449
|
+
f"Expected one of {list(_base_ptemplates.keys())}")
|
450
|
+
return deepcopy( _base_ptemplates[parameter_name] )
|
@@ -0,0 +1,173 @@
|
|
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
|
+
from dataclasses import dataclass
|
9
|
+
from math import floor
|
10
|
+
from scipy.signal import get_window
|
11
|
+
from numbers import Number
|
12
|
+
from warnings import warn
|
13
|
+
from typing import Optional
|
14
|
+
|
15
|
+
from ._parameters import Parameters
|
16
|
+
from ._ptemplates import PNames
|
17
|
+
|
18
|
+
|
19
|
+
def _validate_window(
|
20
|
+
parameters: Parameters
|
21
|
+
) -> 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)
|
26
|
+
|
27
|
+
window_interval = window_size*(1 / sample_rate)
|
28
|
+
if window_interval > batch_size:
|
29
|
+
raise ValueError((f"The windowing interval must be strictly less than the chunk size. "
|
30
|
+
f"Computed the windowing interval to be {window_interval} [s], "
|
31
|
+
f"but the chunk size is {batch_size} [s]"))
|
32
|
+
|
33
|
+
try:
|
34
|
+
_ = get_window(window_type, window_size)
|
35
|
+
except Exception as e:
|
36
|
+
raise Exception((f"An error has occurred while validating the window. "
|
37
|
+
f"Got {str(e)}"))
|
38
|
+
|
39
|
+
|
40
|
+
def _validate_nyquist_criterion(
|
41
|
+
parameters: Parameters
|
42
|
+
) -> None:
|
43
|
+
sample_rate = parameters.get_parameter_value(PNames.SAMPLE_RATE)
|
44
|
+
bandwidth = parameters.get_parameter_value(PNames.BANDWIDTH)
|
45
|
+
|
46
|
+
if sample_rate < bandwidth:
|
47
|
+
raise ValueError((f"Nyquist criterion has not been satisfied. "
|
48
|
+
f"Sample rate must be greater than or equal to the bandwidth. "
|
49
|
+
f"Got sample rate {sample_rate} [Hz], and bandwidth {bandwidth} [Hz]"))
|
50
|
+
|
51
|
+
|
52
|
+
def _compute_num_steps_per_sweep(min_freq: float,
|
53
|
+
max_freq: float,
|
54
|
+
samp_rate: int,
|
55
|
+
freq_step: float) -> int:
|
56
|
+
return floor((max_freq - min_freq + samp_rate/2) / freq_step)
|
57
|
+
|
58
|
+
|
59
|
+
def _validate_num_steps_per_sweep(
|
60
|
+
parameters: Parameters
|
61
|
+
) -> 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)
|
66
|
+
|
67
|
+
num_steps_per_sweep = _compute_num_steps_per_sweep(min_freq,
|
68
|
+
max_freq,
|
69
|
+
sample_rate,
|
70
|
+
freq_step)
|
71
|
+
if num_steps_per_sweep <= 1:
|
72
|
+
raise ValueError((f"We need strictly greater than one step per sweep. "
|
73
|
+
f"Computed {num_steps_per_sweep} step per sweep"))
|
74
|
+
|
75
|
+
|
76
|
+
def _validate_sweep_interval(
|
77
|
+
parameters: Parameters
|
78
|
+
) -> 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)
|
85
|
+
|
86
|
+
num_steps_per_sweep = _compute_num_steps_per_sweep(min_freq,
|
87
|
+
max_freq,
|
88
|
+
sample_rate,
|
89
|
+
freq_step)
|
90
|
+
num_samples_per_sweep = num_steps_per_sweep * samples_per_step
|
91
|
+
sweep_interval = num_samples_per_sweep * 1/sample_rate
|
92
|
+
if sweep_interval > batch_size:
|
93
|
+
raise ValueError((f"Sweep interval must be less than the chunk size. "
|
94
|
+
f"The computed sweep interval is {sweep_interval} [s], "
|
95
|
+
f"but the given chunk size is {batch_size} [s]"))
|
96
|
+
|
97
|
+
|
98
|
+
def _validate_num_samples_per_step(
|
99
|
+
parameters: Parameters
|
100
|
+
) -> None:
|
101
|
+
|
102
|
+
window_size = parameters.get_parameter_value(PNames.WINDOW_SIZE)
|
103
|
+
samples_per_step = parameters.get_parameter_value(PNames.SAMPLES_PER_STEP)
|
104
|
+
|
105
|
+
if window_size >= samples_per_step:
|
106
|
+
raise ValueError((f"Window size must be strictly less than the number of samples per step. "
|
107
|
+
f"Got window size {window_size} [samples], which is more than or equal "
|
108
|
+
f"to the number of samples per step {samples_per_step}"))
|
109
|
+
|
110
|
+
|
111
|
+
def _validate_non_overlapping_steps(
|
112
|
+
parameters: Parameters
|
113
|
+
) -> None:
|
114
|
+
|
115
|
+
freq_step = parameters.get_parameter_value(PNames.FREQUENCY_STEP)
|
116
|
+
sample_rate = parameters.get_parameter_value(PNames.SAMPLE_RATE)
|
117
|
+
|
118
|
+
if freq_step < sample_rate:
|
119
|
+
raise NotImplementedError(f"SPECTRE does not yet support spectral steps overlapping in frequency. "
|
120
|
+
f"Got frequency step {freq_step * 1e-6} [MHz] which is less than the sample "
|
121
|
+
f"rate {sample_rate * 1e-6} [MHz]")
|
122
|
+
|
123
|
+
|
124
|
+
def _validate_step_interval(
|
125
|
+
parameters: Parameters,
|
126
|
+
api_latency: Number
|
127
|
+
) -> None:
|
128
|
+
|
129
|
+
samples_per_step = parameters.get_parameter_value(PNames.SAMPLES_PER_STEP)
|
130
|
+
sample_rate = parameters.get_parameter_value(PNames.SAMPLE_RATE)
|
131
|
+
|
132
|
+
step_interval = samples_per_step * 1/ sample_rate # [s]
|
133
|
+
if step_interval < api_latency:
|
134
|
+
warning_message = (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!")
|
136
|
+
warn(warning_message)
|
137
|
+
_LOGGER.warning(warning_message)
|
138
|
+
|
139
|
+
|
140
|
+
def _validate_fixed_center_frequency_parameters(
|
141
|
+
parameters: Parameters
|
142
|
+
) -> None:
|
143
|
+
_validate_nyquist_criterion(parameters)
|
144
|
+
_validate_window(parameters)
|
145
|
+
|
146
|
+
|
147
|
+
def _validate_swept_center_frequency_parameters(
|
148
|
+
parameters: Parameters,
|
149
|
+
api_retuning_latency: Optional[Number] = None,
|
150
|
+
) -> None:
|
151
|
+
_validate_nyquist_criterion(parameters)
|
152
|
+
_validate_window(parameters)
|
153
|
+
_validate_non_overlapping_steps(parameters)
|
154
|
+
_validate_num_steps_per_sweep(parameters)
|
155
|
+
_validate_num_samples_per_step(parameters)
|
156
|
+
_validate_sweep_interval(parameters)
|
157
|
+
|
158
|
+
if api_retuning_latency is not None:
|
159
|
+
_validate_step_interval(parameters, api_retuning_latency)
|
160
|
+
|
161
|
+
|
162
|
+
@dataclass(frozen=True)
|
163
|
+
class PValidators:
|
164
|
+
window = _validate_window
|
165
|
+
nyquist_criterion = _validate_nyquist_criterion
|
166
|
+
step_interval = _validate_step_interval
|
167
|
+
non_overlapping_steps = _validate_non_overlapping_steps
|
168
|
+
num_steps_per_sweep = _validate_num_steps_per_sweep
|
169
|
+
num_samples_per_step = _validate_num_samples_per_step
|
170
|
+
sweep_interval = _validate_sweep_interval
|
171
|
+
step_interval = _validate_step_interval
|
172
|
+
fixed_center_frequency = _validate_fixed_center_frequency_parameters
|
173
|
+
swept_center_frequency = _validate_swept_center_frequency_parameters
|