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