spectre-core 0.0.9__py3-none-any.whl → 0.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spectre_core/__init__.py +0 -3
- spectre_core/_file_io/__init__.py +15 -0
- spectre_core/_file_io/file_handlers.py +128 -0
- spectre_core/capture_configs/__init__.py +29 -0
- spectre_core/capture_configs/_capture_config.py +85 -0
- spectre_core/capture_configs/_capture_templates.py +222 -0
- spectre_core/capture_configs/_parameters.py +110 -0
- spectre_core/capture_configs/_pconstraints.py +82 -0
- spectre_core/capture_configs/_ptemplates.py +450 -0
- spectre_core/capture_configs/_pvalidators.py +171 -0
- spectre_core/chunks/__init__.py +17 -201
- spectre_core/chunks/{base.py → _base.py} +15 -60
- spectre_core/chunks/_chunks.py +200 -0
- spectre_core/chunks/{factory.py → _factory.py} +6 -7
- spectre_core/chunks/library/{callisto/chunk.py → _callisto.py} +4 -7
- spectre_core/chunks/library/{fixed/chunk.py → _fixed_center_frequency.py} +7 -64
- spectre_core/chunks/library/_swept_center_frequency.py +103 -0
- spectre_core/config/__init__.py +20 -0
- spectre_core/config/_paths.py +77 -0
- spectre_core/config/_time_formats.py +15 -0
- spectre_core/exceptions.py +4 -5
- spectre_core/logging/__init__.py +11 -0
- spectre_core/logging/_configure.py +35 -0
- spectre_core/logging/_decorators.py +19 -0
- spectre_core/{logging.py → logging/_log_handlers.py} +13 -58
- spectre_core/plotting/__init__.py +7 -1
- spectre_core/plotting/{base.py → _base.py} +40 -20
- spectre_core/plotting/_format.py +18 -0
- spectre_core/plotting/{panel_stack.py → _panel_stack.py} +48 -48
- spectre_core/plotting/_panels.py +234 -0
- spectre_core/post_processing/__init__.py +10 -2
- spectre_core/post_processing/_base.py +119 -0
- spectre_core/post_processing/{factory.py → _factory.py} +7 -6
- spectre_core/post_processing/{post_processor.py → _post_processor.py} +3 -3
- spectre_core/post_processing/library/_fixed_center_frequency.py +115 -0
- spectre_core/post_processing/library/_swept_center_frequency.py +382 -0
- spectre_core/receivers/__init__.py +13 -2
- spectre_core/receivers/_base.py +180 -0
- spectre_core/receivers/{factory.py → _factory.py} +2 -2
- spectre_core/receivers/_spec_names.py +20 -0
- spectre_core/receivers/gr/__init__.py +3 -0
- spectre_core/receivers/gr/_base.py +33 -0
- spectre_core/receivers/gr/_rsp1a.py +158 -0
- spectre_core/receivers/gr/_rspduo.py +227 -0
- spectre_core/receivers/gr/_test.py +123 -0
- spectre_core/receivers/library/_rsp1a.py +61 -0
- spectre_core/receivers/library/_rspduo.py +69 -0
- spectre_core/receivers/library/_sdrplay_receiver.py +185 -0
- spectre_core/receivers/library/_test.py +221 -0
- spectre_core/spectrograms/__init__.py +18 -0
- spectre_core/spectrograms/{analytical.py → _analytical.py} +29 -27
- spectre_core/spectrograms/{array_operations.py → _array_operations.py} +47 -1
- spectre_core/spectrograms/{spectrogram.py → _spectrogram.py} +62 -35
- spectre_core/spectrograms/{transform.py → _transform.py} +76 -89
- spectre_core/{post_processing/library → wgetting}/__init__.py +4 -5
- spectre_core/wgetting/_callisto.py +155 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.11.dist-info}/METADATA +1 -1
- spectre_core-0.0.11.dist-info/RECORD +64 -0
- spectre_core/cfg.py +0 -116
- spectre_core/chunks/library/__init__.py +0 -8
- spectre_core/chunks/library/callisto/__init__.py +0 -0
- spectre_core/chunks/library/fixed/__init__.py +0 -0
- spectre_core/chunks/library/sweep/__init__.py +0 -0
- spectre_core/chunks/library/sweep/chunk.py +0 -400
- spectre_core/dynamic_imports.py +0 -22
- spectre_core/file_handlers/base.py +0 -68
- spectre_core/file_handlers/configs.py +0 -271
- spectre_core/file_handlers/json.py +0 -40
- spectre_core/file_handlers/text.py +0 -21
- spectre_core/plotting/factory.py +0 -26
- spectre_core/plotting/format.py +0 -19
- spectre_core/plotting/library/__init__.py +0 -7
- spectre_core/plotting/library/frequency_cuts/panel.py +0 -74
- spectre_core/plotting/library/integral_over_frequency/panel.py +0 -34
- spectre_core/plotting/library/spectrogram/panel.py +0 -92
- spectre_core/plotting/library/time_cuts/panel.py +0 -77
- spectre_core/plotting/panel_register.py +0 -13
- spectre_core/post_processing/base.py +0 -132
- spectre_core/post_processing/library/fixed/__init__.py +0 -0
- spectre_core/post_processing/library/fixed/event_handler.py +0 -40
- spectre_core/post_processing/library/sweep/event_handler.py +0 -54
- spectre_core/receivers/base.py +0 -422
- spectre_core/receivers/library/__init__.py +0 -7
- spectre_core/receivers/library/rsp1a/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/fixed.py +0 -104
- spectre_core/receivers/library/rsp1a/gr/sweep.py +0 -129
- spectre_core/receivers/library/rsp1a/receiver.py +0 -68
- spectre_core/receivers/library/rspduo/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +0 -114
- spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +0 -131
- spectre_core/receivers/library/rspduo/gr/tuner_2_fixed.py +0 -120
- spectre_core/receivers/library/rspduo/gr/tuner_2_sweep.py +0 -119
- spectre_core/receivers/library/rspduo/receiver.py +0 -97
- spectre_core/receivers/library/test/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/cosine_signal_1.py +0 -83
- spectre_core/receivers/library/test/gr/tagged_staircase.py +0 -93
- spectre_core/receivers/library/test/receiver.py +0 -203
- spectre_core/receivers/validators.py +0 -231
- spectre_core/web_fetch/callisto.py +0 -101
- spectre_core-0.0.9.dist-info/RECORD +0 -74
- /spectre_core/chunks/{chunk_register.py → _register.py} +0 -0
- /spectre_core/post_processing/{event_handler_register.py → _register.py} +0 -0
- /spectre_core/receivers/{receiver_register.py → _register.py} +0 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.11.dist-info}/LICENSE +0 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.11.dist-info}/WHEEL +0 -0
- {spectre_core-0.0.9.dist-info → spectre_core-0.0.11.dist-info}/top_level.txt +0 -0
@@ -2,17 +2,21 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
-
from typing import Callable
|
5
|
+
from typing import Callable
|
6
6
|
from dataclasses import dataclass
|
7
7
|
|
8
8
|
import numpy as np
|
9
9
|
|
10
|
-
from spectre_core.
|
11
|
-
from spectre_core.spectrograms.spectrogram import Spectrogram
|
12
|
-
from spectre_core.spectrograms.array_operations import is_close
|
10
|
+
from spectre_core.capture_configs import CaptureConfig, PNames
|
13
11
|
from spectre_core.exceptions import ModeNotFoundError
|
12
|
+
from ._spectrogram import Spectrogram
|
13
|
+
from ._array_operations import is_close
|
14
14
|
|
15
|
-
|
15
|
+
__all__ = [
|
16
|
+
"get_analytical_spectrogram",
|
17
|
+
"validate_analytically",
|
18
|
+
"TestResults"
|
19
|
+
]
|
16
20
|
|
17
21
|
@dataclass
|
18
22
|
class TestResults:
|
@@ -36,7 +40,7 @@ class TestResults:
|
|
36
40
|
return len(self.spectrum_validated) - self.num_validated_spectrums
|
37
41
|
|
38
42
|
|
39
|
-
def
|
43
|
+
def to_dict(self) -> dict[str, bool | dict[float, bool]]:
|
40
44
|
return {
|
41
45
|
"times_validated": self.times_validated,
|
42
46
|
"frequencies_validated": self.frequencies_validated,
|
@@ -72,14 +76,13 @@ class _AnalyticalFactory:
|
|
72
76
|
by parameters in the corresponding capture config and the number of spectrums
|
73
77
|
in the output spectrogram.
|
74
78
|
"""
|
75
|
-
receiver_name, test_mode = capture_config['receiver'], capture_config['mode']
|
76
79
|
|
77
|
-
if receiver_name != "test":
|
80
|
+
if capture_config.receiver_name != "test":
|
78
81
|
raise ValueError(f"Input capture config must correspond to the test receiver")
|
79
82
|
|
80
|
-
builder_method = self.builders.get(
|
83
|
+
builder_method = self.builders.get(capture_config.receiver_mode)
|
81
84
|
if builder_method is None:
|
82
|
-
raise ModeNotFoundError(f"Test mode not found. Expected one of {self.test_modes}, but received {
|
85
|
+
raise ModeNotFoundError(f"Test mode not found. Expected one of {self.test_modes}, but received {capture_config.receiver_mode}")
|
83
86
|
return builder_method(num_spectrums,
|
84
87
|
capture_config)
|
85
88
|
|
@@ -88,14 +91,14 @@ class _AnalyticalFactory:
|
|
88
91
|
num_spectrums: int,
|
89
92
|
capture_config: CaptureConfig) -> Spectrogram:
|
90
93
|
# Extract necessary parameters from the capture configuration.
|
91
|
-
window_size
|
92
|
-
|
93
|
-
amplitude
|
94
|
-
frequency
|
95
|
-
|
96
|
-
|
94
|
+
window_size = capture_config.get_parameter_value(PNames.WINDOW_SIZE)
|
95
|
+
sample_rate = capture_config.get_parameter_value(PNames.SAMPLE_RATE)
|
96
|
+
amplitude = capture_config.get_parameter_value(PNames.AMPLITUDE)
|
97
|
+
frequency = capture_config.get_parameter_value(PNames.FREQUENCY)
|
98
|
+
window_hop = capture_config.get_parameter_value(PNames.WINDOW_HOP)
|
99
|
+
center_frequency = capture_config.get_parameter_value(PNames.CENTER_FREQUENCY)
|
97
100
|
# Calculate derived parameters a (sampling rate ratio) and p (sampled periods).
|
98
|
-
a = int(
|
101
|
+
a = int(sample_rate / frequency)
|
99
102
|
p = int(window_size / a)
|
100
103
|
|
101
104
|
# Create the analytical spectrum, which is constant in time.
|
@@ -111,11 +114,11 @@ class _AnalyticalFactory:
|
|
111
114
|
analytical_dynamic_spectra = np.ones((window_size, num_spectrums)) * spectrum[:, np.newaxis]
|
112
115
|
|
113
116
|
# Compute time array.
|
114
|
-
sampling_interval = 1 /
|
115
|
-
times = np.arange(num_spectrums) *
|
117
|
+
sampling_interval = 1 / sample_rate
|
118
|
+
times = np.arange(num_spectrums) * window_hop * sampling_interval
|
116
119
|
|
117
120
|
# compute the frequency array.
|
118
|
-
frequencies = np.fft.fftshift(np.fft.fftfreq(window_size, sampling_interval))
|
121
|
+
frequencies = np.fft.fftshift(np.fft.fftfreq(window_size, sampling_interval)) + center_frequency
|
119
122
|
|
120
123
|
# Return the spectrogram.
|
121
124
|
return Spectrogram(analytical_dynamic_spectra,
|
@@ -129,11 +132,11 @@ class _AnalyticalFactory:
|
|
129
132
|
num_spectrums: int,
|
130
133
|
capture_config: CaptureConfig) -> Spectrogram:
|
131
134
|
# Extract necessary parameters from the capture configuration.
|
132
|
-
window_size
|
133
|
-
min_samples_per_step = capture_config
|
134
|
-
max_samples_per_step = capture_config
|
135
|
-
step_increment
|
136
|
-
samp_rate
|
135
|
+
window_size = capture_config.get_parameter_value(PNames.WINDOW_SIZE)
|
136
|
+
min_samples_per_step = capture_config.get_parameter_value(PNames.MIN_SAMPLES_PER_STEP)
|
137
|
+
max_samples_per_step = capture_config.get_parameter_value(PNames.MAX_SAMPLES_PER_STEP)
|
138
|
+
step_increment = capture_config.get_parameter_value(PNames.STEP_INCREMENT)
|
139
|
+
samp_rate = capture_config.get_parameter_value(PNames.SAMPLE_RATE)
|
137
140
|
|
138
141
|
# Calculate step sizes and derived parameters.
|
139
142
|
num_samples_per_step = np.arange(min_samples_per_step, max_samples_per_step + 1, step_increment)
|
@@ -152,11 +155,10 @@ class _AnalyticalFactory:
|
|
152
155
|
|
153
156
|
# Compute time array
|
154
157
|
num_samples_per_sweep = sum(num_samples_per_step)
|
155
|
-
midpoint_sample = sum(num_samples_per_step) // 2
|
156
158
|
sampling_interval = 1 / samp_rate
|
157
159
|
# compute the sample index we are "assigning" to each spectrum
|
158
160
|
# and multiply by the sampling interval to get the equivalent physical time
|
159
|
-
times = np.array([
|
161
|
+
times = np.array([(i * num_samples_per_sweep) for i in range(num_spectrums) ]) * sampling_interval
|
160
162
|
|
161
163
|
# Compute the frequency array
|
162
164
|
baseband_frequencies = np.fft.fftshift(np.fft.fftfreq(window_size, sampling_interval))
|
@@ -6,6 +6,53 @@ from datetime import datetime
|
|
6
6
|
|
7
7
|
import numpy as np
|
8
8
|
|
9
|
+
def average_array(array: np.ndarray, average_over: int, axis=0) -> np.ndarray:
|
10
|
+
|
11
|
+
# Check if average_over is an integer
|
12
|
+
if type(average_over) != int:
|
13
|
+
raise TypeError(f"average_over must be an integer. Got {type(average_over)}")
|
14
|
+
|
15
|
+
# Get the size of the specified axis which we will average over
|
16
|
+
axis_size = array.shape[axis]
|
17
|
+
# Check if average_over is within the valid range
|
18
|
+
if not 1 <= average_over <= axis_size:
|
19
|
+
raise ValueError(f"average_over must be between 1 and the length of the axis ({axis_size})")
|
20
|
+
|
21
|
+
max_axis_index = len(np.shape(array)) - 1
|
22
|
+
if axis > max_axis_index: # zero indexing on specifying axis, so minus one
|
23
|
+
raise ValueError(f"Requested axis is out of range of array dimensions. Axis: {axis}, max axis index: {max_axis_index}")
|
24
|
+
|
25
|
+
# find the number of elements in the requested axis
|
26
|
+
num_elements = array.shape[axis]
|
27
|
+
|
28
|
+
# find the number of "full blocks" to average over
|
29
|
+
num_full_blocks = num_elements // average_over
|
30
|
+
# if num_elements is not exactly divisible by average_over, we will have some elements left over
|
31
|
+
# these remaining elements will be padded with nans to become another full block
|
32
|
+
remainder = num_elements % average_over
|
33
|
+
|
34
|
+
# if there exists a remainder, pad the last block
|
35
|
+
if remainder != 0:
|
36
|
+
# initialise an array to hold the padding shape
|
37
|
+
padding_shape = [(0, 0)] * array.ndim
|
38
|
+
# pad after the last column in the requested axis
|
39
|
+
padding_shape[axis] = (0, average_over - remainder)
|
40
|
+
# pad with nan values (so to not contribute towards the mean computation)
|
41
|
+
array = np.pad(array, padding_shape, mode='constant', constant_values=np.nan)
|
42
|
+
|
43
|
+
# initalise a list to hold the new shape
|
44
|
+
new_shape = list(array.shape)
|
45
|
+
# update the shape on the requested access (to the number of blocks we will average over)
|
46
|
+
new_shape[axis] = num_full_blocks + (1 if remainder else 0)
|
47
|
+
# insert a new dimension, with the size of each block
|
48
|
+
new_shape.insert(axis + 1, average_over)
|
49
|
+
# and reshape the array to sort the array into the relevant blocks.
|
50
|
+
reshaped_array = array.reshape(new_shape)
|
51
|
+
# average over the newly created axis, essentially averaging over the blocks.
|
52
|
+
averaged_array = np.nanmean(reshaped_array, axis=axis + 1)
|
53
|
+
# return the averaged array
|
54
|
+
return averaged_array
|
55
|
+
|
9
56
|
|
10
57
|
def is_close(ar: np.ndarray,
|
11
58
|
ar_comparison: np.ndarray,
|
@@ -15,7 +62,6 @@ def is_close(ar: np.ndarray,
|
|
15
62
|
ar_comparison,
|
16
63
|
atol=absolute_tolerance))
|
17
64
|
|
18
|
-
|
19
65
|
def find_closest_index(
|
20
66
|
target_value: float | datetime,
|
21
67
|
array: np.ndarray,
|
@@ -2,18 +2,18 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
-
import
|
6
|
-
from typing import Optional, Any
|
5
|
+
from typing import Optional
|
7
6
|
from warnings import warn
|
8
7
|
from datetime import datetime, timedelta
|
9
8
|
from dataclasses import dataclass
|
9
|
+
import os
|
10
10
|
|
11
11
|
import numpy as np
|
12
12
|
from astropy.io import fits
|
13
13
|
|
14
|
-
from spectre_core.
|
15
|
-
from spectre_core.
|
16
|
-
from
|
14
|
+
from spectre_core.capture_configs import CaptureConfig, PNames
|
15
|
+
from spectre_core.config import get_chunks_dir_path, TimeFormats
|
16
|
+
from ._array_operations import (
|
17
17
|
find_closest_index,
|
18
18
|
normalise_peak_intensity,
|
19
19
|
compute_resolution,
|
@@ -21,6 +21,14 @@ from spectre_core.spectrograms.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
|
@dataclass
|
25
33
|
class FrequencyCut:
|
26
34
|
time: float | datetime
|
@@ -35,8 +43,21 @@ class TimeCut:
|
|
35
43
|
times: np.ndarray
|
36
44
|
cut: np.ndarray
|
37
45
|
spectrum_type: str
|
46
|
+
|
47
|
+
|
48
|
+
@dataclass(frozen=True)
|
49
|
+
class TimeTypes:
|
50
|
+
SECONDS : str = "seconds"
|
51
|
+
DATETIMES: str = "datetimes"
|
38
52
|
|
39
53
|
|
54
|
+
@dataclass(frozen=True)
|
55
|
+
class SpectrumTypes:
|
56
|
+
AMPLITUDE: str = "amplitude"
|
57
|
+
POWER : str = "power"
|
58
|
+
DIGITS : str = "digits"
|
59
|
+
|
60
|
+
|
40
61
|
class Spectrogram:
|
41
62
|
def __init__(self,
|
42
63
|
dynamic_spectra: np.ndarray, # holds the spectrogram data
|
@@ -54,6 +75,9 @@ class Spectrogram:
|
|
54
75
|
self._dynamic_spectra_as_dBb: Optional[np.ndarray] = None # cache
|
55
76
|
|
56
77
|
# assigned times and frequencies
|
78
|
+
if times[0] != 0:
|
79
|
+
raise ValueError(f"The first spectrum must correspond to t=0 [s]")
|
80
|
+
|
57
81
|
self._times = times
|
58
82
|
self._datetimes: Optional[list[datetime]] = None # cache
|
59
83
|
self._frequencies = frequencies
|
@@ -141,7 +165,7 @@ class Spectrogram:
|
|
141
165
|
@property
|
142
166
|
def chunk_start_datetime(self) -> datetime:
|
143
167
|
if self._chunk_start_datetime is None:
|
144
|
-
self._chunk_start_datetime = datetime.strptime(self.chunk_start_time,
|
168
|
+
self._chunk_start_datetime = datetime.strptime(self.chunk_start_time, TimeFormats.DATETIME)
|
145
169
|
return self._chunk_start_datetime
|
146
170
|
|
147
171
|
|
@@ -181,9 +205,9 @@ class Spectrogram:
|
|
181
205
|
# Suppress divide by zero and invalid value warnings for this block of code
|
182
206
|
with np.errstate(divide='ignore'):
|
183
207
|
# Depending on the spectrum type, compute the dBb values differently
|
184
|
-
if self._spectrum_type ==
|
208
|
+
if self._spectrum_type == SpectrumTypes.AMPLITUDE or self._spectrum_type == SpectrumTypes.DIGITS:
|
185
209
|
self._dynamic_spectra_as_dBb = 10 * np.log10(self._dynamic_spectra / background_spectra)
|
186
|
-
elif self._spectrum_type ==
|
210
|
+
elif self._spectrum_type == SpectrumTypes.POWER:
|
187
211
|
self._dynamic_spectra_as_dBb = 20 * np.log10(self._dynamic_spectra / background_spectra)
|
188
212
|
else:
|
189
213
|
raise NotImplementedError(f"{self.spectrum_type} unrecognised, uncertain decibel conversion!")
|
@@ -203,12 +227,12 @@ class Spectrogram:
|
|
203
227
|
|
204
228
|
|
205
229
|
def _update_background_indices_from_interval(self) -> None:
|
206
|
-
start_background = datetime.strptime(self._start_background,
|
230
|
+
start_background = datetime.strptime(self._start_background, TimeFormats.DATETIME)
|
207
231
|
self._start_background_index = find_closest_index(start_background,
|
208
232
|
self.datetimes,
|
209
233
|
enforce_strict_bounds=True)
|
210
234
|
|
211
|
-
end_background = datetime.strptime(self._end_background,
|
235
|
+
end_background = datetime.strptime(self._end_background, TimeFormats.DATETIME)
|
212
236
|
self._end_background_index = find_closest_index(end_background,
|
213
237
|
self.datetimes,
|
214
238
|
enforce_strict_bounds=True)
|
@@ -229,16 +253,13 @@ class Spectrogram:
|
|
229
253
|
|
230
254
|
|
231
255
|
def save(self) -> None:
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
chunk_start_datetime = self.chunk_start_datetime
|
236
|
-
chunk_parent_path = get_chunks_dir_path(year = chunk_start_datetime.year,
|
237
|
-
month = chunk_start_datetime.month,
|
238
|
-
day = chunk_start_datetime.day)
|
256
|
+
chunk_parent_path = get_chunks_dir_path(year = self.chunk_start_datetime.year,
|
257
|
+
month = self.chunk_start_datetime.month,
|
258
|
+
day = self.chunk_start_datetime.day)
|
239
259
|
file_name = f"{self.chunk_start_time}_{self._tag}.fits"
|
240
|
-
write_path = os.path.join(chunk_parent_path,
|
241
|
-
|
260
|
+
write_path = os.path.join(chunk_parent_path,
|
261
|
+
file_name)
|
262
|
+
_save_spectrogram(write_path, self)
|
242
263
|
|
243
264
|
|
244
265
|
def integrate_over_frequency(self,
|
@@ -266,7 +287,7 @@ class Spectrogram:
|
|
266
287
|
# exactly to one of the times assigned to each spectrogram. So, we compute the nearest achievable,
|
267
288
|
# and return it from the function as output too.
|
268
289
|
if isinstance(at_time, str):
|
269
|
-
at_time = datetime.strptime(at_time,
|
290
|
+
at_time = datetime.strptime(at_time, TimeFormats.DATETIME)
|
270
291
|
index_of_cut = find_closest_index(at_time,
|
271
292
|
self.datetimes,
|
272
293
|
enforce_strict_bounds = True)
|
@@ -306,7 +327,7 @@ class Spectrogram:
|
|
306
327
|
dBb: bool = False,
|
307
328
|
peak_normalise = False,
|
308
329
|
correct_background = False,
|
309
|
-
return_time_type: str =
|
330
|
+
return_time_type: str = TimeTypes.SECONDS) -> TimeCut:
|
310
331
|
|
311
332
|
# it is important to note that the "at frequency" specified by the user likely does not correspond
|
312
333
|
# exactly to one of the physical frequencies assigned to each spectral component. So, we compute the nearest achievable,
|
@@ -316,9 +337,9 @@ class Spectrogram:
|
|
316
337
|
enforce_strict_bounds = True)
|
317
338
|
frequency_of_cut = self.frequencies[index_of_cut]
|
318
339
|
|
319
|
-
if return_time_type ==
|
340
|
+
if return_time_type == TimeTypes.DATETIMES:
|
320
341
|
times = self.datetimes
|
321
|
-
elif return_time_type ==
|
342
|
+
elif return_time_type == TimeTypes.SECONDS:
|
322
343
|
times = self.times
|
323
344
|
else:
|
324
345
|
raise ValueError(f"Invalid return_time_type. Got {return_time_type}, expected one of 'datetimes' or 'seconds'")
|
@@ -359,10 +380,16 @@ def _seconds_of_day(dt: datetime) -> float:
|
|
359
380
|
|
360
381
|
# Function to create a FITS file with the specified structure
|
361
382
|
def _save_spectrogram(write_path: str,
|
362
|
-
spectrogram: Spectrogram
|
363
|
-
|
364
|
-
|
365
|
-
|
383
|
+
spectrogram: Spectrogram) -> None:
|
384
|
+
|
385
|
+
capture_config = CaptureConfig(spectrogram.tag)
|
386
|
+
ORIGIN = capture_config.get_parameter_value(PNames.ORIGIN)
|
387
|
+
INSTRUME = capture_config.get_parameter_value(PNames.INSTRUMENT)
|
388
|
+
TELESCOP = capture_config.get_parameter_value(PNames.TELESCOPE)
|
389
|
+
OBJECT = capture_config.get_parameter_value(PNames.OBJECT)
|
390
|
+
OBS_ALT = capture_config.get_parameter_value(PNames.OBS_ALT)
|
391
|
+
OBS_LAT = capture_config.get_parameter_value(PNames.OBS_LAT)
|
392
|
+
OBS_LON = capture_config.get_parameter_value(PNames.OBS_LON)
|
366
393
|
|
367
394
|
# Primary HDU with data
|
368
395
|
primary_data = spectrogram.dynamic_spectra.astype(dtype=np.float32)
|
@@ -397,10 +424,10 @@ def _save_spectrogram(write_path: str,
|
|
397
424
|
|
398
425
|
primary_hdu.header.set('DATE', start_date, 'time of observation')
|
399
426
|
primary_hdu.header.set('CONTENT', f'{start_date} dynamic spectrogram', 'title of image')
|
400
|
-
primary_hdu.header.set('ORIGIN', f'{
|
401
|
-
primary_hdu.header.set('TELESCOP', f'{
|
402
|
-
primary_hdu.header.set('INSTRUME', f'{
|
403
|
-
primary_hdu.header.set('OBJECT', f'{
|
427
|
+
primary_hdu.header.set('ORIGIN', f'{ORIGIN}')
|
428
|
+
primary_hdu.header.set('TELESCOP', f'{TELESCOP}', 'type of instrument')
|
429
|
+
primary_hdu.header.set('INSTRUME', f'{INSTRUME}')
|
430
|
+
primary_hdu.header.set('OBJECT', f'{OBJECT}', 'object description')
|
404
431
|
|
405
432
|
primary_hdu.header.set('DATE-OBS', f'{start_date}', 'date observation starts')
|
406
433
|
primary_hdu.header.set('TIME-OBS', f'{start_time}', 'time observation starts')
|
@@ -424,17 +451,17 @@ def _save_spectrogram(write_path: str,
|
|
424
451
|
primary_hdu.header.set('CTYPE2', 'Frequency [Hz]', 'title of axis 2')
|
425
452
|
primary_hdu.header.set('CDELT2', spectrogram.frequency_resolution, 'step between first and second element in axis')
|
426
453
|
|
427
|
-
primary_hdu.header.set('OBS_LAT', f'{
|
454
|
+
primary_hdu.header.set('OBS_LAT', f'{OBS_LAT}', 'observatory latitude in degree')
|
428
455
|
primary_hdu.header.set('OBS_LAC', 'N', 'observatory latitude code {N,S}')
|
429
|
-
primary_hdu.header.set('OBS_LON', f'{
|
456
|
+
primary_hdu.header.set('OBS_LON', f'{OBS_LON}', 'observatory longitude in degree')
|
430
457
|
primary_hdu.header.set('OBS_LOC', 'W', 'observatory longitude code {E,W}')
|
431
|
-
primary_hdu.header.set('OBS_ALT', f'{
|
458
|
+
primary_hdu.header.set('OBS_ALT', f'{OBS_ALT}', 'observatory altitude in meter asl')
|
432
459
|
|
433
460
|
|
434
461
|
# Wrap arrays in an additional dimension to mimic the e-CALLISTO storage
|
435
462
|
times_wrapped = np.array([spectrogram.times.astype(np.float32)])
|
436
463
|
# To mimic e-Callisto storage, convert frequencies to MHz
|
437
|
-
frequencies_MHz = spectrogram.frequencies *1e-6
|
464
|
+
frequencies_MHz = spectrogram.frequencies * 1e-6
|
438
465
|
frequencies_wrapped = np.array([frequencies_MHz.astype(np.float32)])
|
439
466
|
|
440
467
|
# Binary Table HDU (extension)
|
@@ -5,59 +5,19 @@
|
|
5
5
|
import numpy as np
|
6
6
|
from datetime import datetime, timedelta
|
7
7
|
from typing import Optional
|
8
|
+
from math import floor
|
8
9
|
|
9
|
-
from spectre_core.
|
10
|
-
from
|
11
|
-
from
|
12
|
-
|
13
|
-
|
14
|
-
def _average_array(array: np.ndarray, average_over: int, axis=0) -> np.ndarray:
|
15
|
-
|
16
|
-
# Check if average_over is an integer
|
17
|
-
if type(average_over) != int:
|
18
|
-
raise TypeError(f"average_over must be an integer. Got {type(average_over)}")
|
19
|
-
|
20
|
-
# Get the size of the specified axis which we will average over
|
21
|
-
axis_size = array.shape[axis]
|
22
|
-
# Check if average_over is within the valid range
|
23
|
-
if not 1 <= average_over <= axis_size:
|
24
|
-
raise ValueError(f"average_over must be between 1 and the length of the axis ({axis_size})")
|
25
|
-
|
26
|
-
max_axis_index = len(np.shape(array)) - 1
|
27
|
-
if axis > max_axis_index: # zero indexing on specifying axis, so minus one
|
28
|
-
raise ValueError(f"Requested axis is out of range of array dimensions. Axis: {axis}, max axis index: {max_axis_index}")
|
29
|
-
|
30
|
-
# find the number of elements in the requested axis
|
31
|
-
num_elements = array.shape[axis]
|
32
|
-
|
33
|
-
# find the number of "full blocks" to average over
|
34
|
-
num_full_blocks = num_elements // average_over
|
35
|
-
# if num_elements is not exactly divisible by average_over, we will have some elements left over
|
36
|
-
# these remaining elements will be padded with nans to become another full block
|
37
|
-
remainder = num_elements % average_over
|
38
|
-
|
39
|
-
# if there exists a remainder, pad the last block
|
40
|
-
if remainder != 0:
|
41
|
-
# initialise an array to hold the padding shape
|
42
|
-
padding_shape = [(0, 0)] * array.ndim
|
43
|
-
# pad after the last column in the requested axis
|
44
|
-
padding_shape[axis] = (0, average_over - remainder)
|
45
|
-
# pad with nan values (so to not contribute towards the mean computation)
|
46
|
-
array = np.pad(array, padding_shape, mode='constant', constant_values=np.nan)
|
47
|
-
|
48
|
-
# initalise a list to hold the new shape
|
49
|
-
new_shape = list(array.shape)
|
50
|
-
# update the shape on the requested access (to the number of blocks we will average over)
|
51
|
-
new_shape[axis] = num_full_blocks + (1 if remainder else 0)
|
52
|
-
# insert a new dimension, with the size of each block
|
53
|
-
new_shape.insert(axis + 1, average_over)
|
54
|
-
# and reshape the array to sort the array into the relevant blocks.
|
55
|
-
reshaped_array = array.reshape(new_shape)
|
56
|
-
# average over the newly created axis, essentially averaging over the blocks.
|
57
|
-
averaged_array = np.nanmean(reshaped_array, axis=axis + 1)
|
58
|
-
# return the averaged array
|
59
|
-
return averaged_array
|
10
|
+
from spectre_core.config import TimeFormats
|
11
|
+
from ._array_operations import find_closest_index, average_array
|
12
|
+
from ._spectrogram import Spectrogram
|
60
13
|
|
14
|
+
__all__ = [
|
15
|
+
"frequency_chop",
|
16
|
+
"time_chop",
|
17
|
+
"frequency_average",
|
18
|
+
"time_average",
|
19
|
+
"join_spectrograms"
|
20
|
+
]
|
61
21
|
|
62
22
|
def frequency_chop(input_spectrogram: Spectrogram,
|
63
23
|
start_frequency: float | int,
|
@@ -98,7 +58,7 @@ def frequency_chop(input_spectrogram: Spectrogram,
|
|
98
58
|
def time_chop(input_spectrogram: Spectrogram,
|
99
59
|
start_time: str,
|
100
60
|
end_time: str,
|
101
|
-
time_format: str =
|
61
|
+
time_format: str = TimeFormats.DATETIME) -> Optional[Spectrogram]:
|
102
62
|
|
103
63
|
# parse the strings as datetimes
|
104
64
|
start_datetime = datetime.strptime(start_time, time_format)
|
@@ -122,18 +82,17 @@ def time_chop(input_spectrogram: Spectrogram,
|
|
122
82
|
# chop the spectrogram
|
123
83
|
transformed_dynamic_spectra = input_spectrogram.dynamic_spectra[:, start_index:end_index+1]
|
124
84
|
|
125
|
-
# chop the times array
|
126
|
-
transformed_times = input_spectrogram.times[start_index:end_index+1]
|
127
|
-
#translate the chopped times array to start at zero
|
128
|
-
transformed_times -= transformed_times[0]
|
129
|
-
|
130
85
|
# compute the new start datetime following the time chop
|
131
86
|
transformed_start_datetime = input_spectrogram.datetimes[start_index]
|
132
|
-
#
|
133
|
-
transformed_chunk_start_time = datetime.strftime(transformed_start_datetime,
|
134
|
-
# and compute the microsecond correction
|
87
|
+
# compute the microsecond correction, and chunk start time
|
88
|
+
transformed_chunk_start_time = datetime.strftime(transformed_start_datetime, TimeFormats.DATETIME)
|
135
89
|
transformed_microsecond_correction = transformed_start_datetime.microsecond
|
136
90
|
|
91
|
+
# chop the times array
|
92
|
+
transformed_times = input_spectrogram.times[start_index:end_index+1]
|
93
|
+
# assign the first spectrum to t=0 [s]
|
94
|
+
transformed_times -= transformed_times[0]
|
95
|
+
|
137
96
|
return Spectrogram(transformed_dynamic_spectra,
|
138
97
|
transformed_times,
|
139
98
|
input_spectrogram.frequencies,
|
@@ -144,33 +103,44 @@ def time_chop(input_spectrogram: Spectrogram,
|
|
144
103
|
|
145
104
|
|
146
105
|
def time_average(input_spectrogram: Spectrogram,
|
147
|
-
|
148
|
-
|
106
|
+
resolution: Optional[float] = None,
|
107
|
+
average_over: Optional[int] = None) -> Spectrogram:
|
108
|
+
|
149
109
|
# spectre does not currently support averaging of non-datetime assigned spectrograms
|
150
110
|
if input_spectrogram.chunk_start_time is None:
|
151
111
|
raise ValueError(f"Input spectrogram is missing chunk start time. Averaging is not yet supported for non-datetime assigned spectrograms")
|
152
112
|
|
153
|
-
# if
|
113
|
+
# if nothing is specified, do nothing
|
114
|
+
if (resolution is None) and (average_over is None):
|
115
|
+
average_over = 1
|
116
|
+
|
117
|
+
if not (resolution is not None) ^ (average_over is not None):
|
118
|
+
raise ValueError(f"Exactly one of 'resolution' or 'average_over' "
|
119
|
+
f"must be specified.")
|
120
|
+
|
121
|
+
# if the resolution is specified, compute the appropriate number of spectrums to average over
|
122
|
+
# and recall the same function
|
123
|
+
if resolution is not None:
|
124
|
+
average_over = max(1, floor(resolution / input_spectrogram.time_resolution))
|
125
|
+
return time_average(input_spectrogram, average_over=average_over)
|
126
|
+
|
127
|
+
# No averaging is required, if we have to average over every one spectrum
|
154
128
|
if average_over == 1:
|
155
129
|
return input_spectrogram
|
156
130
|
|
157
131
|
# average the dynamic spectra array
|
158
|
-
transformed_dynamic_spectra =
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
# We need to assign timestamps to the averaged spectrums in the spectrograms.
|
163
|
-
#
|
164
|
-
#
|
165
|
-
|
166
|
-
|
167
|
-
#
|
168
|
-
|
169
|
-
|
170
|
-
# compute the updated chunk start time and the updated microsecond correction based on averaged_t0
|
171
|
-
corrected_start_datetime = input_spectrogram.datetimes[0] + timedelta(seconds = averaged_t0)
|
172
|
-
# then, compute the transformed chunk start time and microsecond correction
|
173
|
-
transformed_chunk_start_time = corrected_start_datetime.strftime(DEFAULT_DATETIME_FORMAT)
|
132
|
+
transformed_dynamic_spectra = average_array(input_spectrogram.dynamic_spectra,
|
133
|
+
average_over,
|
134
|
+
axis=1)
|
135
|
+
|
136
|
+
# We need to assign timestamps to the averaged spectrums in the spectrograms.
|
137
|
+
# The natural way to do this is to assign the i'th averaged spectrogram
|
138
|
+
# to the i'th averaged time
|
139
|
+
transformed_times = average_array(input_spectrogram.times, average_over)
|
140
|
+
|
141
|
+
# find the new chunk start time, which we will assign to the first spectrum after averaging
|
142
|
+
corrected_start_datetime = input_spectrogram.datetimes[0] + timedelta(seconds = float(transformed_times[0]))
|
143
|
+
transformed_chunk_start_time = corrected_start_datetime.strftime(TimeFormats.DATETIME)
|
174
144
|
transformed_microsecond_correction = corrected_start_datetime.microsecond
|
175
145
|
|
176
146
|
# finally, translate the averaged time seconds to begin at t=0 [s]
|
@@ -183,18 +153,37 @@ def time_average(input_spectrogram: Spectrogram,
|
|
183
153
|
microsecond_correction = transformed_microsecond_correction,
|
184
154
|
spectrum_type = input_spectrogram.spectrum_type)
|
185
155
|
|
156
|
+
|
157
|
+
|
186
158
|
def frequency_average(input_spectrogram: Spectrogram,
|
187
|
-
|
159
|
+
resolution: Optional[float] = None,
|
160
|
+
average_over: Optional[int] = None) -> Spectrogram:
|
161
|
+
|
162
|
+
# if nothing is specified, do nothing
|
163
|
+
if (resolution is None) and (average_over is None):
|
164
|
+
average_over = 1
|
165
|
+
|
166
|
+
if not (resolution is not None) ^ (average_over is not None):
|
167
|
+
raise ValueError(f"Exactly one of 'resolution' or 'average_over' "
|
168
|
+
f"must be specified.")
|
169
|
+
|
170
|
+
# if the resolution is specified, compute the appropriate number of spectrums to average over
|
171
|
+
# and recall the same function
|
172
|
+
if resolution is not None:
|
173
|
+
average_over = max(1, floor(resolution / input_spectrogram.frequency_resolution))
|
174
|
+
return frequency_average(input_spectrogram, average_over=average_over)
|
188
175
|
|
189
|
-
#
|
176
|
+
# No averaging is required, if we have to average over every one spectrum
|
190
177
|
if average_over == 1:
|
191
178
|
return input_spectrogram
|
192
|
-
|
179
|
+
|
193
180
|
# We need to assign physical frequencies to the averaged spectrums in the spectrograms.
|
194
181
|
# is to assign the i'th averaged spectral component to the i'th averaged frequency.
|
195
182
|
# average the dynamic spectra array
|
196
|
-
transformed_dynamic_spectra =
|
197
|
-
|
183
|
+
transformed_dynamic_spectra = average_array(input_spectrogram.dynamic_spectra,
|
184
|
+
average_over,
|
185
|
+
axis=0)
|
186
|
+
transformed_frequencies = average_array(input_spectrogram.frequencies, average_over)
|
198
187
|
|
199
188
|
return Spectrogram(transformed_dynamic_spectra,
|
200
189
|
input_spectrogram.times,
|
@@ -213,6 +202,7 @@ def _time_elapsed(datetimes: np.ndarray) -> np.ndarray:
|
|
213
202
|
# Convert the list of seconds to a NumPy array of type float32
|
214
203
|
return np.array(elapsed_time, dtype=np.float32)
|
215
204
|
|
205
|
+
|
216
206
|
# we assume that the spectrogram list is ORDERED chronologically
|
217
207
|
# we assume there is no time overlap in any of the spectrograms in the list
|
218
208
|
def join_spectrograms(spectrograms: list[Spectrogram]) -> Spectrogram:
|
@@ -254,14 +244,11 @@ def join_spectrograms(spectrograms: list[Spectrogram]) -> Spectrogram:
|
|
254
244
|
start_index = end_index
|
255
245
|
|
256
246
|
transformed_times = _time_elapsed(conc_datetimes)
|
257
|
-
|
258
|
-
transformed_microsecond_correction = conc_datetimes[0].microsecond
|
259
247
|
|
260
248
|
return Spectrogram(transformed_dynamic_spectra,
|
261
249
|
transformed_times,
|
262
250
|
reference_spectrogram.frequencies,
|
263
251
|
reference_spectrogram.tag,
|
264
252
|
chunk_start_time = reference_spectrogram.chunk_start_time,
|
265
|
-
microsecond_correction =
|
266
|
-
spectrum_type = reference_spectrogram.spectrum_type)
|
267
|
-
|
253
|
+
microsecond_correction = reference_spectrogram.microsecond_correction,
|
254
|
+
spectrum_type = reference_spectrogram.spectrum_type)
|
@@ -2,9 +2,8 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
-
from
|
6
|
-
|
7
|
-
import_target_modules(__file__, __name__, "event_handler")
|
8
|
-
|
9
|
-
|
5
|
+
from ._callisto import download_callisto_data, CALLISTO_INSTRUMENT_CODES
|
10
6
|
|
7
|
+
__all__ = [
|
8
|
+
"download_callisto_data", "CALLISTO_INSTRUMENT_CODES"
|
9
|
+
]
|