Refactor code structure for improved readability and maintainability
This commit is contained in:
Vendored
+9
@@ -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
|
||||
|
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user