spectre-core 0.0.8__py3-none-any.whl → 0.0.9__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.
@@ -15,7 +15,9 @@ from spectre_core.plotting.factory import get_panel
15
15
  from spectre_core.plotting.library.spectrogram.panel import Panel as SpectrogramPanel
16
16
 
17
17
  class PanelStack:
18
- def __init__(self, time_type: str, figsize: Tuple[int, int] = (10, 10)):
18
+ def __init__(self,
19
+ time_type: str = "seconds",
20
+ figsize: Tuple[int, int] = (10, 10)):
19
21
  self._time_type = time_type
20
22
  self._figsize = figsize
21
23
  self._panels: List[BasePanel] = []
@@ -3,4 +3,4 @@
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  # dynamically import all event handlers
6
- import spectre_core.watchdog.library
6
+ import spectre_core.post_processing.library
@@ -0,0 +1,132 @@
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
9
+ from typing import Optional
10
+ from abc import ABC, abstractmethod
11
+ from math import floor
12
+
13
+ from watchdog.events import (
14
+ FileSystemEventHandler,
15
+ FileCreatedEvent,
16
+ )
17
+
18
+ from spectre_core.chunks.factory import get_chunk_from_tag
19
+ from spectre_core.file_handlers.configs import CaptureConfig
20
+ from spectre_core.spectrograms.spectrogram import Spectrogram
21
+ from spectre_core.spectrograms.transform import join_spectrograms
22
+ from spectre_core.spectrograms.transform import (
23
+ time_average,
24
+ frequency_average
25
+ )
26
+
27
+
28
+ class BaseEventHandler(ABC, FileSystemEventHandler):
29
+ def __init__(self,
30
+ tag: str):
31
+ self._tag = tag
32
+
33
+ self._Chunk = get_chunk_from_tag(tag)
34
+
35
+ self._capture_config = CaptureConfig(tag)
36
+
37
+ self._watch_extension = self._capture_config.get("watch_extension")
38
+ if self._watch_extension is None:
39
+ raise KeyError("The watch extension has not been specified in the capture config")
40
+
41
+ # attribute to store the next file to be processed
42
+ # (specifically, the absolute file path of the file)
43
+ self._queued_file: Optional[str] = None
44
+
45
+ # spectrogram cache stores spectrograms in memory
46
+ # such that they can be periodically written to files
47
+ # according to the joining time.
48
+ self._spectrogram: Optional[Spectrogram] = None
49
+
50
+
51
+
52
+ @abstractmethod
53
+ def process(self,
54
+ absolute_file_path: str) -> None:
55
+ """Process the file stored at the input absolute file path.
56
+
57
+ To be implemented by derived classes.
58
+ """
59
+
60
+
61
+ def on_created(self,
62
+ event: FileCreatedEvent):
63
+ """Process a newly created batch file, only once the next batch is created.
64
+
65
+ Since we assume that the batches are non-overlapping in time, this guarantees
66
+ we avoid post processing a file while it is being written to. Files are processed
67
+ sequentially, in the order they are created.
68
+ """
69
+
70
+ # the 'src_path' attribute holds the absolute path of the newly created file
71
+ absolute_file_path = event.src_path
72
+
73
+ # only 'notice' a file if it ends with the appropriate extension
74
+ # as defined in the capture config
75
+ if absolute_file_path.endswith(self._watch_extension):
76
+ _LOGGER.info(f"Noticed {absolute_file_path}")
77
+
78
+ # If there exists a queued file, try and process it
79
+ if self._queued_file is not None:
80
+ try:
81
+ self.process(self._queued_file)
82
+ except Exception:
83
+ _LOGGER.error(f"An error has occured while processing {self._queued_file}",
84
+ exc_info=True)
85
+ # flush any internally stored spectrogram on error to avoid lost data
86
+ self._flush_spectrogram()
87
+ # re-raise the exception to the main thread
88
+ raise
89
+
90
+ # Queue the current file for processing next
91
+ _LOGGER.info(f"Queueing {absolute_file_path} for post processing")
92
+ self._queued_file = absolute_file_path
93
+
94
+
95
+ def _average_in_time(self,
96
+ spectrogram: Spectrogram) -> Spectrogram:
97
+ _LOGGER.info("Averaging spectrogram in time")
98
+ requested_time_resolution = self._capture_config['time_resolution'] # [s]
99
+ if requested_time_resolution is None:
100
+ raise KeyError(f"Time resolution has not been specified in the capture config")
101
+ average_over = floor(requested_time_resolution/spectrogram.time_resolution) if requested_time_resolution > spectrogram.time_resolution else 1
102
+ return time_average(spectrogram, average_over)
103
+
104
+
105
+ def _average_in_frequency(self,
106
+ spectrogram: Spectrogram) -> Spectrogram:
107
+ _LOGGER.info("Averaging spectrogram in frequency")
108
+ frequency_resolution = self._capture_config['frequency_resolution'] # [Hz]
109
+ if frequency_resolution is None:
110
+ raise KeyError(f"Frequency resolution has not been specified in the capture config")
111
+ average_over = floor(frequency_resolution/spectrogram.frequency_resolution) if frequency_resolution > spectrogram.frequency_resolution else 1
112
+ return frequency_average(spectrogram, average_over)
113
+
114
+
115
+ def _join_spectrogram(self,
116
+ spectrogram: Spectrogram) -> None:
117
+ _LOGGER.info("Joining spectrogram")
118
+ if self._spectrogram is None:
119
+ self._spectrogram = spectrogram
120
+ else:
121
+ self._spectrogram = join_spectrograms([self._spectrogram, spectrogram])
122
+
123
+ if self._spectrogram.time_range >= self._capture_config['joining_time']:
124
+ self._flush_spectrogram()
125
+
126
+
127
+ def _flush_spectrogram(self) -> None:
128
+ if self._spectrogram:
129
+ _LOGGER.info(f"Flushing spectrogram to file with chunk start time {self._spectrogram.chunk_start_time}")
130
+ self._spectrogram.save()
131
+ _LOGGER.info("Flush successful, resetting spectrogram cache")
132
+ self._spectrogram = None # reset the cache
@@ -2,17 +2,17 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- from spectre_core.watchdog.event_handler_register import event_handler_map
6
- from spectre_core.watchdog.base import BaseEventHandler
5
+ from spectre_core.post_processing.event_handler_register import event_handler_map
6
+ from spectre_core.post_processing.base import BaseEventHandler
7
7
  from spectre_core.file_handlers.configs import CaptureConfig
