spectre-core 0.0.25__py3-none-any.whl → 0.0.27__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 (33) hide show
  1. spectre_core/__init__.py +1 -1
  2. spectre_core/capture_configs/_capture_config.py +2 -0
  3. spectre_core/plotting/_panel_stack.py +11 -4
  4. spectre_core/receivers/__init__.py +11 -8
  5. spectre_core/receivers/_factory.py +16 -11
  6. spectre_core/receivers/_receiver.py +246 -0
  7. spectre_core/receivers/_register.py +3 -3
  8. spectre_core/receivers/{_spec_names.py → _specs.py} +42 -7
  9. spectre_core/receivers/plugins/_b200mini.py +218 -34
  10. spectre_core/receivers/plugins/{gr/_usrp.py → _b200mini_gr.py} +38 -61
  11. spectre_core/receivers/plugins/_custom.py +20 -0
  12. spectre_core/receivers/plugins/{gr/_base.py → _gr.py} +1 -1
  13. spectre_core/receivers/plugins/_receiver_names.py +5 -3
  14. spectre_core/receivers/plugins/_rsp1a.py +38 -43
  15. spectre_core/receivers/plugins/_rsp1a_gr.py +112 -0
  16. spectre_core/receivers/plugins/_rspduo.py +47 -57
  17. spectre_core/receivers/plugins/_rspduo_gr.py +165 -0
  18. spectre_core/receivers/plugins/_sdrplay_receiver.py +146 -42
  19. spectre_core/receivers/plugins/_signal_generator.py +225 -0
  20. spectre_core/receivers/plugins/_signal_generator_gr.py +77 -0
  21. spectre_core/spectrograms/_analytical.py +18 -18
  22. {spectre_core-0.0.25.dist-info → spectre_core-0.0.27.dist-info}/METADATA +1 -1
  23. {spectre_core-0.0.25.dist-info → spectre_core-0.0.27.dist-info}/RECORD +26 -27
  24. spectre_core/receivers/_base.py +0 -242
  25. spectre_core/receivers/plugins/_test.py +0 -225
  26. spectre_core/receivers/plugins/_usrp.py +0 -213
  27. spectre_core/receivers/plugins/gr/__init__.py +0 -3
  28. spectre_core/receivers/plugins/gr/_rsp1a.py +0 -127
  29. spectre_core/receivers/plugins/gr/_rspduo.py +0 -202
  30. spectre_core/receivers/plugins/gr/_test.py +0 -117
  31. {spectre_core-0.0.25.dist-info → spectre_core-0.0.27.dist-info}/WHEEL +0 -0
  32. {spectre_core-0.0.25.dist-info → spectre_core-0.0.27.dist-info}/licenses/LICENSE +0 -0
  33. {spectre_core-0.0.25.dist-info → spectre_core-0.0.27.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,11 @@
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, overload
5
+ from abc import ABC, abstractmethod
6
+ from typing import Callable, cast, Optional
7
+ from logging import getLogger
8
+
9
+ _LOGGER = getLogger(__name__)
6
10
 
7
11
  from spectre_core.capture_configs import (
8
12
  CaptureTemplate,
@@ -10,38 +14,136 @@ from spectre_core.capture_configs import (
10
14
  Parameters,
11
15
  Bound,
12
16
  PName,
17
+ OneOf,
13
18
  get_base_capture_template,
14
19
  get_base_ptemplate,
15
- OneOf,
16
20
  validate_fixed_center_frequency,
17
21
  validate_swept_center_frequency,
18
22
  )
19
- from .._base import BaseReceiver
20
- from .._spec_names import SpecName
21
23
 
24
+ from .._receiver import Receiver, ReceiverName
25
+ from .._specs import SpecName
26
+
27
+ LOW_IF_SAMPLE_RATE_CUTOFF = 2e6
28
+ LOW_IF_PERMITTED_SAMPLE_RATES = [LOW_IF_SAMPLE_RATE_CUTOFF / (2**i) for i in range(6)]
29
+
30
+
31
+ class SDRplayReceiver(ABC, Receiver):
32
+ """An abstract base class for SDRplay devices."""
33
+
34
+ def __init__(self, name: ReceiverName, mode: Optional[str]):
35
+ """Initialise an instance of an `SDRplayReceiver`."""
36
+ super().__init__(name, mode)
37
+
38
+ self.add_spec(SpecName.SAMPLE_RATE_LOWER_BOUND, 62.5e3)
39
+ self.add_spec(SpecName.SAMPLE_RATE_UPPER_BOUND, 10.66e6)
40
+ self.add_spec(SpecName.FREQUENCY_LOWER_BOUND, 1e3)
41
+ self.add_spec(SpecName.FREQUENCY_UPPER_BOUND, 2e9)
42
+ self.add_spec(SpecName.IF_GAIN_UPPER_BOUND, -20)
43
+ self.add_spec(SpecName.IF_GAIN_LOWER_BOUND, -59)
44
+ self.add_spec(SpecName.API_RETUNING_LATENCY, 25 * 1e-3)
45
+
46
+ # bandwidth == 0 means 'AUTO', i.e. the largest bandwidth compatible with the sample rate
47
+ self.add_spec(
48
+ SpecName.BANDWIDTH_OPTIONS,
49
+ [0, 200e3, 300e3, 600e3, 1.536e6, 5e6, 6e6, 7e6, 8e6],
50
+ )
51
+
52
+ @abstractmethod
53
+ def get_rf_gains(self, center_frequency: float) -> list[int]:
54
+ """Get an ordered list of RF gain values corresponding to each LNA state at the specified center frequency.
55
+
56
+ The values are taken from the gain reduction tables documented in the SDRplay API specification, and are
57
+ unique to each model. Note that negative gain values represent positive gain reduction.
58
+ """
59
+
60
+
61
+ def _validate_rf_gain(rf_gain: int, expected_rf_gains: list[int]):
62
+ """Validate the RF gain value against the expected values for the current LNA state.
63
+
64
+ The RF gain is determined by the LNA state and can only take specific values as documented in the
65
+ gain reduction tables of the SDRplay API specification.
66
+
67
+ For implementation details, refer to the `gr-sdrplay3` OOT module:
68
+ https://github.com/fventuri/gr-sdrplay3/blob/v3.11.0.9/lib/rsp_impl.cc#L378-L387
69
+ """
70
+ if rf_gain not in expected_rf_gains:
71
+ raise ValueError(
72
+ f"The value of RF gain must be one of {expected_rf_gains}. "
73
+ f"Got {rf_gain}."
74
+ )
75
+
76
+
77
+ def _validate_low_if_sample_rate(sample_rate: int) -> None:
78
+ """Validate the sample rate if the receiver is operating in low IF mode.
79
+
80
+ The minimum physical sampling rate of the SDRplay hardware is 2 MHz. Lower effective rates can be achieved
81
+ through decimation, as handled by the `gr-sdrplay3` OOT module. This function ensures that the sample rate
82
+ is not silently adjusted by the backend.
83
+
84
+ For implementation details, refer to:
85
+ https://github.com/fventuri/gr-sdrplay3/blob/v3.11.0.9/lib/rsp_impl.cc#L140-L179
86
+ """
87
+ if sample_rate <= LOW_IF_SAMPLE_RATE_CUTOFF:
88
+ if sample_rate not in LOW_IF_PERMITTED_SAMPLE_RATES:
89
+ raise ValueError(
90
+ f"If the requested sample rate is less than or equal to {LOW_IF_SAMPLE_RATE_CUTOFF}, "
91
+ f"the receiver will be operating in low IF mode. "
92
+ f"So, the sample rate must be exactly one of {LOW_IF_PERMITTED_SAMPLE_RATES}. "
93
+ f"Got sample rate {sample_rate} Hz"
94
+ )
22
95
 
23
- def get_pvalidator_fixed_center_frequency(
24
- sdrplay_receiver: BaseReceiver,
96
+
97
+ def make_pvalidator_fixed_center_frequency(
98
+ receiver: SDRplayReceiver,
25
99
  ) -> Callable[[Parameters], None]:
26
100
  def pvalidator(parameters: Parameters) -> None:
27
101
  validate_fixed_center_frequency(parameters)
28
102
 
103
+ # Validate the sample rate, in the case the receiver will be operating in low if mode.
104
+ sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
105
+ _validate_low_if_sample_rate(sample_rate)
106
+
107
+ # Validate the rf gain value, which is a function of center frequency.
108
+ rf_gain = cast(int, parameters.get_parameter_value(PName.RF_GAIN))
109
+ center_frequency = cast(
110
+ float, parameters.get_parameter_value(PName.CENTER_FREQUENCY)
111
+ )
112
+ _validate_rf_gain(rf_gain, receiver.get_rf_gains(center_frequency))
113
+
29
114
  return pvalidator
30
115
 
31
116
 
32
- def get_pvalidator_swept_center_frequency(
33
- sdrplay_receiver: BaseReceiver,
117
+ def make_pvalidator_swept_center_frequency(
118
+ receiver: SDRplayReceiver,
34
119
  ) -> Callable[[Parameters], None]:
35
120
  def pvalidator(parameters: Parameters) -> None:
36
121
  validate_swept_center_frequency(
37
- parameters, sdrplay_receiver.get_spec(SpecName.API_RETUNING_LATENCY)
122
+ parameters, receiver.get_spec(SpecName.API_RETUNING_LATENCY)
38
123
  )
39
124
 
125
+ # Validate the sample rate, in the case the receiver will be operating in low if mode.
126
+ sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
127
+ _validate_low_if_sample_rate(sample_rate)
128
+
129
+ # Validate the rf gain value, which is a function of center frequency.
130
+ rf_gain = cast(int, parameters.get_parameter_value(PName.RF_GAIN))
131
+ min_frequency = cast(float, parameters.get_parameter_value(PName.MIN_FREQUENCY))
132
+ max_frequency = cast(float, parameters.get_parameter_value(PName.MAX_FREQUENCY))
133
+ _validate_rf_gain(rf_gain, receiver.get_rf_gains(min_frequency))
134
+ _validate_rf_gain(rf_gain, receiver.get_rf_gains(max_frequency))
135
+
136
+ # Check that we are not cross a threshold where the LNA state has to change
137
+ if receiver.get_rf_gains(min_frequency) != receiver.get_rf_gains(max_frequency):
138
+ _LOGGER.warning(
139
+ "Crossing a threshold where the LNA state has to change. Performance may be reduced."
140
+ )
141
+
40
142
  return pvalidator
41
143
 
42
144
 
43
- def get_capture_template_fixed_center_frequency(
44
- sdrplay_receiver: BaseReceiver,
145
+ def make_capture_template_fixed_center_frequency(
146
+ receiver: SDRplayReceiver,
45
147
  ) -> CaptureTemplate:
46
148
 
47
149
  capture_template = get_base_capture_template(CaptureMode.FIXED_CENTER_FREQUENCY)
@@ -52,12 +154,12 @@ def get_capture_template_fixed_center_frequency(
52
154
  capture_template.set_defaults(
53
155
  (PName.BATCH_SIZE, 3.0),
54
156
  (PName.CENTER_FREQUENCY, 95800000),
55
- (PName.SAMPLE_RATE, 600000),
56
- (PName.BANDWIDTH, 600000),
157
+ (PName.SAMPLE_RATE, 500000),
158
+ (PName.BANDWIDTH, 300000),
57
159
  (PName.WINDOW_HOP, 512),
58
160
  (PName.WINDOW_SIZE, 1024),
59
161
  (PName.WINDOW_TYPE, "blackman"),
60
- (PName.RF_GAIN, -30),
162
+ (PName.RF_GAIN, 0),
61
163
  (PName.IF_GAIN, -30),
62
164
  )
63
165
 
@@ -65,8 +167,8 @@ def get_capture_template_fixed_center_frequency(
65
167
  PName.CENTER_FREQUENCY,
66
168
  [
67
169
  Bound(
68
- lower_bound=sdrplay_receiver.get_spec(SpecName.FREQUENCY_LOWER_BOUND),
69
- upper_bound=sdrplay_receiver.get_spec(SpecName.FREQUENCY_UPPER_BOUND),
170
+ lower_bound=receiver.get_spec(SpecName.FREQUENCY_LOWER_BOUND),
171
+ upper_bound=receiver.get_spec(SpecName.FREQUENCY_UPPER_BOUND),
70
172
  )
71
173
  ],
72
174
  )
@@ -74,27 +176,28 @@ def get_capture_template_fixed_center_frequency(
74
176
  PName.SAMPLE_RATE,
75
177
  [
76
178
  Bound(
77
- lower_bound=sdrplay_receiver.get_spec(SpecName.SAMPLE_RATE_LOWER_BOUND),
78
- upper_bound=sdrplay_receiver.get_spec(SpecName.SAMPLE_RATE_UPPER_BOUND),
179
+ lower_bound=receiver.get_spec(SpecName.SAMPLE_RATE_LOWER_BOUND),
180
+ upper_bound=receiver.get_spec(SpecName.SAMPLE_RATE_UPPER_BOUND),
79
181
  )
80
182
  ],
81
183
  )
82
184
  capture_template.add_pconstraint(
83
- PName.BANDWIDTH, [OneOf(sdrplay_receiver.get_spec(SpecName.BANDWIDTH_OPTIONS))]
185
+ PName.BANDWIDTH, [OneOf(receiver.get_spec(SpecName.BANDWIDTH_OPTIONS))]
84
186
  )
85
187
  capture_template.add_pconstraint(
86
188
  PName.IF_GAIN,
87
- [Bound(upper_bound=sdrplay_receiver.get_spec(SpecName.IF_GAIN_UPPER_BOUND))],
88
- )
89
- capture_template.add_pconstraint(
90
- PName.RF_GAIN,
91
- [Bound(upper_bound=sdrplay_receiver.get_spec(SpecName.RF_GAIN_UPPER_BOUND))],
189
+ [
190
+ Bound(
191
+ lower_bound=receiver.get_spec(SpecName.IF_GAIN_LOWER_BOUND),
192
+ upper_bound=receiver.get_spec(SpecName.IF_GAIN_UPPER_BOUND),
193
+ )
194
+ ],
92
195
  )
93
196
  return capture_template
94
197
 
95
198
 
96
- def get_capture_template_swept_center_frequency(
97
- sdrplay_receiver: BaseReceiver,
199
+ def make_capture_template_swept_center_frequency(
200
+ receiver: Receiver,
98
201
  ) -> CaptureTemplate:
99
202
 
100
203
  capture_template = get_base_capture_template(CaptureMode.SWEPT_CENTER_FREQUENCY)
@@ -103,17 +206,17 @@ def get_capture_template_swept_center_frequency(
103
206
  capture_template.add_ptemplate(get_base_ptemplate(PName.RF_GAIN))
104
207
 
105
208
  capture_template.set_defaults(
106
- (PName.BATCH_SIZE, 4.0),
209
+ (PName.BATCH_SIZE, 3.0),
107
210
  (PName.MIN_FREQUENCY, 95000000),
108
211
  (PName.MAX_FREQUENCY, 100000000),
109
212
  (PName.SAMPLES_PER_STEP, 80000),
110
- (PName.FREQUENCY_STEP, 1536000),
111
- (PName.SAMPLE_RATE, 1536000),
213
+ (PName.FREQUENCY_STEP, 2000000),
214
+ (PName.SAMPLE_RATE, 2e6),
112
215
  (PName.BANDWIDTH, 1536000),
113
216
  (PName.WINDOW_HOP, 512),
114
217
  (PName.WINDOW_SIZE, 1024),
115
218
  (PName.WINDOW_TYPE, "blackman"),
116
- (PName.RF_GAIN, -30),
219
+ (PName.RF_GAIN, 0),
117
220
  (PName.IF_GAIN, -30),
118
221
  )
119
222
 
@@ -121,8 +224,8 @@ def get_capture_template_swept_center_frequency(
121
224
  PName.MIN_FREQUENCY,
122
225
  [
123
226
  Bound(
124
- lower_bound=sdrplay_receiver.get_spec(SpecName.FREQUENCY_LOWER_BOUND),
125
- upper_bound=sdrplay_receiver.get_spec(SpecName.FREQUENCY_UPPER_BOUND),
227
+ lower_bound=receiver.get_spec(SpecName.FREQUENCY_LOWER_BOUND),
228
+ upper_bound=receiver.get_spec(SpecName.FREQUENCY_UPPER_BOUND),
126
229
  )
127
230
  ],
128
231
  )
@@ -130,8 +233,8 @@ def get_capture_template_swept_center_frequency(
130
233
  PName.MAX_FREQUENCY,
131
234
  [
132
235
  Bound(
133
- lower_bound=sdrplay_receiver.get_spec(SpecName.FREQUENCY_LOWER_BOUND),
134
- upper_bound=sdrplay_receiver.get_spec(SpecName.FREQUENCY_UPPER_BOUND),
236
+ lower_bound=receiver.get_spec(SpecName.FREQUENCY_LOWER_BOUND),
237
+ upper_bound=receiver.get_spec(SpecName.FREQUENCY_UPPER_BOUND),
135
238
  )
136
239
  ],
137
240
  )
@@ -139,20 +242,21 @@ def get_capture_template_swept_center_frequency(
139
242
  PName.SAMPLE_RATE,
140
243
  [
141
244
  Bound(
142
- lower_bound=sdrplay_receiver.get_spec(SpecName.SAMPLE_RATE_LOWER_BOUND),
143
- upper_bound=sdrplay_receiver.get_spec(SpecName.SAMPLE_RATE_UPPER_BOUND),
245
+ lower_bound=receiver.get_spec(SpecName.SAMPLE_RATE_LOWER_BOUND),
246
+ upper_bound=receiver.get_spec(SpecName.SAMPLE_RATE_UPPER_BOUND),
144
247
  )
145
248
  ],
146
249
  )
147
250
  capture_template.add_pconstraint(
148
- PName.BANDWIDTH, [OneOf(sdrplay_receiver.get_spec(SpecName.BANDWIDTH_OPTIONS))]
251
+ PName.BANDWIDTH, [OneOf(receiver.get_spec(SpecName.BANDWIDTH_OPTIONS))]
149
252
  )
150
253
  capture_template.add_pconstraint(
151
254
  PName.IF_GAIN,
152
- [Bound(upper_bound=sdrplay_receiver.get_spec(SpecName.IF_GAIN_UPPER_BOUND))],
153
- )
154
- capture_template.add_pconstraint(
155
- PName.RF_GAIN,
156
- [Bound(upper_bound=sdrplay_receiver.get_spec(SpecName.RF_GAIN_UPPER_BOUND))],
255
+ [
256
+ Bound(
257
+ lower_bound=receiver.get_spec(SpecName.IF_GAIN_LOWER_BOUND),
258
+ upper_bound=receiver.get_spec(SpecName.IF_GAIN_UPPER_BOUND),
259
+ )
260
+ ],
157
261
  )
158
262
  return capture_template
@@ -0,0 +1,225 @@
1
+ # SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
2
+ # This file is part of SPECTRE
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Callable, Optional, cast
7
+ from functools import partial
8
+
9
+ from spectre_core.capture_configs import (
10
+ CaptureTemplate,
11
+ CaptureMode,
12
+ Parameters,
13
+ Bound,
14
+ PName,
15
+ get_base_capture_template,
16
+ make_base_capture_template,
17
+ get_base_ptemplate,
18
+ validate_window,
19
+ )
20
+ from ._signal_generator_gr import cosine_wave, constant_staircase
21
+ from ._gr import capture
22
+ from ._receiver_names import ReceiverName
23
+ from .._receiver import Receiver
24
+ from .._specs import SpecName
25
+ from .._register import register_receiver
26
+
27
+
28
+ def _make_capture_template_cos_wave(receiver: Receiver) -> CaptureTemplate:
29
+ capture_template = get_base_capture_template(CaptureMode.FIXED_CENTER_FREQUENCY)
30
+ capture_template.add_ptemplate(get_base_ptemplate(PName.AMPLITUDE))
31
+ capture_template.add_ptemplate(get_base_ptemplate(PName.FREQUENCY))
32
+
33
+ capture_template.set_defaults(
34
+ (PName.BATCH_SIZE, 3.0),
35
+ (PName.CENTER_FREQUENCY, 16000),
36
+ (PName.AMPLITUDE, 2.0),
37
+ (PName.FREQUENCY, 32000),
38
+ (PName.SAMPLE_RATE, 128000),
39
+ (PName.WINDOW_HOP, 512),
40
+ (PName.WINDOW_SIZE, 512),
41
+ (PName.WINDOW_TYPE, "boxcar"),
42
+ )
43
+
44
+ capture_template.enforce_defaults(
45
+ PName.TIME_RESOLUTION,
46
+ PName.TIME_RANGE,
47
+ PName.FREQUENCY_RESOLUTION,
48
+ PName.WINDOW_TYPE,
49
+ )
50
+
51
+ capture_template.add_pconstraint(
52
+ PName.SAMPLE_RATE,
53
+ [
54
+ Bound(
55
+ lower_bound=receiver.get_spec(SpecName.SAMPLE_RATE_LOWER_BOUND),
56
+ upper_bound=receiver.get_spec(SpecName.SAMPLE_RATE_UPPER_BOUND),
57
+ )
58
+ ],
59
+ )
60
+ capture_template.add_pconstraint(
61
+ PName.FREQUENCY,
62
+ [
63
+ Bound(
64
+ lower_bound=receiver.get_spec(SpecName.FREQUENCY_LOWER_BOUND),
65
+ upper_bound=receiver.get_spec(SpecName.FREQUENCY_UPPER_BOUND),
66
+ )
67
+ ],
68
+ )
69
+ return capture_template
70
+
71
+
72
+ def _make_capture_template_constant_staircase(receiver: Receiver) -> CaptureTemplate:
73
+ capture_template = make_base_capture_template(
74
+ PName.TIME_RESOLUTION,
75
+ PName.FREQUENCY_RESOLUTION,
76
+ PName.TIME_RANGE,
77
+ PName.SAMPLE_RATE,
78
+ PName.BATCH_SIZE,
79
+ PName.WINDOW_TYPE,
80
+ PName.WINDOW_HOP,
81
+ PName.WINDOW_SIZE,
82
+ PName.EVENT_HANDLER_KEY,
83
+ PName.BATCH_KEY,
84
+ PName.WATCH_EXTENSION,
85
+ PName.MIN_SAMPLES_PER_STEP,
86
+ PName.MAX_SAMPLES_PER_STEP,
87
+ PName.FREQUENCY_STEP,
88
+ PName.STEP_INCREMENT,
89
+ PName.OBS_ALT,
90
+ PName.OBS_LAT,
91
+ PName.OBS_LON,
92
+ PName.OBJECT,
93
+ PName.ORIGIN,
94
+ PName.TELESCOPE,
95
+ PName.INSTRUMENT,
96
+ )
97
+
98
+ capture_template.set_defaults(
99
+ (PName.BATCH_SIZE, 3.0),
100
+ (PName.FREQUENCY_STEP, 128000),
101
+ (PName.MAX_SAMPLES_PER_STEP, 5000),
102
+ (PName.MIN_SAMPLES_PER_STEP, 4000),
103
+ (PName.SAMPLE_RATE, 128000),
104
+ (PName.STEP_INCREMENT, 200),
105
+ (PName.WINDOW_HOP, 512),
106
+ (PName.WINDOW_SIZE, 512),
107
+ (PName.WINDOW_TYPE, "boxcar"),
108
+ (PName.EVENT_HANDLER_KEY, "swept_center_frequency"),
109
+ (PName.BATCH_KEY, "iq_stream"),
110
+ (PName.WATCH_EXTENSION, "bin"),
111
+ )
112
+
113
+ capture_template.enforce_defaults(
114
+ PName.TIME_RESOLUTION,
115
+ PName.TIME_RANGE,
116
+ PName.FREQUENCY_RESOLUTION,
117
+ PName.WINDOW_TYPE,
118
+ PName.EVENT_HANDLER_KEY,
119
+ PName.BATCH_KEY,
120
+ PName.WATCH_EXTENSION,
121
+ )
122
+
123
+ return capture_template
124
+
125
+
126
+ def _make_pvalidator_cosine_wave(receiver: Receiver) -> Callable[[Parameters], None]:
127
+ def pvalidator(parameters: Parameters) -> None:
128
+ validate_window(parameters)
129
+
130
+ sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
131
+ window_size = cast(int, parameters.get_parameter_value(PName.WINDOW_SIZE))
132
+ frequency = cast(float, parameters.get_parameter_value(PName.FREQUENCY))
133
+
134
+ # check that the sample rate is an integer multiple of the underlying signal frequency
135
+ if sample_rate % frequency != 0:
136
+ raise ValueError(
137
+ "The sampling rate must be some integer multiple of frequency"
138
+ )
139
+
140
+ a = sample_rate / frequency
141
+ if a < 2:
142
+ raise ValueError(
143
+ (
144
+ f"The ratio of sampling rate over frequency must be greater than two. "
145
+ f"Got {a}"
146
+ )
147
+ )
148
+
149
+ # analytical requirement
150
+ # if p is the number of sampled cycles, we can find that p = window_size / a
151
+ # the number of sampled cycles must be a positive natural number.
152
+ p = window_size / a
153
+ if window_size % a != 0:
154
+ raise ValueError(
155
+ (
156
+ f"The number of sampled cycles must be a positive natural number. "
157
+ f"Computed that p={p}"
158
+ )
159
+ )
160
+
161
+ return pvalidator
162
+
163
+
164
+ def _make_pvalidator_constant_staircase(
165
+ receiver: Receiver,
166
+ ) -> Callable[[Parameters], None]:
167
+ def pvalidator(parameters: Parameters) -> None:
168
+ validate_window(parameters)
169
+
170
+ freq_step = cast(float, parameters.get_parameter_value(PName.FREQUENCY_STEP))
171
+ sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
172
+ min_samples_per_step = cast(
173
+ int, parameters.get_parameter_value(PName.MIN_SAMPLES_PER_STEP)
174
+ )
175
+ max_samples_per_step = cast(
176
+ int, parameters.get_parameter_value(PName.MAX_SAMPLES_PER_STEP)
177
+ )
178
+
179
+ if freq_step != sample_rate:
180
+ raise ValueError(f"The frequency step must be equal to the sampling rate")
181
+
182
+ if min_samples_per_step > max_samples_per_step:
183
+ raise ValueError(
184
+ (
185
+ f"Minimum samples per step cannot be greater than the maximum samples per step. "
186
+ f"Got {min_samples_per_step}, which is greater than {max_samples_per_step}"
187
+ )
188
+ )
189
+
190
+ return pvalidator
191
+
192
+
193
+ @dataclass(frozen=True)
194
+ class _Mode:
195
+ """An operating mode for the `SignalGenerator` receiver."""
196
+
197
+ COSINE_WAVE = "cosine_wave"
198
+ CONSTANT_STAIRCASE = "constant_staircase"
199
+
200
+
201
+ @register_receiver(ReceiverName.SIGNAL_GENERATOR)
202
+ class SignalGenerator(Receiver):
203
+ """An entirely software-defined receiver, which generates synthetic signals."""
204
+
205
+ def __init__(self, name: ReceiverName, mode: Optional[str] = None) -> None:
206
+ super().__init__(name, mode)
207
+
208
+ self.add_spec(SpecName.SAMPLE_RATE_LOWER_BOUND, 64000)
209
+ self.add_spec(SpecName.SAMPLE_RATE_UPPER_BOUND, 640000)
210
+ self.add_spec(SpecName.FREQUENCY_LOWER_BOUND, 16000)
211
+ self.add_spec(SpecName.FREQUENCY_UPPER_BOUND, 160000)
212
+
213
+ self.add_mode(
214
+ _Mode.COSINE_WAVE,
215
+ partial(capture, top_block_cls=cosine_wave),
216
+ _make_capture_template_cos_wave(self),
217
+ _make_pvalidator_cosine_wave(self),
218
+ )
219
+
220
+ self.add_mode(
221
+ _Mode.CONSTANT_STAIRCASE,
222
+ partial(capture, top_block_cls=constant_staircase),
223
+ _make_capture_template_constant_staircase(self),
224
+ _make_pvalidator_constant_staircase(self),
225
+ )
@@ -0,0 +1,77 @@
1
+ # SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
2
+ # This file is part of SPECTRE
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from gnuradio import gr
6
+ from gnuradio import blocks
7
+ from gnuradio import analog
8
+ from gnuradio import spectre
9
+
10
+ from spectre_core.capture_configs import Parameters, PName
11
+ from spectre_core.config import get_batches_dir_path
12
+ from ._gr import spectre_top_block
13
+
14
+
15
+ class cosine_wave(spectre_top_block):
16
+ def flowgraph(self, tag: str, parameters: Parameters) -> None:
17
+
18
+ # Unpack the capture config parameters
19
+ samp_rate = parameters.get_parameter_value(PName.SAMPLE_RATE)
20
+ batch_size = parameters.get_parameter_value(PName.BATCH_SIZE)
21
+ frequency = parameters.get_parameter_value(PName.FREQUENCY)
22
+ amplitude = parameters.get_parameter_value(PName.AMPLITUDE)
23
+
24
+ # Blocks
25
+ self.spectre_batched_file_sink = spectre.batched_file_sink(
26
+ get_batches_dir_path(), tag, batch_size, samp_rate
27
+ )
28
+ self.blocks_throttle_0 = blocks.throttle(gr.sizeof_float * 1, samp_rate, True)
29
+ self.blocks_throttle_1 = blocks.throttle(gr.sizeof_float * 1, samp_rate, True)
30
+ self.blocks_null_source = blocks.null_source(gr.sizeof_float * 1)
31
+ self.blocks_float_to_complex = blocks.float_to_complex(1)
32
+ self.analog_sig_source = analog.sig_source_f(
33
+ samp_rate, analog.GR_COS_WAVE, frequency, amplitude, 0, 0
34
+ )
35
+
36
+ # Connections
37
+ self.connect((self.analog_sig_source, 0), (self.blocks_throttle_0, 0))
38
+ self.connect((self.blocks_null_source, 0), (self.blocks_throttle_1, 0))
39
+ self.connect((self.blocks_throttle_0, 0), (self.blocks_float_to_complex, 0))
40
+ self.connect((self.blocks_throttle_1, 0), (self.blocks_float_to_complex, 1))
41
+ self.connect(
42
+ (self.blocks_float_to_complex, 0), (self.spectre_batched_file_sink, 0)
43
+ )
44
+
45
+
46
+ class constant_staircase(spectre_top_block):
47
+ def flowgraph(self, tag: str, parameters: Parameters) -> None:
48
+ # Unpack the capture config parameters
49
+ step_increment = parameters.get_parameter_value(PName.STEP_INCREMENT)
50
+ samp_rate = parameters.get_parameter_value(PName.SAMPLE_RATE)
51
+ min_samples_per_step = parameters.get_parameter_value(
52
+ PName.MIN_SAMPLES_PER_STEP
53
+ )
54
+ max_samples_per_step = parameters.get_parameter_value(
55
+ PName.MAX_SAMPLES_PER_STEP
56
+ )
57
+ frequency_step = parameters.get_parameter_value(PName.FREQUENCY_STEP)
58
+ batch_size = parameters.get_parameter_value(PName.BATCH_SIZE)
59
+
60
+ # Blocks
61
+ self.spectre_constant_staircase = spectre.tagged_staircase(
62
+ min_samples_per_step,
63
+ max_samples_per_step,
64
+ frequency_step,
65
+ step_increment,
66
+ samp_rate,
67
+ )
68
+ self.spectre_batched_file_sink = spectre.batched_file_sink(
69
+ get_batches_dir_path(), tag, batch_size, samp_rate, True, "rx_freq", 0
70
+ ) # zero means the center frequency is unset
71
+ self.blocks_throttle = blocks.throttle(
72
+ gr.sizeof_gr_complex * 1, samp_rate, True
73
+ )
74
+
75
+ # Connections
76
+ self.connect((self.spectre_constant_staircase, 0), (self.blocks_throttle, 0))
77
+ self.connect((self.blocks_throttle, 0), (self.spectre_batched_file_sink, 0))