Skip to content

Commit c188e6a

Browse files
Kyle A LogueTeque5
authored andcommitted
basic tests for signalhound without external repo
1 parent 07cd050 commit c188e6a

File tree

3 files changed

+115
-18
lines changed

3 files changed

+115
-18
lines changed

sigmf/convert/signalhound.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,9 @@ def _build_metadata(xml_path: Path) -> Tuple[dict, dict, list, int]:
203203
# build hardware description with available information
204204
hw_parts = []
205205
if device_type:
206-
hw_parts.append(f"Recorded with {device_type}")
206+
hw_parts.append(f"{device_type}")
207207
else:
208-
hw_parts.append("Recorded with Signal Hound Device")
208+
hw_parts.append("Signal Hound Device")
209209

210210
if serial_number:
211211
hw_parts.append(f"S/N: {serial_number}")

tests/test_convert_signalhound.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,107 @@
1919
from .testdata import get_nonsigmf_path, validate_ncd
2020

2121

22+
class TestSignalHoundConverter(unittest.TestCase):
23+
"""Create a realistic Signal Hound XML/IQ file pair and test conversion methods."""
24+
25+
def setUp(self) -> None:
26+
"""Create temp XML/IQ file pair with tone for testing."""
27+
self.tmp_dir = tempfile.TemporaryDirectory()
28+
self.tmp_path = Path(self.tmp_dir.name)
29+
self.iq_path = self.tmp_path / "test.iq"
30+
self.xml_path = self.tmp_path / "test.xml"
31+
32+
# Generate complex IQ test data
33+
self.samp_rate = 48000
34+
self.center_freq = 915e6
35+
duration_s = 0.1
36+
num_samples = int(self.samp_rate * duration_s)
37+
ttt = np.linspace(0, duration_s, num_samples, endpoint=False)
38+
freq = 440 # A4 note
39+
self.iq_data = 0.5 * np.exp(2j * np.pi * freq * ttt) # complex128, normalized to [-0.5, 0.5]
40+
41+
# Convert complex IQ data to interleaved int16 format (ci16_le - Signal Hound "Complex Short")
42+
scale = 2**15 # int16 range is -32768 to 32767
43+
ci_real = (self.iq_data.real * scale).astype(np.int16)
44+
ci_imag = (self.iq_data.imag * scale).astype(np.int16)
45+
iq_interleaved = np.empty((len(self.iq_data) * 2,), dtype=np.int16)
46+
iq_interleaved[0::2] = ci_real
47+
iq_interleaved[1::2] = ci_imag
48+
49+
# Write IQ file as raw interleaved int16
50+
with open(self.iq_path, "wb") as iq_file:
51+
iq_file.write(iq_interleaved.tobytes())
52+
53+
# Write minimal XML metadata file
54+
with open(self.xml_path, "w") as xml_file:
55+
xml_file.write(
56+
f'<?xml version="1.0" encoding="UTF-8"?>\n'
57+
f'<SignalHoundIQFile Version="1.0">\n'
58+
f" <CenterFrequency>{self.center_freq}</CenterFrequency>\n"
59+
f" <SampleRate>{self.samp_rate}</SampleRate>\n"
60+
f" <DataType>Complex Short</DataType>\n"
61+
f" <IQFileName>{self.iq_path.name}</IQFileName>\n"
62+
f"</SignalHoundIQFile>\n"
63+
)
64+
65+
def tearDown(self) -> None:
66+
"""Clean up temporary directory."""
67+
self.tmp_dir.cleanup()
68+
69+
def _verify(self, meta: sigmf.SigMFFile) -> None:
70+
"""Verify metadata fields and data integrity."""
71+
self.assertIsInstance(meta, sigmf.SigMFFile)
72+
self.assertEqual(meta.get_global_field("core:datatype"), "ci16_le")
73+
self.assertEqual(meta.get_global_field("core:sample_rate"), self.samp_rate)
74+
# center frequency is in capture metadata
75+
self.assertEqual(meta.get_captures()[0]["core:frequency"], self.center_freq)
76+
# verify data
77+
data = meta.read_samples()
78+
self.assertGreater(len(data), 0, "Should read some samples")
79+
# allow numerical differences due to int16 quantization
80+
self.assertTrue(np.allclose(self.iq_data, data, atol=1e-4))
81+
82+
def test_signalhound_to_sigmf_pair(self):
83+
"""Test standard Signal Hound to SigMF conversion with file pairs."""
84+
sigmf_path = self.tmp_path / "converted"
85+
meta = signalhound_to_sigmf(signalhound_path=self.xml_path, out_path=sigmf_path)
86+
filenames = sigmf.sigmffile.get_sigmf_filenames(sigmf_path)
87+
self.assertTrue(filenames["data_fn"].exists(), "dataset path missing")
88+
self.assertTrue(filenames["meta_fn"].exists(), "metadata path missing")
89+
self._verify(meta)
90+
91+
# test overwrite protection
92+
with self.assertRaises(sigmf.error.SigMFFileError) as context:
93+
signalhound_to_sigmf(signalhound_path=self.xml_path, out_path=sigmf_path, overwrite=False)
94+
self.assertIn("already exists", str(context.exception))
95+
96+
# test overwrite works
97+
meta2 = signalhound_to_sigmf(signalhound_path=self.xml_path, out_path=sigmf_path, overwrite=True)
98+
self.assertIsInstance(meta2, sigmf.SigMFFile)
99+
100+
def test_signalhound_to_sigmf_archive(self):
101+
"""Test Signal Hound to SigMF conversion with archive output."""
102+
sigmf_path = self.tmp_path / "converted_archive"
103+
meta = signalhound_to_sigmf(signalhound_path=self.xml_path, out_path=sigmf_path, create_archive=True)
104+
filenames = sigmf.sigmffile.get_sigmf_filenames(sigmf_path)
105+
self.assertTrue(filenames["archive_fn"].exists(), "archive path missing")
106+
self._verify(meta)
107+
108+
# test overwrite protection
109+
with self.assertRaises(sigmf.error.SigMFFileError) as context:
110+
signalhound_to_sigmf(
111+
signalhound_path=self.xml_path, out_path=sigmf_path, create_archive=True, overwrite=False
112+
)
113+
self.assertIn("already exists", str(context.exception))
114+
115+
def test_signalhound_to_sigmf_ncd(self):
116+
"""Test Signal Hound to SigMF conversion as Non-Conforming Dataset."""
117+
meta = signalhound_to_sigmf(signalhound_path=self.xml_path, create_ncd=True)
118+
target_path = self.iq_path
119+
validate_ncd(self, meta, target_path)
120+
self._verify(meta)
121+
122+
22123
class TestSignalHoundWithNonSigMFRepo(unittest.TestCase):
23124
"""Test Signal Hound converter with real example files if available."""
24125

