spectre-core 0.0.21__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.
Files changed (77) hide show
  1. spectre_core/_file_io/__init__.py +5 -5
  2. spectre_core/_file_io/file_handlers.py +61 -107
  3. spectre_core/batches/__init__.py +21 -4
  4. spectre_core/batches/_base.py +86 -135
  5. spectre_core/batches/_batches.py +56 -100
  6. spectre_core/batches/_factory.py +22 -21
  7. spectre_core/batches/_register.py +9 -9
  8. spectre_core/batches/plugins/_batch_keys.py +8 -7
  9. spectre_core/batches/plugins/_callisto.py +66 -98
  10. spectre_core/batches/plugins/_iq_stream.py +106 -170
  11. spectre_core/capture_configs/__init__.py +47 -18
  12. spectre_core/capture_configs/_capture_config.py +26 -53
  13. spectre_core/capture_configs/_capture_modes.py +9 -7
  14. spectre_core/capture_configs/_capture_templates.py +51 -111
  15. spectre_core/capture_configs/_parameters.py +38 -75
  16. spectre_core/capture_configs/_pconstraints.py +41 -41
  17. spectre_core/capture_configs/_pnames.py +37 -35
  18. spectre_core/capture_configs/_ptemplates.py +261 -348
  19. spectre_core/capture_configs/_pvalidators.py +99 -102
  20. spectre_core/config/__init__.py +14 -9
  21. spectre_core/config/_paths.py +19 -36
  22. spectre_core/config/_time_formats.py +7 -6
  23. spectre_core/exceptions.py +39 -1
  24. spectre_core/jobs/__init__.py +4 -7
  25. spectre_core/jobs/_duration.py +12 -0
  26. spectre_core/jobs/_jobs.py +73 -44
  27. spectre_core/jobs/_workers.py +56 -106
  28. spectre_core/logs/__init__.py +8 -3
  29. spectre_core/logs/_configure.py +14 -18
  30. spectre_core/logs/_decorators.py +7 -5
  31. spectre_core/logs/_logs.py +38 -90
  32. spectre_core/logs/_process_types.py +6 -4
  33. spectre_core/plotting/__init__.py +14 -4
  34. spectre_core/plotting/_base.py +65 -139
  35. spectre_core/plotting/_format.py +11 -9
  36. spectre_core/plotting/_panel_names.py +8 -6
  37. spectre_core/plotting/_panel_stack.py +83 -116
  38. spectre_core/plotting/_panels.py +121 -156
  39. spectre_core/post_processing/__init__.py +7 -4
  40. spectre_core/post_processing/_base.py +69 -69
  41. spectre_core/post_processing/_factory.py +15 -12
  42. spectre_core/post_processing/_post_processor.py +17 -13
  43. spectre_core/post_processing/_register.py +11 -8
  44. spectre_core/post_processing/plugins/_event_handler_keys.py +5 -4
  45. spectre_core/post_processing/plugins/_fixed_center_frequency.py +55 -48
  46. spectre_core/post_processing/plugins/_swept_center_frequency.py +200 -175
  47. spectre_core/receivers/__init__.py +10 -3
  48. spectre_core/receivers/_base.py +83 -149
  49. spectre_core/receivers/_factory.py +21 -31
  50. spectre_core/receivers/_register.py +8 -11
  51. spectre_core/receivers/_spec_names.py +18 -16
  52. spectre_core/receivers/plugins/_b200mini.py +48 -61
  53. spectre_core/receivers/plugins/_receiver_names.py +9 -7
  54. spectre_core/receivers/plugins/_rsp1a.py +45 -41
  55. spectre_core/receivers/plugins/_rspduo.py +60 -45
  56. spectre_core/receivers/plugins/_sdrplay_receiver.py +68 -84
  57. spectre_core/receivers/plugins/_test.py +137 -130
  58. spectre_core/receivers/plugins/_usrp.py +94 -86
  59. spectre_core/receivers/plugins/gr/__init__.py +2 -2
  60. spectre_core/receivers/plugins/gr/_base.py +15 -23
  61. spectre_core/receivers/plugins/gr/_rsp1a.py +53 -60
  62. spectre_core/receivers/plugins/gr/_rspduo.py +78 -90
  63. spectre_core/receivers/plugins/gr/_test.py +50 -58
  64. spectre_core/receivers/plugins/gr/_usrp.py +61 -59
  65. spectre_core/spectrograms/__init__.py +22 -14
  66. spectre_core/spectrograms/_analytical.py +109 -100
  67. spectre_core/spectrograms/_array_operations.py +40 -47
  68. spectre_core/spectrograms/_spectrogram.py +290 -323
  69. spectre_core/spectrograms/_transform.py +107 -74
  70. spectre_core/wgetting/__init__.py +2 -4
  71. spectre_core/wgetting/_callisto.py +88 -94
  72. {spectre_core-0.0.21.dist-info → spectre_core-0.0.23.dist-info}/METADATA +9 -23
  73. spectre_core-0.0.23.dist-info/RECORD +79 -0
  74. {spectre_core-0.0.21.dist-info → spectre_core-0.0.23.dist-info}/WHEEL +1 -1
  75. spectre_core-0.0.21.dist-info/RECORD +0 -78
  76. {spectre_core-0.0.21.dist-info → spectre_core-0.0.23.dist-info}/licenses/LICENSE +0 -0
  77. {spectre_core-0.0.21.dist-info → spectre_core-0.0.23.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
1
+ # SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
@@ -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 = "power"
36
- DIGITS = "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
- RELATIVE = "relative"
80
- DATETIMES = "datetimes"
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 : 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
+ 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 = self.num_times
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] = None
125
- self._end_background : Optional[str] = None
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 (self._start_datetime is not None)
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('timedelta64[us]')
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(self._dynamic_spectra[:, self._start_background_index:self._end_background_index+1],
312
- axis=-1)
313
-
314
-
315
- def compute_dynamic_spectra_dBb(
316
- self
317
- ) -> npt.NDArray[np.float32]:
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='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)
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(self._dynamic_spectra / background_spectra)
294
+ dynamic_spectra_dBb = 20 * np.log10(
295
+ self._dynamic_spectra / background_spectra
296
+ )
335
297
  else:
