spectre-core 0.0.9__py3-none-any.whl → 0.0.11__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 +171 -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} +48 -48
- spectre_core/plotting/_panels.py +234 -0
- spectre_core/post_processing/__init__.py +10 -2
- spectre_core/post_processing/_base.py +119 -0
- spectre_core/post_processing/{factory.py → _factory.py} +7 -6
- spectre_core/post_processing/{post_processor.py → _post_processor.py} +3 -3
- 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 +13 -2
- spectre_core/receivers/_base.py +180 -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/_rspduo.py +227 -0
- spectre_core/receivers/gr/_test.py +123 -0
- spectre_core/receivers/library/_rsp1a.py +61 -0
- spectre_core/receivers/library/_rspduo.py +69 -0
- spectre_core/receivers/library/_sdrplay_receiver.py +185 -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/{post_processing/library → wgetting}/__init__.py +4 -5
- spectre_core/wgetting/_callisto.py +155 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.11.dist-info}/METADATA +1 -1
- spectre_core-0.0.11.dist-info/RECORD +64 -0
- spectre_core/cfg.py +0 -116
- spectre_core/chunks/library/__init__.py +0 -8
- spectre_core/chunks/library/callisto/__init__.py +0 -0
- spectre_core/chunks/library/fixed/__init__.py +0 -0
- 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/post_processing/base.py +0 -132
- spectre_core/post_processing/library/fixed/__init__.py +0 -0
- spectre_core/post_processing/library/fixed/event_handler.py +0 -40
- spectre_core/post_processing/library/sweep/event_handler.py +0 -54
- spectre_core/receivers/base.py +0 -422
- spectre_core/receivers/library/__init__.py +0 -7
- 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 -203
- spectre_core/receivers/validators.py +0 -231
- spectre_core/web_fetch/callisto.py +0 -101
- spectre_core-0.0.9.dist-info/RECORD +0 -74
- /spectre_core/chunks/{chunk_register.py → _register.py} +0 -0
- /spectre_core/post_processing/{event_handler_register.py → _register.py} +0 -0
- /spectre_core/receivers/{receiver_register.py → _register.py} +0 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.11.dist-info}/LICENSE +0 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.11.dist-info}/WHEEL +0 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.11.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
|
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
26
|
def __init__(self,
|
19
|
-
|
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
|
-
|
24
|
-
self.
|
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) ->
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
72
|
-
|
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(
|
88
|
-
|
89
|
-
plt.rc('
|
90
|
-
|
91
|
-
|
92
|
-
plt.rc('
|
93
|
-
plt.rc('
|
94
|
-
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)
|
95
94
|
|
96
95
|
|
97
96
|
def _create_figure_and_axes(self) -> None:
|
98
|
-
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")
|
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:
|
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 =
|
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):
|
124
|
-
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
|
125
127
|
super_panel.draw()
|
126
|
-
if
|
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
|
143
|
-
|
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
|
-
#
|
6
|
-
|
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 0.0
|
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
|
6
|
-
from spectre_core.post_processing.
|
7
|
-
from spectre_core.
|
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
|
-
|
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.
|
22
|
-
return
|
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
|
12
|
-
from spectre_core.
|
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
|
-
|
27
|
+
get_chunks_dir_path(),
|
28
28
|
recursive=True,
|
29
29
|
event_filter=[FileCreatedEvent])
|
30
30
|
|