spectre-core 0.0.22__py3-none-any.whl → 0.0.24__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spectre_core/__init__.py +5 -0
- spectre_core/_file_io/__init__.py +4 -4
- spectre_core/_file_io/file_handlers.py +60 -106
- spectre_core/batches/__init__.py +20 -3
- spectre_core/batches/_base.py +85 -134
- spectre_core/batches/_batches.py +55 -99
- spectre_core/batches/_factory.py +21 -20
- spectre_core/batches/_register.py +8 -8
- spectre_core/batches/plugins/_batch_keys.py +7 -6
- spectre_core/batches/plugins/_callisto.py +65 -97
- spectre_core/batches/plugins/_iq_stream.py +105 -169
- spectre_core/capture_configs/__init__.py +46 -17
- spectre_core/capture_configs/_capture_config.py +25 -52
- spectre_core/capture_configs/_capture_modes.py +8 -6
- spectre_core/capture_configs/_capture_templates.py +50 -110
- spectre_core/capture_configs/_parameters.py +37 -74
- spectre_core/capture_configs/_pconstraints.py +40 -40
- spectre_core/capture_configs/_pnames.py +36 -34
- spectre_core/capture_configs/_ptemplates.py +260 -347
- spectre_core/capture_configs/_pvalidators.py +99 -102
- spectre_core/config/__init__.py +19 -8
- spectre_core/config/_paths.py +25 -47
- spectre_core/config/_time_formats.py +6 -5
- spectre_core/exceptions.py +38 -0
- spectre_core/jobs/__init__.py +3 -6
- spectre_core/jobs/_duration.py +12 -0
- spectre_core/jobs/_jobs.py +72 -43
- spectre_core/jobs/_workers.py +55 -105
- spectre_core/logs/__init__.py +7 -2
- spectre_core/logs/_configure.py +13 -17
- spectre_core/logs/_decorators.py +6 -4
- spectre_core/logs/_logs.py +37 -89
- spectre_core/logs/_process_types.py +5 -3
- spectre_core/plotting/__init__.py +19 -3
- spectre_core/plotting/_base.py +112 -177
- spectre_core/plotting/_format.py +10 -8
- spectre_core/plotting/_panel_names.py +7 -5
- spectre_core/plotting/_panel_stack.py +138 -130
- spectre_core/plotting/_panels.py +152 -162
- spectre_core/post_processing/__init__.py +6 -3
- spectre_core/post_processing/_base.py +41 -55
- spectre_core/post_processing/_factory.py +14 -11
- spectre_core/post_processing/_post_processor.py +16 -12
- spectre_core/post_processing/_register.py +10 -7
- spectre_core/post_processing/plugins/_event_handler_keys.py +4 -3
- spectre_core/post_processing/plugins/_fixed_center_frequency.py +54 -47
- spectre_core/post_processing/plugins/_swept_center_frequency.py +199 -174
- spectre_core/receivers/__init__.py +9 -2
- spectre_core/receivers/_base.py +82 -148
- spectre_core/receivers/_factory.py +20 -30
- spectre_core/receivers/_register.py +7 -10
- spectre_core/receivers/_spec_names.py +17 -15
- spectre_core/receivers/plugins/_b200mini.py +47 -60
- spectre_core/receivers/plugins/_receiver_names.py +8 -6
- spectre_core/receivers/plugins/_rsp1a.py +44 -40
- spectre_core/receivers/plugins/_rspduo.py +59 -44
- spectre_core/receivers/plugins/_sdrplay_receiver.py +67 -83
- spectre_core/receivers/plugins/_test.py +136 -129
- spectre_core/receivers/plugins/_usrp.py +93 -85
- spectre_core/receivers/plugins/gr/__init__.py +1 -1
- spectre_core/receivers/plugins/gr/_base.py +14 -22
- spectre_core/receivers/plugins/gr/_rsp1a.py +53 -60
- spectre_core/receivers/plugins/gr/_rspduo.py +77 -89
- spectre_core/receivers/plugins/gr/_test.py +49 -57
- spectre_core/receivers/plugins/gr/_usrp.py +61 -59
- spectre_core/spectrograms/__init__.py +21 -13
- spectre_core/spectrograms/_analytical.py +108 -99
- spectre_core/spectrograms/_array_operations.py +39 -46
- spectre_core/spectrograms/_spectrogram.py +293 -324
- spectre_core/spectrograms/_transform.py +106 -73
- spectre_core/wgetting/__init__.py +1 -3
- spectre_core/wgetting/_callisto.py +87 -93
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.24.dist-info}/METADATA +9 -23
- spectre_core-0.0.24.dist-info/RECORD +79 -0
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.24.dist-info}/WHEEL +1 -1
- spectre_core-0.0.22.dist-info/RECORD +0 -78
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.24.dist-info}/licenses/LICENSE +0 -0
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.24.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@
|
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
5
|
from logging import getLogger
|
6
|
+
|
6
7
|
_LOGGER = getLogger(__name__)
|
7
8
|
|
8
9
|
from typing import Optional, cast
|
@@ -15,34 +16,26 @@ from spectre_core.capture_configs import CaptureConfig, PName
|
|
15
16
|
from spectre_core.spectrograms import Spectrogram, join_spectrograms
|
16
17
|
|
17
18
|
|
18
|
-
def make_sft_instance(
|
19
|
-
capture_config: CaptureConfig
|
20
|
-
) -> ShortTimeFFT:
|
19
|
+
def make_sft_instance(capture_config: CaptureConfig) -> ShortTimeFFT:
|
21
20
|
"""Extract window parameters from the input capture config and create an instance
|
22
21
|
of `ShortTimeFFT` from `scipy.signal`.
|
23
22
|
|
24
23
|
:param capture_config: The capture config storing the parameters.
|
25
|
-
:return: An instance of `ShortTimeFFT` consistent with the window parameters
|
24
|
+
:return: An instance of `ShortTimeFFT` consistent with the window parameters
|
26
25
|
in the capture config.
|
27
26
|
"""
|
28
|
-
window_type
|
29
|
-
sample_rate
|
30
|
-
window_hop
|
31
|
-
window_size
|
32
|
-
window = get_window(window_type,
|
33
|
-
|
34
|
-
return ShortTimeFFT(window,
|
35
|
-
window_hop,
|
36
|
-
sample_rate,
|
37
|
-
fft_mode = "centered")
|
27
|
+
window_type = cast(str, capture_config.get_parameter_value(PName.WINDOW_TYPE))
|
28
|
+
sample_rate = cast(int, capture_config.get_parameter_value(PName.SAMPLE_RATE))
|
29
|
+
window_hop = cast(int, capture_config.get_parameter_value(PName.WINDOW_HOP))
|
30
|
+
window_size = cast(int, capture_config.get_parameter_value(PName.WINDOW_SIZE))
|
31
|
+
window = get_window(window_type, window_size)
|
32
|
+
return ShortTimeFFT(window, window_hop, sample_rate, fft_mode="centered")
|
38
33
|
|
39
34
|
|
40
35
|
class BaseEventHandler(ABC, FileSystemEventHandler):
|
41
36
|
"""An abstract base class for event-driven file post-processing."""
|
42
|
-
|
43
|
-
|
44
|
-
tag: str
|
45
|
-
) -> None:
|
37
|
+
|
38
|
+
def __init__(self, tag: str) -> None:
|
46
39
|
"""Initialise a `BaseEventHandler` instance.
|
47
40
|
|
48
41
|
:param tag: The tag of the capture config used to capture the data.
|
@@ -51,7 +44,7 @@ class BaseEventHandler(ABC, FileSystemEventHandler):
|
|
51
44
|
|
52
45
|
# load the capture config corresponding to the input tag
|
53
46
|
self._capture_config = CaptureConfig(tag)
|
54
|
-
|
47
|
+
|
55
48
|
# store the next file to be processed (specifically, the absolute file path of the file)
|
56
49
|
self._queued_file: Optional[str] = None
|
57
50
|
|
@@ -59,25 +52,17 @@ class BaseEventHandler(ABC, FileSystemEventHandler):
|
|
59
52
|
# this can be flushed periodically to file as required.
|
60
53
|
self._cached_spectrogram: Optional[Spectrogram] = None
|
61
54
|
|
62
|
-
|
63
55
|
@abstractmethod
|
64
|
-
def process(
|
65
|
-
self,
|
66
|
-
absolute_file_path: str
|
67
|
-
) -> None:
|
56
|
+
def process(self, absolute_file_path: str) -> None:
|
68
57
|
"""
|
69
58
|
Process a batch file at the given file path.
|
70
59
|
|
71
60
|
:param absolute_file_path: The absolute path to the batch file to be processed.
|
72
61
|
"""
|
73
62
|
|
74
|
-
|
75
|
-
def on_created(
|
76
|
-
self,
|
77
|
-
event: FileSystemEvent
|
78
|
-
) -> None:
|
63
|
+
def on_created(self, event: FileSystemEvent) -> None:
|
79
64
|
"""Process a newly created batch file, only once the next batch is created.
|
80
|
-
|
65
|
+
|
81
66
|
Since we assume that the batches are non-overlapping in time, this guarantees
|
82
67
|
we avoid post processing a file while it is being written to. Files are processed
|
83
68
|
sequentially, in the order they are created.
|
@@ -86,49 +71,49 @@ class BaseEventHandler(ABC, FileSystemEventHandler):
|
|
86
71
|
"""
|
87
72
|
# The `src_path`` attribute holds the absolute path of the freshly closed file
|
88
73
|
absolute_file_path = event.src_path
|
89
|
-
|
74
|
+
|
90
75
|
# Only process a file if:
|
91
76
|
#
|
92
77
|
# - It's extension matches the `watch_extension` as defined in the capture config.
|
93
78
|
# - It's tag matches the current sessions tag.
|
94
79
|
#
|
95
|
-
# This is important for two reasons.
|
96
|
-
#
|
97
|
-
# In the case of one session, the capture worker may write to two batch files simultaneously
|
98
|
-
# (e.g., raw data file + seperate metadata file). We want to process them together - but this method will get called
|
80
|
+
# This is important for two reasons.
|
81
|
+
#
|
82
|
+
# In the case of one session, the capture worker may write to two batch files simultaneously
|
83
|
+
# (e.g., raw data file + seperate metadata file). We want to process them together - but this method will get called
|
99
84
|
# seperately for both file creation events. So, we filter by extension to account for this.
|
100
85
|
#
|
101
86
|
# Additionally in the case of multiple sessions, the capture workers will create batch files in the same directory concurrently.
|
102
87
|
# This method is triggered for all file creation events, so we ensure the batch file tag matches the session tag and early return
|
103
88
|
# otherwise. This way, each post processor worker picks up the right files to process.
|
104
|
-
watch_extension = cast(
|
89
|
+
watch_extension = cast(
|
90
|
+
str, self._capture_config.get_parameter_value(PName.WATCH_EXTENSION)
|
91
|
+
)
|
105
92
|
if not absolute_file_path.endswith(f"{self._tag}.{watch_extension}"):
|
106
93
|
return
|
107
|
-
|
94
|
+
|
108
95
|
_LOGGER.info(f"Noticed {absolute_file_path}")
|
109
96
|
# If there exists a queued file, try and process it
|
110
97
|
if self._queued_file is not None:
|
111
98
|
try:
|
112
99
|
self.process(self._queued_file)
|
113
100
|
except Exception:
|
114
|
-
_LOGGER.error(
|
115
|
-
|
101
|
+
_LOGGER.error(
|
102
|
+
f"An error has occured while processing {self._queued_file}",
|
103
|
+
exc_info=True,
|
104
|
+
)
|
116
105
|
# Flush any internally stored spectrogram on error to avoid lost data
|
117
106
|
self._flush_cache()
|
118
107
|
# re-raise the exception to the main thread
|
119
108
|
raise
|
120
|
-
|
109
|
+
|
121
110
|
# Queue the current file for processing next
|
122
111
|
_LOGGER.info(f"Queueing {absolute_file_path} for post processing")
|
123
112
|
self._queued_file = absolute_file_path
|
124
113
|
|
125
|
-
|
126
|
-
def _cache_spectrogram(
|
127
|
-
self,
|
128
|
-
spectrogram: Spectrogram
|
129
|
-
) -> None:
|
114
|
+
def _cache_spectrogram(self, spectrogram: Spectrogram) -> None:
|
130
115
|
"""Cache the input spectrogram by storing it in the `_cached_spectrogram` attribute.
|
131
|
-
|
116
|
+
|
132
117
|
If the time range of the cached spectrogram exceeds that as specified in the capture config
|
133
118
|
`PName.TIME_RANGE` parameter, the spectrogram in the cache is flushed to file. If `PName.TIME_RANGE`
|
134
119
|
is nulled, the cache is flushed immediately.
|
@@ -140,20 +125,21 @@ class BaseEventHandler(ABC, FileSystemEventHandler):
|
|
140
125
|
if self._cached_spectrogram is None:
|
141
126
|
self._cached_spectrogram = spectrogram
|
142
127
|
else:
|
143
|
-
self._cached_spectrogram = join_spectrograms(
|
144
|
-
|
128
|
+
self._cached_spectrogram = join_spectrograms(
|
129
|
+
[self._cached_spectrogram, spectrogram]
|
130
|
+
)
|
131
|
+
|
145
132
|
time_range = self._capture_config.get_parameter_value(PName.TIME_RANGE) or 0.0
|
146
133
|
if self._cached_spectrogram.time_range >= cast(float, time_range):
|
147
134
|
self._flush_cache()
|
148
|
-
|
149
135
|
|
150
|
-
def _flush_cache(
|
151
|
-
self
|
152
|
-
) -> None:
|
136
|
+
def _flush_cache(self) -> None:
|
153
137
|
"""Flush the cached spectrogram to file."""
|
154
138
|
if self._cached_spectrogram:
|
155
|
-
_LOGGER.info(
|
156
|
-
|
139
|
+
_LOGGER.info(
|
140
|
+
f"Flushing spectrogram to file with start time "
|
141
|
+
f"'{self._cached_spectrogram.format_start_time()}'"
|
142
|
+
)
|
157
143
|
self._cached_spectrogram.save()
|
158
144
|
_LOGGER.info("Flush successful, resetting spectrogram cache")
|
159
|
-
self._cached_spectrogram = None
|
145
|
+
self._cached_spectrogram = None # reset the cache
|
@@ -10,8 +10,9 @@ from spectre_core.exceptions import EventHandlerNotFoundError
|
|
10
10
|
from .plugins._event_handler_keys import EventHandlerKey
|
11
11
|
from ._register import event_handler_map
|
12
12
|
|
13
|
+
|
13
14
|
def _get_event_handler_cls_from_key(
|
14
|
-
event_handler_key: EventHandlerKey
|
15
|
+
event_handler_key: EventHandlerKey,
|
15
16
|
) -> Type[BaseEventHandler]:
|
16
17
|
"""Get a registered `BaseEventHandler` subclass.
|
17
18
|
|
@@ -22,14 +23,16 @@ def _get_event_handler_cls_from_key(
|
|
22
23
|
event_handler_cls = event_handler_map.get(event_handler_key)
|
23
24
|
if event_handler_cls is None:
|
24
25
|
event_handler_keys = list(event_handler_map.keys())
|
25
|
-
raise EventHandlerNotFoundError(
|
26
|
-
|
26
|
+
raise EventHandlerNotFoundError(
|
27
|
+
(
|
28
|
+
f"No event handler found for the capture mode '{event_handler_key}'. "
|
29
|
+
f"Please specify one of the following capture modes: {event_handler_keys}"
|
30
|
+
)
|
31
|
+
)
|
27
32
|
return event_handler_cls
|
28
33
|
|
29
34
|
|
30
|
-
def get_event_handler_cls_from_tag(
|
31
|
-
tag: str
|
32
|
-
) -> Type[BaseEventHandler]:
|
35
|
+
def get_event_handler_cls_from_tag(tag: str) -> Type[BaseEventHandler]:
|
33
36
|
"""
|
34
37
|
Retrieve the event handler class, using the event handler key stored in a capture config.
|
35
38
|
|
@@ -37,13 +40,13 @@ def get_event_handler_cls_from_tag(
|
|
37
40
|
:return: The event handler class specified in the capture config.
|
38
41
|
"""
|
39
42
|
capture_config = CaptureConfig(tag)
|
40
|
-
event_handler_key = cast(
|
41
|
-
|
43
|
+
event_handler_key = cast(
|
44
|
+
str, capture_config.get_parameter_value(PName.EVENT_HANDLER_KEY)
|
45
|
+
)
|
46
|
+
return _get_event_handler_cls_from_key(EventHandlerKey(event_handler_key))
|
42
47
|
|
43
48
|
|
44
|
-
def get_event_handler(
|
45
|
-
tag: str
|
46
|
-
) -> BaseEventHandler:
|
49
|
+
def get_event_handler(tag: str) -> BaseEventHandler:
|
47
50
|
"""Create an event handler class instance, using the event handler key stored in a capture config.
|
48
51
|
|
49
52
|
:param tag: The capture config tag.
|
@@ -3,6 +3,7 @@
|
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
5
|
from logging import getLogger
|
6
|
+
|
6
7
|
_LOGGER = getLogger(__name__)
|
7
8
|
|
8
9
|
from watchdog.observers import Observer
|
@@ -12,27 +13,30 @@ from ._factory import get_event_handler
|
|
12
13
|
from spectre_core.config import get_batches_dir_path
|
13
14
|
|
14
15
|
|
15
|
-
def start_post_processor(
|
16
|
-
tag: str
|
17
|
-
) -> None:
|
16
|
+
def start_post_processor(tag: str) -> None:
|
18
17
|
"""Start a thread to process newly created files in the `batches` directory.
|
19
18
|
|
20
19
|
:param tag: The tag of the capture config used for data capture.
|
21
20
|
"""
|
22
21
|
post_processor = Observer()
|
23
22
|
event_handler = get_event_handler(tag)
|
24
|
-
post_processor.schedule(
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
23
|
+
post_processor.schedule(
|
24
|
+
event_handler,
|
25
|
+
get_batches_dir_path(),
|
26
|
+
recursive=True,
|
27
|
+
event_filter=[FileCreatedEvent],
|
28
|
+
)
|
29
|
+
|
29
30
|
try:
|
30
|
-
_LOGGER.info("Starting the post processing thread...")
|
31
|
+
_LOGGER.info("Starting the post processing thread...")
|
31
32
|
post_processor.start()
|
32
33
|
post_processor.join()
|
33
34
|
except KeyboardInterrupt:
|
34
|
-
_LOGGER.warning(
|
35
|
-
|
35
|
+
_LOGGER.warning(
|
36
|
+
(
|
37
|
+
"Keyboard interrupt detected. Signalling "
|
38
|
+
"the post processing thread to stop"
|
39
|
+
)
|
40
|
+
)
|
36
41
|
post_processor.stop()
|
37
42
|
_LOGGER.warning(("Post processing thread has been successfully stopped"))
|
38
|
-
|
@@ -10,9 +10,11 @@ from ._base import BaseEventHandler
|
|
10
10
|
# Map populated at runtime via the `register_event_handler` decorator.
|
11
11
|
event_handler_map: dict[EventHandlerKey, Type[BaseEventHandler]] = {}
|
12
12
|
|
13
|
-
T = TypeVar(
|
13
|
+
T = TypeVar("T", bound=BaseEventHandler)
|
14
|
+
|
15
|
+
|
14
16
|
def register_event_handler(
|
15
|
-
event_handler_key: EventHandlerKey
|
17
|
+
event_handler_key: EventHandlerKey,
|
16
18
|
) -> Callable[[Type[T]], Type[T]]:
|
17
19
|
"""Decorator to register a `BaseEventHandler` subclass under a specified `EventHandlerKey`.
|
18
20
|
|
@@ -20,12 +22,13 @@ def register_event_handler(
|
|
20
22
|
:raises ValueError: If the provided `event_handler_key` is already registered.
|
21
23
|
:return: A decorator that registers the `BaseBatch` subclass under the given `batch_key`.
|
22
24
|
"""
|
23
|
-
|
24
|
-
|
25
|
-
) -> Type[T]:
|
25
|
+
|
26
|
+
def decorator(cls: Type[T]) -> Type[T]:
|
26
27
|
if event_handler_key in event_handler_map:
|
27
|
-
raise ValueError(
|
28
|
+
raise ValueError(
|
29
|
+
f"An event handler with key '{event_handler_key}' is already registered!"
|
30
|
+
)
|
28
31
|
event_handler_map[event_handler_key] = cls
|
29
32
|
return cls
|
30
|
-
return decorator
|
31
33
|
|
34
|
+
return decorator
|
@@ -4,13 +4,14 @@
|
|
4
4
|
|
5
5
|
from enum import Enum
|
6
6
|
|
7
|
-
|
7
|
+
|
8
|
+
class EventHandlerKey(Enum):
|
8
9
|
"""Key bound to a `BaseEventHandler` plugin class.
|
9
|
-
|
10
|
+
|
10
11
|
:ivar FIXED_CENTER_FREQUENCY: Postprocess data capture at a fixed center frequency.
|
11
12
|
:ivar SWEPT_CENTER_FREQUENCY: Postprocess data capture where the center frequency is continually sweeping
|
12
13
|
in fixed increments.
|
13
14
|
"""
|
15
|
+
|
14
16
|
FIXED_CENTER_FREQUENCY = "fixed_center_frequency"
|
15
17
|
SWEPT_CENTER_FREQUENCY = "swept_center_frequency"
|
16
|
-
|
@@ -3,6 +3,7 @@
|
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
5
|
from logging import getLogger
|
6
|
+
|
6
7
|
_LOGGER = getLogger(__name__)
|
7
8
|
|
8
9
|
import numpy as np
|
@@ -15,7 +16,12 @@ import numpy as np
|
|
15
16
|
|
16
17
|
from spectre_core.capture_configs import CaptureConfig, PName
|
17
18
|
from spectre_core.batches import IQStreamBatch
|
18
|
-
from spectre_core.spectrograms import
|
19
|
+
from spectre_core.spectrograms import (
|
20
|
+
Spectrogram,
|
21
|
+
SpectrumUnit,
|
22
|
+
time_average,
|
23
|
+
frequency_average,
|
24
|
+
)
|
19
25
|
from ._event_handler_keys import EventHandlerKey
|
20
26
|
from .._base import BaseEventHandler, make_sft_instance
|
21
27
|
from .._register import register_event_handler
|
@@ -26,11 +32,11 @@ def _do_stfft(
|
|
26
32
|
capture_config: CaptureConfig,
|
27
33
|
) -> Tuple[npt.NDArray[np.float32], npt.NDArray[np.float32], npt.NDArray[np.float32]]:
|
28
34
|
"""Do a Short-time Fast Fourier Transform on an array of complex IQ samples.
|
29
|
-
|
30
|
-
The computation requires extra metadata, which is extracted from the detached header in the batch
|
35
|
+
|
36
|
+
The computation requires extra metadata, which is extracted from the detached header in the batch
|
31
37
|
and the capture config used to capture the data.
|
32
|
-
|
33
|
-
The current implementation relies heavily on the `ShortTimeFFT` implementation from
|
38
|
+
|
39
|
+
The current implementation relies heavily on the `ShortTimeFFT` implementation from
|
34
40
|
`scipy.signal` (https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.ShortTimeFFT.html)
|
35
41
|
which takes up a lot of the compute time.
|
36
42
|
"""
|
@@ -38,64 +44,64 @@ def _do_stfft(
|
|
38
44
|
sft = make_sft_instance(capture_config)
|
39
45
|
|
40
46
|
# set p0=0, since by convention in the STFFT docs, p=0 corresponds to the slice centred at t=0
|
41
|
-
p0=0
|
47
|
+
p0 = 0
|
42
48
|
|
43
49
|
# set p1 to the index of the first slice where the "midpoint" of the window is still inside the signal
|
44
50
|
num_samples = len(iq_data)
|
45
51
|
p1 = sft.upper_border_begin(num_samples)[1]
|
46
|
-
|
52
|
+
|
47
53
|
# compute a ShortTimeFFT on the IQ samples
|
48
|
-
complex_spectra = sft.stft(iq_data,
|
49
|
-
|
50
|
-
p1 = p1)
|
51
|
-
|
54
|
+
complex_spectra = sft.stft(iq_data, p0=p0, p1=p1)
|
55
|
+
|
52
56
|
# compute the magnitude of each spectral component
|
53
57
|
dynamic_spectra = np.abs(complex_spectra)
|
54
58
|
|
55
|
-
|
56
59
|
# assign a physical time to each spectrum
|
57
60
|
# p0 is defined to correspond with the first sample, at t=0 [s]
|
58
|
-
times = sft.t(num_samples,
|
59
|
-
p0 = p0,
|
60
|
-
p1 = p1)
|
61
|
+
times = sft.t(num_samples, p0=p0, p1=p1)
|
61
62
|
# assign physical frequencies to each spectral component
|
62
|
-
frequencies = sft.f + cast(
|
63
|
+
frequencies = sft.f + cast(
|
64
|
+
float, capture_config.get_parameter_value(PName.CENTER_FREQUENCY)
|
65
|
+
)
|
63
66
|
|
64
|
-
return
|
67
|
+
return (
|
68
|
+
times.astype(np.float32),
|
69
|
+
frequencies.astype(np.float32),
|
70
|
+
dynamic_spectra.astype(np.float32),
|
71
|
+
)
|
65
72
|
|
66
73
|
|
67
74
|
def _build_spectrogram(
|
68
|
-
batch: IQStreamBatch,
|
69
|
-
capture_config: CaptureConfig
|
75
|
+
batch: IQStreamBatch, capture_config: CaptureConfig
|
70
76
|
) -> Spectrogram:
|
71
77
|
"""Generate a spectrogram using `IQStreamBatch` IQ samples."""
|
72
78
|
# read the data from the batch
|
73
79
|
iq_metadata = batch.hdr_file.read()
|
74
|
-
iq_samples
|
80
|
+
iq_samples = batch.bin_file.read()
|
75
81
|
|
76
|
-
times, frequencies, dynamic_spectra = _do_stfft(iq_samples,
|
77
|
-
capture_config)
|
82
|
+
times, frequencies, dynamic_spectra = _do_stfft(iq_samples, capture_config)
|
78
83
|
|
79
84
|
# compute the start datetime for the spectrogram by adding the millisecond component to the batch start time
|
80
|
-
spectrogram_start_datetime = batch.start_datetime + timedelta(
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
85
|
+
spectrogram_start_datetime = batch.start_datetime + timedelta(
|
86
|
+
milliseconds=iq_metadata.millisecond_correction
|
87
|
+
)
|
88
|
+
|
89
|
+
return Spectrogram(
|
90
|
+
dynamic_spectra,
|
91
|
+
times,
|
92
|
+
frequencies,
|
93
|
+
batch.tag,
|
94
|
+
SpectrumUnit.AMPLITUDE,
|
95
|
+
spectrogram_start_datetime,
|
96
|
+
)
|
88
97
|
|
89
98
|
|
90
99
|
@register_event_handler(EventHandlerKey.FIXED_CENTER_FREQUENCY)
|
91
100
|
class FixedEventHandler(BaseEventHandler):
|
92
|
-
def process(
|
93
|
-
self,
|
94
|
-
absolute_file_path: str
|
95
|
-
) -> None:
|
101
|
+
def process(self, absolute_file_path: str) -> None:
|
96
102
|
"""Compute a spectrogram using `IQStreamBatch` IQ samples, cache it, then save it to file in the FITS
|
97
103
|
format. The IQ samples are assumed to have been collected at a fixed center frequency.
|
98
|
-
|
104
|
+
|
99
105
|
The computed spectrogram is averaged in time and frequency as per the user-configured capture config.
|
100
106
|
Once the spectrogram has been computed successfully, the `.bin` and `.hdr` files are removed.
|
101
107
|
|
@@ -104,22 +110,23 @@ class FixedEventHandler(BaseEventHandler):
|
|
104
110
|
_LOGGER.info(f"Processing: {absolute_file_path}")
|
105
111
|
file_name = os.path.basename(absolute_file_path)
|
106
112
|
base_file_name, _ = os.path.splitext(file_name)
|
107
|
-
batch_start_time, tag = base_file_name.split(
|
113
|
+
batch_start_time, tag = base_file_name.split("_")
|
108
114
|
|
109
115
|
batch = IQStreamBatch(batch_start_time, tag)
|
110
116
|
|
111
117
|
_LOGGER.info("Creating spectrogram")
|
112
|
-
spectrogram = _build_spectrogram(batch,
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
frequency_resolution = cast(
|
120
|
-
|
121
|
-
|
122
|
-
|
118
|
+
spectrogram = _build_spectrogram(batch, self._capture_config)
|
119
|
+
|
120
|
+
time_resolution = cast(
|
121
|
+
float, self._capture_config.get_parameter_value(PName.TIME_RESOLUTION)
|
122
|
+
)
|
123
|
+
spectrogram = time_average(spectrogram, resolution=time_resolution)
|
124
|
+
|
125
|
+
frequency_resolution = cast(
|
126
|
+
float, self._capture_config.get_parameter_value(PName.FREQUENCY_RESOLUTION)
|
127
|
+
)
|
128
|
+
spectrogram = frequency_average(spectrogram, resolution=frequency_resolution)
|
129
|
+
|
123
130
|
self._cache_spectrogram(spectrogram)
|
124
131
|
|
125
132
|
_LOGGER.info(f"Deleting {batch.bin_file.file_path}")
|