spectre-core 0.0.9__py3-none-any.whl → 0.0.10__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 (106) hide show
  1. spectre_core/__init__.py +0 -3
  2. spectre_core/_file_io/__init__.py +15 -0
  3. spectre_core/_file_io/file_handlers.py +128 -0
  4. spectre_core/capture_configs/__init__.py +29 -0
  5. spectre_core/capture_configs/_capture_config.py +85 -0
  6. spectre_core/capture_configs/_capture_templates.py +222 -0
  7. spectre_core/capture_configs/_parameters.py +110 -0
  8. spectre_core/capture_configs/_pconstraints.py +82 -0
  9. spectre_core/capture_configs/_ptemplates.py +450 -0
  10. spectre_core/capture_configs/_pvalidators.py +173 -0
  11. spectre_core/chunks/__init__.py +17 -201
  12. spectre_core/chunks/{base.py → _base.py} +15 -60
  13. spectre_core/chunks/_chunks.py +200 -0
  14. spectre_core/chunks/{factory.py → _factory.py} +6 -7
  15. spectre_core/chunks/library/{callisto/chunk.py → _callisto.py} +4 -7
  16. spectre_core/chunks/library/{fixed/chunk.py → _fixed_center_frequency.py} +7 -64
  17. spectre_core/chunks/library/_swept_center_frequency.py +103 -0
  18. spectre_core/config/__init__.py +20 -0
  19. spectre_core/config/_paths.py +77 -0
  20. spectre_core/config/_time_formats.py +15 -0
  21. spectre_core/exceptions.py +4 -5
  22. spectre_core/logging/__init__.py +11 -0
  23. spectre_core/logging/_configure.py +35 -0
  24. spectre_core/logging/_decorators.py +19 -0
  25. spectre_core/{logging.py → logging/_log_handlers.py} +13 -58
  26. spectre_core/plotting/__init__.py +7 -1
  27. spectre_core/plotting/{base.py → _base.py} +40 -20
  28. spectre_core/plotting/_format.py +18 -0
  29. spectre_core/plotting/{panel_stack.py → _panel_stack.py} +48 -48
  30. spectre_core/plotting/_panels.py +234 -0
  31. spectre_core/post_processing/__init__.py +10 -2
  32. spectre_core/post_processing/_base.py +119 -0
  33. spectre_core/post_processing/{factory.py → _factory.py} +7 -6
  34. spectre_core/post_processing/{post_processor.py → _post_processor.py} +3 -3
  35. spectre_core/post_processing/library/_fixed_center_frequency.py +115 -0
  36. spectre_core/post_processing/library/_swept_center_frequency.py +382 -0
  37. spectre_core/receivers/__init__.py +12 -2
  38. spectre_core/receivers/_base.py +352 -0
  39. spectre_core/receivers/{factory.py → _factory.py} +2 -2
  40. spectre_core/receivers/_spec_names.py +20 -0
  41. spectre_core/receivers/gr/__init__.py +3 -0
  42. spectre_core/receivers/gr/_base.py +33 -0
  43. spectre_core/receivers/gr/_rsp1a.py +158 -0
  44. spectre_core/receivers/gr/_test.py +123 -0
  45. spectre_core/receivers/library/_rsp1a.py +61 -0
  46. spectre_core/receivers/library/_test.py +221 -0
  47. spectre_core/spectrograms/__init__.py +18 -0
  48. spectre_core/spectrograms/{analytical.py → _analytical.py} +29 -27
  49. spectre_core/spectrograms/{array_operations.py → _array_operations.py} +47 -1
  50. spectre_core/spectrograms/{spectrogram.py → _spectrogram.py} +62 -35
  51. spectre_core/spectrograms/{transform.py → _transform.py} +76 -89
  52. spectre_core/{post_processing/library → wgetting}/__init__.py +4 -5
  53. spectre_core/wgetting/_callisto.py +155 -0
  54. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/METADATA +1 -1
  55. spectre_core-0.0.10.dist-info/RECORD +63 -0
  56. spectre_core/cfg.py +0 -116
  57. spectre_core/chunks/library/__init__.py +0 -8
  58. spectre_core/chunks/library/sweep/__init__.py +0 -0
  59. spectre_core/chunks/library/sweep/chunk.py +0 -400
  60. spectre_core/dynamic_imports.py +0 -22
  61. spectre_core/file_handlers/base.py +0 -68
  62. spectre_core/file_handlers/configs.py +0 -271
  63. spectre_core/file_handlers/json.py +0 -40
  64. spectre_core/file_handlers/text.py +0 -21
  65. spectre_core/plotting/factory.py +0 -26
  66. spectre_core/plotting/format.py +0 -19
  67. spectre_core/plotting/library/__init__.py +0 -7
  68. spectre_core/plotting/library/frequency_cuts/panel.py +0 -74
  69. spectre_core/plotting/library/integral_over_frequency/panel.py +0 -34
  70. spectre_core/plotting/library/spectrogram/panel.py +0 -92
  71. spectre_core/plotting/library/time_cuts/panel.py +0 -77
  72. spectre_core/plotting/panel_register.py +0 -13
  73. spectre_core/post_processing/base.py +0 -132
  74. spectre_core/post_processing/library/fixed/__init__.py +0 -0
  75. spectre_core/post_processing/library/fixed/event_handler.py +0 -40
  76. spectre_core/post_processing/library/sweep/event_handler.py +0 -54
  77. spectre_core/receivers/base.py +0 -422
  78. spectre_core/receivers/library/__init__.py +0 -7
  79. spectre_core/receivers/library/rsp1a/__init__.py +0 -0
  80. spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
  81. spectre_core/receivers/library/rsp1a/gr/fixed.py +0 -104
  82. spectre_core/receivers/library/rsp1a/gr/sweep.py +0 -129
  83. spectre_core/receivers/library/rsp1a/receiver.py +0 -68
  84. spectre_core/receivers/library/rspduo/__init__.py +0 -0
  85. spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
  86. spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +0 -114
  87. spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +0 -131
  88. spectre_core/receivers/library/rspduo/gr/tuner_2_fixed.py +0 -120
  89. spectre_core/receivers/library/rspduo/gr/tuner_2_sweep.py +0 -119
  90. spectre_core/receivers/library/rspduo/receiver.py +0 -97
  91. spectre_core/receivers/library/test/__init__.py +0 -0
  92. spectre_core/receivers/library/test/gr/__init__.py +0 -0
  93. spectre_core/receivers/library/test/gr/cosine_signal_1.py +0 -83
  94. spectre_core/receivers/library/test/gr/tagged_staircase.py +0 -93
  95. spectre_core/receivers/library/test/receiver.py +0 -203
  96. spectre_core/receivers/validators.py +0 -231
  97. spectre_core/web_fetch/callisto.py +0 -101
  98. spectre_core-0.0.9.dist-info/RECORD +0 -74
  99. /spectre_core/chunks/{chunk_register.py → _register.py} +0 -0
  100. /spectre_core/post_processing/{event_handler_register.py → _register.py} +0 -0
  101. /spectre_core/receivers/{receiver_register.py → _register.py} +0 -0
  102. /spectre_core/{chunks/library/callisto/__init__.py → receivers/gr/_rspduo.py} +0 -0
  103. /spectre_core/{chunks/library/fixed/__init__.py → receivers/library/_rspduo.py} +0 -0
  104. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/LICENSE +0 -0
  105. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/WHEEL +0 -0
  106. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,115 @@
