Fix adaptive barrel refinement and ambiguous decimal-comma inputs

- adaptive: barrel links could attach to coarse leaves (up to 1 mm),
  making the whole leaf equipotential and deleting the local spreading
  resistance - via-field results read up to ~13% low. Barrel attachment
  cells are now pinned into the keep-fine set before the quadtree is
  built; the guard ring grades around them.
- skin/dialog: the decimal-comma rewrite turned '1,500' into 1.5, a
  silent 1000x error in frequency, test current or cell size. Ambiguous
  comma patterns (thousands separators, multiple commas, mixed with a
  dot) now raise with a message; a real decimal comma ('1,5') still
  parses.
- dialog: Selection.adaptive default now matches the documented
  on-by-default; the adaptive accuracy claim is aligned to the measured
  0.03% quoted in README and config.
This commit is contained in:
2026-07-17 13:23:55 +07:00
parent 8bc3c50872
commit f59ada94e0
3 changed files with 34 additions and 9 deletions
+8 -1
View File
@@ -94,6 +94,7 @@ def run_solve_adaptive(problem: Problem, stack: RasterStack,
# --- leaves per layer ------------------------------------------------- # --- leaves per layer -------------------------------------------------
t0 = time.perf_counter() t0 = time.perf_counter()
links, dead_barrels = sv._barrel_links(stack, problem)
keep = e1 | e2 keep = e1 | e2
if stack.chain is not None: if stack.chain is not None:
keep |= stack.chain keep |= stack.chain
@@ -101,6 +102,13 @@ def run_solve_adaptive(problem: Problem, stack: RasterStack,
keep |= stack.buildup keep |= stack.buildup
if stack.thick_scale is not None: if stack.thick_scale is not None:
keep |= stack.thick_scale != 1.0 keep |= stack.thick_scale != 1.0
# pin every barrel attachment cell fine: a point-like barrel
# injection into a coarse leaf makes the whole leaf equipotential
# and deletes the local spreading resistance (via fields read up
# to ~13% low otherwise); the guard ring then grades around it
for _vi, la, ia_, ja_, lb, ib_, jb_, _r in links:
keep[la, ia_, ja_] = True
keep[lb, ib_, jb_] = True
mb = _max_block(stack.h_nm) mb = _max_block(stack.h_nm)
grids = [quadtree.build_leaves(stack.masks[li], keep_fine=keep[li], grids = [quadtree.build_leaves(stack.masks[li], keep_fine=keep[li],
max_block=mb, max_block=mb,
@@ -181,7 +189,6 @@ def run_solve_adaptive(problem: Problem, stack: RasterStack,
xx.append(np.full(k, -1, dtype=np.int8)) xx.append(np.full(k, -1, dtype=np.int8))
ee.append(np.full(k, -1, dtype=np.int16)) ee.append(np.full(k, -1, dtype=np.int16))
links, dead_barrels = sv._barrel_links(stack, problem)
for vi, la, ia_, ja_, lb, ib_, jb_, r_dc in links: for vi, la, ia_, ja_, lb, ib_, jb_, r_dc in links:
na = offs[la] + grids[la].id_grid[ia_, ja_] na = offs[la] + grids[la].id_grid[ia_, ja_]
nb = offs[lb] + grids[lb].id_grid[ib_, jb_] nb = offs[lb] + grids[lb].id_grid[ib_, jb_]
+8 -5
View File
@@ -38,7 +38,7 @@ class Selection:
include_tracks: bool = True include_tracks: bool = True
vias_capped: bool = True vias_capped: bool = True
cap_max_drill_mm: float = 0.5 cap_max_drill_mm: float = 0.5
adaptive: bool = False adaptive: bool = True
class _Dialog(QDialog): class _Dialog(QDialog):
@@ -81,7 +81,7 @@ class _Dialog(QDialog):
self.adaptive_check = QCheckBox( self.adaptive_check = QCheckBox(
"adaptive cells (coarsen plane interiors; faster on large " "adaptive cells (coarsen plane interiors; faster on large "
"boards, corrected to ≲0.1 % of the uniform grid)") "boards, corrected to ≲0.03 % of the uniform grid)")
self.adaptive_check.setChecked(config.ADAPTIVE_CELLS) self.adaptive_check.setChecked(config.ADAPTIVE_CELLS)
form.addRow("Grid:", self.adaptive_check) form.addRow("Grid:", self.adaptive_check)
@@ -187,10 +187,13 @@ class _Dialog(QDialog):
raise ValueError("Check at least one layer.") raise ValueError("Check at least one layer.")
def number(edit: QLineEdit, name: str) -> float: def number(edit: QLineEdit, name: str) -> float:
text = edit.text().strip()
try: try:
return float(edit.text().strip().replace(",", ".")) return float(skin.normalize_decimal(text))
except ValueError: except ValueError as exc:
raise ValueError(f"{name}: '{edit.text()}' is not a number.") if "separator" in str(exc):
raise ValueError(f"{name}: {exc}")
raise ValueError(f"{name}: '{text}' is not a number.")
current = number(self.current_edit, "Test current") current = number(self.current_edit, "Test current")
if current <= 0: if current <= 0:
+18 -3
View File
@@ -21,6 +21,7 @@ from __future__ import annotations
import cmath import cmath
import math import math
import re
MU0 = 4e-7 * math.pi MU0 = 4e-7 * math.pi
@@ -58,11 +59,25 @@ def resistance_factor(thickness_m: float, freq_hz: float,
/ (rho_ohm_m / thickness_m)) / (rho_ohm_m / thickness_m))
def normalize_decimal(text: str) -> str:
"""Accept a European decimal comma ('1,5' -> '1.5'); reject
thousands-separator commas ('1,500' would silently become 1.5,
a 1000x error that propagates unnoticed into the result)."""
if "," in text:
if "." in text or text.count(",") > 1 \
or re.search(r",\d{3}(?=\D|$)", text):
raise ValueError(
f"ambiguous comma in '{text}': use '.' as the decimal "
"separator and no thousands separators")
text = text.replace(",", ".")
return text
def parse_frequency(text: str) -> float: def parse_frequency(text: str) -> float:
"""'0', '100k', '1.5M', '142500' -> Hz; empty -> 0 (DC). """'0', '100k', '1.5M', '142500' -> Hz; empty -> 0 (DC).
Raises ValueError on unparseable or negative input (a typo silently Raises ValueError on unparseable, ambiguous or negative input (a
becoming DC would mislabel the result).""" typo silently becoming DC would mislabel the result)."""
t = text.strip().lower().replace(",", ".").removesuffix("hz").strip() t = normalize_decimal(text.strip().lower()).removesuffix("hz").strip()
if not t: if not t:
return 0.0 return 0.0
mult = 1.0 mult = 1.0