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:
@@ -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_]
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user