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.
- spectre_core/__init__.py +3 -0
- spectre_core/cfg.py +116 -0
- spectre_core/chunks/__init__.py +206 -0
- spectre_core/chunks/base.py +160 -0
- spectre_core/chunks/chunk_register.py +15 -0
- spectre_core/chunks/factory.py +26 -0
- spectre_core/chunks/library/__init__.py +8 -0
- spectre_core/chunks/library/callisto/__init__.py +0 -0
- spectre_core/chunks/library/callisto/chunk.py +101 -0
- spectre_core/chunks/library/fixed/__init__.py +0 -0
- spectre_core/chunks/library/fixed/chunk.py +185 -0
- spectre_core/chunks/library/sweep/__init__.py +0 -0
- spectre_core/chunks/library/sweep/chunk.py +400 -0
- spectre_core/dynamic_imports.py +22 -0
- spectre_core/exceptions.py +17 -0
- spectre_core/file_handlers/base.py +94 -0
- spectre_core/file_handlers/configs.py +269 -0
- spectre_core/file_handlers/json.py +36 -0
- spectre_core/file_handlers/text.py +21 -0
- spectre_core/logging.py +222 -0
- spectre_core/plotting/__init__.py +5 -0
- spectre_core/plotting/base.py +194 -0
- spectre_core/plotting/factory.py +26 -0
- spectre_core/plotting/format.py +19 -0
- spectre_core/plotting/library/__init__.py +7 -0
- spectre_core/plotting/library/frequency_cuts/panel.py +74 -0
- spectre_core/plotting/library/integral_over_frequency/panel.py +34 -0
- spectre_core/plotting/library/spectrogram/panel.py +92 -0
- spectre_core/plotting/library/time_cuts/panel.py +77 -0
- spectre_core/plotting/panel_register.py +13 -0
- spectre_core/plotting/panel_stack.py +148 -0
- spectre_core/receivers/__init__.py +6 -0
- spectre_core/receivers/base.py +415 -0
- spectre_core/receivers/factory.py +19 -0
- spectre_core/receivers/library/__init__.py +7 -0
- spectre_core/receivers/library/rsp1a/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/fixed.py +104 -0
- spectre_core/receivers/library/rsp1a/gr/sweep.py +129 -0
- spectre_core/receivers/library/rsp1a/receiver.py +68 -0
- spectre_core/receivers/library/rspduo/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +110 -0
- spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +135 -0
- spectre_core/receivers/library/rspduo/receiver.py +68 -0
- spectre_core/receivers/library/test/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/cosine_signal_1.py +83 -0
- spectre_core/receivers/library/test/gr/tagged_staircase.py +93 -0
- spectre_core/receivers/library/test/receiver.py +174 -0
- spectre_core/receivers/receiver_register.py +22 -0
- spectre_core/receivers/validators.py +205 -0
- spectre_core/spectrograms/__init__.py +3 -0
- spectre_core/spectrograms/analytical.py +205 -0
- spectre_core/spectrograms/array_operations.py +77 -0
- spectre_core/spectrograms/spectrogram.py +461 -0
- spectre_core/spectrograms/transform.py +267 -0
- spectre_core/watchdog/__init__.py +6 -0
- spectre_core/watchdog/base.py +105 -0
- spectre_core/watchdog/event_handler_register.py +15 -0
- spectre_core/watchdog/factory.py +22 -0
- spectre_core/watchdog/library/__init__.py +10 -0
- spectre_core/watchdog/library/fixed/__init__.py +0 -0
- spectre_core/watchdog/library/fixed/event_handler.py +41 -0
- spectre_core/watchdog/library/sweep/event_handler.py +55 -0
- spectre_core/watchdog/watcher.py +50 -0
- spectre_core/web_fetch/callisto.py +101 -0
- spectre_core-0.0.1.dist-info/LICENSE +674 -0
- spectre_core-0.0.1.dist-info/METADATA +40 -0
- spectre_core-0.0.1.dist-info/RECORD +72 -0
- spectre_core-0.0.1.dist-info/WHEEL +5 -0
- 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,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
|
+
|