spectre-core 0.0.1__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 (72) hide show
  1. spectre_core/__init__.py +3 -0
  2. spectre_core/cfg.py +116 -0
  3. spectre_core/chunks/__init__.py +206 -0
  4. spectre_core/chunks/base.py +160 -0
  5. spectre_core/chunks/chunk_register.py +15 -0
  6. spectre_core/chunks/factory.py +26 -0
  7. spectre_core/chunks/library/__init__.py +8 -0
  8. spectre_core/chunks/library/callisto/__init__.py +0 -0
  9. spectre_core/chunks/library/callisto/chunk.py +101 -0
  10. spectre_core/chunks/library/fixed/__init__.py +0 -0
  11. spectre_core/chunks/library/fixed/chunk.py +185 -0
  12. spectre_core/chunks/library/sweep/__init__.py +0 -0
  13. spectre_core/chunks/library/sweep/chunk.py +400 -0
  14. spectre_core/dynamic_imports.py +22 -0
  15. spectre_core/exceptions.py +17 -0
  16. spectre_core/file_handlers/base.py +94 -0
  17. spectre_core/file_handlers/configs.py +269 -0
  18. spectre_core/file_handlers/json.py +36 -0
  19. spectre_core/file_handlers/text.py +21 -0
  20. spectre_core/logging.py +222 -0
  21. spectre_core/plotting/__init__.py +5 -0
  22. spectre_core/plotting/base.py +194 -0
  23. spectre_core/plotting/factory.py +26 -0
  24. spectre_core/plotting/format.py +19 -0
  25. spectre_core/plotting/library/__init__.py +7 -0
  26. spectre_core/plotting/library/frequency_cuts/panel.py +74 -0
  27. spectre_core/plotting/library/integral_over_frequency/panel.py +34 -0
  28. spectre_core/plotting/library/spectrogram/panel.py +92 -0
  29. spectre_core/plotting/library/time_cuts/panel.py +77 -0
  30. spectre_core/plotting/panel_register.py +13 -0
  31. spectre_core/plotting/panel_stack.py +148 -0
  32. spectre_core/receivers/__init__.py +6 -0
  33. spectre_core/receivers/base.py +415 -0
  34. spectre_core/receivers/factory.py +19 -0
  35. spectre_core/receivers/library/__init__.py +7 -0
  36. spectre_core/receivers/library/rsp1a/__init__.py +0 -0
  37. spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
  38. spectre_core/receivers/library/rsp1a/gr/fixed.py +104 -0
  39. spectre_core/receivers/library/rsp1a/gr/sweep.py +129 -0
  40. spectre_core/receivers/library/rsp1a/receiver.py +68 -0
  41. spectre_core/receivers/library/rspduo/__init__.py +0 -0
  42. spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
  43. spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +110 -0
  44. spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +135 -0
  45. spectre_core/receivers/library/rspduo/receiver.py +68 -0
  46. spectre_core/receivers/library/test/__init__.py +0 -0
  47. spectre_core/receivers/library/test/gr/__init__.py +0 -0
  48. spectre_core/receivers/library/test/gr/cosine_signal_1.py +83 -0
  49. spectre_core/receivers/library/test/gr/tagged_staircase.py +93 -0
  50. spectre_core/receivers/library/test/receiver.py +174 -0
  51. spectre_core/receivers/receiver_register.py +22 -0
  52. spectre_core/receivers/validators.py +205 -0
  53. spectre_core/spectrograms/__init__.py +3 -0
  54. spectre_core/spectrograms/analytical.py +205 -0
  55. spectre_core/spectrograms/array_operations.py +77 -0
  56. spectre_core/spectrograms/spectrogram.py +461 -0
  57. spectre_core/spectrograms/transform.py +267 -0
  58. spectre_core/watchdog/__init__.py +6 -0
  59. spectre_core/watchdog/base.py +105 -0
  60. spectre_core/watchdog/event_handler_register.py +15 -0
  61. spectre_core/watchdog/factory.py +22 -0
  62. spectre_core/watchdog/library/__init__.py +10 -0
  63. spectre_core/watchdog/library/fixed/__init__.py +0 -0
  64. spectre_core/watchdog/library/fixed/event_handler.py +41 -0
  65. spectre_core/watchdog/library/sweep/event_handler.py +55 -0
  66. spectre_core/watchdog/watcher.py +50 -0
  67. spectre_core/web_fetch/callisto.py +101 -0
  68. spectre_core-0.0.1.dist-info/LICENSE +674 -0
  69. spectre_core-0.0.1.dist-info/METADATA +40 -0
  70. spectre_core-0.0.1.dist-info/RECORD +72 -0
  71. spectre_core-0.0.1.dist-info/WHEEL +5 -0
  72. spectre_core-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,267 @@
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
+ import numpy as np
6
+ from datetime import datetime, timedelta
7
+ from typing import Optional
8
+
9
+ from spectre_core.spectrograms.array_operations import find_closest_index
10
+ from spectre_core.spectrograms.spectrogram import Spectrogram
11
+ from spectre_core.cfg import DEFAULT_DATETIME_FORMAT
12
+
13
+
14
+ def _average_array(array: np.ndarray, average_over: int, axis=0) -> np.ndarray:
15
+
16
+ # Check if average_over is an integer
17
+ if type(average_over) != int:
18
+ raise TypeError(f"average_over must be an integer. Got {type(average_over)}")
19
+
20
+ # Get the size of the specified axis which we will average over
21
+ axis_size = array.shape[axis]
22
+ # Check if average_over is within the valid range
23
+ if not 1 <= average_over <= axis_size:
24
+ raise ValueError(f"average_over must be between 1 and the length of the axis ({axis_size})")
25
+
26
+ max_axis_index = len(np.shape(array)) - 1
27
+ if axis > max_axis_index: # zero indexing on specifying axis, so minus one
28
+ raise ValueError(f"Requested axis is out of range of array dimensions. Axis: {axis}, max axis index: {max_axis_index}")
29
+
30
+ # find the number of elements in the requested axis
31
+ num_elements = array.shape[axis]
32
+
33
+ # find the number of "full blocks" to average over
34
+ num_full_blocks = num_elements // average_over
35
+ # if num_elements is not exactly divisible by average_over, we will have some elements left over
36
+ # these remaining elements will be padded with nans to become another full block
37
+ remainder = num_elements % average_over
38
+
39
+ # if there exists a remainder, pad the last block
40
+ if remainder != 0:
41
+ # initialise an array to hold the padding shape
42
+ padding_shape = [(0, 0)] * array.ndim
43
+ # pad after the last column in the requested axis
44
+ padding_shape[axis] = (0, average_over - remainder)
45
+ # pad with nan values (so to not contribute towards the mean computation)
46
+ array = np.pad(array, padding_shape, mode='constant', constant_values=np.nan)
47
+
48
+ # initalise a list to hold the new shape
49
+ new_shape = list(array.shape)
50
+ # update the shape on the requested access (to the number of blocks we will average over)
51
+ new_shape[axis] = num_full_blocks + (1 if remainder else 0)
52
+ # insert a new dimension, with the size of each block
53
+ new_shape.insert(axis + 1, average_over)
54
+ # and reshape the array to sort the array into the relevant blocks.
55
+ reshaped_array = array.reshape(new_shape)
56
+ # average over the newly created axis, essentially averaging over the blocks.
57
+ averaged_array = np.nanmean(reshaped_array, axis=axis + 1)
58
+ # return the averaged array
59
+ return averaged_array
60
+
61
+
62
+ def frequency_chop(input_spectrogram: Spectrogram,
63
+ start_frequency: float | int,
64
+ end_frequency: float | int) -> Optional[Spectrogram]:
65
+
66
+ is_entirely_below_frequency_range = (start_frequency < input_spectrogram.frequencies[0] and end_frequency < input_spectrogram.frequencies[0])
67
+ is_entirely_above_frequency_range = (start_frequency > input_spectrogram.frequencies[-1] and end_frequency > input_spectrogram.frequencies[-1])
68
+ # if the requested frequency range is out of bounds for the spectrogram return None
69
+ if is_entirely_below_frequency_range or is_entirely_above_frequency_range:
70
+ return None
71
+
72
+ #find the index of the nearest matching frequency bins in the spectrogram
73
+ start_index = find_closest_index(start_frequency, input_spectrogram.frequencies)
74
+ end_index = find_closest_index(end_frequency, input_spectrogram.frequencies)
75
+
76
+ # enforce distinct start and end indices
77
+ if start_index == end_index:
78
+ raise ValueError(f"Start and end indices are equal! Got start_index: {start_index} and end_index: {end_index}")
79
+
80
+ # if start index is more than end index, swap the ordering so to enforce start_index <= end_index
81
+ if start_index > end_index:
82
+ start_index, end_index = end_index, start_index
83
+
84
+ # chop the spectrogram accordingly
85
+ transformed_dynamic_spectra = input_spectrogram.dynamic_spectra[start_index:end_index+1, :]
86
+ transformed_frequencies = input_spectrogram.frequencies[start_index:end_index+1]
87
+
88
+ # return the spectrogram instance
89
+ return Spectrogram(transformed_dynamic_spectra,
90
+ input_spectrogram.times,
91
+ transformed_frequencies,
92
+ input_spectrogram.tag,
93
+ chunk_start_time = input_spectrogram.chunk_start_time,
94
+ microsecond_correction = input_spectrogram.microsecond_correction,
95
+ spectrum_type = input_spectrogram.spectrum_type)
96
+
97
+
98
+ def time_chop(input_spectrogram: Spectrogram,
99
+ start_time: str,
100
+ end_time: str,
101
+ time_format: str = DEFAULT_DATETIME_FORMAT) -> Optional[Spectrogram]:
102
+
103
+ # parse the strings as datetimes
104
+ start_datetime = datetime.strptime(start_time, time_format)
105
+ end_datetime = datetime.strptime(end_time, time_format)
106
+
107
+ # if the requested time range is out of bounds for the spectrogram return None
108
+ is_entirely_below_time_range = (start_datetime < input_spectrogram.datetimes[0] and end_datetime < input_spectrogram.datetimes[0])
109
+ is_entirely_above_time_range = (start_datetime > input_spectrogram.datetimes[-1] and end_datetime > input_spectrogram.datetimes[-1])
110
+ if is_entirely_below_time_range or is_entirely_above_time_range:
111
+ return None
112
+
113
+ start_index = find_closest_index(start_datetime, input_spectrogram.datetimes)
114
+ end_index = find_closest_index(end_datetime, input_spectrogram.datetimes)
115
+
116
+ if start_index == end_index:
117
+ raise ValueError(f"Start and end indices are equal! Got start_index: {start_index} and end_index: {end_index}")
118
+
119
+ if start_index > end_index:
120
+ start_index, end_index = end_index, start_index
121
+
122
+ # chop the spectrogram
123
+ transformed_dynamic_spectra = input_spectrogram.dynamic_spectra[:, start_index:end_index+1]
124
+
125
+ # chop the times array
126
+ transformed_times = input_spectrogram.times[start_index:end_index+1]
127
+ #translate the chopped times array to start at zero
128
+ transformed_times -= transformed_times[0]
129
+
130
+ # compute the new start datetime following the time chop
131
+ transformed_start_datetime = input_spectrogram.datetimes[start_index]
132
+ # parse the chunk start time (as string)
133
+ transformed_chunk_start_time = datetime.strftime(transformed_start_datetime, DEFAULT_DATETIME_FORMAT)
134
+ # and compute the microsecond correction
135
+ transformed_microsecond_correction = transformed_start_datetime.microsecond
136
+
137
+ return Spectrogram(transformed_dynamic_spectra,
138
+ transformed_times,
139
+ input_spectrogram.frequencies,
140
+ input_spectrogram.tag,
141
+ chunk_start_time = transformed_chunk_start_time,
142
+ microsecond_correction = transformed_microsecond_correction,
143
+ spectrum_type = input_spectrogram.spectrum_type)
144
+
145
+
146
+ def time_average(input_spectrogram: Spectrogram,
147
+ average_over: int) -> Spectrogram:
148
+
149
+ # spectre does not currently support averaging of non-datetime assigned spectrograms
150
+ if input_spectrogram.chunk_start_time is None:
151
+ raise ValueError(f"Input spectrogram is missing chunk start time. Averaging is not yet supported for non-datetime assigned spectrograms")
152
+
153
+ # if the user has requested no averaging, just return the same instance unchanged
154
+ if average_over == 1:
155
+ return input_spectrogram
156
+
157
+ # average the dynamic spectra array
158
+ transformed_dynamic_spectra = _average_array(input_spectrogram.dynamic_spectra, average_over, axis=1)
159
+ # average the times array s.t. the ith averaged spectrum is assigned the
160
+ transformed_times = _average_array(input_spectrogram.times, average_over)
161
+
162
+ # We need to assign timestamps to the averaged spectrums in the spectrograms. The natural way to do this
163
+ # is to assign the i'th averaged spectrogram to the i'th averaged time stamp. From this,
164
+ # we then need to compute the chunk start time to assig to the first averaged spectrum,
165
+ # and update the microsecond correction.
166
+
167
+ # define the initial spectrum as the spectrum at time index 0 in the spectrogram
168
+ # then, averaged_t0 is the seconds elapsed between the input intial spectrum and the averaged intial spectrum
169
+ averaged_t0 = float(transformed_times[0])
170
+ # compute the updated chunk start time and the updated microsecond correction based on averaged_t0
171
+ corrected_start_datetime = input_spectrogram.datetimes[0] + timedelta(seconds = averaged_t0)
172
+ # then, compute the transformed chunk start time and microsecond correction
173
+ transformed_chunk_start_time = corrected_start_datetime.strftime(DEFAULT_DATETIME_FORMAT)
174
+ transformed_microsecond_correction = corrected_start_datetime.microsecond
175
+
176
+ # finally, translate the averaged time seconds to begin at t=0 [s]
177
+ transformed_times -= transformed_times[0]
178
+ return Spectrogram(transformed_dynamic_spectra,
179
+ transformed_times,
180
+ input_spectrogram.frequencies,
181
+ input_spectrogram.tag,
182
+ chunk_start_time = transformed_chunk_start_time,
183
+ microsecond_correction = transformed_microsecond_correction,
184
+ spectrum_type = input_spectrogram.spectrum_type)
185
+
186
+ def frequency_average(input_spectrogram: Spectrogram,
187
+ average_over: int) -> Spectrogram:
188
+
189
+ # if the user has requested no averaging, just return the same instance unchanged
190
+ if average_over == 1:
191
+ return input_spectrogram
192
+
193
+ # We need to assign physical frequencies to the averaged spectrums in the spectrograms.
194
+ # is to assign the i'th averaged spectral component to the i'th averaged frequency.
195
+ # average the dynamic spectra array
196
+ transformed_dynamic_spectra = _average_array(input_spectrogram.dynamic_spectra, average_over, axis=0)
197
+ transformed_frequencies = _average_array(input_spectrogram.frequencies, average_over)
198
+
199
+ return Spectrogram(transformed_dynamic_spectra,
200
+ input_spectrogram.times,
201
+ transformed_frequencies,
202
+ input_spectrogram.tag,
203
+ chunk_start_time = input_spectrogram.chunk_start_time,
204
+ microsecond_correction = input_spectrogram.microsecond_correction,
205
+ spectrum_type = input_spectrogram.spectrum_type)
206
+
207
+
208
+ def _time_elapsed(datetimes: np.ndarray) -> np.ndarray:
209
+ # Extract the first datetime to use as the reference point
210
+ base_time = datetimes[0]
211
+ # Calculate elapsed time in seconds for each datetime in the list
212
+ elapsed_time = [(dt - base_time).total_seconds() for dt in datetimes]
213
+ # Convert the list of seconds to a NumPy array of type float32
214
+ return np.array(elapsed_time, dtype=np.float32)
215
+
216
+ # we assume that the spectrogram list is ORDERED chronologically
217
+ # we assume there is no time overlap in any of the spectrograms in the list
218
+ def join_spectrograms(spectrograms: list[Spectrogram]) -> Spectrogram:
219
+
220
+ # check that the length of the list is non-zero
221
+ num_spectrograms = len(spectrograms)
222
+ if num_spectrograms == 0:
223
+ raise ValueError(f"Input list of spectrograms is empty!")
224
+
225
+ # extract the first element of the list, and use this as a reference for comparison
226
+ # input validations.
227
+ reference_spectrogram = spectrograms[0]
228
+
229
+ # perform checks on each spectrogram in teh list
230
+ for spectrogram in spectrograms:
231
+ if not np.all(np.equal(spectrogram.frequencies, reference_spectrogram.frequencies)):
232
+ raise ValueError(f"All spectrograms must have identical frequency ranges")
233
+ if spectrogram.tag != reference_spectrogram.tag:
234
+ raise ValueError(f"All tags must be equal for each spectrogram in the input list!")
235
+ if spectrogram.spectrum_type != reference_spectrogram.spectrum_type:
236
+ raise ValueError(f"All units must be equal for each spectrogram in the input list!")
237
+ if spectrogram.chunk_start_time is None:
238
+ raise ValueError(f"All spectrograms must have chunk_start_time set. Received {spectrogram.chunk_start_time}")
239
+
240
+
241
+ # build a list of the time array of each spectrogram in the list
242
+ conc_datetimes = np.concatenate([s.datetimes for s in spectrograms])
243
+ # find the total number of time stamps
244
+ num_total_time_bins = len(conc_datetimes)
245
+ # find the total number of frequency bins (we can safely now use the first)
246
+ num_total_freq_bins = len(reference_spectrogram.frequencies)
247
+ # create an empty numpy array to hold the joined spectrograms
248
+ transformed_dynamic_spectra = np.empty((num_total_freq_bins, num_total_time_bins))
249
+
250
+ start_index = 0
251
+ for spectrogram in spectrograms:
252
+ end_index = start_index + len(spectrogram.times)
253
+ transformed_dynamic_spectra[:, start_index:end_index] = spectrogram.dynamic_spectra
254
+ start_index = end_index
255
+
256
+ transformed_times = _time_elapsed(conc_datetimes)
257
+
258
+ transformed_microsecond_correction = conc_datetimes[0].microsecond
259
+
260
+ return Spectrogram(transformed_dynamic_spectra,
261
+ transformed_times,
262
+ reference_spectrogram.frequencies,
263
+ reference_spectrogram.tag,
264
+ chunk_start_time = reference_spectrogram.chunk_start_time,
265
+ microsecond_correction = transformed_microsecond_correction,
266
+ spectrum_type = reference_spectrogram.spectrum_type)
267
+
@@ -0,0 +1,6 @@
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
+ # dynamically import all event handlers
6
+ import spectre_core.watchdog.library
@@ -0,0 +1,105 @@
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 os
9
+ import time
10
+ from queue import Queue
11
+ from typing import Any
12
+ from abc import ABC, abstractmethod
13
+ from math import floor
14
+
15
+ from watchdog.events import FileSystemEventHandler
16
+
17
+ from spectre_core.chunks.factory import get_chunk_from_tag
18
+ from spectre_core.file_handlers.configs import CaptureConfig
19
+ from spectre_core.spectrograms.spectrogram import Spectrogram
20
+ from spectre_core.spectrograms.transform import join_spectrograms
21
+ from spectre_core.spectrograms.transform import (
22
+ time_average,
23
+ frequency_average
24
+ )
25
+
26
+
27
+ class BaseEventHandler(ABC, FileSystemEventHandler):
28
+ def __init__(self,
29
+ tag: str,
30
+ exception_queue: Queue,
31
+ extension: str):
32
+ self._tag = tag
33
+ self._Chunk = get_chunk_from_tag(tag)
34
+
35
+ self._capture_config = CaptureConfig(tag)
36
+
37
+ self._extension = extension
38
+ self._exception_queue = exception_queue # Queue to propagate exceptions
39
+
40
+ self._spectrogram: Spectrogram = None # spectrogram cache
41
+
42
+
43
+ @abstractmethod
44
+ def process(self, file_path: str) -> None:
45
+ pass
46
+
47
+
48
+ def on_created(self, event):
49
+ if not event.is_directory and event.src_path.endswith(self._extension):
50
+ _LOGGER.info(f"Noticed: {event.src_path}")
51
+ try:
52
+ self._wait_until_stable(event.src_path)
53
+ self.process(event.src_path)
54
+ except Exception as e:
55
+ _LOGGER.error(f"An error has occured while processing {event.src_path}",
56
+ exc_info=True)
57
+ self._flush_spectrogram() # flush the internally stored spectrogram
58
+ # Capture the exception and propagate it through the queue
59
+ self._exception_queue.put(e)
60
+
61
+
62
+ def _wait_until_stable(self, file_path: str):
63
+ _LOGGER.info(f"Waiting for file stability: {file_path}")
64
+ size = -1
65
+ while True:
66
+ current_size = os.path.getsize(file_path)
67
+ if current_size == size:
68
+ _LOGGER.info(f"File is now stable: {file_path}")
69
+ break # File is stable when the size hasn't changed
70
+ size = current_size
71
+ time.sleep(0.25)
72
+
73
+
74
+ def _average_in_time(self, spectrogram: Spectrogram) -> Spectrogram:
75
+ requested_time_resolution = self._capture_config.get('time_resolution') # [s]
76
+ if requested_time_resolution is None:
77
+ raise KeyError(f"Time resolution has not been specified in the capture config!")
78
+ average_over = floor(requested_time_resolution/spectrogram.time_resolution) if requested_time_resolution > spectrogram.time_resolution else 1
79
+ return time_average(spectrogram, average_over)
80
+
81
+
82
+ def _average_in_frequency(self, spectrogram: Spectrogram) -> Spectrogram:
83
+ frequency_resolution = self._capture_config.get('frequency_resolution') # [Hz]
84
+ if frequency_resolution is None:
85
+ raise KeyError(f"Frequency resolution has not been specified in the capture config!")
86
+ average_over = floor(frequency_resolution/spectrogram.frequency_resolution) if frequency_resolution > spectrogram.frequency_resolution else 1
87
+ return frequency_average(spectrogram, average_over)
88
+
89
+
90
+ def _join_spectrogram(self, spectrogram: Spectrogram) -> None:
91
+ if self._spectrogram is None:
92
+ self._spectrogram = spectrogram
93
+ else:
94
+ self._spectrogram = join_spectrograms([self._spectrogram, spectrogram])
95
+
96
+ if self._spectrogram.time_range >= self._capture_config.get("joining_time"):
97
+ self._flush_spectrogram()
98
+
99
+
100
+ def _flush_spectrogram(self) -> None:
101
+ if self._spectrogram:
102
+ _LOGGER.info(f"Flushing spectrogram to file with chunk start time {self._spectrogram.chunk_start_time}")
103
+ self._spectrogram.save()
104
+ _LOGGER.info("Flush successful, resetting spectrogram cache")
105
+ self._spectrogram = None # reset the cache
@@ -0,0 +1,15 @@
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
+ # Global dictionaries to hold the mappings
6
+ event_handler_map = {}
7
+
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):
12
+ event_handler_map[event_handler_key] = cls
13
+ return cls
14
+ return decorator
15
+
@@ -0,0 +1,22 @@
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 spectre_core.watchdog.event_handler_register import event_handler_map
6
+ from spectre_core.watchdog.base import BaseEventHandler
7
+ from spectre_core.file_handlers.configs import CaptureConfig
8
+ from spectre_core.exceptions import EventHandlerNotFoundError
9
+
10
+ def get_event_handler(event_handler_key: str) -> BaseEventHandler:
11
+ # try and fetch the capture config mount
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}. Please specify one of the following event handler keys {valid_event_handler_keys}")
16
+ return EventHandler
17
+
18
+
19
+ def get_event_handler_from_tag(tag: str) -> BaseEventHandler:
20
+ capture_config = CaptureConfig(tag)
21
+ event_handler_key = capture_config.get('event_handler_key')
22
+ return get_event_handler(event_handler_key)
@@ -0,0 +1,10 @@
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 spectre_core.dynamic_imports import import_target_modules
6
+
7
+ import_target_modules(__file__, __name__, "event_handler")
8
+
9
+
10
+
File without changes
@@ -0,0 +1,41 @@
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 os
9
+
10
+ from spectre_core.watchdog.base import BaseEventHandler
11
+ from spectre_core.watchdog.event_handler_register import register_event_handler
12
+
13
+ @register_event_handler("fixed")
14
+ class EventHandler(BaseEventHandler):
15
+ def __init__(self, *args, **kwargs):
16
+ super().__init__(*args, **kwargs)
17
+
18
+
19
+ def process(self, file_path: str):
20
+ _LOGGER.info(f"Processing: {file_path}")
21
+ file_name = os.path.basename(file_path)
22
+ chunk_start_time, _ = os.path.splitext(file_name)[0].split('_')
23
+ chunk = self._Chunk(chunk_start_time, self._tag)
24
+
25
+ _LOGGER.info("Creating spectrogram")
26
+ spectrogram = chunk.build_spectrogram()
27
+
28
+ _LOGGER.info("Averaging spectrogram")
29
+ spectrogram = self._average_in_time(spectrogram)
30
+ spectrogram = self._average_in_frequency(spectrogram)
31
+
32
+ _LOGGER.info("Joining spectrogram")
33
+ self._join_spectrogram(spectrogram)
34
+
35
+ bin_chunk = chunk.get_file('bin')
36
+ _LOGGER.info(f"Deleting {bin_chunk.file_path}")
37
+ bin_chunk.delete(doublecheck_delete = False)
38
+
39
+ hdr_chunk = chunk.get_file('hdr')
40
+ _LOGGER.info(f"Deleting {hdr_chunk.file_path}")
41
+ hdr_chunk.delete(doublecheck_delete = False)
@@ -0,0 +1,55 @@
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 os
9
+
10
+ from spectre_core.chunks.base import BaseChunk
11
+ from spectre_core.watchdog.base import BaseEventHandler
12
+ from spectre_core.watchdog.event_handler_register import register_event_handler
13
+
14
+ @register_event_handler("sweep")
15
+ class EventHandler(BaseEventHandler):
16
+ def __init__(self, *args, **kwargs):
17
+ super().__init__(*args, **kwargs)
18
+
19
+ self.previous_chunk: BaseChunk = None # cache for previous chunk
20
+
21
+
22
+ def process(self, file_path: str):
23
+ _LOGGER.info(f"Processing: {file_path}")
24
+ file_name = os.path.basename(file_path)
25
+ chunk_start_time, _ = os.path.splitext(file_name)[0].split('_')
26
+ chunk = self._Chunk(chunk_start_time, self._tag)
27
+
28
+ _LOGGER.info("Creating spectrogram")
29
+ spectrogram = chunk.build_spectrogram(previous_chunk = self.previous_chunk)
30
+
31
+ _LOGGER.info("Averaging spectrogram")
32
+ spectrogram = self._average_in_time(spectrogram)
33
+ spectrogram = self._average_in_frequency(spectrogram)
34
+
35
+ _LOGGER.info("Joining spectrogram")
36
+ self._join_spectrogram(spectrogram)
37
+
38
+ # if the previous chunk has not yet been set, it means we were processing the first chunk
39
+ # so we don't need to handle the previous chunk
40
+ if self.previous_chunk is None:
41
+ # instead, only set it for the next time this method is called
42
+ self.previous_chunk = chunk
43
+
44
+ # otherwise the previous chunk is defined (and by this point has already been processed)
45
+ else:
46
+ bin_chunk = self.previous_chunk.get_file('bin')
47
+ _LOGGER.info(f"Deleting {bin_chunk.file_path}")
48
+ bin_chunk.delete(doublecheck_delete = False)
49
+
50
+ hdr_chunk = self.previous_chunk.get_file('hdr')
51
+ _LOGGER.info(f"Deleting {hdr_chunk.file_path}")
52
+ hdr_chunk.delete(doublecheck_delete = False)
53
+
54
+ # and reassign the current chunk to be used as the previous chunk at the next call of this method
55
+ self.previous_chunk = chunk
@@ -0,0 +1,50 @@
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
+ from queue import Queue, Empty
9
+
10
+ from watchdog.observers import Observer
11
+
12
+ from spectre_core.watchdog.factory import get_event_handler_from_tag
13
+ from spectre_core.cfg import CHUNKS_DIR_PATH
14
+
15
+ class Watcher:
16
+ def __init__(self,
17
+ tag: str):
18
+ self._observer: Observer = Observer()
19
+ self._exception_queue: Queue = Queue() # A thread-safe queue for exceptions
20
+
21
+ EventHandler = get_event_handler_from_tag(tag)
22
+ self._event_handler = EventHandler(tag,
23
+ self._exception_queue,
24
+ "bin")
25
+
26
+
27
+ def start(self):
28
+ _LOGGER.info("Starting watcher...")
29
+
30
+ # Schedule and start the observer
31
+ self._observer.schedule(self._event_handler,
32
+ CHUNKS_DIR_PATH,
33
+ recursive=True)
34
+ self._observer.start()
35
+
36
+ try:
37
+ # Monitor the observer and handle exceptions
38
+ while self._observer.is_alive():
39
+ try:
40
+ exc = self._exception_queue.get(block=True, timeout=0.25)
41
+ if exc:
42
+ raise exc # Propagate the exception
43
+ except Empty:
44
+ pass # Continue looping if no exception in queue
45
+ finally:
46
+ # Ensure the observer is properly stopped
47
+ self._observer.stop()
48
+ self._observer.join()
49
+
50
+