spectre-core 0.0.11__py3-none-any.whl → 0.0.12__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/file_handlers.py +12 -12
- spectre_core/batches/__init__.py +22 -0
- spectre_core/batches/_base.py +146 -0
- spectre_core/batches/_batches.py +197 -0
- spectre_core/batches/_factory.py +27 -0
- spectre_core/{chunks → batches}/_register.py +5 -5
- spectre_core/{chunks → batches}/library/_callisto.py +31 -33
- spectre_core/{chunks → batches}/library/_fixed_center_frequency.py +43 -38
- spectre_core/{chunks → batches}/library/_swept_center_frequency.py +22 -20
- spectre_core/capture_configs/_capture_templates.py +6 -6
- spectre_core/capture_configs/_parameters.py +3 -6
- spectre_core/capture_configs/_ptemplates.py +3 -3
- spectre_core/capture_configs/_pvalidators.py +4 -4
- spectre_core/config/__init__.py +2 -2
- spectre_core/config/_paths.py +5 -5
- spectre_core/config/_time_formats.py +5 -3
- spectre_core/exceptions.py +2 -2
- spectre_core/logging/_configure.py +1 -1
- spectre_core/logging/_log_handlers.py +1 -1
- spectre_core/plotting/_panels.py +1 -1
- spectre_core/post_processing/__init__.py +2 -2
- spectre_core/post_processing/_base.py +5 -5
- spectre_core/post_processing/_factory.py +3 -3
- spectre_core/post_processing/_post_processor.py +5 -5
- spectre_core/post_processing/library/_fixed_center_frequency.py +24 -25
- spectre_core/post_processing/library/_swept_center_frequency.py +68 -83
- spectre_core/receivers/gr/_base.py +1 -1
- spectre_core/receivers/gr/_rsp1a.py +3 -3
- spectre_core/receivers/gr/_rspduo.py +4 -4
- spectre_core/receivers/gr/_test.py +3 -3
- spectre_core/receivers/library/_test.py +3 -3
- spectre_core/spectrograms/_analytical.py +0 -6
- spectre_core/spectrograms/_spectrogram.py +113 -79
- spectre_core/spectrograms/_transform.py +19 -36
- spectre_core/wgetting/_callisto.py +20 -24
- {spectre_core-0.0.11.dist-info → spectre_core-0.0.12.dist-info}/METADATA +1 -1
- spectre_core-0.0.12.dist-info/RECORD +64 -0
- spectre_core/chunks/__init__.py +0 -22
- spectre_core/chunks/_base.py +0 -116
- spectre_core/chunks/_chunks.py +0 -200
- spectre_core/chunks/_factory.py +0 -25
- spectre_core-0.0.11.dist-info/RECORD +0 -64
- {spectre_core-0.0.11.dist-info → spectre_core-0.0.12.dist-info}/LICENSE +0 -0
- {spectre_core-0.0.11.dist-info → spectre_core-0.0.12.dist-info}/WHEEL +0 -0
- {spectre_core-0.0.11.dist-info → spectre_core-0.0.12.dist-info}/top_level.txt +0 -0
@@ -12,7 +12,7 @@ import numpy as np
|
|
12
12
|
from astropy.io import fits
|
13
13
|
|
14
14
|
from spectre_core.capture_configs import CaptureConfig, PNames
|
15
|
-
from spectre_core.config import
|
15
|
+
from spectre_core.config import get_batches_dir_path, TimeFormats
|
16
16
|
from ._array_operations import (
|
17
17
|
find_closest_index,
|
18
18
|
normalise_peak_intensity,
|
@@ -21,16 +21,10 @@ from ._array_operations import (
|
|
21
21
|
subtract_background,
|
22
22
|
)
|
23
23
|
|
24
|
-
__all__ = [
|
25
|
-
"FrequencyCut",
|
26
|
-
"TimeCut",
|
27
|
-
"TimeTypes",
|
28
|
-
"SpectrumTypes",
|
29
|
-
"Spectrogram"
|
30
|
-
]
|
31
24
|
|
32
25
|
@dataclass
|
33
26
|
class FrequencyCut:
|
27
|
+
"""A container to hold a cut of a dynamic spectra at a particular instant of time."""
|
34
28
|
time: float | datetime
|
35
29
|
frequencies: np.ndarray
|
36
30
|
cut: np.ndarray
|
@@ -39,6 +33,7 @@ class FrequencyCut:
|
|
39
33
|
|
40
34
|
@dataclass
|
41
35
|
class TimeCut:
|
36
|
+
"""A container to hold a cut of a dynamic spectra at a particular frequency."""
|
42
37
|
frequency: float
|
43
38
|
times: np.ndarray
|
44
39
|
cut: np.ndarray
|
@@ -47,178 +42,211 @@ class TimeCut:
|
|
47
42
|
|
48
43
|
@dataclass(frozen=True)
|
49
44
|
class TimeTypes:
|
45
|
+
"""Container to hold the different types of time we can assign to each spectrum in the dynamic spectra.
|
46
|
+
|
47
|
+
'SECONDS' is equivalent to 'seconds elapsed since the first spectrum'.
|
48
|
+
'DATETIMES' is equivalent to 'the datetime associated with each spectrum'.
|
49
|
+
"""
|
50
50
|
SECONDS : str = "seconds"
|
51
51
|
DATETIMES: str = "datetimes"
|
52
52
|
|
53
53
|
|
54
54
|
@dataclass(frozen=True)
|
55
55
|
class SpectrumTypes:
|
56
|
+
"""A container for defined units of dynamic spectra."""
|
56
57
|
AMPLITUDE: str = "amplitude"
|
57
58
|
POWER : str = "power"
|
58
59
|
DIGITS : str = "digits"
|
59
60
|
|
60
61
|
|
61
62
|
class Spectrogram:
|
63
|
+
"""A convenient, standardised wrapper for spectrogram data."""
|
62
64
|
def __init__(self,
|
63
|
-
dynamic_spectra: np.ndarray,
|
64
|
-
times: np.ndarray,
|
65
|
-
frequencies: np.ndarray,
|
65
|
+
dynamic_spectra: np.ndarray,
|
66
|
+
times: np.ndarray,
|
67
|
+
frequencies: np.ndarray,
|
66
68
|
tag: str,
|
67
|
-
|
68
|
-
|
69
|
-
spectrum_type: Optional[str] = None,
|
70
|
-
start_background: Optional[str] = None,
|
71
|
-
end_background: Optional[str] = None):
|
69
|
+
start_datetime: Optional[datetime] = None,
|
70
|
+
spectrum_type: Optional[str] = None):
|
72
71
|
|
73
72
|
# dynamic spectra
|
74
73
|
self._dynamic_spectra = dynamic_spectra
|
75
|
-
self.
|
74
|
+
self._dynamic_spectra_dBb: Optional[np.ndarray] = None # cache
|
76
75
|
|
77
76
|
# assigned times and frequencies
|
78
77
|
if times[0] != 0:
|
79
|
-
raise ValueError(f"The first spectrum must correspond to t=0
|
78
|
+
raise ValueError(f"The first spectrum must correspond to t=0")
|
80
79
|
|
81
80
|
self._times = times
|
82
|
-
self._datetimes: Optional[list[datetime]] = None # cache
|
83
81
|
self._frequencies = frequencies
|
84
82
|
|
85
83
|
# general metadata
|
86
84
|
self._tag = tag
|
87
|
-
self._chunk_start_time = chunk_start_time
|
88
|
-
self._chunk_start_datetime: Optional[datetime] = None # cache
|
89
|
-
self._microsecond_correction = microsecond_correction
|
90
85
|
self._spectrum_type = spectrum_type
|
91
|
-
|
86
|
+
|
87
|
+
# datetime information
|
88
|
+
self._start_datetime = start_datetime
|
89
|
+
self._datetimes: Optional[list[datetime]] = None # cache
|
90
|
+
|
92
91
|
# background metadata
|
93
92
|
self._background_spectrum: Optional[np.ndarray] = None # cache
|
94
|
-
self.
|
95
|
-
self.
|
96
|
-
|
97
|
-
self.
|
93
|
+
self._start_background_index = 0
|
94
|
+
self._end_background_index = self.num_times
|
95
|
+
# background interval can be set after instanitation.
|
96
|
+
self._start_background = None
|
97
|
+
self._end_background = None
|
98
|
+
|
99
|
+
# finally check that the spectrogram arrays are matching in shape
|
98
100
|
self._check_shapes()
|
99
101
|
|
100
102
|
|
101
103
|
@property
|
102
104
|
def dynamic_spectra(self) -> np.ndarray:
|
105
|
+
"""The dynamic spectra."""
|
103
106
|
return self._dynamic_spectra
|
104
107
|
|
105
108
|
|
106
109
|
@property
|
107
110
|
def times(self) -> np.ndarray:
|
111
|
+
"""The physical time assigned to each spectrum.
|
112
|
+
|
113
|
+
Equivalent to the 'seconds elapsed since the first spectrum'. So, by convention, the
|
114
|
+
first spectrum is at t=0.
|
115
|
+
"""
|
108
116
|
return self._times
|
109
117
|
|
110
118
|
|
111
119
|
@property
|
112
120
|
def num_times(self) -> int:
|
121
|
+
"""The size of the times array. Equivalent to the number of spectrums in the spectrogram."""
|
113
122
|
return len(self._times)
|
114
123
|
|
115
124
|
|
116
125
|
@property
|
117
126
|
def time_resolution(self) -> float:
|
127
|
+
"""The time resolution of the dynamic spectra."""
|
118
128
|
return compute_resolution(self._times)
|
119
129
|
|
120
130
|
|
121
131
|
@property
|
122
132
|
def time_range(self) -> float:
|
133
|
+
"""The time range of the dynamic spectra."""
|
123
134
|
return compute_range(self._times)
|
124
135
|
|
125
136
|
|
126
|
-
@property
|
127
|
-
def datetimes(self) -> list[datetime]:
|
128
|
-
if self._datetimes is None:
|
129
|
-
self._datetimes = [self.chunk_start_datetime + timedelta(seconds=(t + self.microsecond_correction*1e-6)) for t in self._times]
|
130
|
-
return self._datetimes
|
131
|
-
|
132
|
-
|
133
137
|
@property
|
134
138
|
def frequencies(self) -> np.ndarray:
|
139
|
+
"""The physical frequency assigned to each spectral component."""
|
135
140
|
return self._frequencies
|
136
141
|
|
137
142
|
|
138
143
|
@property
|
139
144
|
def num_frequencies(self) -> int:
|
145
|
+
"""The number of spectral components."""
|
140
146
|
return len(self._frequencies)
|
141
147
|
|
142
148
|
|
143
149
|
@property
|
144
150
|
def frequency_resolution(self) -> float:
|
151
|
+
"""The frequency resolution of the dynamic spectra."""
|
145
152
|
return compute_resolution(self._frequencies)
|
146
153
|
|
147
154
|
|
148
155
|
@property
|
149
156
|
def frequency_range(self) -> float:
|
157
|
+
"""The frequency range covered by the dynamic spectra."""
|
150
158
|
return compute_range(self._frequencies)
|
151
159
|
|
152
160
|
|
153
161
|
@property
|
154
162
|
def tag(self) -> str:
|
163
|
+
"""The tag identifier corresponding to the dynamic spectra."""
|
155
164
|
return self._tag
|
156
165
|
|
157
166
|
|
158
167
|
@property
|
159
|
-
def
|
160
|
-
if
|
161
|
-
|
162
|
-
|
168
|
+
def start_datetime_is_set(self) -> bool:
|
169
|
+
"""Returns true if the start datetime for the spectrogram has been set."""
|
170
|
+
return (self._start_datetime is not None)
|
171
|
+
|
163
172
|
|
164
|
-
|
165
173
|
@property
|
166
|
-
def
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
174
|
+
def start_datetime(self) -> datetime:
|
175
|
+
"""The datetime assigned to the first spectrum in the dynamic spectra."""
|
176
|
+
if self._start_datetime is None:
|
177
|
+
raise AttributeError(f"A start time has not been set.")
|
178
|
+
return self._start_datetime
|
179
|
+
|
180
|
+
|
181
|
+
def format_start_time(self,
|
182
|
+
precise: bool = False) -> str:
|
183
|
+
"""The datetime assigned to the first spectrum in the dynamic spectra, formatted as a string."""
|
184
|
+
if precise:
|
185
|
+
return datetime.strftime(self.start_datetime, TimeFormats.PRECISE_DATETIME)
|
186
|
+
return datetime.strftime(self.start_datetime, TimeFormats.DATETIME)
|
187
|
+
|
188
|
+
|
172
189
|
@property
|
173
|
-
def
|
174
|
-
|
190
|
+
def datetimes(self) -> list[datetime]:
|
191
|
+
"""The datetimes associated with each spectrum in the dynamic spectra."""
|
192
|
+
if self._datetimes is None:
|
193
|
+
self._datetimes = [self.start_datetime + timedelta( seconds=(float(t)) ) for t in self._times]
|
194
|
+
return self._datetimes
|
175
195
|
|
176
196
|
|
177
197
|
@property
|
178
198
|
def spectrum_type(self) -> Optional[str]:
|
199
|
+
"""The units of the dynamic spectra."""
|
179
200
|
return self._spectrum_type
|
180
201
|
|
181
202
|
|
182
203
|
@property
|
183
204
|
def start_background(self) -> Optional[str]:
|
205
|
+
"""The start of the background interval, as a datetime string up to seconds precision."""
|
184
206
|
return self._start_background
|
185
207
|
|
186
208
|
|
187
209
|
@property
|
188
210
|
def end_background(self) -> Optional[str]:
|
211
|
+
"""The end of the background interval, as a datetime string up to seconds precision."""
|
189
212
|
return self._end_background
|
190
213
|
|
191
214
|
|
192
215
|
@property
|
193
216
|
def background_spectrum(self) -> np.ndarray:
|
217
|
+
"""The background spectrum, computed by averaging the dynamic spectra according to the specified background interval.
|
218
|
+
|
219
|
+
By default, the entire dynamic spectra is averaged over.
|
220
|
+
"""
|
194
221
|
if self._background_spectrum is None:
|
195
222
|
self._background_spectrum = np.nanmean(self._dynamic_spectra[:, self._start_background_index:self._end_background_index+1],
|
196
|
-
|
223
|
+
axis=-1)
|
197
224
|
return self._background_spectrum
|
198
225
|
|
199
226
|
|
200
227
|
@property
|
201
|
-
def
|
202
|
-
|
228
|
+
def dynamic_spectra_dBb(self) -> np.ndarray:
|
229
|
+
"""The dynamic spectra in units of decibels above the background spectrum."""
|
230
|
+
if self._dynamic_spectra_dBb is None:
|
203
231
|
# Create an artificial spectrogram where each spectrum is identically the background spectrum
|
204
232
|
background_spectra = self.background_spectrum[:, np.newaxis]
|
205
233
|
# Suppress divide by zero and invalid value warnings for this block of code
|
206
234
|
with np.errstate(divide='ignore'):
|
207
235
|
# Depending on the spectrum type, compute the dBb values differently
|
208
236
|
if self._spectrum_type == SpectrumTypes.AMPLITUDE or self._spectrum_type == SpectrumTypes.DIGITS:
|
209
|
-
self.
|
237
|
+
self._dynamic_spectra_dBb = 10 * np.log10(self._dynamic_spectra / background_spectra)
|
210
238
|
elif self._spectrum_type == SpectrumTypes.POWER:
|
211
|
-
self.
|
239
|
+
self._dynamic_spectra_dBb = 20 * np.log10(self._dynamic_spectra / background_spectra)
|
212
240
|
else:
|
213
241
|
raise NotImplementedError(f"{self.spectrum_type} unrecognised, uncertain decibel conversion!")
|
214
|
-
return self.
|
242
|
+
return self._dynamic_spectra_dBb
|
215
243
|
|
216
244
|
|
217
245
|
def set_background(self,
|
218
246
|
start_background: str,
|
219
247
|
end_background: str) -> None:
|
220
248
|
"""Public setter for start and end of the background"""
|
221
|
-
self.
|
249
|
+
self._dynamic_spectra_dBb = None # reset cache
|
222
250
|
self._background_spectrum = None # reset cache
|
223
251
|
self._start_background = start_background
|
224
252
|
self._end_background = end_background
|
@@ -228,14 +256,13 @@ class Spectrogram:
|
|
228
256
|
|
229
257
|
def _update_background_indices_from_interval(self) -> None:
|
230
258
|
start_background = datetime.strptime(self._start_background, TimeFormats.DATETIME)
|
259
|
+
end_background = datetime.strptime(self._end_background, TimeFormats.DATETIME)
|
231
260
|
self._start_background_index = find_closest_index(start_background,
|
232
261
|
self.datetimes,
|
233
262
|
enforce_strict_bounds=True)
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
self.datetimes,
|
238
|
-
enforce_strict_bounds=True)
|
263
|
+
self._end_background_index = find_closest_index(end_background,
|
264
|
+
self.datetimes,
|
265
|
+
enforce_strict_bounds=True)
|
239
266
|
|
240
267
|
|
241
268
|
def _check_shapes(self) -> None:
|
@@ -253,19 +280,14 @@ class Spectrogram:
|
|
253
280
|
|
254
281
|
|
255
282
|
def save(self) -> None:
|
256
|
-
|
257
|
-
|
258
|
-
day = self.chunk_start_datetime.day)
|
259
|
-
file_name = f"{self.chunk_start_time}_{self._tag}.fits"
|
260
|
-
write_path = os.path.join(chunk_parent_path,
|
261
|
-
file_name)
|
262
|
-
_save_spectrogram(write_path, self)
|
283
|
+
"""Save the spectrogram as a fits file."""
|
284
|
+
_save_spectrogram(self)
|
263
285
|
|
264
286
|
|
265
287
|
def integrate_over_frequency(self,
|
266
288
|
correct_background: bool = False,
|
267
289
|
peak_normalise: bool = False) -> np.ndarray[np.float32]:
|
268
|
-
|
290
|
+
"""Return the dynamic spectra, numerically integrated over frequency."""
|
269
291
|
# integrate over frequency
|
270
292
|
I = np.trapz(self._dynamic_spectra, self._frequencies, axis=0)
|
271
293
|
|
@@ -282,10 +304,12 @@ class Spectrogram:
|
|
282
304
|
at_time: float | str,
|
283
305
|
dBb: bool = False,
|
284
306
|
peak_normalise: bool = False) -> FrequencyCut:
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
307
|
+
"""Get a cut of the dynamic spectra at a particular instant of time.
|
308
|
+
|
309
|
+
It is important to note that the 'at_time' as specified at input may not correspond exactly
|
310
|
+
to one of the times assigned to each spectrogram.
|
311
|
+
"""
|
312
|
+
|
289
313
|
if isinstance(at_time, str):
|
290
314
|
at_time = datetime.strptime(at_time, TimeFormats.DATETIME)
|
291
315
|
index_of_cut = find_closest_index(at_time,
|
@@ -303,7 +327,7 @@ class Spectrogram:
|
|
303
327
|
raise ValueError(f"Type of at_time is unsupported: {type(at_time)}")
|
304
328
|
|
305
329
|
if dBb:
|
306
|
-
ds = self.
|
330
|
+
ds = self.dynamic_spectra_dBb
|
307
331
|
else:
|
308
332
|
ds = self._dynamic_spectra
|
309
333
|
|
@@ -328,10 +352,11 @@ class Spectrogram:
|
|
328
352
|
peak_normalise = False,
|
329
353
|
correct_background = False,
|
330
354
|
return_time_type: str = TimeTypes.SECONDS) -> TimeCut:
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
355
|
+
"""Get a cut of the dynamic spectra at a particular frequency.
|
356
|
+
|
357
|
+
It is important to note that the 'at_frequency' as specified at input may not correspond exactly
|
358
|
+
to one of the times assigned to each spectrogram.
|
359
|
+
"""
|
335
360
|
index_of_cut = find_closest_index(at_frequency,
|
336
361
|
self._frequencies,
|
337
362
|
enforce_strict_bounds = True)
|
@@ -346,7 +371,7 @@ class Spectrogram:
|
|
346
371
|
|
347
372
|
# dependent on the requested cut type, we return the dynamic spectra in the preferred units
|
348
373
|
if dBb:
|
349
|
-
ds = self.
|
374
|
+
ds = self.dynamic_spectra_dBb
|
350
375
|
else:
|
351
376
|
ds = self.dynamic_spectra
|
352
377
|
|
@@ -379,9 +404,18 @@ def _seconds_of_day(dt: datetime) -> float:
|
|
379
404
|
|
380
405
|
|
381
406
|
# Function to create a FITS file with the specified structure
|
382
|
-
def _save_spectrogram(
|
383
|
-
|
407
|
+
def _save_spectrogram(spectrogram: Spectrogram) -> None:
|
408
|
+
|
409
|
+
# making the write path
|
410
|
+
batch_parent_path = get_batches_dir_path(year = spectrogram.start_datetime.year,
|
411
|
+
month = spectrogram.start_datetime.month,
|
412
|
+
day = spectrogram.start_datetime.day)
|
413
|
+
# file name formatted as a batch file
|
414
|
+
file_name = f"{spectrogram.format_start_time()}_{spectrogram.tag}.fits"
|
415
|
+
write_path = os.path.join(batch_parent_path,
|
416
|
+
file_name)
|
384
417
|
|
418
|
+
# get optional metadata from the capture config
|
385
419
|
capture_config = CaptureConfig(spectrogram.tag)
|
386
420
|
ORIGIN = capture_config.get_parameter_value(PNames.ORIGIN)
|
387
421
|
INSTRUME = capture_config.get_parameter_value(PNames.INSTRUMENT)
|
@@ -11,13 +11,6 @@ from spectre_core.config import TimeFormats
|
|
11
11
|
from ._array_operations import find_closest_index, average_array
|
12
12
|
from ._spectrogram import Spectrogram
|
13
13
|
|
14
|
-
__all__ = [
|
15
|
-
"frequency_chop",
|
16
|
-
"time_chop",
|
17
|
-
"frequency_average",
|
18
|
-
"time_average",
|
19
|
-
"join_spectrograms"
|
20
|
-
]
|
21
14
|
|
22
15
|
def frequency_chop(input_spectrogram: Spectrogram,
|
23
16
|
start_frequency: float | int,
|
@@ -50,9 +43,8 @@ def frequency_chop(input_spectrogram: Spectrogram,
|
|
50
43
|
input_spectrogram.times,
|
51
44
|
transformed_frequencies,
|
52
45
|
input_spectrogram.tag,
|
53
|
-
|
54
|
-
|
55
|
-
spectrum_type = input_spectrogram.spectrum_type)
|
46
|
+
input_spectrogram.start_datetime,
|
47
|
+
input_spectrogram.spectrum_type)
|
56
48
|
|
57
49
|
|
58
50
|
def time_chop(input_spectrogram: Spectrogram,
|
@@ -84,9 +76,6 @@ def time_chop(input_spectrogram: Spectrogram,
|
|
84
76
|
|
85
77
|
# compute the new start datetime following the time chop
|
86
78
|
transformed_start_datetime = input_spectrogram.datetimes[start_index]
|
87
|
-
# compute the microsecond correction, and chunk start time
|
88
|
-
transformed_chunk_start_time = datetime.strftime(transformed_start_datetime, TimeFormats.DATETIME)
|
89
|
-
transformed_microsecond_correction = transformed_start_datetime.microsecond
|
90
79
|
|
91
80
|
# chop the times array
|
92
81
|
transformed_times = input_spectrogram.times[start_index:end_index+1]
|
@@ -97,8 +86,7 @@ def time_chop(input_spectrogram: Spectrogram,
|
|
97
86
|
transformed_times,
|
98
87
|
input_spectrogram.frequencies,
|
99
88
|
input_spectrogram.tag,
|
100
|
-
|
101
|
-
microsecond_correction = transformed_microsecond_correction,
|
89
|
+
start_time = transformed_start_datetime,
|
102
90
|
spectrum_type = input_spectrogram.spectrum_type)
|
103
91
|
|
104
92
|
|
@@ -107,8 +95,8 @@ def time_average(input_spectrogram: Spectrogram,
|
|
107
95
|
average_over: Optional[int] = None) -> Spectrogram:
|
108
96
|
|
109
97
|
# spectre does not currently support averaging of non-datetime assigned spectrograms
|
110
|
-
if input_spectrogram.
|
111
|
-
raise
|
98
|
+
if not input_spectrogram.start_datetime_is_set:
|
99
|
+
raise NotImplementedError(f"Time averaging is not yet supported for spectrograms without an assigned datetime.")
|
112
100
|
|
113
101
|
# if nothing is specified, do nothing
|
114
102
|
if (resolution is None) and (average_over is None):
|
@@ -130,28 +118,26 @@ def time_average(input_spectrogram: Spectrogram,
|
|
130
118
|
|
131
119
|
# average the dynamic spectra array
|
132
120
|
transformed_dynamic_spectra = average_array(input_spectrogram.dynamic_spectra,
|
133
|
-
|
134
|
-
|
121
|
+
average_over,
|
122
|
+
axis=1)
|
135
123
|
|
136
124
|
# We need to assign timestamps to the averaged spectrums in the spectrograms.
|
137
125
|
# The natural way to do this is to assign the i'th averaged spectrogram
|
138
126
|
# to the i'th averaged time
|
139
127
|
transformed_times = average_array(input_spectrogram.times, average_over)
|
140
128
|
|
141
|
-
# find the new
|
142
|
-
|
143
|
-
transformed_chunk_start_time = corrected_start_datetime.strftime(TimeFormats.DATETIME)
|
144
|
-
transformed_microsecond_correction = corrected_start_datetime.microsecond
|
129
|
+
# find the new batch start time, which we will assign to the first spectrum after averaging
|
130
|
+
transformed_start_datetime = input_spectrogram.datetimes[0] + timedelta(seconds = float(transformed_times[0]))
|
145
131
|
|
146
132
|
# finally, translate the averaged time seconds to begin at t=0 [s]
|
147
133
|
transformed_times -= transformed_times[0]
|
134
|
+
|
148
135
|
return Spectrogram(transformed_dynamic_spectra,
|
149
136
|
transformed_times,
|
150
137
|
input_spectrogram.frequencies,
|
151
138
|
input_spectrogram.tag,
|
152
|
-
|
153
|
-
|
154
|
-
spectrum_type = input_spectrogram.spectrum_type)
|
139
|
+
transformed_start_datetime,
|
140
|
+
input_spectrogram.spectrum_type)
|
155
141
|
|
156
142
|
|
157
143
|
|
@@ -189,9 +175,8 @@ def frequency_average(input_spectrogram: Spectrogram,
|
|
189
175
|
input_spectrogram.times,
|
190
176
|
transformed_frequencies,
|
191
177
|
input_spectrogram.tag,
|
192
|
-
|
193
|
-
|
194
|
-
spectrum_type = input_spectrogram.spectrum_type)
|
178
|
+
input_spectrogram.start_datetime,
|
179
|
+
input_spectrogram.spectrum_type)
|
195
180
|
|
196
181
|
|
197
182
|
def _time_elapsed(datetimes: np.ndarray) -> np.ndarray:
|
@@ -203,7 +188,7 @@ def _time_elapsed(datetimes: np.ndarray) -> np.ndarray:
|
|
203
188
|
return np.array(elapsed_time, dtype=np.float32)
|
204
189
|
|
205
190
|
|
206
|
-
# we assume that the spectrogram list is
|
191
|
+
# we assume that the spectrogram list is ordered chronologically
|
207
192
|
# we assume there is no time overlap in any of the spectrograms in the list
|
208
193
|
def join_spectrograms(spectrograms: list[Spectrogram]) -> Spectrogram:
|
209
194
|
|
@@ -224,9 +209,8 @@ def join_spectrograms(spectrograms: list[Spectrogram]) -> Spectrogram:
|
|
224
209
|
raise ValueError(f"All tags must be equal for each spectrogram in the input list!")
|
225
210
|
if spectrogram.spectrum_type != reference_spectrogram.spectrum_type:
|
226
211
|
raise ValueError(f"All units must be equal for each spectrogram in the input list!")
|
227
|
-
if spectrogram.
|
228
|
-
raise ValueError(f"All spectrograms must have
|
229
|
-
|
212
|
+
if not spectrogram.start_datetime_is_set:
|
213
|
+
raise ValueError(f"All spectrograms must have their start datetime set.")
|
230
214
|
|
231
215
|
# build a list of the time array of each spectrogram in the list
|
232
216
|
conc_datetimes = np.concatenate([s.datetimes for s in spectrograms])
|
@@ -249,6 +233,5 @@ def join_spectrograms(spectrograms: list[Spectrogram]) -> Spectrogram:
|
|
249
233
|
transformed_times,
|
250
234
|
reference_spectrogram.frequencies,
|
251
235
|
reference_spectrogram.tag,
|
252
|
-
|
253
|
-
|
254
|
-
spectrum_type = reference_spectrogram.spectrum_type)
|
236
|
+
reference_spectrogram.start_datetime,
|
237
|
+
reference_spectrogram.spectrum_type)
|
@@ -9,7 +9,7 @@ import gzip
|
|
9
9
|
from datetime import datetime
|
10
10
|
from typing import Optional
|
11
11
|
|
12
|
-
from spectre_core.config import get_spectre_data_dir_path,
|
12
|
+
from spectre_core.config import get_spectre_data_dir_path, get_batches_dir_path, TimeFormats
|
13
13
|
|
14
14
|
CALLISTO_INSTRUMENT_CODES = [
|
15
15
|
"ALASKA-ANCHORAGE",
|
@@ -70,13 +70,13 @@ CALLISTO_INSTRUMENT_CODES = [
|
|
70
70
|
_temp_dir = os.path.join(get_spectre_data_dir_path(), "temp")
|
71
71
|
|
72
72
|
|
73
|
-
def
|
73
|
+
def _get_batch_name(station: str, date: str, time: str, instrument_code: str) -> str:
|
74
74
|
dt = datetime.strptime(f"{date}T{time}", '%Y%m%dT%H%M%S')
|
75
75
|
formatted_time = dt.strftime(TimeFormats.DATETIME)
|
76
76
|
return f"{formatted_time}_callisto-{station.lower()}-{instrument_code}.fits"
|
77
77
|
|
78
78
|
|
79
|
-
def
|
79
|
+
def _get_batch_components(gz_path: str):
|
80
80
|
file_name = os.path.basename(gz_path)
|
81
81
|
if not file_name.endswith(".fit.gz"):
|
82
82
|
raise ValueError(f"Unexpected file extension in {file_name}. Expected .fit.gz")
|
@@ -89,29 +89,29 @@ def _get_chunk_components(gz_path: str):
|
|
89
89
|
return parts
|
90
90
|
|
91
91
|
|
92
|
-
def
|
93
|
-
station, date, time, instrument_code =
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
month =
|
99
|
-
day =
|
100
|
-
if not os.path.exists(
|
101
|
-
os.makedirs(
|
102
|
-
return os.path.join(
|
92
|
+
def _get_batch_path(gz_path: str) -> str:
|
93
|
+
station, date, time, instrument_code = _get_batch_components(gz_path)
|
94
|
+
fits_batch_name = _get_batch_name(station, date, time, instrument_code)
|
95
|
+
batch_start_time = fits_batch_name.split('_')[0]
|
96
|
+
batch_start_datetime = datetime.strptime(batch_start_time, TimeFormats.DATETIME)
|
97
|
+
batch_parent_path = get_batches_dir_path(year = batch_start_datetime.year,
|
98
|
+
month = batch_start_datetime.month,
|
99
|
+
day = batch_start_datetime.day)
|
100
|
+
if not os.path.exists(batch_parent_path):
|
101
|
+
os.makedirs(batch_parent_path)
|
102
|
+
return os.path.join(batch_parent_path, fits_batch_name)
|
103
103
|
|
104
104
|
|
105
|
-
def
|
106
|
-
fits_path =
|
105
|
+
def _unzip_file_to_batches(gz_path: str):
|
106
|
+
fits_path = _get_batch_path(gz_path)
|
107
107
|
with gzip.open(gz_path, 'rb') as f_in, open(fits_path, 'wb') as f_out:
|
108
108
|
shutil.copyfileobj(f_in, f_out)
|
109
109
|
|
110
110
|
|
111
|
-
def
|
111
|
+
def _unzip_to_batches():
|
112
112
|
for entry in os.scandir(_temp_dir):
|
113
113
|
if entry.is_file() and entry.name.endswith('.gz'):
|
114
|
-
|
114
|
+
_unzip_file_to_batches(entry.path)
|
115
115
|
os.remove(entry.path)
|
116
116
|
|
117
117
|
|
@@ -128,11 +128,7 @@ def _wget_callisto_data(instrument_code: str,
|
|
128
128
|
'-P', _temp_dir,
|
129
129
|
base_url
|
130
130
|
]
|
131
|
-
|
132
|
-
try:
|
133
|
-
subprocess.run(command, check=True)
|
134
|
-
except subprocess.CalledProcessError as e:
|
135
|
-
print(f"An error occurred: {e}")
|
131
|
+
subprocess.run(command, check=True)
|
136
132
|
|
137
133
|
|
138
134
|
def download_callisto_data(instrument_code: Optional[str],
|
@@ -151,5 +147,5 @@ def download_callisto_data(instrument_code: Optional[str],
|
|
151
147
|
raise ValueError(f"No match found for '{instrument_code}'. Expected one of {CALLISTO_INSTRUMENT_CODES}")
|
152
148
|
|
153
149
|
_wget_callisto_data(instrument_code, year, month, day)
|
154
|
-
|
150
|
+
_unzip_to_batches()
|
155
151
|
shutil.rmtree(_temp_dir)
|