Skip to main content

pcb_toolkit/
reactance.rs

1//! Capacitive/inductive reactance and resonant frequency calculator.
2//!
3//! Xc = 1 / (2πfC)
4//! Xl = 2πfL
5//! f_res = 1 / (2π√(LC))
6
7use serde::{Deserialize, Serialize};
8
9use crate::CalcError;
10
11/// Result of a reactance calculation.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct ReactanceResult {
14    /// Capacitive reactance in Ohms. None if capacitance was not provided.
15    pub xc_ohms: Option<f64>,
16    /// Inductive reactance in Ohms. None if inductance was not provided.
17    pub xl_ohms: Option<f64>,
18    /// Resonant frequency in Hz. None if both L and C were not provided.
19    pub f_res_hz: Option<f64>,
20}
21
22/// Calculate capacitive reactance, inductive reactance, and resonant frequency.
23///
24/// # Arguments
25/// - `freq_hz` — frequency in Hz (must be > 0)
26/// - `capacitance_f` — capacitance in Farads (None to skip Xc and f_res)
27/// - `inductance_h` — inductance in Henries (None to skip Xl and f_res)
28///
29/// # Errors
30/// Returns [`CalcError::OutOfRange`] if `freq_hz` ≤ 0 or either value is ≤ 0.
31pub fn reactance(
32    freq_hz: f64,
33    capacitance_f: Option<f64>,
34    inductance_h: Option<f64>,
35) -> Result<ReactanceResult, CalcError> {
36    if freq_hz <= 0.0 {
37        return Err(CalcError::OutOfRange {
38            name: "freq_hz",
39            value: freq_hz,
40            expected: "> 0",
41        });
42    }
43
44    if let Some(c) = capacitance_f {
45        if c <= 0.0 {
46            return Err(CalcError::OutOfRange {
47                name: "capacitance_f",
48                value: c,
49                expected: "> 0",
50            });
51        }
52    }
53
54    if let Some(l) = inductance_h {
55        if l <= 0.0 {
56            return Err(CalcError::OutOfRange {
57                name: "inductance_h",
58                value: l,
59                expected: "> 0",
60            });
61        }
62    }
63
64    let two_pi_f = 2.0 * std::f64::consts::PI * freq_hz;
65
66    let xc_ohms = capacitance_f.map(|c| 1.0 / (two_pi_f * c));
67    let xl_ohms = inductance_h.map(|l| two_pi_f * l);
68
69    let f_res_hz = match (capacitance_f, inductance_h) {
70        (Some(c), Some(l)) => Some(1.0 / (2.0 * std::f64::consts::PI * (l * c).sqrt())),
71        _ => None,
72    };
73
74    Ok(ReactanceResult {
75        xc_ohms,
76        xl_ohms,
77        f_res_hz,
78    })
79}
80
81#[cfg(test)]
82mod tests {
83    use approx::assert_relative_eq;
84
85    use super::*;
86
87    // Saturn PDF page 41: f=1 MHz, C=1 µF, L=1 mH
88    // Xc=0.1592 Ω, Xl=6283.1800 Ω, f_res=5032.9255 Hz
89    #[test]
90    fn saturn_page41_vector() {
91        let result = reactance(1e6, Some(1e-6), Some(1e-3)).unwrap();
92        assert_relative_eq!(result.xc_ohms.unwrap(), 0.15915494, epsilon = 1e-4);
93        assert_relative_eq!(result.xl_ohms.unwrap(), 6283.1853, epsilon = 1e-1);
94        assert_relative_eq!(result.f_res_hz.unwrap(), 5032.9255, epsilon = 1e-2);
95    }
96
97    // C=10nF, f=1kHz → Xc = 15915 Ω
98    #[test]
99    fn xc_10nf_1khz() {
100        let result = reactance(1e3, Some(10e-9), None).unwrap();
101        assert_relative_eq!(result.xc_ohms.unwrap(), 15915.494, epsilon = 1e-1);
102    }
103
104    // L=100µH, f=1kHz → Xl = 0.6283 Ω
105    #[test]
106    fn xl_100uh_1khz() {
107        let result = reactance(1e3, None, Some(100e-6)).unwrap();
108        assert_relative_eq!(result.xl_ohms.unwrap(), 0.6283185, epsilon = 1e-4);
109    }
110
111    #[test]
112    fn error_on_zero_freq() {
113        assert!(reactance(0.0, Some(1e-6), None).is_err());
114    }
115
116    #[test]
117    fn error_on_negative_capacitance() {
118        assert!(reactance(1e6, Some(-1e-6), None).is_err());
119    }
120}