spectre-core 0.0.11__py3-none-any.whl → 0.0.13__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.
Files changed (94) hide show
  1. spectre_core/_file_io/__init__.py +1 -3
  2. spectre_core/_file_io/file_handlers.py +170 -65
  3. spectre_core/batches/__init__.py +21 -0
  4. spectre_core/batches/_base.py +238 -0
  5. spectre_core/batches/_batches.py +247 -0
  6. spectre_core/batches/_factory.py +69 -0
  7. spectre_core/batches/_register.py +30 -0
  8. spectre_core/batches/plugins/_batch_keys.py +16 -0
  9. spectre_core/batches/plugins/_callisto.py +183 -0
  10. spectre_core/batches/plugins/_iq_stream.py +354 -0
  11. spectre_core/capture_configs/__init__.py +17 -13
  12. spectre_core/capture_configs/_capture_config.py +93 -34
  13. spectre_core/capture_configs/_capture_modes.py +22 -0
  14. spectre_core/capture_configs/_capture_templates.py +207 -122
  15. spectre_core/capture_configs/_parameters.py +116 -46
  16. spectre_core/capture_configs/_pconstraints.py +86 -35
  17. spectre_core/capture_configs/_pnames.py +49 -0
  18. spectre_core/capture_configs/_ptemplates.py +389 -346
  19. spectre_core/capture_configs/_pvalidators.py +121 -77
  20. spectre_core/config/__init__.py +7 -9
  21. spectre_core/config/_paths.py +66 -26
  22. spectre_core/config/_time_formats.py +15 -8
  23. spectre_core/exceptions.py +2 -4
  24. spectre_core/jobs/__init__.py +14 -0
  25. spectre_core/jobs/_jobs.py +111 -0
  26. spectre_core/jobs/_workers.py +171 -0
  27. spectre_core/logs/__init__.py +17 -0
  28. spectre_core/logs/_configure.py +67 -0
  29. spectre_core/logs/_decorators.py +33 -0
  30. spectre_core/logs/_logs.py +228 -0
  31. spectre_core/logs/_process_types.py +14 -0
  32. spectre_core/plotting/__init__.py +4 -2
  33. spectre_core/plotting/_base.py +204 -102
  34. spectre_core/plotting/_format.py +17 -4
  35. spectre_core/plotting/_panel_names.py +18 -0
  36. spectre_core/plotting/_panel_stack.py +167 -53
  37. spectre_core/plotting/_panels.py +341 -141
  38. spectre_core/post_processing/__init__.py +8 -6
  39. spectre_core/post_processing/_base.py +71 -45
  40. spectre_core/post_processing/_factory.py +42 -12
  41. spectre_core/post_processing/_post_processor.py +27 -29
  42. spectre_core/post_processing/_register.py +22 -6
  43. spectre_core/post_processing/plugins/_event_handler_keys.py +16 -0
  44. spectre_core/post_processing/plugins/_fixed_center_frequency.py +129 -0
  45. spectre_core/post_processing/plugins/_swept_center_frequency.py +439 -0
  46. spectre_core/py.typed +0 -0
  47. spectre_core/receivers/__init__.py +10 -7
  48. spectre_core/receivers/_base.py +220 -69
  49. spectre_core/receivers/_factory.py +53 -7
  50. spectre_core/receivers/_register.py +30 -9
  51. spectre_core/receivers/_spec_names.py +26 -15
  52. spectre_core/receivers/plugins/__init__.py +0 -0
  53. spectre_core/receivers/plugins/_receiver_names.py +16 -0
  54. spectre_core/receivers/plugins/_rsp1a.py +59 -0
  55. spectre_core/receivers/plugins/_rspduo.py +67 -0
  56. spectre_core/receivers/plugins/_sdrplay_receiver.py +190 -0
  57. spectre_core/receivers/plugins/_test.py +218 -0
  58. spectre_core/receivers/plugins/gr/_base.py +80 -0
  59. spectre_core/receivers/{gr → plugins/gr}/_rsp1a.py +45 -55
  60. spectre_core/receivers/{gr → plugins/gr}/_rspduo.py +65 -78
  61. spectre_core/receivers/{gr → plugins/gr}/_test.py +36 -34
  62. spectre_core/spectrograms/__init__.py +5 -3
  63. spectre_core/spectrograms/_analytical.py +121 -72
  64. spectre_core/spectrograms/_array_operations.py +103 -36
  65. spectre_core/spectrograms/_spectrogram.py +410 -203
  66. spectre_core/spectrograms/_transform.py +199 -188
  67. spectre_core/wgetting/__init__.py +4 -2
  68. spectre_core/wgetting/_callisto.py +178 -127
  69. {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/METADATA +14 -7
  70. spectre_core-0.0.13.dist-info/RECORD +75 -0
  71. {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/WHEEL +1 -1
  72. spectre_core/chunks/__init__.py +0 -22
  73. spectre_core/chunks/_base.py +0 -116
  74. spectre_core/chunks/_chunks.py +0 -200
  75. spectre_core/chunks/_factory.py +0 -25
  76. spectre_core/chunks/_register.py +0 -15
  77. spectre_core/chunks/library/_callisto.py +0 -98
  78. spectre_core/chunks/library/_fixed_center_frequency.py +0 -128
  79. spectre_core/chunks/library/_swept_center_frequency.py +0 -103
  80. spectre_core/logging/__init__.py +0 -11
  81. spectre_core/logging/_configure.py +0 -35
  82. spectre_core/logging/_decorators.py +0 -19
  83. spectre_core/logging/_log_handlers.py +0 -176
  84. spectre_core/post_processing/library/_fixed_center_frequency.py +0 -115
  85. spectre_core/post_processing/library/_swept_center_frequency.py +0 -382
  86. spectre_core/receivers/gr/_base.py +0 -33
  87. spectre_core/receivers/library/_rsp1a.py +0 -61
  88. spectre_core/receivers/library/_rspduo.py +0 -69
  89. spectre_core/receivers/library/_sdrplay_receiver.py +0 -185
  90. spectre_core/receivers/library/_test.py +0 -221
  91. spectre_core-0.0.11.dist-info/RECORD +0 -64
  92. /spectre_core/receivers/{gr → plugins/gr}/__init__.py +0 -0
  93. {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/LICENSE +0 -0
  94. {spectre_core-0.0.11.dist-info → spectre_core-0.0.13.dist-info}/top_level.txt +0 -0
@@ -2,9 +2,7 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- """
6
- Internal file handling.
7
- """
5
+ """Basic internal file handling capabilities."""
8
6
 
9
7
  from .file_handlers import (
10
8
  BaseFileHandler, JsonHandler, TextHandler,
@@ -5,124 +5,229 @@
5
5
  import os
6
6
  import json
7
7
  from abc import ABC, abstractmethod
8
- from typing import Any, Optional
9
-
10
-
11
- class BaseFileHandler(ABC):
12
- def __init__(self,
13
- parent_path: str,
14
- base_file_name: str,
15
- extension: Optional[str] = None):
16
- self._parent_path = parent_path
17
- self._base_file_name = base_file_name
8
+ from typing import Any, Optional, TypeVar, Generic
9
+
10
+ T = TypeVar('T')
11
+
12
+ class BaseFileHandler(ABC, Generic[T]):
13
+ """
14
+ Base class for handling file operations.
15
+
16
+ When subclassing, specify the return type of `_read`
17
+ using `Generic[T]`.
18
+
19
+ Example:
20
+ .. code-block:: python
21
+
22
+ from typing import Any, Generic
23
+
24
+ class JsonHandler(BaseFileHandler[dict[str, Any]]):
25
+ def _read(self) -> dict[str, Any]:
26
+ # Implementation here
27
+ ...
28
+ """
29
+ def __init__(
30
+ self,
31
+ parent_dir_path: str,
32
+ base_file_name: str,
33
+ extension: Optional[str] = None
34
+ ) -> None:
35
+ """Initialise a `BaseFileHandler` instance.
36
+
37
+ :param parent_dir_path: The directory path where the file is located (absolute or relative).
38
+ :param base_file_name: The name of the file without its extension.
39
+ :param extension: The file extension (without the dot), defaults to None
40
+ """
41
+ self._data_cache: Optional[T] = None
42
+
43
+ self._parent_dir_path = parent_dir_path
44
+ self._base_file_name = base_file_name
45
+
46
+ if extension == "":
47
+ extension = None
18
48
  self._extension = extension
19
49
 
20
-
50
+
21
51
  @abstractmethod
22
- def read(self) -> Any:
23
- pass
24
-
25
-
52
+ def _read(
53
+ self
54
+ ) -> T:
55
+ """The grunt work to return the file contents.
56
+
57
+ :return: The file contents.
58
+ """
59
+
60
+
26
61
  @property
27
- def parent_path(self) -> str:
28
- return self._parent_path
62
+ def parent_dir_path(
63
+ self
64
+ ) -> str:
65
+ """Return the parent directory path for the file."""
66
+ return self._parent_dir_path
29
67
 
30
68
 
31
69
  @property
32
- def base_file_name(self) -> str:
70
+ def base_file_name(
71
+ self
72
+ ) -> str:
73
+ """Return the file name, stripped of the file extension."""
33
74
  return self._base_file_name
34
75
 
35
76
 
36
77
  @property
37
- def extension(self) -> Optional[str]:
78
+ def extension(
79
+ self
80
+ ) -> Optional[str]:
81
+ """Return the file path suffix, excluding the dot."""
38
82
  return self._extension
39
83
 
40
84
 
41
85
  @property
42
- def file_name(self) -> str:
86
+ def file_name(
87
+ self
88
+ ) -> str:
89
+ """Generate the file name based on the base name and extension.
90
+
91
+ :return: The file name with the extension (including the dot), or the base name if no extension is set.
92
+ """
43
93
  return self._base_file_name if (self._extension is None) else f"{self._base_file_name}.{self._extension}"
44
94
 
45
95
 
46
96
  @property
47
- def file_path(self) -> str:
48
- return os.path.join(self._parent_path, self.file_name)
97
+ def file_path(
98
+ self
99
+ ) -> str:
100
+ """The absolute or relative file path as defined by the parent directory path,
101
+ base file name and extension."""
102
+ return os.path.join(self._parent_dir_path, self.file_name)
49
103
 
50
104
 
51
105
  @property
52
- def exists(self) -> bool:
106
+ def exists(
107
+ self
108
+ ) -> bool:
109
+ """Check if the file exists in the filesystem."""
53
110
  return os.path.exists(self.file_path)
54
111
 
55
112
 
56
- def make_parent_path(self) -> None:
57
- os.makedirs(self.parent_path, exist_ok=True)
113
+ def read(
114
+ self,
115
+ cache: bool = True
116
+ ) -> T:
117
+ """Read the file contents.
118
+
119
+ :param cache: If False, bypasses the cache and reads the file directly on each `read` call, defaults to True
120
+ :return: The file contents.
121
+ """
122
+ # if the user has specified to ignore the cache, simply read the file.
123
+ if not cache:
124
+ return self._read()
125
+
126
+ # otherwise make use of the cache
127
+ if self._data_cache is None:
128
+ self._data_cache = self._read()
129
+ return self._data_cache
130
+
58
131
 
132
+ def make_parent_dir_path(
133
+ self
134
+ ) -> None:
135
+ """Make the parent directory path of the file. No error is raised if the target
136
+ directory already exists.
137
+ """
138
+ os.makedirs(self.parent_dir_path, exist_ok=True)
139
+
140
+
141
+ def delete(
142
+ self,
143
+ ignore_if_missing: bool = False
144
+ ) -> None:
145
+ """Delete the file from the filesystem.
59
146
 
60
- def delete(self,
61
- ignore_if_missing: bool = False) -> None:
147
+ :param ignore_if_missing: If True, skips deletion if the file does not exist, defaults to False
148
+ :raises FileNotFoundError: If the file is missing and `ignore_if_missing` is False.
149
+ """
62
150
  if not self.exists and not ignore_if_missing:
63
151
  raise FileNotFoundError(f"{self.file_name} does not exist, and so cannot be deleted")
64
152
  else:
65
153
  os.remove(self.file_path)
66
154
 
67
155
 
68
- def cat(self) -> None:
156
+ def cat(
157
+ self
158
+ ) -> None:
159
+ """Display the file contents on the standard output."""
69
160
  print(self.read())
70
161
 
71
162
 
72
- class JsonHandler(BaseFileHandler):
73
- def __init__(self,
74
- parent_path: str,
75
- base_file_name: str,
76
- extension: str = "json",
77
- **kwargs):
78
-
79
- self._dict = None # cache
80
- super().__init__(parent_path,
163
+ class JsonHandler(BaseFileHandler[dict[str, Any]]):
164
+ """File handler for JSON formatted files.
165
+
166
+ We assume that the files are of the form
167
+ {
168
+ "foo": <JSON compatable structure>
169
+ ... and so on.
170
+ }
171
+
172
+ """
173
+ def __init__(
174
+ self,
175
+ parent_dir_path: str,
176
+ base_file_name: str,
177
+ extension: str = "json"
178
+ ) -> None:
179
+ super().__init__(parent_dir_path,
81
180
  base_file_name,
82
- extension,
83
- **kwargs)
181
+ extension)
84
182
 
85
183
 
86
- def read(self) -> dict[str, Any]:
184
+ def _read(
185
+ self
186
+ ) -> dict[str, Any]:
87
187
  with open(self.file_path, 'r') as f:
88
188
  return json.load(f)
89
189
 
90
-
91
- def save(self,
92
- d: dict,
93
- force: bool = False) -> None:
94
- self.make_parent_path()
190
+
191
+ def save(
192
+ self,
193
+ d: dict[str, Any],
194
+ force: bool = False
195
+ ) -> None:
196
+ """Save the input dictionary to file in the JSON file format.
197
+
198
+ :param d: The dictionary to save.
199
+ :param force: If True, overwrites the file if it already exists, defaults to False
200
+ :raises FileExistsError: If the file exists and `force` is False.
201
+ """
202
+ self.make_parent_dir_path()
95
203
 
96
204
  if self.exists:
97
205
  if force:
98
206
  pass
99
207
  else:
100
- raise RuntimeError((f"{self.file_name} already exists, write has been abandoned. "
101
- f"You can override this functionality with `force`"))
208
+ raise FileExistsError((f"{self.file_name} already exists, write has been abandoned. "
209
+ f"You can override this functionality with `force`"))
102
210
 
103
211
  with open(self.file_path, 'w') as file:
104
212
  json.dump(d, file, indent=4)
105
213
 
214
+
106
215
 
107
- @property
108
- def dict(self) -> dict[str, Any]:
109
- if self._dict is None:
110
- self._dict = self.read()
111
- return self._dict
112
-
113
-
114
- class TextHandler(BaseFileHandler):
115
- def __init__(self,
116
- parent_path: str,
117
- base_file_name: str,
118
- extension: str = "txt",
119
- **kwargs):
120
- super().__init__(parent_path,
216
+ class TextHandler(BaseFileHandler[str]):
217
+ """File handler for text formatted files."""
218
+ def __init__(
219
+ self,
220
+ parent_dir_path: str,
221
+ base_file_name: str,
222
+ extension: str = "txt"
223
+ ) -> None:
224
+ super().__init__(parent_dir_path,
121
225
  base_file_name,
122
- extension,
123
- **kwargs)
226
+ extension)
124
227
 
125
-
126
- def read(self) -> dict:
228
+
229
+ def _read(
230
+ self
231
+ ) -> str:
127
232
  with open(self.file_path, 'r') as f:
128
233
  return f.read()
@@ -0,0 +1,21 @@
1
+ # SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
2
+ # This file is part of SPECTRE
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ """IO operations on batched data files."""
6
+
7
+ from .plugins._batch_keys import BatchKey
8
+
9
+ # register decorators take effect on import
10
+ from .plugins._iq_stream import IQStreamBatch, IQMetadata
11
+ from .plugins._callisto import CallistoBatch
12
+
13
+ from ._base import BaseBatch, BatchFile
14
+ from ._batches import Batches
15
+ from ._factory import get_batch_cls, get_batch_cls_from_tag
16
+
17
+ __all__ = [
18
+ "IQStreamBatch", "IQMetadata", "CallistoBatch", "BaseBatch", "BatchFile",
19
+ "Batches", "get_batch_cls", "BatchKey", "get_batch_cls_from_tag"
20
+ ]
21
+
@@ -0,0 +1,238 @@
1
+ # SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
2
+ # This file is part of SPECTRE
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from datetime import datetime
6
+ from typing import TypeVar
7
+ from functools import cached_property
8
+ from abc import ABC, abstractmethod
9
+
10
+ from spectre_core._file_io import BaseFileHandler
11
+ from spectre_core.config import get_batches_dir_path, TimeFormat
12
+ from spectre_core.spectrograms import Spectrogram
13
+
14
+
15
+ T = TypeVar('T')
16
+
17
+ class BatchFile(BaseFileHandler[T]):
18
+ """Abstract base class for files belonging to a batch, identified by their file extension.
19
+
20
+ Batch file names must conform to the following structure:
21
+
22
+ `<start time>_<tag>.<extension>`
23
+
24
+ The substring `<start time>_<tag>` is referred to as the batch name. Files with the same batch name
25
+ belong to the same batch.
26
+ """
27
+ def __init__(
28
+ self,
29
+ batch_parent_dir_path: str,
30
+ batch_name: str,
31
+ extension: str
32
+ ) -> None:
33
+ """Initialise a `BatchFile` instance.
34
+
35
+ :param batch_parent_dir_path: Parent directory of the batch.
36
+ :param batch_name: Base file name, composed of the batch start time and tag.
37
+ :param extension: File extension.
38
+ """
39
+ super().__init__(batch_parent_dir_path,
40
+ batch_name,
41
+ extension)
42
+ self._start_time, self._tag = batch_name.split("_")
43
+
44
+
45
+ @property
46
+ def start_time(
47
+ self
48
+ ) -> str:
49
+ """The start time of the batch, formatted as a string up to seconds precision."""
50
+ return self._start_time
51
+
52
+
53
+ @cached_property
54
+ def start_datetime(
55
+ self
56
+ ) -> datetime:
57
+ """The start time of the batch, parsed as a datetime up to seconds precision."""
58
+ return datetime.strptime(self.start_time, TimeFormat.DATETIME)
59
+
60
+
61
+ @property
62
+ def tag(
63
+ self
64
+ ) -> str:
65
+ """The batch name tag."""
66
+ return self._tag
67
+
68
+
69
+ class BaseBatch(ABC):
70
+ """
71
+ An abstract base class representing a group of data files over a common time interval.
72
+
73
+ All files in a batch share a base file name and differ only by their extension.
74
+ Subclasses of `BaseBatch` define the expected data for each file extension and
75
+ provide an API for accessing their contents using `BatchFile` subclasses.
76
+
77
+ Subclasses should expose `BatchFile` instances directly as attributes, which
78
+ simplifies static typing. Additionally, they should call `add_file` in the constructor
79
+ to formally register each `BatchFile`.
80
+ """
81
+ def __init__(
82
+ self,
83
+ start_time: str,
84
+ tag: str
85
+ ) -> None:
86
+ """Initialise a `BaseBatch` instance.
87
+
88
+ :param start_time: Start time of the batch as a string with seconds precision.
89
+ :param tag: The batch name tag.
90
+ """
91
+ self._start_time = start_time
92
+ self._tag: str = tag
93
+ self._start_datetime = datetime.strptime(self.start_time, TimeFormat.DATETIME)
94
+ self._parent_dir_path = get_batches_dir_path(year = self.start_datetime.year,
95
+ month = self.start_datetime.month,
96
+ day = self.start_datetime.day)
97
+
98
+ # internal register of batch files
99
+ self._batch_files: dict[str, BatchFile] = {}
100
+
101
+
102
+ @property
103
+ @abstractmethod
104
+ def spectrogram_file(
105
+ self
106
+ ) -> BatchFile:
107
+ """The batch file which contains spectrogram data."""
108
+
109
+
110
+ @property
111
+ def start_time(
112
+ self
113
+ ) -> str:
114
+ """The start time of the batch, formatted as a string up to seconds precision."""
115
+ return self._start_time
116
+
117
+
118
+ @property
119
+ def start_datetime(
120
+ self
121
+ ) -> datetime:
122
+ """The start time of the batch, parsed as a datetime up to seconds precision."""
123
+ return self._start_datetime
124
+
125
+
126
+ @property
127
+ def tag(
128
+ self
129
+ ) -> str:
130
+ """The batch name tag."""
131
+ return self._tag
132
+
133
+
134
+ @property
135
+ def parent_dir_path(
136
+ self
137
+ ) -> str:
138
+ """The parent directory for the batch."""
139
+ return self._parent_dir_path
140
+
141
+
142
+ @property
143
+ def name(
144
+ self
145
+ ) -> str:
146
+ """Return the base file name shared by all files in the batch,
147
+ composed of the start time and the batch tag."""
148
+ return f"{self._start_time}_{self._tag}"
149
+
150
+
151
+ @property
152
+ def extensions(
153
+ self
154
+ ) -> list[str]:
155
+ """All defined file extensions for the batch."""
156
+ return list(self._batch_files.keys())
157
+
158
+
159
+ @property
160
+ def batch_files(
161
+ self
162
+ ) -> dict[str, BatchFile]:
163
+ """Map each file extension in the batch to the corresponding batch file instance.
164
+
165
+ Use `add_file` to add a file to the batch.
166
+ """
167
+ return self._batch_files
168
+
169
+
170
+ def add_file(
171
+ self,
172
+ batch_file: BatchFile
173
+ ) -> None:
174
+ """Add an instance of a batch file to the batch.
175
+
176
+ :param batch_file: The `BatchFile` instance to add to the batch.
177
+ :raises ValueError: If the `BatchFile` instance does not have a defined file extension.
178
+ """
179
+ if batch_file.extension is None:
180
+ raise ValueError(f"The `BatchFile` must have a defined file extension. "
181
+ f"Received '{batch_file.extension}.")
182
+ self._batch_files[batch_file.extension] = batch_file
183
+
184
+
185
+ def get_file(
186
+ self,
187
+ extension: str
188
+ ) -> BatchFile:
189
+ """Get a batch file instance from the batch, according to the file extension.
190
+
191
+ :param extension: The file extension of the batch file.
192
+ :raises NotImplementedError: If the extension is undefined for the batch.
193
+ :return: The batch file instance registered under the input file extension.
194
+ """
195
+ try:
196
+ return self._batch_files[extension]
197
+ except KeyError:
198
+ raise NotImplementedError(f"A batch file with extension '{extension}' is not implemented for this batch.")
199
+
200
+
201
+ def delete_file(
202
+ self,
203
+ extension: str
204
+ ) -> None:
205
+ """Delete a file from the batch, according to the file extension.
206
+
207
+ :param extension: The file extension of the batch file.
208
+ :raises FileNotFoundError: If the batch file does not exist in the file system.
209
+ """
210
+ batch_file = self.get_file(extension)
211
+ batch_file.delete()
212
+
213
+
214
+ def has_file(
215
+ self,
216
+ extension: str
217
+ ) -> bool:
218
+ """Determine the existance of a batch file in the file system.
219
+
220
+ :param extension: The file extension of the batch file.
221
+ :return: True if the batch file exists in the file system, False otherwise.
222
+ """
223
+ try:
224
+ batch_file = self.get_file(extension)
225
+ return batch_file.exists
226
+ except FileNotFoundError:
227
+ return False
228
+
229
+
230
+ def read_spectrogram(
231
+ self
232
+ ) -> Spectrogram:
233
+ """Read and return the spectrogram data stored in the batch.
234
+
235
+ :return: The spectrogram stored by the batch `spectrogram_file`.
236
+ """
237
+ return self.spectrogram_file.read()
238
+