spectre-core 0.0.22__py3-none-any.whl → 0.0.23__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/_file_io/__init__.py +4 -4
- spectre_core/_file_io/file_handlers.py +60 -106
- spectre_core/batches/__init__.py +20 -3
- spectre_core/batches/_base.py +85 -134
- spectre_core/batches/_batches.py +55 -99
- spectre_core/batches/_factory.py +21 -20
- spectre_core/batches/_register.py +8 -8
- spectre_core/batches/plugins/_batch_keys.py +7 -6
- spectre_core/batches/plugins/_callisto.py +65 -97
- spectre_core/batches/plugins/_iq_stream.py +105 -169
- spectre_core/capture_configs/__init__.py +46 -17
- spectre_core/capture_configs/_capture_config.py +25 -52
- spectre_core/capture_configs/_capture_modes.py +8 -6
- spectre_core/capture_configs/_capture_templates.py +50 -110
- spectre_core/capture_configs/_parameters.py +37 -74
- spectre_core/capture_configs/_pconstraints.py +40 -40
- spectre_core/capture_configs/_pnames.py +36 -34
- spectre_core/capture_configs/_ptemplates.py +260 -347
- spectre_core/capture_configs/_pvalidators.py +99 -102
- spectre_core/config/__init__.py +13 -8
- spectre_core/config/_paths.py +18 -35
- spectre_core/config/_time_formats.py +6 -5
- spectre_core/exceptions.py +38 -0
- spectre_core/jobs/__init__.py +3 -6
- spectre_core/jobs/_duration.py +12 -0
- spectre_core/jobs/_jobs.py +72 -43
- spectre_core/jobs/_workers.py +55 -105
- spectre_core/logs/__init__.py +7 -2
- spectre_core/logs/_configure.py +13 -17
- spectre_core/logs/_decorators.py +6 -4
- spectre_core/logs/_logs.py +37 -89
- spectre_core/logs/_process_types.py +5 -3
- spectre_core/plotting/__init__.py +13 -3
- spectre_core/plotting/_base.py +64 -138
- spectre_core/plotting/_format.py +10 -8
- spectre_core/plotting/_panel_names.py +7 -5
- spectre_core/plotting/_panel_stack.py +82 -115
- spectre_core/plotting/_panels.py +120 -155
- spectre_core/post_processing/__init__.py +6 -3
- spectre_core/post_processing/_base.py +41 -55
- spectre_core/post_processing/_factory.py +14 -11
- spectre_core/post_processing/_post_processor.py +16 -12
- spectre_core/post_processing/_register.py +10 -7
- spectre_core/post_processing/plugins/_event_handler_keys.py +4 -3
- spectre_core/post_processing/plugins/_fixed_center_frequency.py +54 -47
- spectre_core/post_processing/plugins/_swept_center_frequency.py +199 -174
- spectre_core/receivers/__init__.py +9 -2
- spectre_core/receivers/_base.py +82 -148
- spectre_core/receivers/_factory.py +20 -30
- spectre_core/receivers/_register.py +7 -10
- spectre_core/receivers/_spec_names.py +17 -15
- spectre_core/receivers/plugins/_b200mini.py +47 -60
- spectre_core/receivers/plugins/_receiver_names.py +8 -6
- spectre_core/receivers/plugins/_rsp1a.py +44 -40
- spectre_core/receivers/plugins/_rspduo.py +59 -44
- spectre_core/receivers/plugins/_sdrplay_receiver.py +67 -83
- spectre_core/receivers/plugins/_test.py +136 -129
- spectre_core/receivers/plugins/_usrp.py +93 -85
- spectre_core/receivers/plugins/gr/__init__.py +1 -1
- spectre_core/receivers/plugins/gr/_base.py +14 -22
- spectre_core/receivers/plugins/gr/_rsp1a.py +53 -60
- spectre_core/receivers/plugins/gr/_rspduo.py +77 -89
- spectre_core/receivers/plugins/gr/_test.py +49 -57
- spectre_core/receivers/plugins/gr/_usrp.py +61 -59
- spectre_core/spectrograms/__init__.py +21 -13
- spectre_core/spectrograms/_analytical.py +108 -99
- spectre_core/spectrograms/_array_operations.py +39 -46
- spectre_core/spectrograms/_spectrogram.py +289 -322
- spectre_core/spectrograms/_transform.py +106 -73
- spectre_core/wgetting/__init__.py +1 -3
- spectre_core/wgetting/_callisto.py +87 -93
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.23.dist-info}/METADATA +9 -23
- spectre_core-0.0.23.dist-info/RECORD +79 -0
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.23.dist-info}/WHEEL +1 -1
- spectre_core-0.0.22.dist-info/RECORD +0 -78
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.23.dist-info}/licenses/LICENSE +0 -0
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.23.dist-info}/top_level.txt +0 -0
spectre_core/batches/_batches.py
CHANGED
@@ -10,22 +10,23 @@ from datetime import datetime
|
|
10
10
|
from spectre_core.config import TimeFormat
|
11
11
|
from spectre_core.spectrograms import Spectrogram, time_chop, join_spectrograms
|
12
12
|
from spectre_core.config import get_batches_dir_path
|
13
|
-
from spectre_core.exceptions import
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
from spectre_core.exceptions import BatchNotFoundError
|
14
|
+
from ._base import BaseBatch, parse_batch_file_name
|
15
|
+
|
16
|
+
T = TypeVar("T", bound=BaseBatch)
|
17
|
+
|
17
18
|
|
18
|
-
T = TypeVar('T', bound=BaseBatch)
|
19
19
|
class Batches(Generic[T]):
|
20
20
|
"""Managed collection of `Batch` instances for a given tag. Provides a simple
|
21
21
|
interface for read operations on batched data files."""
|
22
|
+
|
22
23
|
def __init__(
|
23
|
-
self,
|
24
|
+
self,
|
24
25
|
tag: str,
|
25
26
|
batch_cls: Type[T],
|
26
|
-
year: Optional[int] = None,
|
27
|
-
month: Optional[int] = None,
|
28
|
-
day: Optional[int] = None
|
27
|
+
year: Optional[int] = None,
|
28
|
+
month: Optional[int] = None,
|
29
|
+
day: Optional[int] = None,
|
29
30
|
) -> None:
|
30
31
|
"""Initialise a `Batches` instance.
|
31
32
|
|
@@ -40,85 +41,54 @@ class Batches(Generic[T]):
|
|
40
41
|
self._batch_map: dict[str, T] = OrderedDict()
|
41
42
|
self.set_date(year, month, day)
|
42
43
|
|
43
|
-
|
44
44
|
@property
|
45
|
-
def tag(
|
46
|
-
self
|
47
|
-
) -> str:
|
45
|
+
def tag(self) -> str:
|
48
46
|
"""The batch name tag."""
|
49
47
|
return self._tag
|
50
48
|
|
51
|
-
|
52
49
|
@property
|
53
|
-
def batch_cls(
|
54
|
-
self
|
55
|
-
) -> Type[T]:
|
50
|
+
def batch_cls(self) -> Type[T]:
|
56
51
|
"""The `Batch` class used to read the batched files."""
|
57
52
|
return self._batch_cls
|
58
53
|
|
59
|
-
|
60
54
|
@property
|
61
|
-
def year(
|
62
|
-
self
|
63
|
-
) -> Optional[int]:
|
55
|
+
def year(self) -> Optional[int]:
|
64
56
|
"""The numeric year, to filter batch files."""
|
65
57
|
return self._year
|
66
58
|
|
67
|
-
|
68
|
-
|
69
|
-
def month(
|
70
|
-
self
|
71
|
-
) -> Optional[int]:
|
59
|
+
@property
|
60
|
+
def month(self) -> Optional[int]:
|
72
61
|
"""The numeric month of the year, to filter batch files."""
|
73
62
|
return self._month
|
74
|
-
|
75
63
|
|
76
64
|
@property
|
77
|
-
def day(
|
78
|
-
self
|
79
|
-
) -> Optional[int]:
|
65
|
+
def day(self) -> Optional[int]:
|
80
66
|
"""The numeric day of the year, to filter batch files."""
|
81
67
|
return self._day
|
82
|
-
|
83
68
|
|
84
69
|
@property
|
85
|
-
def batches_dir_path(
|
86
|
-
self
|
87
|
-
) -> str:
|
70
|
+
def batches_dir_path(self) -> str:
|
88
71
|
"""The shared ancestral path for all the batches. `Batches` recursively searches
|
89
72
|
this directory to find all batches whose batch name contains `tag`."""
|
90
73
|
return get_batches_dir_path(self.year, self.month, self.day)
|
91
|
-
|
92
74
|
|
93
75
|
@property
|
94
|
-
def batch_list(
|
95
|
-
self
|
96
|
-
) -> list[T]:
|
76
|
+
def batch_list(self) -> list[T]:
|
97
77
|
"""A list of all batches found within `batches_dir_path`."""
|
98
|
-
return
|
99
|
-
|
78
|
+
return list(self._batch_map.values())
|
100
79
|
|
101
80
|
@property
|
102
|
-
def start_times(
|
103
|
-
self
|
104
|
-
) -> list[str]:
|
81
|
+
def start_times(self) -> list[str]:
|
105
82
|
"""The start times of each batch found within `batches_dir_path`."""
|
106
83
|
return list(self._batch_map.keys())
|
107
84
|
|
108
|
-
|
109
85
|
@property
|
110
|
-
def num_batches(
|
111
|
-
self
|
112
|
-
) -> int:
|
86
|
+
def num_batches(self) -> int:
|
113
87
|
"""The total number of batches found within `batches_dir_path`."""
|
114
88
|
return len(self.batch_list)
|
115
89
|
|
116
|
-
|
117
90
|
def set_date(
|
118
|
-
self,
|
119
|
-
year: Optional[int],
|
120
|
-
month: Optional[int],
|
121
|
-
day: Optional[int]
|
91
|
+
self, year: Optional[int], month: Optional[int], day: Optional[int]
|
122
92
|
) -> None:
|
123
93
|
"""Reset `batches_dir_path` according to the numeric date, and refresh the list
|
124
94
|
of available batches.
|
@@ -132,72 +102,57 @@ class Batches(Generic[T]):
|
|
132
102
|
self._day = day
|
133
103
|
self.update()
|
134
104
|
|
135
|
-
|
136
|
-
|
137
|
-
self
|
138
|
-
) -> None:
|
139
|
-
"""Perform a fresh search all files in `batches_dir_path` for batches
|
105
|
+
def update(self) -> None:
|
106
|
+
"""Perform a fresh search all files in `batches_dir_path` for batches
|
140
107
|
with `tag` in the batch name."""
|
141
108
|
# reset cache
|
142
|
-
self._batch_map = OrderedDict()
|
143
|
-
|
109
|
+
self._batch_map = OrderedDict()
|
110
|
+
|
144
111
|
# get a list of all batch file names in the batches directory path
|
145
|
-
batch_file_names = [
|
112
|
+
batch_file_names = [
|
113
|
+
f for (_, _, files) in os.walk(self.batches_dir_path) for f in files
|
114
|
+
]
|
146
115
|
for batch_file_name in batch_file_names:
|
147
|
-
start_time, tag, _ =
|
116
|
+
start_time, tag, _ = parse_batch_file_name(batch_file_name)
|
148
117
|
if tag == self._tag:
|
149
118
|
self._batch_map[start_time] = self.batch_cls(start_time, tag)
|
150
|
-
|
119
|
+
|
151
120
|
self._batch_map = OrderedDict(sorted(self._batch_map.items()))
|
152
|
-
|
153
121
|
|
154
|
-
def __iter__(
|
155
|
-
self
|
156
|
-
) -> Iterator[T]:
|
122
|
+
def __iter__(self) -> Iterator[T]:
|
157
123
|
"""Iterate over the stored batch instances."""
|
158
124
|
yield from self.batch_list
|
159
|
-
|
160
|
-
|
161
|
-
def __len__(
|
162
|
-
self
|
163
|
-
):
|
164
|
-
return self.num_batches
|
165
125
|
|
126
|
+
def __len__(self):
|
127
|
+
return self.num_batches
|
166
128
|
|
167
|
-
def _get_from_start_time(
|
168
|
-
self,
|
169
|
-
start_time: str
|
170
|
-
) -> T:
|
129
|
+
def _get_from_start_time(self, start_time: str) -> T:
|
171
130
|
"""Find and return the `Batch` instance based on the string formatted start time."""
|
172
131
|
try:
|
173
132
|
return self._batch_map[start_time]
|
174
133
|
except KeyError:
|
175
|
-
raise BatchNotFoundError(
|
134
|
+
raise BatchNotFoundError(
|
135
|
+
f"Batch with start time {start_time} could not be found within {self.batches_dir_path}"
|
136
|
+
)
|
176
137
|
|
177
|
-
|
178
|
-
def _get_from_index(
|
179
|
-
self,
|
180
|
-
index: int
|
181
|
-
) -> T:
|
138
|
+
def _get_from_index(self, index: int) -> T:
|
182
139
|
"""Find and return the `Batch` instance based on its numeric index.
|
183
|
-
|
140
|
+
|
184
141
|
Batches are ordered sequentially in time, so index `0` corresponds to the oldest
|
185
142
|
`Batch` with respect to the start time.
|
186
143
|
"""
|
187
144
|
if self.num_batches == 0:
|
188
145
|
raise BatchNotFoundError("No batches are available")
|
189
146
|
elif index > self.num_batches:
|
190
|
-
raise IndexError(
|
147
|
+
raise IndexError(
|
148
|
+
f"Index '{index}' is greater than the number of batches '{self.num_batches}'"
|
149
|
+
)
|
191
150
|
return self.batch_list[index]
|
192
151
|
|
193
|
-
|
194
|
-
def __getitem__(
|
195
|
-
self,
|
196
|
-
subscript: str | int
|
197
|
-
) -> T:
|
152
|
+
def __getitem__(self, subscript: str | int) -> T:
|
198
153
|
"""Get a `Batch` instanced based on either the start time or chronological index.
|
199
154
|
|
200
|
-
:param subscript: If the subscript is a string, interpreted as a formatted start time.
|
155
|
+
:param subscript: If the subscript is a string, interpreted as a formatted start time.
|
201
156
|
If the subscript is an integer, it is interpreted as a chronological index.
|
202
157
|
:return: The corresponding `BaseBatch` subclass.
|
203
158
|
"""
|
@@ -205,22 +160,19 @@ class Batches(Generic[T]):
|
|
205
160
|
return self._get_from_start_time(subscript)
|
206
161
|
elif isinstance(subscript, int):
|
207
162
|
return self._get_from_index(subscript)
|
208
|
-
|
209
163
|
|
210
164
|
def get_spectrogram(
|
211
|
-
self,
|
212
|
-
start_datetime: datetime,
|
213
|
-
end_datetime: datetime
|
165
|
+
self, start_datetime: datetime, end_datetime: datetime
|
214
166
|
) -> Spectrogram:
|
215
167
|
"""
|
216
|
-
Retrieve a spectrogram spanning the specified time range.
|
168
|
+
Retrieve a spectrogram spanning the specified time range.
|
217
169
|
|
218
170
|
:param start_datetime: The start time of the range (inclusive).
|
219
171
|
:param end_datetime: The end time of the range (inclusive).
|
220
172
|
:raises FileNotFoundError: If no spectrogram data is available within the specified time range.
|
221
173
|
:return: A spectrogram created by stitching together data from all matching batches.
|
222
174
|
"""
|
223
|
-
|
175
|
+
|
224
176
|
spectrograms = []
|
225
177
|
for batch in self:
|
226
178
|
# skip batches without spectrogram data
|
@@ -233,10 +185,14 @@ class Batches(Generic[T]):
|
|
233
185
|
|
234
186
|
# Check if the batch overlaps with the input time range
|
235
187
|
if start_datetime <= upper_bound and lower_bound <= end_datetime:
|
236
|
-
spectrograms.append(
|
188
|
+
spectrograms.append(
|
189
|
+
time_chop(spectrogram, start_datetime, end_datetime)
|
190
|
+
)
|
237
191
|
|
238
192
|
if spectrograms:
|
239
193
|
return join_spectrograms(spectrograms)
|
240
194
|
else:
|
241
|
-
raise FileNotFoundError(
|
242
|
-
|
195
|
+
raise FileNotFoundError(
|
196
|
+
f"No spectrogram data found for the time range "
|
197
|
+
f"{start_datetime} to {end_datetime}."
|
198
|
+
)
|
spectre_core/batches/_factory.py
CHANGED
@@ -11,25 +11,22 @@ from ._register import batch_map
|
|
11
11
|
from .plugins._batch_keys import BatchKey
|
12
12
|
from .plugins._callisto import CallistoBatch
|
13
13
|
from .plugins._iq_stream import IQStreamBatch
|
14
|
-
|
14
|
+
|
15
15
|
|
16
16
|
@overload
|
17
17
|
def get_batch_cls(
|
18
18
|
batch_key: Literal[BatchKey.CALLISTO],
|
19
|
-
) -> Type[CallistoBatch]:
|
20
|
-
...
|
19
|
+
) -> Type[CallistoBatch]: ...
|
21
20
|
|
22
21
|
|
23
22
|
@overload
|
24
23
|
def get_batch_cls(
|
25
24
|
batch_key: Literal[BatchKey.IQ_STREAM],
|
26
|
-
) -> Type[IQStreamBatch]:
|
27
|
-
|
28
|
-
|
29
|
-
|
25
|
+
) -> Type[IQStreamBatch]: ...
|
26
|
+
|
27
|
+
|
30
28
|
@overload
|
31
|
-
def get_batch_cls(batch_key: BatchKey) -> Type[BaseBatch]:
|
32
|
-
...
|
29
|
+
def get_batch_cls(batch_key: BatchKey) -> Type[BaseBatch]: ...
|
33
30
|
|
34
31
|
|
35
32
|
def get_batch_cls(
|
@@ -44,14 +41,14 @@ def get_batch_cls(
|
|
44
41
|
batch_cls = batch_map.get(batch_key)
|
45
42
|
if batch_cls is None:
|
46
43
|
valid_batch_keys = list(batch_map.keys())
|
47
|
-
raise BatchNotFoundError(
|
48
|
-
|
44
|
+
raise BatchNotFoundError(
|
45
|
+
f"No batch found for the batch key: {batch_key}. "
|
46
|
+
f"Valid batch keys are: {valid_batch_keys}"
|
47
|
+
)
|
49
48
|
return batch_cls
|
50
49
|
|
51
50
|
|
52
|
-
def get_batch_cls_from_tag(
|
53
|
-
tag: str
|
54
|
-
) -> Type[BaseBatch]:
|
51
|
+
def get_batch_cls_from_tag(tag: str) -> Type[BaseBatch]:
|
55
52
|
# if the tag is reserved (i.e., corresponds to third-party spectrogram data)
|
56
53
|
# directly fetch the right class.
|
57
54
|
if "callisto" in tag:
|
@@ -61,9 +58,13 @@ def get_batch_cls_from_tag(
|
|
61
58
|
else:
|
62
59
|
capture_config = CaptureConfig(tag)
|
63
60
|
if PName.BATCH_KEY not in capture_config.parameters.name_list:
|
64
|
-
raise ValueError(
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
61
|
+
raise ValueError(
|
62
|
+
f"Could not infer batch class from the tag 'tag'. "
|
63
|
+
f"A parameter with name `{PName.BATCH_KEY.value}` "
|
64
|
+
f"does not exist."
|
65
|
+
)
|
66
|
+
|
67
|
+
batch_key = BatchKey(
|
68
|
+
cast(str, capture_config.get_parameter_value(PName.BATCH_KEY))
|
69
|
+
)
|
70
|
+
return get_batch_cls(batch_key)
|
@@ -10,21 +10,21 @@ from .plugins._batch_keys import BatchKey
|
|
10
10
|
# Map populated at runtime via the `register_batch` decorator.
|
11
11
|
batch_map: dict[BatchKey, Type[BaseBatch]] = {}
|
12
12
|
|
13
|
-
T = TypeVar(
|
14
|
-
|
15
|
-
|
16
|
-
) -> Callable[[Type[T]], Type[T]]:
|
13
|
+
T = TypeVar("T", bound=BaseBatch)
|
14
|
+
|
15
|
+
|
16
|
+
def register_batch(batch_key: BatchKey) -> Callable[[Type[T]], Type[T]]:
|
17
17
|
"""Decorator to register a `BaseBatch` subclass under a specified `BatchKey`.
|
18
18
|
|
19
19
|
:param batch_key: The key to register the `BaseBatch` subclass under.
|
20
20
|
:raises ValueError: If the provided `batch_key` is already registered.
|
21
21
|
:return: A decorator that registers the `BaseBatch` subclass under the given `batch_key`.
|
22
22
|
"""
|
23
|
-
|
24
|
-
|
25
|
-
) -> Type[T]:
|
23
|
+
|
24
|
+
def decorator(cls: Type[T]) -> Type[T]:
|
26
25
|
if batch_key in batch_map:
|
27
26
|
raise ValueError(f"A batch with key '{batch_key}' is already registered!")
|
28
27
|
batch_map[batch_key] = cls
|
29
28
|
return cls
|
30
|
-
|
29
|
+
|
30
|
+
return decorator
|
@@ -4,13 +4,14 @@
|
|
4
4
|
|
5
5
|
from enum import Enum
|
6
6
|
|
7
|
-
|
7
|
+
|
8
|
+
class BatchKey(Enum):
|
8
9
|
"""Key bound to a `Batch` plugin class.
|
9
|
-
|
10
|
-
:ivar IQ_STREAM: Represents the default batch data generated by `spectre`,
|
10
|
+
|
11
|
+
:ivar IQ_STREAM: Represents the default batch data generated by `spectre`,
|
11
12
|
containing IQ stream data and other data derived from it.
|
12
13
|
:ivar CALLISTO: Represents FITS files generated by the e-Callisto network.
|
13
14
|
"""
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
|
16
|
+
IQ_STREAM = "iq_stream"
|
17
|
+
CALLISTO = "callisto"
|
@@ -21,107 +21,89 @@ from .._register import register_batch
|
|
21
21
|
@dataclass(frozen=True)
|
22
22
|
class _BatchExtension:
|
23
23
|
"""Supported extensions for a `CallistoBatch`.
|
24
|
-
|
24
|
+
|
25
25
|
:ivar FITS: Corresponds to the `.fits` file extension.
|
26
26
|
"""
|
27
|
+
|
27
28
|
FITS: str = "fits"
|
28
|
-
|
29
|
-
|
29
|
+
|
30
|
+
|
30
31
|
class _FitsFile(BatchFile[Spectrogram]):
|
31
32
|
"""Stores spectrogram data in the FITS format generated by the e-Callisto network."""
|
32
|
-
|
33
|
-
|
34
|
-
parent_dir_path: str,
|
35
|
-
base_file_name: str
|
36
|
-
) -> None:
|
33
|
+
|
34
|
+
def __init__(self, parent_dir_path: str, base_file_name: str) -> None:
|
37
35
|
"""Initialise a `_FitsFile` instance.
|
38
36
|
|
39
37
|
:param parent_dir_path: The parent directory for the batch.
|
40
38
|
:param base_file_name: The batch name.
|
41
39
|
"""
|
42
|
-
super().__init__(parent_dir_path,
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
def _read(
|
48
|
-
self
|
49
|
-
) -> Spectrogram:
|
40
|
+
super().__init__(parent_dir_path, base_file_name, _BatchExtension.FITS)
|
41
|
+
|
42
|
+
def _read(self) -> Spectrogram:
|
50
43
|
"""Parses a FITS file to generate a `Spectrogram` instance.
|
51
44
|
|
52
|
-
Reverses the spectra along the frequency axis and converts units to linearised
|
45
|
+
Reverses the spectra along the frequency axis and converts units to linearised
|
53
46
|
values if necessary. Infers the spectrum type from the `BUNIT` header.
|
54
47
|
|
55
48
|
:raises NotImplementedError: If the `BUNIT` header value represents an unsupported spectrum type.
|
56
49
|
:return: A `Spectrogram` instance containing the parsed FITS file data.
|
57
50
|
"""
|
58
|
-
with fits.open(self.file_path, mode=
|
59
|
-
primary_hdu
|
60
|
-
dynamic_spectra
|
61
|
-
spectrogram_start_datetime = self._get_spectrogram_start_datetime(
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
51
|
+
with fits.open(self.file_path, mode="readonly") as hdulist:
|
52
|
+
primary_hdu = self._get_primary_hdu(hdulist)
|
53
|
+
dynamic_spectra = self._get_dynamic_spectra(primary_hdu)
|
54
|
+
spectrogram_start_datetime = self._get_spectrogram_start_datetime(
|
55
|
+
primary_hdu
|
56
|
+
)
|
57
|
+
bintable_hdu = self._get_bintable_hdu(hdulist)
|
58
|
+
times = self._get_times(bintable_hdu)
|
59
|
+
frequencies = self._get_frequencies(bintable_hdu)
|
60
|
+
bunit = self._get_bunit(primary_hdu)
|
66
61
|
|
67
62
|
# bunit is interpreted as a SpectrumUnit
|
68
63
|
spectrum_unit = SpectrumUnit(bunit)
|
69
64
|
if spectrum_unit == SpectrumUnit.DIGITS:
|
70
|
-
dynamic_spectra_linearised = self._convert_units_to_linearised(
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
65
|
+
dynamic_spectra_linearised = self._convert_units_to_linearised(
|
66
|
+
dynamic_spectra
|
67
|
+
)
|
68
|
+
|
69
|
+
return Spectrogram(
|
70
|
+
dynamic_spectra_linearised[
|
71
|
+
::-1, :
|
72
|
+
], # reverse the spectra along the frequency axis
|
73
|
+
times,
|
74
|
+
frequencies[::-1], # sort the frequencies in ascending order
|
75
|
+
self.tag,
|
76
|
+
spectrum_unit,
|
77
|
+
spectrogram_start_datetime,
|
78
|
+
)
|
78
79
|
else:
|
79
|
-
raise NotImplementedError(
|
80
|
+
raise NotImplementedError(
|
81
|
+
f"SPECTRE does not currently support spectrum type with BUNITS '{spectrum_unit}'"
|
82
|
+
)
|
80
83
|
|
84
|
+
def _get_primary_hdu(self, hdulist: HDUList) -> PrimaryHDU:
|
85
|
+
return hdulist["PRIMARY"]
|
81
86
|
|
82
|
-
def
|
83
|
-
self, hdulist: HDUList
|
84
|
-
) -> PrimaryHDU:
|
85
|
-
return hdulist['PRIMARY']
|
86
|
-
|
87
|
-
|
88
|
-
def _get_bintable_hdu(
|
89
|
-
self,
|
90
|
-
hdulist: HDUList
|
91
|
-
) -> BinTableHDU:
|
87
|
+
def _get_bintable_hdu(self, hdulist: HDUList) -> BinTableHDU:
|
92
88
|
return hdulist[1]
|
93
|
-
|
94
|
-
|
95
|
-
def _get_dynamic_spectra(
|
96
|
-
self,
|
97
|
-
primary_hdu: PrimaryHDU
|
98
|
-
) -> npt.NDArray[np.float32]:
|
99
|
-
return primary_hdu.data.astype(np.float32)
|
100
89
|
|
90
|
+
def _get_dynamic_spectra(self, primary_hdu: PrimaryHDU) -> npt.NDArray[np.float32]:
|
91
|
+
return primary_hdu.data.astype(np.float32)
|
101
92
|
|
102
|
-
def _get_spectrogram_start_datetime(
|
103
|
-
|
104
|
-
primary_hdu
|
105
|
-
) -> datetime:
|
106
|
-
date_obs = primary_hdu.header['DATE-OBS']
|
107
|
-
time_obs = primary_hdu.header['TIME-OBS']
|
93
|
+
def _get_spectrogram_start_datetime(self, primary_hdu: PrimaryHDU) -> datetime:
|
94
|
+
date_obs = primary_hdu.header["DATE-OBS"]
|
95
|
+
time_obs = primary_hdu.header["TIME-OBS"]
|
108
96
|
return datetime.strptime(f"{date_obs}T{time_obs}", "%Y/%m/%dT%H:%M:%S.%f")
|
109
97
|
|
110
|
-
|
111
|
-
|
112
|
-
self,
|
113
|
-
primary_hdu: PrimaryHDU
|
114
|
-
) -> str:
|
115
|
-
return primary_hdu.header['BUNIT']
|
116
|
-
|
98
|
+
def _get_bunit(self, primary_hdu: PrimaryHDU) -> str:
|
99
|
+
return primary_hdu.header["BUNIT"]
|
117
100
|
|
118
101
|
def _convert_units_to_linearised(
|
119
|
-
self,
|
120
|
-
raw_digits: npt.NDArray[np.float32]
|
102
|
+
self, raw_digits: npt.NDArray[np.float32]
|
121
103
|
) -> npt.NDArray[np.float32]:
|
122
104
|
"""Converts spectrogram data from raw digit values to linearised units.
|
123
105
|
|
124
|
-
Applies a transformation based on ADC specifications to convert raw values
|
106
|
+
Applies a transformation based on ADC specifications to convert raw values
|
125
107
|
to dB and then to linearised units.
|
126
108
|
|
127
109
|
:param dynamic_spectra: Raw dynamic spectra in digit values.
|
@@ -130,54 +112,40 @@ class _FitsFile(BatchFile[Spectrogram]):
|
|
130
112
|
# conversion as per ADC specs [see email from C. Monstein]
|
131
113
|
dB = (raw_digits / 255) * (2500 / 25)
|
132
114
|
return 10 ** (dB / 10)
|
133
|
-
|
134
|
-
|
135
|
-
def _get_times(
|
136
|
-
self,
|
137
|
-
bintable_hdu: BinTableHDU
|
138
|
-
) -> npt.NDArray[np.float32]:
|
115
|
+
|
116
|
+
def _get_times(self, bintable_hdu: BinTableHDU) -> npt.NDArray[np.float32]:
|
139
117
|
"""Extracts the elapsed times for each spectrum in seconds, with the first spectrum set to t=0
|
140
118
|
by convention.
|
141
119
|
"""
|
142
|
-
return bintable_hdu.data[
|
120
|
+
return bintable_hdu.data["TIME"][0] # already in seconds
|
143
121
|
|
144
|
-
|
145
|
-
def _get_frequencies(
|
146
|
-
self,
|
147
|
-
bintable_hdu: BinTableHDU
|
148
|
-
) -> npt.NDArray[np.float32]:
|
122
|
+
def _get_frequencies(self, bintable_hdu: BinTableHDU) -> npt.NDArray[np.float32]:
|
149
123
|
"""Extracts the frequencies for each spectral component."""
|
150
|
-
frequencies_MHz = bintable_hdu.data[
|
151
|
-
return frequencies_MHz * 1e6
|
152
|
-
|
153
|
-
|
124
|
+
frequencies_MHz = bintable_hdu.data["FREQUENCY"][0]
|
125
|
+
return frequencies_MHz * 1e6 # convert to Hz
|
126
|
+
|
127
|
+
|
154
128
|
@register_batch(BatchKey.CALLISTO)
|
155
129
|
class CallistoBatch(BaseBatch):
|
156
130
|
"""A batch of data generated by the e-Callisto network.
|
157
|
-
|
131
|
+
|
158
132
|
Supports the following file extensions:
|
159
133
|
- `.fits` (via the `spectrogram_file` attribute)
|
160
134
|
"""
|
161
|
-
|
162
|
-
|
163
|
-
start_time: str,
|
164
|
-
tag: str
|
165
|
-
) -> None:
|
135
|
+
|
136
|
+
def __init__(self, start_time: str, tag: str) -> None:
|
166
137
|
"""Initialise a `CallistoBatch` instance.
|
167
138
|
|
168
139
|
:param start_time: The start time of the batch.
|
169
140
|
:param tag: The batch name tag.
|
170
141
|
"""
|
171
|
-
super().__init__(start_time, tag)
|
142
|
+
super().__init__(start_time, tag)
|
172
143
|
self._fits_file = _FitsFile(self.parent_dir_path, self.name)
|
173
|
-
|
144
|
+
|
174
145
|
# add files formally to the batch
|
175
|
-
self.add_file(
|
176
|
-
|
177
|
-
|
146
|
+
self.add_file(self.spectrogram_file)
|
147
|
+
|
178
148
|
@property
|
179
|
-
def spectrogram_file(
|
180
|
-
self
|
181
|
-
) -> _FitsFile:
|
149
|
+
def spectrogram_file(self) -> _FitsFile:
|
182
150
|
"""The batch file corresponding to the `.fits` extension."""
|
183
151
|
return self._fits_file
|