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.
- spectre_core/_file_io/__init__.py +1 -3
- spectre_core/_file_io/file_handlers.py +163 -58
- spectre_core/batches/__init__.py +10 -11
- spectre_core/batches/_base.py +170 -78
- spectre_core/batches/_batches.py +149 -99
- spectre_core/batches/_factory.py +56 -14
- spectre_core/batches/_register.py +23 -8
- spectre_core/batches/plugins/_batch_keys.py +16 -0
- spectre_core/batches/plugins/_callisto.py +183 -0
- spectre_core/batches/plugins/_iq_stream.py +354 -0
- spectre_core/capture_configs/__init__.py +17 -13
- spectre_core/capture_configs/_capture_config.py +93 -34
- spectre_core/capture_configs/_capture_modes.py +22 -0
- spectre_core/capture_configs/_capture_templates.py +207 -122
- spectre_core/capture_configs/_parameters.py +115 -42
- spectre_core/capture_configs/_pconstraints.py +86 -35
- spectre_core/capture_configs/_pnames.py +49 -0
- spectre_core/capture_configs/_ptemplates.py +389 -346
- spectre_core/capture_configs/_pvalidators.py +117 -73
- spectre_core/config/__init__.py +6 -8
- spectre_core/config/_paths.py +65 -25
- spectre_core/config/_time_formats.py +15 -10
- spectre_core/exceptions.py +2 -4
- spectre_core/jobs/__init__.py +14 -0
- spectre_core/jobs/_jobs.py +111 -0
- spectre_core/jobs/_workers.py +171 -0
- spectre_core/logs/__init__.py +17 -0
- spectre_core/logs/_configure.py +67 -0
- spectre_core/logs/_decorators.py +33 -0
- spectre_core/logs/_logs.py +228 -0
- spectre_core/logs/_process_types.py +14 -0
- spectre_core/plotting/__init__.py +4 -2
- spectre_core/plotting/_base.py +204 -102
- spectre_core/plotting/_format.py +17 -4
- spectre_core/plotting/_panel_names.py +18 -0
- spectre_core/plotting/_panel_stack.py +167 -53
- spectre_core/plotting/_panels.py +341 -141
- spectre_core/post_processing/__init__.py +8 -6
- spectre_core/post_processing/_base.py +70 -44
- spectre_core/post_processing/_factory.py +42 -12
- spectre_core/post_processing/_post_processor.py +24 -26
- spectre_core/post_processing/_register.py +22 -6
- spectre_core/post_processing/plugins/_event_handler_keys.py +16 -0
- spectre_core/post_processing/plugins/_fixed_center_frequency.py +129 -0
- spectre_core/post_processing/{library → plugins}/_swept_center_frequency.py +215 -143
- spectre_core/py.typed +0 -0
- spectre_core/receivers/__init__.py +10 -7
- spectre_core/receivers/_base.py +220 -69
- spectre_core/receivers/_factory.py +53 -7
- spectre_core/receivers/_register.py +30 -9
- spectre_core/receivers/_spec_names.py +26 -15
- spectre_core/receivers/plugins/__init__.py +0 -0
- spectre_core/receivers/plugins/_receiver_names.py +16 -0
- spectre_core/receivers/plugins/_rsp1a.py +59 -0
- spectre_core/receivers/plugins/_rspduo.py +67 -0
- spectre_core/receivers/plugins/_sdrplay_receiver.py +190 -0
- spectre_core/receivers/plugins/_test.py +218 -0
- spectre_core/receivers/plugins/gr/_base.py +80 -0
- spectre_core/receivers/{gr → plugins/gr}/_rsp1a.py +42 -52
- spectre_core/receivers/{gr → plugins/gr}/_rspduo.py +61 -74
- spectre_core/receivers/{gr → plugins/gr}/_test.py +33 -31
- spectre_core/spectrograms/__init__.py +5 -3
- spectre_core/spectrograms/_analytical.py +121 -66
- spectre_core/spectrograms/_array_operations.py +103 -36
- spectre_core/spectrograms/_spectrogram.py +380 -207
- spectre_core/spectrograms/_transform.py +197 -169
- spectre_core/wgetting/__init__.py +4 -2
- spectre_core/wgetting/_callisto.py +173 -118
- {spectre_core-0.0.12.dist-info → spectre_core-0.0.13.dist-info}/METADATA +14 -7
- spectre_core-0.0.13.dist-info/RECORD +75 -0
- {spectre_core-0.0.12.dist-info → spectre_core-0.0.13.dist-info}/WHEEL +1 -1
- spectre_core/batches/library/_callisto.py +0 -96
- spectre_core/batches/library/_fixed_center_frequency.py +0 -133
- spectre_core/batches/library/_swept_center_frequency.py +0 -105
- spectre_core/logging/__init__.py +0 -11
- spectre_core/logging/_configure.py +0 -35
- spectre_core/logging/_decorators.py +0 -19
- spectre_core/logging/_log_handlers.py +0 -176
- spectre_core/post_processing/library/_fixed_center_frequency.py +0 -114
- spectre_core/receivers/gr/_base.py +0 -33
- spectre_core/receivers/library/_rsp1a.py +0 -61
- spectre_core/receivers/library/_rspduo.py +0 -69
- spectre_core/receivers/library/_sdrplay_receiver.py +0 -185
- spectre_core/receivers/library/_test.py +0 -221
- spectre_core-0.0.12.dist-info/RECORD +0 -64
- /spectre_core/receivers/{gr → plugins/gr}/__init__.py +0 -0
- {spectre_core-0.0.12.dist-info → spectre_core-0.0.13.dist-info}/LICENSE +0 -0
- {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
|
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.
|
18
|
-
from spectre_core.
|
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(
|
27
|
-
|
28
|
-
|
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(
|
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(
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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(
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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(
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
91
|
-
|
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(
|
101
|
-
|
102
|
-
|
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(
|
107
|
-
|
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(
|
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(
|
132
|
-
|
133
|
-
|
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(
|
143
|
-
|
144
|
-
|
145
|
-
|
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(
|
150
|
-
_validate_center_frequencies_ordering(
|
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
|
-
|
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
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
199
|
+
num_samples,
|
173
200
|
num_full_sweeps,
|
174
201
|
num_steps_per_sweep)
|
175
202
|
|
176
203
|
_fill_frequencies(frequencies,
|
177
|
-
|
204
|
+
center_frequencies,
|
178
205
|
sft.f,
|
179
206
|
window_size)
|
180
207
|
|
181
|
-
sample_rate = capture_config.get_parameter_value(
|
208
|
+
sample_rate = cast(int, capture_config.get_parameter_value(PName.SAMPLE_RATE))
|
182
209
|
_fill_times(times,
|
183
|
-
|
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(
|
196
|
-
|
197
|
-
|
198
|
-
|
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(
|
208
|
-
|
209
|
-
|
210
|
-
|
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(
|
220
|
-
|
221
|
-
|
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(
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
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(
|
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 =
|
238
|
-
final_num_samples =
|
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
|
-
|
245
|
-
|
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,
|
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,
|
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 =
|
264
|
-
center_frequencies = _prepend_center_frequencies(
|
265
|
-
|
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(
|
268
|
-
|
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(
|
273
|
-
return iq_data,
|
274
|
-
|
275
|
-
|
276
|
-
def _build_spectrogram(
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
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,
|
291
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
309
|
-
|
373
|
+
SpectrumUnit.AMPLITUDE,
|
374
|
+
spectrogram_start_datetime)
|
310
375
|
|
311
376
|
|
312
|
-
@register_event_handler(
|
313
|
-
class
|
314
|
-
def __init__(
|
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(
|
324
|
-
|
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 =
|
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
|
-
|
340
|
-
|
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(
|
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(
|
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
|
-
|
359
|
-
|
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
|
-
|
363
|
-
|
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
|
-
|
6
|
-
|
7
|
-
from .
|
8
|
-
from .
|
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
|
13
|
-
from ._spec_names import
|
14
|
+
from ._register import get_registered_receivers
|
15
|
+
from ._spec_names import SpecName
|
14
16
|
|
15
17
|
__all__ = [
|
16
|
-
"
|
18
|
+
"Test", "RSP1A", "RSPduo", "BaseReceiver", "get_receiver",
|
19
|
+
"get_registered_receivers", "SpecName", "ReceiverName"
|
17
20
|
]
|