spectre-core 0.0.12__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 (88) hide show
  1. spectre_core/_file_io/__init__.py +1 -3
  2. spectre_core/_file_io/file_handlers.py +163 -58
  3. spectre_core/batches/__init__.py +10 -11
  4. spectre_core/batches/_base.py +170 -78
  5. spectre_core/batches/_batches.py +149 -99
  6. spectre_core/batches/_factory.py +56 -14
  7. spectre_core/batches/_register.py +23 -8
  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 +115 -42
  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 +117 -73
  20. spectre_core/config/__init__.py +6 -8
  21. spectre_core/config/_paths.py +65 -25
  22. spectre_core/config/_time_formats.py +15 -10
  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 +70 -44
  40. spectre_core/post_processing/_factory.py +42 -12
  41. spectre_core/post_processing/_post_processor.py +24 -26
  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/{library → plugins}/_swept_center_frequency.py +215 -143
  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 +42 -52
  60. spectre_core/receivers/{gr → plugins/gr}/_rspduo.py +61 -74
  61. spectre_core/receivers/{gr → plugins/gr}/_test.py +33 -31
  62. spectre_core/spectrograms/__init__.py +5 -3
  63. spectre_core/spectrograms/_analytical.py +121 -66
  64. spectre_core/spectrograms/_array_operations.py +103 -36
  65. spectre_core/spectrograms/_spectrogram.py +380 -207
  66. spectre_core/spectrograms/_transform.py +197 -169
  67. spectre_core/wgetting/__init__.py +4 -2
  68. spectre_core/wgetting/_callisto.py +173 -118
  69. {spectre_core-0.0.12.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.12.dist-info → spectre_core-0.0.13.dist-info}/WHEEL +1 -1
  72. spectre_core/batches/library/_callisto.py +0 -96
  73. spectre_core/batches/library/_fixed_center_frequency.py +0 -133
  74. spectre_core/batches/library/_swept_center_frequency.py +0 -105
  75. spectre_core/logging/__init__.py +0 -11
  76. spectre_core/logging/_configure.py +0 -35
  77. spectre_core/logging/_decorators.py +0 -19
  78. spectre_core/logging/_log_handlers.py +0 -176
  79. spectre_core/post_processing/library/_fixed_center_frequency.py +0 -114
  80. spectre_core/receivers/gr/_base.py +0 -33
  81. spectre_core/receivers/library/_rsp1a.py +0 -61
  82. spectre_core/receivers/library/_rspduo.py +0 -69
  83. spectre_core/receivers/library/_sdrplay_receiver.py +0 -185
  84. spectre_core/receivers/library/_test.py +0 -221
  85. spectre_core-0.0.12.dist-info/RECORD +0 -64
  86. /spectre_core/receivers/{gr → plugins/gr}/__init__.py +0 -0
  87. {spectre_core-0.0.12.dist-info → spectre_core-0.0.13.dist-info}/LICENSE +0 -0
  88. {spectre_core-0.0.12.dist-info → spectre_core-0.0.13.dist-info}/top_level.txt +0 -0
@@ -6,41 +6,45 @@ from logging import getLogger
6
6
  _LOGGER = getLogger(__name__)
7
7
 
8
8
  import os
9
- from typing import Optional
10
- from typing import Tuple
11
- from datetime import timedelta, datetime
9
+ from typing import Optional, Tuple, cast
10
+ from datetime import timedelta
12
11
  import numpy as np
12
+ import numpy.typing as npt
13
13
 
14
14
  from scipy.signal import ShortTimeFFT
15
15
 
16
- from spectre_core.spectrograms import Spectrogram, time_average, frequency_average
17
- from spectre_core.config import TimeFormats
18
- from spectre_core.capture_configs import CaptureConfig, PNames, CaptureModes
19
- from spectre_core.batches import BaseBatch
20
- from spectre_core.batches import SweepMetadata
16
+ from spectre_core.spectrograms import Spectrogram, SpectrumUnit, time_average, frequency_average
17
+ from spectre_core.capture_configs import CaptureConfig, PName
18
+ from spectre_core.batches import IQStreamBatch
21
19
  from spectre_core.exceptions import InvalidSweepMetadataError
20
+ from ._event_handler_keys import EventHandlerKey
22
21
  from .._base import BaseEventHandler, make_sft_instance
23
22
  from .._register import register_event_handler
24
23
 
