Source code for reasitic.inductance.grover

"""Closed-form partial-inductance formulas of F. W. Grover.

These are the same expressions that the binary evaluates in
``grover_segment_self_inductance``, ``mutual_inductance_4corner_grover``,
and ``coupled_wire_self_inductance_grover`` (decompiled output:
``decomp/output/asitic_kernel.c`` lines 5650, 4010, 19). The
binary computes everything in extended precision using x87 ``f2xm1``
/ ``fscale`` / ``fpatan`` intrinsics. We just call the libm
functions; ``float`` is IEEE-754 binary64 which is sufficient for
all but the most aggressive cancellation paths (Grover's are not
those).

Conventions:

* Lengths are passed in **microns**.
* The closed forms internally convert to **cm** (multiplying by
  ``1e-4``) because Grover's tables are tabulated in those units.
* Returns inductance in **nH**.

Reference:
    F. W. Grover, *Inductance Calculations: Working Formulas and
    Tables*, Dover, 1946. Sections on parallel filaments and
    rectangular bars (Tables 24, 25, formulas 25.4, 7.16).

The Greenhouse formulation used by ASITIC computes the total
inductance of a planar coil as the sum of self-inductances of each
straight segment plus all pairwise mutual inductances. This module
provides the per-segment primitives; the summation lives in
``inductance.partial``.
"""

from __future__ import annotations

import math

from reasitic.units import UM_TO_CM


