"""Portable processing contract: no Qt or application imports.""" import csv import io import math import unittest from dataclasses import replace from set_devices.plot_processing import Axis, Series, Snapshot, prepare, process, write_csv from set_devices.signal_reconstruction import METHODS class PlotProcessingTests(unittest.TestCase): def test_snapshot_copies_mutable_samples_and_excludes_hidden_and_digital(self): points = [[0, 0], [1, 1]] channel = Series("a", "Analog", points) snapshot = Snapshot([channel, Series("hidden", "Hidden", points, visible=False), Series("bit", "Bit", points, discrete=True)]) points[0][1] = 9 points.append([2, 3]) self.assertEqual(((0., 0.), (1., 1.)), prepare(snapshot, "a").series.points) self.assertEqual((channel,), snapshot.analogs) self.assertIsNone(prepare(snapshot, "hidden")) self.assertIsNone(prepare(snapshot, "bit")) self.assertIsNone(prepare(replace(snapshot, blocked_reason="Pause capture"), "a")) with self.assertRaises(ValueError): Snapshot([channel, channel]) def test_all_methods_use_same_contract_and_keep_source(self): source = Series("v", "Voltage", [(100, 5), (300, 9)], y_unit="V") snapshot = Snapshot([source], Axis("Time", "ms"), "scope") for method in METHODS: with self.subTest(method=method): request = prepare(snapshot, "v", method, 5, 1) curve = process(request) self.assertEqual([100, 150, 200, 250, 300], [x for x, y in curve.points]) for (_, value), expected in zip(curve.points, [5, 6, 7, 8, 9]): self.assertAlmostEqual(expected, value) self.assertEqual((2, 2), (curve.input_count, curve.unique_count)) self.assertEqual(request, curve.request) self.assertEqual(((100., 5.), (300., 9.)), source.points) def test_window_is_inclusive_and_never_extrapolates(self): snapshot = Snapshot([Series("a", "A", [(0, 0), (1, 2), (2, 4), (3, 6)])], x_range=(.5, 2)) curve = process(prepare(snapshot, "a", "linear", 3)) self.assertEqual(((1., 2.), (1.5, 3.), (2., 4.)), curve.points) self.assertEqual(2, curve.input_count) with self.assertRaises(ValueError): process(prepare(replace(snapshot, x_range=(.5, .9)), "a")) def test_sparse_spline_and_noisy_polynomial_work_in_shared_pipeline(self): sparse = Snapshot([Series("a", "Sine", [(i * math.pi / 4, math.sin(i * math.pi / 4)) for i in range(9)])]) curve = process(prepare(sparse, "a", "spline", 201)) self.assertLess(max(abs(y - math.sin(x)) for x, y in curve.points), .002) noisy = Snapshot([Series("a", "Ramp", [(i, 2 * i + (1 if i % 2 else -1)) for i in range(11)])]) curve = process(prepare(noisy, "a", "polynomial", 21, 1)) self.assertLess(max(abs(y - 2 * x) for x, y in curve.points), .1) self.assertGreater(curve.rmse, .9) def test_csv_respects_explicit_axis_domain_and_units(self): for axis, start, expected in ((Axis("Time", "ms"), 1000, "1000.0"), (Axis("Frequency", "Hz"), 1.8e12, "1800000000000.0"), (Axis("Time", "ms", "unix_ms"), 1000, "1970-01-01T00:00:01.000000Z")): with self.subTest(axis=axis): snapshot = Snapshot([Series("v", "Voltage", [(start, 0), (start + 1000, 1)], y_unit="V")], axis) output = io.StringIO() write_csv(process(prepare(snapshot, "v", "linear", 3)), output) rows = list(csv.reader(io.StringIO(output.getvalue()))) self.assertEqual(expected, rows[1][0]) self.assertTrue(rows[0][1].endswith("[V]")) self.assertEqual("timestamp" if axis.encoding == "unix_ms" else f"{axis.label} [{axis.unit}]", rows[0][0]) def test_request_tracks_selected_channel_units_source_and_window(self): channel = Series("a", "A", [(0, 0), (1, 1)]) other = Series("b", "B", [(0, 3), (1, 4)]) snapshot = Snapshot([channel, other], source="file1") request = prepare(snapshot, "a") self.assertEqual(request, prepare(replace(snapshot, series=[channel, replace(other, points=[(0, 9)])]), "a")) for changed in (replace(snapshot, source="file2"), replace(snapshot, axis=Axis("Frequency", "Hz")), replace(snapshot, x_range=(0, 1)), replace(snapshot, series=[replace(channel, y_unit="V")])): self.assertNotEqual(request, prepare(changed, "a")) if __name__ == "__main__": unittest.main()