Skip to main content

pcb_toolkit/
wavelength.rs

1//! Signal wavelength calculator.
2//!
3//! λ = c / (f × √Er_eff)
4
5use serde::{Deserialize, Serialize};
6
7use crate::CalcError;
8
9/// Speed of light expressed as inches per nanosecond.
10const C_IN_PER_NS: f64 = 11.803;
11
12/// Result of a wavelength calculation.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct WavelengthResult {
15    /// Full wavelength in inches.
16    pub lambda_inches: f64,
17    /// λ/2 in inches.
18    pub lambda_half_inches: f64,
19    /// λ/4 in inches.
20    pub lambda_quarter_inches: f64,
21    /// λ/7 in inches.
22    pub lambda_seventh_inches: f64,
23    /// λ/10 in inches.
24    pub lambda_tenth_inches: f64,
25    /// λ/20 in inches.
26    pub lambda_twentieth_inches: f64,
27    /// Period in nanoseconds (1 / f_hz × 1e9).
28    pub period_ns: f64,
29}
30
31/// Calculate signal wavelength in a substrate.
32///
33/// # Arguments
34/// - `freq_hz` — frequency in Hz (must be > 0)
35/// - `er_eff` — effective relative permittivity (must be ≥ 1.0)
36///
37/// # Errors
38/// Returns [`CalcError::OutOfRange`] if inputs are out of valid range.
39pub fn wavelength(freq_hz: f64, er_eff: f64) -> Result<WavelengthResult, CalcError> {
40    if freq_hz <= 0.0 {
41        return Err(CalcError::OutOfRange {
42            name: "freq_hz",
43            value: freq_hz,
44            expected: "> 0",
45        });
46    }
47    if er_eff < 1.0 {
48        return Err(CalcError::OutOfRange {
49            name: "er_eff",
50            value: er_eff,
51            expected: ">= 1.0",
52        });
53    }
54
55    let period_ns = 1.0 / freq_hz * 1e9;
56    // λ = c × T_ns / √Er_eff  (c in in/ns, T in ns → λ in inches)
57    let lambda_inches = C_IN_PER_NS * period_ns / er_eff.sqrt();
58
59    Ok(WavelengthResult {
60        lambda_inches,
61        lambda_half_inches: lambda_inches / 2.0,
62        lambda_quarter_inches: lambda_inches / 4.0,
63        lambda_seventh_inches: lambda_inches / 7.0,
64        lambda_tenth_inches: lambda_inches / 10.0,
65        lambda_twentieth_inches: lambda_inches / 20.0,
66        period_ns,
67    })
68}
69
70#[cfg(test)]
71mod tests {
72    use approx::assert_relative_eq;
73
74    use super::*;
75
76    // Saturn notes: Period=10ns (f=100MHz), Er_eff=4 → λ=59.01426 inches (but formula
77    // gives 11.803 × 10 / √4 = 118.03 / 2 = 59.015 — rounding in the notes).
78    // The note says 59.01426 which matches using c=11.80285 (exact c / in/ns).
79    // Saturn uses c=11.803 → λ = 11.803 × 10 / 2 = 59.015 inches.
80    #[test]
81    fn saturn_notes_100mhz_er4() {
82        let result = wavelength(100e6, 4.0).unwrap();
83        assert_relative_eq!(result.period_ns, 10.0, epsilon = 1e-9);
84        assert_relative_eq!(result.lambda_inches, 59.015, epsilon = 1e-2);
85        assert_relative_eq!(result.lambda_half_inches, result.lambda_inches / 2.0, epsilon = 1e-10);
86        assert_relative_eq!(result.lambda_quarter_inches, result.lambda_inches / 4.0, epsilon = 1e-10);
87        assert_relative_eq!(result.lambda_seventh_inches, result.lambda_inches / 7.0, epsilon = 1e-10);
88        assert_relative_eq!(result.lambda_tenth_inches, result.lambda_inches / 10.0, epsilon = 1e-10);
89        assert_relative_eq!(result.lambda_twentieth_inches, result.lambda_inches / 20.0, epsilon = 1e-10);
90    }
91
92    #[test]
93    fn error_on_zero_freq() {
94        assert!(wavelength(0.0, 4.0).is_err());
95    }
96
97    #[test]
98    fn error_on_er_below_one() {
99        assert!(wavelength(100e6, 0.5).is_err());
100    }
101
102    #[test]
103    fn vacuum_er1() {
104        let result = wavelength(1e9, 1.0).unwrap();
105        // At 1 GHz, period = 1 ns, λ = 11.803 inches in vacuum
106        assert_relative_eq!(result.lambda_inches, 11.803, epsilon = 1e-3);
107    }
108}