spectre-core 0.0.22__py3-none-any.whl → 0.0.24__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spectre_core/__init__.py +5 -0
- spectre_core/_file_io/__init__.py +4 -4
- spectre_core/_file_io/file_handlers.py +60 -106
- spectre_core/batches/__init__.py +20 -3
- spectre_core/batches/_base.py +85 -134
- spectre_core/batches/_batches.py +55 -99
- spectre_core/batches/_factory.py +21 -20
- spectre_core/batches/_register.py +8 -8
- spectre_core/batches/plugins/_batch_keys.py +7 -6
- spectre_core/batches/plugins/_callisto.py +65 -97
- spectre_core/batches/plugins/_iq_stream.py +105 -169
- spectre_core/capture_configs/__init__.py +46 -17
- spectre_core/capture_configs/_capture_config.py +25 -52
- spectre_core/capture_configs/_capture_modes.py +8 -6
- spectre_core/capture_configs/_capture_templates.py +50 -110
- spectre_core/capture_configs/_parameters.py +37 -74
- spectre_core/capture_configs/_pconstraints.py +40 -40
- spectre_core/capture_configs/_pnames.py +36 -34
- spectre_core/capture_configs/_ptemplates.py +260 -347
- spectre_core/capture_configs/_pvalidators.py +99 -102
- spectre_core/config/__init__.py +19 -8
- spectre_core/config/_paths.py +25 -47
- spectre_core/config/_time_formats.py +6 -5
- spectre_core/exceptions.py +38 -0
- spectre_core/jobs/__init__.py +3 -6
- spectre_core/jobs/_duration.py +12 -0
- spectre_core/jobs/_jobs.py +72 -43
- spectre_core/jobs/_workers.py +55 -105
- spectre_core/logs/__init__.py +7 -2
- spectre_core/logs/_configure.py +13 -17
- spectre_core/logs/_decorators.py +6 -4
- spectre_core/logs/_logs.py +37 -89
- spectre_core/logs/_process_types.py +5 -3
- spectre_core/plotting/__init__.py +19 -3
- spectre_core/plotting/_base.py +112 -177
- spectre_core/plotting/_format.py +10 -8
- spectre_core/plotting/_panel_names.py +7 -5
- spectre_core/plotting/_panel_stack.py +138 -130
- spectre_core/plotting/_panels.py +152 -162
- spectre_core/post_processing/__init__.py +6 -3
- spectre_core/post_processing/_base.py +41 -55
- spectre_core/post_processing/_factory.py +14 -11
- spectre_core/post_processing/_post_processor.py +16 -12
- spectre_core/post_processing/_register.py +10 -7
- spectre_core/post_processing/plugins/_event_handler_keys.py +4 -3
- spectre_core/post_processing/plugins/_fixed_center_frequency.py +54 -47
- spectre_core/post_processing/plugins/_swept_center_frequency.py +199 -174
- spectre_core/receivers/__init__.py +9 -2
- spectre_core/receivers/_base.py +82 -148
- spectre_core/receivers/_factory.py +20 -30
- spectre_core/receivers/_register.py +7 -10
- spectre_core/receivers/_spec_names.py +17 -15
- spectre_core/receivers/plugins/_b200mini.py +47 -60
- spectre_core/receivers/plugins/_receiver_names.py +8 -6
- spectre_core/receivers/plugins/_rsp1a.py +44 -40
- spectre_core/receivers/plugins/_rspduo.py +59 -44
- spectre_core/receivers/plugins/_sdrplay_receiver.py +67 -83
- spectre_core/receivers/plugins/_test.py +136 -129
- spectre_core/receivers/plugins/_usrp.py +93 -85
- spectre_core/receivers/plugins/gr/__init__.py +1 -1
- spectre_core/receivers/plugins/gr/_base.py +14 -22
- spectre_core/receivers/plugins/gr/_rsp1a.py +53 -60
- spectre_core/receivers/plugins/gr/_rspduo.py +77 -89
- spectre_core/receivers/plugins/gr/_test.py +49 -57
- spectre_core/receivers/plugins/gr/_usrp.py +61 -59
- spectre_core/spectrograms/__init__.py +21 -13
- spectre_core/spectrograms/_analytical.py +108 -99
- spectre_core/spectrograms/_array_operations.py +39 -46
- spectre_core/spectrograms/_spectrogram.py +293 -324
- spectre_core/spectrograms/_transform.py +106 -73
- spectre_core/wgetting/__init__.py +1 -3
- spectre_core/wgetting/_callisto.py +87 -93
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.24.dist-info}/METADATA +9 -23
- spectre_core-0.0.24.dist-info/RECORD +79 -0
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.24.dist-info}/WHEEL +1 -1
- spectre_core-0.0.22.dist-info/RECORD +0 -78
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.24.dist-info}/licenses/LICENSE +0 -0
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.24.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
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
59
|
-
|
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] =
|
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(
|
97
|
-
|
98
|
-
|
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(
|
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(
|
140
|
-
|
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(
|
176
|
-
|
177
|
-
|
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 = (
|
186
|
-
|
187
|
-
|
188
|
-
|
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(
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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
|
269
|
+
previous_iq_data = previous_batch.bin_file.read()
|
268
270
|
previous_iq_metadata = previous_batch.hdr_file.read()
|
269
|
-
|
270
|
-
if
|
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(
|
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[
|
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(
|
286
|
-
|
287
|
-
|
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
|
-
|
295
|
-
|
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
|
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 =
|
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
|
-
|
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 =
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
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
|
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 =
|
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(
|
345
|
-
|
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(
|
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 =
|
355
|
-
|
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
|
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
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
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(
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
spectrogram = time_average(
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
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",
|
20
|
-
"
|
19
|
+
"Test",
|
20
|
+
"RSP1A",
|
21
|
+
"RSPduo",
|
22
|
+
"B200mini",
|
23
|
+
"BaseReceiver",
|
24
|
+
"get_receiver",
|
25
|
+
"get_registered_receivers",
|
26
|
+
"SpecName",
|
27
|
+
"ReceiverName",
|
21
28
|
]
|