spectre-core 0.0.9__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.
Files changed (106) hide show
  1. spectre_core/__init__.py +0 -3
  2. spectre_core/_file_io/__init__.py +15 -0
  3. spectre_core/_file_io/file_handlers.py +128 -0
  4. spectre_core/capture_configs/__init__.py +29 -0
  5. spectre_core/capture_configs/_capture_config.py +85 -0
  6. spectre_core/capture_configs/_capture_templates.py +222 -0
  7. spectre_core/capture_configs/_parameters.py +110 -0
  8. spectre_core/capture_configs/_pconstraints.py +82 -0
  9. spectre_core/capture_configs/_ptemplates.py +450 -0
  10. spectre_core/capture_configs/_pvalidators.py +173 -0
  11. spectre_core/chunks/__init__.py +17 -201
  12. spectre_core/chunks/{base.py → _base.py} +15 -60
  13. spectre_core/chunks/_chunks.py +200 -0
  14. spectre_core/chunks/{factory.py → _factory.py} +6 -7
  15. spectre_core/chunks/library/{callisto/chunk.py → _callisto.py} +4 -7
  16. spectre_core/chunks/library/{fixed/chunk.py → _fixed_center_frequency.py} +7 -64
  17. spectre_core/chunks/library/_swept_center_frequency.py +103 -0
  18. spectre_core/config/__init__.py +20 -0
  19. spectre_core/config/_paths.py +77 -0
  20. spectre_core/config/_time_formats.py +15 -0
  21. spectre_core/exceptions.py +4 -5
  22. spectre_core/logging/__init__.py +11 -0
  23. spectre_core/logging/_configure.py +35 -0
  24. spectre_core/logging/_decorators.py +19 -0
  25. spectre_core/{logging.py → logging/_log_handlers.py} +13 -58
  26. spectre_core/plotting/__init__.py +7 -1
  27. spectre_core/plotting/{base.py → _base.py} +40 -20
  28. spectre_core/plotting/_format.py +18 -0
  29. spectre_core/plotting/{panel_stack.py → _panel_stack.py} +48 -48
  30. spectre_core/plotting/_panels.py +234 -0
  31. spectre_core/post_processing/__init__.py +10 -2
  32. spectre_core/post_processing/_base.py +119 -0
  33. spectre_core/post_processing/{factory.py → _factory.py} +7 -6
  34. spectre_core/post_processing/{post_processor.py → _post_processor.py} +3 -3
  35. spectre_core/post_processing/library/_fixed_center_frequency.py +115 -0
  36. spectre_core/post_processing/library/_swept_center_frequency.py +382 -0
  37. spectre_core/receivers/__init__.py +12 -2
  38. spectre_core/receivers/_base.py +352 -0
  39. spectre_core/receivers/{factory.py → _factory.py} +2 -2
  40. spectre_core/receivers/_spec_names.py +20 -0
  41. spectre_core/receivers/gr/__init__.py +3 -0
  42. spectre_core/receivers/gr/_base.py +33 -0
  43. spectre_core/receivers/gr/_rsp1a.py +158 -0
  44. spectre_core/receivers/gr/_test.py +123 -0
  45. spectre_core/receivers/library/_rsp1a.py +61 -0
  46. spectre_core/receivers/library/_test.py +221 -0
  47. spectre_core/spectrograms/__init__.py +18 -0
  48. spectre_core/spectrograms/{analytical.py → _analytical.py} +29 -27
  49. spectre_core/spectrograms/{array_operations.py → _array_operations.py} +47 -1
  50. spectre_core/spectrograms/{spectrogram.py → _spectrogram.py} +62 -35
  51. spectre_core/spectrograms/{transform.py → _transform.py} +76 -89
  52. spectre_core/{post_processing/library → wgetting}/__init__.py +4 -5
  53. spectre_core/wgetting/_callisto.py +155 -0
  54. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/METADATA +1 -1
  55. spectre_core-0.0.10.dist-info/RECORD +63 -0
  56. spectre_core/cfg.py +0 -116
  57. spectre_core/chunks/library/__init__.py +0 -8
  58. spectre_core/chunks/library/sweep/__init__.py +0 -0
  59. spectre_core/chunks/library/sweep/chunk.py +0 -400
  60. spectre_core/dynamic_imports.py +0 -22
  61. spectre_core/file_handlers/base.py +0 -68
  62. spectre_core/file_handlers/configs.py +0 -271
  63. spectre_core/file_handlers/json.py +0 -40
  64. spectre_core/file_handlers/text.py +0 -21
  65. spectre_core/plotting/factory.py +0 -26
  66. spectre_core/plotting/format.py +0 -19
  67. spectre_core/plotting/library/__init__.py +0 -7
  68. spectre_core/plotting/library/frequency_cuts/panel.py +0 -74
  69. spectre_core/plotting/library/integral_over_frequency/panel.py +0 -34
  70. spectre_core/plotting/library/spectrogram/panel.py +0 -92
  71. spectre_core/plotting/library/time_cuts/panel.py +0 -77
  72. spectre_core/plotting/panel_register.py +0 -13
  73. spectre_core/post_processing/base.py +0 -132
  74. spectre_core/post_processing/library/fixed/__init__.py +0 -0
  75. spectre_core/post_processing/library/fixed/event_handler.py +0 -40
  76. spectre_core/post_processing/library/sweep/event_handler.py +0 -54
  77. spectre_core/receivers/base.py +0 -422
  78. spectre_core/receivers/library/__init__.py +0 -7
  79. spectre_core/receivers/library/rsp1a/__init__.py +0 -0
  80. spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
  81. spectre_core/receivers/library/rsp1a/gr/fixed.py +0 -104
  82. spectre_core/receivers/library/rsp1a/gr/sweep.py +0 -129
  83. spectre_core/receivers/library/rsp1a/receiver.py +0 -68
  84. spectre_core/receivers/library/rspduo/__init__.py +0 -0
  85. spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
  86. spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +0 -114
  87. spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +0 -131
  88. spectre_core/receivers/library/rspduo/gr/tuner_2_fixed.py +0 -120
  89. spectre_core/receivers/library/rspduo/gr/tuner_2_sweep.py +0 -119
  90. spectre_core/receivers/library/rspduo/receiver.py +0 -97
  91. spectre_core/receivers/library/test/__init__.py +0 -0
  92. spectre_core/receivers/library/test/gr/__init__.py +0 -0
  93. spectre_core/receivers/library/test/gr/cosine_signal_1.py +0 -83
  94. spectre_core/receivers/library/test/gr/tagged_staircase.py +0 -93
  95. spectre_core/receivers/library/test/receiver.py +0 -203
  96. spectre_core/receivers/validators.py +0 -231
  97. spectre_core/web_fetch/callisto.py +0 -101
  98. spectre_core-0.0.9.dist-info/RECORD +0 -74
  99. /spectre_core/chunks/{chunk_register.py → _register.py} +0 -0
  100. /spectre_core/post_processing/{event_handler_register.py → _register.py} +0 -0
  101. /spectre_core/receivers/{receiver_register.py → _register.py} +0 -0
  102. /spectre_core/{chunks/library/callisto/__init__.py → receivers/gr/_rspduo.py} +0 -0
  103. /spectre_core/{chunks/library/fixed/__init__.py → receivers/library/_rspduo.py} +0 -0
  104. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/LICENSE +0 -0
  105. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/WHEEL +0 -0
  106. {spectre_core-0.0.9.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