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
@@ -5,23 +5,30 @@
|
|
5
5
|
from logging import getLogger
|
6
6
|
_LOGGER = getLogger(__name__)
|
7
7
|
|
8
|
-
from typing import Optional
|
8
|
+
from typing import Optional, cast
|
9
9
|
from abc import ABC, abstractmethod
|
10
10
|
from scipy.signal import ShortTimeFFT, get_window
|
11
11
|
|
12
|
-
from watchdog.events import FileSystemEventHandler,
|
12
|
+
from watchdog.events import FileSystemEventHandler, FileSystemEvent
|
13
13
|
|
14
|
-
from spectre_core.capture_configs import CaptureConfig,
|
15
|
-
from spectre_core.batches import BaseBatch, get_batch_cls_from_tag
|
14
|
+
from spectre_core.capture_configs import CaptureConfig, PName
|
16
15
|
from spectre_core.spectrograms import Spectrogram, join_spectrograms
|
17
16
|
|
18
17
|
|
19
|
-
def make_sft_instance(
|
18
|
+
def make_sft_instance(
|
19
|
+
capture_config: CaptureConfig
|
20
20
|
) -> ShortTimeFFT:
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
"""Extract window parameters from the input capture config and create an instance
|
22
|
+
of `ShortTimeFFT` from `scipy.signal`.
|
23
|
+
|
24
|
+
:param capture_config: The capture config storing the parameters.
|
25
|
+
:return: An instance of `ShortTimeFFT` consistent with the window parameters
|
26
|
+
in the capture config.
|
27
|
+
"""
|
28
|
+
window_type = cast(str, capture_config.get_parameter_value(PName.WINDOW_TYPE))
|
29
|
+
sample_rate = cast(int, capture_config.get_parameter_value(PName.SAMPLE_RATE))
|
30
|
+
window_hop = cast(int, capture_config.get_parameter_value(PName.WINDOW_HOP))
|
31
|
+
window_size = cast(int, capture_config.get_parameter_value(PName.WINDOW_SIZE))
|
25
32
|
window = get_window(window_type,
|
26
33
|
window_size)
|
27
34
|
return ShortTimeFFT(window,
|
@@ -31,52 +38,60 @@ def make_sft_instance(capture_config: CaptureConfig
|
|
31
38
|
|
32
39
|
|
33
40
|
class BaseEventHandler(ABC, FileSystemEventHandler):
|
34
|
-
|
35
|
-
|
41
|
+
"""An abstract base class for event-driven file post-processing."""
|
42
|
+
def __init__(
|
43
|
+
self,
|
44
|
+
tag: str
|
45
|
+
) -> None:
|
46
|
+
"""Initialise a `BaseEventHandler` instance.
|
47
|
+
|
48
|
+
:param tag: The tag of the capture config used to capture the data.
|
49
|
+
"""
|
36
50
|
self._tag = tag
|
37
51
|
|
38
|
-
# the
|
39
|
-
self.
|
40
|
-
|
41
|
-
self._capture_config = CaptureConfig(tag)
|
42
|
-
|
43
|
-
# post processing is triggered by files with this extension
|
44
|
-
self._watch_extension = self._capture_config.get_parameter_value(PNames.WATCH_EXTENSION)
|
45
|
-
|
52
|
+
# load the capture config corresponding to the input tag
|
53
|
+
self._capture_config = CaptureConfig(tag)
|
54
|
+
|
46
55
|
# store the next file to be processed (specifically, the absolute file path of the file)
|
47
56
|
self._queued_file: Optional[str] = None
|
48
57
|
|
49
|
-
# store batched spectrograms as they are created into a cache
|
50
|
-
#
|
51
|
-
# time range
|
58
|
+
# optionally store batched spectrograms as they are created into a cache
|
59
|
+
# this can be flushed periodically to file as required.
|
52
60
|
self._cached_spectrogram: Optional[Spectrogram] = None
|
53
61
|
|
54
62
|
|
55
63
|
@abstractmethod
|
56
|
-
def process(
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
To be implemented by derived classes.
|
64
|
+
def process(
|
65
|
+
self,
|
66
|
+
absolute_file_path: str
|
67
|
+
) -> None:
|
61
68
|
"""
|
69
|
+
Process a batch file at the given file path.
|
62
70
|
|
63
|
-
|
64
|
-
|
71
|
+
:param absolute_file_path: The absolute path to the batch file to be processed.
|
72
|
+
"""
|
73
|
+
|
74
|
+
|
75
|
+
def on_created(
|
76
|
+
self,
|
77
|
+
event: FileSystemEvent
|
78
|
+
) -> None:
|
65
79
|
"""Process a newly created batch file, only once the next batch is created.
|
66
80
|
|
67
81
|
Since we assume that the batches are non-overlapping in time, this guarantees
|
68
82
|
we avoid post processing a file while it is being written to. Files are processed
|
69
83
|
sequentially, in the order they are created.
|
70
|
-
"""
|
71
84
|
|
72
|
-
|
85
|
+
:param event: The file system event containing the file details.
|
86
|
+
"""
|
87
|
+
# the `src_path`` attribute holds the absolute path of the freshly closed file
|
73
88
|
absolute_file_path = event.src_path
|
74
89
|
|
75
|
-
# only 'notice' a file if it ends with the appropriate extension
|
76
|
-
|
77
|
-
|
90
|
+
# only 'notice' a file if it ends with the appropriate extension as defined in the capture config
|
91
|
+
watch_extension = cast(str, self._capture_config.get_parameter_value(PName.WATCH_EXTENSION))
|
92
|
+
|
93
|
+
if absolute_file_path.endswith( watch_extension ):
|
78
94
|
_LOGGER.info(f"Noticed {absolute_file_path}")
|
79
|
-
|
80
95
|
# If there exists a queued file, try and process it
|
81
96
|
if self._queued_file is not None:
|
82
97
|
try:
|
@@ -92,28 +107,39 @@ class BaseEventHandler(ABC, FileSystemEventHandler):
|
|
92
107
|
# Queue the current file for processing next
|
93
108
|
_LOGGER.info(f"Queueing {absolute_file_path} for post processing")
|
94
109
|
self._queued_file = absolute_file_path
|
95
|
-
|
96
110
|
|
97
|
-
|
98
|
-
|
111
|
+
|
112
|
+
def _cache_spectrogram(
|
113
|
+
self,
|
114
|
+
spectrogram: Spectrogram
|
115
|
+
) -> None:
|
116
|
+
"""Cache the input spectrogram by storing it in the `_cached_spectrogram` attribute.
|
117
|
+
|
118
|
+
If the time range of the cached spectrogram exceeds that as specified in the capture config
|
119
|
+
`PName.TIME_RANGE` parameter, the spectrogram in the cache is flushed to file. If `PName.TIME_RANGE`
|
120
|
+
is nulled, the cache is flushed immediately.
|
121
|
+
|
122
|
+
:param spectrogram: The spectrogram to store in the cache.
|
123
|
+
"""
|
99
124
|
_LOGGER.info("Joining spectrogram")
|
100
125
|
|
101
126
|
if self._cached_spectrogram is None:
|
102
127
|
self._cached_spectrogram = spectrogram
|
103
128
|
else:
|
104
129
|
self._cached_spectrogram = join_spectrograms([self._cached_spectrogram, spectrogram])
|
105
|
-
|
106
|
-
# if the time range is not specified
|
107
|
-
time_range = self._capture_config.get_parameter_value(PNames.TIME_RANGE) or 0.0
|
108
130
|
|
109
|
-
|
131
|
+
time_range = self._capture_config.get_parameter_value(PName.TIME_RANGE) or 0.0
|
132
|
+
if self._cached_spectrogram.time_range >= cast(float, time_range):
|
110
133
|
self._flush_cache()
|
111
134
|
|
112
135
|
|
113
|
-
def _flush_cache(
|
136
|
+
def _flush_cache(
|
137
|
+
self
|
138
|
+
) -> None:
|
139
|
+
"""Flush the cached spectrogram to file."""
|
114
140
|
if self._cached_spectrogram:
|
115
141
|
_LOGGER.info(f"Flushing spectrogram to file with start time "
|
116
|
-
f"'{self._cached_spectrogram.format_start_time(
|
142
|
+
f"'{self._cached_spectrogram.format_start_time()}'")
|
117
143
|
self._cached_spectrogram.save()
|
118
144
|
_LOGGER.info("Flush successful, resetting spectrogram cache")
|
119
145
|
self._cached_spectrogram = None # reset the cache
|
@@ -2,22 +2,52 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
-
from
|
5
|
+
from typing import Type, cast
|
6
|
+
|
6
7
|
from spectre_core.post_processing._base import BaseEventHandler
|
7
|
-
from spectre_core.capture_configs import CaptureConfig,
|
8
|
+
from spectre_core.capture_configs import CaptureConfig, PName
|
8
9
|
from spectre_core.exceptions import EventHandlerNotFoundError
|
10
|
+
from .plugins._event_handler_keys import EventHandlerKey
|
11
|
+
from ._register import event_handler_map
|
12
|
+
|
13
|
+
def _get_event_handler_cls_from_key(
|
14
|
+
event_handler_key: EventHandlerKey
|
15
|
+
) -> Type[BaseEventHandler]:
|
16
|
+
"""Get a registered `BaseEventHandler` subclass.
|
9
17
|
|
18
|
+
:param event_handler_key: The key used to register the `BaseEventHandler` subclass.
|
19
|
+
:raises EventHandlerNotFoundError: If an invalid `event_handler_key` is provided.
|
20
|
+
:return: The `BaseEventHandler` subclass registered under `event_handler_key`.
|
21
|
+
"""
|
22
|
+
event_handler_cls = event_handler_map.get(event_handler_key)
|
23
|
+
if event_handler_cls is None:
|
24
|
+
event_handler_keys = list(event_handler_map.keys())
|
25
|
+
raise EventHandlerNotFoundError((f"No event handler found for the capture mode '{event_handler_key}'. "
|
26
|
+
f"Please specify one of the following capture modes: {event_handler_keys}"))
|
27
|
+
return event_handler_cls
|
10
28
|
|
11
|
-
def _get_event_handler_cls(event_handler_key: str) -> BaseEventHandler:
|
12
|
-
EventHandler = event_handler_map.get(event_handler_key)
|
13
|
-
if EventHandler is None:
|
14
|
-
valid_event_handler_keys = list(event_handler_map.keys())
|
15
|
-
raise EventHandlerNotFoundError((f"No event handler found for the event handler key '{event_handler_key}'. "
|
16
|
-
f"Please specify one of the following event handler keys: {valid_event_handler_keys}"))
|
17
|
-
return EventHandler
|
18
29
|
|
30
|
+
def get_event_handler_cls_from_tag(
|
31
|
+
tag: str
|
32
|
+
) -> Type[BaseEventHandler]:
|
33
|
+
"""
|
34
|
+
Retrieve the event handler class, using the event handler key stored in a capture config.
|
19
35
|
|
20
|
-
|
36
|
+
:param tag: The capture config tag.
|
37
|
+
:return: The event handler class specified in the capture config.
|
38
|
+
"""
|
21
39
|
capture_config = CaptureConfig(tag)
|
22
|
-
event_handler_key = capture_config.get_parameter_value(
|
23
|
-
return
|
40
|
+
event_handler_key = cast(str, capture_config.get_parameter_value(PName.EVENT_HANDLER_KEY))
|
41
|
+
return _get_event_handler_cls_from_key( EventHandlerKey(event_handler_key) )
|
42
|
+
|
43
|
+
|
44
|
+
def get_event_handler(
|
45
|
+
tag: str
|
46
|
+
) -> BaseEventHandler:
|
47
|
+
"""Create an event handler class instance, using the event handler key stored in a capture config.
|
48
|
+
|
49
|
+
:param tag: The capture config tag.
|
50
|
+
:return: An instance of the event handler class.
|
51
|
+
"""
|
52
|
+
event_handler_cls = get_event_handler_cls_from_tag(tag)
|
53
|
+
return event_handler_cls(tag)
|
@@ -8,33 +8,31 @@ _LOGGER = getLogger(__name__)
|
|
8
8
|
from watchdog.observers import Observer
|
9
9
|
from watchdog.events import FileCreatedEvent
|
10
10
|
|
11
|
-
from ._factory import
|
11
|
+
from ._factory import get_event_handler
|
12
12
|
from spectre_core.config import get_batches_dir_path
|
13
13
|
|
14
|
-
class PostProcessor:
|
15
|
-
def __init__(self,
|
16
|
-
tag: str):
|
17
|
-
|
18
|
-
self._observer = Observer()
|
19
14
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
15
|
+
def start_post_processor(
|
16
|
+
tag: str
|
17
|
+
) -> None:
|
18
|
+
"""Start a thread to process newly created files in the `batches` directory.
|
19
|
+
|
20
|
+
:param tag: The tag of the capture config used for data capture.
|
21
|
+
"""
|
22
|
+
post_processor = Observer()
|
23
|
+
event_handler = get_event_handler(tag)
|
24
|
+
post_processor.schedule(event_handler,
|
25
|
+
get_batches_dir_path(),
|
26
|
+
recursive=True,
|
27
|
+
event_filter=[ FileCreatedEvent ])
|
28
|
+
|
29
|
+
try:
|
30
|
+
_LOGGER.info("Starting the post processing thread...")
|
31
|
+
post_processor.start()
|
32
|
+
post_processor.join()
|
33
|
+
except KeyboardInterrupt:
|
34
|
+
_LOGGER.warning(("Keyboard interrupt detected. Signalling "
|
35
|
+
"the post processing thread to stop"))
|
36
|
+
post_processor.stop()
|
37
|
+
_LOGGER.warning(("Post processing thread has been successfully stopped"))
|
40
38
|
|
@@ -2,13 +2,29 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
-
|
6
|
-
event_handler_map = {}
|
5
|
+
from typing import Type, Callable, TypeVar
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
from .plugins._event_handler_keys import EventHandlerKey
|
8
|
+
from ._base import BaseEventHandler
|
9
|
+
|
10
|
+
# Map populated at runtime via the `register_event_handler` decorator.
|
11
|
+
event_handler_map: dict[EventHandlerKey, Type[BaseEventHandler]] = {}
|
12
|
+
|
13
|
+
T = TypeVar('T', bound=BaseEventHandler)
|
14
|
+
def register_event_handler(
|
15
|
+
event_handler_key: EventHandlerKey
|
16
|
+
) -> Callable[[Type[T]], Type[T]]:
|
17
|
+
"""Decorator to register a `BaseEventHandler` subclass under a specified `EventHandlerKey`.
|
18
|
+
|
19
|
+
:param event_handler_key: The key to register the `BaseEventHandler` subclass.
|
20
|
+
:raises ValueError: If the provided `event_handler_key` is already registered.
|
21
|
+
:return: A decorator that registers the `BaseBatch` subclass under the given `batch_key`.
|
22
|
+
"""
|
23
|
+
def decorator(
|
24
|
+
cls: Type[T]
|
25
|
+
) -> Type[T]:
|
26
|
+
if event_handler_key in event_handler_map:
|
27
|
+
raise ValueError(f"An event handler with key '{event_handler_key}' is already registered!")
|
12
28
|
event_handler_map[event_handler_key] = cls
|
13
29
|
return cls
|
14
30
|
return decorator
|
@@ -0,0 +1,16 @@
|
|
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 enum import Enum
|
6
|
+
|
7
|
+
class EventHandlerKey(Enum):
|
8
|
+
"""Key bound to a `BaseEventHandler` plugin class.
|
9
|
+
|
10
|
+
:ivar FIXED_CENTER_FREQUENCY: Postprocess data capture at a fixed center frequency.
|
11
|
+
:ivar SWEPT_CENTER_FREQUENCY: Postprocess data capture where the center frequency is continually sweeping
|
12
|
+
in fixed increments.
|
13
|
+
"""
|
14
|
+
FIXED_CENTER_FREQUENCY = "fixed_center_frequency"
|
15
|
+
SWEPT_CENTER_FREQUENCY = "swept_center_frequency"
|
16
|
+
|
@@ -0,0 +1,129 @@
|
|
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, cast
|
10
|
+
from datetime import timedelta
|
11
|
+
|
12
|
+
import os
|
13
|
+
import numpy.typing as npt
|
14
|
+
import numpy as np
|
15
|
+
|
16
|
+
from spectre_core.capture_configs import CaptureConfig, PName
|
17
|
+
from spectre_core.batches import IQStreamBatch
|
18
|
+
from spectre_core.spectrograms import Spectrogram, SpectrumUnit, time_average, frequency_average
|
19
|
+
from ._event_handler_keys import EventHandlerKey
|
20
|
+
from .._base import BaseEventHandler, make_sft_instance
|
21
|
+
from .._register import register_event_handler
|
22
|
+
|
23
|
+
|
24
|
+
def _do_stfft(
|
25
|
+
iq_data: npt.NDArray[np.complex64],
|
26
|
+
capture_config: CaptureConfig,
|
27
|
+
) -> Tuple[npt.NDArray[np.float32], npt.NDArray[np.float32], npt.NDArray[np.float32]]:
|
28
|
+
"""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
|
31
|
+
and the capture config used to capture the data.
|
32
|
+
|
33
|
+
The current implementation relies heavily on the `ShortTimeFFT` implementation from
|
34
|
+
`scipy.signal` (https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.ShortTimeFFT.html)
|
35
|
+
which takes up a lot of the compute time.
|
36
|
+
"""
|
37
|
+
|
38
|
+
sft = make_sft_instance(capture_config)
|
39
|
+
|
40
|
+
# set p0=0, since by convention in the STFFT docs, p=0 corresponds to the slice centred at t=0
|
41
|
+
p0=0
|
42
|
+
|
43
|
+
# set p1 to the index of the first slice where the "midpoint" of the window is still inside the signal
|
44
|
+
num_samples = len(iq_data)
|
45
|
+
p1 = sft.upper_border_begin(num_samples)[1]
|
46
|
+
|
47
|
+
# compute a ShortTimeFFT on the IQ samples
|
48
|
+
complex_spectra = sft.stft(iq_data,
|
49
|
+
p0 = p0,
|
50
|
+
p1 = p1)
|
51
|
+
|
52
|
+
# compute the magnitude of each spectral component
|
53
|
+
dynamic_spectra = np.abs(complex_spectra)
|
54
|
+
|
55
|
+
|
56
|
+
# assign a physical time to each spectrum
|
57
|
+
# 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
|
+
# assign physical frequencies to each spectral component
|
62
|
+
frequencies = sft.f + cast(float, capture_config.get_parameter_value(PName.CENTER_FREQUENCY))
|
63
|
+
|
64
|
+
return times.astype(np.float32), frequencies.astype(np.float32), dynamic_spectra.astype(np.float32)
|
65
|
+
|
66
|
+
|
67
|
+
def _build_spectrogram(
|
68
|
+
batch: IQStreamBatch,
|
69
|
+
capture_config: CaptureConfig
|
70
|
+
) -> Spectrogram:
|
71
|
+
"""Generate a spectrogram using `IQStreamBatch` IQ samples."""
|
72
|
+
# read the data from the batch
|
73
|
+
iq_metadata = batch.hdr_file.read()
|
74
|
+
iq_samples = batch.bin_file.read()
|
75
|
+
|
76
|
+
times, frequencies, dynamic_spectra = _do_stfft(iq_samples,
|
77
|
+
capture_config)
|
78
|
+
|
79
|
+
# 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(milliseconds=iq_metadata.millisecond_correction)
|
81
|
+
|
82
|
+
return Spectrogram(dynamic_spectra,
|
83
|
+
times,
|
84
|
+
frequencies,
|
85
|
+
batch.tag,
|
86
|
+
SpectrumUnit.AMPLITUDE,
|
87
|
+
spectrogram_start_datetime)
|
88
|
+
|
89
|
+
|
90
|
+
@register_event_handler(EventHandlerKey.FIXED_CENTER_FREQUENCY)
|
91
|
+
class FixedEventHandler(BaseEventHandler):
|
92
|
+
def process(
|
93
|
+
self,
|
94
|
+
absolute_file_path: str
|
95
|
+
) -> None:
|
96
|
+
"""Compute a spectrogram using `IQStreamBatch` IQ samples, cache it, then save it to file in the FITS
|
97
|
+
format. The IQ samples are assumed to have been collected at a fixed center frequency.
|
98
|
+
|
99
|
+
The computed spectrogram is averaged in time and frequency as per the user-configured capture config.
|
100
|
+
Once the spectrogram has been computed successfully, the `.bin` and `.hdr` files are removed.
|
101
|
+
|
102
|
+
:param absolute_file_path: The absolute file path of the `.bin` file in the batch.
|
103
|
+
"""
|
104
|
+
_LOGGER.info(f"Processing: {absolute_file_path}")
|
105
|
+
file_name = os.path.basename(absolute_file_path)
|
106
|
+
base_file_name, _ = os.path.splitext(file_name)
|
107
|
+
batch_start_time, tag = base_file_name.split('_')
|
108
|
+
|
109
|
+
batch = IQStreamBatch(batch_start_time, tag)
|
110
|
+
|
111
|
+
_LOGGER.info("Creating spectrogram")
|
112
|
+
spectrogram = _build_spectrogram(batch,
|
113
|
+
self._capture_config)
|
114
|
+
|
115
|
+
time_resolution = cast(float, self._capture_config.get_parameter_value(PName.TIME_RESOLUTION))
|
116
|
+
spectrogram = time_average(spectrogram,
|
117
|
+
resolution=time_resolution)
|
118
|
+
|
119
|
+
frequency_resolution = cast(float, self._capture_config.get_parameter_value(PName.FREQUENCY_RESOLUTION))
|
120
|
+
spectrogram = frequency_average(spectrogram,
|
121
|
+
resolution=frequency_resolution)
|
122
|
+
|
123
|
+
self._cache_spectrogram(spectrogram)
|
124
|
+
|
125
|
+
_LOGGER.info(f"Deleting {batch.bin_file.file_path}")
|
126
|
+
batch.bin_file.delete()
|
127
|
+
|
128
|
+
_LOGGER.info(f"Deleting {batch.hdr_file.file_path}")
|
129
|
+
batch.hdr_file.delete()
|