[docs] def rectangular_bar_self_inductance( length_um: float, width_um: float, thickness_um: float ) -> float: """Self-inductance of a rectangular conductor bar. .. math:: L = 2\\ell\\,\\Bigl[ \\ln\\!\\bigl(\\tfrac{2\\ell}{W+T}\\bigr) + 0.50049 + \\frac{W+T}{3\\ell} \\Bigr] where ``ℓ``, ``W``, ``T`` are in cm and the result is in nH. The constant ``0.50049`` is Grover's value for the GMD correction of a thin rectangular cross-section. This is the exact formula used by the original ASITIC binary in ``cmd_inductance_compute`` (decomp ``asitic_repl.c:1417``). """ if length_um <= 1e-6: return 0.0 L = length_um * UM_TO_CM wt = (width_um + thickness_um) * UM_TO_CM if wt <= 0: return 0.0 return 2.0 * L * ( math.log(2.0 * L / wt) + 0.50049 + wt / (3.0 * L) )
[docs] def segment_self_inductance(length_um: float, radius_um: float) -> float: """Self-inductance of a single straight round wire (Grover §7.4). .. math:: L = 2 \\ell\\, \\Bigl[ \\sinh^{-1}(\\ell/r) + r/\\ell - \\sqrt{1 + (r/\\ell)^2} \\Bigr] where ``ℓ`` is length in cm, ``r`` is the equivalent round-wire radius in cm, and the result is in nH. Mirrors the decompiled ``grover_segment_self_inductance`` at ``0x08064308``. Example: >>> from reasitic.inductance import segment_self_inductance >>> round(segment_self_inductance(100.0, 0.5), 4) 0.0999 """ if length_um < 1e-6: return 0.0 L = length_um * UM_TO_CM r = max(radius_um * UM_TO_CM, 1e-12) z = L / r inv_z = 1.0 / z asinh_z = math.log(z + math.sqrt(z * z + 1.0)) return 2.0 * L * (asinh_z + inv_z - math.sqrt(1.0 + inv_z * inv_z))
[docs] def coupled_wire_self_inductance( width_um: float, thickness_um: float, separation_um: float ) -> float: """Self-inductance of a rectangular bar of width ``width_um``, thickness ``thickness_um``, with a parallel-bar separation of ``separation_um`` (the latter affects the GMD via the proximity correction). Mirrors the decompiled ``coupled_wire_self_inductance_grover`` at ``0x0804cb90`` (Grover Table 24). Parameters and result are in microns / nH. """ if width_um <= 0 or thickness_um <= 0: return 0.0 # Convert all lengths to cm w = width_um * UM_TO_CM h = thickness_um * UM_TO_CM s = max(separation_um * UM_TO_CM, 1e-12 * w) a = w / h b = s / h a2 = a * a a3 = a2 * a b2 = b * b # Auxiliary terms R1 = math.sqrt(a2 + 1.0) R2 = math.sqrt(b2 + 1.0) R3 = math.sqrt(a2 + b2) R4 = math.sqrt(b2 + 1.0 + a2) # The Grover §24 formula (recovered from the decomp): a single # composite expression in ln/atan terms. We build it piecewise # to track the original structure. LN2 = math.log(2.0) L_a = LN2 * ((R4 + 1.0) / R3) L_b = LN2 * ((b + R4) / R1) L_c = LN2 * ((a + R4) / R2) L_60 = a * 60.0 A1 = math.atan2(b, a * R4) A2 = math.atan2(a, b * R4) A3 = math.atan2(a * b, R4) # The original kernel groups this in nested doubles; group it # into a clearer sum here. All coefficients are taken verbatim # from the decompiled output. term1 = a3 * (LN2 * ((b + R3) / a) - L_b) / (b * 24.0) term2 = a3 * (LN2 * ((R1 + 1.0) / a) - L_a) / (b2 * 24.0) term3 = a * (R3 - R4) / 20.0 term4 = (1.0 - R2) / (b2 * 60.0 * a) term5 = a * (R1 - R4) / (b2 * 20.0) term6 = (LN2 * (a + R1) - L_c) / (b2 * 24.0) term7 = L_c * 0.25 term8 = (a * L_b) / (4.0 * b) term9 = a * L_a * 0.25 term10 = (R2 - R4) / (a * 20.0) term11 = (b2 * (b - R2)) / L_60 term12 = (b2 * (LN2 * ((a + R3) / b) - L_c)) / 24.0 term13 = (LN2 * (b + R2) - L_b) / ((a * b) * 24.0) term14 = (LN2 * ((R2 + 1.0) / b) - L_a) * b2 / (a * 24.0) term15 = (R4 - R3) * b2 / (a * 60.0) term16 = -(A1 * a2) / (b * 6.0) term17 = -(b * A2) / 6.0 term18 = -A3 / (b * 6.0) term19 = (R4 - R1) / (b2 * L_60) term20 = ((a - R3) + (R4 - R1)) * a3 / (b2 * 60.0) inner = ( term1 + term2 + term3 + term4 + term5 + term6 + term7 + term8 + term9 + term10 + term11 + term12 + term13 + term14 + term15 + term16 + term17 + term18 + term19 + term20 ) # Final scaling: 8 * w in cm gives result in 0.1 nH·cm? No — # Grover's tables in this layout return microhenries; to convert # to nH multiply by 1000. The decompiled code returns # ``result * 8.0 * width`` directly — width is in cm via the # 1e-4 scaling so the result is in nH already. return inner * 8.0 * w * 1000.0
[docs] def perpendicular_segment_mutual( length1_um: float, length2_um: float, *, common_distance_um: float = 0.0, ) -> float: """Mutual inductance between two perpendicular straight filaments. Two filaments meeting at right angles (e.g. adjacent legs of a square spiral): segment 1 along the x-axis on ``[0, L1]``, segment 2 along the y-axis on ``[0, L2]``, sharing the origin if ``common_distance_um`` is zero, otherwise displaced along the common axis by that distance. For *filamentary* perpendicular wires Maxwell's formula reduces to **0** because ``ds₁ · ds₂ = 0``. The binary's ``mutual_inductance_orthogonal_segments`` (``asitic_kernel.c:0x08061b84``) is a closed-form for finite- width *bars* — it captures the fringe / GMD contribution that appears at corners. For axis-aligned 2-D spirals this term is typically <1 % of the total inductance; we return zero here as the standard Greenhouse approximation. Callers that need the corner correction can substitute their own kernel. """ return 0.0
[docs] def mohan_modified_wheeler( *, n_turns: float, d_outer_um: float, d_inner_um: float, shape: str = "square", ) -> float: """Mohan 1999 modified-Wheeler closed-form L estimate. Example: >>> from reasitic.inductance import mohan_modified_wheeler >>> round(mohan_modified_wheeler( ... n_turns=5, d_outer_um=200, d_inner_um=100, ... ), 2) 5.75 >>> mohan_modified_wheeler( ... n_turns=5, d_outer_um=50, d_inner_um=100, ... ) 0.0 .. math:: L_\\text{mw} = K_1 \\mu_0 n^2 d_\\text{avg} / (1 + K_2 \\rho) where ``ρ = (d_out − d_in) / (d_out + d_in)``, ``d_avg = (d_out + d_in) / 2``, and ``(K_1, K_2)`` come from Mohan 1999 Table 1: * ``square``: K_1 = 2.34, K_2 = 2.75 * ``hexagonal``: K_1 = 2.33, K_2 = 3.82 * ``octagonal``: K_1 = 2.25, K_2 = 3.55 * ``circular``: K_1 = 2.40, K_2 = 1.75 Returns L in **nH**. Fast first-order estimate, useful for sanity-checking the Greenhouse summation. """ coeffs = { "square": (2.34, 2.75), "hexagonal": (2.33, 3.82), "octagonal": (2.25, 3.55), "circular": (2.40, 1.75), } if shape not in coeffs: raise ValueError( f"unknown shape {shape!r}; choose from {list(coeffs)}" ) K1, K2 = coeffs[shape] if d_outer_um <= 0 or d_outer_um <= d_inner_um: return 0.0 rho = (d_outer_um - d_inner_um) / (d_outer_um + d_inner_um) d_avg_m = 0.5 * (d_outer_um + d_inner_um) * 1.0e-6 mu_0 = 4.0e-7 * math.pi L_h = K1 * mu_0 * n_turns * n_turns * d_avg_m / (1.0 + K2 * rho) return L_h * 1.0e9 # H → nH
[docs] def hoer_love_perpendicular_mutual( *, L1_um: float, L2_um: float, a_um: float, b_um: float, c_um: float, ) -> float: """Hoer-Love mutual-inductance integral for perpendicular bars. Two filaments meeting at a corner: filament 1 from ``(0,0,0)`` to ``(L1,0,0)``, filament 2 from ``(a, b, c)`` to ``(a, b+L2, c)``. For perpendicular *filaments* the dot product is zero so M=0; for *finite-width bars* the proximity integral .. math:: M_\\perp = \\frac{\\mu_0}{4\\pi}\\int\\int \\frac{(\\hat{x}\\cdot\\hat{y}) \\,dx\\,dy} {\\sqrt{(x-a)^2 + (y-b)^2 + c^2}} is identically zero (the dot-product vanishes element-wise). Hoer & Love's 1965 result captures the same vanishing for arbitrary 3-D orientations. We provide this function so the dispatch surface in :func:`reasitic.inductance.partial._segment_pair_mutual` can call it without conditional logic; it always returns 0. The non-zero "corner" contribution that the binary's ``mutual_inductance_orthogonal_segments`` reports is actually a *self-inductance* artifact — the L_self of the bend itself, folded into a per-pair lookup. Callers wanting that should add a corner-correction term to the diagonal of the partial-L matrix, not the off-diagonal. """ return 0.0
[docs] def parallel_segment_mutual( length1_um: float, length2_um: float, sep_um: float, offset_um: float = 0.0, ) -> float: """Mutual inductance between two parallel filamentary segments. Both segments lie along the same axis. With segment 1 occupying ``[0, L1]`` along the axis and segment 2 occupying ``[offset, offset + L2]``, separated by perpendicular distance ``d``, the closed form is .. math:: M = \\tfrac{\\mu_0}{4\\pi}\\, \\bigl[\\phi(L_1-o) - \\phi(-o) - \\phi(L_1-o-L_2) + \\phi(-o-L_2)\\bigr] where ``φ(t) = t·asinh(t/d) − √(t² + d²)`` is the antiderivative of the double-integral kernel. ``φ`` is even, so absolute values are used. The prefactor ``μ₀/(4π)`` evaluates to **1 nH/cm**, hence with all lengths in cm the result is in nH directly. For two equal-length parallel filaments with no axial offset this reduces to the canonical Greenhouse Eq. 8:: M = 2L·[asinh(L/d) − √(1 + (d/L)²) + d/L] (nH) """ if length1_um <= 0 or length2_um <= 0: return 0.0 L1 = length1_um * UM_TO_CM L2 = length2_um * UM_TO_CM d = max(abs(sep_um) * UM_TO_CM, 1e-12) o = offset_um * UM_TO_CM def phi(t: float) -> float: t = abs(t) return t * math.asinh(t / d) - math.sqrt(t * t + d * d) # F1 = [0, L1], F2 = [o, o + L2] return phi(L1 - o) - phi(-o) - phi(L1 - o - L2) + phi(-o - L2)