spectre-core 0.0.9__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 (106) 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} +48 -48
  30. spectre_core/plotting/_panels.py +234 -0
  31. spectre_core/post_processing/__init__.py +10 -2
  32. spectre_core/post_processing/_base.py +119 -0
  33. spectre_core/post_processing/{factory.py → _factory.py} +7 -6
  34. spectre_core/post_processing/{post_processor.py → _post_processor.py} +3 -3
  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/{post_processing/library → wgetting}/__init__.py +4 -5
  53. spectre_core/wgetting/_callisto.py +155 -0
  54. {spectre_core-0.0.9.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/post_processing/base.py +0 -132
  74. spectre_core/post_processing/library/fixed/__init__.py +0 -0
  75. spectre_core/post_processing/library/fixed/event_handler.py +0 -40
  76. spectre_core/post_processing/library/sweep/event_handler.py +0 -54
  77. spectre_core/receivers/base.py +0 -422
  78. spectre_core/receivers/library/__init__.py +0 -7
  79. spectre_core/receivers/library/rsp1a/__init__.py +0 -0
  80. spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
  81. spectre_core/receivers/library/rsp1a/gr/fixed.py +0 -104
  82. spectre_core/receivers/library/rsp1a/gr/sweep.py +0 -129
  83. spectre_core/receivers/library/rsp1a/receiver.py +0 -68
  84. spectre_core/receivers/library/rspduo/__init__.py +0 -0
  85. spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
  86. spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +0 -114
  87. spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +0 -131
  88. spectre_core/receivers/library/rspduo/gr/tuner_2_fixed.py +0 -120
  89. spectre_core/receivers/library/rspduo/gr/tuner_2_sweep.py +0 -119
  90. spectre_core/receivers/library/rspduo/receiver.py +0 -97
  91. spectre_core/receivers/library/test/__init__.py +0 -0
  92. spectre_core/receivers/library/test/gr/__init__.py +0 -0
  93. spectre_core/receivers/library/test/gr/cosine_signal_1.py +0 -83
  94. spectre_core/receivers/library/test/gr/tagged_staircase.py +0 -93
  95. spectre_core/receivers/library/test/receiver.py +0 -203
  96. spectre_core/receivers/validators.py +0 -231
  97. spectre_core/web_fetch/callisto.py +0 -101
  98. spectre_core-0.0.9.dist-info/RECORD +0 -74
  99. /spectre_core/chunks/{chunk_register.py → _register.py} +0 -0
  100. /spectre_core/post_processing/{event_handler_register.py → _register.py} +0 -0
  101. /spectre_core/receivers/{receiver_register.py → _register.py} +0 -0
  102. /spectre_core/{chunks/library/callisto/__init__.py → receivers/gr/_rspduo.py} +0 -0
  103. /spectre_core/{chunks/library/fixed/__init__.py → receivers/library/_rspduo.py} +0 -0
  104. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/LICENSE +0 -0
  105. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/WHEEL +0 -0
  106. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/top_level.txt +0 -0
@@ -3,25 +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
26
  def __init__(self,
19
- time_type: str = "seconds",
27
+ panel_format: PanelFormat = DEFAULT_FORMAT,
28
+ time_type: str = TimeTypes.SECONDS,
20
29
  figsize: Tuple[int, int] = (10, 10)):
30
+ self._panel_format = panel_format
21
31
  self._time_type = time_type
22
32
  self._figsize = figsize
23
- self._panels: List[BasePanel] = []
24
- self._superimposed_panels: List[BasePanel] = []
33
+
34
+ self._panels: list[BasePanel] = []
35
+ self._superimposed_panels: list[BasePanel] = []
25
36
  self._fig: Optional[Figure] = None
26
37
  self._axs: Optional[np.ndarray[Axes]] = None
27
38
 
@@ -32,7 +43,7 @@ class PanelStack:
32
43
 
33
44
 
34
45
  @property
35
- def panels(self) -> List[BasePanel]:
46
+ def panels(self) -> list[BasePanel]:
36
47
  return sorted(self._panels, key=lambda panel: panel.x_axis_type)
37
48
 
38
49
 
@@ -52,50 +63,41 @@ class PanelStack:
52
63
 
53
64
 
54
65
  def add_panel(self,
55
- panel_name: str,
56
- spectrogram: Spectrogram,
57
- *args,
58
- identifier: Optional[str] = None,
59
- **kwargs) -> None:
60
- panel = get_panel(panel_name,
61
- spectrogram,
62
- self._time_type,
63
- *args,
64
- **kwargs)
66
+ panel: BasePanel,
67
+ identifier: Optional[str] = None) -> None:
68
+ panel.panel_format = self._panel_format
69
+ panel.time_type = self._time_type
65
70
  if identifier:
66
71
  panel.identifier = identifier
67
72
  self._panels.append(panel)
68
73
 
69
74
 
70
75
  def superimpose_panel(self,
71
- panel_name: str,
72
- spectrogram: Spectrogram,
73
- *args,
74
- identifier: Optional[str] = None,
75
- **kwargs) -> None:
76
- panel = get_panel(panel_name,
77
- spectrogram,
78
- self._time_type,
79
- *args,
80
- **kwargs)
76
+ panel: BasePanel,
77
+ identifier: Optional[str] = None) -> None:
81
78
  if identifier:
