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.
Files changed (77) hide show
  1. spectre_core/_file_io/__init__.py +5 -5
  2. spectre_core/_file_io/file_handlers.py +61 -107
  3. spectre_core/batches/__init__.py +21 -4
  4. spectre_core/batches/_base.py +86 -135
  5. spectre_core/batches/_batches.py +56 -100
  6. spectre_core/batches/_factory.py +22 -21
  7. spectre_core/batches/_register.py +9 -9
  8. spectre_core/batches/plugins/_batch_keys.py +8 -7
  9. spectre_core/batches/plugins/_callisto.py +66 -98
  10. spectre_core/batches/plugins/_iq_stream.py +106 -170
  11. spectre_core/capture_configs/__init__.py +47 -18
  12. spectre_core/capture_configs/_capture_config.py +26 -53
  13. spectre_core/capture_configs/_capture_modes.py +9 -7
  14. spectre_core/capture_configs/_capture_templates.py +51 -111
  15. spectre_core/capture_configs/_parameters.py +38 -75
  16. spectre_core/capture_configs/_pconstraints.py +41 -41
  17. spectre_core/capture_configs/_pnames.py +37 -35
  18. spectre_core/capture_configs/_ptemplates.py +261 -348
  19. spectre_core/capture_configs/_pvalidators.py +99 -102
  20. spectre_core/config/__init__.py +14 -9
  21. spectre_core/config/_paths.py +19 -36
  22. spectre_core/config/_time_formats.py +7 -6
  23. spectre_core/exceptions.py +39 -1
  24. spectre_core/jobs/__init__.py +4 -7
  25. spectre_core/jobs/_duration.py +12 -0
  26. spectre_core/jobs/_jobs.py +73 -44
  27. spectre_core/jobs/_workers.py +56 -106
  28. spectre_core/logs/__init__.py +8 -3
  29. spectre_core/logs/_configure.py +14 -18
  30. spectre_core/logs/_decorators.py +7 -5
  31. spectre_core/logs/_logs.py +38 -90
  32. spectre_core/logs/_process_types.py +6 -4
  33. spectre_core/plotting/__init__.py +14 -4
  34. spectre_core/plotting/_base.py +65 -139
  35. spectre_core/plotting/_format.py +11 -9
  36. spectre_core/plotting/_panel_names.py +8 -6
  37. spectre_core/plotting/_panel_stack.py +83 -116
  38. spectre_core/plotting/_panels.py +121 -156
  39. spectre_core/post_processing/__init__.py +7 -4
  40. spectre_core/post_processing/_base.py +69 -69
  41. spectre_core/post_processing/_factory.py +15 -12
  42. spectre_core/post_processing/_post_processor.py +17 -13
  43. spectre_core/post_processing/_register.py +11 -8
  44. spectre_core/post_processing/plugins/_event_handler_keys.py +5 -4
  45. spectre_core/post_processing/plugins/_fixed_center_frequency.py +55 -48
  46. spectre_core/post_processing/plugins/_swept_center_frequency.py +200 -175
  47. spectre_core/receivers/__init__.py +10 -3
  48. spectre_core/receivers/_base.py +83 -149
  49. spectre_core/receivers/_factory.py +21 -31
  50. spectre_core/receivers/_register.py +8 -11
  51. spectre_core/receivers/_spec_names.py +18 -16
  52. spectre_core/receivers/plugins/_b200mini.py +48 -61
  53. spectre_core/receivers/plugins/_receiver_names.py +9 -7
  54. spectre_core/receivers/plugins/_rsp1a.py +45 -41
  55. spectre_core/receivers/plugins/_rspduo.py +60 -45
  56. spectre_core/receivers/plugins/_sdrplay_receiver.py +68 -84
  57. spectre_core/receivers/plugins/_test.py +137 -130
  58. spectre_core/receivers/plugins/_usrp.py +94 -86
  59. spectre_core/receivers/plugins/gr/__init__.py +2 -2
  60. spectre_core/receivers/plugins/gr/_base.py +15 -23
  61. spectre_core/receivers/plugins/gr/_rsp1a.py +53 -60
  62. spectre_core/receivers/plugins/gr/_rspduo.py +78 -90
  63. spectre_core/receivers/plugins/gr/_test.py +50 -58
  64. spectre_core/receivers/plugins/gr/_usrp.py +61 -59
  65. spectre_core/spectrograms/__init__.py +22 -14
  66. spectre_core/spectrograms/_analytical.py +109 -100
  67. spectre_core/spectrograms/_array_operations.py +40 -47
  68. spectre_core/spectrograms/_spectrogram.py +290 -323
  69. spectre_core/spectrograms/_transform.py +107 -74
  70. spectre_core/wgetting/__init__.py +2 -4
  71. spectre_core/wgetting/_callisto.py +88 -94
  72. {spectre_core-0.0.21.dist-info → spectre_core-0.0.23.dist-info}/METADATA +9 -23
  73. spectre_core-0.0.23.dist-info/RECORD +79 -0
  74. {spectre_core-0.0.21.dist-info → spectre_core-0.0.23.dist-info}/WHEEL +1 -1
  75. spectre_core-0.0.21.dist-info/RECORD +0 -78
  76. {spectre_core-0.0.21.dist-info → spectre_core-0.0.23.dist-info}/licenses/LICENSE +0 -0
  77. {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 = 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))