tests/test_convert_wav.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,23 @@ def tearDown(self) -> None:
4646
"""clean up temporary directory"""
4747
self.tmp_dir.cleanup()
4848

49+
def _verify(self, meta: sigmf.SigMFFile) -> None:
50+
"""Verify metadata fields and data integrity."""
51+
self.assertIsInstance(meta, sigmf.SigMFFile)
52+
# verify data
53+
data = meta.read_samples()
54+
self.assertGreater(len(data), 0, "Should read some samples")
55+
# allow numerical differences due to PCM quantization
56+
self.assertTrue(np.allclose(self.audio_data, data, atol=1e-4))
57+
4958
def test_wav_to_sigmf_pair(self) -> None:
5059
"""test standard wav to sigmf conversion with file pairs"""
5160
sigmf_path = self.tmp_path / "bar"
5261
meta = wav_to_sigmf(wav_path=self.wav_path, out_path=sigmf_path)
5362
filenames = sigmf.sigmffile.get_sigmf_filenames(sigmf_path)
5463
self.assertTrue(filenames["data_fn"].exists(), "dataset path missing")
5564
self.assertTrue(filenames["meta_fn"].exists(), "metadata path missing")
56-
# verify data
57-
data = meta.read_samples()
58-
self.assertGreater(len(data), 0, "Should read some samples")
59-
# allow numerical differences due to PCM quantization
60-
self.assertTrue(np.allclose(self.audio_data, data, atol=1e-4))
65+
self._verify(meta)
6166

6267
# test overwrite protection
6368
with self.assertRaises(sigmf.error.SigMFFileError) as context:
@@ -74,11 +79,7 @@ def test_wav_to_sigmf_archive(self) -> None:
7479
meta = wav_to_sigmf(wav_path=self.wav_path, out_path=sigmf_path, create_archive=True)
7580
filenames = sigmf.sigmffile.get_sigmf_filenames(sigmf_path)
7681
self.assertTrue(filenames["archive_fn"].exists(), "archive path missing")
77-
# verify data
78-
data = meta.read_samples()
79-
self.assertGreater(len(data), 0, "Should read some samples")
80-
# allow numerical differences due to PCM quantization
81-
self.assertTrue(np.allclose(self.audio_data, data, atol=1e-4))
82+
self._verify(meta)
8283

8384
# test overwrite protection
8485
with self.assertRaises(sigmf.error.SigMFFileError) as context:
@@ -93,12 +94,7 @@ def test_wav_to_sigmf_ncd(self) -> None:
9394
"""test wav to sigmf conversion as Non-Conforming Dataset"""
9495
meta = wav_to_sigmf(wav_path=self.wav_path, create_ncd=True)
9596
validate_ncd(self, meta, self.wav_path)
96-
97-
# verify data
98-
data = meta.read_samples()
99-
# allow numerical differences due to PCM quantization
100-
self.assertGreater(len(data), 0, "Should read some samples")
101-
self.assertTrue(np.allclose(self.audio_data, data, atol=1e-4))
97+
self._verify(meta)
10298

10399
# test overwrite protection when creating NCD with output path
104100
sigmf_path = self.tmp_path / "ncd_test"

0 commit comments

Comments
 (0)