Skip to content

Commit 2fb1425

Browse files
committed
support full auto-generate
1 parent a368715 commit 2fb1425

File tree

2 files changed

+47
-41
lines changed

2 files changed

+47
-41
lines changed

sigmf/generate.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class SigMFGenerator:
4242
"""
4343

4444
def __init__(self, seed: Optional[int] = None):
45-
# random state for reproducible generation
45+
# random state for reproducible generation across arch / platforms
4646
self._rng = np.random.RandomState(seed)
4747
self._seed = seed
4848

@@ -320,9 +320,18 @@ def _fill_random_parameters(self) -> None:
320320
if self._duration_s is None:
321321
self._duration_s = self._rng.uniform(0.1, 5.0)
322322

323-
# fill parameters for each signal component
324-
max_freq = self._sample_rate_hz / 4
323+
# if no components specified, randomly generate some
324+
if len(self._signal_components) == 0:
325+
while True:
326+
if self._rng.random() < 0.5:
327+
self._signal_components.append({"type": "tone"})
328+
else:
329+
self._signal_components.append({"type": "sweep"})
330+
if self._rng.random() <= 0.2:
331+
# E[N] = 1 / threshold -> 5 components on average
332+
break
325333

334+
# fill parameters for each signal component
326335
for component in self._signal_components:
327336
# add random timing for each component
328337
if "start_time_s" not in component:
@@ -342,34 +351,29 @@ def _fill_random_parameters(self) -> None:
342351

343352
if component["type"] == "tone":
344353
if "frequency_hz" not in component:
345-
# random frequency between 100hz and 1/4 sample rate
346-
component["frequency_hz"] = round(self._rng.uniform(100.0, max_freq), 1)
354+
# random frequency across full baseband: -nyquist to +nyquist (excluding DC ±100 Hz)
355+
nyquist = self._sample_rate_hz / 2
356+
freq = self._rng.uniform(-nyquist + 100.0, nyquist - 100.0)
357+
component["frequency_hz"] = round(freq, 1)
347358

348359
elif component["type"] == "sweep":
349360
if "start_frequency_hz" not in component:
350-
component["start_frequency_hz"] = round(self._rng.uniform(100.0, max_freq * 0.8), 1)
361+
component["start_frequency_hz"] = round(self._rng.uniform(100.0, self._sample_rate_hz / 4 * 0.8), 1)
351362
if "end_frequency_hz" not in component:
352363
start_freq = component["start_frequency_hz"]
353364
# ensure end freq is different from start
354-
if start_freq < max_freq * 0.5:
355-
component["end_frequency_hz"] = round(self._rng.uniform(start_freq * 1.5, max_freq), 1)
365+
if start_freq < self._sample_rate_hz / 4 * 0.5:
366+
component["end_frequency_hz"] = round(
367+
self._rng.uniform(start_freq * 1.5, self._sample_rate_hz / 4), 1
368+
)
356369
else:
357370
component["end_frequency_hz"] = round(self._rng.uniform(100.0, start_freq * 0.7), 1)
358371

359372
def _validate_parameters(self) -> None:
360373
"""Validate current parameters."""
361-
if len(self._signal_components) == 0:
362-
raise SigMFGeneratorError("no signal components specified - call tone() or sweep()")
363-
364-
if self._sample_rate_hz is None:
365-
raise SigMFGeneratorError("sample rate not specified")
366-
367374
if self._sample_rate_hz <= 0:
368375
raise SigMFGeneratorError(f"sample rate must be positive, got {self._sample_rate_hz}")
369376

370-
if self._duration_s is None:
371-
raise SigMFGeneratorError("duration not specified")
372-
373377
if self._duration_s <= 0:
374378
raise SigMFGeneratorError(f"duration must be positive, got {self._duration_s}")
375379

@@ -388,7 +392,7 @@ def _validate_parameters(self) -> None:
388392
]
389393

390394
for freq, freq_name in frequencies_to_check:
391-
if freq >= nyquist:
395+
if abs(freq) >= nyquist:
392396
raise SigMFGeneratorError(f"{freq_name} {freq} hz exceeds nyquist limit {nyquist} hz")
393397

394398
def _generate_samples(self) -> np.ndarray:
@@ -431,9 +435,6 @@ def _generate_samples(self) -> np.ndarray:
431435
phase = 2 * np.pi * (start_freq * component_time + 0.5 * freq_slope * component_time**2)
432436
component_signal = amplitude * np.exp(1j * phase)
433437

434-
else:
435-
raise SigMFGeneratorError(f"unknown signal type: {component['type']}")
436-
437438
# apply tapering to avoid clicks (5ms taper or 10% of component duration, whichever is smaller)
438439
taper_samples = min(int(0.005 * self._sample_rate_hz), component_samples // 10)
439440
if taper_samples > 1:

tests/test_generator.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def setUp(self):
2525
self.test_seed = 0xDEADBEEF
2626
self.test_sample_rate = 48000
2727
self.test_duration = 1.0
28-
self.test_freq = 1000.0
28+
self.test_freq = -1000.0
2929

3030
def test_deterministic_tone_generation(self):
3131
"""test deterministic tone generation with specified parameters"""
@@ -38,7 +38,7 @@ def test_deterministic_tone_generation(self):
3838
# verify metadata
3939
self.assertEqual(signal.sample_rate, self.test_sample_rate)
4040
self.assertEqual(signal.get_global_info()[SigMFFile.DATATYPE_KEY], "cf32_le")
41-
self.assertIn("1000.0 hz tone", signal.get_global_info()[SigMFFile.DESCRIPTION_KEY])
41+
self.assertIn("-1000.0 hz tone", signal.get_global_info()[SigMFFile.DESCRIPTION_KEY])
4242

4343
# verify signal characteristics
4444
samples = signal.read_samples()
@@ -52,8 +52,8 @@ def test_deterministic_tone_generation(self):
5252
fft_samples = np.fft.fft(samples)
5353
fft_freqs = np.fft.fftfreq(len(samples), 1 / self.test_sample_rate)
5454
dominant_freq_idx = np.argmax(np.abs(fft_samples))
55-
dominant_freq = abs(fft_freqs[dominant_freq_idx])
56-
self.assertAlmostEqual(dominant_freq, self.test_freq, delta=10) # within 10 hz
55+
dominant_freq = fft_freqs[dominant_freq_idx]
56+
self.assertAlmostEqual(dominant_freq, self.test_freq, delta=10) # within 10 hz, signed
5757

5858
def test_random_tone_generation(self):
5959
"""test random tone generation"""
@@ -92,7 +92,7 @@ def test_reproducible_random_generation(self):
9292

9393
def test_sweep_generation(self):
9494
"""test linear frequency sweep generation"""
95-
start_freq = 500.0
95+
start_freq = -500.0
9696
end_freq = 2000.0
9797

9898
gen = SigMFGenerator(seed=self.test_seed)
@@ -101,7 +101,7 @@ def test_sweep_generation(self):
101101
)
102102

103103
# verify metadata
104-
self.assertIn("500.0-2000.0 hz sweep", signal.get_global_info()[SigMFFile.DESCRIPTION_KEY])
104+
self.assertIn("-500.0-2000.0 hz sweep", signal.get_global_info()[SigMFFile.DESCRIPTION_KEY])
105105

106106
# verify signal properties
107107
samples = signal.read_samples()
@@ -187,13 +187,15 @@ def test_parameter_validation(self):
187187
"""test parameter validation and error handling"""
188188
gen = SigMFGenerator()
189189

190-
# should raise error if no signal type specified
191-
with self.assertRaises(SigMFGeneratorError):
192-
gen.generate()
190+
# bare generate() should now work (auto-generates components)
191+
signal = gen.generate()
192+
self.assertIsInstance(signal, SigMFFile)
193193

194-
# should raise error for tone frequency exceeding nyquist
194+
# should raise error for tone frequency exceeding nyquist (positive and negative)
195195
with self.assertRaises(SigMFGeneratorError):
196196
gen.tone(30000).sample_rate(48000).duration(1.0).generate()
197+
with self.assertRaises(SigMFGeneratorError):
198+
gen.tone(-30000).sample_rate(48000).duration(1.0).generate()
197199

198200
# should raise error for negative duration
199201
with self.assertRaises(SigMFGeneratorError):
@@ -207,12 +209,15 @@ def test_sweep_parameter_validation(self):
207209
"""test sweep-specific parameter validation"""
208210
gen = SigMFGenerator()
209211

210-
# sweep frequencies exceeding nyquist should raise error
212+
# sweep frequencies exceeding nyquist should raise error (positive and negative)
211213
with self.assertRaises(SigMFGeneratorError):
212214
gen.sweep(1000, 30000).sample_rate(48000).duration(1.0).generate()
213-
214215
with self.assertRaises(SigMFGeneratorError):
215216
gen.sweep(30000, 1000).sample_rate(48000).duration(1.0).generate()
217+
with self.assertRaises(SigMFGeneratorError):
218+
gen.sweep(1000, -30000).sample_rate(48000).duration(1.0).generate()
219+
with self.assertRaises(SigMFGeneratorError):
220+
gen.sweep(-30000, 1000).sample_rate(48000).duration(1.0).generate()
216221

217222
def test_random_parameters_reasonable(self):
218223
"""test that random parameters are within reasonable ranges"""
@@ -363,31 +368,31 @@ def test_automatic_annotations(self):
363368
self.assertIn("+200.0 Hz", offset_annotation[SigMFFile.LABEL_KEY])
364369

365370
def test_sweep_annotations(self):
366-
"""test sweep annotations have correct frequency bounds"""
371+
"""test sweep annotations have correct frequency bounds including negative"""
367372
gen = SigMFGenerator(seed=42)
368-
signal = gen.sweep(500, 2500).sample_rate(22050).duration(0.1).generate()
373+
signal = gen.sweep(-2500, 2500).sample_rate(22050).duration(0.1).generate()
369374

370375
annotations = signal.get_annotations()
371376
self.assertEqual(len(annotations), 1) # just main sweep annotation
372377

373378
sweep_annotation = annotations[0]
374-
self.assertEqual(sweep_annotation[SigMFFile.FLO_KEY], 500.0)
379+
self.assertEqual(sweep_annotation[SigMFFile.FLO_KEY], -2500.0)
375380
self.assertEqual(sweep_annotation[SigMFFile.FHI_KEY], 2500.0)
376-
self.assertIn("500.0-2500.0 Hz sweep", sweep_annotation[SigMFFile.LABEL_KEY])
381+
self.assertIn("-2500.0-2500.0 Hz sweep", sweep_annotation[SigMFFile.LABEL_KEY])
377382

378383
def test_reverse_sweep_annotations(self):
379-
"""test reverse sweep (high to low freq) has correct bounds"""
384+
"""test reverse sweep crossing DC has correct bounds"""
380385
gen = SigMFGenerator(seed=42)
381-
signal = gen.sweep(3000, 800).sample_rate(48000).duration(0.1).generate()
386+
signal = gen.sweep(3000, -800).sample_rate(48000).duration(0.1).generate()
382387

383388
annotations = signal.get_annotations()
384389
sweep_annotation = annotations[0]
385390

386391
# frequency bounds should be min/max regardless of sweep direction
387-
self.assertEqual(sweep_annotation[SigMFFile.FLO_KEY], 800.0)
392+
self.assertEqual(sweep_annotation[SigMFFile.FLO_KEY], -800.0)
388393
self.assertEqual(sweep_annotation[SigMFFile.FHI_KEY], 3000.0)
389394
# but label should show original order
390-
self.assertIn("3000.0-800.0 Hz sweep", sweep_annotation[SigMFFile.LABEL_KEY])
395+
self.assertIn("3000.0--800.0 Hz sweep", sweep_annotation[SigMFFile.LABEL_KEY])
391396

392397
def test_minimal_annotations(self):
393398
"""test that simple signals get minimal but complete annotations"""

0 commit comments

Comments
 (0)