spectre-core 0.0.23__py3-none-any.whl → 0.0.24__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 CHANGED
@@ -0,0 +1,5 @@
1
+ # SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
2
+ # This file is part of SPECTRE
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ __version__ = "0.0.24"
@@ -5,6 +5,8 @@
5
5
 
6
6
  """General `spectre` package configurations."""
7
7
 
8
+ import os
9
+
8
10
  from ._paths import (
9
11
  get_spectre_data_dir_path,
10
12
  get_batches_dir_path,
@@ -14,6 +16,10 @@ from ._paths import (
14
16
  )
15
17
  from ._time_formats import TimeFormat
16
18
 
19
+ os.makedirs(get_batches_dir_path(), exist_ok=True)
20
+ os.makedirs(get_logs_dir_path(), exist_ok=True)
21
+ os.makedirs(get_configs_dir_path(), exist_ok=True)
22
+
17
23
  __all__ = [
18
24
  "get_spectre_data_dir_path",
19
25
  "get_batches_dir_path",
@@ -16,24 +16,17 @@ and creates three directories inside it:
16
16
  import os
17
17
  from typing import Optional
18
18
 
19
- _SPECTRE_DATA_DIR_PATH = os.environ.get("SPECTRE_DATA_DIR_PATH", "NOTSET")
20
- if _SPECTRE_DATA_DIR_PATH == "NOTSET":
21
- raise ValueError("The environment variable `SPECTRE_DATA_DIR_PATH` must be set.")
22
-
23
- _BATCHES_DIR_PATH = os.path.join(_SPECTRE_DATA_DIR_PATH, "batches")
24
- _LOGS_DIR_PATH = os.path.join(_SPECTRE_DATA_DIR_PATH, "logs")
25
- _CONFIGS_DIR_PATH = os.path.join(_SPECTRE_DATA_DIR_PATH, "configs")
26
-
27
- os.makedirs(_BATCHES_DIR_PATH, exist_ok=True)
28
- os.makedirs(_LOGS_DIR_PATH, exist_ok=True)
29
- os.makedirs(_CONFIGS_DIR_PATH, exist_ok=True)
30
-
31
19
 
32
20
  def get_spectre_data_dir_path() -> str:
33
21
  """The default ancestral path for all `spectre` file system data.
34
22
 
35
23
  :return: The value stored by the `SPECTRE_DATA_DIR_PATH` environment variable.
36
24
  """
25
+ _SPECTRE_DATA_DIR_PATH = os.environ.get("SPECTRE_DATA_DIR_PATH", "NOTSET")
26
+ if _SPECTRE_DATA_DIR_PATH == "NOTSET":
27
+ raise ValueError(
28
+ "The environment variable `SPECTRE_DATA_DIR_PATH` must be set."
29
+ )
37
30
  return _SPECTRE_DATA_DIR_PATH
38
31
 
39
32
 
@@ -80,7 +73,8 @@ def get_batches_dir_path(
80
73
  :param day: The numeric day. Defaults to None.
81
74
  :return: The directory path for batched data files, optionally with a date-based subdirectory.
82
75
  """
83
- return _get_date_based_dir_path(_BATCHES_DIR_PATH, year, month, day)
76
+ batches_dir_path = os.path.join(get_spectre_data_dir_path(), "batches")
77
+ return _get_date_based_dir_path(batches_dir_path, year, month, day)
84
78
 
85
79
 
86
80
  def get_logs_dir_path(
@@ -94,7 +88,8 @@ def get_logs_dir_path(
94
88
  :param day: The numeric day. Defaults to None.
95
89
  :return: The directory path for log files, optionally with a date-based subdirectory.
96
90
  """
97
- return _get_date_based_dir_path(_LOGS_DIR_PATH, year, month, day)
91
+ logs_dir_path = os.path.join(get_spectre_data_dir_path(), "logs")
92
+ return _get_date_based_dir_path(logs_dir_path, year, month, day)
98
93
 
99
94
 
100
95
  def get_configs_dir_path() -> str:
@@ -102,7 +97,7 @@ def get_configs_dir_path() -> str:
102
97
 
103
98
  :return: The directory path for configuration files.
104
99
  """
105
- return _CONFIGS_DIR_PATH
100
+ return os.path.join(get_spectre_data_dir_path(), "configs")
106
101
 
107
102
 
108
103
  def trim_spectre_data_dir_path(full_path: str) -> str:
@@ -116,7 +111,7 @@ def trim_spectre_data_dir_path(full_path: str) -> str:
116
111
  :param full_path: The full file path to be trimmed.
117
112
  :return: The relative path with `SPECTRE_DATA_DIR_PATH` removed.
118
113
  """
119
- return os.path.relpath(full_path, _SPECTRE_DATA_DIR_PATH)
114
+ return os.path.relpath(full_path, get_spectre_data_dir_path())
120
115
 
121
116
 
122
117
  def add_spectre_data_dir_path(rel_path: str) -> str:
@@ -129,4 +124,4 @@ def add_spectre_data_dir_path(rel_path: str) -> str:
129
124
  :param rel_path: The relative file path to be appended.
130
125
  :return: The full file path prefixed with `SPECTRE_DATA_DIR_PATH`.
131
126
  """
132
- return os.path.join(_SPECTRE_DATA_DIR_PATH, rel_path)
127
+ return os.path.join(get_spectre_data_dir_path(), rel_path)
@@ -5,6 +5,8 @@
5
5
  """An intuitive API for plotting spectrogram data."""
6
6
 
7
7
  from ._format import PanelFormat
8
+ from ._panel_names import PanelName
9
+ from ._base import BasePanel, BaseTimeSeriesPanel, XAxisType
8
10
  from ._panels import (
9
11
  SpectrogramPanel,
10
12
  FrequencyCutsPanel,
@@ -14,6 +16,10 @@ from ._panels import (
14
16
  from ._panel_stack import PanelStack
15
17
 
16
18
  __all__ = [
19
+ "BaseTimeSeriesPanel",
20
+ "PanelName",
21
+ "XAxisType",
22
+ "BasePanel",
17
23
  "PanelFormat",
18
24
  "PanelStack",
19
25
  "SpectrogramPanel",
@@ -20,7 +20,7 @@ from ._panel_names import PanelName
20
20
 
21
21
 
22
22
  class XAxisType(Enum):
23
- """The x-axis type for a panel.
23
+ """The xaxis type for a panel.
24
24
 
25
25
  Axes are shared in a stack between panels with common `XAxisType`.
26
26
 
@@ -37,23 +37,30 @@ class BasePanel(ABC):
37
37
 
38
38
  `BasePanel` instances are designed to be part of a `PanelStack`, where multiple
39
39
  panels contribute to a composite plot. Subclasses must implement methods to define
40
- how the panel is drawn and annotated, and specify its x-axis type.
40
+ how the panel is drawn and annotated, and specify its xaxis type.
41
41
  """
42
42
 
43
- def __init__(self, name: PanelName, spectrogram: Spectrogram) -> None:
43
+ def __init__(
44
+ self,
45
+ name: PanelName,
46
+ spectrogram: Spectrogram,
47
+ time_type: TimeType = TimeType.RELATIVE,
48
+ ) -> None:
44
49
  """Initialize an instance of `BasePanel`.
45
50
 
46
51
  :param name: The name of the panel.
47
52
  :param spectrogram: The spectrogram being visualised.
53
+ :param time_type: Indicates whether the times of each spectrum are relative to the first
54
+ spectrum in the spectrogram, or datetimes.
48
55
  """
49
56
  self._name = name
50
57
  self._spectrogram = spectrogram
58
+ self._time_type = time_type
51
59
 
52
- # internal attributes set by `PanelStack` during stacking.
53
- self._panel_format: Optional[PanelFormat] = None
54
- self._time_type: Optional[TimeType] = None
60
+ # These attributes should be set by instances of `PanelStack`.
55
61
  self._ax: Optional[Axes] = None
56
62
  self._fig: Optional[Figure] = None
63
+ self._panel_format: Optional[PanelFormat] = None
57
64
  self._identifier: Optional[str] = None
58
65
 
59
66
  @abstractmethod
@@ -62,16 +69,16 @@ class BasePanel(ABC):
62
69
 
63
70
  @abstractmethod
64
71
  def annotate_xaxis(self) -> None:
65
- """Modify the `ax` attribute to annotate the x-axis of the panel."""
72
+ """Modify the `ax` attribute to annotate the xaxis of the panel."""
66
73
 
67
74
  @abstractmethod
68
75
  def annotate_yaxis(self) -> None:
69
- """Modify the `ax` attribute to annotate the y-axis of the panel."""
76
+ """Modify the `ax` attribute to annotate the yaxis of the panel."""
70
77
 
71
78
  @property
72
79
  @abstractmethod
73
80
  def xaxis_type(self) -> XAxisType:
74
- """Specify the x-axis type for the panel."""
81
+ """Specify the xaxis type for the panel."""
75
82
 
76
83
  @property
77
84
  def spectrogram(self) -> Spectrogram:
@@ -84,17 +91,18 @@ class BasePanel(ABC):
84
91
  return self._spectrogram.tag
85
92
 
86
93
  @property
87
- def time_type(self) -> TimeType:
94
+ def name(self) -> PanelName:
95
+ """The name of the panel."""
96
+ return self._name
97
+
98
+ def get_time_type(self) -> TimeType:
88
99
  """The time type of the spectrogram.
89
100
 
90
101
  :raises ValueError: If the `time_type` has not been set.
91
102
  """
92
- if self._time_type is None:
93
- raise ValueError(f"`time_type` for the panel '{self.name}' must be set.")
94
103
  return self._time_type
95
104
 
96
- @time_type.setter
97
- def time_type(self, value: TimeType) -> None:
105
+ def set_time_type(self, value: TimeType) -> None:
98
106
  """Set the `TimeType` for the spectrogram.
99
107
 
100
108
  This controls how time is represented and annotated on the panel.
@@ -103,41 +111,35 @@ class BasePanel(ABC):
103
111
  """
104
112
  self._time_type = value
105
113
 
106
- @property
107
- def name(self) -> PanelName:
108
- """The name of the panel."""
109
- return self._name
110
-
111
- @property
112
- def panel_format(self) -> PanelFormat:
114
+ def get_panel_format(self) -> PanelFormat:
113
115
  """Retrieve the panel format, which controls the style of the panel.
114
116
 
115
117
  :raises ValueError: If the `panel_format` has not been set.
116
118
  """
117
119
  if self._panel_format is None:
118
- raise ValueError(f"`panel_format` for the panel '{self.name}' must be set.")
120
+ raise ValueError(f"`panel_format` must be set for the panel `{self.name}`")
119
121
  return self._panel_format
120
122
 
121
- @panel_format.setter
122
- def panel_format(self, value: PanelFormat) -> None:
123
+ def set_panel_format(self, value: PanelFormat) -> None:
123
124
  """Set the panel format to control the style of the panel.
124
125
 
125
126
  :param value: The `PanelFormat` to assign to the panel.
126
127
  """
127
128
  self._panel_format = value
128
129
 
129
- @property
130
- def ax(self) -> Axes:
131
- """The `Axes` object bound to this panel.
130
+ def _get_ax(self) -> Axes:
131
+ """Return the `Axes` object bound to this panel.
132
132
 
133
- :raises AttributeError: If the `Axes` object has not been set.
133
+ This method is protected to restrict direct access to `matplotlib` functionality,
134
+ promoting encapsulation, promoting encapsulation.
135
+
136
+ :raises ValueError: If the `Axes` object has not been set.
134
137
  """
135
138
  if self._ax is None:
136
- raise AttributeError(f"`ax` must be set for the panel `{self.name}`")
139
+ raise ValueError(f"`ax` must be set for the panel `{self.name}`")
137
140
  return self._ax
138
141
 
139
- @ax.setter
140
- def ax(self, value: Axes) -> None:
142
+ def set_ax(self, value: Axes) -> None:
141
143
  """Assign a Matplotlib `Axes` object to this panel.
142
144
 
143
145
  This `Axes` will be used for drawing and annotations.
@@ -146,19 +148,19 @@ class BasePanel(ABC):
146
148
  """
147
149
  self._ax = value
148
150
 
149
- @property
150
- def fig(self) -> Figure:
151
- """
152
- The `Figure` object bound to this panel.
151
+ def _get_fig(self) -> Figure:
152
+ """Return the `Figure` object bound to this panel.
153
+
154
+ This method is protected to restrict direct access to `matplotlib` functionality,
155
+ promoting encapsulation, promoting encapsulation.
153
156
 
154
- :raises AttributeError: If the `Figure` object has not been set.
157
+ :raises ValueError: If the `Figure` object has not been set.
155
158
  """
156
159
  if self._fig is None:
157
- raise AttributeError(f"`fig` must be set for the panel `{self.name}`")
160
+ raise ValueError(f"`fig` must be set for the panel `{self.name}`")
158
161
  return self._fig
159
162
 
160
- @fig.setter
161
- def fig(self, value: Figure) -> None:
163
+ def set_fig(self, value: Figure) -> None:
162
164
  """
163
165
  Assign a Matplotlib `Figure` object to this panel.
164
166
 
@@ -168,8 +170,7 @@ class BasePanel(ABC):
168
170
  """
169
171
  self._fig = value
170
172
 
171
- @property
172
- def identifier(self) -> Optional[str]:
173
+ def get_identifier(self) -> Optional[str]:
173
174
  """Optional identifier for the panel.
174
175
 
175
176
  This identifier can be used to distinguish panels or aid in superimposing
@@ -177,8 +178,7 @@ class BasePanel(ABC):
177
178
  """
178
179
  return self._identifier
179
180
 
180
- @identifier.setter
181
- def identifier(self, value: str) -> None:
181
+ def set_identifier(self, value: str) -> None:
182
182
  """Set the optional identifier for the panel.
183
183
 
184
184
  This can be used to distinguish panels or aid in superimposing panels.
@@ -186,12 +186,40 @@ class BasePanel(ABC):
186
186
  self._identifier = value
187
187
 
188
188
  def hide_xaxis_labels(self) -> None:
189
- """Hide the x-axis labels for this panel."""
190
- self.ax.tick_params(axis="x", labelbottom=False)
189
+ """Hide the labels for xaxis ticks in the panel."""
190
+ self._get_ax().tick_params(axis="x", labelbottom=False)
191
191
 
192
192
  def hide_yaxis_labels(self) -> None:
193
- """Hide the y-axis labels for this panel."""
194
- self.ax.tick_params(axis="y", labelbottom=False)
193
+ """Hide the labels for yaxis ticks in the panel."""
194
+ self._get_ax().tick_params(axis="y", labelleft=False)
195
+
196
+ def sharex(self, axes: Axes):
197
+ """Share the xaxis with another axes."""
198
+ self._get_ax().sharex(axes)
199
+
200
+ def get_xaxis_labels(self) -> list[str]:
201
+ """Get the text string of all xaxis tick labels."""
202
+ tick_labels = self._get_ax().get_xaxis().get_ticklabels(which="both")
203
+ return [tick_label.get_text() for tick_label in tick_labels]
204
+
205
+ def get_yaxis_labels(self) -> list[str]:
206
+ """Get the text string of all yaxis tick labels."""
207
+ tick_labels = self._get_ax().get_yaxis().get_ticklabels(which="both")
208
+ return [tick_label.get_text() for tick_label in tick_labels]
209
+
210
+ def get_xlabel(self) -> str:
211
+ """Get the xlabel text string."""
212
+ return self._get_ax().get_xlabel()
213
+
214
+ def get_ylabel(self) -> str:
215
+ """Get the ylabel text string."""
216
+ return self._get_ax().get_ylabel()
217
+
218
+ def share_axes(self, panel: "BasePanel") -> None:
219
+ # TODO: More elegantly share axes, rather than access protected method from
220
+ # another instance. Probably, this should just be entirely managed by the `PanelStack`
221
+ self.set_ax(panel._get_ax())
222
+ self.set_fig(panel._get_fig())
195
223
 
196
224
 
197
225
  class BaseTimeSeriesPanel(BasePanel):
@@ -210,38 +238,19 @@ class BaseTimeSeriesPanel(BasePanel):
210
238
  """The times assigned to each spectrum according to the `TimeType`."""
211
239
  return (
212
240
  self.spectrogram.times
213
- if self.time_type == TimeType.RELATIVE
241
+ if self.get_time_type() == TimeType.RELATIVE
214
242
  else self.spectrogram.datetimes
215
243
  )
216
244
 
217
245
  def annotate_xaxis(self) -> None:
218
- """Annotate the x-axis according to the specified `TimeType`."""
219
- if self.time_type == TimeType.RELATIVE:
220
- self.ax.set_xlabel("Time [s]")
246
+ """Annotate the xaxis according to the specified `TimeType`."""
247
+ ax = self._get_ax()
248
+ if self.get_time_type() == TimeType.RELATIVE:
249
+ ax.set_xlabel("Time [s]")
221
250
  else:
222
251
  # TODO: Adapt for time ranges greater than one day
223
252
  start_date = datetime.strftime(
224
253
  self.spectrogram.start_datetime.astype(datetime), TimeFormat.DATE
225
254
  )
226
- self.ax.set_xlabel(f"Time [UTC] (Start Date: {start_date})")
227
- self.ax.xaxis.set_major_formatter(mdates.DateFormatter(TimeFormat.TIME))
228
-
229
-
230
- class BaseSpectrumPanel(BasePanel):
231
- """An abstract subclass of `BasePanel` tailored for visualising spectrum data.
232
-
233
- Subclasses must implement any remaining abstract methods as described by `BasePanel`.
234
- """
235
-
236
- @property
237
- def xaxis_type(self) -> Literal[XAxisType.FREQUENCY]:
238
- return XAxisType.FREQUENCY
239
-
240
- @property
241
- def frequencies(self) -> npt.NDArray[np.float32]:
242
- """The physical frequencies assigned to each spectral component."""
243
- return self._spectrogram.frequencies
244
-
245
- def annotate_xaxis(self) -> None:
246
- """Annotate the x-axis assuming frequency in units of Hz."""
247
- self.ax.set_xlabel("Frequency [Hz]")
255
+ ax.set_xlabel(f"Time [UTC] (Start Date: {start_date})")
256
+ ax.xaxis.set_major_formatter(mdates.DateFormatter(TimeFormat.TIME))
@@ -66,18 +66,48 @@ class PanelStack:
66
66
  self._fig: Optional[Figure] = None
67
67
  self._axs: Optional[np.ndarray] = None
68
68
 
69
- @property
70
- def time_type(self) -> TimeType:
71
- """The type of time we assign to the spectrograms"""
72
- return self._time_type
69
+ def _sort_by_xaxis_type(self, panels: list[BasePanel]) -> list[BasePanel]:
70
+ return list(sorted(panels, key=lambda panel: panel.xaxis_type.value))
73
71
 
74
72
  @property
75
73
  def panels(self) -> list[BasePanel]:
76
74
  """Get the panels in the stack, sorted by their `XAxisType`."""
77
- return list(sorted(self._panels, key=lambda panel: panel.xaxis_type.value))
75
+ return self._sort_by_xaxis_type(self._panels)
76
+
77
+ @property
78
+ def superimposed_panels(self) -> list[BasePanel]:
79
+ """Get the superimposed panels in the stack, sorted by their `XAxisType"""
80
+ return self._sort_by_xaxis_type(self._superimposed_panels)
81
+
82
+ @property
83
+ def num_panels(self) -> int:
84
+ """Get the number of panels in the stack."""
85
+ return len(self._panels)
86
+
87
+ @property
88
+ def num_superimposed_panels(self) -> int:
89
+ """Get the number of superimposed panels in the stack."""
90
+ return len(self._superimposed_panels)
78
91
 
79
92
  @property
80
- def fig(self) -> Figure:
93
+ def time_type(self) -> TimeType:
94
+ """The imposed time type on all time series panels in the stack.
95
+
96
+ :raises ValueError: If the `time_type` has not been set.
97
+ """
98
+ return self._time_type
99
+
100
+ @time_type.setter
101
+ def time_type(self, value: TimeType) -> None:
102
+ """Set the `TimeType` for all time series panels in the stack.
103
+
104
+ This controls how time is represented and annotated on the panel.
105
+
106
+ :param value: The `TimeType` to impose on all time series panels in the stack.
107
+ """
108
+ self._time_type = value
109
+
110
+ def _get_fig(self) -> Figure:
81
111
  """Get the shared `matplotlib` figure for the panel stack.
82
112
 
83
113
  :raises ValueError: If the axes have not been initialized.
@@ -88,8 +118,7 @@ class PanelStack:
88
118
  )
89
119
  return self._fig
90
120
 
91
- @property
92
- def axs(self) -> np.ndarray:
121
+ def _get_axes(self) -> np.ndarray:
93
122
  """Get the `matplotlib` axes in the stack.
94
123
 
95
124
  :return: An array of `matplotlib.axes.Axes`, one for each panel in the stack.
@@ -101,10 +130,13 @@ class PanelStack:
101
130
  )
102
131
  return np.atleast_1d(self._axs)
103
132
 
104
- @property
105
- def num_panels(self) -> int:
106
- """Get the number of panels in the stack."""
107
- return len(self._panels)
133
+ def _validate_time_type(self, panel: BasePanel) -> None:
134
+ """Check that the time type of the input panel, is consistent with the time type of the stack."""
135
+ if panel.get_time_type() != self._time_type:
136
+ raise ValueError(
137
+ f"Cannot add a panel with inconsistent time type. "
138
+ f"Expected {self._time_type.value}, but got {panel.get_time_type().value}"
139
+ )
108
140
 
109
141
  def add_panel(
110
142
  self,
@@ -114,13 +146,16 @@ class PanelStack:
114
146
  ) -> None:
115
147
  """Add a panel to the stack.
116
148
 
149
+ Overrides the time type of the panel, to the time type of the stack.
150
+
117
151
  :param panel: An instance of a `BasePanel` subclass to be added to the stack.
118
152
  :param identifier: An optional string to link the panel with others for superimposing.
119
153
  """
120
- panel.panel_format = panel_format or self._panel_format
121
- panel.time_type = self._time_type
154
+ panel.set_panel_format(panel_format or self._panel_format)
155
+ panel.set_time_type(self._time_type)
122
156
  if identifier:
123
- panel.identifier = identifier
157
+ panel.set_identifier(identifier)
158
+
124
159
  self._panels.append(panel)
125
160
 
126
161
  def superimpose_panel(
@@ -131,13 +166,16 @@ class PanelStack:
131
166
  ) -> None:
132
167
  """Superimpose a panel onto an existing panel in the stack.
133
168
 
169
+ Overrides the time type of the panel, to the time type of the stack.
170
+
134
171
  :param panel: The panel to superimpose.
135
172
  :param identifier: An optional identifier to link panels during superimposing, defaults to None
136
173
  """
137
174
  if identifier:
138
- panel.identifier = identifier
139
- panel.panel_format = panel_format or self._panel_format
140
- panel.time_type = self._time_type
175
+ panel.set_identifier(identifier)
176
+ panel.set_panel_format(panel_format or self._panel_format)
177
+ panel.set_time_type(self._time_type)
178
+
141
179
  self._superimposed_panels.append(panel)
142
180
 
143
181
  def _init_plot_style(self) -> None:
@@ -173,16 +211,17 @@ class PanelStack:
173
211
  def _assign_axes(self) -> None:
174
212
  """Assign each axes in the figure to some panel in the stack.
175
213
 
176
- Axes are shared between panels with common `xaxis_type`.
214
+ Axes are shared between panels with common `XAxisType`.
177
215
  """
178
216
  shared_axes: dict[XAxisType, Axes] = {}
179
217
  for i, panel in enumerate(self.panels):
180
- panel.ax = self.axs[i]
181
- panel.fig = self._fig
218
+ ax = self._get_axes()[i]
219
+ panel.set_ax(ax)
220
+ panel.set_fig(self._fig)
182
221
  if panel.xaxis_type in shared_axes:
183
- panel.ax.sharex(shared_axes[panel.xaxis_type])
222
+ panel.sharex(shared_axes[panel.xaxis_type])
184
223
  else:
185
- shared_axes[panel.xaxis_type] = panel.ax
224
+ shared_axes[panel.xaxis_type] = ax
186
225
 
187
226
  def _overlay_cuts(self, cuts_panel: BasePanel) -> None:
188
227
  """Overlay cuts onto corresponding spectrogram panels.
@@ -210,9 +249,9 @@ class PanelStack:
210
249
  for super_panel in self._superimposed_panels:
211
250
  for panel in self._panels:
212
251
  if panel.name == super_panel.name and (
213
- panel.identifier == super_panel.identifier
252
+ panel.get_identifier() == super_panel.get_identifier()
214
253
  ):
215
- super_panel.ax, super_panel.fig = panel.ax, self._fig
254
+ super_panel.share_axes(panel)
216
255
  super_panel.draw()
217
256
  if _is_cuts_panel(super_panel):
218
257
  self._overlay_cuts(super_panel)
@@ -245,7 +284,7 @@ class PanelStack:
245
284
  def show(self) -> None:
246
285
  """Display the panel stack figure."""
247
286
  self._make_figure()
248
- plt.show()
287
+ self._get_fig().show()
249
288
 
250
289
  def save(
251
290
  self,
@@ -268,5 +307,7 @@ class PanelStack:
268
307
  get_batches_dir_path(start_dt.year, start_dt.month, start_dt.day),
269
308
  f"{batch_name}.png",
270
309
  )
271
- plt.savefig(batch_file_path)
310
+ # If the parent directory does not exist, create it.
311
+ os.makedirs(os.path.dirname(batch_file_path), exist_ok=True)
312
+ self._get_fig().savefig(batch_file_path)
272
313
  return batch_file_path
@@ -2,16 +2,17 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- from typing import TypeVar, Tuple, Iterator, Optional
5
+ from typing import TypeVar, Tuple, Iterator, Optional, Literal
6
6
  from datetime import datetime
7
7
 
8
8
  from matplotlib.colors import LogNorm
9
9
  from matplotlib import cm
10
+ from matplotlib.colorbar import Colorbar
10
11
  import numpy as np
11
12
  import numpy.typing as npt
12
13
 
13
14
  from spectre_core.spectrograms import Spectrogram, FrequencyCut, TimeCut
14
- from ._base import BaseSpectrumPanel, BaseTimeSeriesPanel
15
+ from ._base import BasePanel, BaseTimeSeriesPanel, XAxisType
15
16
  from ._panel_names import PanelName
16
17
 
17
18
 
@@ -36,7 +37,7 @@ def _bind_to_colors(
36
37
  return zip(values, rgbas)
37
38
 
38
39
 
39
- class FrequencyCutsPanel(BaseSpectrumPanel):
40
+ class FrequencyCutsPanel(BasePanel):
40
41
  """
41
42
  Panel for visualising spectrogram data as frequency cuts.
42
43
 
@@ -73,6 +74,19 @@ class FrequencyCutsPanel(BaseSpectrumPanel):
73
74
  self._peak_normalise = peak_normalise
74
75
  self._frequency_cuts: dict[float | datetime, FrequencyCut] = {}
75
76
 
77
+ @property
78
+ def xaxis_type(self) -> Literal[XAxisType.FREQUENCY]:
79
+ return XAxisType.FREQUENCY
80
+
81
+ @property
82
+ def frequencies(self) -> npt.NDArray[np.float32]:
83
+ """The physical frequencies assigned to each spectral component."""
84
+ return self._spectrogram.frequencies
85
+
86
+ def annotate_xaxis(self) -> None:
87
+ """Annotate the x-axis assuming frequency in units of Hz."""
88
+ self._get_ax().set_xlabel("Frequency [Hz]")
89
+
76
90
  def get_frequency_cuts(self) -> dict[float | datetime, FrequencyCut]:
77
91
  """
78
92
  Get the frequency cuts for the specified times.
@@ -106,7 +120,7 @@ class FrequencyCutsPanel(BaseSpectrumPanel):
106
120
  frequency_cuts = self.get_frequency_cuts()
107
121
  for time, color in self.bind_to_colors():
108
122
  frequency_cut = frequency_cuts[time]
109
- self.ax.step(
123
+ self._get_ax().step(
110
124
  self.frequencies, # convert to MHz
111
125
  frequency_cut.cut,
112
126
  where="mid",
@@ -119,13 +133,14 @@ class FrequencyCutsPanel(BaseSpectrumPanel):
119
133
  The y-axis label reflects whether the data is in decibels above the background (`dBb`),
120
134
  normalized to peak values, or in the original units of the spectrogram.
121
135
  """
136
+ ax = self._get_ax()
122
137
  if self._dBb:
123
- self.ax.set_ylabel("dBb")
138
+ ax.set_ylabel("dBb")
124
139
  elif self._peak_normalise:
125
140
  # no y-axis label if we are peak normalising
126
141
  return
127
142
  else:
128
- self.ax.set_ylabel(f"{self._spectrogram.spectrum_unit.value.capitalize()}")
143
+ ax.set_ylabel(f"{self._spectrogram.spectrum_unit.value.capitalize()}")
129
144
 
130
145
  def bind_to_colors(
131
146
  self,
@@ -137,7 +152,9 @@ class FrequencyCutsPanel(BaseSpectrumPanel):
137
152
 
138
153
  :return: An iterator of tuples, each containing a cut time and its corresponding RGBA color.
139
154
  """
140
- return _bind_to_colors(self.get_cut_times(), cmap=self.panel_format.line_cmap)
155
+ return _bind_to_colors(
156
+ self.get_cut_times(), cmap=self.get_panel_format().line_cmap
157
+ )
141
158
 
142
159
 
143
160
  class TimeCutsPanel(BaseTimeSeriesPanel):
@@ -196,7 +213,7 @@ class TimeCutsPanel(BaseTimeSeriesPanel):
196
213
  dBb=self._dBb,
197
214
  peak_normalise=self._peak_normalise,
198
215
  correct_background=self._background_subtract,
199
- return_time_type=self.time_type,
216
+ return_time_type=self.get_time_type(),
200
217
  )
201
218
  self._time_cuts[time_cut.frequency] = time_cut
202
219
  return self._time_cuts
@@ -215,7 +232,7 @@ class TimeCutsPanel(BaseTimeSeriesPanel):
215
232
  time_cuts = self.get_time_cuts()
216
233
  for frequency, color in self.bind_to_colors():
217
234
  time_cut = time_cuts[frequency]
218
- self.ax.step(self.times, time_cut.cut, where="mid", color=color)
235
+ self._get_ax().step(self.times, time_cut.cut, where="mid", color=color)
219
236
 
220
237
  def annotate_yaxis(self) -> None:
221
238
  """
@@ -224,12 +241,13 @@ class TimeCutsPanel(BaseTimeSeriesPanel):
224
241
  The y-axis label reflects whether the data is in decibels above the background (`dBb`),
225
242
  normalized to peak values, or in the original units of the spectrogram.
226
243
  """
244
+ ax = self._get_ax()
227
245
  if self._dBb:
228
- self.ax.set_ylabel("dBb")
246
+ ax.set_ylabel("dBb")
229
247
  elif self._peak_normalise:
230
248
  return # no y-axis label if we are peak normalising.
231
249
  else:
232
- self.ax.set_ylabel(f"{self._spectrogram.spectrum_unit.value.capitalize()}")
250
+ ax.set_ylabel(f"{self._spectrogram.spectrum_unit.value.capitalize()}")
233
251
 
234
252
  def bind_to_colors(self) -> Iterator[Tuple[float, npt.NDArray[np.float32]]]:
235
253
  """
@@ -239,7 +257,9 @@ class TimeCutsPanel(BaseTimeSeriesPanel):
239
257
 
240
258
  :return: An iterator of tuples, each containing a frequency and its corresponding RGBA color.
241
259
  """
242
- return _bind_to_colors(self.get_frequencies(), cmap=self.panel_format.line_cmap)
260
+ return _bind_to_colors(
261
+ self.get_frequencies(), cmap=self.get_panel_format().line_cmap
262
+ )
243
263
 
244
264
 
245
265
  class IntegralOverFrequencyPanel(BaseTimeSeriesPanel):
@@ -272,7 +292,9 @@ class IntegralOverFrequencyPanel(BaseTimeSeriesPanel):
272
292
  correct_background=self._background_subtract,
273
293
  peak_normalise=self._peak_normalise,
274
294
  )
275
- self.ax.step(self.times, I, where="mid", color=self.panel_format.line_color)
295
+ self._get_ax().step(
296
+ self.times, I, where="mid", color=self.get_panel_format().line_color
297
+ )
276
298
 
277
299
  def annotate_yaxis(self):
278
300
  """This panel does not annotate the y-axis."""
@@ -320,19 +342,20 @@ class SpectrogramPanel(BaseTimeSeriesPanel):
320
342
  vmin = self._vmin or -1
321
343
  vmax = self._vmax or 2
322
344
 
345
+ ax = self._get_ax()
323
346
  # Plot the spectrogram
324
- pcm = self.ax.pcolormesh(
347
+ pcm = ax.pcolormesh(
325
348
  self.times,
326
349
  self._spectrogram.frequencies,
327
350
  dynamic_spectra,
328
351
  vmin=vmin,
329
352
  vmax=vmax,
330
- cmap=self.panel_format.spectrogram_cmap,
353
+ cmap=self.get_panel_format().spectrogram_cmap,
331
354
  )
332
355
 
333
356
  # Add colorbar
334
357
  cbar_ticks = np.linspace(vmin, vmax, 6)
335
- cbar = self.fig.colorbar(pcm, ax=self.ax, ticks=cbar_ticks)
358
+ cbar = self._get_fig().colorbar(pcm, ax=ax, ticks=cbar_ticks)
336
359
  cbar.set_label("dBb")
337
360
 
338
361
  def _draw_normal(self) -> None:
@@ -352,11 +375,11 @@ class SpectrogramPanel(BaseTimeSeriesPanel):
352
375
  norm = None
353
376
 
354
377
  # Plot the spectrogram
355
- self.ax.pcolormesh(
378
+ self._get_ax().pcolormesh(
356
379
  self.times,
357
380
  self._spectrogram.frequencies,
358
381
  dynamic_spectra,
359
- cmap=self.panel_format.spectrogram_cmap,
382
+ cmap=self.get_panel_format().spectrogram_cmap,
360
383
  norm=norm,
361
384
  )
362
385
 
@@ -369,7 +392,7 @@ class SpectrogramPanel(BaseTimeSeriesPanel):
369
392
 
370
393
  def annotate_yaxis(self) -> None:
371
394
  """Annotate the yaxis, assuming units of Hz."""
372
- self.ax.set_ylabel("Frequency [Hz]")
395
+ self._get_ax().set_ylabel("Frequency [Hz]")
373
396
  return
374
397
 
375
398
  def overlay_time_cuts(self, cuts_panel: TimeCutsPanel) -> None:
@@ -382,8 +405,8 @@ class SpectrogramPanel(BaseTimeSeriesPanel):
382
405
  :param cuts_panel: The `TimeCutsPanel` containing the cut frequencies to overlay.
383
406
  """
384
407
  for frequency, color in cuts_panel.bind_to_colors():
385
- self.ax.axhline(
386
- frequency, color=color, linewidth=self.panel_format.line_width
408
+ self._get_ax().axhline(
409
+ frequency, color=color, linewidth=self.get_panel_format().line_width
387
410
  )
388
411
 
389
412
  def overlay_frequency_cuts(self, cuts_panel: FrequencyCutsPanel) -> None:
@@ -396,4 +419,6 @@ class SpectrogramPanel(BaseTimeSeriesPanel):
396
419
  :param cuts_panel: The `FrequencyCutsPanel` containing the cut times to overlay.
397
420
  """
398
421
  for time, color in cuts_panel.bind_to_colors():
399
- self.ax.axvline(time, color=color, linewidth=self.panel_format.line_width)
422
+ self._get_ax().axvline(
423
+ time, color=color, linewidth=self.get_panel_format().line_width
424
+ )
@@ -120,7 +120,9 @@ class Spectrogram:
120
120
  self._frequencies = frequencies
121
121
  self._tag = tag
122
122
  self._spectrum_unit = spectrum_unit
123
- self._start_datetime = np.datetime64(start_datetime)
123
+ self._start_datetime = (
124
+ np.datetime64(start_datetime) if start_datetime is not None else None
125
+ )
124
126
 
125
127
  # by default, the background is evaluated over the whole spectrogram
126
128
  self._start_background_index = 0
@@ -218,7 +220,7 @@ class Spectrogram:
218
220
  :raises AttributeError: If the start_datetime has not been set.
219
221
  """
220
222
  if self._start_datetime is None:
221
- raise AttributeError(f"A start time has not been set.")
223
+ raise ValueError(f"A start time has not been set.")
222
224
  return self._start_datetime
223
225
 
224
226
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spectre-core
3
- Version: 0.0.23
3
+ Version: 0.0.24
4
4
  Summary: The core Python package used by the spectre program.
5
5
  Maintainer-email: Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1,4 +1,4 @@
1
- spectre_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1
+ spectre_core/__init__.py,sha256=Hsh_-YQxbxSv8T4mjUwiFbG-w2HMgaXutAsG7XmKz3A,184
2
2
  spectre_core/exceptions.py,sha256=MJSUkuH8BYFt9zfv9m4dNISELEit6Sv_M4ifXHmeBGU,1167
3
3
  spectre_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  spectre_core/_file_io/__init__.py,sha256=H_-6xE-T3yUyywTe4hQn-oU5ACUumY1O-7Sp80EnKfQ,358
@@ -20,8 +20,8 @@ spectre_core/capture_configs/_pconstraints.py,sha256=2doJEr9ntR9NnVBGAfgZOYAD6TI
20
20
  spectre_core/capture_configs/_pnames.py,sha256=sidIsvVCjOuOLVu58vrG0PCBBNyfHugWKSGaJkJ4A68,1680
21
21
  spectre_core/capture_configs/_ptemplates.py,sha256=xo3qVF1bKMagK7myiDnUWU793qgmVbZ7UV_23CXtvYw,15504
22
22
  spectre_core/capture_configs/_pvalidators.py,sha256=3EuddlktB3bhSj6U92JLvT5D1VCBKGPIjkWiEFu-Fd4,9441
23
- spectre_core/config/__init__.py,sha256=NPv2CM13IZyq4_doxDluvgk9eq0WsbVUeT5-4yPKwzA,592
24
- spectre_core/config/_paths.py,sha256=RDrJL6LQFqUGMw0ABXiAUuyezxQXTFTPpXAsJKOElIE,4954
23
+ spectre_core/config/__init__.py,sha256=wEZ7VXjwpKCsEYZOkdKcJLWJdPt-a7lDF5u-qbs_MCg,754
24
+ spectre_core/config/_paths.py,sha256=vEg5sdFCS2HJ6QDtev1tGqthN1IXD0QofGC8JwAuLo4,4841
25
25
  spectre_core/config/_time_formats.py,sha256=yAnGweUfbnuEDTO-kGtljV1himVbEunBFA0n0Hv8cXo,823
26
26
  spectre_core/jobs/__init__.py,sha256=is9Ur36OL-iUcLCZ9SMGE_vuKW0yytL4zhNEKQx0LEI,380
27
27
  spectre_core/jobs/_duration.py,sha256=xjQ3fnN-dBcTZW8AjX073R8kNrM4HS1fROIMSvrUP6o,327
@@ -32,12 +32,12 @@ spectre_core/logs/_configure.py,sha256=y5R6vOl824KlNZVvYykYZ1qalIKbLO8BlSPtXxh-C
32
32
  spectre_core/logs/_decorators.py,sha256=M0wJwh6mxQhwq4Y9ORCRSFMpfi-pIzSK18hy8O9QedM,1173
33
33
  spectre_core/logs/_logs.py,sha256=rrC9D5C-mb1WguuqrdWLpMQATkSemV4ew69WsfQEDc4,6590
34
34
  spectre_core/logs/_process_types.py,sha256=Fm_aimKhSjO4kqQ8okpOsNT6VZlnkienFVdx6YhBtIc,490
35
- spectre_core/plotting/__init__.py,sha256=EQ9HYKyeRSH0u-8BfmIlfOOl4Zd0jDtQcTw3fkAUBgE,565
36
- spectre_core/plotting/_base.py,sha256=7qEfwa8480sVsDxYuQwzRND1CI_ETPRZdVok92p6woc,7983
35
+ spectre_core/plotting/__init__.py,sha256=AGu9ooOhVmzeLY7BDN7yj1W6ej1G6VBliIga0WRZYu4,740
36
+ spectre_core/plotting/_base.py,sha256=0lm_KhnexgXqrx7TfskHJiut-2KTyZ-CmBfujc2Yl4I,8783
37
37
  spectre_core/plotting/_format.py,sha256=ACpwfQw5raBAQLRvLBfwQto-Hqg9JsmrZ5eLqU-51Kg,1352
38
38
  spectre_core/plotting/_panel_names.py,sha256=XxWIOfQRs5hhas1X6q9vRarH06SWMC_Kv96Hy4r6jYY,799
39
- spectre_core/plotting/_panel_stack.py,sha256=diBjkyUD7oCyNi7JtP9Pg0SxRZ-IUAXAX0sFieo4iJk,9909
40
- spectre_core/plotting/_panels.py,sha256=uqmkF53y9IS111Ud6539HjNmqjihgzoNqGS-Pz-0Nn4,15225
39
+ spectre_core/plotting/_panel_stack.py,sha256=R278vkhvDYKkyJxuhkyA8yEVy7v0rbpCQ6QwGduDBq4,11619
40
+ spectre_core/plotting/_panels.py,sha256=9tk_ebpIN4ADx6vcauy-reUWg5gR_FYK9d_--_mG598,15982
41
41
  spectre_core/post_processing/__init__.py,sha256=jMIfEpEi1cH61nyZXXtUxYXxN1hinvWml1tdhhZrUEo,642
42
42
  spectre_core/post_processing/_base.py,sha256=cS7f0TRAbd_6vVG4dxngzdsftyq3py4PRXhEQWby-UE,6473
43
43
  spectre_core/post_processing/_factory.py,sha256=wpbB9wjw0Bhcxp02ORNtRwqOSVBLivLcwfg_s18XZg4,2227
@@ -68,12 +68,12 @@ spectre_core/receivers/plugins/gr/_usrp.py,sha256=0pyyW-Fn6rO1NlJXzXQswdJLdgbQxj
68
68
  spectre_core/spectrograms/__init__.py,sha256=olHW9pFhYg6-yCcLL6sP8C4noLwzgBGDOO9_RDyP6Cw,803
69
69
  spectre_core/spectrograms/_analytical.py,sha256=xW8Zm955ls-PqHBm9tf6nNyCWxrrhwZxUbPa4eXPbXs,11019
70
70
  spectre_core/spectrograms/_array_operations.py,sha256=AsBDmxFMnC3hYRP6QCFN65Fh0OLUAY7CeCx8kqPG2ks,7518
71
- spectre_core/spectrograms/_spectrogram.py,sha256=g0kelDpYfTKpgwCSCIlAdjJAdDeZ0HfK8GoZo8oeH-w,27132
71
+ spectre_core/spectrograms/_spectrogram.py,sha256=k0Azaz1NRV-ls1NOrwb0TpKwwcpGzjxv-MFj-MTyXsE,27192
72
72
  spectre_core/spectrograms/_transform.py,sha256=S9V0dROKv3L8dg9EiPxR90XNeKaNmvbfZh66sFjETRo,11749
73
73
  spectre_core/wgetting/__init__.py,sha256=9QD-8DjYq0hbU7f-NQ_Cg-TLKNbq0E0PI3yWJXJoC4k,341
74
74
  spectre_core/wgetting/_callisto.py,sha256=eZyTG07WQic-w9STbx91bOYWhWFof85TX41WY5WrJg4,7148
75
- spectre_core-0.0.23.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
76
- spectre_core-0.0.23.dist-info/METADATA,sha256=aBRiX_75UMG8txXmcODbRUhL8b2KOGE1L3yLke3HCyg,41951
77
- spectre_core-0.0.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
78
- spectre_core-0.0.23.dist-info/top_level.txt,sha256=-UsyjpFohXgZpgcZ9QbVeXhsIyF3Am8RxNFNDV_Ta2Y,13
79
- spectre_core-0.0.23.dist-info/RECORD,,
75
+ spectre_core-0.0.24.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
76
+ spectre_core-0.0.24.dist-info/METADATA,sha256=FH1RnPWWBhp8alZcUdZhNx8JoF1ov0qtRAZm57QPdjk,41951
77
+ spectre_core-0.0.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
78
+ spectre_core-0.0.24.dist-info/top_level.txt,sha256=-UsyjpFohXgZpgcZ9QbVeXhsIyF3Am8RxNFNDV_Ta2Y,13
79
+ spectre_core-0.0.24.dist-info/RECORD,,