32
- window = get_window(window_type,
33
- window_size)
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
- def __init__(
43
- self,
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
- # the `src_path`` attribute holds the absolute path of the freshly closed file
72
+ # The `src_path`` attribute holds the absolute path of the freshly closed file
88
73
  absolute_file_path = event.src_path
89
-
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 ):
94
- _LOGGER.info(f"Noticed {absolute_file_path}")
95
- # If there exists a queued file, try and process it
96
- if self._queued_file is not None:
97
- try:
98
- self.process(self._queued_file)
99
- except Exception:
100
- _LOGGER.error(f"An error has occured while processing {self._queued_file}",
101
- exc_info=True)
102
- # flush any internally stored spectrogram on error to avoid lost data
103
- self._flush_cache()
104
- # re-raise the exception to the main thread
105
- raise
106
-
107
- # Queue the current file for processing next
108
- _LOGGER.info(f"Queueing {absolute_file_path} for post processing")
109
- self._queued_file = absolute_file_path
110
-
111
-
112
- def _cache_spectrogram(
113
- self,
114
- spectrogram: Spectrogram
115
- ) -> None:
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([self._cached_spectrogram, spectrogram])
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(f"Flushing spectrogram to file with start time "
142
- f"'{self._cached_spectrogram.format_start_time()}'")
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 # reset the cache
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((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}"))
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(str, capture_config.get_parameter_value(PName.EVENT_HANDLER_KEY))
41
- return _get_event_handler_cls_from_key( EventHandlerKey(event_handler_key) )
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(event_handler,
25
- get_batches_dir_path(),
26
- recursive=True,
27
- event_filter=[ FileCreatedEvent ])
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(("Keyboard interrupt detected. Signalling "
35
- "the post processing thread to stop"))
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('T', bound=BaseEventHandler)
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
- def decorator(
24
- cls: Type[T]
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(f"An event handler with key '{event_handler_key}' is already registered!")
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
- class EventHandlerKey(Enum):
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 Spectrogram, SpectrumUnit, time_average, frequency_average
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
- p0 = p0,
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(float, capture_config.get_parameter_value(PName.CENTER_FREQUENCY))
63
+ frequencies = sft.f + cast(
64
+ float, capture_config.get_parameter_value(PName.CENTER_FREQUENCY)
65
+ )
63
66
 
64
- return times.astype(np.float32), frequencies.astype(np.float32), dynamic_spectra.astype(np.float32)
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 = batch.bin_file.read()
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(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)
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
- 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
-
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}")