spectre-core 0.0.12__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 +163 -58
- spectre_core/batches/__init__.py +10 -11
- spectre_core/batches/_base.py +170 -78
- spectre_core/batches/_batches.py +149 -99
- spectre_core/batches/_factory.py +56 -14
- spectre_core/batches/_register.py +23 -8
- 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 +115 -42
- 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 +117 -73
- spectre_core/config/__init__.py +6 -8
- spectre_core/config/_paths.py +65 -25
- spectre_core/config/_time_formats.py +15 -10
- 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 +70 -44
- spectre_core/post_processing/_factory.py +42 -12
- spectre_core/post_processing/_post_processor.py +24 -26
- 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/{library → plugins}/_swept_center_frequency.py +215 -143
- 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 +42 -52
- spectre_core/receivers/{gr → plugins/gr}/_rspduo.py +61 -74
- spectre_core/receivers/{gr → plugins/gr}/_test.py +33 -31
- spectre_core/spectrograms/__init__.py +5 -3
- spectre_core/spectrograms/_analytical.py +121 -66
- spectre_core/spectrograms/_array_operations.py +103 -36
- spectre_core/spectrograms/_spectrogram.py +380 -207
- spectre_core/spectrograms/_transform.py +197 -169
- spectre_core/wgetting/__init__.py +4 -2
- spectre_core/wgetting/_callisto.py +173 -118
- {spectre_core-0.0.12.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.12.dist-info → spectre_core-0.0.13.dist-info}/WHEEL +1 -1
- spectre_core/batches/library/_callisto.py +0 -96
- spectre_core/batches/library/_fixed_center_frequency.py +0 -133
- spectre_core/batches/library/_swept_center_frequency.py +0 -105
- 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 -114
- 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.12.dist-info/RECORD +0 -64
- /spectre_core/receivers/{gr → plugins/gr}/__init__.py +0 -0
- {spectre_core-0.0.12.dist-info → spectre_core-0.0.13.dist-info}/LICENSE +0 -0
- {spectre_core-0.0.12.dist-info → spectre_core-0.0.13.dist-info}/top_level.txt +0 -0
@@ -2,17 +2,19 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
-
from typing import Optional
|
5
|
+
from typing import Optional, cast
|
6
6
|
from warnings import warn
|
7
|
-
from datetime import datetime
|
7
|
+
from datetime import datetime
|
8
8
|
from dataclasses import dataclass
|
9
|
+
from enum import Enum
|
9
10
|
import os
|
10
11
|
|
11
12
|
import numpy as np
|
13
|
+
import numpy.typing as npt
|
12
14
|
from astropy.io import fits
|
13
15
|
|
14
|
-
from spectre_core.capture_configs import CaptureConfig,
|
15
|
-
from spectre_core.config import get_batches_dir_path,
|
16
|
+
from spectre_core.capture_configs import CaptureConfig, PName
|
17
|
+
from spectre_core.config import get_batches_dir_path, TimeFormat
|
16
18
|
from ._array_operations import (
|
17
19
|
find_closest_index,
|
18
20
|
normalise_peak_intensity,
|
@@ -22,250 +24,369 @@ from ._array_operations import (
|
|
22
24
|
)
|
23
25
|
|
24
26
|
|
27
|
+
class SpectrumUnit(Enum):
|
28
|
+
"""A defined unit for dynamic spectra values.
|
29
|
+
|
30
|
+
:ivar AMPLITUDE: Formal definition TBC.
|
31
|
+
:ivar POWER: Formal definition TBC.
|
32
|
+
:ivar DIGITS: Formal definition TBC.
|
33
|
+
"""
|
34
|
+
AMPLITUDE = "amplitude"
|
35
|
+
POWER = "power"
|
36
|
+
DIGITS = "digits"
|
37
|
+
|
38
|
+
|
25
39
|
@dataclass
|
26
40
|
class FrequencyCut:
|
27
|
-
"""A
|
41
|
+
"""A cut of a dynamic spectra, at a particular instant of time. Equivalently, some spectrum in
|
42
|
+
the spectrogram.
|
43
|
+
|
44
|
+
:ivar time: The time of the frequency cut, either in relative time (if time is a float)
|
45
|
+
or as a datetime.
|
46
|
+
:ivar frequencies: The physical frequencies assigned to each spectral component, in Hz.
|
47
|
+
:ivar cut: The spectrum values.
|
48
|
+
:ivar spectrum_unit: The unit of each spectrum value.
|
49
|
+
"""
|
28
50
|
time: float | datetime
|
29
|
-
frequencies: np.
|
30
|
-
cut: np.
|
31
|
-
|
51
|
+
frequencies: npt.NDArray[np.float32]
|
52
|
+
cut: npt.NDArray[np.float32]
|
53
|
+
spectrum_unit: SpectrumUnit
|
32
54
|
|
33
55
|
|
34
56
|
@dataclass
|
35
57
|
class TimeCut:
|
36
|
-
"""A
|
58
|
+
"""A cut of a dynamic spectra, at some fixed frequency. Equivalently, a time series of
|
59
|
+
some spectral component in the spectrogram.
|
60
|
+
|
61
|
+
:ivar frequency: The physical frequency assigned to the spectral component, in Hz.
|
62
|
+
:ivar times: The time for each time series value, either as a relative time (if
|
63
|
+
the elements are floats) or as a datetimes.
|
64
|
+
:ivar cut: The time series values of the spectral component.
|
65
|
+
:ivar spectrum_unit: The unit of each time series value.
|
66
|
+
"""
|
37
67
|
frequency: float
|
38
|
-
times: np.
|
39
|
-
cut: np.
|
40
|
-
|
68
|
+
times: npt.NDArray[np.float32 | np.datetime64]
|
69
|
+
cut: npt.NDArray[np.float32]
|
70
|
+
spectrum_unit: SpectrumUnit
|
41
71
|
|
42
72
|
|
43
|
-
|
44
|
-
|
45
|
-
"""Container to hold the different types of time we can assign to each spectrum in the dynamic spectra.
|
73
|
+
class TimeType(Enum):
|
74
|
+
"""The type of time we can assign to each spectrum in the dynamic spectra.
|
46
75
|
|
47
|
-
|
48
|
-
|
76
|
+
:ivar RELATIVE: The elapsed time from the first spectrum, in seconds.
|
77
|
+
:ivar DATETIMES: The datetime associated with each spectrum.
|
49
78
|
"""
|
50
|
-
|
51
|
-
DATETIMES
|
52
|
-
|
53
|
-
|
54
|
-
@dataclass(frozen=True)
|
55
|
-
class SpectrumTypes:
|
56
|
-
"""A container for defined units of dynamic spectra."""
|
57
|
-
AMPLITUDE: str = "amplitude"
|
58
|
-
POWER : str = "power"
|
59
|
-
DIGITS : str = "digits"
|
79
|
+
RELATIVE = "relative"
|
80
|
+
DATETIMES = "datetimes"
|
60
81
|
|
61
82
|
|
62
83
|
class Spectrogram:
|
63
|
-
"""
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
84
|
+
"""Standardised wrapper for spectrogram data, providing a consistent
|
85
|
+
interface for storing, accessing, and manipulating spectrogram data,
|
86
|
+
along with associated metadata.
|
87
|
+
"""
|
88
|
+
def __init__(
|
89
|
+
self,
|
90
|
+
dynamic_spectra: npt.NDArray[np.float32],
|
91
|
+
times : npt.NDArray[np.float32],
|
92
|
+
frequencies : npt.NDArray[np.float32],
|
93
|
+
tag : str,
|
94
|
+
spectrum_unit : SpectrumUnit,
|
95
|
+
start_datetime : Optional[datetime | np.datetime64] = None
|
96
|
+
) -> None:
|
97
|
+
"""Initialise a Spectrogram instance.
|
98
|
+
|
99
|
+
:param dynamic_spectra: A 2D array of spectrogram data.
|
100
|
+
:param times: A 1D array representing the elapsed time of each spectrum, in seconds, relative to the first
|
101
|
+
in the spectrogram.
|
102
|
+
:param frequencies: A 1D array representing the physical frequencies assigned to each spectral component, in Hz.
|
103
|
+
:param tag: A string identifier for the spectrogram.
|
104
|
+
:param spectrum_unit: The unit of the dynamic_spectra values.
|
105
|
+
:param start_datetime: The datetime corresponding to the first spectrum, defaults to None.
|
106
|
+
:raises ValueError: If times does not start at 0 or array shapes are inconsistent.
|
107
|
+
"""
|
71
108
|
|
72
|
-
# dynamic spectra
|
73
109
|
self._dynamic_spectra = dynamic_spectra
|
74
|
-
self._dynamic_spectra_dBb: Optional[np.ndarray] = None # cache
|
75
110
|
|
76
|
-
# assigned times and frequencies
|
77
111
|
if times[0] != 0:
|
78
112
|
raise ValueError(f"The first spectrum must correspond to t=0")
|
79
|
-
|
80
113
|
self._times = times
|
114
|
+
|
81
115
|
self._frequencies = frequencies
|
82
|
-
|
83
|
-
# general metadata
|
84
116
|
self._tag = tag
|
85
|
-
self.
|
86
|
-
|
87
|
-
# datetime information
|
88
|
-
self._start_datetime = start_datetime
|
89
|
-
self._datetimes: Optional[list[datetime]] = None # cache
|
117
|
+
self._spectrum_unit = spectrum_unit
|
118
|
+
self._start_datetime = np.datetime64(start_datetime)
|
90
119
|
|
91
|
-
# background
|
92
|
-
self._background_spectrum: Optional[np.ndarray] = None # cache
|
120
|
+
# by default, the background is evaluated over the whole spectrogram
|
93
121
|
self._start_background_index = 0
|
94
122
|
self._end_background_index = self.num_times
|
95
|
-
# background interval can be set after
|
96
|
-
self._start_background = None
|
97
|
-
self._end_background = None
|
123
|
+
# the background interval can be set after instantiation
|
124
|
+
self._start_background: Optional[str] = None
|
125
|
+
self._end_background : Optional[str] = None
|
98
126
|
|
99
127
|
# finally check that the spectrogram arrays are matching in shape
|
100
128
|
self._check_shapes()
|
101
129
|
|
102
130
|
|
103
131
|
@property
|
104
|
-
def dynamic_spectra(
|
105
|
-
|
132
|
+
def dynamic_spectra(
|
133
|
+
self
|
134
|
+
) -> npt.NDArray[np.float32]:
|
135
|
+
"""The dynamic spectra array.
|
136
|
+
|
137
|
+
Returns the 2D array of spectrogram data with shape (num_frequencies, num_times),
|
138
|
+
where values are in the units specified by `spectrum_unit`.
|
139
|
+
"""
|
106
140
|
return self._dynamic_spectra
|
107
141
|
|
108
142
|
|
109
143
|
@property
|
110
|
-
def times(
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
"""
|
144
|
+
def times(
|
145
|
+
self
|
146
|
+
) -> npt.NDArray[np.float32]:
|
147
|
+
"""A 1D array representing the elapsed time of each spectrum, in seconds, relative to the first
|
148
|
+
in the spectrogram."""
|
116
149
|
return self._times
|
117
150
|
|
118
151
|
|
119
152
|
@property
|
120
|
-
def num_times(
|
153
|
+
def num_times(
|
154
|
+
self
|
155
|
+
) -> int:
|
121
156
|
"""The size of the times array. Equivalent to the number of spectrums in the spectrogram."""
|
122
157
|
return len(self._times)
|
123
158
|
|
124
159
|
|
125
160
|
@property
|
126
|
-
def time_resolution(
|
127
|
-
|
161
|
+
def time_resolution(
|
162
|
+
self
|
163
|
+
) -> float:
|
164
|
+
"""The time resolution of the dynamic spectra.
|
165
|
+
|
166
|
+
Represents the spacing between consecutive time values in the times array,
|
167
|
+
calculated as the median difference between adjacent elements.
|
168
|
+
"""
|
128
169
|
return compute_resolution(self._times)
|
129
170
|
|
130
171
|
|
131
172
|
@property
|
132
|
-
def time_range(
|
133
|
-
|
173
|
+
def time_range(
|
174
|
+
self
|
175
|
+
) -> float:
|
176
|
+
"""The time range of the dynamic spectra.
|
177
|
+
|
178
|
+
Represents the difference between the first and last time values
|
179
|
+
in the times array.
|
180
|
+
"""
|
134
181
|
return compute_range(self._times)
|
135
182
|
|
136
183
|
|
137
184
|
@property
|
138
|
-
def frequencies(
|
139
|
-
|
185
|
+
def frequencies(
|
186
|
+
self
|
187
|
+
) -> npt.NDArray[np.float32]:
|
188
|
+
"""A 1D array representing the physical frequencies assigned to each spectral component, in Hz."""
|
140
189
|
return self._frequencies
|
141
190
|
|
142
191
|
|
143
192
|
@property
|
144
|
-
def num_frequencies(
|
145
|
-
|
193
|
+
def num_frequencies(
|
194
|
+
self
|
195
|
+
) -> int:
|
196
|
+
"""The number of spectral components in the spectrogram.
|
197
|
+
|
198
|
+
Equivalent to the size of the frequencies array.
|
199
|
+
"""
|
146
200
|
return len(self._frequencies)
|
147
201
|
|
148
202
|
|
149
203
|
@property
|
150
|
-
def frequency_resolution(
|
151
|
-
|
204
|
+
def frequency_resolution(
|
205
|
+
self
|
206
|
+
) -> float:
|
207
|
+
"""The frequency resolution of the dynamic spectra.
|
208
|
+
|
209
|
+
Represents the spacing between consecutive frequency values in the frequencies array,
|
210
|
+
calculated as the median difference between adjacent elements.
|
211
|
+
"""
|
152
212
|
return compute_resolution(self._frequencies)
|
153
213
|
|
154
214
|
|
155
215
|
@property
|
156
|
-
def frequency_range(
|
157
|
-
|
216
|
+
def frequency_range(
|
217
|
+
self
|
218
|
+
) -> float:
|
219
|
+
"""The frequency range covered by the dynamic spectra.
|
220
|
+
|
221
|
+
Represents the difference between the minimum and maximum frequency
|
222
|
+
values in the frequencies array.
|
223
|
+
"""
|
158
224
|
return compute_range(self._frequencies)
|
159
225
|
|
160
226
|
|
161
227
|
@property
|
162
|
-
def tag(
|
163
|
-
|
228
|
+
def tag(
|
229
|
+
self
|
230
|
+
) -> str:
|
231
|
+
"""The tag identifier for the spectrogram"""
|
164
232
|
return self._tag
|
165
233
|
|
166
234
|
|
167
235
|
@property
|
168
|
-
def start_datetime_is_set(
|
169
|
-
|
236
|
+
def start_datetime_is_set(
|
237
|
+
self
|
238
|
+
) -> bool:
|
239
|
+
"""Indicates whether the start datetime for the spectrogram has been set."""
|
170
240
|
return (self._start_datetime is not None)
|
171
241
|
|
172
242
|
|
173
243
|
@property
|
174
|
-
def start_datetime(
|
175
|
-
|
244
|
+
def start_datetime(
|
245
|
+
self
|
246
|
+
) -> np.datetime64:
|
247
|
+
"""The datetime assigned to the first spectrum in `dynamic_spectra`.
|
248
|
+
|
249
|
+
:raises AttributeError: If the start_datetime has not been set.
|
250
|
+
"""
|
176
251
|
if self._start_datetime is None:
|
177
252
|
raise AttributeError(f"A start time has not been set.")
|
178
253
|
return self._start_datetime
|
179
254
|
|
180
255
|
|
181
|
-
def format_start_time(self,
|
182
|
-
precise: bool = False) -> str:
|
183
|
-
"""The datetime assigned to the first spectrum in the dynamic spectra, formatted as a string."""
|
184
|
-
if precise:
|
185
|
-
return datetime.strftime(self.start_datetime, TimeFormats.PRECISE_DATETIME)
|
186
|
-
return datetime.strftime(self.start_datetime, TimeFormats.DATETIME)
|
187
|
-
|
188
|
-
|
189
256
|
@property
|
190
|
-
def datetimes(
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
257
|
+
def datetimes(
|
258
|
+
self
|
259
|
+
) -> npt.NDArray[np.datetime64]:
|
260
|
+
"""The datetimes associated with each spectrum in `dynamic_spectra`.
|
261
|
+
|
262
|
+
Returns a list of datetime objects, calculated by adding the elapsed
|
263
|
+
times in the times array to the start_datetime.
|
264
|
+
"""
|
265
|
+
return self.start_datetime + (1e6*self._times).astype('timedelta64[us]')
|
195
266
|
|
196
267
|
|
197
268
|
@property
|
198
|
-
def
|
199
|
-
|
200
|
-
|
269
|
+
def spectrum_unit(
|
270
|
+
self
|
271
|
+
) -> SpectrumUnit:
|
272
|
+
"""The units associated with the `dynamic_spectra` array."""
|
273
|
+
return self._spectrum_unit
|
201
274
|
|
202
275
|
|
203
276
|
@property
|
204
|
-
def start_background(
|
205
|
-
|
277
|
+
def start_background(
|
278
|
+
self
|
279
|
+
) -> Optional[str]:
|
280
|
+
"""The start of the background interval.
|
281
|
+
|
282
|
+
Returns a string-formatted datetime up to seconds precision, or None
|
283
|
+
if the background interval has not been set.
|
284
|
+
"""
|
206
285
|
return self._start_background
|
207
286
|
|
208
287
|
|
209
288
|
@property
|
210
|
-
def end_background(
|
211
|
-
|
289
|
+
def end_background(
|
290
|
+
self
|
291
|
+
) -> Optional[str]:
|
292
|
+
"""The end of the background interval.
|
293
|
+
|
294
|
+
Returns a string-formatted datetime up to seconds precision, or None
|
295
|
+
if the background interval has not been set.
|
296
|
+
"""
|
212
297
|
return self._end_background
|
213
298
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
299
|
+
|
300
|
+
def compute_background_spectrum(
|
301
|
+
self
|
302
|
+
) -> npt.NDArray[np.float32]:
|
303
|
+
"""Compute the background spectrum by averaging the dynamic spectra in time.
|
304
|
+
|
305
|
+
By default, the entire dynamic spectra is averaged. Use set_background to
|
306
|
+
specify a custom background interval.
|
307
|
+
|
308
|
+
:return: A 1D array representing the time-averaged dynamic spectra over the
|
309
|
+
specified background interval.
|
220
310
|
"""
|
221
|
-
|
222
|
-
self._background_spectrum = np.nanmean(self._dynamic_spectra[:, self._start_background_index:self._end_background_index+1],
|
311
|
+
return np.nanmean(self._dynamic_spectra[:, self._start_background_index:self._end_background_index+1],
|
223
312
|
axis=-1)
|
224
|
-
return self._background_spectrum
|
225
313
|
|
226
314
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
315
|
+
def compute_dynamic_spectra_dBb(
|
316
|
+
self
|
317
|
+
) -> npt.NDArray[np.float32]:
|
318
|
+
"""Compute the dynamic spectra in units of decibels above the background spectrum.
|
319
|
+
|
320
|
+
The computation applies logarithmic scaling based on the `spectrum_unit`.
|
321
|
+
|
322
|
+
:raises NotImplementedError: If the spectrum_unit is unrecognised.
|
323
|
+
:return: A 2D array with the same shape as `dynamic_spectra`, representing
|
324
|
+
the values in decibels above the background.
|
325
|
+
"""
|
326
|
+
# Create a 'background' spectrogram where each spectrum is identically the background spectrum
|
327
|
+
background_spectrum = self.compute_background_spectrum()
|
328
|
+
background_spectra = background_spectrum[:, np.newaxis]
|
329
|
+
# Suppress divide by zero and invalid value warnings for this block of code
|
330
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
331
|
+
if self._spectrum_unit == SpectrumUnit.AMPLITUDE or self._spectrum_unit == SpectrumUnit.DIGITS:
|
332
|
+
dynamic_spectra_dBb = 10 * np.log10(self._dynamic_spectra / background_spectra)
|
333
|
+
elif self._spectrum_unit == SpectrumUnit.POWER:
|
334
|
+
dynamic_spectra_dBb = 20 * np.log10(self._dynamic_spectra / background_spectra)
|
335
|
+
else:
|
336
|
+
raise NotImplementedError(f"{self._spectrum_unit} is unrecognised; decibel conversion is uncertain!")
|
337
|
+
return dynamic_spectra_dBb.astype(np.float32)
|
338
|
+
|
339
|
+
|
340
|
+
def format_start_time(
|
341
|
+
self
|
342
|
+
) -> str:
|
343
|
+
"""Format the datetime assigned to the first spectrum in the dynamic spectra.
|
344
|
+
|
345
|
+
:return: A string representation of the `start_datetime`, up to seconds precision.
|
346
|
+
"""
|
347
|
+
dt = self.start_datetime.astype(datetime)
|
348
|
+
return datetime.strftime(dt, TimeFormat.DATETIME)
|
349
|
+
|
350
|
+
|
351
|
+
def set_background(
|
352
|
+
self,
|
353
|
+
start_background: str,
|
354
|
+
end_background: str
|
355
|
+
) -> None:
|
356
|
+
"""Set the background interval for computing the background spectrum, and doing
|
357
|
+
background subtractions.
|
358
|
+
|
359
|
+
:param start_background: The start time of the background interval, formatted as
|
360
|
+
a string in the format `TimeFormat.DATETIME` (up to seconds precision).
|
361
|
+
:param end_background: The end time of the background interval, formatted as
|
362
|
+
a string in the format `TimeFormat.DATETIME` (up to seconds precision).
|
363
|
+
"""
|
251
364
|
self._start_background = start_background
|
252
|
-
self._end_background
|
253
|
-
self._update_background_indices_from_interval(
|
365
|
+
self._end_background = end_background
|
366
|
+
self._update_background_indices_from_interval(self._start_background,
|
367
|
+
self._end_background)
|
254
368
|
|
255
369
|
|
256
370
|
|
257
|
-
def _update_background_indices_from_interval(
|
258
|
-
|
259
|
-
|
260
|
-
|
371
|
+
def _update_background_indices_from_interval(
|
372
|
+
self,
|
373
|
+
start_background: str,
|
374
|
+
end_background: str
|
375
|
+
) -> None:
|
376
|
+
start_background_datetime = np.datetime64( datetime.strptime(start_background, TimeFormat.DATETIME) )
|
377
|
+
end_background_datetime = np.datetime64( datetime.strptime(end_background, TimeFormat.DATETIME) )
|
378
|
+
self._start_background_index = find_closest_index(start_background_datetime,
|
261
379
|
self.datetimes,
|
262
380
|
enforce_strict_bounds=True)
|
263
|
-
self._end_background_index = find_closest_index(
|
381
|
+
self._end_background_index = find_closest_index(end_background_datetime,
|
264
382
|
self.datetimes,
|
265
383
|
enforce_strict_bounds=True)
|
266
384
|
|
267
385
|
|
268
|
-
def _check_shapes(
|
386
|
+
def _check_shapes(
|
387
|
+
self
|
388
|
+
) -> None:
|
389
|
+
"""Check that the data array shapes are consistent with one another."""
|
269
390
|
num_spectrogram_dims = np.ndim(self._dynamic_spectra)
|
270
391
|
# Check if 'dynamic_spectra' is a 2D array
|
271
392
|
if num_spectrogram_dims != 2:
|
@@ -279,16 +400,28 @@ class Spectrogram:
|
|
279
400
|
raise ValueError(f"Mismatch in number of time bins: Expected {self.num_times}, but got {dynamic_spectra_shape[1]}")
|
280
401
|
|
281
402
|
|
282
|
-
def save(
|
283
|
-
|
403
|
+
def save(
|
404
|
+
self
|
405
|
+
) -> None:
|
406
|
+
"""Write the spectrogram and its associated metadata to a batch file in the FITS format."""
|
284
407
|
_save_spectrogram(self)
|
285
408
|
|
286
409
|
|
287
|
-
def integrate_over_frequency(
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
410
|
+
def integrate_over_frequency(
|
411
|
+
self,
|
412
|
+
correct_background: bool = False,
|
413
|
+
peak_normalise: bool = False
|
414
|
+
) -> npt.NDArray[np.float32]:
|
415
|
+
"""Numerically integrate the spectrogram over the frequency axis.
|
416
|
+
|
417
|
+
:param correct_background: Indicates whether to subtract the background after
|
418
|
+
computing the integral, defaults to False
|
419
|
+
:param peak_normalise: Indicates whether to normalise the integral such that
|
420
|
+
the peak value is equal to unity, defaults to False
|
421
|
+
:return: A 1D array containing each spectrum numerically integrated over the
|
422
|
+
frequency axis.
|
423
|
+
"""
|
424
|
+
# numerically integrate over frequency
|
292
425
|
I = np.trapz(self._dynamic_spectra, self._frequencies, axis=0)
|
293
426
|
|
294
427
|
if correct_background:
|
@@ -300,34 +433,46 @@ class Spectrogram:
|
|
300
433
|
return I
|
301
434
|
|
302
435
|
|
303
|
-
def get_frequency_cut(
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
436
|
+
def get_frequency_cut(
|
437
|
+
self,
|
438
|
+
at_time: float | str,
|
439
|
+
dBb: bool = False,
|
440
|
+
peak_normalise: bool = False
|
441
|
+
) -> FrequencyCut:
|
442
|
+
"""Retrieve a cut of the dynamic spectra at a specific time.
|
443
|
+
|
444
|
+
If `at_time` does not match exactly with a time in `times`, the closest match
|
445
|
+
is selected. The cut represents one of the spectrums in the spectrogram.
|
446
|
+
|
447
|
+
:param at_time: The requested time for the cut. If a string, it is parsed
|
448
|
+
as a datetime. If a float, it is treated as elapsed time since the first spectrum.
|
449
|
+
:param dBb: If True, returns the cut in decibels above the background,
|
450
|
+
defaults to False.
|
451
|
+
:param peak_normalise: If True, normalises the cut such that its peak value
|
452
|
+
is equal to 1. Ignored if dBb is True, defaults to False.
|
453
|
+
:raises ValueError: If at_time is not a recognised type.
|
454
|
+
:return: A FrequencyCut object containing the spectral values and associated metadata.
|
311
455
|
"""
|
312
456
|
|
313
457
|
if isinstance(at_time, str):
|
314
|
-
|
315
|
-
index_of_cut = find_closest_index(
|
316
|
-
self.datetimes,
|
458
|
+
_at_datetime = np.datetime64( datetime.strptime(at_time, TimeFormat.DATETIME) )
|
459
|
+
index_of_cut = find_closest_index(_at_datetime,
|
460
|
+
self.datetimes,
|
317
461
|
enforce_strict_bounds = True)
|
318
462
|
time_of_cut = self.datetimes[index_of_cut]
|
319
463
|
|
320
|
-
elif isinstance(at_time,
|
321
|
-
|
464
|
+
elif isinstance(at_time, float):
|
465
|
+
_at_time = np.float32(at_time)
|
466
|
+
index_of_cut = find_closest_index(_at_time,
|
322
467
|
self._times,
|
323
468
|
enforce_strict_bounds = True)
|
324
469
|
time_of_cut = self.times[index_of_cut]
|
325
470
|
|
326
471
|
else:
|
327
|
-
raise ValueError(f"
|
472
|
+
raise ValueError(f"'at_time' type '{type(at_time)}' is unsupported.")
|
328
473
|
|
329
474
|
if dBb:
|
330
|
-
ds = self.
|
475
|
+
ds = self.compute_dynamic_spectra_dBb()
|
331
476
|
else:
|
332
477
|
ds = self._dynamic_spectra
|
333
478
|
|
@@ -343,35 +488,44 @@ class Spectrogram:
|
|
343
488
|
return FrequencyCut(time_of_cut,
|
344
489
|
self._frequencies,
|
345
490
|
cut,
|
346
|
-
self.
|
491
|
+
self._spectrum_unit)
|
347
492
|
|
348
493
|
|
349
|
-
def get_time_cut(
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
494
|
+
def get_time_cut(
|
495
|
+
self,
|
496
|
+
at_frequency: float,
|
497
|
+
dBb: bool = False,
|
498
|
+
peak_normalise = False,
|
499
|
+
correct_background = False,
|
500
|
+
return_time_type: TimeType = TimeType.RELATIVE
|
501
|
+
) -> TimeCut:
|
502
|
+
"""Retrieve a cut of the dynamic spectra at a specific frequency.
|
503
|
+
|
504
|
+
If `at_frequency` does not exactly match a frequency in `frequencies`, the
|
505
|
+
closest match is selected. The cut represents the time series of some spectral
|
506
|
+
component.
|
507
|
+
|
508
|
+
:param at_frequency: The requested frequency for the cut, in Hz.
|
509
|
+
:param dBb: If True, returns the cut in decibels above the background.
|
510
|
+
Defaults to False.
|
511
|
+
:param peak_normalise: If True, normalises the cut so its peak value is 1.
|
512
|
+
Ignored if dBb is True. Defaults to False.
|
513
|
+
:param correct_background: If True, subtracts the background from the cut.
|
514
|
+
Ignored if dBb is True. Defaults to False.
|
515
|
+
:param return_time_type: Specifies the type of time values in the cut
|
516
|
+
(TimeType.RELATIVE or TimeType.DATETIMES). Defaults to TimeType.RELATIVE.
|
517
|
+
:raises ValueError: If return_time_type is not recognised.
|
518
|
+
:return: A TimeCut object containing the temporal values and associated metadata.
|
359
519
|
"""
|
360
|
-
|
520
|
+
_at_frequency = np.float32(at_frequency)
|
521
|
+
index_of_cut = find_closest_index(_at_frequency,
|
361
522
|
self._frequencies,
|
362
523
|
enforce_strict_bounds = True)
|
363
|
-
frequency_of_cut = self.frequencies[index_of_cut]
|
364
|
-
|
365
|
-
if return_time_type == TimeTypes.DATETIMES:
|
366
|
-
times = self.datetimes
|
367
|
-
elif return_time_type == TimeTypes.SECONDS:
|
368
|
-
times = self.times
|
369
|
-
else:
|
370
|
-
raise ValueError(f"Invalid return_time_type. Got {return_time_type}, expected one of 'datetimes' or 'seconds'")
|
524
|
+
frequency_of_cut = float( self.frequencies[index_of_cut] )
|
371
525
|
|
372
526
|
# dependent on the requested cut type, we return the dynamic spectra in the preferred units
|
373
527
|
if dBb:
|
374
|
-
ds = self.
|
528
|
+
ds = self.compute_dynamic_spectra_dBb()
|
375
529
|
else:
|
376
530
|
ds = self.dynamic_spectra
|
377
531
|
|
@@ -392,24 +546,43 @@ class Spectrogram:
|
|
392
546
|
if peak_normalise:
|
393
547
|
cut = normalise_peak_intensity(cut)
|
394
548
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
549
|
+
if return_time_type == TimeType.DATETIMES:
|
550
|
+
return TimeCut(frequency_of_cut,
|
551
|
+
self.datetimes,
|
552
|
+
cut,
|
553
|
+
self.spectrum_unit)
|
554
|
+
elif return_time_type == TimeType.RELATIVE:
|
555
|
+
return TimeCut(frequency_of_cut,
|
556
|
+
self.times,
|
557
|
+
cut,
|
558
|
+
self.spectrum_unit)
|
559
|
+
else:
|
560
|
+
raise ValueError(f"Invalid return_time_type. Got {return_time_type}, "
|
561
|
+
f"expected one of 'datetimes' or 'seconds'")
|
562
|
+
|
399
563
|
|
400
564
|
|
401
|
-
def _seconds_of_day(
|
565
|
+
def _seconds_of_day(
|
566
|
+
dt: datetime
|
567
|
+
) -> float:
|
402
568
|
start_of_day = datetime(dt.year, dt.month, dt.day)
|
403
569
|
return (dt - start_of_day).total_seconds()
|
404
570
|
|
405
571
|
|
406
572
|
# Function to create a FITS file with the specified structure
|
407
|
-
def _save_spectrogram(
|
573
|
+
def _save_spectrogram(
|
574
|
+
spectrogram: Spectrogram
|
575
|
+
) -> None:
|
576
|
+
"""Save the input spectrogram and associated metadata to a fits file. Some metadata
|
577
|
+
will be fetched from the corresponding capture config.
|
408
578
|
|
579
|
+
:param spectrogram: The spectrogram containing the data to be saved.
|
580
|
+
"""
|
581
|
+
dt = spectrogram.start_datetime.astype(datetime)
|
409
582
|
# making the write path
|
410
|
-
batch_parent_path = get_batches_dir_path(year =
|
411
|
-
month =
|
412
|
-
day =
|
583
|
+
batch_parent_path = get_batches_dir_path(year = dt.year,
|
584
|
+
month = dt.month,
|
585
|
+
day = dt.day)
|
413
586
|
# file name formatted as a batch file
|
414
587
|
file_name = f"{spectrogram.format_start_time()}_{spectrogram.tag}.fits"
|
415
588
|
write_path = os.path.join(batch_parent_path,
|
@@ -417,13 +590,13 @@ def _save_spectrogram(spectrogram: Spectrogram) -> None:
|
|
417
590
|
|
418
591
|
# get optional metadata from the capture config
|
419
592
|
capture_config = CaptureConfig(spectrogram.tag)
|
420
|
-
ORIGIN = capture_config.get_parameter_value(
|
421
|
-
INSTRUME = capture_config.get_parameter_value(
|
422
|
-
TELESCOP = capture_config.get_parameter_value(
|
423
|
-
OBJECT = capture_config.get_parameter_value(
|
424
|
-
OBS_ALT = capture_config.get_parameter_value(
|
425
|
-
OBS_LAT = capture_config.get_parameter_value(
|
426
|
-
OBS_LON = capture_config.get_parameter_value(
|
593
|
+
ORIGIN = cast(str, capture_config.get_parameter_value(PName.ORIGIN))
|
594
|
+
INSTRUME = cast(str, capture_config.get_parameter_value(PName.INSTRUMENT))
|
595
|
+
TELESCOP = cast(str, capture_config.get_parameter_value(PName.TELESCOPE))
|
596
|
+
OBJECT = cast(str, capture_config.get_parameter_value(PName.OBJECT))
|
597
|
+
OBS_ALT = cast(float, capture_config.get_parameter_value(PName.OBS_ALT))
|
598
|
+
OBS_LAT = cast(float, capture_config.get_parameter_value(PName.OBS_LAT))
|
599
|
+
OBS_LON = cast(float, capture_config.get_parameter_value(PName.OBS_LON))
|
427
600
|
|
428
601
|
# Primary HDU with data
|
429
602
|
primary_data = spectrogram.dynamic_spectra.astype(dtype=np.float32)
|
@@ -448,13 +621,13 @@ def _save_spectrogram(spectrogram: Spectrogram) -> None:
|
|
448
621
|
for comment in comments:
|
449
622
|
primary_hdu.header.add_comment(comment)
|
450
623
|
|
451
|
-
start_datetime = spectrogram.datetimes[0]
|
452
|
-
start_date
|
453
|
-
start_time
|
624
|
+
start_datetime = cast(datetime, spectrogram.datetimes[0].astype(datetime))
|
625
|
+
start_date = start_datetime.strftime("%Y-%m-%d")
|
626
|
+
start_time = start_datetime.strftime("%H:%M:%S.%f")
|
454
627
|
|
455
|
-
end_datetime
|
456
|
-
end_date
|
457
|
-
end_time
|
628
|
+
end_datetime = cast(datetime, spectrogram.datetimes[-1].astype(datetime))
|
629
|
+
end_date = end_datetime.strftime("%Y-%m-%d")
|
630
|
+
end_time = end_datetime.strftime("%H:%M:%S.%f")
|
458
631
|
|
459
632
|
primary_hdu.header.set('DATE', start_date, 'time of observation')
|
460
633
|
primary_hdu.header.set('CONTENT', f'{start_date} dynamic spectrogram', 'title of image')
|
@@ -470,12 +643,12 @@ def _save_spectrogram(spectrogram: Spectrogram) -> None:
|
|
470
643
|
|
471
644
|
primary_hdu.header.set('BZERO', 0, 'scaling offset')
|
472
645
|
primary_hdu.header.set('BSCALE', 1, 'scaling factor')
|
473
|
-
primary_hdu.header.set('BUNIT', f"{spectrogram.
|
646
|
+
primary_hdu.header.set('BUNIT', f"{spectrogram.spectrum_unit.value}", 'z-axis title')
|
474
647
|
|
475
648
|
primary_hdu.header.set('DATAMIN', np.nanmin(spectrogram.dynamic_spectra), 'minimum element in image')
|
476
649
|
primary_hdu.header.set('DATAMAX', np.nanmax(spectrogram.dynamic_spectra), 'maximum element in image')
|
477
650
|
|
478
|
-
primary_hdu.header.set('CRVAL1', f'{_seconds_of_day(start_datetime)}', 'value on axis 1 at reference pixel [sec of day]')
|
651
|
+
primary_hdu.header.set('CRVAL1', f'{_seconds_of_day( start_datetime)}', 'value on axis 1 at reference pixel [sec of day]')
|
479
652
|
primary_hdu.header.set('CRPIX1', 0, 'reference pixel of axis 1')
|
480
653
|
primary_hdu.header.set('CTYPE1', 'TIME [UT]', 'title of axis 1')
|
481
654
|
primary_hdu.header.set('CDELT1', spectrogram.time_resolution, 'step between first and second element in x-axis')
|
@@ -519,4 +692,4 @@ def _save_spectrogram(spectrogram: Spectrogram) -> None:
|
|
519
692
|
|
520
693
|
# Create HDU list and write to file
|
521
694
|
hdul = fits.HDUList([primary_hdu, bin_table_hdu])
|
522
|
-
hdul.writeto(write_path, overwrite=True)
|
695
|
+
hdul.writeto(write_path, overwrite=True)
|