feat(map): add edge-center delta stats, thickness overlay, and layout fix

- Add edge-center sensor pair tables and delta calculations per wafer family.
- Parse and interpolate wafer thickness CSV data for offscreen visualization.
- Refactor ReadoutPanel layout using Item + anchors to fix right-margin clipping.
- Compact E-C Delta text format to fit standard sidebar width.
- Add pytest suite covering delta calculations, layout pairs, and thickness CSV.
This commit is contained in:
jack
2026-07-10 17:32:12 -07:00
parent 25fa7507ce
commit 94f917b116
12 changed files with 518 additions and 63 deletions
+38
View File
@@ -14,6 +14,44 @@ class Stats:
sigma: float; three_sigma: float
@dataclass(frozen=True)
class EdgeCenterStats:
min_edge_index: int; min_center_index: int
min_edge: float; min_center: float; min_delta: float
max_edge_index: int; max_center_index: int
max_edge: float; max_center: float; max_delta: float
def compute_edge_center(
values: list[float],
pairs: tuple[tuple[int, int], ...],
excluded: set[int],
) -> EdgeCenterStats | None:
"""Min/max |edge - center| over the family's pair table.
Pairs with an excluded (replaced) sensor, an index outside the frame, or a
NaN value are skipped. Returns None when no pair is computable.
"""
n = len(values)
best: list[tuple[float, int, int]] = []
for e, c in pairs:
if e in excluded or c in excluded or e >= n or c >= n:
continue
if math.isnan(values[e]) or math.isnan(values[c]):
continue
best.append((abs(values[e] - values[c]), e, c))
if not best:
return None
min_d, min_e, min_c = min(best)
max_d, max_e, max_c = max(best)
return EdgeCenterStats(
min_edge_index=min_e, min_center_index=min_c,
min_edge=values[min_e], min_center=values[min_c], min_delta=min_d,
max_edge_index=max_e, max_center_index=max_c,
max_edge=values[max_e], max_center=values[max_c], max_delta=max_d,
)
def compute_stats(values: list[float]) -> Stats:
clean = [(i, v) for i, v in enumerate(values) if not math.isnan(v)]
if not clean:
@@ -28,6 +28,11 @@ class SensorEditor:
def has_overrides(self) -> bool:
return bool(self._replacements or self._offsets)
def replaced_indices(self) -> set[int]:
"""Sensors whose value is a replacement (offsets don't count — an
offset sensor still reads from its physical location)."""
return set(self._replacements)
def active_indices(self) -> list[int]:
"""Return sorted sensor indices that have any override."""
return sorted(set(self._replacements) | set(self._offsets))