spectre-core 0.0.23__tar.gz → 0.0.24__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. {spectre_core-0.0.23 → spectre_core-0.0.24}/PKG-INFO +1 -1
  2. {spectre_core-0.0.23 → spectre_core-0.0.24}/pyproject.toml +1 -1
  3. spectre_core-0.0.23/tests/test_config.py → spectre_core-0.0.24/src/spectre_core/__init__.py +2 -0
  4. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/config/__init__.py +6 -0
  5. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/config/_paths.py +12 -17
  6. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/plotting/__init__.py +6 -0
  7. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/plotting/_base.py +82 -73
  8. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/plotting/_panel_stack.py +68 -27
  9. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/plotting/_panels.py +47 -22
  10. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/spectrograms/_spectrogram.py +4 -2
  11. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core.egg-info/PKG-INFO +1 -1
  12. spectre_core-0.0.24/tests/test_config.py +74 -0
  13. {spectre_core-0.0.23 → spectre_core-0.0.24}/tests/test_jobs.py +12 -12
  14. spectre_core-0.0.24/tests/test_plotting.py +500 -0
  15. spectre_core-0.0.23/src/spectre_core/receivers/plugins/__init__.py +0 -0
  16. spectre_core-0.0.23/tests/test_plotting.py +0 -3
  17. {spectre_core-0.0.23 → spectre_core-0.0.24}/LICENSE +0 -0
  18. {spectre_core-0.0.23 → spectre_core-0.0.24}/README.md +0 -0
  19. {spectre_core-0.0.23 → spectre_core-0.0.24}/setup.cfg +0 -0
  20. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/_file_io/__init__.py +0 -0
  21. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/_file_io/file_handlers.py +0 -0
  22. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/batches/__init__.py +0 -0
  23. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/batches/_base.py +0 -0
  24. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/batches/_batches.py +0 -0
  25. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/batches/_factory.py +0 -0
  26. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/batches/_register.py +0 -0
  27. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/batches/plugins/_batch_keys.py +0 -0
  28. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/batches/plugins/_callisto.py +0 -0
  29. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/batches/plugins/_iq_stream.py +0 -0
  30. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/capture_configs/__init__.py +0 -0
  31. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/capture_configs/_capture_config.py +0 -0
  32. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/capture_configs/_capture_modes.py +0 -0
  33. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/capture_configs/_capture_templates.py +0 -0
  34. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/capture_configs/_parameters.py +0 -0
  35. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/capture_configs/_pconstraints.py +0 -0
  36. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/capture_configs/_pnames.py +0 -0
  37. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/capture_configs/_ptemplates.py +0 -0
  38. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/capture_configs/_pvalidators.py +0 -0
  39. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/config/_time_formats.py +0 -0
  40. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/exceptions.py +0 -0
  41. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/jobs/__init__.py +0 -0
  42. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/jobs/_duration.py +0 -0
  43. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/jobs/_jobs.py +0 -0
  44. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/jobs/_workers.py +0 -0
  45. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/logs/__init__.py +0 -0
  46. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/logs/_configure.py +0 -0
  47. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/logs/_decorators.py +0 -0
  48. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/logs/_logs.py +0 -0
  49. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/logs/_process_types.py +0 -0
  50. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/plotting/_format.py +0 -0
  51. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/plotting/_panel_names.py +0 -0
  52. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/post_processing/__init__.py +0 -0
  53. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/post_processing/_base.py +0 -0
  54. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/post_processing/_factory.py +0 -0
  55. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/post_processing/_post_processor.py +0 -0
  56. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/post_processing/_register.py +0 -0
  57. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/post_processing/plugins/_event_handler_keys.py +0 -0
  58. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/post_processing/plugins/_fixed_center_frequency.py +0 -0
  59. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/post_processing/plugins/_swept_center_frequency.py +0 -0
  60. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/py.typed +0 -0
  61. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/__init__.py +0 -0
  62. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/_base.py +0 -0
  63. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/_factory.py +0 -0
  64. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/_register.py +0 -0
  65. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/_spec_names.py +0 -0
  66. {spectre_core-0.0.23/src/spectre_core → spectre_core-0.0.24/src/spectre_core/receivers/plugins}/__init__.py +0 -0
  67. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/_b200mini.py +0 -0
  68. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/_receiver_names.py +0 -0
  69. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/_rsp1a.py +0 -0
  70. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/_rspduo.py +0 -0
  71. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/_sdrplay_receiver.py +0 -0
  72. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/_test.py +0 -0
  73. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/_usrp.py +0 -0
  74. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/gr/__init__.py +0 -0
  75. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/gr/_base.py +0 -0
  76. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/gr/_rsp1a.py +0 -0
  77. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/gr/_rspduo.py +0 -0
  78. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/gr/_test.py +0 -0
  79. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/receivers/plugins/gr/_usrp.py +0 -0
  80. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/spectrograms/__init__.py +0 -0
  81. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/spectrograms/_analytical.py +0 -0
  82. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/spectrograms/_array_operations.py +0 -0
  83. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/spectrograms/_transform.py +0 -0
  84. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/wgetting/__init__.py +0 -0
  85. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core/wgetting/_callisto.py +0 -0
  86. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core.egg-info/SOURCES.txt +0 -0
  87. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core.egg-info/dependency_links.txt +0 -0
  88. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core.egg-info/requires.txt +0 -0
  89. {spectre_core-0.0.23 → spectre_core-0.0.24}/src/spectre_core.egg-info/top_level.txt +0 -0
  90. {spectre_core-0.0.23 → spectre_core-0.0.24}/tests/test_batches.py +0 -0
  91. {spectre_core-0.0.23 → spectre_core-0.0.24}/tests/test_capture_configs.py +0 -0
  92. {spectre_core-0.0.23 → spectre_core-0.0.24}/tests/test_logs.py +0 -0
  93. {spectre_core-0.0.23 → spectre_core-0.0.24}/tests/test_post_processing.py +0 -0
  94. {spectre_core-0.0.23 → spectre_core-0.0.24}/tests/test_receivers.py +0 -0
  95. {spectre_core-0.0.23 → spectre_core-0.0.24}/tests/test_spectrograms.py +0 -0
  96. {spectre_core-0.0.23 → spectre_core-0.0.24}/tests/test_wgetting.py +0 -0
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spectre-core"
7
- version = "0.0.23"
7
+ version = "0.0.24"
8
8
  maintainers = [
9
9
  { name="Jimmy Fitzpatrick", email="jcfitzpatrick12@gmail.com" },
10
10
  ]
@@ -1,3 +1,5 @@
1
1
  # SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
2
2
  # This file is part of SPECTRE
3
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