Skip to main content

pcb_toolkit/differential/
edge_coupled_internal_sym.rs

1//! Edge-coupled internal symmetric (centered stripline) differential pair impedance calculator.
2//!
3//! Computes odd-mode, even-mode, and differential impedance for a symmetric stripline
4//! differential pair using the Cohn stripline Z0 formula with exponential coupling correction.
5
6use crate::CalcError;
7use super::types::{DifferentialResult, kb_terminated};
8
9/// Inputs for edge-coupled internal symmetric (centered stripline) differential pair.
10pub struct EdgeCoupledInternalSymInput {
11    /// Conductor width (mils).
12    pub width: f64,
13    /// Gap between traces (mils).
14    pub spacing: f64,
15    /// Dielectric height — distance from trace to each ground plane (mils).
16    /// For centered stripline, total dielectric thickness = 2 × height.
17    pub height: f64,
18    /// Conductor thickness (mils).
19    pub thickness: f64,
20    /// Substrate relative permittivity.
21    pub er: f64,
22}
23
24/// Compute differential impedance for an edge-coupled internal symmetric (centered stripline) pair.
25pub fn calculate(input: &EdgeCoupledInternalSymInput) -> Result<DifferentialResult, CalcError> {
26    let EdgeCoupledInternalSymInput { width, spacing, height, thickness, er } = *input;
27
28    if width <= 0.0 {
29        return Err(CalcError::NegativeDimension { name: "width", value: width });
30    }
31    if spacing <= 0.0 {
32        return Err(CalcError::NegativeDimension { name: "spacing", value: spacing });
33    }
34    if height <= 0.0 {
35        return Err(CalcError::NegativeDimension { name: "height", value: height });
36    }
37    if thickness <= 0.0 {
38        return Err(CalcError::NegativeDimension { name: "thickness", value: thickness });
39    }
40    if er < 1.0 {
41        return Err(CalcError::OutOfRange {
42            name: "er",
43            value: er,
44            expected: ">= 1.0",
45        });
46    }
47
48    let z0 = (60.0 / er.sqrt()) * (1.9 * (2.0 * height + thickness) / (0.8 * width + thickness)).ln();
49
50    let zodd = z0 * (1.0 - 0.48 * (-0.96 * spacing / height).exp());
51    let zeven = z0 * z0 / zodd;
52    let zdiff = 2.0 * zodd;
53    let kb = (zeven - zodd) / (zeven + zodd);
54    let kb_db = 20.0 * kb.log10();
55    let kb_term = kb_terminated(kb);
56    let kb_term_db = 20.0 * kb_term.log10();
57
58    Ok(DifferentialResult {
59        zdiff,
60        zo: z0,
61        zodd,
62        zeven,
63        kb,
64        kb_db,
65        kb_term,
66        kb_term_db,
67    })
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use approx::assert_relative_eq;
74
75    fn input(width: f64, spacing: f64, height: f64, thickness: f64, er: f64) -> EdgeCoupledInternalSymInput {
76        EdgeCoupledInternalSymInput { width, spacing, height, thickness, er }
77    }
78
79    /// Web reference vector: W=10, S=63, H=63, T=1.2, Er=4.
80    /// S/H=1, weak coupling; Zdiff should be in the range 140–170 Ω.
81    #[test]
82    fn web_reference_weak_coupling() {
83        let result = calculate(&input(10.0, 63.0, 63.0, 1.2, 4.0)).unwrap();
84
85        assert!(
86            result.zdiff >= 140.0 && result.zdiff <= 170.0,
87            "Zdiff {:.3} should be in range 140–170 Ω",
88            result.zdiff
89        );
90    }
91
92    /// Narrower spacing increases coupling magnitude (higher Kb).
93    #[test]
94    fn wider_spacing_reduces_coupling() {
95        let close = calculate(&input(10.0, 5.0, 63.0, 1.2, 4.0)).unwrap();
96        let far   = calculate(&input(10.0, 20.0, 63.0, 1.2, 4.0)).unwrap();
97
98        assert!(
99            far.kb.abs() < close.kb.abs(),
100            "wider spacing Kb {:.4} should be smaller than {:.4}",
101            far.kb,
102            close.kb
103        );
104    }
105
106    /// Higher Er gives lower Z0.
107    #[test]
108    fn higher_er_gives_lower_z0() {
109        let low_er  = calculate(&input(10.0, 10.0, 63.0, 1.2, 2.2)).unwrap();
110        let high_er = calculate(&input(10.0, 10.0, 63.0, 1.2, 4.6)).unwrap();
111
112        assert!(
113            high_er.zo < low_er.zo,
114            "higher Er Z0 {:.3} should be less than {:.3}",
115            high_er.zo,
116            low_er.zo
117        );
118    }
119
120    /// For a fully embedded stripline, Er_eff equals Er exactly.
121    #[test]
122    fn er_eff_equals_er() {
123        let er = 4.6_f64;
124        let result = calculate(&input(10.0, 10.0, 63.0, 1.2, er)).unwrap();
125
126        // Z0 = (60/sqrt(Er)) * ln(...); verify by back-computing Er from Z0 and the log term.
127        let ln_term = (1.9_f64 * (2.0 * 63.0 + 1.2) / (0.8 * 10.0 + 1.2)).ln();
128        let er_back = (60.0 * ln_term / result.zo).powi(2);
129        assert_relative_eq!(er_back, er, max_relative = 1e-10);
130    }
131
132    #[test]
133    fn rejects_negative_width() {
134        let result = calculate(&input(-1.0, 10.0, 63.0, 1.2, 4.0));
135        assert!(result.is_err());
136    }
137
138    #[test]
139    fn rejects_er_below_one() {
140        let result = calculate(&input(10.0, 10.0, 63.0, 1.2, 0.5));
141        assert!(result.is_err());
142    }
143}