336
- raise NotImplementedError(f"{self._spectrum_unit} is unrecognised; decibel conversion is uncertain!")
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 = end_background
366
- self._update_background_indices_from_interval(self._start_background,
367
- self._end_background)
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(f"Expected dynamic spectrogram to be a 2D array, but got {num_spectrogram_dims}D array")
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(f"Mismatch in number of frequency bins: Expected {self.num_frequencies}, but got {dynamic_spectra_shape[0]}")
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(f"Mismatch in number of time bins: Expected {self.num_times}, but got {dynamic_spectra_shape[1]}")
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(I,
429
- self._start_background_index,
430
- self._end_background_index)
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( datetime.strptime(at_time, TimeFormat.DATETIME) )
459
- index_of_cut = find_closest_index(_at_datetime,
460
- self.datetimes,
461
- enforce_strict_bounds = True)
462
- time_of_cut = self.datetimes[index_of_cut]
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(_at_time,
467
- self._times,
468
- enforce_strict_bounds = True)
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[:, index_of_cut].copy() # make a copy so to preserve the spectrum on transformations of the cut
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("Ignoring frequency cut normalisation, since dBb units have been specified")
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 = False,
499
- correct_background = False,
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(_at_frequency,
522
- self._frequencies,
523
- enforce_strict_bounds = True)
524
- frequency_of_cut = float( self.frequencies[index_of_cut] )
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[index_of_cut,:].copy() # make a copy so to preserve the spectrum on transformations of the cut
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("Ignoring time cut normalisation, since dBb units have been specified")
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(cut,
542
- self._start_background_index,
543
- self._end_background_index)
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(f"Invalid return_time_type. Got {return_time_type}, "
561
- f"expected one of 'datetimes' or 'seconds'")
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 = dt.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
- file_name)
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 = 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))
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('SIMPLE', True, 'file does conform to FITS standard')
606
- primary_hdu.header.set('BITPIX', -32, 'number of bits per data pixel')
607
- primary_hdu.header.set('NAXIS', 2, 'number of data axes')
608
- primary_hdu.header.set('NAXIS1', spectrogram.dynamic_spectra.shape[1], 'length of data axis 1')
609
- primary_hdu.header.set('NAXIS2', spectrogram.dynamic_spectra.shape[0], 'length of data axis 2')
610
- primary_hdu.header.set('EXTEND', True, 'FITS dataset may contain extensions')
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 = start_datetime.strftime("%Y-%m-%d")
626
- start_time = start_datetime.strftime("%H:%M:%S.%f")
627
-
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")
631
-
632
- primary_hdu.header.set('DATE', start_date, 'time of observation')
633
- primary_hdu.header.set('CONTENT', f'{start_date} dynamic spectrogram', 'title of image')
634
- primary_hdu.header.set('ORIGIN', f'{ORIGIN}')
635
- primary_hdu.header.set('TELESCOP', f'{TELESCOP}', 'type of instrument')
636
- primary_hdu.header.set('INSTRUME', f'{INSTRUME}')
637
- primary_hdu.header.set('OBJECT', f'{OBJECT}', 'object description')
638
-
639
- primary_hdu.header.set('DATE-OBS', f'{start_date}', 'date observation starts')
640
- primary_hdu.header.set('TIME-OBS', f'{start_time}', 'time observation starts')
641
- primary_hdu.header.set('DATE-END', f'{end_date}', 'date observation ends')
642
- primary_hdu.header.set('TIME-END', f'{end_time}', 'time observation ends')
643
-
644
- primary_hdu.header.set('BZERO', 0, 'scaling offset')
645
- primary_hdu.header.set('BSCALE', 1, 'scaling factor')
646
- primary_hdu.header.set('BUNIT', f"{spectrogram.spectrum_unit.value}", 'z-axis title')
647
-
648
- primary_hdu.header.set('DATAMIN', np.nanmin(spectrogram.dynamic_spectra), 'minimum element in image')
649
- primary_hdu.header.set('DATAMAX', np.nanmax(spectrogram.dynamic_spectra), 'maximum element in image')
650
-
651
- primary_hdu.header.set('CRVAL1', f'{_seconds_of_day( start_datetime)}', 'value on axis 1 at reference pixel [sec of day]')
652
- primary_hdu.header.set('CRPIX1', 0, 'reference pixel of axis 1')
653
- primary_hdu.header.set('CTYPE1', 'TIME [UT]', 'title of axis 1')
654
- primary_hdu.header.set('CDELT1', spectrogram.time_resolution, 'step between first and second element in x-axis')
655
-
656
- primary_hdu.header.set('CRVAL2', 0, 'value on axis 2 at reference pixel')
657
- primary_hdu.header.set('CRPIX2', 0, 'reference pixel of axis 2')
658
- primary_hdu.header.set('CTYPE2', 'Frequency [Hz]', 'title of axis 2')
659
- primary_hdu.header.set('CDELT2', spectrogram.frequency_resolution, 'step between first and second element in axis')
660
-
661
- primary_hdu.header.set('OBS_LAT', f'{OBS_LAT}', 'observatory latitude in degree')
662
- primary_hdu.header.set('OBS_LAC', 'N', 'observatory latitude code {N,S}')
663
- primary_hdu.header.set('OBS_LON', f'{OBS_LON}', 'observatory longitude in degree')
664
- primary_hdu.header.set('OBS_LOC', 'W', 'observatory longitude code {E,W}')
665
- primary_hdu.header.set('OBS_ALT', f'{OBS_ALT}', 'observatory altitude in meter asl')
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='TIME', format='PD', array=times_wrapped)
676
- col2 = fits.Column(name='FREQUENCY', format='PD', array=frequencies_wrapped)
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('PCOUNT', 0, 'size of special data area')
682
- bin_table_hdu.header.set('GCOUNT', 1, 'one data group (required keyword)')
683
- bin_table_hdu.header.set('TFIELDS', 2, 'number of fields in each row')
684
- bin_table_hdu.header.set('TTYPE1', 'TIME', 'label for field 1')
685
- bin_table_hdu.header.set('TFORM1', 'D', 'data format of field: 8-byte DOUBLE')
686
- bin_table_hdu.header.set('TTYPE2', 'FREQUENCY', 'label for field 2')
687
- bin_table_hdu.header.set('TFORM2', 'D', 'data format of field: 8-byte DOUBLE')
688
- bin_table_hdu.header.set('TSCAL1', 1, '')
689
- bin_table_hdu.header.set('TZERO1', 0, '')
690
- bin_table_hdu.header.set('TSCAL2', 1, '')
691
- bin_table_hdu.header.set('TZERO2', 0, '')
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)