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.
- spectre_core/__init__.py +0 -3
- spectre_core/_file_io/__init__.py +15 -0
- spectre_core/_file_io/file_handlers.py +128 -0
- spectre_core/capture_configs/__init__.py +29 -0
- spectre_core/capture_configs/_capture_config.py +85 -0
- spectre_core/capture_configs/_capture_templates.py +222 -0
- spectre_core/capture_configs/_parameters.py +110 -0
- spectre_core/capture_configs/_pconstraints.py +82 -0
- spectre_core/capture_configs/_ptemplates.py +450 -0
- spectre_core/capture_configs/_pvalidators.py +173 -0
- spectre_core/chunks/__init__.py +17 -201
- spectre_core/chunks/{base.py → _base.py} +15 -60
- spectre_core/chunks/_chunks.py +200 -0
- spectre_core/chunks/{factory.py → _factory.py} +6 -7
- spectre_core/chunks/library/{callisto/chunk.py → _callisto.py} +4 -7
- spectre_core/chunks/library/{fixed/chunk.py → _fixed_center_frequency.py} +7 -64
- spectre_core/chunks/library/_swept_center_frequency.py +103 -0
- spectre_core/config/__init__.py +20 -0
- spectre_core/config/_paths.py +77 -0
- spectre_core/config/_time_formats.py +15 -0
- spectre_core/exceptions.py +4 -5
- spectre_core/logging/__init__.py +11 -0
- spectre_core/logging/_configure.py +35 -0
- spectre_core/logging/_decorators.py +19 -0
- spectre_core/{logging.py → logging/_log_handlers.py} +13 -58
- spectre_core/plotting/__init__.py +7 -1
- spectre_core/plotting/{base.py → _base.py} +40 -20
- spectre_core/plotting/_format.py +18 -0
- spectre_core/plotting/{panel_stack.py → _panel_stack.py} +50 -48
- spectre_core/plotting/_panels.py +234 -0
- spectre_core/post_processing/__init__.py +14 -0
- spectre_core/post_processing/_base.py +119 -0
- spectre_core/post_processing/_factory.py +23 -0
- spectre_core/post_processing/_post_processor.py +40 -0
- spectre_core/post_processing/library/_fixed_center_frequency.py +115 -0
- spectre_core/post_processing/library/_swept_center_frequency.py +382 -0
- spectre_core/receivers/__init__.py +12 -2
- spectre_core/receivers/_base.py +352 -0
- spectre_core/receivers/{factory.py → _factory.py} +2 -2
- spectre_core/receivers/_spec_names.py +20 -0
- spectre_core/receivers/gr/__init__.py +3 -0
- spectre_core/receivers/gr/_base.py +33 -0
- spectre_core/receivers/gr/_rsp1a.py +158 -0
- spectre_core/receivers/gr/_test.py +123 -0
- spectre_core/receivers/library/_rsp1a.py +61 -0
- spectre_core/receivers/library/_test.py +221 -0
- spectre_core/spectrograms/__init__.py +18 -0
- spectre_core/spectrograms/{analytical.py → _analytical.py} +29 -27
- spectre_core/spectrograms/{array_operations.py → _array_operations.py} +47 -1
- spectre_core/spectrograms/{spectrogram.py → _spectrogram.py} +62 -35
- spectre_core/spectrograms/{transform.py → _transform.py} +76 -89
- spectre_core/{receivers/library → wgetting}/__init__.py +4 -2
- spectre_core/wgetting/_callisto.py +155 -0
- {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/METADATA +1 -1
- spectre_core-0.0.10.dist-info/RECORD +63 -0
- spectre_core/cfg.py +0 -116
- spectre_core/chunks/library/__init__.py +0 -8
- spectre_core/chunks/library/sweep/__init__.py +0 -0
- spectre_core/chunks/library/sweep/chunk.py +0 -400
- spectre_core/dynamic_imports.py +0 -22
- spectre_core/file_handlers/base.py +0 -68
- spectre_core/file_handlers/configs.py +0 -271
- spectre_core/file_handlers/json.py +0 -40
- spectre_core/file_handlers/text.py +0 -21
- spectre_core/plotting/factory.py +0 -26
- spectre_core/plotting/format.py +0 -19
- spectre_core/plotting/library/__init__.py +0 -7
- spectre_core/plotting/library/frequency_cuts/panel.py +0 -74
- spectre_core/plotting/library/integral_over_frequency/panel.py +0 -34
- spectre_core/plotting/library/spectrogram/panel.py +0 -92
- spectre_core/plotting/library/time_cuts/panel.py +0 -77
- spectre_core/plotting/panel_register.py +0 -13
- spectre_core/receivers/base.py +0 -415
- spectre_core/receivers/library/rsp1a/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/fixed.py +0 -104
- spectre_core/receivers/library/rsp1a/gr/sweep.py +0 -129
- spectre_core/receivers/library/rsp1a/receiver.py +0 -68
- spectre_core/receivers/library/rspduo/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +0 -114
- spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +0 -131
- spectre_core/receivers/library/rspduo/gr/tuner_2_fixed.py +0 -120
- spectre_core/receivers/library/rspduo/gr/tuner_2_sweep.py +0 -119
- spectre_core/receivers/library/rspduo/receiver.py +0 -97
- spectre_core/receivers/library/test/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/cosine_signal_1.py +0 -83
- spectre_core/receivers/library/test/gr/tagged_staircase.py +0 -93
- spectre_core/receivers/library/test/receiver.py +0 -178
- spectre_core/receivers/validators.py +0 -193
- spectre_core/watchdog/__init__.py +0 -6
- spectre_core/watchdog/base.py +0 -105
- spectre_core/watchdog/factory.py +0 -22
- spectre_core/watchdog/library/__init__.py +0 -10
- spectre_core/watchdog/library/fixed/__init__.py +0 -0
- spectre_core/watchdog/library/fixed/event_handler.py +0 -41
- spectre_core/watchdog/library/sweep/event_handler.py +0 -55
- spectre_core/watchdog/post_processor.py +0 -50
- spectre_core/web_fetch/callisto.py +0 -101
- spectre_core-0.0.8.dist-info/RECORD +0 -74
- /spectre_core/chunks/{chunk_register.py → _register.py} +0 -0
- /spectre_core/{watchdog/event_handler_register.py → post_processing/_register.py} +0 -0
- /spectre_core/receivers/{receiver_register.py → _register.py} +0 -0
- /spectre_core/{chunks/library/callisto/__init__.py → receivers/gr/_rspduo.py} +0 -0
- /spectre_core/{chunks/library/fixed/__init__.py → receivers/library/_rspduo.py} +0 -0
- {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/LICENSE +0 -0
- {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/WHEEL +0 -0
- {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
|
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
|
12
|
-
from
|
13
|
-
from
|
14
|
-
from
|
15
|
-
|
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,
|
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
|
-
|
22
|
-
self.
|
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) ->
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
70
|
-
|
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(
|
86
|
-
|
87
|
-
plt.rc('
|
88
|
-
|
89
|
-
|
90
|
-
plt.rc('
|
91
|
-
plt.rc('
|
92
|
-
plt.rc('
|
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,
|
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:
|
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 =
|
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):
|
122
|
-
super_panel.ax, super_panel.fig = panel.ax, self._fig
|
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
|
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
|
141
|
-
|
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
|
+
|