Skip to main content

pcb_toolkit/
fusing.rs

1//! Fusing current calculator (Onderdonk's equation).
2//!
3//! Calculates the current required to melt a copper conductor
4//! in a given time period.
5//!
6//! Reference: Onderdonk's equation adapted for PCB traces.
7
8use serde::{Deserialize, Serialize};
9
10use crate::copper::{CopperWeight, EtchFactor, PlatingThickness};
11use crate::CalcError;
12
13/// Copper melting temperature in °C (pure copper, 1084.62 °C).
14pub const COPPER_MELTING_TEMP_C: f64 = 1084.62;
15
16/// Result of a fusing current calculation.
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct FusingResult {
19    /// Total copper thickness in mils.
20    pub copper_thickness_mils: f64,
21    /// Conductor cross-section in square mils (trapezoidal for etched profiles).
22    pub area_sq_mils: f64,
23    /// Conductor cross-section in circular mils (= area_sq_mils × 4/π).
24    pub area_circular_mils: f64,
25    /// Fusing (melting) current in Amperes.
26    pub fusing_current_a: f64,
27    /// Copper melting temperature used (°C).
28    pub melting_temp_c: f64,
29}
30
31/// Calculate fusing current from cross-sectional area using Onderdonk's equation.
32///
33/// I = A_circ × √( log₁₀(1 + (Tm − Ta) / (234 + Ta)) / (33 × t) )
34///
35/// # Arguments
36/// - `area_circular_mils` — conductor cross-section in circular mils (must be > 0)
37/// - `time_s` — pulse duration in seconds (must be > 0)
38/// - `ambient_c` — ambient temperature in °C
39/// - `melting_temp_c` — melting temperature of conductor in °C (must be > ambient_c)
40///
41/// # Errors
42/// Returns [`CalcError::OutOfRange`] if inputs are invalid.
43pub fn fusing_current(
44    area_circular_mils: f64,
45    time_s: f64,
46    ambient_c: f64,
47    melting_temp_c: f64,
48) -> Result<f64, CalcError> {
49    if area_circular_mils <= 0.0 {
50        return Err(CalcError::OutOfRange {
51            name: "area_circular_mils",
52            value: area_circular_mils,
53            expected: "> 0",
54        });
55    }
56    if time_s <= 0.0 {
57        return Err(CalcError::OutOfRange {
58            name: "time_s",
59            value: time_s,
60            expected: "> 0",
61        });
62    }
63    if melting_temp_c <= ambient_c {
64        return Err(CalcError::OutOfRange {
65            name: "melting_temp_c",
66            value: melting_temp_c,
67            expected: "> ambient_c",
68        });
69    }
70
71    let delta_t = melting_temp_c - ambient_c;
72    let log_term = (1.0 + delta_t / (234.0 + ambient_c)).log10();
73    let current = area_circular_mils * (log_term / (33.0 * time_s)).sqrt();
74
75    Ok(current)
76}
77
78/// Calculate the fusing current for a PCB trace from physical dimensions.
79///
80/// # Arguments
81/// - `width_mils` — trace width in mils (must be > 0)
82/// - `base_copper` — base copper weight
83/// - `plating` — plating thickness (use `PlatingThickness::Bare` for bare board)
84/// - `etch_factor` — etch profile affecting the cross-section shape
85/// - `time_s` — pulse duration in seconds (must be > 0)
86/// - `ambient_c` — ambient temperature in °C
87///
88/// # Errors
89/// Returns [`CalcError::OutOfRange`] or [`CalcError::NegativeDimension`] if inputs are invalid.
90pub fn fusing_current_trace(
91    width_mils: f64,
92    base_copper: CopperWeight,
93    plating: PlatingThickness,
94    etch_factor: EtchFactor,
95    time_s: f64,
96    ambient_c: f64,
97) -> Result<FusingResult, CalcError> {
98    if width_mils <= 0.0 {
99        return Err(CalcError::NegativeDimension {
100            name: "width_mils",
101            value: width_mils,
102        });
103    }
104
105    let copper_thickness_mils =
106        base_copper.thickness_mils() + plating.thickness_mils();
107    let area_sq_mils = etch_factor.cross_section_sq_mils(width_mils, copper_thickness_mils);
108    let area_circular_mils = area_sq_mils * (4.0 / std::f64::consts::PI);
109
110    let fusing_current_a = fusing_current(
111        area_circular_mils,
112        time_s,
113        ambient_c,
114        COPPER_MELTING_TEMP_C,
115    )?;
116
117    Ok(FusingResult {
118        copper_thickness_mils,
119        area_sq_mils,
120        area_circular_mils,
121        fusing_current_a,
122        melting_temp_c: COPPER_MELTING_TEMP_C,
123    })
124}
125
126#[cfg(test)]
127mod tests {
128    use approx::assert_relative_eq;
129
130    use super::*;
131
132    // Saturn PDF page 16: A_circ=23.93, t=1s, Ta=22°C → I=3.5147 A
133    // Copper melting point: 1084.62 °C (correct physical value; the spec note of
134    // 1064.62 is a transcription error — reverse-engineering confirms 1084.62).
135    #[test]
136    fn saturn_fusing_current_from_area() {
137        let current = fusing_current(23.93, 1.0, 22.0, COPPER_MELTING_TEMP_C).unwrap();
138        assert_relative_eq!(current, 3.5147, max_relative = 0.001);
139    }
140
141    // Verify cross-section math: A_sq=18.79 ↔ A_circ=23.93
142    #[test]
143    fn circular_mils_conversion() {
144        let a_sq = 18.79_f64;
145        let a_circ = a_sq * (4.0 / std::f64::consts::PI);
146        assert_relative_eq!(a_circ, 23.93, epsilon = 0.01);
147    }
148
149    // Verify EtchFactor::TwoToOne geometry: W=10, T=2.10 → A_sq≈18.79
150    #[test]
151    fn etch_two_to_one_area() {
152        // T=2.10 mils is CopperWeight::Oz15 (1.5 oz) bare
153        let area = EtchFactor::TwoToOne.cross_section_sq_mils(10.0, 2.10);
154        assert_relative_eq!(area, 18.795, epsilon = 0.01);
155    }
156
157    // Full trace calculation with 1.5oz bare, 2:1 etch, W=10 mil, t=1s, Ta=22°C
158    #[test]
159    fn saturn_full_trace_oz15_bare() {
160        let result = fusing_current_trace(
161            10.0,
162            CopperWeight::Oz15,
163            PlatingThickness::Bare,
164            EtchFactor::TwoToOne,
165            1.0,
166            22.0,
167        )
168        .unwrap();
169        assert_relative_eq!(result.copper_thickness_mils, 2.10, epsilon = 1e-10);
170        assert_relative_eq!(result.area_sq_mils, 18.795, epsilon = 0.01);
171        assert_relative_eq!(result.area_circular_mils, 23.93, epsilon = 0.02);
172        assert_relative_eq!(result.fusing_current_a, 3.5147, max_relative = 0.001);
173    }
174
175    #[test]
176    fn error_on_zero_time() {
177        assert!(fusing_current(23.93, 0.0, 22.0, COPPER_MELTING_TEMP_C).is_err());
178    }
179
180    #[test]
181    fn error_on_ambient_above_melting() {
182        assert!(fusing_current(23.93, 1.0, 1100.0, COPPER_MELTING_TEMP_C).is_err());
183    }
184}