8
8
  from spectre_core.exceptions import EventHandlerNotFoundError
9
9
 
10
10
  def get_event_handler(event_handler_key: str) -> BaseEventHandler:
11
- # try and fetch the capture config mount
12
11
  EventHandler = event_handler_map.get(event_handler_key)
13
12
  if EventHandler is None:
14
13
  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}")
14
+ raise EventHandlerNotFoundError((f"No event handler found for the event handler key '{event_handler_key}'. "
15
+ f"Please specify one of the following event handler keys: {valid_event_handler_keys}"))
16
16
  return EventHandler
17
17
 
18
18
 
@@ -7,8 +7,8 @@ _LOGGER = getLogger(__name__)
7
7
 
8
8
  import os
9
9
 
10
- from spectre_core.watchdog.base import BaseEventHandler
11
- from spectre_core.watchdog.event_handler_register import register_event_handler
10
+ from spectre_core.post_processing.base import BaseEventHandler
11
+ from spectre_core.post_processing.event_handler_register import register_event_handler
12
12
 
13
13
  @register_event_handler("fixed")
14
14
  class EventHandler(BaseEventHandler):
@@ -16,20 +16,19 @@ class EventHandler(BaseEventHandler):
16
16
  super().__init__(*args, **kwargs)
17
17
 
18
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('_')
19
+ def process(self,
20
+ absolute_file_path: str):
21
+ _LOGGER.info(f"Processing: {absolute_file_path}")
22
+ file_name = os.path.basename(absolute_file_path)
23
+ base_file_name, _ = os.path.splitext(file_name)
24
+ chunk_start_time, _ = base_file_name.split('_')
23
25
  chunk = self._Chunk(chunk_start_time, self._tag)
24
26
 
25
27
  _LOGGER.info("Creating spectrogram")
26
28
  spectrogram = chunk.build_spectrogram()
27
29
 
28
- _LOGGER.info("Averaging spectrogram")
29
30
  spectrogram = self._average_in_time(spectrogram)
30
31
  spectrogram = self._average_in_frequency(spectrogram)
31
-
32
- _LOGGER.info("Joining spectrogram")
33
32
  self._join_spectrogram(spectrogram)
34
33
 
35
34
  bin_chunk = chunk.get_file('bin')
@@ -8,8 +8,8 @@ _LOGGER = getLogger(__name__)
8
8
  import os
9
9
 
10
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
11
+ from spectre_core.post_processing.base import BaseEventHandler
12
+ from spectre_core.post_processing.event_handler_register import register_event_handler
13
13
 
14
14
  @register_event_handler("sweep")
15
15
  class EventHandler(BaseEventHandler):
@@ -19,23 +19,22 @@ class EventHandler(BaseEventHandler):
19
19
  self.previous_chunk: BaseChunk = None # cache for previous chunk
20
20
 
21
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('_')
22
+ def process(self,
23
+ absolute_file_path: str):
24
+ _LOGGER.info(f"Processing: {absolute_file_path}")
25
+ file_name = os.path.basename(absolute_file_path)
26
+ base_file_name, _ = os.path.splitext(file_name)
27
+ chunk_start_time, _ = base_file_name.split('_')
26
28
  chunk = self._Chunk(chunk_start_time, self._tag)
27
29
 
28
30
  _LOGGER.info("Creating spectrogram")
29
31
  spectrogram = chunk.build_spectrogram(previous_chunk = self.previous_chunk)
30
32
 
31
- _LOGGER.info("Averaging spectrogram")
32
33
  spectrogram = self._average_in_time(spectrogram)
33
34
  spectrogram = self._average_in_frequency(spectrogram)
34
-
35
- _LOGGER.info("Joining spectrogram")
36
35
  self._join_spectrogram(spectrogram)
37
36
 
38
- # if the previous chunk has not yet been set, it means we were processing the first chunk
37
+ # if the previous chunk has not yet been set, it means we are processing the first chunk
39
38
  # so we don't need to handle the previous chunk
40
39
  if self.previous_chunk is None:
41
40
  # instead, only set it for the next time this method is called
