Files
micheleandCursor 6fc2ac583d 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>
2026-09-10 23:14:57 +02:00

145 lines
5.8 KiB
Python

"""Closed-form characteristic-impedance solvers.
Static (zero-frequency) Hammerstad-Jensen microstrip and Cohn/IPC-2141
stripline formulas, ported term-for-term from KiCad's own pcb_calculator
engine (common/transline_calculations/{microstrip,stripline}.cpp, verified
against the KiCad 10.0.4 source tag) with the frequency-dispersion, cover,
and conductor/dielectric-loss terms dropped — this tool needs the static Z0
for post-route verification, not a full RF loss/dispersion analysis.
Differential corrections use the separate, simpler IPC-2141A empirical
odd-mode formulas rather than KiCad's full coupled-line even/odd-mode solver.
All functions are pure: same inputs always give the same output, no state,
no I/O. Dimensions are millimetres, er is dimensionless, results are ohms.
"""
from __future__ import annotations
import math
_FREE_SPACE_IMPEDANCE_OHMS = 376.730313668 # NIST CODATA Z0
def _thickness_width_correction(u: float, t_h: float, er: float) -> float:
"""Hammerstad-Jensen effective-width correction for finite copper
thickness (delta_u in microstrip.cpp)."""
if t_h <= 0:
return 0.0
delta_u = (t_h / math.pi) * math.log(
1.0 + (4.0 * math.e) * math.tanh(math.sqrt(6.517 * u)) ** 2 / t_h
)
return 0.5 * delta_u * (1.0 + 1.0 / math.cosh(math.sqrt(er - 1.0)))
def _homogeneous_impedance_ohms(u: float) -> float:
"""Hammerstad's single-formula air-filled microstrip impedance for
shape ratio u = W/H, valid across the full range of u."""
shape = 6.0 + (2.0 * math.pi - 6.0) * math.exp(-((30.666 / u) ** 0.7528))
return (_FREE_SPACE_IMPEDANCE_OHMS / (2.0 * math.pi)) * math.log(
shape / u + math.sqrt(1.0 + 4.0 / (u * u))
)
def _filling_factor(u: float, er: float) -> float:
"""Hammerstad-Jensen dielectric filling factor q for shape ratio u."""
u2, u3, u4 = u * u, u**3, u**4
a = (
1.0
+ math.log((u4 + u2 / 2704.0) / (u4 + 0.432)) / 49.0
+ math.log(1.0 + u3 / 5929.741) / 18.7
)
b = 0.564 * ((er - 0.9) / (er + 3.0)) ** 0.053
return (1.0 + 10.0 / u) ** (-a * b)
def microstrip_z0(width_mm: float, height_mm: float, er: float, t_mm: float = 0.0) -> float:
"""Single-ended microstrip Z0 via Hammerstad-Jensen, static (f=0).
width_mm: trace width. height_mm: dielectric height to the reference
plane below the trace. er: dielectric relative permittivity. t_mm:
copper thickness (0 disables the thickness correction).
"""
u = width_mm / height_mm
t_h = t_mm / height_mm
u_er = u + _thickness_width_correction(u, t_h, er)
z0_dielectric = _homogeneous_impedance_ohms(u_er)
q = _filling_factor(u_er, er) - (2.0 * math.log(2.0) / math.pi) * (t_h / math.sqrt(u_er))
er_eff = 0.5 * (er + 1.0) + 0.5 * q * (er - 1.0)
return z0_dielectric / math.sqrt(er_eff)
def _stripline_line_impedance_ohms(
plane_spacing_mm: float, width_mm: float, t_mm: float, er: float
) -> float:
"""Cohn's stripline formula as used by KiCad's stripline.cpp, specialized
to the width-dominated (>=0.35) and narrow-trace regimes."""
hmt = plane_spacing_mm - t_mm
if width_mm / hmt >= 0.35:
wide = width_mm + (
2.0 * plane_spacing_mm * math.log((2.0 * plane_spacing_mm - t_mm) / hmt)
- t_mm * math.log(plane_spacing_mm**2 / hmt**2 - 1.0)
) / math.pi
return _FREE_SPACE_IMPEDANCE_OHMS * hmt / math.sqrt(er) / 4.0 / wide
ratio = t_mm / width_mm
if ratio > 1.0:
ratio = width_mm / t_mm
effective_diameter = (
1.0 + ratio / math.pi * (1.0 + math.log(4.0 * math.pi / ratio)) + 0.236 * ratio**1.65
)
effective_diameter *= (t_mm / 2.0) if (t_mm / width_mm) > 1.0 else (width_mm / 2.0)
return (
_FREE_SPACE_IMPEDANCE_OHMS
/ (2.0 * math.pi * math.sqrt(er))
* math.log(4.0 * plane_spacing_mm / math.pi / effective_diameter)
)
def stripline_z0(width_mm: float, b_mm: float, er: float, t_mm: float) -> float:
"""Symmetric (centered) stripline Z0, ported from KiCad's Cohn-derived
stripline.cpp, specialized to a trace centered between two reference
planes spaced b_mm apart.
t_mm must be > 0: the formula divides by hmt = b_mm - t_mm and takes a
log that is singular at t_mm == 0. Real copper always has finite
thickness, so callers must supply it (e.g. 0.035 mm for 1 oz copper).
"""
if t_mm <= 0:
raise ValueError("stripline_z0 requires t_mm > 0 (finite copper thickness)")
return _stripline_line_impedance_ohms(b_mm, width_mm, t_mm, er)
def diff_microstrip_z0(
width_mm: float, height_mm: float, spacing_mm: float, er: float, t_mm: float = 0.0
) -> float:
"""Edge-coupled differential microstrip Z0: IPC-2141A's empirical
odd-mode correction applied to the single-ended Hammerstad-Jensen value.
spacing_mm: edge-to-edge gap between the two traces of the pair.
"""
z0 = microstrip_z0(width_mm, height_mm, er, t_mm)
return 2.0 * z0 * (1.0 - 0.48 * math.exp(-0.96 * spacing_mm / height_mm))
def diff_stripline_z0(
width_mm: float, b_mm: float, spacing_mm: float, er: float, t_mm: float
) -> float:
"""Edge-coupled differential stripline Z0: IPC-2141A's empirical
odd-mode correction applied to the single-ended stripline value."""
z0 = stripline_z0(width_mm, b_mm, er, t_mm)
return 2.0 * z0 * (1.0 - 0.347 * math.exp(-2.9 * spacing_mm / b_mm))
def cpwg_z0(*_args, **_kwargs) -> float:
"""Grounded coplanar waveguide Z0 — not implemented yet.
CPWG needs the coplanar-ground-gap geometry in addition to the
reference-plane height, which isn't modeled by this solver set yet.
Raises explicitly so a CPWG classification surfaces as a clear
"not supported" flag (see geometry.classify_topology) instead of a
silently wrong number.
"""
raise NotImplementedError("CPWG closed-form solver is not implemented yet")