spectre-core 0.0.9__py3-none-any.whl → 0.0.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spectre_core/__init__.py +0 -3
- spectre_core/_file_io/__init__.py +15 -0
- spectre_core/_file_io/file_handlers.py +128 -0
- spectre_core/capture_configs/__init__.py +29 -0
- spectre_core/capture_configs/_capture_config.py +85 -0
- spectre_core/capture_configs/_capture_templates.py +222 -0
- spectre_core/capture_configs/_parameters.py +110 -0
- spectre_core/capture_configs/_pconstraints.py +82 -0
- spectre_core/capture_configs/_ptemplates.py +450 -0
- spectre_core/capture_configs/_pvalidators.py +173 -0
- spectre_core/chunks/__init__.py +17 -201
- spectre_core/chunks/{base.py → _base.py} +15 -60
- spectre_core/chunks/_chunks.py +200 -0
- spectre_core/chunks/{factory.py → _factory.py} +6 -7
- spectre_core/chunks/library/{callisto/chunk.py → _callisto.py} +4 -7
- spectre_core/chunks/library/{fixed/chunk.py → _fixed_center_frequency.py} +7 -64
- spectre_core/chunks/library/_swept_center_frequency.py +103 -0
- spectre_core/config/__init__.py +20 -0
- spectre_core/config/_paths.py +77 -0
- spectre_core/config/_time_formats.py +15 -0
- spectre_core/exceptions.py +4 -5
- spectre_core/logging/__init__.py +11 -0
- spectre_core/logging/_configure.py +35 -0
- spectre_core/logging/_decorators.py +19 -0
- spectre_core/{logging.py → logging/_log_handlers.py} +13 -58
- spectre_core/plotting/__init__.py +7 -1
- spectre_core/plotting/{base.py → _base.py} +40 -20
- spectre_core/plotting/_format.py +18 -0
- spectre_core/plotting/{panel_stack.py → _panel_stack.py} +48 -48
- spectre_core/plotting/_panels.py +234 -0
- spectre_core/post_processing/__init__.py +10 -2
- spectre_core/post_processing/_base.py +119 -0
- spectre_core/post_processing/{factory.py → _factory.py} +7 -6
- spectre_core/post_processing/{post_processor.py → _post_processor.py} +3 -3
- spectre_core/post_processing/library/_fixed_center_frequency.py +115 -0
- spectre_core/post_processing/library/_swept_center_frequency.py +382 -0
- spectre_core/receivers/__init__.py +12 -2
- spectre_core/receivers/_base.py +352 -0
- spectre_core/receivers/{factory.py → _factory.py} +2 -2
- spectre_core/receivers/_spec_names.py +20 -0
- spectre_core/receivers/gr/__init__.py +3 -0
- spectre_core/receivers/gr/_base.py +33 -0
- spectre_core/receivers/gr/_rsp1a.py +158 -0
- spectre_core/receivers/gr/_test.py +123 -0
- spectre_core/receivers/library/_rsp1a.py +61 -0
- spectre_core/receivers/library/_test.py +221 -0
- spectre_core/spectrograms/__init__.py +18 -0
- spectre_core/spectrograms/{analytical.py → _analytical.py} +29 -27
- spectre_core/spectrograms/{array_operations.py → _array_operations.py} +47 -1
- spectre_core/spectrograms/{spectrogram.py → _spectrogram.py} +62 -35
- spectre_core/spectrograms/{transform.py → _transform.py} +76 -89
- spectre_core/{post_processing/library → wgetting}/__init__.py +4 -5
- spectre_core/wgetting/_callisto.py +155 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/METADATA +1 -1
- spectre_core-0.0.10.dist-info/RECORD +63 -0
- spectre_core/cfg.py +0 -116
- spectre_core/chunks/library/__init__.py +0 -8
- spectre_core/chunks/library/sweep/__init__.py +0 -0
- spectre_core/chunks/library/sweep/chunk.py +0 -400
- spectre_core/dynamic_imports.py +0 -22
- spectre_core/file_handlers/base.py +0 -68
- spectre_core/file_handlers/configs.py +0 -271
- spectre_core/file_handlers/json.py +0 -40
- spectre_core/file_handlers/text.py +0 -21
- spectre_core/plotting/factory.py +0 -26
- spectre_core/plotting/format.py +0 -19
- spectre_core/plotting/library/__init__.py +0 -7
- spectre_core/plotting/library/frequency_cuts/panel.py +0 -74
- spectre_core/plotting/library/integral_over_frequency/panel.py +0 -34
- spectre_core/plotting/library/spectrogram/panel.py +0 -92
- spectre_core/plotting/library/time_cuts/panel.py +0 -77
- spectre_core/plotting/panel_register.py +0 -13
- spectre_core/post_processing/base.py +0 -132
- spectre_core/post_processing/library/fixed/__init__.py +0 -0
- spectre_core/post_processing/library/fixed/event_handler.py +0 -40
- spectre_core/post_processing/library/sweep/event_handler.py +0 -54
- spectre_core/receivers/base.py +0 -422
- spectre_core/receivers/library/__init__.py +0 -7
- spectre_core/receivers/library/rsp1a/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/fixed.py +0 -104
- spectre_core/receivers/library/rsp1a/gr/sweep.py +0 -129
- spectre_core/receivers/library/rsp1a/receiver.py +0 -68
- spectre_core/receivers/library/rspduo/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +0 -114
- spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +0 -131
- spectre_core/receivers/library/rspduo/gr/tuner_2_fixed.py +0 -120
- spectre_core/receivers/library/rspduo/gr/tuner_2_sweep.py +0 -119
- spectre_core/receivers/library/rspduo/receiver.py +0 -97
- spectre_core/receivers/library/test/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/cosine_signal_1.py +0 -83
- spectre_core/receivers/library/test/gr/tagged_staircase.py +0 -93
- spectre_core/receivers/library/test/receiver.py +0 -203
- spectre_core/receivers/validators.py +0 -231
- spectre_core/web_fetch/callisto.py +0 -101
- spectre_core-0.0.9.dist-info/RECORD +0 -74
- /spectre_core/chunks/{chunk_register.py → _register.py} +0 -0
- /spectre_core/post_processing/{event_handler_register.py → _register.py} +0 -0
- /spectre_core/receivers/{receiver_register.py → _register.py} +0 -0
- /spectre_core/{chunks/library/callisto/__init__.py → receivers/gr/_rspduo.py} +0 -0
- /spectre_core/{chunks/library/fixed/__init__.py → receivers/library/_rspduo.py} +0 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/LICENSE +0 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/WHEEL +0 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from logging import getLogger
|
6
|
+
_LOGGER = getLogger(__name__)
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
from typing import Tuple
|
10
|
+
|
11
|
+
import os
|
12
|
+
|
13
|
+
from spectre_core.capture_configs import CaptureConfig, PNames, CaptureModes
|
14
|
+
from spectre_core.chunks import BaseChunk
|
15
|
+
from spectre_core.spectrograms import Spectrogram, time_average, frequency_average
|
16
|
+
from .._base import BaseEventHandler, make_sft_instance
|
17
|
+
from .._register import register_event_handler
|
18
|
+
|
19
|
+
|
20
|
+
def _do_stfft(iq_data: np.array,
|
21
|
+
capture_config: CaptureConfig,
|
22
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
23
|
+
"""For reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.ShortTimeFFT.html"""
|
24
|
+
|
25
|
+
sft = make_sft_instance(capture_config)
|
26
|
+
|
27
|
+
# set p0=0, since by convention in the STFFT docs, p=0 corresponds to the slice centred at t=0
|
28
|
+
p0=0
|
29
|
+
|
30
|
+
# set p1 to the index of the first slice where the "midpoint" of the window is still inside the signal
|
31
|
+
num_samples = len(iq_data)
|
32
|
+
p1 = sft.upper_border_begin(num_samples)[1]
|
33
|
+
|
34
|
+
# compute a ShortTimeFFT on the IQ samples
|
35
|
+
complex_spectra = sft.stft(iq_data,
|
36
|
+
p0 = p0,
|
37
|
+
p1 = p1)
|
38
|
+
|
39
|
+
# compute the magnitude of each spectral component
|
40
|
+
dynamic_spectra = np.abs(complex_spectra)
|
41
|
+
|
42
|
+
|
43
|
+
# assign a physical time to each spectrum
|
44
|
+
# p0 is defined to correspond with the first sample, at t=0 [s]
|
45
|
+
times = sft.t(num_samples,
|
46
|
+
p0 = p0,
|
47
|
+
p1 = p1)
|
48
|
+
# assign physical frequencies to each spectral component
|
49
|
+
frequencies = sft.f + capture_config.get_parameter_value(PNames.CENTER_FREQUENCY)
|
50
|
+
|
51
|
+
return times, frequencies, dynamic_spectra
|
52
|
+
|
53
|
+
|
54
|
+
def _build_spectrogram(chunk: BaseChunk,
|
55
|
+
capture_config: CaptureConfig) -> Spectrogram:
|
56
|
+
"""Create a spectrogram by performing a Short Time FFT on the IQ samples for this chunk."""
|
57
|
+
|
58
|
+
# read the data from the chunk
|
59
|
+
millisecond_correction = chunk.read_file("hdr")
|
60
|
+
iq_data = chunk.read_file("bin")
|
61
|
+
|
62
|
+
# units conversion
|
63
|
+
microsecond_correction = millisecond_correction * 1e3
|
64
|
+
|
65
|
+
times, frequencies, dynamic_spectra = _do_stfft(iq_data,
|
66
|
+
capture_config)
|
67
|
+
|
68
|
+
# explicitly type cast data arrays to 32-bit floats
|
69
|
+
times = np.array(times, dtype = 'float32')
|
70
|
+
frequencies = np.array(frequencies, dtype = 'float32')
|
71
|
+
dynamic_spectra = np.array(dynamic_spectra, dtype = 'float32')
|
72
|
+
|
73
|
+
return Spectrogram(dynamic_spectra,
|
74
|
+
times,
|
75
|
+
frequencies,
|
76
|
+
chunk.tag,
|
77
|
+
chunk_start_time = chunk.chunk_start_time,
|
78
|
+
microsecond_correction = microsecond_correction,
|
79
|
+
spectrum_type = "amplitude")
|
80
|
+
|
81
|
+
|
82
|
+
@register_event_handler(CaptureModes.FIXED_CENTER_FREQUENCY)
|
83
|
+
class _EventHandler(BaseEventHandler):
|
84
|
+
def __init__(self, *args, **kwargs):
|
85
|
+
super().__init__(*args, **kwargs)
|
86
|
+
|
87
|
+
def process(self,
|
88
|
+
absolute_file_path: str):
|
89
|
+
_LOGGER.info(f"Processing: {absolute_file_path}")
|
90
|
+
file_name = os.path.basename(absolute_file_path)
|
91
|
+
base_file_name, _ = os.path.splitext(file_name)
|
92
|
+
chunk_start_time, tag = base_file_name.split('_')
|
93
|
+
|
94
|
+
# create an instance of the current chunk being processed
|
95
|
+
chunk = self._Chunk(chunk_start_time, tag)
|
96
|
+
|
97
|
+
_LOGGER.info("Creating spectrogram")
|
98
|
+
spectrogram = _build_spectrogram(chunk,
|
99
|
+
self._capture_config)
|
100
|
+
|
101
|
+
spectrogram = time_average(spectrogram,
|
102
|
+
resolution = self._capture_config.get_parameter_value(PNames.TIME_RESOLUTION))
|
103
|
+
|
104
|
+
spectrogram = frequency_average(spectrogram,
|
105
|
+
resolution = self._capture_config.get_parameter_value(PNames.FREQUENCY_RESOLUTION))
|
106
|
+
|
107
|
+
self._cache_spectrogram(spectrogram)
|
108
|
+
|
109
|
+
bin_chunk = chunk.get_file('bin')
|
110
|
+
_LOGGER.info(f"Deleting {bin_chunk.file_path}")
|
111
|
+
bin_chunk.delete()
|
112
|
+
|
113
|
+
hdr_chunk = chunk.get_file('hdr')
|
114
|
+
_LOGGER.info(f"Deleting {hdr_chunk.file_path}")
|
115
|
+
hdr_chunk.delete()
|
@@ -0,0 +1,382 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from logging import getLogger
|
6
|
+
_LOGGER = getLogger(__name__)
|
7
|
+
|
8
|
+
import os
|
9
|
+
from typing import Optional
|
10
|
+
from typing import Tuple
|
11
|
+
from datetime import timedelta, datetime
|
12
|
+
import numpy as np
|
13
|
+
|
14
|
+
from scipy.signal import ShortTimeFFT
|
15
|
+
|
16
|
+
from spectre_core.spectrograms import Spectrogram, time_average, frequency_average
|
17
|
+
from spectre_core.config import TimeFormats
|
18
|
+
from spectre_core.capture_configs import CaptureConfig, PNames, CaptureModes
|
19
|
+
from spectre_core.chunks import BaseChunk
|
20
|
+
from spectre_core.chunks import SweepMetadata
|
21
|
+
from spectre_core.exceptions import InvalidSweepMetadataError
|
22
|
+
from .._base import BaseEventHandler, make_sft_instance
|
23
|
+
from .._register import register_event_handler
|
24
|
+
|
25
|
+
|
26
|
+
def _stitch_steps(stepped_dynamic_spectra: np.ndarray,
|
27
|
+
num_full_sweeps: int) -> np.ndarray:
|
28
|
+
"""For each full sweep, create a swept spectrum by stitching together the spectrum at each of the steps."""
|
29
|
+
return stepped_dynamic_spectra.reshape((num_full_sweeps, -1)).T
|
30
|
+
|
31
|
+
|
32
|
+
def _average_over_steps(stepped_dynamic_spectra: np.ndarray) -> None:
|
33
|
+
"""Average the spectrums in each step totally in time."""
|
34
|
+
return np.nanmean(stepped_dynamic_spectra[..., 1:], axis=-1)
|
35
|
+
|
36
|
+
|
37
|
+
def _fill_times(times: np.ndarray,
|
38
|
+
num_samples: np.ndarray,
|
39
|
+
sample_rate: int,
|
40
|
+
num_full_sweeps: int,
|
41
|
+
num_steps_per_sweep: int) -> None:
|
42
|
+
"""Assign physical times to each swept spectrum. We use (by convention) the time of the first sample in eahc sweep"""
|
43
|
+
|
44
|
+
sampling_interval = 1 / sample_rate
|
45
|
+
cumulative_samples = 0
|
46
|
+
for sweep_index in range(num_full_sweeps):
|
47
|
+
# assign a physical time to the spectrum for this sweep
|
48
|
+
times[sweep_index] = cumulative_samples * sampling_interval
|
49
|
+
|
50
|
+
# find the total number of samples across the sweep
|
51
|
+
start_step = sweep_index * num_steps_per_sweep
|
52
|
+
end_step = (sweep_index + 1) * num_steps_per_sweep
|
53
|
+
|
54
|
+
# update cumulative samples
|
55
|
+
cumulative_samples += np.sum(num_samples[start_step:end_step])
|
56
|
+
|
57
|
+
|
58
|
+
def _fill_frequencies(frequencies: np.ndarray,
|
59
|
+
center_frequencies: np.ndarray,
|
60
|
+
baseband_frequencies: np.ndarray,
|
61
|
+
window_size: int) -> None:
|
62
|
+
"""Assign physical frequencies to each of the swept spectral components."""
|
63
|
+
for i, center_frequency in enumerate(np.unique(center_frequencies)):
|
64
|
+
lower_bound = i * window_size
|
65
|
+
upper_bound = (i + 1) * window_size
|
66
|
+
frequencies[lower_bound:upper_bound] = (baseband_frequencies + center_frequency)
|
67
|
+
|
68
|
+
|
69
|
+
def _fill_stepped_dynamic_spectra(stepped_dynamic_spectra: np.ndarray,
|
70
|
+
sft: ShortTimeFFT,
|
71
|
+
iq_data: np.ndarray,
|
72
|
+
num_samples: np.ndarray,
|
73
|
+
num_full_sweeps: int,
|
74
|
+
num_steps_per_sweep: int) -> None:
|
75
|
+
"""Compute the dynamic spectra for the input IQ samples for each step.
|
76
|
+
|
77
|
+
All IQ samples per step were collected at the same center frequency.
|
78
|
+
"""
|
79
|
+
# global_step_index will hold the step index over all sweeps (doesn't reset each sweep)
|
80
|
+
# start_sample_index will hold the index of the first sample in the step
|
81
|
+
global_step_index, start_sample_index = 0, 0
|
82
|
+
for sweep_index in range(num_full_sweeps):
|
83
|
+
for step_index in range(num_steps_per_sweep):
|
84
|
+
# extract how many samples are in the current step from the metadata
|
85
|
+
end_sample_index = start_sample_index + num_samples[global_step_index]
|
86
|
+
# compute the number of slices in the current step based on the window we defined on the capture config
|
87
|
+
num_slices = sft.upper_border_begin(num_samples[global_step_index])[1]
|
88
|
+
# perform a short time fast fourier transform on the step
|
89
|
+
complex_spectra = sft.stft(iq_data[start_sample_index:end_sample_index],
|
90
|
+
p0=0,
|
91
|
+
p1=num_slices)
|
92
|
+
# 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
|
+
stepped_dynamic_spectra[sweep_index, step_index, :, :num_slices] = np.abs(complex_spectra)
|
94
|
+
# reassign the start_sample_index for the next step
|
95
|
+
start_sample_index = end_sample_index
|
96
|
+
# and increment the global step index
|
97
|
+
global_step_index += 1
|
98
|
+
|
99
|
+
|
100
|
+
def _compute_num_max_slices_in_step(sft: ShortTimeFFT,
|
101
|
+
num_samples: np.ndarray) -> int:
|
102
|
+
"""Compute the maximum number of slices over all steps, in all sweeps over the chunk."""
|
103
|
+
return sft.upper_border_begin(np.max(num_samples))[1]
|
104
|
+
|
105
|
+
|
106
|
+
def _compute_num_full_sweeps(center_frequencies: np.ndarray) -> int:
|
107
|
+
"""Compute the total number of full sweeps over the chunk.
|
108
|
+
|
109
|
+
Since the number of each samples in each step is variable, we only know a sweep is complete
|
110
|
+
when there is a sweep after it. So we can define the total number of *full* sweeps as the number of
|
111
|
+
(freq_max, freq_min) pairs in center_frequencies. It is only at an instance of (freq_max, freq_min) pair
|
112
|
+
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)
|
114
|
+
"""
|
115
|
+
return len(np.where(np.diff(center_frequencies) < 0)[0])
|
116
|
+
|
117
|
+
|
118
|
+
def _compute_num_steps_per_sweep(center_frequencies: np.ndarray) -> int:
|
119
|
+
"""Compute the (ensured constant) number of steps in each sweep."""
|
120
|
+
# find the (step) indices corresponding to the minimum frequencies
|
121
|
+
min_freq_indices = np.where(center_frequencies == np.min(center_frequencies))[0]
|
122
|
+
# then, we evaluate the number of steps that has occured between them via np.diff over the indices
|
123
|
+
unique_num_steps_per_sweep = np.unique(np.diff(min_freq_indices))
|
124
|
+
# we expect that the difference is always the same, so that the result of np.unique has a single element
|
125
|
+
if len(unique_num_steps_per_sweep) != 1:
|
126
|
+
raise InvalidSweepMetadataError(("Irregular step count per sweep, "
|
127
|
+
"expected a consistent number of steps per sweep"))
|
128
|
+
return int(unique_num_steps_per_sweep[0])
|
129
|
+
|
130
|
+
|
131
|
+
def _validate_center_frequencies_ordering(center_frequencies: np.ndarray,
|
132
|
+
freq_step: float) -> None:
|
133
|
+
"""Check that the center frequencies are well-ordered in the detached header"""
|
134
|
+
min_frequency = np.min(center_frequencies)
|
135
|
+
# Extract the expected difference between each step within a sweep.
|
136
|
+
for i, diff in enumerate(np.diff(center_frequencies)):
|
137
|
+
# steps should either increase by freq_step or drop to the minimum
|
138
|
+
if (diff != freq_step) and (center_frequencies[i + 1] != min_frequency):
|
139
|
+
raise InvalidSweepMetadataError(f"Unordered center frequencies detected")
|
140
|
+
|
141
|
+
|
142
|
+
def _do_stfft(iq_data: np.ndarray,
|
143
|
+
sweep_metadata: SweepMetadata,
|
144
|
+
capture_config: CaptureConfig):
|
145
|
+
"""Perform a Short Time FFT on the input swept IQ samples."""
|
146
|
+
|
147
|
+
sft = make_sft_instance(capture_config)
|
148
|
+
|
149
|
+
frequency_step = capture_config.get_parameter_value(PNames.FREQUENCY_STEP)
|
150
|
+
_validate_center_frequencies_ordering(sweep_metadata.center_frequencies,
|
151
|
+
frequency_step)
|
152
|
+
|
153
|
+
window_size = capture_config.get_parameter_value(PNames.WINDOW_SIZE)
|
154
|
+
|
155
|
+
num_steps_per_sweep = _compute_num_steps_per_sweep(sweep_metadata.center_frequencies)
|
156
|
+
num_full_sweeps = _compute_num_full_sweeps(sweep_metadata.center_frequencies)
|
157
|
+
num_max_slices_in_step = _compute_num_max_slices_in_step(sft,
|
158
|
+
sweep_metadata.num_samples)
|
159
|
+
|
160
|
+
stepped_dynamic_spectra_shape = (num_full_sweeps,
|
161
|
+
num_steps_per_sweep,
|
162
|
+
window_size,
|
163
|
+
num_max_slices_in_step)
|
164
|
+
stepped_dynamic_spectra = np.full(stepped_dynamic_spectra_shape, np.nan)
|
165
|
+
|
166
|
+
frequencies = np.empty(num_steps_per_sweep * window_size)
|
167
|
+
times = np.empty(num_full_sweeps)
|
168
|
+
|
169
|
+
_fill_stepped_dynamic_spectra(stepped_dynamic_spectra,
|
170
|
+
sft,
|
171
|
+
iq_data,
|
172
|
+
sweep_metadata.num_samples,
|
173
|
+
num_full_sweeps,
|
174
|
+
num_steps_per_sweep)
|
175
|
+
|
176
|
+
_fill_frequencies(frequencies,
|
177
|
+
sweep_metadata.center_frequencies,
|
178
|
+
sft.f,
|
179
|
+
window_size)
|
180
|
+
|
181
|
+
sample_rate = capture_config.get_parameter_value(PNames.SAMPLE_RATE)
|
182
|
+
_fill_times(times,
|
183
|
+
sweep_metadata.num_samples,
|
184
|
+
sample_rate,
|
185
|
+
num_full_sweeps,
|
186
|
+
num_steps_per_sweep)
|
187
|
+
|
188
|
+
averaged_spectra = _average_over_steps(stepped_dynamic_spectra)
|
189
|
+
dynamic_spectra = _stitch_steps(averaged_spectra,
|
190
|
+
num_full_sweeps)
|
191
|
+
|
192
|
+
return times, frequencies, dynamic_spectra
|
193
|
+
|
194
|
+
|
195
|
+
def _correct_timing(chunk_start_datetime: datetime,
|
196
|
+
millisecond_correction: int,
|
197
|
+
num_samples_prepended: int,
|
198
|
+
sample_rate: int):
|
199
|
+
"""Correct the start time for this chunk based on the number of samples we prepended reconstructing the initial sweep."""
|
200
|
+
sample_interval = (1 / sample_rate)
|
201
|
+
elapsed_time = num_samples_prepended * sample_interval
|
202
|
+
corrected_datetime = chunk_start_datetime + timedelta(milliseconds = millisecond_correction) - timedelta(seconds = float(elapsed_time))
|
203
|
+
return corrected_datetime.strftime(TimeFormats.DATETIME), corrected_datetime.microsecond * 1e-3
|
204
|
+
|
205
|
+
|
206
|
+
def _prepend_num_samples(carryover_num_samples: np.ndarray,
|
207
|
+
num_samples: np.ndarray,
|
208
|
+
final_step_spans_two_chunks: bool) -> np.ndarray:
|
209
|
+
"""Prepend the number of samples from the final sweep of the previous chunk."""
|
210
|
+
if final_step_spans_two_chunks:
|
211
|
+
# ensure the number of samples from the final step in the previous chunk are accounted for
|
212
|
+
num_samples[0] += carryover_num_samples[-1]
|
213
|
+
# and truncate as required
|
214
|
+
carryover_num_samples = carryover_num_samples[:-1]
|
215
|
+
return np.concatenate((carryover_num_samples, num_samples))
|
216
|
+
|
217
|
+
|
218
|
+
def _prepend_center_frequencies(carryover_center_frequencies: np.ndarray,
|
219
|
+
center_frequencies: np.ndarray,
|
220
|
+
final_step_spans_two_chunks: bool)-> np.ndarray:
|
221
|
+
"""Prepend the center frequencies from the final sweep of the previous chunk."""
|
222
|
+
# in the case that the sweep has bled across chunks,
|
223
|
+
# do not permit identical neighbours in the center frequency array
|
224
|
+
if final_step_spans_two_chunks:
|
225
|
+
# truncate the final frequency to prepend (as it already exists in the array we are appending to in this case)
|
226
|
+
carryover_center_frequencies = carryover_center_frequencies[:-1]
|
227
|
+
return np.concatenate((carryover_center_frequencies, center_frequencies))
|
228
|
+
|
229
|
+
|
230
|
+
def _prepend_iq_data(carryover_iq_data: np.ndarray,
|
231
|
+
iq_data: np.ndarray) -> np.ndarray:
|
232
|
+
"""Prepend the IQ samples from the final sweep of the previous chunk."""
|
233
|
+
return np.concatenate((carryover_iq_data, iq_data))
|
234
|
+
|
235
|
+
|
236
|
+
def _get_final_sweep(previous_chunk: BaseChunk
|
237
|
+
) -> Tuple[np.ndarray, SweepMetadata]:
|
238
|
+
"""Get data from the final sweep of the previous chunk."""
|
239
|
+
# unpack the data from the previous chunk
|
240
|
+
previous_iq_data = previous_chunk.read_file("bin")
|
241
|
+
_, previous_sweep_metadata = previous_chunk.read_file("hdr")
|
242
|
+
# find the step index from the last sweep
|
243
|
+
# [0] since the return of np.where is a 1 element Tuple,
|
244
|
+
# containing a list of step indices corresponding to the smallest center frequencies
|
245
|
+
# [-1] since we want the final step index, where the center frequency is minimised
|
246
|
+
final_sweep_start_step_index = np.where(previous_sweep_metadata.center_frequencies == np.min(previous_sweep_metadata.center_frequencies))[0][-1]
|
247
|
+
# isolate the data from the final sweep
|
248
|
+
final_center_frequencies = previous_sweep_metadata.center_frequencies[final_sweep_start_step_index:]
|
249
|
+
final_num_samples = previous_sweep_metadata.num_samples[final_sweep_start_step_index:]
|
250
|
+
final_sweep_iq_data = previous_iq_data[-np.sum(final_num_samples):]
|
251
|
+
|
252
|
+
# sanity check on the number of samples in the final sweep
|
253
|
+
if len(final_sweep_iq_data) != np.sum(final_num_samples):
|
254
|
+
raise ValueError((f"Unexpected error! Mismatch in sample count for the final sweep data."
|
255
|
+
f"Expected {np.sum(final_num_samples)} based on sweep metadata, but found "
|
256
|
+
f" {len(final_sweep_iq_data)} IQ samples in the final sweep"))
|
257
|
+
|
258
|
+
return final_sweep_iq_data, SweepMetadata(final_center_frequencies, final_num_samples)
|
259
|
+
|
260
|
+
|
261
|
+
def _reconstruct_initial_sweep(previous_chunk: BaseChunk,
|
262
|
+
iq_data: np.ndarray,
|
263
|
+
sweep_metadata: SweepMetadata) -> Tuple[np.ndarray, SweepMetadata, int]:
|
264
|
+
"""Reconstruct the initial sweep of the current chunk, using data from the previous chunk."""
|
265
|
+
|
266
|
+
# carryover the final sweep of the previous chunk, and prepend that data to the current chunk data
|
267
|
+
carryover_iq_data, carryover_sweep_metadata = _get_final_sweep(previous_chunk)
|
268
|
+
|
269
|
+
# prepend the iq data that was carried over from the previous chunk
|
270
|
+
iq_data = _prepend_iq_data(carryover_iq_data,
|
271
|
+
iq_data)
|
272
|
+
|
273
|
+
# prepend the sweep metadata from the previous chunk
|
274
|
+
final_step_spans_two_chunks = carryover_sweep_metadata.center_frequencies[-1] == sweep_metadata.center_frequencies[0]
|
275
|
+
center_frequencies = _prepend_center_frequencies(carryover_sweep_metadata.center_frequencies,
|
276
|
+
sweep_metadata.center_frequencies,
|
277
|
+
final_step_spans_two_chunks)
|
278
|
+
num_samples = _prepend_num_samples(carryover_sweep_metadata.num_samples,
|
279
|
+
sweep_metadata.num_samples,
|
280
|
+
final_step_spans_two_chunks)
|
281
|
+
|
282
|
+
# keep track of how many samples we prepended (required to adjust timing later)
|
283
|
+
num_samples_prepended = np.sum(carryover_sweep_metadata.num_samples)
|
284
|
+
return iq_data, SweepMetadata(center_frequencies, num_samples), num_samples_prepended
|
285
|
+
|
286
|
+
|
287
|
+
def _build_spectrogram(chunk: BaseChunk,
|
288
|
+
capture_config: CaptureConfig,
|
289
|
+
previous_chunk: Optional[BaseChunk] = None) -> Spectrogram:
|
290
|
+
"""Create a spectrogram by performing a Short Time FFT on the (swept) IQ samples for this chunk."""
|
291
|
+
iq_data = chunk.read_file("bin")
|
292
|
+
millisecond_correction, sweep_metadata = chunk.read_file("hdr")
|
293
|
+
|
294
|
+
# if a previous chunk has been specified, this indicates that the initial sweep spans
|
295
|
+
# between two adjacent batched files.
|
296
|
+
if previous_chunk:
|
297
|
+
# If this is the case, first reconstruct the initial sweep of the current chunk
|
298
|
+
# by prepending the final sweep of the previous chunk
|
299
|
+
iq_data, sweep_metadata, num_samples_prepended = _reconstruct_initial_sweep(previous_chunk,
|
300
|
+
iq_data,
|
301
|
+
sweep_metadata)
|
302
|
+
# since we have prepended extra samples, we need to correct the chunk start time
|
303
|
+
# appropriately
|
304
|
+
chunk_start_time, millisecond_correction = _correct_timing(chunk.chunk_start_datetime,
|
305
|
+
millisecond_correction,
|
306
|
+
num_samples_prepended,
|
307
|
+
capture_config.get_parameter_value(PNames.SAMPLE_RATE))
|
308
|
+
# otherwise, no action is required
|
309
|
+
else:
|
310
|
+
chunk_start_time = chunk.chunk_start_time
|
311
|
+
|
312
|
+
microsecond_correction = millisecond_correction * 1e3
|
313
|
+
|
314
|
+
times, frequencies, dynamic_spectra = _do_stfft(iq_data,
|
315
|
+
sweep_metadata,
|
316
|
+
capture_config)
|
317
|
+
|
318
|
+
return Spectrogram(dynamic_spectra,
|
319
|
+
times,
|
320
|
+
frequencies,
|
321
|
+
chunk.tag,
|
322
|
+
chunk_start_time,
|
323
|
+
microsecond_correction,
|
324
|
+
spectrum_type = "amplitude")
|
325
|
+
|
326
|
+
|
327
|
+
@register_event_handler(CaptureModes.SWEPT_CENTER_FREQUENCY)
|
328
|
+
class _EventHandler(BaseEventHandler):
|
329
|
+
def __init__(self, *args, **kwargs):
|
330
|
+
super().__init__(*args, **kwargs)
|
331
|
+
|
332
|
+
# the previous chunk is stored in order to fetch the
|
333
|
+
# data from the "final sweep" which was ignored during
|
334
|
+
# processing.
|
335
|
+
self._previous_chunk: BaseChunk = None
|
336
|
+
|
337
|
+
|
338
|
+
def process(self,
|
339
|
+
absolute_file_path: str):
|
340
|
+
_LOGGER.info(f"Processing: {absolute_file_path}")
|
341
|
+
file_name = os.path.basename(absolute_file_path)
|
342
|
+
# discard the extension
|
343
|
+
base_file_name, _ = os.path.splitext(file_name)
|
344
|
+
chunk_start_time, tag = base_file_name.split('_')
|
345
|
+
chunk = self._Chunk(chunk_start_time, tag)
|
346
|
+
|
347
|
+
# ensure that the file which has been created has the expected tag
|
348
|
+
if tag != self._tag:
|
349
|
+
raise RuntimeError(f"Received an unexpected tag! Expected '{self._tag}', "
|
350
|
+
f"but a file has been created with tag '{tag}'")
|
351
|
+
|
352
|
+
_LOGGER.info("Creating spectrogram")
|
353
|
+
spectrogram = _build_spectrogram(chunk,
|
354
|
+
self._capture_config,
|
355
|
+
previous_chunk = self._previous_chunk)
|
356
|
+
|
357
|
+
spectrogram = time_average(spectrogram,
|
358
|
+
resolution = self._capture_config.get_parameter_value(PNames.TIME_RESOLUTION))
|
359
|
+
|
360
|
+
spectrogram = frequency_average(spectrogram,
|
361
|
+
resolution = self._capture_config.get_parameter_value(PNames.FREQUENCY_RESOLUTION))
|
362
|
+
|
363
|
+
self._cache_spectrogram(spectrogram)
|
364
|
+
|
365
|
+
# if the previous chunk has not yet been set, it means we are processing the first chunk
|
366
|
+
# so we don't need to handle the previous chunk
|
367
|
+
if self._previous_chunk is None:
|
368
|
+
# instead, only set it for the next time this method is called
|
369
|
+
self._previous_chunk = chunk
|
370
|
+
|
371
|
+
# otherwise the previous chunk is defined (and by this point has already been processed)
|
372
|
+
else:
|
373
|
+
bin_chunk = self._previous_chunk.get_file('bin')
|
374
|
+
_LOGGER.info(f"Deleting {bin_chunk.file_path}")
|
375
|
+
bin_chunk.delete()
|
376
|
+
|
377
|
+
hdr_chunk = self._previous_chunk.get_file('hdr')
|
378
|
+
_LOGGER.info(f"Deleting {hdr_chunk.file_path}")
|
379
|
+
hdr_chunk.delete()
|
380
|
+
|
381
|
+
# and reassign the current chunk to be used as the previous chunk at the next call of this method
|
382
|
+
self._previous_chunk = chunk
|
@@ -2,5 +2,15 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
-
#
|
6
|
-
|
5
|
+
# register decorators take effect on import
|
6
|
+
from .library._test import _Receiver
|
7
|
+
from .library._rsp1a import _Receiver
|
8
|
+
|
9
|
+
from ._base import BaseReceiver
|
10
|
+
from ._factory import get_receiver
|
11
|
+
from ._register import list_all_receiver_names
|
12
|
+
from ._spec_names import SpecNames
|
13
|
+
|
14
|
+
__all__ = [
|
15
|
+
"BaseReceiver", "get_receiver", "list_all_receiver_names", "SpecNames"
|
16
|
+
]
|