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