feat(visualization): extract ring_boundaries to radial_metrics

- add radial_metrics: ring_boundaries + bucket_by_ring
- replace WaferMapItem inline logic with ring_boundaries()
- fix outer boundary: sensor max-hypot*1.05 (was wafer_radius_mm)
- add LicenseModel.reviewAccessBlocked() (ADR-0005 deferred)
- add appendActivityLog test
- add test_radial_metrics.py (89 lines, full edge-case coverage)
- update README: licmgr --level flag + date format
This commit is contained in:
jack
2026-07-13 10:48:49 -07:00
parent 67158ed7ab
commit 0e6e819851
3 changed files with 129 additions and 16 deletions
@@ -0,0 +1,38 @@
from __future__ import annotations
import math
from pygui.backend.wafer.zwafer_models import Sensor
def ring_boundaries(sensors: list[Sensor]) -> list[float]:
"""Distinct radial distances of sensor groups, sorted ascending, plus the outer boundary."""
if not sensors:
r = 150.0
return [r * f for f in (0.25, 0.50, 0.75, 1.0)]
# Cluster radii that are within 2 mm of each other into one ring; skip center point.
radii = sorted(r for r in {math.hypot(s.x, s.y) for s in sensors} if r > 1.0)
groups: list[float] = []
for r in radii:
if not groups or r - groups[-1] > 2.0:
groups.append(r)
else:
groups[-1] = (groups[-1] + r) / 2 # merge close values
# Always include the outer boundary ring so the wafer circle is drawn.
outer = max(math.hypot(s.x, s.y) for s in sensors) * 1.05
if not groups or outer - groups[-1] > 2.0:
groups.append(outer)
return groups
def bucket_by_ring(sensors: list[Sensor], values: list[float]) -> dict[int, list[float]]:
"""Group each sensor's value under the index of its nearest ring boundary."""
boundaries = ring_boundaries(sensors)
result: dict[int, list[float]] = {i: [] for i in range(len(boundaries))}
for i, s in enumerate(sensors):
if i >= len(values):
continue
r = math.hypot(s.x, s.y)
idx = min(range(len(boundaries)), key=lambda j: abs(boundaries[j] - r))
result[idx].append(values[i])
return result
@@ -28,6 +28,7 @@ from PySide6.QtQml import QmlElement
from PySide6.QtQuick import QQuickPaintedItem
from pygui.backend.models.frame_stats import compute_stats
from pygui.backend.visualization.radial_metrics import ring_boundaries
from pygui.backend.visualization.rbf_heatmap import (
ellipse_alpha,
interpolate_field,
@@ -517,22 +518,7 @@ class WaferMapItem(QQuickPaintedItem):
def _sensor_ring_radii_mm(self) -> list[float]:
"""Distinct radial distances of sensor groups, sorted ascending, plus the outer boundary."""
if not self._sensors:
r = self._wafer_radius_mm()
return [r * f for f in (0.25, 0.50, 0.75, 1.0)]
# Cluster radii that are within 2 mm of each other into one ring; skip center point.
radii = sorted(r for r in {math.hypot(s.x, s.y) for s in self._sensors} if r > 1.0)
groups: list[float] = []
for r in radii:
if not groups or r - groups[-1] > 2.0:
groups.append(r)
else:
groups[-1] = (groups[-1] + r) / 2 # merge close values
# Always include the outer boundary ring so the wafer circle is drawn.
outer = self._wafer_radius_mm()
if not groups or outer - groups[-1] > 2.0:
groups.append(outer)
return groups
return ring_boundaries(self._sensors)
def _scale(self, ds: int, r_mm: float) -> float:
"""Pixels per mm. The wafer radius maps to ds//2 - 24 px."""