spectre-core 0.0.21__py3-none-any.whl → 0.0.23__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 +5 -5
- spectre_core/_file_io/file_handlers.py +61 -107
- spectre_core/batches/__init__.py +21 -4
- spectre_core/batches/_base.py +86 -135
- spectre_core/batches/_batches.py +56 -100
- spectre_core/batches/_factory.py +22 -21
- spectre_core/batches/_register.py +9 -9
- spectre_core/batches/plugins/_batch_keys.py +8 -7
- spectre_core/batches/plugins/_callisto.py +66 -98
- spectre_core/batches/plugins/_iq_stream.py +106 -170
- spectre_core/capture_configs/__init__.py +47 -18
- spectre_core/capture_configs/_capture_config.py +26 -53
- spectre_core/capture_configs/_capture_modes.py +9 -7
- spectre_core/capture_configs/_capture_templates.py +51 -111
- spectre_core/capture_configs/_parameters.py +38 -75
- spectre_core/capture_configs/_pconstraints.py +41 -41
- spectre_core/capture_configs/_pnames.py +37 -35
- spectre_core/capture_configs/_ptemplates.py +261 -348
- spectre_core/capture_configs/_pvalidators.py +99 -102
- spectre_core/config/__init__.py +14 -9
- spectre_core/config/_paths.py +19 -36
- spectre_core/config/_time_formats.py +7 -6
- spectre_core/exceptions.py +39 -1
- spectre_core/jobs/__init__.py +4 -7
- spectre_core/jobs/_duration.py +12 -0
- spectre_core/jobs/_jobs.py +73 -44
- spectre_core/jobs/_workers.py +56 -106
- spectre_core/logs/__init__.py +8 -3
- spectre_core/logs/_configure.py +14 -18
- spectre_core/logs/_decorators.py +7 -5
- spectre_core/logs/_logs.py +38 -90
- spectre_core/logs/_process_types.py +6 -4
- spectre_core/plotting/__init__.py +14 -4
- spectre_core/plotting/_base.py +65 -139
- spectre_core/plotting/_format.py +11 -9
- spectre_core/plotting/_panel_names.py +8 -6
- spectre_core/plotting/_panel_stack.py +83 -116
- spectre_core/plotting/_panels.py +121 -156
- spectre_core/post_processing/__init__.py +7 -4
- spectre_core/post_processing/_base.py +69 -69
- spectre_core/post_processing/_factory.py +15 -12
- spectre_core/post_processing/_post_processor.py +17 -13
- spectre_core/post_processing/_register.py +11 -8
- spectre_core/post_processing/plugins/_event_handler_keys.py +5 -4
- spectre_core/post_processing/plugins/_fixed_center_frequency.py +55 -48
- spectre_core/post_processing/plugins/_swept_center_frequency.py +200 -175
- spectre_core/receivers/__init__.py +10 -3
- spectre_core/receivers/_base.py +83 -149
- spectre_core/receivers/_factory.py +21 -31
- spectre_core/receivers/_register.py +8 -11
- spectre_core/receivers/_spec_names.py +18 -16
- spectre_core/receivers/plugins/_b200mini.py +48 -61
- spectre_core/receivers/plugins/_receiver_names.py +9 -7
- spectre_core/receivers/plugins/_rsp1a.py +45 -41
- spectre_core/receivers/plugins/_rspduo.py +60 -45
- spectre_core/receivers/plugins/_sdrplay_receiver.py +68 -84
- spectre_core/receivers/plugins/_test.py +137 -130
- spectre_core/receivers/plugins/_usrp.py +94 -86
- spectre_core/receivers/plugins/gr/__init__.py +2 -2
- spectre_core/receivers/plugins/gr/_base.py +15 -23
- spectre_core/receivers/plugins/gr/_rsp1a.py +53 -60
- spectre_core/receivers/plugins/gr/_rspduo.py +78 -90
- spectre_core/receivers/plugins/gr/_test.py +50 -58
- spectre_core/receivers/plugins/gr/_usrp.py +61 -59
- spectre_core/spectrograms/__init__.py +22 -14
- spectre_core/spectrograms/_analytical.py +109 -100
- spectre_core/spectrograms/_array_operations.py +40 -47
- spectre_core/spectrograms/_spectrogram.py +290 -323
- spectre_core/spectrograms/_transform.py +107 -74
- spectre_core/wgetting/__init__.py +2 -4
- spectre_core/wgetting/_callisto.py +88 -94
- {spectre_core-0.0.21.dist-info → spectre_core-0.0.23.dist-info}/METADATA +9 -23
- spectre_core-0.0.23.dist-info/RECORD +79 -0
- {spectre_core-0.0.21.dist-info → spectre_core-0.0.23.dist-info}/WHEEL +1 -1
- spectre_core-0.0.21.dist-info/RECORD +0 -78
- {spectre_core-0.0.21.dist-info → spectre_core-0.0.23.dist-info}/licenses/LICENSE +0 -0
- {spectre_core-0.0.21.dist-info → spectre_core-0.0.23.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,9 @@
|
|
1
|
-
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
1
|
+
# SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
2
|
# This file is part of SPECTRE
|
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,62 +52,68 @@ 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.
|
84
69
|
|
85
70
|
:param event: The file system event containing the file details.
|
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
|
-
|
90
|
-
#
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
74
|
+
|
75
|
+
# Only process a file if:
|
76
|
+
#
|
77
|
+
# - It's extension matches the `watch_extension` as defined in the capture config.
|
78
|
+
# - It's tag matches the current sessions tag.
|
79
|
+
#
|
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
|
84
|
+
# seperately for both file creation events. So, we filter by extension to account for this.
|
85
|
+
#
|
86
|
+
# Additionally in the case of multiple sessions, the capture workers will create batch files in the same directory concurrently.
|
87
|
+
# This method is triggered for all file creation events, so we ensure the batch file tag matches the session tag and early return
|
88
|
+
# otherwise. This way, each post processor worker picks up the right files to process.
|
89
|
+
watch_extension = cast(
|
90
|
+
str, self._capture_config.get_parameter_value(PName.WATCH_EXTENSION)
|
91
|
+
)
|
92
|
+
if not absolute_file_path.endswith(f"{self._tag}.{watch_extension}"):
|
93
|
+
return
|
94
|
+
|
95
|
+
_LOGGER.info(f"Noticed {absolute_file_path}")
|
96
|
+
# If there exists a queued file, try and process it
|
97
|
+
if self._queued_file is not None:
|
98
|
+
try:
|
99
|
+
self.process(self._queued_file)
|
100
|
+
except Exception:
|
101
|
+
_LOGGER.error(
|
102
|
+
f"An error has occured while processing {self._queued_file}",
|
103
|
+
exc_info=True,
|
104
|
+
)
|
105
|
+
# Flush any internally stored spectrogram on error to avoid lost data
|
106
|
+
self._flush_cache()
|
107
|
+
# re-raise the exception to the main thread
|
108
|
+
raise
|
109
|
+
|
110
|
+
# Queue the current file for processing next
|
111
|
+
_LOGGER.info(f"Queueing {absolute_file_path} for post processing")
|
112
|
+
self._queued_file = absolute_file_path
|
113
|
+
|
114
|
+
def _cache_spectrogram(self, spectrogram: Spectrogram) -> None:
|
116
115
|
"""Cache the input spectrogram by storing it in the `_cached_spectrogram` attribute.
|
117
|
-
|
116
|
+
|
118
117
|
If the time range of the cached spectrogram exceeds that as specified in the capture config
|
119
118
|
`PName.TIME_RANGE` parameter, the spectrogram in the cache is flushed to file. If `PName.TIME_RANGE`
|
120
119
|
is nulled, the cache is flushed immediately.
|
@@ -126,20 +125,21 @@ class BaseEventHandler(ABC, FileSystemEventHandler):
|
|
126
125
|
if self._cached_spectrogram is None:
|
127
126
|
self._cached_spectrogram = spectrogram
|
128
127
|
else:
|
129
|
-
self._cached_spectrogram = join_spectrograms(
|
130
|
-
|
128
|
+
self._cached_spectrogram = join_spectrograms(
|
129
|
+
[self._cached_spectrogram, spectrogram]
|
130
|
+
)
|
131
|
+
|
131
132
|
time_range = self._capture_config.get_parameter_value(PName.TIME_RANGE) or 0.0
|
132
133
|
if self._cached_spectrogram.time_range >= cast(float, time_range):
|
133
134
|
self._flush_cache()
|
134
|
-
|
135
135
|
|
136
|
-
def _flush_cache(
|
137
|
-
self
|
138
|
-
) -> None:
|
136
|
+
def _flush_cache(self) -> None:
|
139
137
|
"""Flush the cached spectrogram to file."""
|
140
138
|
if self._cached_spectrogram:
|
141
|
-
_LOGGER.info(
|
142
|
-
|
139
|
+
_LOGGER.info(
|
140
|
+
f"Flushing spectrogram to file with start time "
|
141
|
+
f"'{self._cached_spectrogram.format_start_time()}'"
|
142
|
+
)
|
143
143
|
self._cached_spectrogram.save()
|
144
144
|
_LOGGER.info("Flush successful, resetting spectrogram cache")
|
145
|
-
self._cached_spectrogram = None
|
145
|
+
self._cached_spectrogram = None # reset the cache
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
1
|
+
# SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
@@ -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.
|
@@ -1,8 +1,9 @@
|
|
1
|
-
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
1
|
+
# SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
2
|
# This file is part of SPECTRE
|
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
|
-
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
1
|
+
# SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
@@ -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
|
@@ -1,16 +1,17 @@
|
|
1
|
-
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
1
|
+
# SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
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
|
-
|
@@ -1,8 +1,9 @@
|
|
1
|
-
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
1
|
+
# SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
2
|
# This file is part of SPECTRE
|
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}")
|