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
@@ -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