@@ -0,0 +1,40 @@
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 watchdog.observers import Observer
9
+ from watchdog.events import FileCreatedEvent
10
+
11
+ from spectre_core.post_processing.factory import get_event_handler_from_tag
12
+ from spectre_core.cfg import CHUNKS_DIR_PATH
13
+
14
+ class PostProcessor:
15
+ def __init__(self,
16
+ tag: str):
17
+
18
+ self._observer = Observer()
19
+
20
+ EventHandler = get_event_handler_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 chunks directory"""
26
+ self._observer.schedule(self._event_handler,
27
+ CHUNKS_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"))
40
+
@@ -242,6 +242,7 @@ class SPECTREReceiver(BaseReceiver):
242
242
  "hop": int, # STFFT window hops by so many samples
243
243
  "chunk_key": str, # maps to the corresponding chunk class
244
244
  "event_handler_key": str, # maps to the event handler used in post processing
245
+ "watch_extension": str, # event handlers watch for files with this extension
245
246
  },
246
247
  "sweep": {
247
248
  "min_freq": float, # [Hz]
@@ -262,6 +263,7 @@ class SPECTREReceiver(BaseReceiver):
262
263
  "hop": int, # keyword arguments for the scipy STFFT class
263
264
  "chunk_key": str, # maps to the corresponding chunk class
264
265
  "event_handler_key": str, # maps to the event handler used in post processing
266
+ "watch_extension": str, # event handlers watch for files with this extension
265
267
  }
266
268
  }
267
269
 
@@ -291,17 +293,18 @@ class SPECTREReceiver(BaseReceiver):
291
293
  window_size = capture_config["window_size"]
292
294
  hop = capture_config["hop"]
293
295
  chunk_key = capture_config["chunk_key"]
294
- event_handler_key = capture_config[ "event_handler_key"]
296
+ event_handler_key = capture_config["event_handler_key"]
297
+ watch_extension = capture_config["watch_extension"]
295
298
 
296
299
  validators.center_freq_strictly_positive(min_freq)
297
300
  validators.center_freq_strictly_positive(max_freq)
298
301
  validators.samp_rate_strictly_positive(samp_rate)
299
302
  validators.bandwidth_strictly_positive(bandwidth)
300
303
  validators.nyquist_criterion(samp_rate,
301
- bandwidth)
304
+ bandwidth)
302
305
  validators.chunk_size_strictly_positive(chunk_size)
303
306
  validators.time_resolution(time_resolution,
304
- chunk_size)
307
+ chunk_size)
305
308
  validators.window(window_type,
306
309
  window_kwargs,
307
310
  window_size,
@@ -326,6 +329,8 @@ class SPECTREReceiver(BaseReceiver):
326
329
  samp_rate)
327
330
  validators.num_samples_per_step(samples_per_step,
328
331
  window_size)
332
+ validators.watch_extension(watch_extension,
333
+ "bin")
329
334
 
330
335
  # if the api latency is defined, raise a warning if the step interval is of the same order
331
336
  api_latency = self.specifications.get("api_latency")
@@ -350,6 +355,7 @@ class SPECTREReceiver(BaseReceiver):
350
355
  hop = capture_config["hop"]
351
356
  chunk_key = capture_config["chunk_key"]
352
357
  event_handler_key = capture_config["event_handler_key"]
358
+ watch_extension = capture_config["watch_extension"]
353
359
 
354
360
  validators.center_freq_strictly_positive(center_freq)
355
361
  validators.samp_rate_strictly_positive(samp_rate)
@@ -369,7 +375,8 @@ class SPECTREReceiver(BaseReceiver):
369
375
  "fixed")
370
376
  validators.gain_is_negative(IF_gain)
371
377
  validators.gain_is_negative(RF_gain)
372
-
378
+ validators.watch_extension(watch_extension,
379
+ "bin")
373
380
 
374
381
  # parent class for shared methods and attributes of SDRPlay receivers
375
382
  class SDRPlayReceiver(SPECTREReceiver):
@@ -46,6 +46,7 @@ class Receiver(SPECTREReceiver):
46
46
  "hop": int, # STFFT hop shifts window by so many samples
47
47
  "chunk_key": str, # maps to the corresponding chunk class
48
48
  "event_handler_key": str, # maps to the event handler used in post processing
49
+ "watch_extension": str # event handlers watch for files with this extension
49
50
  },
50
51
  "tagged-staircase": {
51
52
  "samp_rate": int, # [Hz]
@@ -63,6 +64,7 @@ class Receiver(SPECTREReceiver):
63
64
  "hop": int, # keyword arguments for scipy STFFT class
64
65
  "chunk_key": str, # maps to the corresponding chunk class
65
66
  "event_handler_key": str, # maps to the event handler used in post processing
67
+ "watch_extension": str # event handlers watch for files with this extension
66
68
  }
67
69
  }
68
70
 
@@ -93,27 +95,35 @@ class Receiver(SPECTREReceiver):
93
95
  event_handler_key = capture_config["event_handler_key"]
94
96
  time_resolution = capture_config["time_resolution"]
95
97
  frequency_resolution = capture_config["frequency_resolution"]
98
+ watch_extension = capture_config["watch_extension"]
96
99
 
97
100
  validators.samp_rate_strictly_positive(samp_rate)
98
101
  validators.chunk_size_strictly_positive(chunk_size)
99
102
  validators.time_resolution(time_resolution, chunk_size)
100
103
  validators.window(window_type,
101
- {},
102
- window_size,
103
- chunk_size,
104
- samp_rate)
104
+ {},
105
+ window_size,
106
+ chunk_size,
107
+ samp_rate)
105
108
  validators.hop(hop)
106
- validators.chunk_key(chunk_key, "fixed")
107
- validators.event_handler_key(event_handler_key, "fixed")
109
+ validators.chunk_key(chunk_key,
110
+ "fixed")
111
+ validators.event_handler_key(event_handler_key,
112
+ "fixed")
113
+ validators.watch_extension(watch_extension,
114
+ "bin")
108
115
 
109
116
  if samp_rate < self.specifications.get("samp_rate_lower_bound"):
110
- raise ValueError(f"Sample rate must be greater than or equal to {self.specifications.get('samp_rate_lower_bound')}")
117
+ raise ValueError((f"Sample rate must be greater than or equal to "
118
+ f"{self.specifications.get('samp_rate_lower_bound')}"))
111
119
 
112
120
  if time_resolution != 0:
113
- raise ValueError(f"Time resolution must be zero. Received: {time_resolution}")
121
+ raise ValueError(f"Time resolution must be zero. "
122
+ f"Got {time_resolution} [s]")
114
123
 
115
124
  if frequency_resolution != 0:
116
- raise ValueError(f"Frequency resolution must be zero. Received {frequency_resolution}")
125
+ raise ValueError((f"Frequency resolution must be zero. "
126
+ f"Got {frequency_resolution}"))
117
127
 
118
128
  # check that the sample rate is an integer multiple of the underlying signal frequency
119
129
  if samp_rate % frequency != 0:
@@ -121,22 +131,26 @@ class Receiver(SPECTREReceiver):
121
131
 
122
132
  a = samp_rate/frequency
123
133
  if a < 2:
124
- raise ValueError(f"The ratio of sampling rate over frequency must be a natural number greater than two. Received: {a}")
134
+ raise ValueError((f"The ratio of sampling rate over frequency must be a natural number greater than two. "
135
+ f"Got {a}"))
125
136
 
126
137
  # ensuring the window type is rectangular
127
138
  if window_type != "boxcar":
128
- raise ValueError(f"The window type must be \"boxcar\". Received: {window_type}")
139
+ raise ValueError((f"The window type must be 'boxcar'. "
140
+ f"Got {window_type}"))
129
141
 
130
142
  # analytical requirement
131
143
  # if p is the number of sampled cycles, we can find that p = window_size / a
132
144
  # the number of sampled cycles must be a positive natural number.
133
145
  p = window_size / a
134
146
  if window_size % a != 0:
135
- raise ValueError(f"The number of sampled cycles must be a positive natural number. Computed that p={p}")
147
+ raise ValueError((f"The number of sampled cycles must be a positive natural number. "
148
+ f"Computed that p={p}"))
136
149
 
137
150
 
138
151
  if amplitude <= 0:
139
- raise ValueError(f"The amplitude must be strictly positive. Received: {amplitude}")
152
+ raise ValueError((f"The amplitude must be strictly positive. "
153
+ f"Got {amplitude}"))
140
154
 
141
155
 
142
156
  def __tagged_staircase_validator(self, capture_config: CaptureConfig) -> None:
@@ -153,26 +167,37 @@ class Receiver(SPECTREReceiver):
153
167
  chunk_key = capture_config["chunk_key"]
154
168
  event_handler_key = capture_config["event_handler_key"]
155
169
  time_resolution = capture_config["time_resolution"]
156
-
170
+ watch_extension = capture_config["watch_extension"]
171
+
157
172
  validators.samp_rate_strictly_positive(samp_rate)
158
173
  validators.chunk_size_strictly_positive(chunk_size)
159
174
  validators.time_resolution(time_resolution, chunk_size)
160
- validators.window(window_type, window_kwargs, window_size, chunk_size, samp_rate)
175
+ validators.window(window_type,
176
+ window_kwargs,
177
+ window_size,
178
+ chunk_size,
179
+ samp_rate)
161
180
  validators.hop(hop)
162
181
  validators.chunk_key(chunk_key, "sweep")
163
182
  validators.event_handler_key(event_handler_key, "sweep")
164
-
183
+ validators.watch_extension(watch_extension,
184
+ "bin")
185
+
165
186
  if freq_step != samp_rate:
166
187
  raise ValueError(f"The frequency step must be equal to the sampling rate")
167
188
 
168
189
  if min_samples_per_step <= 0:
169
- raise ValueError(f"Minimum samples per step must be strictly positive. Received: {min_samples_per_step}")
190
+ raise ValueError((f"Minimum samples per step must be strictly positive. "
191
+ f"Got {min_samples_per_step}"))
170
192
 
171
193
  if max_samples_per_step <= 0:
172
- raise ValueError(f"Maximum samples per step must be strictly positive. Received: {max_samples_per_step}")
194
+ raise ValueError((f"Maximum samples per step must be strictly positive. "
195
+ f"Got {max_samples_per_step}"))
173
196
 
174
197
  if step_increment <= 0:
175
- raise ValueError(f"Step increment must be strictly positive. Received: {step_increment}")
198
+ raise ValueError((f"Step increment must be strictly positive. "
199
+ f"Got {step_increment}"))
176
200
 
177
201
  if min_samples_per_step > max_samples_per_step:
178
- raise ValueError(f"Minimum samples per step cannot be greater than the maximum samples per step. Received: {min_samples_per_step} > {max_samples_per_step}")
202
+ raise ValueError((f"Minimum samples per step cannot be greater than the maximum samples per step. "
203
+ f"Got {min_samples_per_step}, which is greater than {max_samples_per_step}"))
@@ -14,34 +14,39 @@ from scipy.signal import get_window
14
14
  def closed_upper_bound_RF_gain(RF_gain: float,
15
15
  RF_gain_upper_bound: float) -> None:
16
16
  if not (RF_gain <= RF_gain_upper_bound):
17
- raise ValueError(f"RF gain must be strictly less than or equal to {RF_gain_upper_bound} [dB]. Got {RF_gain} [dB]")
17
+ raise ValueError((f"RF gain must be less than or equal to {RF_gain_upper_bound} [dB]. "
18
+ f"Got {RF_gain} [dB]"))
18
19
 
19
20
 
20
21
  def closed_upper_bound_IF_gain(IF_gain: float,
21
22
  IF_gain_upper_bound: float) -> None:
22
23
  if not (IF_gain <= IF_gain_upper_bound):
23
- raise ValueError(f"IF gain must be strictly less than or equal to {IF_gain_upper_bound} [dB]. Got {IF_gain} [dB]")
24
+ raise ValueError((f"IF gain must be less than or equal to {IF_gain_upper_bound} [dB]. "
25
+ f"Got {IF_gain} [dB]"))
24
26
 
25
27
 
26
28
  def closed_confine_center_freq(center_freq: float,
27
29
  center_freq_lower_bound: float,
28
30
  center_freq_upper_bound: float) -> None:
29
31
  if not (center_freq_lower_bound <= center_freq <= center_freq_upper_bound):
30
- raise ValueError(f"Center frequency must be between {center_freq_lower_bound*1e-3} [kHz] and {center_freq_upper_bound*1e-9} [GHz]. Received {center_freq*1e-6} [MHz]")
32
+ raise ValueError((f"Center frequency must be between {center_freq_lower_bound*1e-3} [kHz] and {center_freq_upper_bound*1e-9} [GHz]. "
33
+ f"Got {center_freq*1e-6} [MHz]"))
31
34
 
32
35
 
33
36
  def closed_confine_samp_rate(samp_rate: int,
34
37
  samp_rate_lower_bound: int,
35
38
  samp_rate_upper_bound: int) -> None:
36
39
  if not (samp_rate_lower_bound <= samp_rate <= samp_rate_upper_bound):
37
- raise ValueError(f"Sampling rate must be between {samp_rate_lower_bound*1e-6} [MHz] and {samp_rate_upper_bound*1e-6} [MHz]. Received {samp_rate*1e-6} [MHz]")
40
+ raise ValueError((f"Sampling rate must be between {samp_rate_lower_bound*1e-6} [MHz] and {samp_rate_upper_bound*1e-6} [MHz]. "
41
+ f"Got {samp_rate*1e-6} [MHz]"))
38
42
 
39
43
 
40
44
  def closed_confine_bandwidth(bandwidth: float,
41
45
  bandwidth_lower_bound: float,
42
46
  bandwidth_upper_bound: float) -> None:
43
47
  if not (bandwidth_lower_bound <= bandwidth <= bandwidth_upper_bound):
44
- raise ValueError(f"Bandwidth must be between {bandwidth_lower_bound*1e-3} [kHz] and {bandwidth_upper_bound*1e-6} [MHz]. Received {bandwidth*1e-6} [MHz]")
48
+ raise ValueError((f"Bandwidth must be between {bandwidth_lower_bound*1e-3} [kHz] and {bandwidth_upper_bound*1e-6} [MHz]. "
49
+ f"Got {bandwidth*1e-6} [MHz]"))
45
50
 
46
51
 
47
52
  def is_power_of_two(n):
@@ -55,84 +60,103 @@ def window(window_type: str,
55
60
  samp_rate: float) -> None:
56
61
 
57
62
  if not is_power_of_two(window_size):
58
- raise ValueError(f"Window size must be some power of two. Received: {window_size}")
63
+ raise ValueError((f"Window size must be some power of two. "
64
+ f"Got {window_size} [samples]"))
59
65
 
60
-
61
- if window_size*(1/samp_rate) > chunk_size:
62
- raise ValueError("Windowing interval must be strictly less than the chunk size")
66
+ window_interval = window_size*(1/samp_rate)
67
+ if window_interval > chunk_size:
68
+ raise ValueError((f"The windowing interval must be strictly less than the chunk size. "
69
+ f"Computed the windowing interval to be {window_interval} [s], "
70
+ f"but the chunk size is {chunk_size} [s]"))
63
71
 
64
72
  try:
65
73
  window_params = (window_type,
66
74
  *window_kwargs.values())
67
75
  _ = get_window(window_params, window_size)
68
76
  except Exception as e:
69
- raise Exception(f"An error has occurred while validating the window. Received: {str(e)}")
77
+ raise Exception((f"An error has occurred while validating the window. "
78
+ f"Got {str(e)}"))
70
79
 
71
80
 
72
81
  def hop(hop: int):
73
82
  if hop < 0:
74
- raise ValueError(f"\"hop\" must be strictly positive. Received: {hop_value}")
83
+ raise ValueError((f"Window hop must be strictly positive. "
84
+ f"Got {hop} [samples]"))
75
85
 
76
86
 
77
87
  def center_freq_strictly_positive(center_freq: float):
78
88
  if center_freq <= 0:
79
- raise ValueError(f"Center frequency must be strictly positive. Received: {center_freq*1e-6} [MHz]")
89
+ raise ValueError((f"Center frequency must be strictly positive. "
90
+ f"Got {center_freq*1e-6} [MHz]"))
80
91
 
81
92
 
82
93
  def bandwidth_strictly_positive(bandwidth: float) -> None:
83
94
  if bandwidth < 0:
84
- raise ValueError(f"Bandwidth must be non-negative. Received: {bandwidth*1e-6} [MHz]")
95
+ raise ValueError((f"Bandwidth must be non-negative. "
96
+ f"Got {bandwidth*1e-6} [MHz]"))
85
97
 
86
98
 
87
99
  def nyquist_criterion(samp_rate: int,
88
100
  bandwidth: float) -> None:
89
101
  if samp_rate < bandwidth:
90
- raise ValueError("Sample rate must be greater than or equal to the bandwidth")
102
+ raise ValueError((f"Sample rate must be greater than or equal to the bandwidth. "
103
+ f"Got sample rate {samp_rate} [Hz], and bandwidth {bandwidth} [Hz]"))
91
104
 
92
105
 
93
106
  def samp_rate_strictly_positive(samp_rate: int) -> None:
94
- if samp_rate < 0:
95
- raise ValueError(f"Sample rate must be strictly positive. Received: {samp_rate} [Hz]")
107
+ if samp_rate <= 0:
108
+ raise ValueError((f"Sample rate must be strictly positive. "
109
+ f"Got {samp_rate} [Hz]"))
96
110
 
97
111
 
98
112
  def chunk_size_strictly_positive(chunk_size: int) -> None:
99
113
  if chunk_size <= 0:
100
- raise ValueError(f"Chunk size must be strictly positive. Received: {chunk_size} [s]")
114
+ raise ValueError((f"Chunk size must be strictly positive. "
115
+ f"Got {chunk_size} [s]"))
101
116
 
102
117
 
103
118
  def time_resolution(time_resolution: float,
104
119
  chunk_size: int) -> None:
105
120
  if time_resolution < 0:
106
- raise ValueError(f"Time resolution must be non-negative. Received: {time_resolution} [s]")
121
+ raise ValueError((f"Time resolution must be non-negative. "
122
+ f"Got {time_resolution} [s]"))
107
123
 
108
124
  if time_resolution > chunk_size:
109
- raise ValueError("Time resolution must be less than or equal to chunk size")
125
+ raise ValueError(f"Time resolution must be less than or equal to chunk size. "
126
+ f"Got time resolution {time_resolution} [s], "
127
+ f"and chunk size {chunk_size} [s]")
110
128
 
111
129
 
112
130
  def frequency_resolution(frequency_resolution: float,
113
131
  bandwidth: float = None) -> None:
114
132
  if frequency_resolution < 0:
115
- raise ValueError(f"Frequency resolution must be non-negative. Received {frequency_resolution} [Hz]")
133
+ raise ValueError((f"Frequency resolution must be non-negative. "
134
+ f"Got {frequency_resolution} [Hz]"))
116
135
 
117
136
  if bandwidth is not None and frequency_resolution >= bandwidth:
118
- raise ValueError(f"Frequency resolution must be less than the bandwidth. Received frequency resolution to be {frequency_resolution} [Hz], with bandwidth {bandwidth} [Hz]")
137
+ raise ValueError((f"Frequency resolution must be less than the bandwidth. "
138
+ f"Got frequency resolution to be {frequency_resolution} [Hz], "
139
+ f"with bandwidth {bandwidth} [Hz]"))
119
140
 
120
141
 
121
142
  def chunk_key(chunk_key: str,
122
143
  expected_chunk_key: str) -> None:
123
144
  if chunk_key != expected_chunk_key:
124
- raise ValueError(f"Expected \"{expected_chunk_key}\" for the chunk_key, received: {chunk_key}")
145
+ raise ValueError((f"Expected {expected_chunk_key} for the chunk key. "
146
+ f"Got {chunk_key}"))
125
147
 
126
148
 
127
149
  def event_handler_key(event_handler_key: str,
128
150
  expected_event_handler_key: str) -> None:
129
151
  if event_handler_key != expected_event_handler_key:
130
- raise ValueError(f"Expected \"{expected_event_handler_key}\" for the event_handler_key, received: {event_handler_key}")
152
+ raise ValueError((f"Expected {expected_event_handler_key} for the event handler key. "
153
+ f"Got {event_handler_key}"))
131
154
 
132
155
 
133
156
  def gain_is_negative(gain: float) -> None:
134
157
  if gain > 0:
135
- raise ValueError(f"Gain must be non-positive. Received {gain} [dB]")
158
+ raise ValueError(f"Gain must be non-positive. "
159
+ f"Got {gain} [dB]")
136
160
 
137
161
 
138
162
  def _compute_num_steps_per_sweep(min_freq: float,
@@ -151,7 +175,8 @@ def num_steps_per_sweep(min_freq: float,
151
175
  samp_rate,
152
176
  freq_step)
153
177
  if num_steps_per_sweep <= 1:
154
- raise ValueError(f"We need strictly greater than one sample per step. Computed: {num_steps_per_sweep}")
178
+ raise ValueError((f"We need strictly greater than one step per sweep. "
179
+ f"Computed {num_steps_per_sweep} step per sweep"))
155
180
 
156
181
 
157
182
  def sweep_interval(min_freq: float,
@@ -167,19 +192,25 @@ def sweep_interval(min_freq: float,
167
192
  num_samples_per_sweep = num_steps_per_sweep * samples_per_step
168
193
  sweep_interval = num_samples_per_sweep * 1/samp_rate
169
194
  if sweep_interval > chunk_size:
170
- raise ValueError(f"Sweep interval must be less than the chunk size. Computed sweep interval: {sweep_interval} [s] is greater than the given chunk size {chunk_size} [s]")
195
+ raise ValueError((f"Sweep interval must be less than the chunk size. "
196
+ f"The computed sweep interval is {sweep_interval} [s], "
197
+ f"but the given chunk size is {chunk_size} [s]"))
171
198
 
172
199
 
173
200
  def num_samples_per_step(samples_per_step: int,
174
201
  window_size: int) -> None:
175
202
  if window_size >= samples_per_step:
176
- raise ValueError(f"Window size must be strictly less than the number of samples per step. Received window size {window_size} [samples], which is more than or equal to the number of samples per step {samples_per_step}")
203
+ raise ValueError((f"Window size must be strictly less than the number of samples per step. "
204
+ f"Got window size {window_size} [samples], which is more than or equal "
205
+ f"to the number of samples per step {samples_per_step}"))
177
206
 
178
207
 
179
208
  def non_overlapping_steps(freq_step: float,
180
209
  samp_rate: int) -> None:
181
210
  if freq_step < samp_rate:
182
- raise NotImplementedError(f"SPECTRE does not yet support spectral steps overlapping in frequency. Received frequency step {freq_step/1e6} [MHz] which is less than the sample rate {samp_rate/1e6} [MHz]")
211
+ raise NotImplementedError(f"SPECTRE does not yet support spectral steps overlapping in frequency. "
212
+ f"Got frequency step {freq_step/1e6} [MHz] which is less than the sample "
213
+ f"rate {samp_rate/1e6} [MHz]")
183
214
 
184
215
 
185
216
  def step_interval(samples_per_step: int,
@@ -187,7 +218,14 @@ def step_interval(samples_per_step: int,
187
218
  api_latency: float) -> None:
188
219
  step_interval = samples_per_step * 1/samp_rate # [s]
189
220
  if step_interval < api_latency:
190
- warning_message = f"The computed step interval of {step_interval} [s] is of the order of empirically derived api latency {api_latency} [s]; you may experience undefined behaviour!"
221
+ warning_message = (f"The computed step interval of {step_interval} [s] is of the order of empirically "
222
+ f"derived api latency {api_latency} [s]; you may experience undefined behaviour!")
191
223
  warnings.warn(warning_message)
192
224
  _LOGGER.warning(warning_message)
193
225
 
226
+
227
+ def watch_extension(watch_extension: str,
228
+ target_extension: str) -> None:
229
+ if watch_extension != target_extension:
230
+ raise ValueError((f"Expected {target_extension} for the watch extension. "
231
+ f"Got {watch_extension}"))
@@ -94,7 +94,7 @@ def fetch_chunks(instrument_code: Optional[str],
94
94
  os.mkdir(temp_dir)
95
95
 
96
96
  if instrument_code not in CALLISTO_INSTRUMENT_CODES:
97
- raise ValueError(f"No match found for \"{instrument_code}\". Expected one of {CALLISTO_INSTRUMENT_CODES}")
97
+ raise ValueError(f"No match found for '{instrument_code}'. Expected one of {CALLISTO_INSTRUMENT_CODES}")
98
98
 
99
99
  download_callisto_data(instrument_code, year, month, day)
100
100
  unzip_to_chunks()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spectre-core
3
- Version: 0.0.8
3
+ Version: 0.0.9
4
4
  Summary: The core Python package used by the spectre program.
5
5
  Maintainer-email: Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -23,17 +23,26 @@ spectre_core/plotting/base.py,sha256=4HhPPP7BNe5_SUAl1Ee52_QP62Zzh3kmNJwLzCHKG3c
23
23
  spectre_core/plotting/factory.py,sha256=pYil7PbTs3e08ek6nq-y8mSEtNu7DI6uloiCwsuIoOw,940
24
24
  spectre_core/plotting/format.py,sha256=Et-uc4juDl_2spLRZOzqaUVBP8-J3LPnV5M6CllM74E,512
25
25
  spectre_core/plotting/panel_register.py,sha256=rsmG2hXnvVOy6vfWsEZlZQAXej7bRtqtvjnXObyUCg4,351
26
- spectre_core/plotting/panel_stack.py,sha256=PxtoXp9WzSe38gG6dILoM__X_BxwNdydXRpzs3knsF0,5191
26
+ spectre_core/plotting/panel_stack.py,sha256=fB1kz9NgsnMl4l41cncNPaVmg5DtYRRvO4lhX8OLOOo,5239
27
27
  spectre_core/plotting/library/__init__.py,sha256=lQhj4kme2eKb-JSrHvLWkAjI7eVqexOxxUguo8LUXno,270
28
28
  spectre_core/plotting/library/frequency_cuts/panel.py,sha256=LtCWNad4Z1gxBOxi_Ni1d6KZxsgoR5Mbm2tWXN7VDqI,2772
29
29
  spectre_core/plotting/library/integral_over_frequency/panel.py,sha256=tvro2MCtY4Q0q7WdMUz9eW5Cvrweeqqse20q3x4D4fM,1274
30
30
  spectre_core/plotting/library/spectrogram/panel.py,sha256=CAaPz7sDYoWZ3-4Jb1kVRu9bvJYaBRiXvoMkV7QXWqk,3556
31
31
  spectre_core/plotting/library/time_cuts/panel.py,sha256=u9Sbnwy6ex61y5Jl-D77HlYvuuXdK8_YB-o2gCovCTY,2947
32
+ spectre_core/post_processing/__init__.py,sha256=pRzy46C32j9sfQjbCJKuVw0tQjuOErfrievq2m1Vx8c,239
33
+ spectre_core/post_processing/base.py,sha256=h0qtoigD6ZBkHm0_wV1lJX55AwppKC5htfXVWUWMMLQ,5403
34
+ spectre_core/post_processing/event_handler_register.py,sha256=DwlkU92IFkZ1_qvGfhep_OfuqTOIR_efY5qFpn1perw,498
35
+ spectre_core/post_processing/factory.py,sha256=7KZBysPymsYhpW4KI4H1jvjssmZY1mIvhkln91joeUM,1133
36
+ spectre_core/post_processing/post_processor.py,sha256=dQX71pUh83i6mNedTk-EVAYnxjAUs6S2Q8HkCXkiwns,1428
37
+ spectre_core/post_processing/library/__init__.py,sha256=vEwAnAV-sv7WcNYOdnjr1JVqZYr29Wr2cv01eoxwdmg,282
38
+ spectre_core/post_processing/library/fixed/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
+ spectre_core/post_processing/library/fixed/event_handler.py,sha256=wfOdahnY78Movu0ESbuUy-QmpBbXhNk6_DKzsXS9G0Y,1394
40
+ spectre_core/post_processing/library/sweep/event_handler.py,sha256=ap7K8hOxgb8wsX9uJfEDW7lYXCyNKFlsva7vinW_6Ak,2194
32
41
  spectre_core/receivers/__init__.py,sha256=kKfhqrGs9sSPGLbrpTqScv816iPZOvT3ry3zSMcqLkM,227
33
- spectre_core/receivers/base.py,sha256=UW2L28juLWyzI6t6Ub6Ws-K4J9U4HT4JSgdVtuiYP8k,16012
42
+ spectre_core/receivers/base.py,sha256=szW7BfJEQiZ7dEiD_Oyk2lG16Kns3zKmnl_lIqKHP1o,16478
34
43
  spectre_core/receivers/factory.py,sha256=aE-Yw_cnlkhRe5HxK0JqhDzd2AwZcKmB2QkAKwaq27Y,873
35
44
  spectre_core/receivers/receiver_register.py,sha256=xHcRnT-3NQxyIWL3nyT3P9qT14Wl5liM9HbflOvOUAM,617
36
- spectre_core/receivers/validators.py,sha256=aP8nbRWZxU5pOwBIXTojXuHd3i9yksHW_vIBn4cbKug,8481
45
+ spectre_core/receivers/validators.py,sha256=ZWnFsZJsbHBB-ZIrp3WK8BPqb4ev7TYm6VPQPbljTE4,9965
37
46
  spectre_core/receivers/library/__init__.py,sha256=xmtF5p3_ZkGfso_pKnxSgUcXXFLEBwERGPq1Pek7cOU,274
38
47
  spectre_core/receivers/library/rsp1a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
48
  spectre_core/receivers/library/rsp1a/receiver.py,sha256=xs_aNMhwIYD83KwutizjBziyu9XsfHqGqvQXOFcglz4,2224
@@ -48,7 +57,7 @@ spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py,sha256=gR5arbjvJBJjCdQ
48
57
  spectre_core/receivers/library/rspduo/gr/tuner_2_fixed.py,sha256=O86R3tuJ-7cZXfVECuZkds-wCxKdBKJ8MhVACJFgzeo,4130
49
58
  spectre_core/receivers/library/rspduo/gr/tuner_2_sweep.py,sha256=4JgJ2QRO5DFPfqUTHBojj8ERR_4QpHyTBqnAXhOzykE,4747
50
59
  spectre_core/receivers/library/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
- spectre_core/receivers/library/test/receiver.py,sha256=ihfmUFFFEcyjMnMrqP4ajgB1mdC0vWEe9LFkNYHZn_Y,8346
60
+ spectre_core/receivers/library/test/receiver.py,sha256=hr6t8jpUwVEP2zMpjORKpzqLaBllxmbqF1fQE1DLWQw,9329
52
61
  spectre_core/receivers/library/test/gr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
62
  spectre_core/receivers/library/test/gr/cosine_signal_1.py,sha256=6XgYjYMh-QNPs_UUGUQcU_VQFr6BG4OLdsW-M-RU5Ww,2943
54
63
  spectre_core/receivers/library/test/gr/tagged_staircase.py,sha256=5rJHbB-3vdXjqT8DrcAGUSebaAqZ5RQtYHBWgH9iU2E,3465
@@ -57,18 +66,9 @@ spectre_core/spectrograms/analytical.py,sha256=c5X40YgLlutP8sbz6dqODZaCmZ98sAjub
57
66
  spectre_core/spectrograms/array_operations.py,sha256=6qKd3y2z6Pmu_U8yxTR4FN4eMhS10KgZ8rH60B_IXqw,2577
58
67
  spectre_core/spectrograms/spectrogram.py,sha256=EqeQyvjzjoKaXou4vJbPbRx85BeMPB9iiJtFZcCyimI,19488
59
68
  spectre_core/spectrograms/transform.py,sha256=xo7ch2lrRkJ54cfIqbkaTHNo_AptBuK0zRELPf7SfIE,13860
60
- spectre_core/watchdog/__init__.py,sha256=gGjyC7MeCfarB9Yu0RycZs-Wh_of7Cbp_wIyovYvtOQ,232
61
- spectre_core/watchdog/base.py,sha256=xDvWtt7fXeBQoZpR-ZxopAAKNHRa8YNSmfR63bYsSlU,4178
62
- spectre_core/watchdog/event_handler_register.py,sha256=DwlkU92IFkZ1_qvGfhep_OfuqTOIR_efY5qFpn1perw,498
63
- spectre_core/watchdog/factory.py,sha256=Uqx4nIZPVRxx7hRgm1M-1p2Sc7m3ObkIKWIC_ru9ObU,1115
64
- spectre_core/watchdog/post_processor.py,sha256=rZwe4u_T-7F7_q485PtUUZMAxPPec35zXV81MyERIjg,1673
65
- spectre_core/watchdog/library/__init__.py,sha256=vEwAnAV-sv7WcNYOdnjr1JVqZYr29Wr2cv01eoxwdmg,282
66
- spectre_core/watchdog/library/fixed/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
- spectre_core/watchdog/library/fixed/event_handler.py,sha256=yWWS80LukB-cTrKBsF4-pRvw2obkX2MzQ5ZGytOtmAg,1387
68
- spectre_core/watchdog/library/sweep/event_handler.py,sha256=wDISZiQXBeqLDPxgEMo0a2QAXqQVOO7fng3yhZWSR74,2188
69
- spectre_core/web_fetch/callisto.py,sha256=874osjbp61qFwRgV584fpSp7E-xz8g1FEelbNBKhCsw,3632
70
- spectre_core-0.0.8.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
71
- spectre_core-0.0.8.dist-info/METADATA,sha256=ltPXaVWUSk8ggdxRsRN9RLj3SIjRvNjGueNoIk3cst4,42150
72
- spectre_core-0.0.8.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
73
- spectre_core-0.0.8.dist-info/top_level.txt,sha256=-UsyjpFohXgZpgcZ9QbVeXhsIyF3Am8RxNFNDV_Ta2Y,13
74
- spectre_core-0.0.8.dist-info/RECORD,,
69
+ spectre_core/web_fetch/callisto.py,sha256=yW0NkmHqfUf2uYgJjSRxou6e65_mYciPhHwQIvKLK_w,3630
70
+ spectre_core-0.0.9.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
71
+ spectre_core-0.0.9.dist-info/METADATA,sha256=15Uh828ojJg6w-HCrIv7nka9wT_eVhv9svE33hmHI7k,42150
72
+ spectre_core-0.0.9.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
73
+ spectre_core-0.0.9.dist-info/top_level.txt,sha256=-UsyjpFohXgZpgcZ9QbVeXhsIyF3Am8RxNFNDV_Ta2Y,13
74
+ spectre_core-0.0.9.dist-info/RECORD,,
@@ -1,105 +0,0 @@
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
@@ -1,50 +0,0 @@
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 PostProcessor:
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 post processor...")
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
-