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.
Files changed (88) hide show
  1. spectre_core/_file_io/__init__.py +1 -3
  2. spectre_core/_file_io/file_handlers.py +163 -58
  3. spectre_core/batches/__init__.py +10 -11
  4. spectre_core/batches/_base.py +170 -78
  5. spectre_core/batches/_batches.py +149 -99
  6. spectre_core/batches/_factory.py +56 -14
  7. spectre_core/batches/_register.py +23 -8
  8. spectre_core/batches/plugins/_batch_keys.py +16 -0
  9. spectre_core/batches/plugins/_callisto.py +183 -0
  10. spectre_core/batches/plugins/_iq_stream.py +354 -0
  11. spectre_core/capture_configs/__init__.py +17 -13
  12. spectre_core/capture_configs/_capture_config.py +93 -34
  13. spectre_core/capture_configs/_capture_modes.py +22 -0
  14. spectre_core/capture_configs/_capture_templates.py +207 -122
  15. spectre_core/capture_configs/_parameters.py +115 -42
  16. spectre_core/capture_configs/_pconstraints.py +86 -35
  17. spectre_core/capture_configs/_pnames.py +49 -0
  18. spectre_core/capture_configs/_ptemplates.py +389 -346
  19. spectre_core/capture_configs/_pvalidators.py +117 -73
  20. spectre_core/config/__init__.py +6 -8
  21. spectre_core/config/_paths.py +65 -25
  22. spectre_core/config/_time_formats.py +15 -10
  23. spectre_core/exceptions.py +2 -4
  24. spectre_core/jobs/__init__.py +14 -0
  25. spectre_core/jobs/_jobs.py +111 -0
  26. spectre_core/jobs/_workers.py +171 -0
  27. spectre_core/logs/__init__.py +17 -0
  28. spectre_core/logs/_configure.py +67 -0
  29. spectre_core/logs/_decorators.py +33 -0
  30. spectre_core/logs/_logs.py +228 -0
  31. spectre_core/logs/_process_types.py +14 -0
  32. spectre_core/plotting/__init__.py +4 -2
  33. spectre_core/plotting/_base.py +204 -102
  34. spectre_core/plotting/_format.py +17 -4
  35. spectre_core/plotting/_panel_names.py +18 -0
  36. spectre_core/plotting/_panel_stack.py +167 -53
  37. spectre_core/plotting/_panels.py +341 -141
  38. spectre_core/post_processing/__init__.py +8 -6
  39. spectre_core/post_processing/_base.py +70 -44
  40. spectre_core/post_processing/_factory.py +42 -12
  41. spectre_core/post_processing/_post_processor.py +24 -26
  42. spectre_core/post_processing/_register.py +22 -6
  43. spectre_core/post_processing/plugins/_event_handler_keys.py +16 -0
  44. spectre_core/post_processing/plugins/_fixed_center_frequency.py +129 -0
  45. spectre_core/post_processing/{library → plugins}/_swept_center_frequency.py +215 -143
  46. spectre_core/py.typed +0 -0
  47. spectre_core/receivers/__init__.py +10 -7
  48. spectre_core/receivers/_base.py +220 -69
  49. spectre_core/receivers/_factory.py +53 -7
  50. spectre_core/receivers/_register.py +30 -9
  51. spectre_core/receivers/_spec_names.py +26 -15
  52. spectre_core/receivers/plugins/__init__.py +0 -0
  53. spectre_core/receivers/plugins/_receiver_names.py +16 -0
  54. spectre_core/receivers/plugins/_rsp1a.py +59 -0
  55. spectre_core/receivers/plugins/_rspduo.py +67 -0
  56. spectre_core/receivers/plugins/_sdrplay_receiver.py +190 -0
  57. spectre_core/receivers/plugins/_test.py +218 -0
  58. spectre_core/receivers/plugins/gr/_base.py +80 -0
  59. spectre_core/receivers/{gr → plugins/gr}/_rsp1a.py +42 -52
  60. spectre_core/receivers/{gr → plugins/gr}/_rspduo.py +61 -74
  61. spectre_core/receivers/{gr → plugins/gr}/_test.py +33 -31
  62. spectre_core/spectrograms/__init__.py +5 -3
  63. spectre_core/spectrograms/_analytical.py +121 -66
  64. spectre_core/spectrograms/_array_operations.py +103 -36
  65. spectre_core/spectrograms/_spectrogram.py +380 -207
  66. spectre_core/spectrograms/_transform.py +197 -169
  67. spectre_core/wgetting/__init__.py +4 -2
  68. spectre_core/wgetting/_callisto.py +173 -118
  69. {spectre_core-0.0.12.dist-info → spectre_core-0.0.13.dist-info}/METADATA +14 -7
  70. spectre_core-0.0.13.dist-info/RECORD +75 -0
  71. {spectre_core-0.0.12.dist-info → spectre_core-0.0.13.dist-info}/WHEEL +1 -1
  72. spectre_core/batches/library/_callisto.py +0 -96
  73. spectre_core/batches/library/_fixed_center_frequency.py +0 -133
  74. spectre_core/batches/library/_swept_center_frequency.py +0 -105
  75. spectre_core/logging/__init__.py +0 -11
  76. spectre_core/logging/_configure.py +0 -35
  77. spectre_core/logging/_decorators.py +0 -19
  78. spectre_core/logging/_log_handlers.py +0 -176
  79. spectre_core/post_processing/library/_fixed_center_frequency.py +0 -114
  80. spectre_core/receivers/gr/_base.py +0 -33
  81. spectre_core/receivers/library/_rsp1a.py +0 -61
  82. spectre_core/receivers/library/_rspduo.py +0 -69
  83. spectre_core/receivers/library/_sdrplay_receiver.py +0 -185
  84. spectre_core/receivers/library/_test.py +0 -221
  85. spectre_core-0.0.12.dist-info/RECORD +0 -64
  86. /spectre_core/receivers/{gr → plugins/gr}/__init__.py +0 -0
  87. {spectre_core-0.0.12.dist-info → spectre_core-0.0.13.dist-info}/LICENSE +0 -0
  88. {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, FileCreatedEvent
12
+ from watchdog.events import FileSystemEventHandler, FileSystemEvent
13
13
 
14
- from spectre_core.capture_configs import CaptureConfig, PNames
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(capture_config: CaptureConfig
18
+ def make_sft_instance(
19
+ capture_config: CaptureConfig
20
20
  ) -> ShortTimeFFT:
21
- sample_rate = capture_config.get_parameter_value(PNames.SAMPLE_RATE)
22
- window_hop = capture_config.get_parameter_value(PNames.WINDOW_HOP)
23
- window_type = capture_config.get_parameter_value(PNames.WINDOW_TYPE)
24
- window_size = capture_config.get_parameter_value(PNames.WINDOW_SIZE)
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
- def __init__(self,
35
- tag: str):
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 tag tells us 'what type' of data is stored in the files for each batch
39
- self._Batch = get_batch_cls_from_tag(tag)
40
- # load the capture config corresponding to the tag
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
- # which is flushed periodically according to a user defined
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(self,
57
- absolute_file_path: str) -> None:
58
- """Process the file stored at the input absolute file path.
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
- def on_created(self,
64
- event: FileCreatedEvent):
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
- # the 'src_path' attribute holds the absolute path of the newly created file
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
- # as defined in the capture config
77
- if absolute_file_path.endswith(self._watch_extension):
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
- def _cache_spectrogram(self,
98
- spectrogram: Spectrogram) -> None:
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
- if self._cached_spectrogram.time_range >= time_range:
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(self) -> None:
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(precise=True)}'")
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 ._register import event_handler_map
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, PNames
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
- def get_event_handler_cls_from_tag(tag: str) -> BaseEventHandler:
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(PNames.EVENT_HANDLER_KEY)
23
- return _get_event_handler_cls(event_handler_key)
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 get_event_handler_cls_from_tag
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
- EventHandler = get_event_handler_cls_from_tag(tag)
21
- self._event_handler = EventHandler(tag)
22
-
23
-
24
- def start(self):
25
- """Start an observer to process newly created files in the batches directory"""
26
- self._observer.schedule(self._event_handler,
27
- get_batches_dir_path(),
28
- recursive=True,
29
- event_filter=[FileCreatedEvent])
30
-
31
- try:
32
- _LOGGER.info("Starting the post processing thread...")
33
- self._observer.start()
34
- self._observer.join()
35
- except KeyboardInterrupt:
36
- _LOGGER.warning(("Keyboard interrupt detected. Signalling "
37
- "the post processing thread to stop"))
38
- self._observer.stop()
39
- _LOGGER.warning(("Post processing thread has been successfully stopped"))
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
- # Global dictionaries to hold the mappings
6
- event_handler_map = {}
5
+ from typing import Type, Callable, TypeVar
7
6
 
8
- # classes decorated with @register_event_handler([EVENT_HANDLER_KEY])
9
- # will be added to event_handler_map
10
- def register_event_handler(event_handler_key: str):
11
- def decorator(cls):
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()