25
24
 
26
- def _stitch_steps(stepped_dynamic_spectra: np.ndarray,
27
- num_full_sweeps: int) -> np.ndarray:
28
- """For each full sweep, create a swept spectrum by stitching together the spectrum at each of the steps."""
25
+ def _stitch_steps(
26
+ stepped_dynamic_spectra: npt.NDArray[np.float32],
27
+ num_full_sweeps: int
28
+ ) -> npt.NDArray[np.float32]:
29
+ """For each full sweep, create a single spectrum by stitching together the spectrum at each step."""
29
30
  return stepped_dynamic_spectra.reshape((num_full_sweeps, -1)).T
30
31
 
31
32
 
32
- def _average_over_steps(stepped_dynamic_spectra: np.ndarray) -> None:
33
+ def _average_over_steps(
34
+ stepped_dynamic_spectra: npt.NDArray[np.float32]
35
+ ) -> npt.NDArray[np.float32]:
33
36
  """Average the spectrums in each step totally in time."""
34
37
  return np.nanmean(stepped_dynamic_spectra[..., 1:], axis=-1)
35
38
 
36
39
 
37
- def _fill_times(times: np.ndarray,
38
- num_samples: np.ndarray,
39
- sample_rate: int,
40
- num_full_sweeps: int,
41
- num_steps_per_sweep: int) -> None:
42
- """Assign physical times to each swept spectrum. We use (by convention) the time of the first sample in eahc sweep"""
43
-
40
+ def _fill_times(
41
+ times: npt.NDArray[np.float32],
42
+ num_samples: npt.NDArray[np.int32],
43
+ sample_rate: int,
44
+ num_full_sweeps: int,
45
+ num_steps_per_sweep: int
46
+ ) -> None:
47
+ """Assign physical times to each swept spectrum. We use (by convention) the time of the first sample in each sweep"""
44
48
  sampling_interval = 1 / sample_rate
45
49
  cumulative_samples = 0
46
50
  for sweep_index in range(num_full_sweeps):
