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>
145 lines
5.8 KiB
Python
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")
|