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