diff --git a/fill_resistance/adaptive.py b/fill_resistance/adaptive.py index 799b407..22d1cfd 100644 --- a/fill_resistance/adaptive.py +++ b/fill_resistance/adaptive.py @@ -94,6 +94,7 @@ def run_solve_adaptive(problem: Problem, stack: RasterStack, # --- leaves per layer ------------------------------------------------- t0 = time.perf_counter() + links, dead_barrels = sv._barrel_links(stack, problem) keep = e1 | e2 if stack.chain is not None: keep |= stack.chain @@ -101,6 +102,13 @@ def run_solve_adaptive(problem: Problem, stack: RasterStack, keep |= stack.buildup if stack.thick_scale is not None: 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) grids = [quadtree.build_leaves(stack.masks[li], keep_fine=keep[li], max_block=mb, @@ -181,7 +189,6 @@ def run_solve_adaptive(problem: Problem, stack: RasterStack, xx.append(np.full(k, -1, dtype=np.int8)) 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: na = offs[la] + grids[la].id_grid[ia_, ja_] nb = offs[lb] + grids[lb].id_grid[ib_, jb_] diff --git a/fill_resistance/dialog.py b/fill_resistance/dialog.py index 81a9f37..bb23a27 100644 --- a/fill_resistance/dialog.py +++ b/fill_resistance/dialog.py @@ -38,7 +38,7 @@ class Selection: include_tracks: bool = True vias_capped: bool = True cap_max_drill_mm: float = 0.5 - adaptive: bool = False + adaptive: bool = True class _Dialog(QDialog): @@ -81,7 +81,7 @@ class _Dialog(QDialog): self.adaptive_check = QCheckBox( "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) form.addRow("Grid:", self.adaptive_check) @@ -187,10 +187,13 @@ class _Dialog(QDialog): raise ValueError("Check at least one layer.") def number(edit: QLineEdit, name: str) -> float: + text = edit.text().strip() try: - return float(edit.text().strip().replace(",", ".")) - except ValueError: - raise ValueError(f"{name}: '{edit.text()}' is not a number.") + return float(skin.normalize_decimal(text)) + except ValueError as exc: + 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") if current <= 0: diff --git a/fill_resistance/skin.py b/fill_resistance/skin.py index f3b8618..fb83434 100644 --- a/fill_resistance/skin.py +++ b/fill_resistance/skin.py @@ -21,6 +21,7 @@ from __future__ import annotations import cmath import math +import re MU0 = 4e-7 * math.pi @@ -58,11 +59,25 @@ def resistance_factor(thickness_m: float, freq_hz: float, / (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: """'0', '100k', '1.5M', '142500' -> Hz; empty -> 0 (DC). - Raises ValueError on unparseable or negative input (a typo silently - becoming DC would mislabel the result).""" - t = text.strip().lower().replace(",", ".").removesuffix("hz").strip() + Raises ValueError on unparseable, ambiguous or negative input (a + typo silently becoming DC would mislabel the result).""" + t = normalize_decimal(text.strip().lower()).removesuffix("hz").strip() if not t: return 0.0 mult = 1.0