82
79
  panel.identifier = identifier
80
+ panel.panel_format = self._panel_format
83
81
  self._superimposed_panels.append(panel)
84
82
 
85
83
 
86
84
  def _init_plot_style(self) -> None:
87
- plt.style.use(DEFAULT_FORMATS.style)
88
- plt.rc('font', size=DEFAULT_FORMATS.small_size)
89
- plt.rc('axes', titlesize=DEFAULT_FORMATS.medium_size,
90
- labelsize=DEFAULT_FORMATS.medium_size)
91
- plt.rc('xtick', labelsize=DEFAULT_FORMATS.small_size)
92
- plt.rc('ytick', labelsize=DEFAULT_FORMATS.small_size)
93
- plt.rc('legend', fontsize=DEFAULT_FORMATS.small_size)
94
- 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)
95
94
 
96
95
 
97
96
  def _create_figure_and_axes(self) -> None:
98
- 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")
99
101
 
100
102
 
101
103
  def _assign_axes(self) -> None:
@@ -109,10 +111,10 @@ class PanelStack:
109
111
  shared_axes[panel.x_axis_type] = panel.ax
110
112
 
111
113
 
112
- def _overlay_cuts(self, cuts_panel: CutsPanel) -> None:
114
+ def _overlay_cuts(self, cuts_panel: BasePanel) -> None:
113
115
  """Given a cuts panel, finds any corresponding spectrogram panels and adds the appropriate overlay"""
114
116
  for panel in self.panels:
115
- 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)
116
118
  if is_corresponding_panel:
117
119
  panel.overlay_cuts(cuts_panel)
118
120
 
@@ -120,10 +122,10 @@ class PanelStack:
120
122
  def _overlay_superimposed_panels(self) -> None:
121
123
  for super_panel in self._superimposed_panels:
122
124
  for panel in self._panels:
123
- if panel.name == super_panel.name and (panel.identifier == super_panel.identifier): # find the matching panels via panel names
124
- 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
125
127
  super_panel.draw()
126
- if isinstance(super_panel, CutsPanel):
128
+ if _is_cuts_panel(super_panel):
127
129
  self._overlay_cuts(super_panel)
128
130
 
129
131
 
@@ -139,9 +141,7 @@ class PanelStack:
139
141
  panel.annotate_x_axis()
140
142
  else:
141
143
  panel.hide_x_axis_labels()
142
- if isinstance(panel, CutsPanel):
143
- self._overlay_cuts(panel)
144
-
145
-
144
+ if _is_cuts_panel(panel):
145
+ self._overlay_cuts(panel)
146
146
  self._overlay_superimposed_panels()
147
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
@@ -2,5 +2,13 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- # dynamically import all event handlers
6
- import spectre_core.post_processing.library
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
@@ -2,12 +2,13 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- from spectre_core.post_processing.event_handler_register import event_handler_map
6
- from spectre_core.post_processing.base import BaseEventHandler
7
- from spectre_core.file_handlers.configs import CaptureConfig
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
8
  from spectre_core.exceptions import EventHandlerNotFoundError
9
9
 
10
- def get_event_handler(event_handler_key: str) -> BaseEventHandler:
10
+
11
+ def _get_event_handler(event_handler_key: str) -> BaseEventHandler:
11
12
  EventHandler = event_handler_map.get(event_handler_key)
12
13
  if EventHandler is None:
13
14
  valid_event_handler_keys = list(event_handler_map.keys())
@@ -18,5 +19,5 @@ def get_event_handler(event_handler_key: str) -> BaseEventHandler:
18
19
 
19
20
  def get_event_handler_from_tag(tag: str) -> BaseEventHandler:
20
21
  capture_config = CaptureConfig(tag)
21
- event_handler_key = capture_config.get('event_handler_key')
22
- return get_event_handler(event_handler_key)
22
+ event_handler_key = capture_config.get_parameter_value(PNames.EVENT_HANDLER_KEY)
23
+ return _get_event_handler(event_handler_key)
@@ -8,8 +8,8 @@ _LOGGER = getLogger(__name__)
8
8
  from watchdog.observers import Observer
9
9
  from watchdog.events import FileCreatedEvent
10
10
 
11
- from spectre_core.post_processing.factory import get_event_handler_from_tag
12
- from spectre_core.cfg import CHUNKS_DIR_PATH
11
+ from ._factory import get_event_handler_from_tag
12
+ from spectre_core.config import get_chunks_dir_path
13
13
 
14
14
  class PostProcessor:
15
15
  def __init__(self,
@@ -24,7 +24,7 @@ class PostProcessor:
24
24
  def start(self):
25
25
  """Start an observer to process newly created files in the chunks directory"""
26
26
  self._observer.schedule(self._event_handler,
27
- CHUNKS_DIR_PATH,
27
+ get_chunks_dir_path(),
28
28
  recursive=True,
29
29
  event_filter=[FileCreatedEvent])
30
30