use crate::CalcError;
use crate::impedance::{common, types::ImpedanceResult};
pub struct CoplanarInput {
pub width: f64,
pub gap: f64,
pub height: f64,
pub thickness: f64,
pub er: f64,
}
fn elliptic_ratio(k: f64) -> f64 {
let threshold = 1.0 / std::f64::consts::SQRT_2;
if k <= threshold {
let kp = (1.0 - k * k).sqrt();
std::f64::consts::PI / (2.0 * (1.0 + kp.sqrt()) / (1.0 - kp.sqrt())).ln()
} else {
(1.0 / std::f64::consts::PI) * (2.0 * (1.0 + k.sqrt()) / (1.0 - k.sqrt())).ln()
}
}
pub fn calculate(input: &CoplanarInput) -> Result<ImpedanceResult, CalcError> {
let CoplanarInput { width, gap, height, thickness: _, er } = *input;
if width <= 0.0 {
return Err(CalcError::NegativeDimension { name: "width", value: width });
}
if gap <= 0.0 {
return Err(CalcError::NegativeDimension { name: "gap", value: gap });
}
if height <= 0.0 {
return Err(CalcError::NegativeDimension { name: "height", value: height });
}
if er < 1.0 {
return Err(CalcError::OutOfRange {
name: "er",
value: er,
expected: ">= 1.0",
});
}
let k = width / (width + 2.0 * gap);
let k3 = (std::f64::consts::PI * width / (4.0 * height)).tanh()
/ (std::f64::consts::PI * (width + 2.0 * gap) / (4.0 * height)).tanh();
let er_eff = 1.0 + (er - 1.0) / 2.0 * (1.0 / elliptic_ratio(k)) * elliptic_ratio(k3);
let zo = (30.0 * std::f64::consts::PI) / (er_eff.sqrt() * elliptic_ratio(k));
let tpd = common::propagation_delay(er_eff);
let lo = common::inductance_per_length(zo, tpd);
let co = common::capacitance_per_length(zo, tpd);
Ok(ImpedanceResult {
zo,
er_eff,
tpd_ps_per_in: tpd,
lo_nh_per_in: lo,
co_pf_per_in: co,
})
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn typical() -> CoplanarInput {
CoplanarInput { width: 10.0, gap: 5.0, height: 10.0, thickness: 1.4, er: 4.6 }
}
#[test]
fn reasonable_impedance() {
let result = calculate(&typical()).unwrap();
assert!(
result.zo >= 30.0 && result.zo <= 150.0,
"Z0 = {} should be in 30–150 Ω range",
result.zo
);
}
#[test]
fn narrower_gap_lowers_impedance() {
let wide_gap = calculate(&typical()).unwrap();
let narrow_gap = calculate(&CoplanarInput { gap: 2.0, ..typical() }).unwrap();
assert!(
narrow_gap.zo < wide_gap.zo,
"narrow gap Z0 {} should be < wide gap Z0 {}",
narrow_gap.zo,
wide_gap.zo
);
}
#[test]
fn higher_er_lowers_impedance() {
let low_er = calculate(&typical()).unwrap();
let high_er = calculate(&CoplanarInput { er: 9.8, ..typical() }).unwrap();
assert!(
high_er.zo < low_er.zo,
"high-Er Z0 {} should be < low-Er Z0 {}",
high_er.zo,
low_er.zo
);
}
#[test]
fn er_eff_between_one_and_er() {
let result = calculate(&typical()).unwrap();
assert!(
result.er_eff > 1.0 && result.er_eff < 4.6,
"er_eff = {} should be in (1.0, 4.6)",
result.er_eff
);
}
#[test]
fn wide_gap_approaches_microstrip_range() {
let result = calculate(&CoplanarInput {
width: 10.0,
gap: 1000.0,
height: 10.0,
thickness: 1.4,
er: 4.6,
})
.unwrap();
assert!(
result.zo > 40.0 && result.zo < 200.0,
"wide-gap Z0 {} should be in a plausible transmission-line range (40–200 Ω)",
result.zo
);
}
#[test]
fn rejects_non_positive_width() {
let result = calculate(&CoplanarInput { width: 0.0, ..typical() });
assert!(result.is_err());
let result = calculate(&CoplanarInput { width: -1.0, ..typical() });
assert!(result.is_err());
}
#[test]
fn rejects_non_positive_gap() {
let result = calculate(&CoplanarInput { gap: 0.0, ..typical() });
assert!(result.is_err());
let result = calculate(&CoplanarInput { gap: -5.0, ..typical() });
assert!(result.is_err());
}
#[test]
fn rejects_non_positive_height() {
let result = calculate(&CoplanarInput { height: 0.0, ..typical() });
assert!(result.is_err());
}
#[test]
fn rejects_er_below_one() {
let result = calculate(&CoplanarInput { er: 0.5, ..typical() });
assert!(result.is_err());
}
#[test]
fn derived_quantities_consistent() {
let r = calculate(&typical()).unwrap();
let lo_check = r.zo * r.tpd_ps_per_in / 1000.0;
assert_relative_eq!(r.lo_nh_per_in, lo_check, max_relative = 1e-10);
let co_check = (r.tpd_ps_per_in / 1000.0) / r.zo * 1000.0;
assert_relative_eq!(r.co_pf_per_in, co_check, max_relative = 1e-10);
}
}