1use serde::{Deserialize, Serialize};
9
10use crate::copper::{CopperWeight, EtchFactor, PlatingThickness};
11use crate::CalcError;
12
13pub const COPPER_MELTING_TEMP_C: f64 = 1084.62;
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct FusingResult {
19 pub copper_thickness_mils: f64,
21 pub area_sq_mils: f64,
23 pub area_circular_mils: f64,
25 pub fusing_current_a: f64,
27 pub melting_temp_c: f64,
29}
30
31pub 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
78pub 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 #[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 #[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 #[test]
151 fn etch_two_to_one_area() {
152 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 #[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}