Refactor code structure for improved readability and maintainability

This commit is contained in:
jack
2026-06-11 11:57:46 -07:00
parent 97ca58bfc2
commit b9f8032203
16 changed files with 512 additions and 7 deletions
+9
View File
@@ -0,0 +1,9 @@
Wafer ID=A12
Acquisition Date=03/26/2025
Label,1,2,3
X (mm),0,10,-10
Y (mm),10,-10,-10
data
0.0,149.0,148.5,150.6
0.5,149.2,148.4,150.7
1.0,149.1,148.6,150.5
1 Wafer ID=A12
2 Acquisition Date=03/26/2025
3 Label,1,2,3
4 X (mm),0,10,-10
5 Y (mm),10,-10,-10
6 data
7 0.0,149.0,148.5,150.6
8 0.5,149.2,148.4,150.7
9 1.0,149.1,148.6,150.5
+40
View File
@@ -0,0 +1,40 @@
from pygui.backend.csv_recorder import CsvRecorder
from pygui.backend.data_records import read_data_records
from pygui.backend.frame import Frame
from pygui.backend.zwafer_models import Sensor
from pygui.backend.zwafer_parser import ZWaferParser
def test_recording_roundtrips_through_parser(tmp_path):
sensors = [Sensor("1", 0.0, 1.0), Sensor("2", 1.0, 0.0)]
path = tmp_path / "rec.csv"
rec = CsvRecorder()
rec.start(str(path), sensors, serial="A12")
rec.write(Frame(seq=0, time=0.0, values=[149.0, 148.0]))
rec.write(Frame(seq=1, time=0.5, values=[149.5, 148.5]))
rec.stop()
assert path.exists()
data, _ = ZWaferParser().parse(str(path))
assert data is not None
assert[s.label for s in data.sensors] == ["1", "2"]
assert len(data.csv_headers) == 2
def test_recorded_data_rows_are_readable(tmp_path):
sensors = [Sensor("1", 0.0, 1.0), Sensor("2", 1.0, 0.0)]
path = tmp_path / "rec.csv"
rec = CsvRecorder()
rec.start(str(path), sensors, serial="A12")
rec.write(Frame(seq=0, time=0.0, values=[149.0, 148.0]))
rec.write(Frame(seq=1, time=0.5, values=[149.5, 148.5]))
rec.stop()
records = read_data_records(str(path))
assert len(records) == 2
assert records[0].time == 0.0
assert records[0].values == [149.0, 148.0]
assert records[1].time == 0.5
assert records[1].values == [149.5, 148.5]
+19
View File
@@ -0,0 +1,19 @@
"""Tests for src/pygui/backend/data_records.py."""
from pygui.backend.data_records import read_data_records
def test_reads_rows_after_data_marker(tmp_path):
p = tmp_path / "s.csv"
p.write_text(
"Wafer ID=A12\n"
"Label,1,2\nX (mm),0,10\nY (mm),10,-10\n"
"data\n"
"0.0,149.0,148.5\n"
"0.5,149.2,148.4\n",
encoding="utf-8",
)
recs = read_data_records(str(p))
assert len(recs) == 2
assert recs[0].time == 0.0
assert recs[0].values == [149.0, 148.5]
assert recs[1].values == [149.2, 148.4]
+30
View File
@@ -0,0 +1,30 @@
from pygui.backend.frame import Frame
from pygui.backend.frame_player import FramePlayer
def frames(n):
return [Frame(seq=i, time=float(i), values=[149.0 + i])for i in range(n)]
def test_loads_and_reports_total():
p = FramePlayer(); p.load(frames(3))
assert p.total == 3 and p.index == 0
current = p.current()
assert current is not None
assert current.values == [149.0]
def test_step_forward_and_back_clamps():
p = FramePlayer(); p.load(frames(3))
assert p.step(1).index == 1
assert p.step(1).index == 2
assert p.step(1).index == 2
assert p.step(-5).index == 0
def test_seek():
p = FramePlayer(); p.load(frames(5))
assert p.seek(3).index == 3
assert p.seek(99).index == 4
def test_at_end():
p = FramePlayer(); p.load(frames(2))
assert not p.at_end
p.seek(1)
assert p.at_end
+29
View File
@@ -0,0 +1,29 @@
"""Tests for src/pygui/backend/rbf_heatmap.py."""
import numpy as np
from pygui.backend.rbf_heatmap import interpolate_field, BACKEND
def test_field_shape_and_interpolates_at_sites():
xs = np.array([0.0, 10.0, -10.0, 0.0])
ys = np.array([10.0, -10.0, -10.0, 0.0])
vs = np.array([149.0, 150.0, 148.0, 149.5])
field = interpolate_field(xs, ys, vs, width=64, height=64,
extent=(-15, 15, -15, 15))
assert field.shape == (64, 64)
assert np.isfinite(field).all()
assert field.min() <= 150.0 and field.max() >= 148.0
def test_round_clip_produces_nans_outside_circle():
xs = np.array([0.0, 5.0, -5.0])
ys = np.array([5.0, -5.0, 0.0])
vs = np.array([149.0, 150.0, 148.0])
field = interpolate_field(xs, ys, vs, width=64, height=64,
extent=(-15, 15, -15, 15), round_clip=True)
assert np.isnan(field).any()
# center pixel must be finite
assert np.isfinite(field[32, 32])
def test_backend_reported():
assert BACKEND in ("cupy", "numpy")
+49
View File
@@ -0,0 +1,49 @@
"""Tests for src/pygui/backend/sensor_editor.py."""
import pytest
from pygui.backend.sensor_editor import SensorEditor
def test_replace_overrides_raw_value():
ed = SensorEditor()
ed.set_replacement(2, 150.0) # index 2 forced to 150.0 regardless of raw
assert ed.apply([149.0, 149.0, 99.0]) == [149.0, 149.0, 150.0]
def test_offset_shifts_raw_value():
ed = SensorEditor()
ed.set_offset(0, +0.5)
assert ed.apply([149.0, 149.0]) == [149.5, 149.0]
def test_replace_and_offset_apply_in_order():
"""Replacement wins over raw, then offset is added on top."""
ed = SensorEditor()
ed.set_replacement(2, 150.0)
ed.set_offset(2, +1.0) # sensor 2: replace 99→150, then +1 → 151
ed.set_offset(0, +0.5)
out = ed.apply([149.0, 149.0, 99.0, 149.0])
assert out == [149.5, 149.0, 151.0, 149.0]
def test_clear_single_index_restores_only_that_sensor():
ed = SensorEditor()
ed.set_replacement(0, 0.0)
ed.set_replacement(1, 0.0)
ed.clear(0) # clear only index 0
assert ed.apply([149.0, 149.0]) == [149.0, 0.0]
def test_clear_restores_all():
ed = SensorEditor()
ed.set_offset(0, 5.0)
ed.set_replacement(1, 200.0)
ed.clear()
assert ed.apply([149.0, 149.0]) == [149.0, 149.0]
def test_no_overrides_returns_copy():
ed = SensorEditor()
original = [149.0, 149.0]
result = ed.apply(original)
assert result == original
assert result is not original
+28
View File
@@ -0,0 +1,28 @@
from pygui.backend.frame import Frame
from pygui.backend.threshold_classifier import ThresholdConfig, BAND_HIGH, BAND_IN, BAND_LOW
from pygui.backend.session_model import SessionModel, SessionUpdate
def test_manual_banding_and_stats():
model = SessionModel(ThresholdConfig(set_point=149.0, margin=1.0, auto=False))
update = model.process(Frame(seq=7, time=1.0, values=[151.0, 149.0, 147.0]))
assert update.seq == 7
assert update.bands == [BAND_HIGH, BAND_IN, BAND_LOW]
assert update.stats.avg == 149.0
assert (update.target, update.margin) == (149.0, 1.0)
assert update.state in ("idle", "ramp", "set")
def test_auto_banding_uses_mean_and_sigma():
model = SessionModel(ThresholdConfig(auto=True))
# Tight cluster around 149 -> small σ -> the 200.0 outlier reads HIGH
update = model.process(Frame(seq=0, time=0.0, values=[149.0, 149.0, 149.0, 200.0]))
assert update.bands[3] == BAND_HIGH
assert update.target == update.stats.avg
def test_set_thresholds_reband_on_next_frame():
m = SessionModel(ThresholdConfig(set_point=149.0, margin=1.0, auto=False))
m.set_thresholds(ThresholdConfig(set_point=149.0, margin=5.0, auto=False))
upd = m.process(Frame(seq=0, time=0.0, values=[151.0]))
assert upd.bands == [BAND_IN]
+28
View File
@@ -0,0 +1,28 @@
import time
from pygui.backend.frame import Frame
from pygui.serialcomm.stream_reader import StreamReader
class FakeTransport:
"""Yield canned line then blocks (returns b'')."""
def __init__(self, lines): self._lines = list(lines); self.closed = False
def readline(self):
time.sleep(0.001)
return self._lines.pop(0) if self._lines else b""
def close(self): self.closed = True
def parse_line(raw: str, seq: int) -> Frame:
parts = [float(x) for x in raw.split(",")]
return Frame(seq = seq, time = parts[0], values = parts[1:])
def test_reads_frames_and_counts_errors():
got, errors = [], []
transport = FakeTransport([b"0.0,149,148\n", b"garbage\n", b"0.5,150,149\n"])
r = StreamReader(transport, parse_line,
on_frame = got.append, on_error = lambda e: errors.append(e))
r.start()
time.sleep(0.1)
r.stop()
assert [f.values for f in got] == [[149.0, 148.0], [150.0, 149.0]]
assert r.error_count == 1
assert transport.closed
+21
View File
@@ -0,0 +1,21 @@
"""Tests for src/pygui/backend/wafer_layouts.py."""
import pytest
from pygui.backend.wafer_layouts import load_layout, available_families
from pygui.backend.zwafer_models import Sensor
def test_lists_bundled_families():
fams = available_families()
assert "aep" in fams and "x" in fams
def test_loads_sensors_in_mm():
sensors = load_layout("aep")
assert sensors and all(isinstance(s, Sensor) for s in sensors)
assert any(s.x < 0 for s in sensors)
assert max(abs(s.x) for s in sensors) < 200.0
def test_unknown_family_raises():
with pytest.raises(KeyError):
load_layout("nope")