pcb_toolkit/impedance/
coplanar.rs1use crate::CalcError;
6use crate::impedance::{common, types::ImpedanceResult};
7
8pub struct CoplanarInput {
11 pub width: f64,
13 pub gap: f64,
15 pub height: f64,
17 pub thickness: f64,
19 pub er: f64,
21}
22
23fn elliptic_ratio(k: f64) -> f64 {
30 let threshold = 1.0 / std::f64::consts::SQRT_2;
31 if k <= threshold {
32 let kp = (1.0 - k * k).sqrt();
33 std::f64::consts::PI / (2.0 * (1.0 + kp.sqrt()) / (1.0 - kp.sqrt())).ln()
34 } else {
35 (1.0 / std::f64::consts::PI) * (2.0 * (1.0 + k.sqrt()) / (1.0 - k.sqrt())).ln()
36 }
37}
38
39pub fn calculate(input: &CoplanarInput) -> Result<ImpedanceResult, CalcError> {
41 let CoplanarInput { width, gap, height, thickness: _, er } = *input;
42
43 if width <= 0.0 {
44 return Err(CalcError::NegativeDimension { name: "width", value: width });
45 }
46 if gap <= 0.0 {
47 return Err(CalcError::NegativeDimension { name: "gap", value: gap });
48 }
49 if height <= 0.0 {
50 return Err(CalcError::NegativeDimension { name: "height", value: height });
51 }
52 if er < 1.0 {
53 return Err(CalcError::OutOfRange {
54 name: "er",
55 value: er,
56 expected: ">= 1.0",
57 });
58 }
59
60 let k = width / (width + 2.0 * gap);
62
63 let k3 = (std::f64::consts::PI * width / (4.0 * height)).tanh()
65 / (std::f64::consts::PI * (width + 2.0 * gap) / (4.0 * height)).tanh();
66
67 let er_eff = 1.0 + (er - 1.0) / 2.0 * (1.0 / elliptic_ratio(k)) * elliptic_ratio(k3);
73
74 let zo = (30.0 * std::f64::consts::PI) / (er_eff.sqrt() * elliptic_ratio(k));
77
78 let tpd = common::propagation_delay(er_eff);
79 let lo = common::inductance_per_length(zo, tpd);
80 let co = common::capacitance_per_length(zo, tpd);
81
82 Ok(ImpedanceResult {
83 zo,
84 er_eff,
85 tpd_ps_per_in: tpd,
86 lo_nh_per_in: lo,
87 co_pf_per_in: co,
88 })
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use approx::assert_relative_eq;
95
96 fn typical() -> CoplanarInput {
97 CoplanarInput { width: 10.0, gap: 5.0, height: 10.0, thickness: 1.4, er: 4.6 }
98 }
99
100 #[test]
101 fn reasonable_impedance() {
102 let result = calculate(&typical()).unwrap();
103 assert!(
104 result.zo >= 30.0 && result.zo <= 150.0,
105 "Z0 = {} should be in 30–150 Ω range",
106 result.zo
107 );
108 }
109
110 #[test]
111 fn narrower_gap_lowers_impedance() {
112 let wide_gap = calculate(&typical()).unwrap();
113 let narrow_gap = calculate(&CoplanarInput { gap: 2.0, ..typical() }).unwrap();
114 assert!(
115 narrow_gap.zo < wide_gap.zo,
116 "narrow gap Z0 {} should be < wide gap Z0 {}",
117 narrow_gap.zo,
118 wide_gap.zo
119 );
120 }
121
122 #[test]
123 fn higher_er_lowers_impedance() {
124 let low_er = calculate(&typical()).unwrap();
125 let high_er = calculate(&CoplanarInput { er: 9.8, ..typical() }).unwrap();
126 assert!(
127 high_er.zo < low_er.zo,
128 "high-Er Z0 {} should be < low-Er Z0 {}",
129 high_er.zo,
130 low_er.zo
131 );
132 }
133
134 #[test]
135 fn er_eff_between_one_and_er() {
136 let result = calculate(&typical()).unwrap();
137 assert!(
138 result.er_eff > 1.0 && result.er_eff < 4.6,
139 "er_eff = {} should be in (1.0, 4.6)",
140 result.er_eff
141 );
142 }
143
144 #[test]
145 fn wide_gap_approaches_microstrip_range() {
146 let result = calculate(&CoplanarInput {
149 width: 10.0,
150 gap: 1000.0,
151 height: 10.0,
152 thickness: 1.4,
153 er: 4.6,
154 })
155 .unwrap();
156 assert!(
161 result.zo > 40.0 && result.zo < 200.0,
162 "wide-gap Z0 {} should be in a plausible transmission-line range (40–200 Ω)",
163 result.zo
164 );
165 }
166
167 #[test]
168 fn rejects_non_positive_width() {
169 let result = calculate(&CoplanarInput { width: 0.0, ..typical() });
170 assert!(result.is_err());
171 let result = calculate(&CoplanarInput { width: -1.0, ..typical() });
172 assert!(result.is_err());
173 }
174
175 #[test]
176 fn rejects_non_positive_gap() {
177 let result = calculate(&CoplanarInput { gap: 0.0, ..typical() });
178 assert!(result.is_err());
179 let result = calculate(&CoplanarInput { gap: -5.0, ..typical() });
180 assert!(result.is_err());
181 }
182
183 #[test]
184 fn rejects_non_positive_height() {
185 let result = calculate(&CoplanarInput { height: 0.0, ..typical() });
186 assert!(result.is_err());
187 }
188
189 #[test]
190 fn rejects_er_below_one() {
191 let result = calculate(&CoplanarInput { er: 0.5, ..typical() });
192 assert!(result.is_err());
193 }
194
195 #[test]
196 fn derived_quantities_consistent() {
197 let r = calculate(&typical()).unwrap();
198 let lo_check = r.zo * r.tpd_ps_per_in / 1000.0;
200 assert_relative_eq!(r.lo_nh_per_in, lo_check, max_relative = 1e-10);
201 let co_check = (r.tpd_ps_per_in / 1000.0) / r.zo * 1000.0;
203 assert_relative_eq!(r.co_pf_per_in, co_check, max_relative = 1e-10);
204 }
205}