spectre-core 0.0.8__py3-none-any.whl → 0.0.10__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 (109) hide show
  1. spectre_core/__init__.py +0 -3
  2. spectre_core/_file_io/__init__.py +15 -0
  3. spectre_core/_file_io/file_handlers.py +128 -0
  4. spectre_core/capture_configs/__init__.py +29 -0
  5. spectre_core/capture_configs/_capture_config.py +85 -0
  6. spectre_core/capture_configs/_capture_templates.py +222 -0
  7. spectre_core/capture_configs/_parameters.py +110 -0
  8. spectre_core/capture_configs/_pconstraints.py +82 -0
  9. spectre_core/capture_configs/_ptemplates.py +450 -0
  10. spectre_core/capture_configs/_pvalidators.py +173 -0
  11. spectre_core/chunks/__init__.py +17 -201
  12. spectre_core/chunks/{base.py → _base.py} +15 -60
  13. spectre_core/chunks/_chunks.py +200 -0
  14. spectre_core/chunks/{factory.py → _factory.py} +6 -7
  15. spectre_core/chunks/library/{callisto/chunk.py → _callisto.py} +4 -7
  16. spectre_core/chunks/library/{fixed/chunk.py → _fixed_center_frequency.py} +7 -64
  17. spectre_core/chunks/library/_swept_center_frequency.py +103 -0
  18. spectre_core/config/__init__.py +20 -0
  19. spectre_core/config/_paths.py +77 -0
  20. spectre_core/config/_time_formats.py +15 -0
  21. spectre_core/exceptions.py +4 -5
  22. spectre_core/logging/__init__.py +11 -0
  23. spectre_core/logging/_configure.py +35 -0
  24. spectre_core/logging/_decorators.py +19 -0
  25. spectre_core/{logging.py → logging/_log_handlers.py} +13 -58
  26. spectre_core/plotting/__init__.py +7 -1
  27. spectre_core/plotting/{base.py → _base.py} +40 -20
  28. spectre_core/plotting/_format.py +18 -0
  29. spectre_core/plotting/{panel_stack.py → _panel_stack.py} +50 -48
  30. spectre_core/plotting/_panels.py +234 -0
  31. spectre_core/post_processing/__init__.py +14 -0
  32. spectre_core/post_processing/_base.py +119 -0
  33. spectre_core/post_processing/_factory.py +23 -0
  34. spectre_core/post_processing/_post_processor.py +40 -0
  35. spectre_core/post_processing/library/_fixed_center_frequency.py +115 -0
  36. spectre_core/post_processing/library/_swept_center_frequency.py +382 -0
  37. spectre_core/receivers/__init__.py +12 -2
  38. spectre_core/receivers/_base.py +352 -0
  39. spectre_core/receivers/{factory.py → _factory.py} +2 -2
  40. spectre_core/receivers/_spec_names.py +20 -0
  41. spectre_core/receivers/gr/__init__.py +3 -0
  42. spectre_core/receivers/gr/_base.py +33 -0
  43. spectre_core/receivers/gr/_rsp1a.py +158 -0
  44. spectre_core/receivers/gr/_test.py +123 -0
  45. spectre_core/receivers/library/_rsp1a.py +61 -0
  46. spectre_core/receivers/library/_test.py +221 -0
  47. spectre_core/spectrograms/__init__.py +18 -0
  48. spectre_core/spectrograms/{analytical.py → _analytical.py} +29 -27
  49. spectre_core/spectrograms/{array_operations.py → _array_operations.py} +47 -1
  50. spectre_core/spectrograms/{spectrogram.py → _spectrogram.py} +62 -35
  51. spectre_core/spectrograms/{transform.py → _transform.py} +76 -89
  52. spectre_core/{receivers/library → wgetting}/__init__.py +4 -2
  53. spectre_core/wgetting/_callisto.py +155 -0
  54. {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/METADATA +1 -1
  55. spectre_core-0.0.10.dist-info/RECORD +63 -0
  56. spectre_core/cfg.py +0 -116
  57. spectre_core/chunks/library/__init__.py +0 -8
  58. spectre_core/chunks/library/sweep/__init__.py +0 -0
  59. spectre_core/chunks/library/sweep/chunk.py +0 -400
  60. spectre_core/dynamic_imports.py +0 -22
  61. spectre_core/file_handlers/base.py +0 -68
  62. spectre_core/file_handlers/configs.py +0 -271
  63. spectre_core/file_handlers/json.py +0 -40
  64. spectre_core/file_handlers/text.py +0 -21
  65. spectre_core/plotting/factory.py +0 -26
  66. spectre_core/plotting/format.py +0 -19
  67. spectre_core/plotting/library/__init__.py +0 -7
  68. spectre_core/plotting/library/frequency_cuts/panel.py +0 -74
  69. spectre_core/plotting/library/integral_over_frequency/panel.py +0 -34
  70. spectre_core/plotting/library/spectrogram/panel.py +0 -92
  71. spectre_core/plotting/library/time_cuts/panel.py +0 -77
  72. spectre_core/plotting/panel_register.py +0 -13
  73. spectre_core/receivers/base.py +0 -415
  74. spectre_core/receivers/library/rsp1a/__init__.py +0 -0
  75. spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
  76. spectre_core/receivers/library/rsp1a/gr/fixed.py +0 -104
  77. spectre_core/receivers/library/rsp1a/gr/sweep.py +0 -129
  78. spectre_core/receivers/library/rsp1a/receiver.py +0 -68
  79. spectre_core/receivers/library/rspduo/__init__.py +0 -0
  80. spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
  81. spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +0 -114
  82. spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +0 -131
  83. spectre_core/receivers/library/rspduo/gr/tuner_2_fixed.py +0 -120
  84. spectre_core/receivers/library/rspduo/gr/tuner_2_sweep.py +0 -119
  85. spectre_core/receivers/library/rspduo/receiver.py +0 -97
  86. spectre_core/receivers/library/test/__init__.py +0 -0
  87. spectre_core/receivers/library/test/gr/__init__.py +0 -0
  88. spectre_core/receivers/library/test/gr/cosine_signal_1.py +0 -83
  89. spectre_core/receivers/library/test/gr/tagged_staircase.py +0 -93
  90. spectre_core/receivers/library/test/receiver.py +0 -178
  91. spectre_core/receivers/validators.py +0 -193
  92. spectre_core/watchdog/__init__.py +0 -6
  93. spectre_core/watchdog/base.py +0 -105
  94. spectre_core/watchdog/factory.py +0 -22
  95. spectre_core/watchdog/library/__init__.py +0 -10
  96. spectre_core/watchdog/library/fixed/__init__.py +0 -0
  97. spectre_core/watchdog/library/fixed/event_handler.py +0 -41
  98. spectre_core/watchdog/library/sweep/event_handler.py +0 -55
  99. spectre_core/watchdog/post_processor.py +0 -50
  100. spectre_core/web_fetch/callisto.py +0 -101
  101. spectre_core-0.0.8.dist-info/RECORD +0 -74
  102. /spectre_core/chunks/{chunk_register.py → _register.py} +0 -0
  103. /spectre_core/{watchdog/event_handler_register.py → post_processing/_register.py} +0 -0
  104. /spectre_core/receivers/{receiver_register.py → _register.py} +0 -0
  105. /spectre_core/{chunks/library/callisto/__init__.py → receivers/gr/_rspduo.py} +0 -0
  106. /spectre_core/{chunks/library/fixed/__init__.py → receivers/library/_rspduo.py} +0 -0
  107. {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/LICENSE +0 -0
  108. {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/WHEEL +0 -0
  109. {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/top_level.txt +0 -0
@@ -3,23 +3,36 @@
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  import numpy as np
6
- from typing import List, Optional, Tuple
6
+ from typing import Optional, Tuple
7
7
  import matplotlib.pyplot as plt
8
8
  from matplotlib.figure import Figure
9
9
  from matplotlib.axes import Axes
10
10
 
11
- from spectre_core.spectrograms.spectrogram import Spectrogram
12
- from spectre_core.plotting.base import BasePanel, CutsPanel
13
- from spectre_core.plotting.format import DEFAULT_FORMATS
14
- from spectre_core.plotting.factory import get_panel
15
- from spectre_core.plotting.library.spectrogram.panel import Panel as SpectrogramPanel
11
+ from spectre_core.spectrograms import TimeTypes
12
+ from ._base import BasePanel
13
+ from ._format import PanelFormat, DEFAULT_FORMAT
14
+ from ._panels import PanelNames
15
+
16
+
17
+ def _is_cuts_panel(panel: BasePanel) -> bool:
18
+ return (panel.name == PanelNames.FREQUENCY_CUTS or panel.name == PanelNames.TIME_CUTS)
19
+
20
+
21
+ def _is_spectrogram_panel(panel: BasePanel) -> bool:
22
+ return (panel.name == PanelNames.SPECTROGRAM)
23
+
16
24
 
17
25
  class PanelStack:
18
- def __init__(self, time_type: str, figsize: Tuple[int, int] = (10, 10)):
26
+ def __init__(self,
27
+ panel_format: PanelFormat = DEFAULT_FORMAT,
28
+ time_type: str = TimeTypes.SECONDS,
29
+ figsize: Tuple[int, int] = (10, 10)):
30
+ self._panel_format = panel_format
19
31
  self._time_type = time_type
20
32
  self._figsize = figsize
21
- self._panels: List[BasePanel] = []
22
- self._superimposed_panels: List[BasePanel] = []
33
+
34
+ self._panels: list[BasePanel] = []
35
+ self._superimposed_panels: list[BasePanel] = []
23
36
  self._fig: Optional[Figure] = None
24
37
  self._axs: Optional[np.ndarray[Axes]] = None
25
38
 
@@ -30,7 +43,7 @@ class PanelStack:
30
43
 
31
44
 
32
45
  @property
33
- def panels(self) -> List[BasePanel]:
46
+ def panels(self) -> list[BasePanel]:
34
47
  return sorted(self._panels, key=lambda panel: panel.x_axis_type)
35
48
 
36
49
 
@@ -50,50 +63,41 @@ class PanelStack:
50
63
 
51
64
 
52
65
  def add_panel(self,
53
- panel_name: str,
54
- spectrogram: Spectrogram,
55
- *args,
56
- identifier: Optional[str] = None,
57
- **kwargs) -> None:
58
- panel = get_panel(panel_name,
59
- spectrogram,
60
- self._time_type,
61
- *args,
62
- **kwargs)
66
+ panel: BasePanel,
67
+ identifier: Optional[str] = None) -> None:
68
+ panel.panel_format = self._panel_format
69
+ panel.time_type = self._time_type
63
70
  if identifier:
64
71
  panel.identifier = identifier
65
72
  self._panels.append(panel)
66
73
 
67
74
 
68
75
  def superimpose_panel(self,
69
- panel_name: str,
70
- spectrogram: Spectrogram,
71
- *args,
72
- identifier: Optional[str] = None,
73
- **kwargs) -> None:
74
- panel = get_panel(panel_name,
75
- spectrogram,
76
- self._time_type,
77
- *args,
78
- **kwargs)
76
+ panel: BasePanel,
77
+ identifier: Optional[str] = None) -> None:
79
78
  if identifier:
80
79
  panel.identifier = identifier
80
+ panel.panel_format = self._panel_format
81
81
  self._superimposed_panels.append(panel)
82
82
 
83
83
 
84
84
  def _init_plot_style(self) -> None:
85
- plt.style.use(DEFAULT_FORMATS.style)
86
- plt.rc('font', size=DEFAULT_FORMATS.small_size)
87
- plt.rc('axes', titlesize=DEFAULT_FORMATS.medium_size,
88
- labelsize=DEFAULT_FORMATS.medium_size)
89
- plt.rc('xtick', labelsize=DEFAULT_FORMATS.small_size)
90
- plt.rc('ytick', labelsize=DEFAULT_FORMATS.small_size)
91
- plt.rc('legend', fontsize=DEFAULT_FORMATS.small_size)
92
- plt.rc('figure', titlesize=DEFAULT_FORMATS.large_size)
85
+ plt.style.use(self._panel_format.style)
86
+
87
+ plt.rc('font' , size=self._panel_format.small_size)
88
+ plt.rc('axes' , titlesize=self._panel_format.medium_size,
89
+ labelsize=self._panel_format.medium_size)
90
+ plt.rc('xtick' , labelsize=self._panel_format.small_size)
91
+ plt.rc('ytick' , labelsize=self._panel_format.small_size)
92
+ plt.rc('legend', fontsize=self._panel_format.small_size)
93
+ plt.rc('figure', titlesize=self._panel_format.large_size)
93
94
 
94
95
 
95
96
  def _create_figure_and_axes(self) -> None:
96
- self._fig, self._axs = plt.subplots(self.num_panels, 1, figsize=self._figsize, layout="constrained")
97
+ self._fig, self._axs = plt.subplots(self.num_panels,
98
+ 1,
99
+ figsize=self._figsize,
100
+ layout="constrained")
97
101
 
98
102
 
99
103
  def _assign_axes(self) -> None:
@@ -107,10 +111,10 @@ class PanelStack:
107
111
  shared_axes[panel.x_axis_type] = panel.ax
108
112
 
109
113
 
110
- def _overlay_cuts(self, cuts_panel: CutsPanel) -> None:
114
+ def _overlay_cuts(self, cuts_panel: BasePanel) -> None:
111
115
  """Given a cuts panel, finds any corresponding spectrogram panels and adds the appropriate overlay"""
112
116
  for panel in self.panels:
113
- is_corresponding_panel = isinstance(panel, SpectrogramPanel) and (panel.tag == cuts_panel.tag)
117
+ is_corresponding_panel = _is_spectrogram_panel(panel) and (panel.tag == cuts_panel.tag)
114
118
  if is_corresponding_panel:
115
119
  panel.overlay_cuts(cuts_panel)
116
120
 
@@ -118,10 +122,10 @@ class PanelStack:
118
122
  def _overlay_superimposed_panels(self) -> None:
119
123
  for super_panel in self._superimposed_panels:
120
124
  for panel in self._panels:
121
- if panel.name == super_panel.name and (panel.identifier == super_panel.identifier): # find the matching panels via panel names
122
- super_panel.ax, super_panel.fig = panel.ax, self._fig # and superimpose via axes sharing
125
+ if panel.name == super_panel.name and (panel.identifier == super_panel.identifier):
126
+ super_panel.ax, super_panel.fig = panel.ax, self._fig
123
127
  super_panel.draw()
124
- if isinstance(super_panel, CutsPanel):
128
+ if _is_cuts_panel(super_panel):
125
129
  self._overlay_cuts(super_panel)
126
130
 
127
131
 
@@ -137,9 +141,7 @@ class PanelStack:
137
141
  panel.annotate_x_axis()
138
142
  else:
139
143
  panel.hide_x_axis_labels()
140
- if isinstance(panel, CutsPanel):
141
- self._overlay_cuts(panel)
142
-
143
-
144
+ if _is_cuts_panel(panel):
145
+ self._overlay_cuts(panel)
144
146
  self._overlay_superimposed_panels()
145
147
  plt.show()
@@ -0,0 +1,234 @@
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 typing import Optional
6
+ from datetime import datetime
7
+ from dataclasses import dataclass
8
+ from warnings import warn
9
+
10
+ from matplotlib.colors import LogNorm
11
+ import numpy as np
12
+
13
+ from spectre_core.spectrograms import Spectrogram, FrequencyCut, TimeCut
14
+ from ._base import BasePanel, BaseSpectrumPanel, BaseTimeSeriesPanel
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class PanelNames:
19
+ SPECTROGRAM : str = "spectrogram"
20
+ FREQUENCY_CUTS : str = "frequency_cuts"
21
+ TIME_CUTS : str = "time_cuts"
22
+ INTEGRAL_OVER_FREQUENCY: str = "integral_over_frequency"
23
+
24
+
25
+ class _FrequencyCutsPanel(BaseSpectrumPanel):
26
+ def __init__(self,
27
+ spectrogram: Spectrogram,
28
+ *times: list[float | str],
29
+ dBb: bool = False,
30
+ peak_normalise: bool = False):
31
+ super().__init__(PanelNames.FREQUENCY_CUTS,
32
+ spectrogram)
33
+ self._times = times
34
+ self._dBb = dBb
35
+ self._peak_normalise = peak_normalise
36
+ # map each time cut to the corresponding FrequencyCut dataclass
37
+ self._frequency_cuts: Optional[dict[float | datetime, FrequencyCut]] = {}
38
+
39
+
40
+ @property
41
+ def frequency_cuts(self) -> dict[float | str, FrequencyCut]:
42
+ if not self._frequency_cuts:
43
+ for time in self._times:
44
+ frequency_cut = self._spectrogram.get_frequency_cut(time,
45
+ dBb = self._dBb,
46
+ peak_normalise = self._peak_normalise)
47
+ self._frequency_cuts[frequency_cut.time] = frequency_cut
48
+ return self._frequency_cuts
49
+
50
+
51
+ @property
52
+ def times(self) -> list[float | datetime]:
53
+ return list(self.frequency_cuts.keys())
54
+
55
+
56
+ def draw(self):
57
+ for time, color in self.bind_to_colors():
58
+ frequency_cut = self.frequency_cuts[time]
59
+ self.ax.step(self.frequencies*1e-6, # convert to MHz
60
+ frequency_cut.cut,
61
+ where='mid',
62
+ color = color)
63
+
64
+
65
+ def annotate_y_axis(self) -> None:
66
+ if self._dBb:
67
+ self.ax.set_ylabel('dBb')
68
+ elif self._peak_normalise:
69
+ return # no y-axis label
70
+ else:
71
+ self.ax.set_ylabel(f'{self._spectrogram.spectrum_type.capitalize()}')
72
+
73
+
74
+ def bind_to_colors(self):
75
+ return super().bind_to_colors(self.times, cmap = self.panel_format.cuts_cmap)
76
+
77
+
78
+ class _IntegralOverFrequencyPanel(BaseTimeSeriesPanel):
79
+ def __init__(self,
80
+ spectrogram: Spectrogram,
81
+ peak_normalise: bool = False,
82
+ background_subtract: bool = False):
83
+ super().__init__(PanelNames.INTEGRAL_OVER_FREQUENCY,
84
+ spectrogram)
85
+ self._peak_normalise = peak_normalise
86
+ self._background_subtract = background_subtract
87
+
88
+
89
+ def draw(self):
90
+ I = self._spectrogram.integrate_over_frequency(correct_background = self._background_subtract,
91
+ peak_normalise = self._peak_normalise)
92
+ self.ax.step(self.times, I, where="mid", color = self.panel_format.integral_color)
93
+
94
+
95
+ def annotate_y_axis(self):
96
+ # no y-axis label
97
+ return
98
+
99
+
100
+ class _TimeCutsPanel(BaseTimeSeriesPanel):
101
+ def __init__(self,
102
+ spectrogram: Spectrogram,
103
+ *frequencies: list[float],
104
+ dBb: bool = False,
105
+ peak_normalise: bool = False,
106
+ background_subtract: bool = False):
107
+ super().__init__(PanelNames.TIME_CUTS,
108
+ spectrogram)
109
+ self._frequencies = frequencies
110
+ self._dBb = dBb
111
+ self._peak_normalise = peak_normalise
112
+ self._background_subtract = background_subtract
113
+ # map each cut frequency to the corresponding TimeCut dataclass
114
+ self._time_cuts: Optional[dict[float, TimeCut]] = {}
115
+
116
+
117
+ @property
118
+ def time_cuts(self) -> dict[float, TimeCut]:
119
+ if not self._time_cuts:
120
+ for frequency in self._frequencies:
121
+ time_cut = self._spectrogram.get_time_cut(frequency,
122
+ dBb = self._dBb,
123
+ peak_normalise = self._peak_normalise,
124
+ correct_background = self._background_subtract,
125
+ return_time_type=self._time_type)
126
+ self._time_cuts[time_cut.frequency] = time_cut
127
+ return self._time_cuts
128
+
129
+
130
+ @property
131
+ def frequencies(self) -> list[float]:
132
+ return list(self.time_cuts.keys())
133
+
134
+
135
+ def draw(self):
136
+ for frequency, color in self.bind_to_colors():
137
+ time_cut = self.time_cuts[frequency]
138
+ self.ax.step(self.times,
139
+ time_cut.cut,
140
+ where='mid',
141
+ color = color)
142
+
143
+
144
+ def annotate_y_axis(self) -> None:
145
+ if self._dBb:
146
+ self.ax.set_ylabel('dBb')
147
+ elif self._peak_normalise:
148
+ return # no y-axis label
149
+ else:
150
+ self.ax.set_ylabel(f'{self._spectrogram.spectrum_type.capitalize()}')
151
+
152
+
153
+ def bind_to_colors(self):
154
+ return super().bind_to_colors(self.frequencies, cmap = self.panel_format.cuts_cmap)
155
+
156
+
157
+ class _SpectrogramPanel(BaseTimeSeriesPanel):
158
+ def __init__(self,
159
+ spectrogram: Spectrogram,
160
+ log_norm: bool = False,
161
+ dBb: bool = False,
162
+ vmin: float | None = -1,
163
+ vmax: float | None = 2):
164
+ super().__init__(PanelNames.SPECTROGRAM,
165
+ spectrogram)
166
+ self._log_norm = log_norm
167
+ self._dBb = dBb
168
+ self._vmin = vmin
169
+ self._vmax = vmax
170
+
171
+
172
+ def draw(self):
173
+ dynamic_spectra = self._spectrogram.dynamic_spectra_as_dBb if self._dBb else self._spectrogram.dynamic_spectra
174
+
175
+ norm = LogNorm(vmin=np.nanmin(dynamic_spectra[dynamic_spectra > 0]),
176
+ vmax=np.nanmax(dynamic_spectra)) if self._log_norm else None
177
+
178
+
179
+ if self._log_norm and (self._vmin or self._vmax):
180
+ warn("vmin/vmax will be ignored while using log_norm.")
181
+ self._vmin = None
182
+ self._vmax = None
183
+
184
+ # Plot the spectrogram with kwargs
185
+ pcm = self.ax.pcolormesh(self.times,
186
+ self._spectrogram.frequencies * 1e-6,
187
+ dynamic_spectra,
188
+ vmin=self._vmin,
189
+ vmax=self._vmax,
190
+ norm=norm,
191
+ cmap=self.panel_format.spectrogram_cmap)
192
+
193
+ # Add colorbar if dBb is used
194
+ if self._dBb:
195
+ cbar = self.fig.colorbar(pcm,
196
+ ax=self.ax,
197
+ ticks=np.linspace(self._vmin, self._vmax, 6, dtype=int))
198
+ cbar.set_label('dBb')
199
+
200
+
201
+ def annotate_y_axis(self) -> None:
202
+ self.ax.set_ylabel('Frequency [MHz]')
203
+ return
204
+
205
+
206
+ def overlay_cuts(self, cuts_panel: BasePanel) -> None:
207
+ if cuts_panel.name == PanelNames.TIME_CUTS:
208
+ self._overlay_time_cuts(cuts_panel)
209
+ elif cuts_panel.name == PanelNames.FREQUENCY_CUTS:
210
+ self._overlay_frequency_cuts(cuts_panel)
211
+
212
+
213
+ def _overlay_time_cuts(self, time_cuts_panel: _TimeCutsPanel) -> None:
214
+ for frequency, color in time_cuts_panel.bind_to_colors():
215
+ self.ax.axhline(frequency*1e-6, # convert to MHz
216
+ color = color,
217
+ linewidth=self.panel_format.line_width
218
+ )
219
+
220
+
221
+ def _overlay_frequency_cuts(self, frequency_cuts_panel: _FrequencyCutsPanel) -> None:
222
+ for time, color in frequency_cuts_panel.bind_to_colors():
223
+ self.ax.axvline(time,
224
+ color = color,
225
+ linewidth=self.panel_format.line_width
226
+ )
227
+
228
+
229
+ @dataclass(frozen=True)
230
+ class Panels:
231
+ TimeCuts = _TimeCutsPanel
232
+ FrequencyCuts = _FrequencyCutsPanel
233
+ Spectrogram = _SpectrogramPanel
234
+ IntegralOverFrequency = _IntegralOverFrequencyPanel
@@ -0,0 +1,14 @@
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
+ # event handler class decorators take effect on import
6
+ from .library._fixed_center_frequency import _EventHandler
7
+ from .library._swept_center_frequency import _EventHandler
8
+
9
+ from ._factory import get_event_handler_from_tag
10
+ from ._post_processor import PostProcessor
11
+
12
+ __all__ = [
13
+ "PostProcessor", "get_event_handler_from_tag"
14
+ ]
@@ -0,0 +1,119 @@
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 typing import Optional
9
+ from abc import ABC, abstractmethod
10
+ from scipy.signal import ShortTimeFFT, get_window
11
+
12
+ from watchdog.events import FileSystemEventHandler, FileCreatedEvent
13
+
14
+ from spectre_core.capture_configs import CaptureConfig, PNames
15
+ from spectre_core.chunks import BaseChunk, get_chunk_from_tag
16
+ from spectre_core.spectrograms import Spectrogram, join_spectrograms
17
+
18
+
19
+ def make_sft_instance(capture_config: CaptureConfig
20
+ ) -> ShortTimeFFT:
21
+ sample_rate = capture_config.get_parameter_value(PNames.SAMPLE_RATE)
22
+ window_hop = capture_config.get_parameter_value(PNames.WINDOW_HOP)
23
+ window_type = capture_config.get_parameter_value(PNames.WINDOW_TYPE)
24
+ window_size = capture_config.get_parameter_value(PNames.WINDOW_SIZE)
25
+ window = get_window(window_type,
26
+ window_size)
27
+ return ShortTimeFFT(window,
28
+ window_hop,
29
+ sample_rate,
30
+ fft_mode = "centered")
31
+
32
+
33
+ class BaseEventHandler(ABC, FileSystemEventHandler):
34
+ def __init__(self,
35
+ tag: str):
36
+ self._tag = tag
37
+
38
+ # the tag tells us 'what type' of data is stored in the files for each chunk
39
+ self._Chunk: BaseChunk = get_chunk_from_tag(tag)
40
+ # load the capture config corresponding to the tag
41
+ self._capture_config = CaptureConfig(tag)
42
+
43
+ # post processing is triggered by files with this extension
44
+ self._watch_extension = self._capture_config.get_parameter_value(PNames.WATCH_EXTENSION)
45
+
46
+ # store the next file to be processed (specifically, the absolute file path of the file)
47
+ self._queued_file: Optional[str] = None
48
+
49
+ # store batched spectrograms as they are created into a cache
50
+ # which is flushed periodically according to a user defined
51
+ # time range
52
+ self._cached_spectrogram: Optional[Spectrogram] = None
53
+
54
+
55
+ @abstractmethod
56
+ def process(self,
57
+ absolute_file_path: str) -> None:
58
+ """Process the file stored at the input absolute file path.
59
+
60
+ To be implemented by derived classes.
61
+ """
62
+
63
+ def on_created(self,
64
+ event: FileCreatedEvent):
65
+ """Process a newly created batch file, only once the next batch is created.
66
+
67
+ Since we assume that the batches are non-overlapping in time, this guarantees
68
+ we avoid post processing a file while it is being written to. Files are processed
69
+ sequentially, in the order they are created.
70
+ """
71
+
72
+ # the 'src_path' attribute holds the absolute path of the newly created file
73
+ absolute_file_path = event.src_path
74
+
75
+ # only 'notice' a file if it ends with the appropriate extension
76
+ # as defined in the capture config
77
+ if absolute_file_path.endswith(self._watch_extension):
78
+ _LOGGER.info(f"Noticed {absolute_file_path}")
79
+
80
+ # If there exists a queued file, try and process it
81
+ if self._queued_file is not None:
82
+ try:
83
+ self.process(self._queued_file)
84
+ except Exception:
85
+ _LOGGER.error(f"An error has occured while processing {self._queued_file}",
86
+ exc_info=True)
87
+ # flush any internally stored spectrogram on error to avoid lost data
88
+ self._flush_cache()
89
+ # re-raise the exception to the main thread
90
+ raise
91
+
92
+ # Queue the current file for processing next
93
+ _LOGGER.info(f"Queueing {absolute_file_path} for post processing")
94
+ self._queued_file = absolute_file_path
95
+
96
+
97
+ def _cache_spectrogram(self,
98
+ spectrogram: Spectrogram) -> None:
99
+ _LOGGER.info("Joining spectrogram")
100
+
101
+ if self._cached_spectrogram is None:
102
+ self._cached_spectrogram = spectrogram
103
+ else:
104
+ self._cached_spectrogram = join_spectrograms([self._cached_spectrogram, spectrogram])
105
+
106
+ # if the time range is not specified
107
+ time_range = self._capture_config.get_parameter_value(PNames.TIME_RANGE) or self._capture_config.get_parameter_value(PNames.BATCH_SIZE)
108
+
109
+ if self._cached_spectrogram.time_range >= time_range:
110
+ self._flush_cache()
111
+
112
+
113
+ def _flush_cache(self) -> None:
114
+ if self._cached_spectrogram:
115
+ _LOGGER.info(f"Flushing spectrogram to file with chunk start time "
116
+ f"'{self._cached_spectrogram.chunk_start_time}'")
117
+ self._cached_spectrogram.save()
118
+ _LOGGER.info("Flush successful, resetting spectrogram cache")
119
+ self._cached_spectrogram = None # reset the cache
@@ -0,0 +1,23 @@
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 ._register import event_handler_map
6
+ from spectre_core.post_processing._base import BaseEventHandler
7
+ from spectre_core.capture_configs import CaptureConfig, PNames
8
+ from spectre_core.exceptions import EventHandlerNotFoundError
9
+
10
+
11
+ def _get_event_handler(event_handler_key: str) -> BaseEventHandler:
12
+ EventHandler = event_handler_map.get(event_handler_key)
13
+ if EventHandler is None:
14
+ valid_event_handler_keys = list(event_handler_map.keys())
15
+ raise EventHandlerNotFoundError((f"No event handler found for the event handler key '{event_handler_key}'. "
16
+ f"Please specify one of the following event handler keys: {valid_event_handler_keys}"))
17
+ return EventHandler
18
+
19
+
20
+ def get_event_handler_from_tag(tag: str) -> BaseEventHandler:
21
+ capture_config = CaptureConfig(tag)
22
+ event_handler_key = capture_config.get_parameter_value(PNames.EVENT_HANDLER_KEY)
23
+ return _get_event_handler(event_handler_key)
@@ -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 ._factory import get_event_handler_from_tag
12
+ from spectre_core.config import get_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
+ get_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
+