spectre-core 0.0.11__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 +170 -65
- spectre_core/batches/__init__.py +21 -0
- spectre_core/batches/_base.py +238 -0
- spectre_core/batches/_batches.py +247 -0
- spectre_core/batches/_factory.py +69 -0
- spectre_core/batches/_register.py +30 -0
- 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 +116 -46
- 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 +121 -77
- spectre_core/config/__init__.py +7 -9
- spectre_core/config/_paths.py +66 -26
- spectre_core/config/_time_formats.py +15 -8
- 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 +71 -45
- spectre_core/post_processing/_factory.py +42 -12
- spectre_core/post_processing/_post_processor.py +27 -29
- 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/plugins/_swept_center_frequency.py +439 -0
- 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 +45 -55
- spectre_core/receivers/{gr → plugins/gr}/_rspduo.py +65 -78
- spectre_core/receivers/{gr → plugins/gr}/_test.py +36 -34
- spectre_core/spectrograms/__init__.py +5 -3
- spectre_core/spectrograms/_analytical.py +121 -72
- spectre_core/spectrograms/_array_operations.py +103 -36
- spectre_core/spectrograms/_spectrogram.py +410 -203
- spectre_core/spectrograms/_transform.py +199 -188
- spectre_core/wgetting/__init__.py +4 -2
- spectre_core/wgetting/_callisto.py +178 -127
- {spectre_core-0.0.11.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.11.dist-info → spectre_core-0.0.13.dist-info}/WHEEL +1 -1
- spectre_core/chunks/__init__.py +0 -22
- spectre_core/chunks/_base.py +0 -116
- spectre_core/chunks/_chunks.py +0 -200
- spectre_core/chunks/_factory.py +0 -25
- spectre_core/chunks/_register.py +0 -15
- spectre_core/chunks/library/_callisto.py +0 -98
- spectre_core/chunks/library/_fixed_center_frequency.py +0 -128
- spectre_core/chunks/library/_swept_center_frequency.py +0 -103
- 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 -115
- spectre_core/post_processing/library/_swept_center_frequency.py +0 -382
- 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.11.dist-info/RECORD +0 -64
- /spectre_core/receivers/{gr → plugins/gr}/__init__.py +0 -0
- {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/LICENSE +0 -0
- {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/top_level.txt +0 -0
@@ -1,176 +0,0 @@
|
|
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
|
-
|
6
|
-
from logging import getLogger
|
7
|
-
_LOGGER = getLogger(__name__)
|
8
|
-
|
9
|
-
import os
|
10
|
-
import logging
|
11
|
-
from dataclasses import dataclass
|
12
|
-
from typing import Optional
|
13
|
-
import warnings
|
14
|
-
from collections import OrderedDict
|
15
|
-
from datetime import datetime
|
16
|
-
|
17
|
-
from spectre_core._file_io import TextHandler
|
18
|
-
from spectre_core.config import get_logs_dir_path, TimeFormats
|
19
|
-
|
20
|
-
PROCESS_TYPES = [
|
21
|
-
"user",
|
22
|
-
"worker"
|
23
|
-
]
|
24
|
-
|
25
|
-
def _validate_process_type(process_type: str
|
26
|
-
) -> None:
|
27
|
-
if process_type not in PROCESS_TYPES:
|
28
|
-
raise ValueError(f"Invalid process type: {process_type}. Expected one of {PROCESS_TYPES}")
|
29
|
-
|
30
|
-
|
31
|
-
class LogHandler(TextHandler):
|
32
|
-
def __init__(self,
|
33
|
-
datetime_stamp: str,
|
34
|
-
pid: str,
|
35
|
-
process_type: str):
|
36
|
-
self._datetime_stamp = datetime_stamp
|
37
|
-
self._pid = pid
|
38
|
-
_validate_process_type(process_type)
|
39
|
-
self._process_type = process_type
|
40
|
-
|
41
|
-
dt = datetime.strptime(datetime_stamp, TimeFormats.DATETIME)
|
42
|
-
parent_path = get_logs_dir_path(dt.year, dt.month, dt.day)
|
43
|
-
base_file_name = f"{datetime_stamp}_{pid}_{process_type}"
|
44
|
-
|
45
|
-
super().__init__(parent_path, base_file_name, extension = "log")
|
46
|
-
|
47
|
-
|
48
|
-
@property
|
49
|
-
def datetime_stamp(self) -> str:
|
50
|
-
return self._datetime_stamp
|
51
|
-
|
52
|
-
|
53
|
-
@property
|
54
|
-
def pid(self) -> str:
|
55
|
-
return self._pid
|
56
|
-
|
57
|
-
|
58
|
-
@property
|
59
|
-
def process_type(self) -> str:
|
60
|
-
return self._process_type
|
61
|
-
|
62
|
-
|
63
|
-
class LogHandlers:
|
64
|
-
def __init__(self,
|
65
|
-
process_type: Optional[str] = None,
|
66
|
-
year: Optional[int] = None,
|
67
|
-
month: Optional[int] = None,
|
68
|
-
day: Optional[int] = None):
|
69
|
-
self._log_handler_map: dict[str, LogHandler] = OrderedDict()
|
70
|
-
self._process_type = process_type
|
71
|
-
self.set_date(year, month, day)
|
72
|
-
|
73
|
-
|
74
|
-
@property
|
75
|
-
def process_type(self) -> str:
|
76
|
-
return self._process_type
|
77
|
-
|
78
|
-
|
79
|
-
@property
|
80
|
-
def year(self) -> Optional[int]:
|
81
|
-
return self._year
|
82
|
-
|
83
|
-
|
84
|
-
@property
|
85
|
-
def month(self) -> Optional[int]:
|
86
|
-
return self._month
|
87
|
-
|
88
|
-
|
89
|
-
@property
|
90
|
-
def day(self) -> Optional[int]:
|
91
|
-
return self._day
|
92
|
-
|
93
|
-
|
94
|
-
@property
|
95
|
-
def logs_dir_path(self) -> str:
|
96
|
-
return get_logs_dir_path(self.year, self.month, self.day)
|
97
|
-
|
98
|
-
|
99
|
-
@property
|
100
|
-
def log_handler_map(self) -> dict[str, LogHandler]:
|
101
|
-
if not self._log_handler_map: # check for empty dictionary
|
102
|
-
self._update_log_handler_map()
|
103
|
-
return self._log_handler_map
|
104
|
-
|
105
|
-
|
106
|
-
@property
|
107
|
-
def log_handler_list(self) -> list[LogHandler]:
|
108
|
-
return list(self.log_handler_map.values())
|
109
|
-
|
110
|
-
|
111
|
-
@property
|
112
|
-
def num_logs(self) -> int:
|
113
|
-
return len(self.log_handler_list)
|
114
|
-
|
115
|
-
|
116
|
-
@property
|
117
|
-
def file_names(self) -> list[str]:
|
118
|
-
return list(self.log_handler_map.keys())
|
119
|
-
|
120
|
-
|
121
|
-
def set_date(self,
|
122
|
-
year: Optional[int],
|
123
|
-
month: Optional[int],
|
124
|
-
day: Optional[int]) -> None:
|
125
|
-
self._year = year
|
126
|
-
self._month = month
|
127
|
-
self._day = day
|
128
|
-
self._update_log_handler_map()
|
129
|
-
|
130
|
-
|
131
|
-
def _update_log_handler_map(self) -> None:
|
132
|
-
log_files = [f for (_, _, files) in os.walk(self.logs_dir_path) for f in files]
|
133
|
-
|
134
|
-
if not log_files:
|
135
|
-
warning_message = "No logs found, setting log map to an empty dictionary"
|
136
|
-
_LOGGER.warning(warning_message)
|
137
|
-
warnings.warn(warning_message)
|
138
|
-
return
|
139
|
-
|
140
|
-
for log_file in log_files:
|
141
|
-
file_name, _ = os.path.splitext(log_file)
|
142
|
-
log_start_time, pid, process_type = file_name.split("_")
|
143
|
-
|
144
|
-
if self.process_type and process_type != self.process_type:
|
145
|
-
continue
|
146
|
-
|
147
|
-
self._log_handler_map[file_name] = LogHandler(log_start_time, pid, process_type)
|
148
|
-
|
149
|
-
self._log_handler_map = OrderedDict(sorted(self._log_handler_map.items()))
|
150
|
-
|
151
|
-
|
152
|
-
def update(self) -> None:
|
153
|
-
"""Public alias for setting log handler map"""
|
154
|
-
self._update_log_handler_map()
|
155
|
-
|
156
|
-
|
157
|
-
def __iter__(self):
|
158
|
-
yield from self.log_handler_list
|
159
|
-
|
160
|
-
|
161
|
-
def get_from_file_name(self,
|
162
|
-
file_name: str) -> LogHandler:
|
163
|
-
# auto strip the extension if present
|
164
|
-
file_name, _ = os.path.splitext(file_name)
|
165
|
-
try:
|
166
|
-
return self.log_handler_map[file_name]
|
167
|
-
except KeyError:
|
168
|
-
raise FileNotFoundError(f"Log handler for file name '{file_name}' not found in log map")
|
169
|
-
|
170
|
-
|
171
|
-
def get_from_pid(self,
|
172
|
-
pid: str) -> LogHandler:
|
173
|
-
for log_handler in self.log_handler_list:
|
174
|
-
if log_handler.pid == pid:
|
175
|
-
return log_handler
|
176
|
-
raise FileNotFoundError(f"Log handler for PID '{pid}' not found in log map")
|
@@ -1,115 +0,0 @@
|
|
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()
|
@@ -1,382 +0,0 @@
|
|
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
|