Skip to main content

pcb_toolkit/
inductor.rs

1//! Planar spiral inductor calculator (Mohan/Wheeler modified).
2//!
3//! Reference: Mohan, Hershenson, Boyd, Lee — "Simple Accurate Expressions
4//! for Planar Spiral Inductances", IEEE JSSC, October 1999.
5//!
6//! Supports square, hexagonal, octagonal, and circular geometries.
7
8use serde::{Deserialize, Serialize};
9
10use crate::CalcError;
11
12/// Permeability of free space (H/m).
13const MU_0: f64 = 4.0 * std::f64::consts::PI * 1e-7;
14
15/// Mils to meters conversion factor.
16const MILS_TO_METERS: f64 = 25.4e-6;
17
18/// Spiral geometry shape.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub enum SpiralShape {
21    Square,
22    Hexagonal,
23    Octagonal,
24    Circle,
25}
26
27impl SpiralShape {
28    /// Mohan coefficients (K1, K2) for each geometry.
29    fn coefficients(self) -> (f64, f64) {
30        match self {
31            Self::Square => (2.34, 2.75),
32            Self::Hexagonal => (2.33, 3.82),
33            Self::Octagonal => (2.25, 3.55),
34            Self::Circle => (2.23, 3.45),
35        }
36    }
37}
38
39/// Result of a planar spiral inductor calculation.
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct InductorResult {
42    /// Inner diameter of the spiral in mils.
43    pub din_mils: f64,
44    /// Fill factor ρ = (dout − din) / (dout + din).
45    pub rho: f64,
46    /// Average diameter d_avg = (dout + din) / 2, in mils.
47    pub d_avg_mils: f64,
48    /// Calculated inductance in nanohenries.
49    pub inductance_nh: f64,
50}
51
52/// Calculate planar spiral inductor using the modified Wheeler / Mohan formula.
53///
54/// # Arguments
55/// - `n_turns` — number of turns (must be ≥ 1)
56/// - `width_mils` — trace width in mils (must be > 0)
57/// - `spacing_mils` — inter-turn spacing in mils (must be > 0)
58/// - `dout_mils` — outer diameter in mils (must be > 0)
59/// - `shape` — spiral geometry
60///
61/// The inner diameter is derived as:
62/// `din = dout − 2×n×(w+s) + 2×s`
63///
64/// # Errors
65/// Returns [`CalcError::OutOfRange`] or [`CalcError::NegativeDimension`] if inputs are invalid.
66pub fn planar_spiral(
67    n_turns: u32,
68    width_mils: f64,
69    spacing_mils: f64,
70    dout_mils: f64,
71    shape: SpiralShape,
72) -> Result<InductorResult, CalcError> {
73    if n_turns == 0 {
74        return Err(CalcError::OutOfRange {
75            name: "n_turns",
76            value: n_turns as f64,
77            expected: ">= 1",
78        });
79    }
80    if width_mils <= 0.0 {
81        return Err(CalcError::NegativeDimension {
82            name: "width_mils",
83            value: width_mils,
84        });
85    }
86    if spacing_mils <= 0.0 {
87        return Err(CalcError::NegativeDimension {
88            name: "spacing_mils",
89            value: spacing_mils,
90        });
91    }
92    if dout_mils <= 0.0 {
93        return Err(CalcError::NegativeDimension {
94            name: "dout_mils",
95            value: dout_mils,
96        });
97    }
98
99    let n = n_turns as f64;
100    let din_mils = dout_mils - 2.0 * n * (width_mils + spacing_mils) + 2.0 * spacing_mils;
101
102    if din_mils <= 0.0 {
103        return Err(CalcError::OutOfRange {
104            name: "din_mils (derived)",
105            value: din_mils,
106            expected: "> 0 — reduce n_turns, width, or spacing, or increase dout",
107        });
108    }
109
110    let rho = (dout_mils - din_mils) / (dout_mils + din_mils);
111    let d_avg_mils = (dout_mils + din_mils) / 2.0;
112    let d_avg_m = d_avg_mils * MILS_TO_METERS;
113
114    let (k1, k2) = shape.coefficients();
115    let inductance_h = k1 * MU_0 * n * n * d_avg_m / (1.0 + k2 * rho);
116    let inductance_nh = inductance_h * 1e9;
117
118    Ok(InductorResult {
119        din_mils,
120        rho,
121        d_avg_mils,
122        inductance_nh,
123    })
124}
125
126#[cfg(test)]
127mod tests {
128    use approx::assert_relative_eq;
129
130    use super::*;
131
132    // Saturn PDF page 30: n=5, w=10 mils, s=10 mils, dout=350 mils, Square
133    // din=170, ρ=0.3462, L=248.5936 nH
134    #[test]
135    fn saturn_page30_square() {
136        let result = planar_spiral(5, 10.0, 10.0, 350.0, SpiralShape::Square).unwrap();
137        assert_relative_eq!(result.din_mils, 170.0, epsilon = 1e-10);
138        assert_relative_eq!(result.rho, 0.34615, epsilon = 1e-4);
139        assert_relative_eq!(result.inductance_nh, 248.59, epsilon = 0.2);
140    }
141
142    #[test]
143    fn derived_din_matches_spec() {
144        // din = 350 - 2×5×(10+10) + 2×10 = 350 - 200 + 20 = 170
145        let result = planar_spiral(5, 10.0, 10.0, 350.0, SpiralShape::Square).unwrap();
146        assert_relative_eq!(result.din_mils, 170.0, epsilon = 1e-10);
147    }
148
149    #[test]
150    fn error_on_zero_turns() {
151        assert!(planar_spiral(0, 10.0, 10.0, 350.0, SpiralShape::Square).is_err());
152    }
153
154    #[test]
155    fn error_on_din_negative() {
156        // Very large n will make din negative
157        assert!(planar_spiral(50, 10.0, 10.0, 350.0, SpiralShape::Square).is_err());
158    }
159
160    #[test]
161    fn hexagonal_shape_accepted() {
162        assert!(planar_spiral(3, 10.0, 10.0, 200.0, SpiralShape::Hexagonal).is_ok());
163    }
164}