spectre-core 0.0.23__py3-none-any.whl → 0.0.25__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 +5 -0
- spectre_core/config/__init__.py +6 -0
- spectre_core/config/_paths.py +12 -17
- spectre_core/plotting/__init__.py +6 -0
- spectre_core/plotting/_base.py +82 -73
- spectre_core/plotting/_panel_stack.py +72 -27
- spectre_core/plotting/_panels.py +47 -22
- spectre_core/spectrograms/_spectrogram.py +4 -2
- {spectre_core-0.0.23.dist-info → spectre_core-0.0.25.dist-info}/METADATA +1 -1
- {spectre_core-0.0.23.dist-info → spectre_core-0.0.25.dist-info}/RECORD +13 -13
- {spectre_core-0.0.23.dist-info → spectre_core-0.0.25.dist-info}/WHEEL +0 -0
- {spectre_core-0.0.23.dist-info → spectre_core-0.0.25.dist-info}/licenses/LICENSE +0 -0
- {spectre_core-0.0.23.dist-info → spectre_core-0.0.25.dist-info}/top_level.txt +0 -0
spectre_core/__init__.py
CHANGED
spectre_core/config/__init__.py
CHANGED
@@ -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",
|
spectre_core/config/_paths.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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,
|
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(
|
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",
|
spectre_core/plotting/_base.py
CHANGED
@@ -20,7 +20,7 @@ from ._panel_names import PanelName
|
|
20
20
|
|
21
21
|
|
22
22
|
class XAxisType(Enum):
|
23
|
-
"""The
|
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
|
40
|
+
how the panel is drawn and annotated, and specify its xaxis type.
|
41
41
|
"""
|
42
42
|
|
43
|
-
def __init__(
|
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
|
-
#
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
120
|
+
raise ValueError(f"`panel_format` must be set for the panel `{self.name}`")
|
119
121
|
return self._panel_format
|
120
122
|
|
121
|
-
|
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
|
-
|
130
|
-
|
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
|
-
|
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
|
139
|
+
raise ValueError(f"`ax` must be set for the panel `{self.name}`")
|
137
140
|
return self._ax
|
138
141
|
|
139
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
157
|
+
:raises ValueError: If the `Figure` object has not been set.
|
155
158
|
"""
|
156
159
|
if self._fig is None:
|
157
|
-
raise
|
160
|
+
raise ValueError(f"`fig` must be set for the panel `{self.name}`")
|
158
161
|
return self._fig
|
159
162
|
|
160
|
-
|
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
|
-
|
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
|
-
|
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
|
190
|
-
self.
|
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
|
194
|
-
self.
|
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.
|
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
|
219
|
-
|
220
|
-
|
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
|
-
|
227
|
-
|
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
|
-
|
70
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
121
|
-
panel.
|
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
|
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
|
139
|
-
panel.panel_format
|
140
|
-
panel.
|
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 `
|
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
|
-
|
181
|
-
panel.
|
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.
|
222
|
+
panel.sharex(shared_axes[panel.xaxis_type])
|
184
223
|
else:
|
185
|
-
shared_axes[panel.xaxis_type] =
|
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.
|
252
|
+
panel.get_identifier() == super_panel.get_identifier()
|
214
253
|
):
|
215
|
-
super_panel.
|
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,9 @@ class PanelStack:
|
|
245
284
|
def show(self) -> None:
|
246
285
|
"""Display the panel stack figure."""
|
247
286
|
self._make_figure()
|
248
|
-
|
287
|
+
self._get_fig().show()
|
288
|
+
self._get_fig().clear()
|
289
|
+
plt.close(self._fig)
|
249
290
|
|
250
291
|
def save(
|
251
292
|
self,
|
@@ -268,5 +309,9 @@ class PanelStack:
|
|
268
309
|
get_batches_dir_path(start_dt.year, start_dt.month, start_dt.day),
|
269
310
|
f"{batch_name}.png",
|
270
311
|
)
|
271
|
-
|
312
|
+
# If the parent directory does not exist, create it.
|
313
|
+
os.makedirs(os.path.dirname(batch_file_path), exist_ok=True)
|
314
|
+
self._get_fig().savefig(batch_file_path)
|
315
|
+
self._get_fig().clear()
|
316
|
+
plt.close(self._fig)
|
272
317
|
return batch_file_path
|
spectre_core/plotting/_panels.py
CHANGED
@@ -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
|
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(
|
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.
|
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
|
-
|
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
|
-
|
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(
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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(
|
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.
|
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 =
|
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.
|
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.
|
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.
|
378
|
+
self._get_ax().pcolormesh(
|
356
379
|
self.times,
|
357
380
|
self._spectrogram.frequencies,
|
358
381
|
dynamic_spectra,
|
359
|
-
cmap=self.
|
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.
|
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.
|
386
|
-
frequency, color=color, linewidth=self.
|
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.
|
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 =
|
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
|
223
|
+
raise ValueError(f"A start time has not been set.")
|
222
224
|
return self._start_datetime
|
223
225
|
|
224
226
|
@property
|
@@ -1,4 +1,4 @@
|
|
1
|
-
spectre_core/__init__.py,sha256=
|
1
|
+
spectre_core/__init__.py,sha256=9IvouSe7A5Kn4chmTaCqEmSGYo4EykkC6u4t9VDUbz8,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=
|
24
|
-
spectre_core/config/_paths.py,sha256=
|
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=
|
36
|
-
spectre_core/plotting/_base.py,sha256=
|
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=
|
40
|
-
spectre_core/plotting/_panels.py,sha256=
|
39
|
+
spectre_core/plotting/_panel_stack.py,sha256=OX2thaRKbcsse0ChiezRPZtqK1dTxgd1N4W_4ETwEkA,11741
|
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=
|
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.
|
76
|
-
spectre_core-0.0.
|
77
|
-
spectre_core-0.0.
|
78
|
-
spectre_core-0.0.
|
79
|
-
spectre_core-0.0.
|
75
|
+
spectre_core-0.0.25.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
76
|
+
spectre_core-0.0.25.dist-info/METADATA,sha256=N52E9xy1VYdmCMlOnTONWOO3A32JJheLkjBCUVajQMI,41951
|
77
|
+
spectre_core-0.0.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
78
|
+
spectre_core-0.0.25.dist-info/top_level.txt,sha256=-UsyjpFohXgZpgcZ9QbVeXhsIyF3Am8RxNFNDV_Ta2Y,13
|
79
|
+
spectre_core-0.0.25.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|