|
10 | 10 |
|
11 | 11 | import numpy as np |
12 | 12 | import numpy.testing as npt |
| 13 | +from hypothesis import assume, given |
| 14 | +from hypothesis import strategies as st |
13 | 15 |
|
14 | 16 | import sigmf |
15 | | -from sigmf import SigMFFile |
16 | 17 | from sigmf.error import SigMFGeneratorError |
17 | 18 | from sigmf.siggen import SigMFGenerator |
18 | 19 |
|
@@ -212,20 +213,21 @@ def test_data_buffer_creation(self): |
212 | 213 | # verify data is complex64 |
213 | 214 | self.assertEqual(samples_0.dtype, np.complex64) |
214 | 215 |
|
215 | | - def test_with_different_amplitudes(self): |
216 | | - """test amplitude parameter""" |
217 | | - amp_low = 0.5 |
218 | | - amp_high = 1.5 |
219 | | - |
| 216 | + @given( |
| 217 | + amp_low=st.floats(min_value=0.1, max_value=0.9, allow_nan=False, allow_infinity=False), |
| 218 | + amp_high=st.floats(min_value=1.1, max_value=3.0, allow_nan=False, allow_infinity=False), |
| 219 | + ) |
| 220 | + def test_amplitude_power_ratio(self, amp_low, amp_high): |
| 221 | + """test that power scales with amplitude squared for any pair of amplitudes""" |
220 | 222 | signal_low = SigMFGenerator(self.seed).amplitude(amp_low).generate() |
221 | 223 | signal_high = SigMFGenerator(self.seed).amplitude(amp_high).generate() |
222 | 224 |
|
223 | 225 | power_low = np.mean(np.abs(signal_low.read_samples()) ** 2) |
224 | 226 | power_high = np.mean(np.abs(signal_high.read_samples()) ** 2) |
225 | 227 |
|
226 | | - expected_power_ratio = (amp_high / amp_low) ** 2 |
227 | | - actual_power_ratio = power_high / power_low |
228 | | - self.assertAlmostEqual(actual_power_ratio, expected_power_ratio, places=1) |
| 228 | + expected_ratio = (amp_high / amp_low) ** 2 |
| 229 | + actual_ratio = power_high / power_low |
| 230 | + npt.assert_almost_equal(actual_ratio, expected_ratio, decimal=1) |
229 | 231 |
|
230 | 232 | def test_automatic_annotations(self): |
231 | 233 | """test that appropriate annotations are automatically created""" |
@@ -270,30 +272,22 @@ def test_automatic_annotations(self): |
270 | 272 | offset_annotation = next(ann for ann in annotations if "freq offset" in ann.get(sigmf.LABEL_KEY, "")) |
271 | 273 | self.assertIn("+200.0 Hz", offset_annotation[sigmf.LABEL_KEY]) |
272 | 274 |
|
273 | | - def test_sweep_annotations(self): |
274 | | - """test sweep annotations have correct frequency bounds including negative""" |
275 | | - signal = SigMFGenerator().sweep(-2500, 2500).sample_rate(22050).generate() |
276 | | - |
277 | | - annotations = signal.get_annotations() |
278 | | - self.assertEqual(len(annotations), 1) # just main sweep annotation |
| 275 | + @given( |
| 276 | + start_freq_hz=st.integers(min_value=-20000, max_value=20000), |
| 277 | + end_freq_hz=st.integers(min_value=-20000, max_value=20000), |
| 278 | + ) |
| 279 | + def test_sweep_annotation_bounds(self, start_freq_hz, end_freq_hz): |
| 280 | + """test sweep annotation bounds are always (min, max) regardless of direction, and label preserves original order""" |
| 281 | + assume(start_freq_hz != end_freq_hz) |
| 282 | + signal = SigMFGenerator().sweep(start_freq_hz, end_freq_hz).sample_rate(48000).generate() |
| 283 | + sweep_ann = signal.get_annotations()[0] |
279 | 284 |
|
280 | | - sweep_annotation = annotations[0] |
281 | | - self.assertEqual(sweep_annotation[sigmf.FREQ_LOWER_EDGE_KEY], -2500.0) |
282 | | - self.assertEqual(sweep_annotation[sigmf.FREQ_UPPER_EDGE_KEY], 2500.0) |
283 | | - self.assertIn("sweep from -2500 to 2500 Hz", sweep_annotation[sigmf.LABEL_KEY]) |
| 285 | + # bounds must be min/max regardless of sweep direction |
| 286 | + self.assertEqual(sweep_ann[sigmf.FREQ_LOWER_EDGE_KEY], float(min(start_freq_hz, end_freq_hz))) |
| 287 | + self.assertEqual(sweep_ann[sigmf.FREQ_UPPER_EDGE_KEY], float(max(start_freq_hz, end_freq_hz))) |
284 | 288 |
|
285 | | - def test_reverse_sweep_annotations(self): |
286 | | - """test reverse sweep crossing DC has correct bounds""" |
287 | | - signal = SigMFGenerator().sweep(3000, -800).sample_rate(48000).generate() |
288 | | - |
289 | | - annotations = signal.get_annotations() |
290 | | - sweep_annotation = annotations[0] |
291 | | - |
292 | | - # frequency bounds should be min/max regardless of sweep direction |
293 | | - self.assertEqual(sweep_annotation[sigmf.FREQ_LOWER_EDGE_KEY], -800.0) |
294 | | - self.assertEqual(sweep_annotation[sigmf.FREQ_UPPER_EDGE_KEY], 3000.0) |
295 | | - # but label should show original order |
296 | | - self.assertIn("sweep from 3000 to -800 Hz", sweep_annotation[sigmf.LABEL_KEY]) |
| 289 | + # label must preserve the original start-to-end order |
| 290 | + self.assertIn(f"sweep from {start_freq_hz} to {end_freq_hz} Hz", sweep_ann[sigmf.LABEL_KEY]) |
297 | 291 |
|
298 | 292 | def test_minimal_annotations(self): |
299 | 293 | """test that simple signals get minimal but complete annotations""" |
@@ -340,38 +334,39 @@ def test_phase_offset(self): |
340 | 334 | class TestEdgeCases(unittest.TestCase): |
341 | 335 | """Test edge cases and error conditions.""" |
342 | 336 |
|
343 | | - def test_zero_duration(self): |
344 | | - """test zero duration raises error""" |
345 | | - with self.assertRaises(SigMFGeneratorError): |
346 | | - SigMFGenerator().duration(0).generate() |
347 | | - |
348 | | - def test_negative_duration(self): |
349 | | - """test negative duration raises error""" |
350 | | - with self.assertRaises(SigMFGeneratorError): |
351 | | - SigMFGenerator().duration(-1.0).generate() |
352 | | - |
353 | | - def test_negative_sample_rate(self): |
354 | | - """test negative sample rate raises error""" |
| 337 | + @given(st.floats(max_value=0.0, allow_nan=False, allow_infinity=False)) |
| 338 | + def test_nonpositive_duration_raises(self, duration_s): |
| 339 | + """test that any non-positive duration raises SigMFGeneratorError""" |
355 | 340 | with self.assertRaises(SigMFGeneratorError): |
356 | | - SigMFGenerator().sample_rate(-8000).generate() |
| 341 | + SigMFGenerator().duration(duration_s).generate() |
357 | 342 |
|
358 | | - def test_tone_nyquist_validation(self): |
359 | | - """test tone frequency exceeding nyquist raises error""" |
360 | | - with self.assertRaises(SigMFGeneratorError): |
361 | | - SigMFGenerator().tone(5000).sample_rate(8000).generate() |
362 | | - with self.assertRaises(SigMFGeneratorError): |
363 | | - SigMFGenerator().tone(-5000).sample_rate(8000).generate() |
364 | | - |
365 | | - def test_sweep_nyquist_validation(self): |
366 | | - """test sweep frequencies exceeding nyquist raise error""" |
367 | | - with self.assertRaises(SigMFGeneratorError): |
368 | | - SigMFGenerator().sweep(1000, 5000).sample_rate(8000).generate() |
| 343 | + @given(st.integers(max_value=0)) |
| 344 | + def test_nonpositive_sample_rate_raises(self, samp_rate_hz): |
| 345 | + """test that any non-positive sample rate raises SigMFGeneratorError""" |
369 | 346 | with self.assertRaises(SigMFGeneratorError): |
370 | | - SigMFGenerator().sweep(5000, 1000).sample_rate(8000).generate() |
| 347 | + SigMFGenerator().sample_rate(samp_rate_hz).generate() |
| 348 | + |
| 349 | + @given( |
| 350 | + samp_rate_hz=st.integers(min_value=100, max_value=200000), |
| 351 | + freq_hz=st.floats(allow_nan=False, allow_infinity=False), |
| 352 | + ) |
| 353 | + def test_tone_nyquist_raises(self, samp_rate_hz, freq_hz): |
| 354 | + """test that any tone frequency exceeding nyquist raises SigMFGeneratorError""" |
| 355 | + assume(abs(freq_hz) > samp_rate_hz / 2) |
371 | 356 | with self.assertRaises(SigMFGeneratorError): |
372 | | - SigMFGenerator().sweep(1000, -5000).sample_rate(8000).generate() |
| 357 | + SigMFGenerator().tone(freq_hz).sample_rate(samp_rate_hz).generate() |
| 358 | + |
| 359 | + @given( |
| 360 | + samp_rate_hz=st.integers(min_value=100, max_value=200000), |
| 361 | + start_freq_hz=st.floats(allow_nan=False, allow_infinity=False), |
| 362 | + end_freq_hz=st.floats(allow_nan=False, allow_infinity=False), |
| 363 | + ) |
| 364 | + def test_sweep_nyquist_raises(self, samp_rate_hz, start_freq_hz, end_freq_hz): |
| 365 | + """test that any sweep with at least one frequency exceeding nyquist raises SigMFGeneratorError""" |
| 366 | + nyquist_hz = samp_rate_hz / 2 |
| 367 | + assume(abs(start_freq_hz) > nyquist_hz or abs(end_freq_hz) > nyquist_hz) |
373 | 368 | with self.assertRaises(SigMFGeneratorError): |
374 | | - SigMFGenerator().sweep(-5000, 1000).sample_rate(8000).generate() |
| 369 | + SigMFGenerator().sweep(start_freq_hz, end_freq_hz).sample_rate(samp_rate_hz).generate() |
375 | 370 |
|
376 | 371 | def test_sweep_same_start_end_frequency(self): |
377 | 372 | """test sweep with same start and end frequency""" |
|
0 commit comments