spectre-core 0.0.22__py3-none-any.whl → 0.0.24__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/__init__.py +5 -0
- 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 +19 -8
- spectre_core/config/_paths.py +25 -47
- 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 +19 -3
- spectre_core/plotting/_base.py +112 -177
- spectre_core/plotting/_format.py +10 -8
- spectre_core/plotting/_panel_names.py +7 -5
- spectre_core/plotting/_panel_stack.py +138 -130
- spectre_core/plotting/_panels.py +152 -162
- 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 +293 -324
- 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.24.dist-info}/METADATA +9 -23
- spectre_core-0.0.24.dist-info/RECORD +79 -0
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.24.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.24.dist-info}/licenses/LICENSE +0 -0
- {spectre_core-0.0.22.dist-info → spectre_core-0.0.24.dist-info}/top_level.txt +0 -0
@@ -26,27 +26,29 @@ from ._array_operations import (
|
|
26
26
|
|
27
27
|
class SpectrumUnit(Enum):
|
28
28
|
"""A defined unit for dynamic spectra values.
|
29
|
-
|
29
|
+
|
30
30
|
:ivar AMPLITUDE: Formal definition TBC.
|
31
31
|
:ivar POWER: Formal definition TBC.
|
32
32
|
:ivar DIGITS: Formal definition TBC.
|
33
33
|
"""
|
34
|
+
|
34
35
|
AMPLITUDE = "amplitude"
|
35
|
-
POWER
|
36
|
-
DIGITS
|
37
|
-
|
36
|
+
POWER = "power"
|
37
|
+
DIGITS = "digits"
|
38
|
+
|
38
39
|
|
39
40
|
@dataclass
|
40
41
|
class FrequencyCut:
|
41
|
-
"""A cut of a dynamic spectra, at a particular instant of time. Equivalently, some spectrum in
|
42
|
+
"""A cut of a dynamic spectra, at a particular instant of time. Equivalently, some spectrum in
|
42
43
|
the spectrogram.
|
43
|
-
|
44
|
+
|
44
45
|
:ivar time: The time of the frequency cut, either in relative time (if time is a float)
|
45
46
|
or as a datetime.
|
46
47
|
:ivar frequencies: The physical frequencies assigned to each spectral component, in Hz.
|
47
48
|
:ivar cut: The spectrum values.
|
48
49
|
:ivar spectrum_unit: The unit of each spectrum value.
|
49
50
|
"""
|
51
|
+
|
50
52
|
time: float | datetime
|
51
53
|
frequencies: npt.NDArray[np.float32]
|
52
54
|
cut: npt.NDArray[np.float32]
|
@@ -57,13 +59,14 @@ class FrequencyCut:
|
|
57
59
|
class TimeCut:
|
58
60
|
"""A cut of a dynamic spectra, at some fixed frequency. Equivalently, a time series of
|
59
61
|
some spectral component in the spectrogram.
|
60
|
-
|
62
|
+
|
61
63
|
: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
|
64
|
+
:ivar times: The time for each time series value, either as a relative time (if
|
63
65
|
the elements are floats) or as a datetimes.
|
64
66
|
:ivar cut: The time series values of the spectral component.
|
65
67
|
:ivar spectrum_unit: The unit of each time series value.
|
66
68
|
"""
|
69
|
+
|
67
70
|
frequency: float
|
68
71
|
times: npt.NDArray[np.float32 | np.datetime64]
|
69
72
|
cut: npt.NDArray[np.float32]
|
@@ -72,27 +75,29 @@ class TimeCut:
|
|
72
75
|
|
73
76
|
class TimeType(Enum):
|
74
77
|
"""The type of time we can assign to each spectrum in the dynamic spectra.
|
75
|
-
|
78
|
+
|
76
79
|
:ivar RELATIVE: The elapsed time from the first spectrum, in seconds.
|
77
80
|
:ivar DATETIMES: The datetime associated with each spectrum.
|
78
81
|
"""
|
79
|
-
|
80
|
-
|
82
|
+
|
83
|
+
RELATIVE = "relative"
|
84
|
+
DATETIMES = "datetimes"
|
81
85
|
|
82
86
|
|
83
87
|
class Spectrogram:
|
84
88
|
"""Standardised wrapper for spectrogram data, providing a consistent
|
85
|
-
interface for storing, accessing, and manipulating spectrogram data,
|
89
|
+
interface for storing, accessing, and manipulating spectrogram data,
|
86
90
|
along with associated metadata.
|
87
91
|
"""
|
92
|
+
|
88
93
|
def __init__(
|
89
|
-
self,
|
94
|
+
self,
|
90
95
|
dynamic_spectra: npt.NDArray[np.float32],
|
91
|
-
times
|
92
|
-
frequencies
|
93
|
-
tag
|
94
|
-
spectrum_unit
|
95
|
-
start_datetime
|
96
|
+
times: npt.NDArray[np.float32],
|
97
|
+
frequencies: npt.NDArray[np.float32],
|
98
|
+
tag: str,
|
99
|
+
spectrum_unit: SpectrumUnit,
|
100
|
+
start_datetime: Optional[datetime | np.datetime64] = None,
|
96
101
|
) -> None:
|
97
102
|
"""Initialise a Spectrogram instance.
|
98
103
|
|
@@ -105,312 +110,263 @@ class Spectrogram:
|
|
105
110
|
:param start_datetime: The datetime corresponding to the first spectrum, defaults to None.
|
106
111
|
:raises ValueError: If times does not start at 0 or array shapes are inconsistent.
|
107
112
|
"""
|
108
|
-
|
113
|
+
|
109
114
|
self._dynamic_spectra = dynamic_spectra
|
110
115
|
|
111
116
|
if times[0] != 0:
|
112
117
|
raise ValueError(f"The first spectrum must correspond to t=0")
|
113
118
|
self._times = times
|
114
|
-
|
119
|
+
|
115
120
|
self._frequencies = frequencies
|
116
121
|
self._tag = tag
|
117
122
|
self._spectrum_unit = spectrum_unit
|
118
|
-
self._start_datetime =
|
119
|
-
|
123
|
+
self._start_datetime = (
|
124
|
+
np.datetime64(start_datetime) if start_datetime is not None else None
|
125
|
+
)
|
126
|
+
|
120
127
|
# by default, the background is evaluated over the whole spectrogram
|
121
|
-
self._start_background_index = 0
|
122
|
-
self._end_background_index
|
128
|
+
self._start_background_index = 0
|
129
|
+
self._end_background_index = self.num_times
|
123
130
|
# the background interval can be set after instantiation
|
124
|
-
self._start_background: Optional[str]
|
125
|
-
self._end_background
|
126
|
-
|
131
|
+
self._start_background: Optional[str] = None
|
132
|
+
self._end_background: Optional[str] = None
|
133
|
+
|
127
134
|
# finally check that the spectrogram arrays are matching in shape
|
128
135
|
self._check_shapes()
|
129
136
|
|
130
|
-
|
131
137
|
@property
|
132
|
-
def dynamic_spectra(
|
133
|
-
self
|
134
|
-
) -> npt.NDArray[np.float32]:
|
138
|
+
def dynamic_spectra(self) -> npt.NDArray[np.float32]:
|
135
139
|
"""The dynamic spectra array.
|
136
|
-
|
137
|
-
Returns the 2D array of spectrogram data with shape (num_frequencies, num_times),
|
140
|
+
|
141
|
+
Returns the 2D array of spectrogram data with shape (num_frequencies, num_times),
|
138
142
|
where values are in the units specified by `spectrum_unit`.
|
139
143
|
"""
|
140
144
|
return self._dynamic_spectra
|
141
|
-
|
142
145
|
|
143
146
|
@property
|
144
|
-
def times(
|
145
|
-
self
|
146
|
-
) -> npt.NDArray[np.float32]:
|
147
|
+
def times(self) -> npt.NDArray[np.float32]:
|
147
148
|
"""A 1D array representing the elapsed time of each spectrum, in seconds, relative to the first
|
148
149
|
in the spectrogram."""
|
149
150
|
return self._times
|
150
|
-
|
151
|
-
|
151
|
+
|
152
152
|
@property
|
153
|
-
def num_times(
|
154
|
-
self
|
155
|
-
) -> int:
|
153
|
+
def num_times(self) -> int:
|
156
154
|
"""The size of the times array. Equivalent to the number of spectrums in the spectrogram."""
|
157
155
|
return len(self._times)
|
158
|
-
|
159
156
|
|
160
157
|
@property
|
161
|
-
def time_resolution(
|
162
|
-
self
|
163
|
-
) -> float:
|
158
|
+
def time_resolution(self) -> float:
|
164
159
|
"""The time resolution of the dynamic spectra.
|
165
160
|
|
166
|
-
Represents the spacing between consecutive time values in the times array,
|
161
|
+
Represents the spacing between consecutive time values in the times array,
|
167
162
|
calculated as the median difference between adjacent elements.
|
168
163
|
"""
|
169
164
|
return compute_resolution(self._times)
|
170
|
-
|
171
165
|
|
172
166
|
@property
|
173
|
-
def time_range(
|
174
|
-
self
|
175
|
-
) -> float:
|
167
|
+
def time_range(self) -> float:
|
176
168
|
"""The time range of the dynamic spectra.
|
177
169
|
|
178
|
-
Represents the difference between the first and last time values
|
170
|
+
Represents the difference between the first and last time values
|
179
171
|
in the times array.
|
180
172
|
"""
|
181
173
|
return compute_range(self._times)
|
182
|
-
|
183
174
|
|
184
175
|
@property
|
185
|
-
def frequencies(
|
186
|
-
self
|
187
|
-
) -> npt.NDArray[np.float32]:
|
176
|
+
def frequencies(self) -> npt.NDArray[np.float32]:
|
188
177
|
"""A 1D array representing the physical frequencies assigned to each spectral component, in Hz."""
|
189
178
|
return self._frequencies
|
190
179
|
|
191
|
-
|
192
180
|
@property
|
193
|
-
def num_frequencies(
|
194
|
-
self
|
195
|
-
) -> int:
|
181
|
+
def num_frequencies(self) -> int:
|
196
182
|
"""The number of spectral components in the spectrogram.
|
197
183
|
|
198
184
|
Equivalent to the size of the frequencies array.
|
199
185
|
"""
|
200
186
|
return len(self._frequencies)
|
201
|
-
|
202
|
-
|
187
|
+
|
203
188
|
@property
|
204
|
-
def frequency_resolution(
|
205
|
-
self
|
206
|
-
) -> float:
|
189
|
+
def frequency_resolution(self) -> float:
|
207
190
|
"""The frequency resolution of the dynamic spectra.
|
208
191
|
|
209
|
-
Represents the spacing between consecutive frequency values in the frequencies array,
|
192
|
+
Represents the spacing between consecutive frequency values in the frequencies array,
|
210
193
|
calculated as the median difference between adjacent elements.
|
211
194
|
"""
|
212
195
|
return compute_resolution(self._frequencies)
|
213
|
-
|
214
196
|
|
215
197
|
@property
|
216
|
-
def frequency_range(
|
217
|
-
self
|
218
|
-
) -> float:
|
198
|
+
def frequency_range(self) -> float:
|
219
199
|
"""The frequency range covered by the dynamic spectra.
|
220
200
|
|
221
|
-
Represents the difference between the minimum and maximum frequency
|
201
|
+
Represents the difference between the minimum and maximum frequency
|
222
202
|
values in the frequencies array.
|
223
203
|
"""
|
224
204
|
return compute_range(self._frequencies)
|
225
|
-
|
226
205
|
|
227
206
|
@property
|
228
|
-
def tag(
|
229
|
-
self
|
230
|
-
) -> str:
|
207
|
+
def tag(self) -> str:
|
231
208
|
"""The tag identifier for the spectrogram"""
|
232
209
|
return self._tag
|
233
|
-
|
234
210
|
|
235
211
|
@property
|
236
|
-
def start_datetime_is_set(
|
237
|
-
self
|
238
|
-
) -> bool:
|
212
|
+
def start_datetime_is_set(self) -> bool:
|
239
213
|
"""Indicates whether the start datetime for the spectrogram has been set."""
|
240
|
-
return
|
241
|
-
|
242
|
-
|
214
|
+
return self._start_datetime is not None
|
215
|
+
|
243
216
|
@property
|
244
|
-
def start_datetime(
|
245
|
-
self
|
246
|
-
) -> np.datetime64:
|
217
|
+
def start_datetime(self) -> np.datetime64:
|
247
218
|
"""The datetime assigned to the first spectrum in `dynamic_spectra`.
|
248
219
|
|
249
220
|
:raises AttributeError: If the start_datetime has not been set.
|
250
221
|
"""
|
251
222
|
if self._start_datetime is None:
|
252
|
-
raise
|
223
|
+
raise ValueError(f"A start time has not been set.")
|
253
224
|
return self._start_datetime
|
254
|
-
|
255
|
-
|
225
|
+
|
256
226
|
@property
|
257
|
-
def datetimes(
|
258
|
-
self
|
259
|
-
) -> npt.NDArray[np.datetime64]:
|
227
|
+
def datetimes(self) -> npt.NDArray[np.datetime64]:
|
260
228
|
"""The datetimes associated with each spectrum in `dynamic_spectra`.
|
261
229
|
|
262
|
-
Returns a list of datetime objects, calculated by adding the elapsed
|
230
|
+
Returns a list of datetime objects, calculated by adding the elapsed
|
263
231
|
times in the times array to the start_datetime.
|
264
232
|
"""
|
265
|
-
return self.start_datetime + (1e6*self._times).astype(
|
266
|
-
|
233
|
+
return self.start_datetime + (1e6 * self._times).astype("timedelta64[us]")
|
267
234
|
|
268
235
|
@property
|
269
|
-
def spectrum_unit(
|
270
|
-
self
|
271
|
-
) -> SpectrumUnit:
|
236
|
+
def spectrum_unit(self) -> SpectrumUnit:
|
272
237
|
"""The units associated with the `dynamic_spectra` array."""
|
273
238
|
return self._spectrum_unit
|
274
|
-
|
275
239
|
|
276
240
|
@property
|
277
|
-
def start_background(
|
278
|
-
self
|
279
|
-
) -> Optional[str]:
|
241
|
+
def start_background(self) -> Optional[str]:
|
280
242
|
"""The start of the background interval.
|
281
243
|
|
282
|
-
Returns a string-formatted datetime up to seconds precision, or None
|
244
|
+
Returns a string-formatted datetime up to seconds precision, or None
|
283
245
|
if the background interval has not been set.
|
284
246
|
"""
|
285
247
|
return self._start_background
|
286
|
-
|
287
248
|
|
288
249
|
@property
|
289
|
-
def end_background(
|
290
|
-
self
|
291
|
-
) -> Optional[str]:
|
250
|
+
def end_background(self) -> Optional[str]:
|
292
251
|
"""The end of the background interval.
|
293
252
|
|
294
|
-
Returns a string-formatted datetime up to seconds precision, or None
|
253
|
+
Returns a string-formatted datetime up to seconds precision, or None
|
295
254
|
if the background interval has not been set.
|
296
255
|
"""
|
297
256
|
return self._end_background
|
298
|
-
|
299
257
|
|
300
|
-
def compute_background_spectrum(
|
301
|
-
self
|
302
|
-
) -> npt.NDArray[np.float32]:
|
258
|
+
def compute_background_spectrum(self) -> npt.NDArray[np.float32]:
|
303
259
|
"""Compute the background spectrum by averaging the dynamic spectra in time.
|
304
260
|
|
305
|
-
By default, the entire dynamic spectra is averaged. Use set_background to
|
261
|
+
By default, the entire dynamic spectra is averaged. Use set_background to
|
306
262
|
specify a custom background interval.
|
307
263
|
|
308
|
-
:return: A 1D array representing the time-averaged dynamic spectra over the
|
264
|
+
:return: A 1D array representing the time-averaged dynamic spectra over the
|
309
265
|
specified background interval.
|
310
266
|
"""
|
311
|
-
return np.nanmean(
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
267
|
+
return np.nanmean(
|
268
|
+
self._dynamic_spectra[
|
269
|
+
:, self._start_background_index : self._end_background_index + 1
|
270
|
+
],
|
271
|
+
axis=-1,
|
272
|
+
)
|
273
|
+
|
274
|
+
def compute_dynamic_spectra_dBb(self) -> npt.NDArray[np.float32]:
|
318
275
|
"""Compute the dynamic spectra in units of decibels above the background spectrum.
|
319
276
|
|
320
|
-
The computation applies logarithmic scaling based on the `spectrum_unit`.
|
277
|
+
The computation applies logarithmic scaling based on the `spectrum_unit`.
|
321
278
|
|
322
279
|
:raises NotImplementedError: If the spectrum_unit is unrecognised.
|
323
|
-
:return: A 2D array with the same shape as `dynamic_spectra`, representing
|
280
|
+
:return: A 2D array with the same shape as `dynamic_spectra`, representing
|
324
281
|
the values in decibels above the background.
|
325
282
|
"""
|
326
283
|
# Create a 'background' spectrogram where each spectrum is identically the background spectrum
|
327
284
|
background_spectrum = self.compute_background_spectrum()
|
328
285
|
background_spectra = background_spectrum[:, np.newaxis]
|
329
286
|
# Suppress divide by zero and invalid value warnings for this block of code
|
330
|
-
with np.errstate(divide=
|
331
|
-
if
|
332
|
-
|
287
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
288
|
+
if (
|
289
|
+
self._spectrum_unit == SpectrumUnit.AMPLITUDE
|
290
|
+
or self._spectrum_unit == SpectrumUnit.DIGITS
|
291
|
+
):
|
292
|
+
dynamic_spectra_dBb = 10 * np.log10(
|
293
|
+
self._dynamic_spectra / background_spectra
|
294
|
+
)
|
333
295
|
elif self._spectrum_unit == SpectrumUnit.POWER:
|
334
|
-
dynamic_spectra_dBb = 20 * np.log10(
|
296
|
+
dynamic_spectra_dBb = 20 * np.log10(
|
297
|
+
self._dynamic_spectra / background_spectra
|
298
|
+
)
|
335
299
|
else:
|
336
|
-
raise NotImplementedError(
|
300
|
+
raise NotImplementedError(
|
301
|
+
f"{self._spectrum_unit} is unrecognised; decibel conversion is uncertain!"
|
302
|
+
)
|
337
303
|
return dynamic_spectra_dBb.astype(np.float32)
|
338
|
-
|
339
|
-
|
340
|
-
def format_start_time(
|
341
|
-
self
|
342
|
-
) -> str:
|
304
|
+
|
305
|
+
def format_start_time(self) -> str:
|
343
306
|
"""Format the datetime assigned to the first spectrum in the dynamic spectra.
|
344
307
|
|
345
308
|
:return: A string representation of the `start_datetime`, up to seconds precision.
|
346
309
|
"""
|
347
310
|
dt = self.start_datetime.astype(datetime)
|
348
311
|
return datetime.strftime(dt, TimeFormat.DATETIME)
|
349
|
-
|
350
|
-
|
351
|
-
def set_background(
|
352
|
-
self,
|
353
|
-
start_background: str,
|
354
|
-
end_background: str
|
355
|
-
) -> None:
|
312
|
+
|
313
|
+
def set_background(self, start_background: str, end_background: str) -> None:
|
356
314
|
"""Set the background interval for computing the background spectrum, and doing
|
357
315
|
background subtractions.
|
358
316
|
|
359
|
-
:param start_background: The start time of the background interval, formatted as
|
317
|
+
:param start_background: The start time of the background interval, formatted as
|
360
318
|
a string in the format `TimeFormat.DATETIME` (up to seconds precision).
|
361
|
-
:param end_background: The end time of the background interval, formatted as
|
319
|
+
:param end_background: The end time of the background interval, formatted as
|
362
320
|
a string in the format `TimeFormat.DATETIME` (up to seconds precision).
|
363
321
|
"""
|
364
322
|
self._start_background = start_background
|
365
|
-
self._end_background
|
366
|
-
self._update_background_indices_from_interval(
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
323
|
+
self._end_background = end_background
|
324
|
+
self._update_background_indices_from_interval(
|
325
|
+
self._start_background, self._end_background
|
326
|
+
)
|
327
|
+
|
371
328
|
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,
|
379
|
-
self.datetimes,
|
380
|
-
enforce_strict_bounds=True)
|
381
|
-
self._end_background_index = find_closest_index(end_background_datetime,
|
382
|
-
self.datetimes,
|
383
|
-
enforce_strict_bounds=True)
|
384
|
-
|
385
|
-
|
386
|
-
def _check_shapes(
|
387
|
-
self
|
329
|
+
self, start_background: str, end_background: str
|
388
330
|
) -> None:
|
331
|
+
start_background_datetime = np.datetime64(
|
332
|
+
datetime.strptime(start_background, TimeFormat.DATETIME)
|
333
|
+
)
|
334
|
+
end_background_datetime = np.datetime64(
|
335
|
+
datetime.strptime(end_background, TimeFormat.DATETIME)
|
336
|
+
)
|
337
|
+
self._start_background_index = find_closest_index(
|
338
|
+
start_background_datetime, self.datetimes, enforce_strict_bounds=True
|
339
|
+
)
|
340
|
+
self._end_background_index = find_closest_index(
|
341
|
+
end_background_datetime, self.datetimes, enforce_strict_bounds=True
|
342
|
+
)
|
343
|
+
|
344
|
+
def _check_shapes(self) -> None:
|
389
345
|
"""Check that the data array shapes are consistent with one another."""
|
390
346
|
num_spectrogram_dims = np.ndim(self._dynamic_spectra)
|
391
347
|
# Check if 'dynamic_spectra' is a 2D array
|
392
348
|
if num_spectrogram_dims != 2:
|
393
|
-
raise ValueError(
|
349
|
+
raise ValueError(
|
350
|
+
f"Expected dynamic spectrogram to be a 2D array, but got {num_spectrogram_dims}D array"
|
351
|
+
)
|
394
352
|
dynamic_spectra_shape = self.dynamic_spectra.shape
|
395
353
|
# Check if the dimensions of 'dynamic_spectra' are consistent with the time and frequency arrays
|
396
354
|
if dynamic_spectra_shape[0] != self.num_frequencies:
|
397
|
-
raise ValueError(
|
398
|
-
|
355
|
+
raise ValueError(
|
356
|
+
f"Mismatch in number of frequency bins: Expected {self.num_frequencies}, but got {dynamic_spectra_shape[0]}"
|
357
|
+
)
|
358
|
+
|
399
359
|
if dynamic_spectra_shape[1] != self.num_times:
|
400
|
-
raise ValueError(
|
401
|
-
|
360
|
+
raise ValueError(
|
361
|
+
f"Mismatch in number of time bins: Expected {self.num_times}, but got {dynamic_spectra_shape[1]}"
|
362
|
+
)
|
402
363
|
|
403
|
-
def save(
|
404
|
-
self
|
405
|
-
) -> None:
|
364
|
+
def save(self) -> None:
|
406
365
|
"""Write the spectrogram and its associated metadata to a batch file in the FITS format."""
|
407
366
|
_save_spectrogram(self)
|
408
|
-
|
409
367
|
|
410
368
|
def integrate_over_frequency(
|
411
|
-
self,
|
412
|
-
correct_background: bool = False,
|
413
|
-
peak_normalise: bool = False
|
369
|
+
self, correct_background: bool = False, peak_normalise: bool = False
|
414
370
|
) -> npt.NDArray[np.float32]:
|
415
371
|
"""Numerically integrate the spectrogram over the frequency axis.
|
416
372
|
|
@@ -418,278 +374,291 @@ class Spectrogram:
|
|
418
374
|
computing the integral, defaults to False
|
419
375
|
:param peak_normalise: Indicates whether to normalise the integral such that
|
420
376
|
the peak value is equal to unity, defaults to False
|
421
|
-
:return: A 1D array containing each spectrum numerically integrated over the
|
377
|
+
:return: A 1D array containing each spectrum numerically integrated over the
|
422
378
|
frequency axis.
|
423
379
|
"""
|
424
380
|
# numerically integrate over frequency
|
425
381
|
I = np.trapz(self._dynamic_spectra, self._frequencies, axis=0)
|
426
382
|
|
427
383
|
if correct_background:
|
428
|
-
I = subtract_background(
|
429
|
-
|
430
|
-
|
384
|
+
I = subtract_background(
|
385
|
+
I, self._start_background_index, self._end_background_index
|
386
|
+
)
|
431
387
|
if peak_normalise:
|
432
388
|
I = normalise_peak_intensity(I)
|
433
389
|
return I
|
434
390
|
|
435
|
-
|
436
391
|
def get_frequency_cut(
|
437
|
-
self,
|
438
|
-
at_time: float | str,
|
439
|
-
dBb: bool = False,
|
440
|
-
peak_normalise: bool = False
|
392
|
+
self, at_time: float | str, dBb: bool = False, peak_normalise: bool = False
|
441
393
|
) -> FrequencyCut:
|
442
394
|
"""Retrieve a cut of the dynamic spectra at a specific time.
|
443
395
|
|
444
|
-
If `at_time` does not match exactly with a time in `times`, the closest match
|
396
|
+
If `at_time` does not match exactly with a time in `times`, the closest match
|
445
397
|
is selected. The cut represents one of the spectrums in the spectrogram.
|
446
398
|
|
447
|
-
:param at_time: The requested time for the cut. If a string, it is parsed
|
399
|
+
:param at_time: The requested time for the cut. If a string, it is parsed
|
448
400
|
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,
|
401
|
+
:param dBb: If True, returns the cut in decibels above the background,
|
450
402
|
defaults to False.
|
451
|
-
:param peak_normalise: If True, normalises the cut such that its peak value
|
403
|
+
:param peak_normalise: If True, normalises the cut such that its peak value
|
452
404
|
is equal to 1. Ignored if dBb is True, defaults to False.
|
453
405
|
:raises ValueError: If at_time is not a recognised type.
|
454
406
|
:return: A FrequencyCut object containing the spectral values and associated metadata.
|
455
407
|
"""
|
456
408
|
|
457
409
|
if isinstance(at_time, str):
|
458
|
-
_at_datetime = np.datetime64(
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
410
|
+
_at_datetime = np.datetime64(
|
411
|
+
datetime.strptime(at_time, TimeFormat.DATETIME)
|
412
|
+
)
|
413
|
+
index_of_cut = find_closest_index(
|
414
|
+
_at_datetime, self.datetimes, enforce_strict_bounds=True
|
415
|
+
)
|
416
|
+
time_of_cut = self.datetimes[index_of_cut]
|
463
417
|
|
464
418
|
elif isinstance(at_time, float):
|
465
419
|
_at_time = np.float32(at_time)
|
466
|
-
index_of_cut = find_closest_index(
|
467
|
-
|
468
|
-
|
420
|
+
index_of_cut = find_closest_index(
|
421
|
+
_at_time, self._times, enforce_strict_bounds=True
|
422
|
+
)
|
469
423
|
time_of_cut = self.times[index_of_cut]
|
470
|
-
|
424
|
+
|
471
425
|
else:
|
472
426
|
raise ValueError(f"'at_time' type '{type(at_time)}' is unsupported.")
|
473
|
-
|
427
|
+
|
474
428
|
if dBb:
|
475
429
|
ds = self.compute_dynamic_spectra_dBb()
|
476
430
|
else:
|
477
431
|
ds = self._dynamic_spectra
|
478
|
-
|
479
|
-
cut = ds[
|
432
|
+
|
433
|
+
cut = ds[
|
434
|
+
:, index_of_cut
|
435
|
+
].copy() # make a copy so to preserve the spectrum on transformations of the cut
|
480
436
|
|
481
437
|
if dBb:
|
482
438
|
if peak_normalise:
|
483
|
-
warn(
|
439
|
+
warn(
|
440
|
+
"Ignoring frequency cut normalisation, since dBb units have been specified"
|
441
|
+
)
|
484
442
|
else:
|
485
443
|
if peak_normalise:
|
486
444
|
cut = normalise_peak_intensity(cut)
|
487
|
-
|
488
|
-
return FrequencyCut(time_of_cut,
|
489
|
-
self._frequencies,
|
490
|
-
cut,
|
491
|
-
self._spectrum_unit)
|
492
445
|
|
493
|
-
|
446
|
+
return FrequencyCut(time_of_cut, self._frequencies, cut, self._spectrum_unit)
|
447
|
+
|
494
448
|
def get_time_cut(
|
495
449
|
self,
|
496
450
|
at_frequency: float,
|
497
451
|
dBb: bool = False,
|
498
|
-
peak_normalise
|
499
|
-
correct_background
|
500
|
-
return_time_type: TimeType = TimeType.RELATIVE
|
452
|
+
peak_normalise=False,
|
453
|
+
correct_background=False,
|
454
|
+
return_time_type: TimeType = TimeType.RELATIVE,
|
501
455
|
) -> TimeCut:
|
502
456
|
"""Retrieve a cut of the dynamic spectra at a specific frequency.
|
503
457
|
|
504
|
-
If `at_frequency` does not exactly match a frequency in `frequencies`, the
|
458
|
+
If `at_frequency` does not exactly match a frequency in `frequencies`, the
|
505
459
|
closest match is selected. The cut represents the time series of some spectral
|
506
460
|
component.
|
507
461
|
|
508
462
|
:param at_frequency: The requested frequency for the cut, in Hz.
|
509
|
-
:param dBb: If True, returns the cut in decibels above the background.
|
463
|
+
:param dBb: If True, returns the cut in decibels above the background.
|
510
464
|
Defaults to False.
|
511
|
-
:param peak_normalise: If True, normalises the cut so its peak value is 1.
|
465
|
+
:param peak_normalise: If True, normalises the cut so its peak value is 1.
|
512
466
|
Ignored if dBb is True. Defaults to False.
|
513
|
-
:param correct_background: If True, subtracts the background from the cut.
|
467
|
+
:param correct_background: If True, subtracts the background from the cut.
|
514
468
|
Ignored if dBb is True. Defaults to False.
|
515
|
-
:param return_time_type: Specifies the type of time values in the cut
|
469
|
+
:param return_time_type: Specifies the type of time values in the cut
|
516
470
|
(TimeType.RELATIVE or TimeType.DATETIMES). Defaults to TimeType.RELATIVE.
|
517
471
|
:raises ValueError: If return_time_type is not recognised.
|
518
472
|
:return: A TimeCut object containing the temporal values and associated metadata.
|
519
473
|
"""
|
520
474
|
_at_frequency = np.float32(at_frequency)
|
521
|
-
index_of_cut = find_closest_index(
|
522
|
-
|
523
|
-
|
524
|
-
frequency_of_cut = float(
|
475
|
+
index_of_cut = find_closest_index(
|
476
|
+
_at_frequency, self._frequencies, enforce_strict_bounds=True
|
477
|
+
)
|
478
|
+
frequency_of_cut = float(self.frequencies[index_of_cut])
|
525
479
|
|
526
480
|
# dependent on the requested cut type, we return the dynamic spectra in the preferred units
|
527
481
|
if dBb:
|
528
482
|
ds = self.compute_dynamic_spectra_dBb()
|
529
483
|
else:
|
530
484
|
ds = self.dynamic_spectra
|
531
|
-
|
532
|
-
cut = ds[
|
485
|
+
|
486
|
+
cut = ds[
|
487
|
+
index_of_cut, :
|
488
|
+
].copy() # make a copy so to preserve the spectrum on transformations of the cut
|
533
489
|
|
534
490
|
# Warn if dBb is used with background correction or peak normalisation
|
535
491
|
if dBb:
|
536
492
|
if correct_background or peak_normalise:
|
537
|
-
warn(
|
493
|
+
warn(
|
494
|
+
"Ignoring time cut normalisation, since dBb units have been specified"
|
495
|
+
)
|
538
496
|
else:
|
539
497
|
# Apply background correction if required
|
540
498
|
if correct_background:
|
541
|
-
cut = subtract_background(
|
542
|
-
|
543
|
-
|
544
|
-
|
499
|
+
cut = subtract_background(
|
500
|
+
cut, self._start_background_index, self._end_background_index
|
501
|
+
)
|
502
|
+
|
545
503
|
# Apply peak normalisation if required
|
546
504
|
if peak_normalise:
|
547
505
|
cut = normalise_peak_intensity(cut)
|
548
506
|
|
549
507
|
if return_time_type == TimeType.DATETIMES:
|
550
|
-
return TimeCut(frequency_of_cut,
|
551
|
-
self.datetimes,
|
552
|
-
cut,
|
553
|
-
self.spectrum_unit)
|
508
|
+
return TimeCut(frequency_of_cut, self.datetimes, cut, self.spectrum_unit)
|
554
509
|
elif return_time_type == TimeType.RELATIVE:
|
555
|
-
return TimeCut(frequency_of_cut,
|
556
|
-
self.times,
|
557
|
-
cut,
|
558
|
-
self.spectrum_unit)
|
510
|
+
return TimeCut(frequency_of_cut, self.times, cut, self.spectrum_unit)
|
559
511
|
else:
|
560
|
-
raise ValueError(
|
561
|
-
|
512
|
+
raise ValueError(
|
513
|
+
f"Invalid return_time_type. Got {return_time_type}, "
|
514
|
+
f"expected one of 'datetimes' or 'seconds'"
|
515
|
+
)
|
562
516
|
|
563
|
-
|
564
517
|
|
565
|
-
def _seconds_of_day(
|
566
|
-
dt: datetime
|
567
|
-
) -> float:
|
518
|
+
def _seconds_of_day(dt: datetime) -> float:
|
568
519
|
start_of_day = datetime(dt.year, dt.month, dt.day)
|
569
520
|
return (dt - start_of_day).total_seconds()
|
570
521
|
|
571
522
|
|
572
523
|
# Function to create a FITS file with the specified structure
|
573
|
-
def _save_spectrogram(
|
574
|
-
spectrogram: Spectrogram
|
575
|
-
) -> None:
|
524
|
+
def _save_spectrogram(spectrogram: Spectrogram) -> None:
|
576
525
|
"""Save the input spectrogram and associated metadata to a fits file. Some metadata
|
577
526
|
will be fetched from the corresponding capture config.
|
578
|
-
|
527
|
+
|
579
528
|
:param spectrogram: The spectrogram containing the data to be saved.
|
580
529
|
"""
|
581
530
|
dt = spectrogram.start_datetime.astype(datetime)
|
582
531
|
# making the write path
|
583
|
-
batch_parent_path = get_batches_dir_path(year
|
584
|
-
month = dt.month,
|
585
|
-
day = dt.day)
|
532
|
+
batch_parent_path = get_batches_dir_path(year=dt.year, month=dt.month, day=dt.day)
|
586
533
|
# file name formatted as a batch file
|
587
534
|
file_name = f"{spectrogram.format_start_time()}_{spectrogram.tag}.fits"
|
588
|
-
write_path = os.path.join(batch_parent_path,
|
589
|
-
|
590
|
-
|
535
|
+
write_path = os.path.join(batch_parent_path, file_name)
|
536
|
+
|
591
537
|
# get optional metadata from the capture config
|
592
538
|
capture_config = CaptureConfig(spectrogram.tag)
|
593
|
-
ORIGIN
|
594
|
-
INSTRUME
|
595
|
-
TELESCOP
|
596
|
-
OBJECT
|
597
|
-
OBS_ALT
|
598
|
-
OBS_LAT
|
599
|
-
OBS_LON
|
600
|
-
|
539
|
+
ORIGIN = cast(str, capture_config.get_parameter_value(PName.ORIGIN))
|
540
|
+
INSTRUME = cast(str, capture_config.get_parameter_value(PName.INSTRUMENT))
|
541
|
+
TELESCOP = cast(str, capture_config.get_parameter_value(PName.TELESCOPE))
|
542
|
+
OBJECT = cast(str, capture_config.get_parameter_value(PName.OBJECT))
|
543
|
+
OBS_ALT = cast(float, capture_config.get_parameter_value(PName.OBS_ALT))
|
544
|
+
OBS_LAT = cast(float, capture_config.get_parameter_value(PName.OBS_LAT))
|
545
|
+
OBS_LON = cast(float, capture_config.get_parameter_value(PName.OBS_LON))
|
546
|
+
|
601
547
|
# Primary HDU with data
|
602
|
-
primary_data = spectrogram.dynamic_spectra.astype(dtype=np.float32)
|
548
|
+
primary_data = spectrogram.dynamic_spectra.astype(dtype=np.float32)
|
603
549
|
primary_hdu = fits.PrimaryHDU(primary_data)
|
604
550
|
|
605
|
-
primary_hdu.header.set(
|
606
|
-
primary_hdu.header.set(
|
607
|
-
primary_hdu.header.set(
|
608
|
-
primary_hdu.header.set(
|
609
|
-
|
610
|
-
|
551
|
+
primary_hdu.header.set("SIMPLE", True, "file does conform to FITS standard")
|
552
|
+
primary_hdu.header.set("BITPIX", -32, "number of bits per data pixel")
|
553
|
+
primary_hdu.header.set("NAXIS", 2, "number of data axes")
|
554
|
+
primary_hdu.header.set(
|
555
|
+
"NAXIS1", spectrogram.dynamic_spectra.shape[1], "length of data axis 1"
|
556
|
+
)
|
557
|
+
primary_hdu.header.set(
|
558
|
+
"NAXIS2", spectrogram.dynamic_spectra.shape[0], "length of data axis 2"
|
559
|
+
)
|
560
|
+
primary_hdu.header.set("EXTEND", True, "FITS dataset may contain extensions")
|
611
561
|
|
612
562
|
# Add comments
|
613
563
|
comments = [
|
614
564
|
"FITS (Flexible Image Transport System) format defined in Astronomy and",
|
615
565
|
"Astrophysics Supplement Series v44/p363, v44/p371, v73/p359, v73/p365.",
|
616
566
|
"Contact the NASA Science Office of Standards and Technology for the",
|
617
|
-
"FITS Definition document #100 and other FITS information."
|
567
|
+
"FITS Definition document #100 and other FITS information.",
|
618
568
|
]
|
619
|
-
|
569
|
+
|
620
570
|
# The comments section remains unchanged since add_comment is the correct approach
|
621
571
|
for comment in comments:
|
622
572
|
primary_hdu.header.add_comment(comment)
|
623
573
|
|
624
574
|
start_datetime = cast(datetime, spectrogram.datetimes[0].astype(datetime))
|
625
|
-
start_date
|
626
|
-
start_time
|
627
|
-
|
628
|
-
end_datetime
|
629
|
-
end_date
|
630
|
-
end_time
|
631
|
-
|
632
|
-
primary_hdu.header.set(
|
633
|
-
primary_hdu.header.set(
|
634
|
-
|
635
|
-
|
636
|
-
primary_hdu.header.set(
|
637
|
-
primary_hdu.header.set(
|
638
|
-
|
639
|
-
primary_hdu.header.set(
|
640
|
-
|
641
|
-
primary_hdu.header.set(
|
642
|
-
primary_hdu.header.set(
|
643
|
-
|
644
|
-
primary_hdu.header.set(
|
645
|
-
|
646
|
-
primary_hdu.header.set(
|
647
|
-
|
648
|
-
primary_hdu.header.set(
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
primary_hdu.header.set(
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
primary_hdu.header.set(
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
primary_hdu.header.set(
|
665
|
-
primary_hdu.header.set(
|
666
|
-
|
575
|
+
start_date = start_datetime.strftime("%Y-%m-%d")
|
576
|
+
start_time = start_datetime.strftime("%H:%M:%S.%f")
|
577
|
+
|
578
|
+
end_datetime = cast(datetime, spectrogram.datetimes[-1].astype(datetime))
|
579
|
+
end_date = end_datetime.strftime("%Y-%m-%d")
|
580
|
+
end_time = end_datetime.strftime("%H:%M:%S.%f")
|
581
|
+
|
582
|
+
primary_hdu.header.set("DATE", start_date, "time of observation")
|
583
|
+
primary_hdu.header.set(
|
584
|
+
"CONTENT", f"{start_date} dynamic spectrogram", "title of image"
|
585
|
+
)
|
586
|
+
primary_hdu.header.set("ORIGIN", f"{ORIGIN}")
|
587
|
+
primary_hdu.header.set("TELESCOP", f"{TELESCOP}", "type of instrument")
|
588
|
+
primary_hdu.header.set("INSTRUME", f"{INSTRUME}")
|
589
|
+
primary_hdu.header.set("OBJECT", f"{OBJECT}", "object description")
|
590
|
+
|
591
|
+
primary_hdu.header.set("DATE-OBS", f"{start_date}", "date observation starts")
|
592
|
+
primary_hdu.header.set("TIME-OBS", f"{start_time}", "time observation starts")
|
593
|
+
primary_hdu.header.set("DATE-END", f"{end_date}", "date observation ends")
|
594
|
+
primary_hdu.header.set("TIME-END", f"{end_time}", "time observation ends")
|
595
|
+
|
596
|
+
primary_hdu.header.set("BZERO", 0, "scaling offset")
|
597
|
+
primary_hdu.header.set("BSCALE", 1, "scaling factor")
|
598
|
+
primary_hdu.header.set(
|
599
|
+
"BUNIT", f"{spectrogram.spectrum_unit.value}", "z-axis title"
|
600
|
+
)
|
601
|
+
|
602
|
+
primary_hdu.header.set(
|
603
|
+
"DATAMIN", np.nanmin(spectrogram.dynamic_spectra), "minimum element in image"
|
604
|
+
)
|
605
|
+
primary_hdu.header.set(
|
606
|
+
"DATAMAX", np.nanmax(spectrogram.dynamic_spectra), "maximum element in image"
|
607
|
+
)
|
608
|
+
|
609
|
+
primary_hdu.header.set(
|
610
|
+
"CRVAL1",
|
611
|
+
f"{_seconds_of_day( start_datetime)}",
|
612
|
+
"value on axis 1 at reference pixel [sec of day]",
|
613
|
+
)
|
614
|
+
primary_hdu.header.set("CRPIX1", 0, "reference pixel of axis 1")
|
615
|
+
primary_hdu.header.set("CTYPE1", "TIME [UT]", "title of axis 1")
|
616
|
+
primary_hdu.header.set(
|
617
|
+
"CDELT1",
|
618
|
+
spectrogram.time_resolution,
|
619
|
+
"step between first and second element in x-axis",
|
620
|
+
)
|
621
|
+
|
622
|
+
primary_hdu.header.set("CRVAL2", 0, "value on axis 2 at reference pixel")
|
623
|
+
primary_hdu.header.set("CRPIX2", 0, "reference pixel of axis 2")
|
624
|
+
primary_hdu.header.set("CTYPE2", "Frequency [Hz]", "title of axis 2")
|
625
|
+
primary_hdu.header.set(
|
626
|
+
"CDELT2",
|
627
|
+
spectrogram.frequency_resolution,
|
628
|
+
"step between first and second element in axis",
|
629
|
+
)
|
630
|
+
|
631
|
+
primary_hdu.header.set("OBS_LAT", f"{OBS_LAT}", "observatory latitude in degree")
|
632
|
+
primary_hdu.header.set("OBS_LAC", "N", "observatory latitude code {N,S}")
|
633
|
+
primary_hdu.header.set("OBS_LON", f"{OBS_LON}", "observatory longitude in degree")
|
634
|
+
primary_hdu.header.set("OBS_LOC", "W", "observatory longitude code {E,W}")
|
635
|
+
primary_hdu.header.set("OBS_ALT", f"{OBS_ALT}", "observatory altitude in meter asl")
|
667
636
|
|
668
637
|
# Wrap arrays in an additional dimension to mimic the e-CALLISTO storage
|
669
638
|
times_wrapped = np.array([spectrogram.times.astype(np.float32)])
|
670
639
|
# To mimic e-Callisto storage, convert frequencies to MHz
|
671
640
|
frequencies_MHz = spectrogram.frequencies * 1e-6
|
672
641
|
frequencies_wrapped = np.array([frequencies_MHz.astype(np.float32)])
|
673
|
-
|
642
|
+
|
674
643
|
# Binary Table HDU (extension)
|
675
|
-
col1 = fits.Column(name=
|
676
|
-
col2 = fits.Column(name=
|
644
|
+
col1 = fits.Column(name="TIME", format="PD", array=times_wrapped)
|
645
|
+
col2 = fits.Column(name="FREQUENCY", format="PD", array=frequencies_wrapped)
|
677
646
|
cols = fits.ColDefs([col1, col2])
|
678
647
|
|
679
648
|
bin_table_hdu = fits.BinTableHDU.from_columns(cols)
|
680
649
|
|
681
|
-
bin_table_hdu.header.set(
|
682
|
-
bin_table_hdu.header.set(
|
683
|
-
bin_table_hdu.header.set(
|
684
|
-
bin_table_hdu.header.set(
|
685
|
-
bin_table_hdu.header.set(
|
686
|
-
bin_table_hdu.header.set(
|
687
|
-
bin_table_hdu.header.set(
|
688
|
-
bin_table_hdu.header.set(
|
689
|
-
bin_table_hdu.header.set(
|
690
|
-
bin_table_hdu.header.set(
|
691
|
-
bin_table_hdu.header.set(
|
650
|
+
bin_table_hdu.header.set("PCOUNT", 0, "size of special data area")
|
651
|
+
bin_table_hdu.header.set("GCOUNT", 1, "one data group (required keyword)")
|
652
|
+
bin_table_hdu.header.set("TFIELDS", 2, "number of fields in each row")
|
653
|
+
bin_table_hdu.header.set("TTYPE1", "TIME", "label for field 1")
|
654
|
+
bin_table_hdu.header.set("TFORM1", "D", "data format of field: 8-byte DOUBLE")
|
655
|
+
bin_table_hdu.header.set("TTYPE2", "FREQUENCY", "label for field 2")
|
656
|
+
bin_table_hdu.header.set("TFORM2", "D", "data format of field: 8-byte DOUBLE")
|
657
|
+
bin_table_hdu.header.set("TSCAL1", 1, "")
|
658
|
+
bin_table_hdu.header.set("TZERO1", 0, "")
|
659
|
+
bin_table_hdu.header.set("TSCAL2", 1, "")
|
660
|
+
bin_table_hdu.header.set("TZERO2", 0, "")
|
692
661
|
|
693
662
|
# Create HDU list and write to file
|
694
663
|
hdul = fits.HDUList([primary_hdu, bin_table_hdu])
|
695
|
-
hdul.writeto(write_path, overwrite=True)
|
664
|
+
hdul.writeto(write_path, overwrite=True)
|