1
+ # SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
2
+ # This file is part of SPECTRE
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from logging import getLogger
6
+ _LOGGER = getLogger(__name__)
7
+
8
+ import numpy as np
9
+ from typing import Tuple
10
+
11
+ import os
12
+
13
+ from spectre_core.capture_configs import CaptureConfig, PNames, CaptureModes
14
+ from spectre_core.chunks import BaseChunk
15
+ from spectre_core.spectrograms import Spectrogram, time_average, frequency_average
16
+ from .._base import BaseEventHandler, make_sft_instance
17
+ from .._register import register_event_handler
18
+
19
+
20
+ def _do_stfft(iq_data: np.array,
21
+ capture_config: CaptureConfig,
22
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
23
+ """For reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.ShortTimeFFT.html"""
24
+
25
+ sft = make_sft_instance(capture_config)
26
+
27
+ # set p0=0, since by convention in the STFFT docs, p=0 corresponds to the slice centred at t=0
28
+ p0=0
29
+
30
+ # set p1 to the index of the first slice where the "midpoint" of the window is still inside the signal
31
+ num_samples = len(iq_data)
32
+ p1 = sft.upper_border_begin(num_samples)[1]
33
+
34
+ # compute a ShortTimeFFT on the IQ samples
35
+ complex_spectra = sft.stft(iq_data,
36
+ p0 = p0,
37
+ p1 = p1)
38
+
39
+ # compute the magnitude of each spectral component
40
+ dynamic_spectra = np.abs(complex_spectra)
41
+
42
+
43
+ # assign a physical time to each spectrum
44
+ # p0 is defined to correspond with the first sample, at t=0 [s]
45
+ times = sft.t(num_samples,
46
+ p0 = p0,
47
+ p1 = p1)
48
+ # assign physical frequencies to each spectral component
49
+ frequencies = sft.f + capture_config.get_parameter_value(PNames.CENTER_FREQUENCY)
50
+
51
+ return times, frequencies, dynamic_spectra
52
+
53
+
54
+ def _build_spectrogram(chunk: BaseChunk,
55
+ capture_config: CaptureConfig) -> Spectrogram:
56
+ """Create a spectrogram by performing a Short Time FFT on the IQ samples for this chunk."""
57
+
58
+ # read the data from the chunk
59
+ millisecond_correction = chunk.read_file("hdr")
60
+ iq_data = chunk.read_file("bin")
61
+
62
+ # units conversion
63
+ microsecond_correction = millisecond_correction * 1e3
64
+
65
+ times, frequencies, dynamic_spectra = _do_stfft(iq_data,
66
+ capture_config)
67
+
68
+ # explicitly type cast data arrays to 32-bit floats
69
+ times = np.array(times, dtype = 'float32')
70
+ frequencies = np.array(frequencies, dtype = 'float32')
71
+ dynamic_spectra = np.array(dynamic_spectra, dtype = 'float32')
72
+
73
+ return Spectrogram(dynamic_spectra,
74
+ times,
75
+ frequencies,
76
+ chunk.tag,
77
+ chunk_start_time = chunk.chunk_start_time,
78
+ microsecond_correction = microsecond_correction,
79
+ spectrum_type = "amplitude")
80
+
81
+
82
+ @register_event_handler(CaptureModes.FIXED_CENTER_FREQUENCY)
83
+ class _EventHandler(BaseEventHandler):
84
+ def __init__(self, *args, **kwargs):
85
+ super().__init__(*args, **kwargs)
86
+
87
+ def process(self,
88
+ absolute_file_path: str):
89
+ _LOGGER.info(f"Processing: {absolute_file_path}")
90
+ file_name = os.path.basename(absolute_file_path)
91
+ base_file_name, _ = os.path.splitext(file_name)
92
+ chunk_start_time, tag = base_file_name.split('_')
93
+
94
+ # create an instance of the current chunk being processed
95
+ chunk = self._Chunk(chunk_start_time, tag)
96
+
97
+ _LOGGER.info("Creating spectrogram")
98
+ spectrogram = _build_spectrogram(chunk,
99
+ self._capture_config)
100
+
101
+ spectrogram = time_average(spectrogram,
102
+ resolution = self._capture_config.get_parameter_value(PNames.TIME_RESOLUTION))
103
+
104
+ spectrogram = frequency_average(spectrogram,
105
+ resolution = self._capture_config.get_parameter_value(PNames.FREQUENCY_RESOLUTION))
106
+
107
+ self._cache_spectrogram(spectrogram)
108
+
109
+ bin_chunk = chunk.get_file('bin')
110
+ _LOGGER.info(f"Deleting {bin_chunk.file_path}")
111
+ bin_chunk.delete()
112
+
113
+ hdr_chunk = chunk.get_file('hdr')
114
+ _LOGGER.info(f"Deleting {hdr_chunk.file_path}")
115
+ hdr_chunk.delete()
@@ -0,0 +1,382 @@
1
+ # SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
2
+ # This file is part of SPECTRE
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from logging import getLogger
6
+ _LOGGER = getLogger(__name__)
7
+
8
+ import os
9
+ from typing import Optional
10
+ from typing import Tuple
11
+ from datetime import timedelta, datetime
12
+ import numpy as np
13
+
14
+ from scipy.signal import ShortTimeFFT
15
+
16
+ from spectre_core.spectrograms import Spectrogram, time_average, frequency_average
17
+ from spectre_core.config import TimeFormats
18
+ from spectre_core.capture_configs import CaptureConfig, PNames, CaptureModes
19
+ from spectre_core.chunks import BaseChunk
20
+ from spectre_core.chunks import SweepMetadata
21
+ from spectre_core.exceptions import InvalidSweepMetadataError
22
+ from .._base import BaseEventHandler, make_sft_instance
23
+ from .._register import register_event_handler
24
+
25
+
26
+ def _stitch_steps(stepped_dynamic_spectra: np.ndarray,
27
+ num_full_sweeps: int) -> np.ndarray:
28
+ """For each full sweep, create a swept spectrum by stitching together the spectrum at each of the steps."""
29
+ return stepped_dynamic_spectra.reshape((num_full_sweeps, -1)).T
30
+
31
+
32
+ def _average_over_steps(stepped_dynamic_spectra: np.ndarray) -> None:
33
+ """Average the spectrums in each step totally in time."""
34
+ return np.nanmean(stepped_dynamic_spectra[..., 1:], axis=-1)
35
+
36
+
37
+ def _fill_times(times: np.ndarray,
38
+ num_samples: np.ndarray,
39
+ sample_rate: int,
40
+ num_full_sweeps: int,
41
+ num_steps_per_sweep: int) -> None:
42
+ """Assign physical times to each swept spectrum. We use (by convention) the time of the first sample in eahc sweep"""
43
+
44
+ sampling_interval = 1 / sample_rate
45
+ cumulative_samples = 0
46
+ for sweep_index in range(num_full_sweeps):
47
+ # assign a physical time to the spectrum for this sweep
48
+ times[sweep_index] = cumulative_samples * sampling_interval
49
+
50
+ # find the total number of samples across the sweep
51
+ start_step = sweep_index * num_steps_per_sweep
52
+ end_step = (sweep_index + 1) * num_steps_per_sweep
53
+
54
+ # update cumulative samples
55
+ cumulative_samples += np.sum(num_samples[start_step:end_step])
56
+
57
+
58
+ def _fill_frequencies(frequencies: np.ndarray,
59
+ center_frequencies: np.ndarray,
60
+ baseband_frequencies: np.ndarray,
61
+ window_size: int) -> None:
62
+ """Assign physical frequencies to each of the swept spectral components."""
63
+ for i, center_frequency in enumerate(np.unique(center_frequencies)):
64
+ lower_bound = i * window_size
65
+ upper_bound = (i + 1) * window_size
66
+ frequencies[lower_bound:upper_bound] = (baseband_frequencies + center_frequency)
67
+
68
+
69
+ def _fill_stepped_dynamic_spectra(stepped_dynamic_spectra: np.ndarray,
70
+ sft: ShortTimeFFT,
71
+ iq_data: np.ndarray,
72
+ num_samples: np.ndarray,
73
+ num_full_sweeps: int,
74
+ num_steps_per_sweep: int) -> None:
75
+ """Compute the dynamic spectra for the input IQ samples for each step.
76
+
77
+ All IQ samples per step were collected at the same center frequency.
78
+ """
79
+ # global_step_index will hold the step index over all sweeps (doesn't reset each sweep)
80
+ # start_sample_index will hold the index of the first sample in the step
81
+ global_step_index, start_sample_index = 0, 0
82
+ for sweep_index in range(num_full_sweeps):
83
+ for step_index in range(num_steps_per_sweep):
84
+ # extract how many samples are in the current step from the metadata
85
+ end_sample_index = start_sample_index + num_samples[global_step_index]
86
+ # compute the number of slices in the current step based on the window we defined on the capture config
87
+ num_slices = sft.upper_border_begin(num_samples[global_step_index])[1]
88
+ # perform a short time fast fourier transform on the step
89
+ complex_spectra = sft.stft(iq_data[start_sample_index:end_sample_index],
90
+ p0=0,
91
+ p1=num_slices)
92
+ # and pack the absolute values into the stepped spectrogram where the step slot is padded to the maximum size for ease of processing later)
93
+ stepped_dynamic_spectra[sweep_index, step_index, :, :num_slices] = np.abs(complex_spectra)
94
+ # reassign the start_sample_index for the next step
95
+ start_sample_index = end_sample_index
96
+ # and increment the global step index
97
+ global_step_index += 1
98
+
99
+
100
+ def _compute_num_max_slices_in_step(sft: ShortTimeFFT,
101
+ num_samples: np.ndarray) -> int:
102
+ """Compute the maximum number of slices over all steps, in all sweeps over the chunk."""
103
+ return sft.upper_border_begin(np.max(num_samples))[1]
104
+
105
+
106
+ def _compute_num_full_sweeps(center_frequencies: np.ndarray) -> int:
107
+ """Compute the total number of full sweeps over the chunk.
108
+
109
+ Since the number of each samples in each step is variable, we only know a sweep is complete
110
+ when there is a sweep after it. So we can define the total number of *full* sweeps as the number of
111
+ (freq_max, freq_min) pairs in center_frequencies. It is only at an instance of (freq_max, freq_min) pair
112
+ in center frequencies that the frequency decreases, so, we can compute the number of full sweeps by
113
+ counting the numbers of negative values in np.diff(center_frequencies)
114
+ """
115
+ return len(np.where(np.diff(center_frequencies) < 0)[0])
116
+
117
+
118
+ def _compute_num_steps_per_sweep(center_frequencies: np.ndarray) -> int:
119
+ """Compute the (ensured constant) number of steps in each sweep."""
120
+ # find the (step) indices corresponding to the minimum frequencies
121
+ min_freq_indices = np.where(center_frequencies == np.min(center_frequencies))[0]
122
+ # then, we evaluate the number of steps that has occured between them via np.diff over the indices
123
+ unique_num_steps_per_sweep = np.unique(np.diff(min_freq_indices))
124
+ # we expect that the difference is always the same, so that the result of np.unique has a single element
125
+ if len(unique_num_steps_per_sweep) != 1:
126
+ raise InvalidSweepMetadataError(("Irregular step count per sweep, "
127
+ "expected a consistent number of steps per sweep"))
128
+ return int(unique_num_steps_per_sweep[0])
129
+
130
+
131
+ def _validate_center_frequencies_ordering(center_frequencies: np.ndarray,
132
+ freq_step: float) -> None:
133
+ """Check that the center frequencies are well-ordered in the detached header"""
134
+ min_frequency = np.min(center_frequencies)
135
+ # Extract the expected difference between each step within a sweep.
136
+ for i, diff in enumerate(np.diff(center_frequencies)):
137
+ # steps should either increase by freq_step or drop to the minimum
138
+ if (diff != freq_step) and (center_frequencies[i + 1] != min_frequency):
139
+ raise InvalidSweepMetadataError(f"Unordered center frequencies detected")
140
+
141
+
142
+ def _do_stfft(iq_data: np.ndarray,
143
+ sweep_metadata: SweepMetadata,
144
+ capture_config: CaptureConfig):
145
+ """Perform a Short Time FFT on the input swept IQ samples."""
146
+
147
+ sft = make_sft_instance(capture_config)
148
+
149
+ frequency_step = capture_config.get_parameter_value(PNames.FREQUENCY_STEP)
150
+ _validate_center_frequencies_ordering(sweep_metadata.center_frequencies,
151
+ frequency_step)
152
+
153
+ window_size = capture_config.get_parameter_value(PNames.WINDOW_SIZE)
154
+
155
+ num_steps_per_sweep = _compute_num_steps_per_sweep(sweep_metadata.center_frequencies)
156
+ num_full_sweeps = _compute_num_full_sweeps(sweep_metadata.center_frequencies)
157
+ num_max_slices_in_step = _compute_num_max_slices_in_step(sft,
158
+ sweep_metadata.num_samples)
159
+
160
+ stepped_dynamic_spectra_shape = (num_full_sweeps,
161
+ num_steps_per_sweep,
162
+ window_size,
163
+ num_max_slices_in_step)
164
+ stepped_dynamic_spectra = np.full(stepped_dynamic_spectra_shape, np.nan)
165
+
166
+ frequencies = np.empty(num_steps_per_sweep * window_size)
167
+ times = np.empty(num_full_sweeps)
168
+
169
+ _fill_stepped_dynamic_spectra(stepped_dynamic_spectra,
170
+ sft,
171
+ iq_data,
172
+ sweep_metadata.num_samples,
173
+ num_full_sweeps,
174
+ num_steps_per_sweep)
175
+
176
+ _fill_frequencies(frequencies,
177
+ sweep_metadata.center_frequencies,
178
+ sft.f,
179
+ window_size)
180
+
181
+ sample_rate = capture_config.get_parameter_value(PNames.SAMPLE_RATE)
182
+ _fill_times(times,
183
+ sweep_metadata.num_samples,
184
+ sample_rate,
185
+ num_full_sweeps,
186
+ num_steps_per_sweep)
187
+
188
+ averaged_spectra = _average_over_steps(stepped_dynamic_spectra)
189
+ dynamic_spectra = _stitch_steps(averaged_spectra,
190
+ num_full_sweeps)
191
+
192
+ return times, frequencies, dynamic_spectra
193
+
194
+
195
+ def _correct_timing(chunk_start_datetime: datetime,
196
+ millisecond_correction: int,
197
+ num_samples_prepended: int,
198
+ sample_rate: int):
199
+ """Correct the start time for this chunk based on the number of samples we prepended reconstructing the initial sweep."""
200
+ sample_interval = (1 / sample_rate)
201
+ elapsed_time = num_samples_prepended * sample_interval
202
+ corrected_datetime = chunk_start_datetime + timedelta(milliseconds = millisecond_correction) - timedelta(seconds = float(elapsed_time))
203
+ return corrected_datetime.strftime(TimeFormats.DATETIME), corrected_datetime.microsecond * 1e-3
204
+
205
+
206
+ def _prepend_num_samples(carryover_num_samples: np.ndarray,
207
+ num_samples: np.ndarray,
208
+ final_step_spans_two_chunks: bool) -> np.ndarray:
209
+ """Prepend the number of samples from the final sweep of the previous chunk."""
210
+ if final_step_spans_two_chunks:
211
+ # ensure the number of samples from the final step in the previous chunk are accounted for
212
+ num_samples[0] += carryover_num_samples[-1]
213
+ # and truncate as required
214
+ carryover_num_samples = carryover_num_samples[:-1]
215
+ return np.concatenate((carryover_num_samples, num_samples))
216
+
217
+
218
+ def _prepend_center_frequencies(carryover_center_frequencies: np.ndarray,
219
+ center_frequencies: np.ndarray,
220
+ final_step_spans_two_chunks: bool)-> np.ndarray:
221
+ """Prepend the center frequencies from the final sweep of the previous chunk."""
222
+ # in the case that the sweep has bled across chunks,
223
+ # do not permit identical neighbours in the center frequency array
224
+ if final_step_spans_two_chunks:
225
+ # truncate the final frequency to prepend (as it already exists in the array we are appending to in this case)
226
+ carryover_center_frequencies = carryover_center_frequencies[:-1]
227
+ return np.concatenate((carryover_center_frequencies, center_frequencies))
228
+
229
+
230
+ def _prepend_iq_data(carryover_iq_data: np.ndarray,
231
+ iq_data: np.ndarray) -> np.ndarray:
232
+ """Prepend the IQ samples from the final sweep of the previous chunk."""
233
+ return np.concatenate((carryover_iq_data, iq_data))
234
+
235
+
236
+ def _get_final_sweep(previous_chunk: BaseChunk
237
+ ) -> Tuple[np.ndarray, SweepMetadata]:
238
+ """Get data from the final sweep of the previous chunk."""
239
+ # unpack the data from the previous chunk
240
+ previous_iq_data = previous_chunk.read_file("bin")
241
+ _, previous_sweep_metadata = previous_chunk.read_file("hdr")
242
+ # find the step index from the last sweep
243
+ # [0] since the return of np.where is a 1 element Tuple,
244
+ # containing a list of step indices corresponding to the smallest center frequencies
245
+ # [-1] since we want the final step index, where the center frequency is minimised
246
+ final_sweep_start_step_index = np.where(previous_sweep_metadata.center_frequencies == np.min(previous_sweep_metadata.center_frequencies))[0][-1]
247
+ # isolate the data from the final sweep
248
+ final_center_frequencies = previous_sweep_metadata.center_frequencies[final_sweep_start_step_index:]
249
+ final_num_samples = previous_sweep_metadata.num_samples[final_sweep_start_step_index:]
250
+ final_sweep_iq_data = previous_iq_data[-np.sum(final_num_samples):]
251
+
252
+ # sanity check on the number of samples in the final sweep
253
+ if len(final_sweep_iq_data) != np.sum(final_num_samples):
254
+ raise ValueError((f"Unexpected error! Mismatch in sample count for the final sweep data."
255
+ f"Expected {np.sum(final_num_samples)} based on sweep metadata, but found "
256
+ f" {len(final_sweep_iq_data)} IQ samples in the final sweep"))
257
+
258
+ return final_sweep_iq_data, SweepMetadata(final_center_frequencies, final_num_samples)
259
+
260
+
261
+ def _reconstruct_initial_sweep(previous_chunk: BaseChunk,
262
+ iq_data: np.ndarray,
263
+ sweep_metadata: SweepMetadata) -> Tuple[np.ndarray, SweepMetadata, int]:
264
+ """Reconstruct the initial sweep of the current chunk, using data from the previous chunk."""
265
+
266
+ # carryover the final sweep of the previous chunk, and prepend that data to the current chunk data
267
+ carryover_iq_data, carryover_sweep_metadata = _get_final_sweep(previous_chunk)
268
+
269
+ # prepend the iq data that was carried over from the previous chunk
270
+ iq_data = _prepend_iq_data(carryover_iq_data,
271
+ iq_data)
272
+
273
+ # prepend the sweep metadata from the previous chunk
274
+ final_step_spans_two_chunks = carryover_sweep_metadata.center_frequencies[-1] == sweep_metadata.center_frequencies[0]
275
+ center_frequencies = _prepend_center_frequencies(carryover_sweep_metadata.center_frequencies,
276
+ sweep_metadata.center_frequencies,
277
+ final_step_spans_two_chunks)
278
+ num_samples = _prepend_num_samples(carryover_sweep_metadata.num_samples,
279
+ sweep_metadata.num_samples,
280
+ final_step_spans_two_chunks)
281
+
282
+ # keep track of how many samples we prepended (required to adjust timing later)
283
+ num_samples_prepended = np.sum(carryover_sweep_metadata.num_samples)
284
+ return iq_data, SweepMetadata(center_frequencies, num_samples), num_samples_prepended
285
+
286
+
287
+ def _build_spectrogram(chunk: BaseChunk,
288
+ capture_config: CaptureConfig,
289
+ previous_chunk: Optional[BaseChunk] = None) -> Spectrogram:
290
+ """Create a spectrogram by performing a Short Time FFT on the (swept) IQ samples for this chunk."""
291
+ iq_data = chunk.read_file("bin")
292
+ millisecond_correction, sweep_metadata = chunk.read_file("hdr")
293
+
294
+ # if a previous chunk has been specified, this indicates that the initial sweep spans
295
+ # between two adjacent batched files.
296
+ if previous_chunk:
297
+ # If this is the case, first reconstruct the initial sweep of the current chunk
298
+ # by prepending the final sweep of the previous chunk
299
+ iq_data, sweep_metadata, num_samples_prepended = _reconstruct_initial_sweep(previous_chunk,
300
+ iq_data,
301
+ sweep_metadata)
302
+ # since we have prepended extra samples, we need to correct the chunk start time
303
+ # appropriately
304
+ chunk_start_time, millisecond_correction = _correct_timing(chunk.chunk_start_datetime,
305
+ millisecond_correction,
306
+ num_samples_prepended,
307
+ capture_config.get_parameter_value(PNames.SAMPLE_RATE))
308
+ # otherwise, no action is required
309
+ else:
310
+ chunk_start_time = chunk.chunk_start_time
311
+
312
+ microsecond_correction = millisecond_correction * 1e3
313
+
314
+ times, frequencies, dynamic_spectra = _do_stfft(iq_data,
315
+ sweep_metadata,
316
+ capture_config)
317
+
318
+ return Spectrogram(dynamic_spectra,
319
+ times,
320
+ frequencies,
321
+ chunk.tag,
322
+ chunk_start_time,
323
+ microsecond_correction,
324
+ spectrum_type = "amplitude")
325
+
326
+
327
+ @register_event_handler(CaptureModes.SWEPT_CENTER_FREQUENCY)
328
+ class _EventHandler(BaseEventHandler):
329
+ def __init__(self, *args, **kwargs):
330
+ super().__init__(*args, **kwargs)
331
+
332
+ # the previous chunk is stored in order to fetch the
333
+ # data from the "final sweep" which was ignored during
334
+ # processing.
335
+ self._previous_chunk: BaseChunk = None
336
+
337
+
338
+ def process(self,
339
+ absolute_file_path: str):
340
+ _LOGGER.info(f"Processing: {absolute_file_path}")
341
+ file_name = os.path.basename(absolute_file_path)
342
+ # discard the extension
343
+ base_file_name, _ = os.path.splitext(file_name)
344
+ chunk_start_time, tag = base_file_name.split('_')
345
+ chunk = self._Chunk(chunk_start_time, tag)
346
+
347
+ # ensure that the file which has been created has the expected tag
348
+ if tag != self._tag:
349
+ raise RuntimeError(f"Received an unexpected tag! Expected '{self._tag}', "
350
+ f"but a file has been created with tag '{tag}'")
351
+
352
+ _LOGGER.info("Creating spectrogram")
353
+ spectrogram = _build_spectrogram(chunk,
354
+ self._capture_config,
355
+ previous_chunk = self._previous_chunk)
356
+
357
+ spectrogram = time_average(spectrogram,
358
+ resolution = self._capture_config.get_parameter_value(PNames.TIME_RESOLUTION))
359
+
360
+ spectrogram = frequency_average(spectrogram,
361
+ resolution = self._capture_config.get_parameter_value(PNames.FREQUENCY_RESOLUTION))
362
+
363
+ self._cache_spectrogram(spectrogram)
364
+
365
+ # if the previous chunk has not yet been set, it means we are processing the first chunk
366
+ # so we don't need to handle the previous chunk
367
+ if self._previous_chunk is None:
368
+ # instead, only set it for the next time this method is called
369
+ self._previous_chunk = chunk
370
+
371
+ # otherwise the previous chunk is defined (and by this point has already been processed)
372
+ else:
373
+ bin_chunk = self._previous_chunk.get_file('bin')
374
+ _LOGGER.info(f"Deleting {bin_chunk.file_path}")
375
+ bin_chunk.delete()
376
+
377
+ hdr_chunk = self._previous_chunk.get_file('hdr')
378
+ _LOGGER.info(f"Deleting {hdr_chunk.file_path}")
379
+ hdr_chunk.delete()
380
+
381
+ # and reassign the current chunk to be used as the previous chunk at the next call of this method
382
+ self._previous_chunk = chunk
@@ -2,5 +2,15 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- # dynamically import all receiver
6
- import spectre_core.receivers.library
5
+ # register decorators take effect on import
6
+ from .library._test import _Receiver
7
+ from .library._rsp1a import _Receiver
8
+
9
+ from ._base import BaseReceiver
10
+ from ._factory import get_receiver
11
+ from ._register import list_all_receiver_names
12
+ from ._spec_names import SpecNames
13
+
14
+ __all__ = [
15
+ "BaseReceiver", "get_receiver", "list_all_receiver_names", "SpecNames"
16
+ ]