@@ -55,26 +59,29 @@ def _fill_times(times: np.ndarray,
55
59
  cumulative_samples += np.sum(num_samples[start_step:end_step])
56
60
 
57
61
 
58
- def _fill_frequencies(frequencies: np.ndarray,
59
- center_frequencies: np.ndarray,
60
- baseband_frequencies: np.ndarray,
61
- window_size: int) -> None:
62
- """Assign physical frequencies to each of the swept spectral components."""
62
+ def _fill_frequencies(
63
+ frequencies: npt.NDArray[np.float32],
64
+ center_frequencies: npt.NDArray[np.float32],
65
+ baseband_frequencies: npt.NDArray[np.float32],
66
+ window_size: int
67
+ ) -> None:
68
+ """Assign physical frequencies to each of the spectral components in the stitched spectrum."""
63
69
  for i, center_frequency in enumerate(np.unique(center_frequencies)):
64
70
  lower_bound = i * window_size
65
71
  upper_bound = (i + 1) * window_size
66
72
  frequencies[lower_bound:upper_bound] = (baseband_frequencies + center_frequency)
67
73
 
68
74
 
69
- def _fill_stepped_dynamic_spectra(stepped_dynamic_spectra: np.ndarray,
70
- sft: ShortTimeFFT,
71
- iq_data: np.ndarray,
72
- num_samples: np.ndarray,
73
- num_full_sweeps: int,
74
- num_steps_per_sweep: int) -> None:
75
- """Compute the dynamic spectra for the input IQ samples for each step.
76
-
77
- All IQ samples per step were collected at the same center frequency.
75
+ def _fill_stepped_dynamic_spectra(
76
+ stepped_dynamic_spectra: npt.NDArray[np.float32],
77
+ sft: ShortTimeFFT,
78
+ iq_data: npt.NDArray[np.complex64],
79
+ num_samples: npt.NDArray[np.int32],
80
+ num_full_sweeps: int,
81
+ num_steps_per_sweep: int
82
+ ) -> None:
83
+ """For each full sweep, compute the dynamic spectra by performing a Short-time Fast Fourier Transform
84
+ on the IQ samples within each step.
78
85
  """
79
86
  # global_step_index will hold the step index over all sweeps (doesn't reset each sweep)
80
87
  # start_sample_index will hold the index of the first sample in the step
@@ -87,8 +94,8 @@ def _fill_stepped_dynamic_spectra(stepped_dynamic_spectra: np.ndarray,
87
94
  num_slices = sft.upper_border_begin(num_samples[global_step_index])[1]
88
95
  # perform a short time fast fourier transform on the step
89
96
  complex_spectra = sft.stft(iq_data[start_sample_index:end_sample_index],
90
- p0=0,
91
- p1=num_slices)
97
+ p0=0,
98
+ p1=num_slices)
92
99
  # and pack the absolute values into the stepped spectrogram where the step slot is padded to the maximum size for ease of processing later)
93
100
  stepped_dynamic_spectra[sweep_index, step_index, :, :num_slices] = np.abs(complex_spectra)
94
101
  # reassign the start_sample_index for the next step
@@ -97,25 +104,31 @@ def _fill_stepped_dynamic_spectra(stepped_dynamic_spectra: np.ndarray,
97
104
  global_step_index += 1
98
105
 
99
106
 
100
- def _compute_num_max_slices_in_step(sft: ShortTimeFFT,
101
- num_samples: np.ndarray) -> int:
102
- """Compute the maximum number of slices over all steps, in all sweeps over the batch."""
107
+ def _compute_num_max_slices_in_step(
108
+ sft: ShortTimeFFT,
109
+ num_samples: npt.NDArray[np.int32]
110
+ ) -> int:
111
+ """Compute the maximum number of slices over all steps (and all sweeps) in the batch."""
103
112
  return sft.upper_border_begin(np.max(num_samples))[1]
104
113
 
105
114
 
106
- def _compute_num_full_sweeps(center_frequencies: np.ndarray) -> int:
107
- """Compute the total number of full sweeps over the batch.
115
+ def _compute_num_full_sweeps(
116
+ center_frequencies: npt.NDArray[np.float32]
117
+ ) -> int:
118
+ """Compute the total number of full sweeps in the batch.
108
119
 
109
120
  Since the number of each samples in each step is variable, we only know a sweep is complete
110
121
  when there is a sweep after it. So we can define the total number of *full* sweeps as the number of
111
122
  (freq_max, freq_min) pairs in center_frequencies. It is only at an instance of (freq_max, freq_min) pair
112
123
  in center frequencies that the frequency decreases, so, we can compute the number of full sweeps by
113
- counting the numbers of negative values in np.diff(center_frequencies)
124
+ counting the numbers of negative values in np.diff(center_frequencies).
114
125
  """
115
126
  return len(np.where(np.diff(center_frequencies) < 0)[0])
116
127
 
117
128
 
118
- def _compute_num_steps_per_sweep(center_frequencies: np.ndarray) -> int:
129
+ def _compute_num_steps_per_sweep(
130
+ center_frequencies: npt.NDArray[np.float32]
131
+ ) -> int:
119
132
  """Compute the (ensured constant) number of steps in each sweep."""
120
133
  # find the (step) indices corresponding to the minimum frequencies
121
134
  min_freq_indices = np.where(center_frequencies == np.min(center_frequencies))[0]
@@ -128,9 +141,11 @@ def _compute_num_steps_per_sweep(center_frequencies: np.ndarray) -> int:
128
141
  return int(unique_num_steps_per_sweep[0])
129
142
 
130
143
 
131
- def _validate_center_frequencies_ordering(center_frequencies: np.ndarray,
132
- freq_step: float) -> None:
133
- """Check that the center frequencies are well-ordered in the detached header"""
144
+ def _validate_center_frequencies_ordering(
145
+ center_frequencies: npt.NDArray[np.float32],
146
+ freq_step: float
147
+ ) -> None:
148
+ """Check that the center frequencies are well-ordered in the detached header."""
134
149
  min_frequency = np.min(center_frequencies)
135
150
  # Extract the expected difference between each step within a sweep.
136
151
  for i, diff in enumerate(np.diff(center_frequencies)):
@@ -139,48 +154,60 @@ def _validate_center_frequencies_ordering(center_frequencies: np.ndarray,
139
154
  raise InvalidSweepMetadataError(f"Unordered center frequencies detected")
140
155
 
141
156
 
142
- def _do_stfft(iq_data: np.ndarray,
143
- sweep_metadata: SweepMetadata,
144
- capture_config: CaptureConfig):
145
- """Perform a Short Time FFT on the input swept IQ samples."""
157
+ def _do_stfft(
158
+ iq_data: npt.NDArray[np.complex64],
159
+ center_frequencies: npt.NDArray[np.float32],
160
+ num_samples: npt.NDArray[np.int32],
161
+ capture_config: CaptureConfig
162
+ ) -> Tuple[npt.NDArray[np.float32], npt.NDArray[np.float32], npt.NDArray[np.float32]]:
163
+ """Do a Short-time Fast Fourier Transform on an array of complex IQ samples.
164
+
165
+ The computation requires extra metadata, which is extracted from the detached header in the batch
166
+ and the capture config used to capture the data.
167
+
168
+ The current implementation relies heavily on the `ShortTimeFFT` implementation from
169
+ `scipy.signal` (https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.ShortTimeFFT.html)
170
+ which takes up a lot of the compute time.
171
+ """
146
172
 
147
173
  sft = make_sft_instance(capture_config)
148
174
 
149
- frequency_step = capture_config.get_parameter_value(PNames.FREQUENCY_STEP)
150
- _validate_center_frequencies_ordering(sweep_metadata.center_frequencies,
175
+ frequency_step = cast(float, capture_config.get_parameter_value(PName.FREQUENCY_STEP))
176
+ _validate_center_frequencies_ordering(center_frequencies,
151
177
  frequency_step)
152
178
 
153
- window_size = capture_config.get_parameter_value(PNames.WINDOW_SIZE)
154
-
155
- num_steps_per_sweep = _compute_num_steps_per_sweep(sweep_metadata.center_frequencies)
156
- num_full_sweeps = _compute_num_full_sweeps(sweep_metadata.center_frequencies)
179
+ num_steps_per_sweep = _compute_num_steps_per_sweep(center_frequencies)
180
+ num_full_sweeps = _compute_num_full_sweeps(center_frequencies)
157
181
  num_max_slices_in_step = _compute_num_max_slices_in_step(sft,
158
- sweep_metadata.num_samples)
182
+ num_samples)
159
183
 
184
+ window_size = cast(int, capture_config.get_parameter_value(PName.WINDOW_SIZE))
160
185
  stepped_dynamic_spectra_shape = (num_full_sweeps,
161
186
  num_steps_per_sweep,
162
187
  window_size,
163
188
  num_max_slices_in_step)
164
- stepped_dynamic_spectra = np.full(stepped_dynamic_spectra_shape, np.nan)
165
-
166
- frequencies = np.empty(num_steps_per_sweep * window_size)
167
- times = np.empty(num_full_sweeps)
189
+
190
+ # pad with nan values up to the max number of slices to make computations simpler.
191
+ # nans are required, so that they are easily ignored, e.g. in averaging operations.
192
+ stepped_dynamic_spectra = np.full(stepped_dynamic_spectra_shape, np.nan, dtype=np.float32)
193
+ frequencies = np.empty(num_steps_per_sweep * window_size, dtype=np.float32)
194
+ times = np.empty(num_full_sweeps, dtype=np.float32)
168
195
 
169
196
  _fill_stepped_dynamic_spectra(stepped_dynamic_spectra,
170
197
  sft,
171
198
  iq_data,
172
- sweep_metadata.num_samples,
199
+ num_samples,
173
200
  num_full_sweeps,
174
201
  num_steps_per_sweep)
175
202
 
176
203
  _fill_frequencies(frequencies,
177
- sweep_metadata.center_frequencies,
204
+ center_frequencies,
178
205
  sft.f,
179
206
  window_size)
180
207
 
181
- sample_rate = capture_config.get_parameter_value(PNames.SAMPLE_RATE)
208
+ sample_rate = cast(int, capture_config.get_parameter_value(PName.SAMPLE_RATE))
182
209
  _fill_times(times,
183
- sweep_metadata.num_samples,
210
+ num_samples,
184
211
  sample_rate,
185
212
  num_full_sweeps,
186
213
  num_steps_per_sweep)
@@ -192,10 +219,13 @@ def _do_stfft(iq_data: np.ndarray,
192
219
  return times, frequencies, dynamic_spectra
193
220
 
194
221
 
195
- def _prepend_num_samples(carryover_num_samples: np.ndarray,
196
- num_samples: np.ndarray,
197
- final_step_spans_two_batches: bool) -> np.ndarray:
198
- """Prepend the number of samples from the final sweep of the previous batch."""
222
+ def _prepend_num_samples(
223
+ carryover_num_samples: npt.NDArray[np.int32],
224
+ num_samples: npt.NDArray[np.int32],
225
+ final_step_spans_two_batches: bool
226
+ ) -> npt.NDArray[np.int32]:
227
+ """Prepend the number of samples from the final sweep of the previous batch, to the first
228
+ sweep of the current batch."""
199
229
  if final_step_spans_two_batches:
200
230
  # ensure the number of samples from the final step in the previous batch are accounted for
201
231
  num_samples[0] += carryover_num_samples[-1]
@@ -204,10 +234,13 @@ def _prepend_num_samples(carryover_num_samples: np.ndarray,
204
234
  return np.concatenate((carryover_num_samples, num_samples))
205
235
 
206
236
 
207
- def _prepend_center_frequencies(carryover_center_frequencies: np.ndarray,
208
- center_frequencies: np.ndarray,
209
- final_step_spans_two_batches: bool)-> np.ndarray:
210
- """Prepend the center frequencies from the final sweep of the previous batch."""
237
+ def _prepend_center_frequencies(
238
+ carryover_center_frequencies: npt.NDArray[np.float32],
239
+ center_frequencies: npt.NDArray[np.float32],
240
+ final_step_spans_two_batches: bool
241
+ )-> npt.NDArray[np.float32]:
242
+ """Prepend the center frequencies from the final sweep of the previous batch, to the first
243
+ sweep of the current batch."""
211
244
  # in the case that the sweep has bled across batches,
212
245
  # do not permit identical neighbours in the center frequency array
213
246
  if final_step_spans_two_batches:
@@ -216,134 +249,175 @@ def _prepend_center_frequencies(carryover_center_frequencies: np.ndarray,
216
249
  return np.concatenate((carryover_center_frequencies, center_frequencies))
217
250
 
218
251
 
219
- def _prepend_iq_data(carryover_iq_data: np.ndarray,
220
- iq_data: np.ndarray) -> np.ndarray:
221
- """Prepend the IQ samples from the final sweep of the previous batch."""
252
+ def _prepend_iq_data(
253
+ carryover_iq_data: npt.NDArray[np.complex64],
254
+ iq_data: npt.NDArray[np.complex64]
255
+ ) -> npt.NDArray[np.complex64]:
256
+ """Prepend the IQ samples from the final sweep of the previous batch, to the first sweep
257
+ of the current batch."""
222
258
  return np.concatenate((carryover_iq_data, iq_data))
223
259
 
224
260
 
225
- def _get_final_sweep(previous_batch: BaseBatch
226
- ) -> Tuple[np.ndarray, SweepMetadata]:
227
- """Get data from the final sweep of the previous batch."""
228
- # unpack the data from the previous batch
229
- previous_iq_data = previous_batch.read_file("bin")
230
- _, previous_sweep_metadata = previous_batch.read_file("hdr")
261
+ def _get_final_sweep(
262
+ previous_batch: IQStreamBatch
263
+ ) -> Tuple[npt.NDArray[np.complex64], npt.NDArray[np.float32], npt.NDArray[np.int32]]:
264
+ """Get IQ samples and metadata from the final sweep of the previous batch."""
265
+
266
+ # unpack the data from the previous batch (using the cached values!)
267
+ previous_iq_data = previous_batch.bin_file.read()
268
+ previous_iq_metadata = previous_batch.hdr_file.read()
269
+
270
+ if previous_iq_metadata.center_frequencies is None or previous_iq_metadata.num_samples is None:
271
+ raise ValueError(f"Expected non-empty IQ metadata!")
272
+
231
273
  # find the step index from the last sweep
232
274
  # [0] since the return of np.where is a 1 element Tuple,
233
275
  # containing a list of step indices corresponding to the smallest center frequencies
234
276
  # [-1] since we want the final step index, where the center frequency is minimised
235
- final_sweep_start_step_index = np.where(previous_sweep_metadata.center_frequencies == np.min(previous_sweep_metadata.center_frequencies))[0][-1]
277
+ final_sweep_start_step_index = np.where(previous_iq_metadata.center_frequencies == np.min(previous_iq_metadata.center_frequencies))[0][-1]
236
278
  # isolate the data from the final sweep
237
- final_center_frequencies = previous_sweep_metadata.center_frequencies[final_sweep_start_step_index:]
238
- final_num_samples = previous_sweep_metadata.num_samples[final_sweep_start_step_index:]
279
+ final_center_frequencies = previous_iq_metadata.center_frequencies[final_sweep_start_step_index:]
280
+ final_num_samples = previous_iq_metadata.num_samples[final_sweep_start_step_index:]
239
281
  final_sweep_iq_data = previous_iq_data[-np.sum(final_num_samples):]
240
282
 
241
283
  # sanity check on the number of samples in the final sweep
242
284
  if len(final_sweep_iq_data) != np.sum(final_num_samples):
243
285
  raise ValueError((f"Unexpected error! Mismatch in sample count for the final sweep data."
244
- f"Expected {np.sum(final_num_samples)} based on sweep metadata, but found "
245
- f" {len(final_sweep_iq_data)} IQ samples in the final sweep"))
286
+ f"Expected {np.sum(final_num_samples)} based on sweep metadata, but found "
287
+ f" {len(final_sweep_iq_data)} IQ samples in the final sweep"))
246
288
 
247
- return final_sweep_iq_data, SweepMetadata(final_center_frequencies, final_num_samples)
248
-
289
+ return final_sweep_iq_data, final_center_frequencies, final_num_samples
249
290
 
250
- def _reconstruct_initial_sweep(previous_batch: BaseBatch,
251
- iq_data: np.ndarray,
252
- sweep_metadata: SweepMetadata) -> Tuple[np.ndarray, SweepMetadata, int]:
253
- """Reconstruct the initial sweep of the current batch, using data from the previous batch."""
254
291
 
292
+ def _reconstruct_initial_sweep(
293
+ previous_batch: IQStreamBatch,
294
+ batch: IQStreamBatch
295
+ ) -> Tuple[npt.NDArray[np.complex64], npt.NDArray[np.float32], npt.NDArray[np.int32], int]:
296
+ """Reconstruct the initial sweep of the current batch, using data from the previous batch.
297
+
298
+ Specifically, we extract the data from the final sweep of the previous batch and prepend
299
+ it to the first sweep of the current batch. Additionally, we return how many IQ samples
300
+ we prepended, which will allow us to correct the spectrogram start time of the current batch.
301
+ """
302
+
303
+ iq_data = batch.bin_file.read()
304
+ iq_metadata = batch.hdr_file.read()
305
+
306
+ if iq_metadata.center_frequencies is None or iq_metadata.num_samples is None:
307
+ raise ValueError(f"Expected non-empty IQ metadata!")
308
+
255
309
  # carryover the final sweep of the previous batch, and prepend that data to the current batch data
256
- carryover_iq_data, carryover_sweep_metadata = _get_final_sweep(previous_batch)
310
+ carryover_iq_data, carryover_center_frequencies, carryover_num_samples = _get_final_sweep(previous_batch)
257
311
 
258
312
  # prepend the iq data that was carried over from the previous batch
259
313
  iq_data = _prepend_iq_data(carryover_iq_data,
260
314
  iq_data)
261
315
 
262
316
  # prepend the sweep metadata from the previous batch
263
- final_step_spans_two_batches = carryover_sweep_metadata.center_frequencies[-1] == sweep_metadata.center_frequencies[0]
264
- center_frequencies = _prepend_center_frequencies(carryover_sweep_metadata.center_frequencies,
265
- sweep_metadata.center_frequencies,
317
+ final_step_spans_two_batches = carryover_center_frequencies[-1] == iq_metadata.center_frequencies[0]
318
+ center_frequencies = _prepend_center_frequencies(carryover_center_frequencies,
319
+ iq_metadata.center_frequencies,
266
320
  final_step_spans_two_batches)
267
- num_samples = _prepend_num_samples(carryover_sweep_metadata.num_samples,
268
- sweep_metadata.num_samples,
321
+ num_samples = _prepend_num_samples(carryover_num_samples,
322
+ iq_metadata.num_samples,
269
323
  final_step_spans_two_batches)
270
324
 
271
325
  # keep track of how many samples we prepended (required to adjust timing later)
272
- num_samples_prepended = np.sum(carryover_sweep_metadata.num_samples)
273
- return iq_data, SweepMetadata(center_frequencies, num_samples), num_samples_prepended
274
-
275
-
276
- def _build_spectrogram(batch: BaseBatch,
277
- capture_config: CaptureConfig,
278
- previous_batch: Optional[BaseBatch] = None) -> Spectrogram:
279
- """Create a spectrogram by performing a Short Time FFT on the (swept) IQ samples for this batch."""
280
- iq_data = batch.read_file("bin")
281
- millisecond_correction, sweep_metadata = batch.read_file("hdr")
326
+ num_samples_prepended = int(np.sum(carryover_num_samples))
327
+ return (iq_data, center_frequencies, num_samples, num_samples_prepended)
328
+
329
+
330
+ def _build_spectrogram(
331
+ batch: IQStreamBatch,
332
+ capture_config: CaptureConfig,
333
+ previous_batch: Optional[IQStreamBatch] = None
334
+ ) -> Spectrogram:
335
+ """Generate a spectrogram using `IQStreamBatch` IQ samples."""
336
+ # read the batch files.
337
+ iq_data = batch.bin_file.read()
338
+ iq_metadata = batch.hdr_file.read()
339
+
340
+ # extract the center frequencies and num samples
341
+ center_frequencies, num_samples = iq_metadata.center_frequencies, iq_metadata.num_samples
342
+
343
+ if center_frequencies is None or num_samples is None:
344
+ raise ValueError(f"An unexpected error has occured, expected frequency tag metadata "
345
+ f"in the detached header.")
282
346
 
283
347
  # correct the batch start datetime with the millisecond correction stored in the detached header
284
- spectrogram_start_datetime = batch.start_datetime + timedelta(milliseconds=millisecond_correction)
348
+ spectrogram_start_datetime = batch.start_datetime + timedelta(milliseconds=iq_metadata.millisecond_correction)
285
349
 
286
350
  # if a previous batch has been specified, this indicates that the initial sweep spans between two adjacent batched files.
287
351
  if previous_batch:
288
352
  # If this is the case, first reconstruct the initial sweep of the current batch
289
353
  # by prepending the final sweep of the previous batch
290
- iq_data, sweep_metadata, num_samples_prepended = _reconstruct_initial_sweep(previous_batch,
291
- iq_data,
292
- sweep_metadata)
354
+ iq_data, center_frequencies, num_samples, num_samples_prepended = _reconstruct_initial_sweep(previous_batch,
355
+ batch)
293
356
 
294
357
  # since we have prepended extra samples, we need to correct the spectrogram start time appropriately
295
- elapsed_time = num_samples_prepended * (1 / capture_config.get_parameter_value(PNames.SAMPLE_RATE))
358
+ sample_rate = cast(int, capture_config.get_parameter_value(PName.SAMPLE_RATE))
359
+ elapsed_time = num_samples_prepended * (1 / sample_rate)
296
360
  spectrogram_start_datetime -= timedelta(seconds = float(elapsed_time))
297
361
 
298
362
 
299
363
 
300
364
  times, frequencies, dynamic_spectra = _do_stfft(iq_data,
301
- sweep_metadata,
365
+ center_frequencies,
366
+ num_samples,
302
367
  capture_config)
303
368
 
304
369
  return Spectrogram(dynamic_spectra,
305
370
  times,
306
371
  frequencies,
307
372
  batch.tag,
308
- spectrogram_start_datetime,
309
- spectrum_type = "amplitude")
373
+ SpectrumUnit.AMPLITUDE,
374
+ spectrogram_start_datetime)
310
375
 
311
376
 
312
- @register_event_handler(CaptureModes.SWEPT_CENTER_FREQUENCY)
313
- class _EventHandler(BaseEventHandler):
314
- def __init__(self, *args, **kwargs):
377
+ @register_event_handler(EventHandlerKey.SWEPT_CENTER_FREQUENCY)
378
+ class SweptEventHandler(BaseEventHandler):
379
+ def __init__(
380
+ self,
381
+ *args,
382
+ **kwargs
383
+ ) -> None:
315
384
  super().__init__(*args, **kwargs)
316
-
317
- # the previous batch is stored in order to fetch the
318
- # data from the "final sweep" which was ignored during
319
- # processing.
320
- self._previous_batch: BaseBatch = None
385
+ self._previous_batch: Optional[IQStreamBatch] = None
321
386
 
322
-
323
- def process(self,
324
- absolute_file_path: str):
387
+
388
+ def process(
389
+ self,
390
+ absolute_file_path: str
391
+ ) -> None:
392
+ """
393
+ Compute a spectrogram using IQ samples from an `IQStreamBatch`, cache the results, and save them in the
394
+ FITS format. The IQ samples are assumed to be collected at a center frequency periodically swept in
395
+ fixed increments. Neighbouring IQ samples collected at the same frequency constitute a "step." Neighbouring
396
+ steps collected at incrementally increasing center frequencies form a "sweep." A new sweep begins when the
397
+ center frequency resets to its minimum value.
398
+
399
+ The computed spectrogram is averaged in time and frequency based on user-configured settings in the capture
400
+ config. The batch is cached after computation for use in subsequent processing steps.
401
+
402
+ :param absolute_file_path: The absolute path to the `.bin` file containing the IQ sample batch.
403
+ """
325
404
  _LOGGER.info(f"Processing: {absolute_file_path}")
326
405
  file_name = os.path.basename(absolute_file_path)
327
406
  # discard the extension
328
407
  base_file_name, _ = os.path.splitext(file_name)
329
408
  batch_start_time, tag = base_file_name.split('_')
330
- batch = self._Batch(batch_start_time, tag)
331
-
332
- # ensure that the file which has been created has the expected tag
333
- if tag != self._tag:
334
- raise RuntimeError(f"Received an unexpected tag! Expected '{self._tag}', "
335
- f"but a file has been created with tag '{tag}'")
409
+ batch = IQStreamBatch(batch_start_time, tag)
336
410
 
337
411
  _LOGGER.info("Creating spectrogram")
338
412
  spectrogram = _build_spectrogram(batch,
339
- self._capture_config,
340
- previous_batch = self._previous_batch)
413
+ self._capture_config,
414
+ previous_batch = self._previous_batch)
341
415
 
342
416
  spectrogram = time_average(spectrogram,
343
- resolution = self._capture_config.get_parameter_value(PNames.TIME_RESOLUTION))
417
+ resolution = self._capture_config.get_parameter_value(PName.TIME_RESOLUTION))
344
418
 
345
419
  spectrogram = frequency_average(spectrogram,
346
- resolution = self._capture_config.get_parameter_value(PNames.FREQUENCY_RESOLUTION))
420
+ resolution = self._capture_config.get_parameter_value(PName.FREQUENCY_RESOLUTION))
347
421
 
348
422
  self._cache_spectrogram(spectrogram)
349
423
 
@@ -355,13 +429,11 @@ class _EventHandler(BaseEventHandler):
355
429
 
356
430
  # otherwise the previous batch is defined (and by this point has already been processed)
357
431
  else:
358
- bin_file = self._previous_batch.get_file('bin')
359
- _LOGGER.info(f"Deleting {bin_file.file_path}")
360
- bin_file.delete()
432
+ _LOGGER.info(f"Deleting {self._previous_batch.bin_file.file_path}")
433
+ self._previous_batch.bin_file.delete()
361
434
 
362
- hdr_file = self._previous_batch.get_file('hdr')
363
- _LOGGER.info(f"Deleting {hdr_file.file_path}")
364
- hdr_file.delete()
435
+ _LOGGER.info(f"Deleting {self._previous_batch.hdr_file.file_path}")
436
+ self._previous_batch.hdr_file.delete()
365
437
 
366
438
  # and reassign the current batch to be used as the previous batch at the next call of this method
367
439
  self._previous_batch = batch
spectre_core/py.typed ADDED
File without changes
@@ -2,16 +2,19 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- # register decorators take effect on import
6
- from .library._test import _Receiver
7
- from .library._rsp1a import _Receiver
8
- from .library._rspduo import _Receiver
5
+ """A vendor-neutral interface for collecting data from SDRs."""
6
+
7
+ from .plugins._receiver_names import ReceiverName
8
+ from .plugins._test import Test
9
+ from .plugins._rsp1a import RSP1A
10
+ from .plugins._rspduo import RSPduo
9
11
 
10
12
  from ._base import BaseReceiver
11
13
  from ._factory import get_receiver
12
- from ._register import list_all_receiver_names
13
- from ._spec_names import SpecNames
14
+ from ._register import get_registered_receivers
15
+ from ._spec_names import SpecName
14
16
 
15
17
  __all__ = [
16
- "BaseReceiver", "get_receiver", "list_all_receiver_names", "SpecNames"
18
+ "Test", "RSP1A", "RSPduo", "BaseReceiver", "get_receiver",
19
+ "get_registered_receivers", "SpecName", "ReceiverName"
17
20
  ]