Skip to main content

pcb_toolkit/
ohms_law.rs

1//! Ohm's law and basic electrical calculators.
2//!
3//! Sub-calculators:
4//! 1. E-I-R (V = IR, P = VI)
5//! 2. LED Bias resistor
6//! 3. Resistor series/parallel
7//! 4. Pi-pad attenuator
8//! 5. T-pad attenuator
9//! 6. Capacitor series/parallel
10//! 7. Inductor series/parallel
11
12use serde::{Deserialize, Serialize};
13
14use crate::CalcError;
15
16// ---------------------------------------------------------------------------
17// E-I-R
18// ---------------------------------------------------------------------------
19
20/// Result of an E-I-R (voltage-current-resistance) calculation.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct EirResult {
23    /// Voltage in Volts.
24    pub voltage_v: f64,
25    /// Current in Amperes.
26    pub current_a: f64,
27    /// Resistance in Ohms.
28    pub resistance_ohm: f64,
29    /// Power in Watts (P = V × I).
30    pub power_w: f64,
31}
32
33/// Calculate voltage, current, resistance, and power given any two of V, I, R.
34///
35/// Exactly two of the three options must be `Some`.
36///
37/// # Errors
38/// Returns [`CalcError::InsufficientInputs`] if fewer or more than two values are provided,
39/// or [`CalcError::OutOfRange`] if a zero denominator would result.
40pub fn eir(
41    voltage_v: Option<f64>,
42    current_a: Option<f64>,
43    resistance_ohm: Option<f64>,
44) -> Result<EirResult, CalcError> {
45    let provided = [voltage_v, current_a, resistance_ohm]
46        .iter()
47        .filter(|v| v.is_some())
48        .count();
49    if provided != 2 {
50        return Err(CalcError::InsufficientInputs(
51            "exactly 2 of voltage_v, current_a, resistance_ohm must be provided",
52        ));
53    }
54
55    let (v, i, r) = match (voltage_v, current_a, resistance_ohm) {
56        (Some(v), Some(i), None) => {
57            if i == 0.0 {
58                return Err(CalcError::OutOfRange {
59                    name: "current_a",
60                    value: i,
61                    expected: "!= 0 when computing resistance",
62                });
63            }
64            (v, i, v / i)
65        }
66        (Some(v), None, Some(r)) => {
67            if r == 0.0 {
68                return Err(CalcError::OutOfRange {
69                    name: "resistance_ohm",
70                    value: r,
71                    expected: "!= 0 when computing current",
72                });
73            }
74            (v, v / r, r)
75        }
76        (None, Some(i), Some(r)) => (i * r, i, r),
77        _ => unreachable!(),
78    };
79
80    Ok(EirResult {
81        voltage_v: v,
82        current_a: i,
83        resistance_ohm: r,
84        power_w: v * i,
85    })
86}
87
88// ---------------------------------------------------------------------------
89// LED bias resistor
90// ---------------------------------------------------------------------------
91
92/// Result of an LED bias resistor calculation.
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94pub struct LedBiasResult {
95    /// Required series resistor value in Ohms.
96    pub resistance_ohm: f64,
97    /// Power dissipated by the resistor in Watts.
98    pub power_w: f64,
99}
100
101/// Calculate LED bias resistor.
102///
103/// R = (Vs − Vled) / Iled
104///
105/// # Arguments
106/// - `supply_v` — supply voltage in Volts (must be > led_v)
107/// - `led_v` — LED forward voltage in Volts (must be > 0)
108/// - `led_current_a` — desired LED current in Amperes (must be > 0)
109///
110/// # Errors
111/// Returns [`CalcError::OutOfRange`] if inputs are invalid.
112pub fn led_bias(supply_v: f64, led_v: f64, led_current_a: f64) -> Result<LedBiasResult, CalcError> {
113    if led_v <= 0.0 {
114        return Err(CalcError::OutOfRange {
115            name: "led_v",
116            value: led_v,
117            expected: "> 0",
118        });
119    }
120    if supply_v <= led_v {
121        return Err(CalcError::OutOfRange {
122            name: "supply_v",
123            value: supply_v,
124            expected: "> led_v",
125        });
126    }
127    if led_current_a <= 0.0 {
128        return Err(CalcError::OutOfRange {
129            name: "led_current_a",
130            value: led_current_a,
131            expected: "> 0",
132        });
133    }
134
135    let v_drop = supply_v - led_v;
136    let resistance_ohm = v_drop / led_current_a;
137    let power_w = v_drop * led_current_a;
138
139    Ok(LedBiasResult {
140        resistance_ohm,
141        power_w,
142    })
143}
144
145// ---------------------------------------------------------------------------
146// Resistor combinations
147// ---------------------------------------------------------------------------
148
149/// Result of a resistor series/parallel combination.
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151pub struct ResistorCombinationResult {
152    /// Combined resistance in Ohms.
153    pub resistance_ohm: f64,
154}
155
156/// Sum resistors in series.
157///
158/// R_total = R1 + R2 + … + Rn
159pub fn resistors_series(values: &[f64]) -> Result<ResistorCombinationResult, CalcError> {
160    if values.is_empty() {
161        return Err(CalcError::InsufficientInputs("at least one resistor required"));
162    }
163    Ok(ResistorCombinationResult {
164        resistance_ohm: values.iter().sum(),
165    })
166}
167
168/// Combine resistors in parallel.
169///
170/// 1/R_total = 1/R1 + 1/R2 + … + 1/Rn
171pub fn resistors_parallel(values: &[f64]) -> Result<ResistorCombinationResult, CalcError> {
172    if values.is_empty() {
173        return Err(CalcError::InsufficientInputs("at least one resistor required"));
174    }
175    for (idx, &r) in values.iter().enumerate() {
176        if r == 0.0 {
177            return Err(CalcError::OutOfRange {
178                name: "resistor value",
179                value: r,
180                expected: "!= 0 for parallel combination",
181            });
182        }
183        let _ = idx;
184    }
185    let reciprocal_sum: f64 = values.iter().map(|r| 1.0 / r).sum();
186    Ok(ResistorCombinationResult {
187        resistance_ohm: 1.0 / reciprocal_sum,
188    })
189}
190
191// ---------------------------------------------------------------------------
192// Capacitor combinations
193// ---------------------------------------------------------------------------
194
195/// Result of a capacitor series/parallel combination.
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct CapacitorCombinationResult {
198    /// Combined capacitance in Farads.
199    pub capacitance_f: f64,
200}
201
202/// Sum capacitors in parallel (C_total = C1 + C2 + … + Cn).
203pub fn capacitors_parallel(values: &[f64]) -> Result<CapacitorCombinationResult, CalcError> {
204    if values.is_empty() {
205        return Err(CalcError::InsufficientInputs("at least one capacitor required"));
206    }
207    Ok(CapacitorCombinationResult {
208        capacitance_f: values.iter().sum(),
209    })
210}
211
212/// Combine capacitors in series (1/C_total = 1/C1 + 1/C2 + … + 1/Cn).
213pub fn capacitors_series(values: &[f64]) -> Result<CapacitorCombinationResult, CalcError> {
214    if values.is_empty() {
215        return Err(CalcError::InsufficientInputs("at least one capacitor required"));
216    }
217    for &c in values.iter() {
218        if c == 0.0 {
219            return Err(CalcError::OutOfRange {
220                name: "capacitor value",
221                value: c,
222                expected: "!= 0 for series combination",
223            });
224        }
225    }
226    let reciprocal_sum: f64 = values.iter().map(|c| 1.0 / c).sum();
227    Ok(CapacitorCombinationResult {
228        capacitance_f: 1.0 / reciprocal_sum,
229    })
230}
231
232// ---------------------------------------------------------------------------
233// Inductor combinations
234// ---------------------------------------------------------------------------
235
236/// Result of an inductor series/parallel combination.
237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
238pub struct InductorCombinationResult {
239    /// Combined inductance in Henries.
240    pub inductance_h: f64,
241}
242
243/// Sum inductors in series (L_total = L1 + L2 + … + Ln, no mutual coupling).
244pub fn inductors_series(values: &[f64]) -> Result<InductorCombinationResult, CalcError> {
245    if values.is_empty() {
246        return Err(CalcError::InsufficientInputs("at least one inductor required"));
247    }
248    Ok(InductorCombinationResult {
249        inductance_h: values.iter().sum(),
250    })
251}
252
253/// Combine inductors in parallel (1/L_total = 1/L1 + … + 1/Ln, no mutual coupling).
254pub fn inductors_parallel(values: &[f64]) -> Result<InductorCombinationResult, CalcError> {
255    if values.is_empty() {
256        return Err(CalcError::InsufficientInputs("at least one inductor required"));
257    }
258    for &l in values.iter() {
259        if l == 0.0 {
260            return Err(CalcError::OutOfRange {
261                name: "inductor value",
262                value: l,
263                expected: "!= 0 for parallel combination",
264            });
265        }
266    }
267    let reciprocal_sum: f64 = values.iter().map(|l| 1.0 / l).sum();
268    Ok(InductorCombinationResult {
269        inductance_h: 1.0 / reciprocal_sum,
270    })
271}
272
273// ---------------------------------------------------------------------------
274// Attenuators
275// ---------------------------------------------------------------------------
276
277/// Result of a symmetric Pi-pad or T-pad attenuator calculation.
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub struct AttenuatorResult {
280    /// Attenuation in dB.
281    pub attenuation_db: f64,
282    /// Voltage ratio K = 10^(dB/20).
283    pub k: f64,
284    /// Series resistor (Ω): two outer elements for Pi-pad, two outer elements for T-pad.
285    pub r_series_ohm: f64,
286    /// Shunt resistor (Ω): centre element for Pi-pad, centre element for T-pad.
287    pub r_shunt_ohm: f64,
288}
289
290/// Calculate a symmetric Pi-pad attenuator.
291///
292/// Topology: Rshunt — Rseries — Rshunt (shunt to ground at input and output).
293///
294/// ```text
295/// in ─┬── R_series ──┬─ out
296///     R_shunt        R_shunt
297///     GND            GND
298/// ```
299///
300/// Formulas (symmetric, Z_in = Z_out = z_ohm):
301/// - K = 10^(dB/20)
302/// - R_shunt = Z × (K + 1) / (K − 1)
303/// - R_series = Z × (K − 1) / (K + 1)
304///
305/// # Errors
306/// Returns [`CalcError::OutOfRange`] if `attenuation_db` ≤ 0 or `z_ohm` ≤ 0.
307pub fn pi_pad(attenuation_db: f64, z_ohm: f64) -> Result<AttenuatorResult, CalcError> {
308    if attenuation_db <= 0.0 {
309        return Err(CalcError::OutOfRange {
310            name: "attenuation_db",
311            value: attenuation_db,
312            expected: "> 0",
313        });
314    }
315    if z_ohm <= 0.0 {
316        return Err(CalcError::OutOfRange {
317            name: "z_ohm",
318            value: z_ohm,
319            expected: "> 0",
320        });
321    }
322
323    let k = 10.0_f64.powf(attenuation_db / 20.0);
324    let r_shunt_ohm = z_ohm * (k + 1.0) / (k - 1.0);
325    let r_series_ohm = z_ohm * (k - 1.0) / (k + 1.0);
326
327    Ok(AttenuatorResult {
328        attenuation_db,
329        k,
330        r_series_ohm,
331        r_shunt_ohm,
332    })
333}
334
335/// Calculate a symmetric T-pad attenuator.
336///
337/// Topology: Rseries — Rshunt — Rseries (shunt to ground in the centre).
338///
339/// ```text
340/// in ── R_series ──┬── R_series ── out
341///                  R_shunt
342///                  GND
343/// ```
344///
345/// Formulas (symmetric, Z_in = Z_out = z_ohm):
346/// - K = 10^(dB/20)
347/// - R_series = Z × (K − 1) / (K + 1)
348/// - R_shunt  = Z × 2K / (K² − 1)
349///
350/// # Errors
351/// Returns [`CalcError::OutOfRange`] if `attenuation_db` ≤ 0 or `z_ohm` ≤ 0.
352pub fn t_pad(attenuation_db: f64, z_ohm: f64) -> Result<AttenuatorResult, CalcError> {
353    if attenuation_db <= 0.0 {
354        return Err(CalcError::OutOfRange {
355            name: "attenuation_db",
356            value: attenuation_db,
357            expected: "> 0",
358        });
359    }
360    if z_ohm <= 0.0 {
361        return Err(CalcError::OutOfRange {
362            name: "z_ohm",
363            value: z_ohm,
364            expected: "> 0",
365        });
366    }
367
368    let k = 10.0_f64.powf(attenuation_db / 20.0);
369    let r_series_ohm = z_ohm * (k - 1.0) / (k + 1.0);
370    let r_shunt_ohm = z_ohm * 2.0 * k / (k * k - 1.0);
371
372    Ok(AttenuatorResult {
373        attenuation_db,
374        k,
375        r_series_ohm,
376        r_shunt_ohm,
377    })
378}
379
380#[cfg(test)]
381mod tests {
382    use approx::assert_relative_eq;
383
384    use super::*;
385
386    // Saturn PDF page 21: V=12V, I=1A, R=12Ω → P=12W
387    #[test]
388    fn saturn_eir_vi() {
389        let result = eir(Some(12.0), Some(1.0), None).unwrap();
390        assert_relative_eq!(result.resistance_ohm, 12.0, epsilon = 1e-10);
391        assert_relative_eq!(result.power_w, 12.0, epsilon = 1e-10);
392    }
393
394    #[test]
395    fn eir_solve_voltage() {
396        let result = eir(None, Some(1.0), Some(12.0)).unwrap();
397        assert_relative_eq!(result.voltage_v, 12.0, epsilon = 1e-10);
398        assert_relative_eq!(result.power_w, 12.0, epsilon = 1e-10);
399    }
400
401    // Saturn PDF page 21: LED: Vs=12V, Vled=2V, Iled=10mA → R=1000Ω, P=0.1W
402    #[test]
403    fn saturn_led_bias() {
404        let result = led_bias(12.0, 2.0, 0.01).unwrap();
405        assert_relative_eq!(result.resistance_ohm, 1000.0, epsilon = 1e-6);
406        assert_relative_eq!(result.power_w, 0.1, epsilon = 1e-8);
407    }
408
409    // Pi-pad 1dB, 50Ω: R_series≈2.9Ω, R_shunt≈870Ω
410    #[test]
411    fn pi_pad_1db_50ohm() {
412        let result = pi_pad(1.0, 50.0).unwrap();
413        assert_relative_eq!(result.r_series_ohm, 2.88, epsilon = 0.01);
414        assert_relative_eq!(result.r_shunt_ohm, 869.5, epsilon = 0.5);
415    }
416
417    // Pi-pad 10dB, 50Ω: R_series≈26.0Ω, R_shunt≈96.2Ω
418    #[test]
419    fn pi_pad_10db_50ohm() {
420        let result = pi_pad(10.0, 50.0).unwrap();
421        assert_relative_eq!(result.r_series_ohm, 25.97, epsilon = 0.02);
422        assert_relative_eq!(result.r_shunt_ohm, 96.25, epsilon = 0.05);
423    }
424
425    // T-pad 10dB, 50Ω: R_series≈26.0Ω, R_shunt≈35.1Ω
426    #[test]
427    fn t_pad_10db_50ohm() {
428        let result = t_pad(10.0, 50.0).unwrap();
429        assert_relative_eq!(result.r_series_ohm, 25.97, epsilon = 0.02);
430        assert_relative_eq!(result.r_shunt_ohm, 35.14, epsilon = 0.02);
431    }
432
433    #[test]
434    fn resistors_series_two() {
435        let result = resistors_series(&[100.0, 200.0]).unwrap();
436        assert_relative_eq!(result.resistance_ohm, 300.0, epsilon = 1e-10);
437    }
438
439    #[test]
440    fn resistors_parallel_two_equal() {
441        let result = resistors_parallel(&[100.0, 100.0]).unwrap();
442        assert_relative_eq!(result.resistance_ohm, 50.0, epsilon = 1e-10);
443    }
444
445    #[test]
446    fn capacitors_parallel_two() {
447        let result = capacitors_parallel(&[10e-12, 10e-12]).unwrap();
448        assert_relative_eq!(result.capacitance_f, 20e-12, epsilon = 1e-22);
449    }
450
451    #[test]
452    fn capacitors_series_two_equal() {
453        let result = capacitors_series(&[10e-12, 10e-12]).unwrap();
454        assert_relative_eq!(result.capacitance_f, 5e-12, epsilon = 1e-22);
455    }
456
457    #[test]
458    fn inductors_series_two() {
459        let result = inductors_series(&[1e-6, 2e-6]).unwrap();
460        assert_relative_eq!(result.inductance_h, 3e-6, epsilon = 1e-16);
461    }
462
463    #[test]
464    fn inductors_parallel_two_equal() {
465        let result = inductors_parallel(&[10e-6, 10e-6]).unwrap();
466        assert_relative_eq!(result.inductance_h, 5e-6, epsilon = 1e-16);
467    }
468
469    #[test]
470    fn error_on_zero_attenuation() {
471        assert!(pi_pad(0.0, 50.0).is_err());
472        assert!(t_pad(0.0, 50.0).is_err());
473    }
474
475    #[test]
476    fn error_on_insufficient_eir_inputs() {
477        assert!(eir(Some(12.0), None, None).is_err());
478        assert!(eir(Some(12.0), Some(1.0), Some(12.0)).is_err());
479    }
480}