new
This commit is contained in:
42
tests/test_cli.py
Normal file
42
tests/test_cli.py
Normal file
@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _run(*args: str) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
[sys.executable, *args],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
class CliTests(unittest.TestCase):
|
||||
def test_wrapper_help_works(self):
|
||||
proc = _run("RFG_ADC_dataplotter.py", "--help")
|
||||
self.assertEqual(proc.returncode, 0)
|
||||
self.assertIn("usage:", proc.stdout)
|
||||
self.assertIn("--peak_search", proc.stdout)
|
||||
|
||||
def test_module_help_works(self):
|
||||
proc = _run("-m", "rfg_adc_plotter.main", "--help")
|
||||
self.assertEqual(proc.returncode, 0)
|
||||
self.assertIn("usage:", proc.stdout)
|
||||
self.assertIn("--parser_16_bit_x2", proc.stdout)
|
||||
|
||||
def test_backend_mpl_reports_removal(self):
|
||||
proc = _run("-m", "rfg_adc_plotter.main", "/dev/null", "--backend", "mpl")
|
||||
self.assertNotEqual(proc.returncode, 0)
|
||||
self.assertIn("Matplotlib backend removed", proc.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
68
tests/test_processing.py
Normal file
68
tests/test_processing.py
Normal file
@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import unittest
|
||||
|
||||
from rfg_adc_plotter.processing.calibration import calibrate_freqs, recalculate_calibration_c
|
||||
from rfg_adc_plotter.processing.fft import compute_distance_axis, compute_fft_mag_row, compute_fft_row
|
||||
from rfg_adc_plotter.processing.normalization import build_calib_envelopes, normalize_by_calib
|
||||
from rfg_adc_plotter.processing.peaks import find_peak_width_markers, find_top_peaks_over_ref, rolling_median_ref
|
||||
|
||||
|
||||
class ProcessingTests(unittest.TestCase):
|
||||
def test_recalculate_calibration_preserves_requested_edges(self):
|
||||
coeffs = recalculate_calibration_c(np.asarray([0.0, 1.0, 0.025], dtype=np.float64), 3.3, 14.3)
|
||||
y0 = coeffs[0] + coeffs[1] * 3.3 + coeffs[2] * (3.3 ** 2)
|
||||
y1 = coeffs[0] + coeffs[1] * 14.3 + coeffs[2] * (14.3 ** 2)
|
||||
self.assertTrue(np.isclose(y0, 3.3))
|
||||
self.assertTrue(np.isclose(y1, 14.3))
|
||||
|
||||
def test_calibrate_freqs_returns_monotonic_axis_and_same_shape(self):
|
||||
sweep = {"F": np.linspace(3.3, 14.3, 32), "I": np.linspace(-1.0, 1.0, 32)}
|
||||
calibrated = calibrate_freqs(sweep)
|
||||
self.assertEqual(calibrated["F"].shape, (32,))
|
||||
self.assertEqual(calibrated["I"].shape, (32,))
|
||||
self.assertTrue(np.all(np.diff(calibrated["F"]) >= 0.0))
|
||||
|
||||
def test_normalizers_and_envelopes_return_finite_ranges(self):
|
||||
calib = (np.sin(np.linspace(0.0, 4.0 * np.pi, 64)) * 5.0).astype(np.float32)
|
||||
raw = calib * 0.75
|
||||
lower, upper = build_calib_envelopes(calib)
|
||||
self.assertEqual(lower.shape, calib.shape)
|
||||
self.assertEqual(upper.shape, calib.shape)
|
||||
self.assertTrue(np.all(lower <= upper))
|
||||
|
||||
simple = normalize_by_calib(raw, calib + 10.0, norm_type="simple")
|
||||
projector = normalize_by_calib(raw, calib, norm_type="projector")
|
||||
self.assertEqual(simple.shape, raw.shape)
|
||||
self.assertEqual(projector.shape, raw.shape)
|
||||
self.assertTrue(np.any(np.isfinite(simple)))
|
||||
self.assertTrue(np.any(np.isfinite(projector)))
|
||||
|
||||
def test_fft_helpers_return_expected_shapes(self):
|
||||
sweep = np.sin(np.linspace(0.0, 4.0 * np.pi, 128)).astype(np.float32)
|
||||
freqs = np.linspace(3.3, 14.3, 128, dtype=np.float64)
|
||||
mag = compute_fft_mag_row(sweep, freqs, 513)
|
||||
row = compute_fft_row(sweep, freqs, 513)
|
||||
axis = compute_distance_axis(freqs, 513)
|
||||
self.assertEqual(mag.shape, (513,))
|
||||
self.assertEqual(row.shape, (513,))
|
||||
self.assertEqual(axis.shape, (513,))
|
||||
self.assertTrue(np.all(np.diff(axis) >= 0.0))
|
||||
|
||||
def test_peak_helpers_find_reference_and_peak_boxes(self):
|
||||
xs = np.linspace(0.0, 10.0, 200)
|
||||
ys = np.exp(-((xs - 5.0) ** 2) / 0.4) * 10.0 + 1.0
|
||||
ref = rolling_median_ref(xs, ys, 2.0)
|
||||
peaks = find_top_peaks_over_ref(xs, ys, ref, top_n=3)
|
||||
width = find_peak_width_markers(xs, ys)
|
||||
self.assertEqual(ref.shape, ys.shape)
|
||||
self.assertEqual(len(peaks), 1)
|
||||
self.assertGreater(peaks[0]["x"], 4.0)
|
||||
self.assertLess(peaks[0]["x"], 6.0)
|
||||
self.assertIsNotNone(width)
|
||||
self.assertGreater(width["width"], 0.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
44
tests/test_ring_buffer.py
Normal file
44
tests/test_ring_buffer.py
Normal file
@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import unittest
|
||||
|
||||
from rfg_adc_plotter.state.ring_buffer import RingBuffer
|
||||
|
||||
|
||||
class RingBufferTests(unittest.TestCase):
|
||||
def test_ring_buffer_initializes_on_first_push(self):
|
||||
ring = RingBuffer(max_sweeps=4)
|
||||
sweep = np.linspace(-1.0, 1.0, 64, dtype=np.float32)
|
||||
ring.push(sweep, np.linspace(3.3, 14.3, 64))
|
||||
self.assertIsNotNone(ring.ring)
|
||||
self.assertIsNotNone(ring.ring_fft)
|
||||
self.assertIsNotNone(ring.ring_time)
|
||||
self.assertIsNotNone(ring.distance_axis)
|
||||
self.assertIsNotNone(ring.last_fft_db)
|
||||
self.assertEqual(ring.ring.shape[0], 4)
|
||||
self.assertEqual(ring.ring_fft.shape, (4, ring.fft_bins))
|
||||
|
||||
def test_ring_buffer_reallocates_when_sweep_width_grows(self):
|
||||
ring = RingBuffer(max_sweeps=3)
|
||||
ring.push(np.ones((32,), dtype=np.float32), np.linspace(3.3, 14.3, 32))
|
||||
first_width = ring.width
|
||||
ring.push(np.ones((2048,), dtype=np.float32), np.linspace(3.3, 14.3, 2048))
|
||||
self.assertGreater(ring.width, first_width)
|
||||
self.assertIsNotNone(ring.ring)
|
||||
self.assertEqual(ring.ring.shape, (3, ring.width))
|
||||
|
||||
def test_ring_buffer_tracks_latest_fft_and_display_arrays(self):
|
||||
ring = RingBuffer(max_sweeps=2)
|
||||
ring.push(np.linspace(0.0, 1.0, 64, dtype=np.float32), np.linspace(3.3, 14.3, 64))
|
||||
ring.push(np.linspace(1.0, 0.0, 64, dtype=np.float32), np.linspace(3.3, 14.3, 64))
|
||||
raw = ring.get_display_raw()
|
||||
fft = ring.get_display_fft_linear()
|
||||
self.assertEqual(raw.shape[1], 2)
|
||||
self.assertEqual(fft.shape[1], 2)
|
||||
self.assertIsNotNone(ring.last_fft_db)
|
||||
self.assertEqual(ring.last_fft_db.shape, (ring.fft_bins,))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
156
tests/test_sweep_parser_core.py
Normal file
156
tests/test_sweep_parser_core.py
Normal file
@ -0,0 +1,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import unittest
|
||||
|
||||
from rfg_adc_plotter.io.sweep_parser_core import (
|
||||
AsciiSweepParser,
|
||||
LegacyBinaryParser,
|
||||
LogScale16BitX2BinaryParser,
|
||||
LogScaleBinaryParser32,
|
||||
ParserTestStreamParser,
|
||||
PointEvent,
|
||||
StartEvent,
|
||||
SweepAssembler,
|
||||
log_pair_to_sweep,
|
||||
)
|
||||
|
||||
|
||||
def _u16le(word: int) -> bytes:
|
||||
w = int(word) & 0xFFFF
|
||||
return bytes((w & 0xFF, (w >> 8) & 0xFF))
|
||||
|
||||
|
||||
def _pack_legacy_start(ch: int) -> bytes:
|
||||
return b"\xff\xff" * 3 + bytes((0x0A, int(ch) & 0xFF))
|
||||
|
||||
|
||||
def _pack_legacy_point(ch: int, step: int, value_i32: int) -> bytes:
|
||||
value = int(value_i32) & 0xFFFF_FFFF
|
||||
return b"".join(
|
||||
[
|
||||
_u16le(step),
|
||||
_u16le((value >> 16) & 0xFFFF),
|
||||
_u16le(value & 0xFFFF),
|
||||
bytes((0x0A, int(ch) & 0xFF)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _pack_log_start(ch: int) -> bytes:
|
||||
return b"\xff\xff" * 5 + bytes((0x0A, int(ch) & 0xFF))
|
||||
|
||||
|
||||
def _pack_log_point(step: int, avg1: int, avg2: int, ch: int = 0) -> bytes:
|
||||
a1 = int(avg1) & 0xFFFF_FFFF
|
||||
a2 = int(avg2) & 0xFFFF_FFFF
|
||||
return b"".join(
|
||||
[
|
||||
_u16le(step),
|
||||
_u16le((a1 >> 16) & 0xFFFF),
|
||||
_u16le(a1 & 0xFFFF),
|
||||
_u16le((a2 >> 16) & 0xFFFF),
|
||||
_u16le(a2 & 0xFFFF),
|
||||
bytes((0x0A, int(ch) & 0xFF)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _pack_log16_start(ch: int) -> bytes:
|
||||
return b"\xff\xff" * 3 + bytes((0x0A, int(ch) & 0xFF))
|
||||
|
||||
|
||||
def _pack_log16_point(step: int, avg1: int, avg2: int) -> bytes:
|
||||
return b"".join(
|
||||
[
|
||||
_u16le(step),
|
||||
_u16le(avg1),
|
||||
_u16le(avg2),
|
||||
_u16le(0xFFFF),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SweepParserCoreTests(unittest.TestCase):
|
||||
def test_ascii_parser_emits_start_and_points(self):
|
||||
parser = AsciiSweepParser()
|
||||
events = parser.feed(b"Sweep_start\ns 1 2 -3\ns2 4 5\n")
|
||||
self.assertIsInstance(events[0], StartEvent)
|
||||
self.assertIsInstance(events[1], PointEvent)
|
||||
self.assertIsInstance(events[2], PointEvent)
|
||||
self.assertEqual(events[1].ch, 1)
|
||||
self.assertEqual(events[1].x, 2)
|
||||
self.assertEqual(events[1].y, -3.0)
|
||||
self.assertEqual(events[2].ch, 2)
|
||||
self.assertEqual(events[2].x, 4)
|
||||
self.assertEqual(events[2].y, 5.0)
|
||||
|
||||
def test_legacy_binary_parser_resynchronizes_after_garbage(self):
|
||||
parser = LegacyBinaryParser()
|
||||
stream = b"\x00junk" + _pack_legacy_start(3) + _pack_legacy_point(3, 1, -2)
|
||||
events = parser.feed(stream)
|
||||
self.assertIsInstance(events[0], StartEvent)
|
||||
self.assertEqual(events[0].ch, 3)
|
||||
self.assertIsInstance(events[1], PointEvent)
|
||||
self.assertEqual(events[1].ch, 3)
|
||||
self.assertEqual(events[1].x, 1)
|
||||
self.assertEqual(events[1].y, -2.0)
|
||||
|
||||
def test_logscale_32_parser_keeps_channel_and_aux_values(self):
|
||||
parser = LogScaleBinaryParser32()
|
||||
stream = _pack_log_start(5) + _pack_log_point(7, 1500, 700, ch=5)
|
||||
events = parser.feed(stream)
|
||||
self.assertIsInstance(events[0], StartEvent)
|
||||
self.assertEqual(events[0].ch, 5)
|
||||
self.assertIsInstance(events[1], PointEvent)
|
||||
self.assertEqual(events[1].ch, 5)
|
||||
self.assertEqual(events[1].x, 7)
|
||||
self.assertAlmostEqual(events[1].y, log_pair_to_sweep(1500, 700), places=6)
|
||||
self.assertEqual(events[1].aux, (1500.0, 700.0))
|
||||
|
||||
def test_logscale_16bit_parser_uses_last_start_channel(self):
|
||||
parser = LogScale16BitX2BinaryParser()
|
||||
stream = _pack_log16_start(2) + _pack_log16_point(1, 100, 90)
|
||||
events = parser.feed(stream)
|
||||
self.assertIsInstance(events[0], StartEvent)
|
||||
self.assertEqual(events[0].ch, 2)
|
||||
self.assertIsInstance(events[1], PointEvent)
|
||||
self.assertEqual(events[1].ch, 2)
|
||||
self.assertEqual(events[1].aux, (100.0, 90.0))
|
||||
|
||||
def test_parser_test_stream_parser_recovers_point_after_single_separator(self):
|
||||
parser = ParserTestStreamParser()
|
||||
stream = b"".join(
|
||||
[
|
||||
b"\xff\xff\xff\xff",
|
||||
bytes((0x0A, 4)),
|
||||
_u16le(1),
|
||||
_u16le(100),
|
||||
_u16le(90),
|
||||
_u16le(0xFFFF),
|
||||
]
|
||||
)
|
||||
events = parser.feed(stream)
|
||||
events.extend(parser.feed(_u16le(2)))
|
||||
self.assertIsInstance(events[0], StartEvent)
|
||||
self.assertEqual(events[0].ch, 4)
|
||||
self.assertIsInstance(events[1], PointEvent)
|
||||
self.assertEqual(events[1].ch, 4)
|
||||
self.assertEqual(events[1].x, 1)
|
||||
self.assertTrue(math.isfinite(events[1].y))
|
||||
|
||||
def test_sweep_assembler_builds_aux_curves_without_inversion(self):
|
||||
assembler = SweepAssembler(fancy=False, apply_inversion=False)
|
||||
self.assertIsNone(assembler.consume(StartEvent(ch=1)))
|
||||
assembler.consume(PointEvent(ch=1, x=1, y=10.0, aux=(100.0, 90.0)))
|
||||
assembler.consume(PointEvent(ch=1, x=2, y=20.0, aux=(110.0, 95.0)))
|
||||
sweep, info, aux = assembler.finalize_current()
|
||||
self.assertEqual(sweep.shape[0], 3)
|
||||
self.assertEqual(info["ch"], 1)
|
||||
self.assertIsNotNone(aux)
|
||||
self.assertEqual(aux[0][1], 100.0)
|
||||
self.assertEqual(aux[1][2], 95.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user