Integrate ImpedenceFinder closed-form Z0 into the project Impedance tab.
Use the vendored Hammerstad-Jensen/Cohn engine for microstrip, stripline, and coupled-diff advice. Skip OpenEMS/pcbnew and refuse CPWG rather than inventing a number. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
"""Pinscope facade over ImpedanceFinder's closed-form Z0 solver.
|
||||
|
||||
All Z0 numbers come from ImpedenceFinder (`vendor/impedancefinder`,
|
||||
Hammerstad-Jensen / Cohn as in KiCad pcb_calculator). This module only
|
||||
validates geometry, inverts width for a target Z, and exports KiCad
|
||||
custom-rule advice. It never emits Findings. CPWG is not implemented
|
||||
upstream — we raise instead of inventing a number.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from backend.vendor_path import ensure_impedancefinder
|
||||
|
||||
ensure_impedancefinder()
|
||||
from impedancefinder import zsolver
|
||||
|
||||
|
||||
class GeometryError(ValueError):
|
||||
"""Trace geometry is missing, non-physical, or unsupported."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TraceGeometry:
|
||||
h: float
|
||||
er: float
|
||||
t: float
|
||||
w: float | None = None
|
||||
s: float | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ImpedanceResult:
|
||||
kind: str
|
||||
w_mm: float | None = None
|
||||
s_mm: float | None = None
|
||||
z0: float | None = None
|
||||
zodd: float | None = None
|
||||
zeven: float | None = None
|
||||
zdiff: float | None = None
|
||||
formula: str = "impedancefinder"
|
||||
|
||||
|
||||
def _require_positive(name: str, value: float | None) -> float:
|
||||
if value is None or value <= 0:
|
||||
raise GeometryError(f"{name} must be > 0")
|
||||
return float(value)
|
||||
|
||||
|
||||
def microstrip_z0(geo: TraceGeometry) -> float:
|
||||
h = _require_positive("h", geo.h)
|
||||
er = _require_positive("er", geo.er)
|
||||
w = _require_positive("w", geo.w)
|
||||
t = geo.t
|
||||
if t < 0:
|
||||
raise GeometryError("t must be >= 0")
|
||||
return zsolver.microstrip_z0(w, h, er, t)
|
||||
|
||||
|
||||
def stripline_z0(geo: TraceGeometry) -> float:
|
||||
h = _require_positive("h", geo.h)
|
||||
er = _require_positive("er", geo.er)
|
||||
w = _require_positive("w", geo.w)
|
||||
t = _require_positive("t", geo.t)
|
||||
try:
|
||||
return zsolver.stripline_z0(w, h, er, t)
|
||||
except ValueError as exc:
|
||||
raise GeometryError(str(exc)) from exc
|
||||
|
||||
|
||||
def coupled_diff_z(geo: TraceGeometry) -> tuple[float, float, float]:
|
||||
"""Return (Zodd, Zeven, Zdiff) via ImpedanceFinder IPC-2141A odd-mode."""
|
||||
s = _require_positive("s", geo.s)
|
||||
h = _require_positive("h", geo.h)
|
||||
z0 = microstrip_z0(geo)
|
||||
zdiff = zsolver.diff_microstrip_z0(
|
||||
_require_positive("w", geo.w), h, s, geo.er, geo.t
|
||||
)
|
||||
zodd = zdiff / 2.0
|
||||
zeven = 2.0 * z0 - zodd
|
||||
return (zodd, zeven, zdiff)
|
||||
|
||||
|
||||
def cpw_z0(geo: TraceGeometry) -> float:
|
||||
_require_positive("h", geo.h)
|
||||
_require_positive("er", geo.er)
|
||||
_require_positive("w", geo.w)
|
||||
_require_positive("s", geo.s)
|
||||
try:
|
||||
return zsolver.cpwg_z0(geo.w, geo.h, geo.s, geo.er, geo.t)
|
||||
except NotImplementedError as exc:
|
||||
raise GeometryError(str(exc)) from exc
|
||||
|
||||
|
||||
def solve_width(
|
||||
kind: str,
|
||||
target_z: float,
|
||||
h: float,
|
||||
er: float,
|
||||
t: float,
|
||||
s: float | None = None,
|
||||
) -> float:
|
||||
_require_positive("target_z", target_z)
|
||||
_require_positive("h", h)
|
||||
_require_positive("er", er)
|
||||
if kind == "stripline":
|
||||
_require_positive("t", t)
|
||||
elif t < 0:
|
||||
raise GeometryError("t must be >= 0")
|
||||
|
||||
def z_of(w: float) -> float:
|
||||
geo = TraceGeometry(h=h, er=er, t=t, w=w, s=s)
|
||||
if kind == "microstrip":
|
||||
return microstrip_z0(geo)
|
||||
if kind == "stripline":
|
||||
return stripline_z0(geo)
|
||||
if kind == "diff":
|
||||
return coupled_diff_z(geo)[2]
|
||||
if kind == "cpw":
|
||||
return cpw_z0(geo)
|
||||
raise GeometryError(f"unknown kind {kind}")
|
||||
|
||||
lo, hi = 0.01 * h, 40.0 * h
|
||||
z_lo, z_hi = z_of(lo), z_of(hi)
|
||||
if not (min(z_lo, z_hi) <= target_z <= max(z_lo, z_hi)):
|
||||
raise GeometryError("target_z is outside the solvable width range")
|
||||
for _ in range(48):
|
||||
mid = 0.5 * (lo + hi)
|
||||
zm = z_of(mid)
|
||||
if zm > target_z:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
return 0.5 * (lo + hi)
|
||||
|
||||
|
||||
def stackup_targets(
|
||||
h: float,
|
||||
er: float,
|
||||
t: float,
|
||||
s: float,
|
||||
) -> dict[str, ImpedanceResult]:
|
||||
w50 = solve_width("microstrip", 50.0, h, er, t)
|
||||
w90 = solve_width("diff", 90.0, h, er, t, s=s)
|
||||
w100 = solve_width("diff", 100.0, h, er, t, s=s)
|
||||
z50 = microstrip_z0(TraceGeometry(h=h, er=er, t=t, w=w50))
|
||||
_, _, zd90 = coupled_diff_z(TraceGeometry(h=h, er=er, t=t, w=w90, s=s))
|
||||
_, _, zd100 = coupled_diff_z(TraceGeometry(h=h, er=er, t=t, w=w100, s=s))
|
||||
return {
|
||||
"microstrip_50": ImpedanceResult(kind="microstrip", w_mm=w50, z0=z50),
|
||||
"diff_90": ImpedanceResult(kind="diff", w_mm=w90, s_mm=s, zdiff=zd90),
|
||||
"diff_100": ImpedanceResult(kind="diff", w_mm=w100, s_mm=s, zdiff=zd100),
|
||||
}
|
||||
|
||||
|
||||
def export_kicad_dru(targets: dict[str, ImpedanceResult]) -> str:
|
||||
"""KiCad custom-rule advice. The user applies it; Pinscope does not DRC the PCB."""
|
||||
lines = [
|
||||
"(version 1)",
|
||||
"# Pinscope impedance advice (ImpedanceFinder solver) — apply in pcbnew.",
|
||||
]
|
||||
mapping = (
|
||||
("microstrip_50", "PINSCOPE_50OHM", "50Ohm"),
|
||||
("diff_90", "PINSCOPE_90OHM_USB", "90Ohm"),
|
||||
("diff_100", "PINSCOPE_100OHM_DIFF", "100Ohm"),
|
||||
)
|
||||
for key, rule, netclass in mapping:
|
||||
r = targets[key]
|
||||
w = r.w_mm
|
||||
if w is None:
|
||||
continue
|
||||
lines.append("")
|
||||
lines.append(f"(rule {rule}")
|
||||
lines.append(f' (constraint track_width (min {w:.4f}mm) (opt {w:.4f}mm) (max {w:.4f}mm))')
|
||||
if r.s_mm:
|
||||
lines.append(
|
||||
f" (constraint diff_pair_gap (min {r.s_mm:.4f}mm) "
|
||||
f"(opt {r.s_mm:.4f}mm) (max {r.s_mm:.4f}mm))"
|
||||
)
|
||||
lines.append(f' (condition "A.NetClass == \'{netclass}\'"))')
|
||||
return "\n".join(lines) + "\n"
|
||||
Reference in New Issue
Block a user