1use serde::{Deserialize, Serialize};
17
18use crate::CalcError;
19use crate::copper::EtchFactor;
20
21const K_EXTERNAL: f64 = 0.048;
23
24const K_INTERNAL: f64 = 0.024;
26
27const RESISTIVITY_OHM_MIL: f64 = 6.787e-4;
33
34const COPPER_RESISTIVITY_OHM_M: f64 = 1.724e-8;
36
37const MU_0: f64 = 1.256_637_061_435_9e-6;
39
40pub struct CurrentInput {
42 pub width: f64,
44 pub thickness: f64,
46 pub length: f64,
48 pub temperature_rise: f64,
50 pub ambient_temp: f64,
52 pub frequency: f64,
54 pub etch_factor: EtchFactor,
56 pub is_internal: bool,
58}
59
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62pub struct CurrentResult {
63 pub current_capacity: f64,
65 pub cross_section: f64,
67 pub resistance_dc: f64,
69 pub voltage_drop: f64,
71 pub power_dissipation: f64,
73 pub current_density: f64,
75 pub skin_depth_mils: f64,
77}
78
79pub fn calculate(input: &CurrentInput) -> Result<CurrentResult, CalcError> {
85 let CurrentInput {
86 width,
87 thickness,
88 length,
89 temperature_rise,
90 etch_factor,
91 is_internal,
92 frequency,
93 ..
94 } = *input;
95
96 if width <= 0.0 {
97 return Err(CalcError::NegativeDimension { name: "width", value: width });
98 }
99 if thickness <= 0.0 {
100 return Err(CalcError::NegativeDimension { name: "thickness", value: thickness });
101 }
102 if length <= 0.0 {
103 return Err(CalcError::NegativeDimension { name: "length", value: length });
104 }
105 if temperature_rise <= 0.0 {
106 return Err(CalcError::OutOfRange {
107 name: "temperature_rise",
108 value: temperature_rise,
109 expected: "> 0",
110 });
111 }
112 if frequency < 0.0 {
113 return Err(CalcError::OutOfRange {
114 name: "frequency",
115 value: frequency,
116 expected: ">= 0",
117 });
118 }
119
120 let cross_section = etch_factor.cross_section_sq_mils(width, thickness);
122
123 let k = if is_internal { K_INTERNAL } else { K_EXTERNAL };
125 let current_capacity = k * temperature_rise.powf(0.44) * cross_section.powf(0.725);
126
127 let resistance_dc = RESISTIVITY_OHM_MIL * length / cross_section;
129
130 let voltage_drop = current_capacity * resistance_dc;
132 let power_dissipation = current_capacity * voltage_drop;
133
134 let current_density = current_capacity / cross_section;
136
137 let skin_depth_mils = if frequency > 0.0 {
139 let delta_m = (COPPER_RESISTIVITY_OHM_M
140 / (std::f64::consts::PI * frequency * MU_0))
141 .sqrt();
142 delta_m / crate::constants::MIL_TO_M
144 } else {
145 0.0
146 };
147
148 Ok(CurrentResult {
149 current_capacity,
150 cross_section,
151 resistance_dc,
152 voltage_drop,
153 power_dissipation,
154 current_density,
155 skin_depth_mils,
156 })
157}
158
159pub struct Ipc2152Input {
161 pub width: f64,
163 pub thickness: f64,
165 pub length: f64,
167 pub temperature_rise: f64,
169 pub ambient_temp: f64,
171 pub frequency: f64,
173 pub etch_factor: EtchFactor,
175 pub is_internal: bool,
177 pub board_thickness_mils: f64,
179 pub has_copper_plane: bool,
181 pub material_modifier: f64,
183 pub user_modifier: f64,
185}
186
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct Ipc2152Result {
190 pub current_capacity: f64,
192 pub cross_section: f64,
194 pub resistance_dc: f64,
196 pub voltage_drop: f64,
198 pub power_dissipation: f64,
200 pub current_density: f64,
202 pub skin_depth_mils: f64,
204 pub m_area: f64,
206 pub m_temp: f64,
208 pub m_board: f64,
210}
211
212fn m_temp_lookup(dt: f64) -> f64 {
213 if dt <= 10.0 {
214 0.40
215 } else if dt <= 20.0 {
216 0.48
217 } else if dt <= 30.0 {
218 0.58
219 } else if dt <= 40.0 {
220 0.67
221 } else if dt <= 50.0 {
222 0.75
223 } else if dt <= 60.0 {
224 0.85
225 } else if dt <= 70.0 {
226 0.95
227 } else if dt <= 80.0 {
228 1.00
229 } else if dt <= 90.0 {
230 1.10
231 } else if dt <= 100.0 {
232 1.20
233 } else {
234 1.30
235 }
236}
237
238fn m_board_lookup(thickness_mils: f64, has_plane: bool) -> f64 {
239 if has_plane {
240 if thickness_mils <= 10.0 {
241 1.63
242 } else if thickness_mils <= 20.0 {
243 1.59
244 } else if thickness_mils <= 30.0 {
245 1.56
246 } else if thickness_mils <= 40.0 {
247 1.52
248 } else if thickness_mils <= 50.0 {
249 1.49
250 } else if thickness_mils <= 60.0 {
251 1.46
252 } else if thickness_mils <= 70.0 {
253 1.43
254 } else if thickness_mils <= 80.0 {
255 1.41
256 } else if thickness_mils <= 90.0 {
257 1.37
258 } else if thickness_mils <= 100.0 {
259 1.34
260 } else {
261 1.24
262 }
263 } else {
264 if thickness_mils <= 10.0 {
265 1.59
266 } else if thickness_mils <= 20.0 {
267 1.55
268 } else if thickness_mils <= 30.0 {
269 1.52
270 } else if thickness_mils <= 40.0 {
271 1.48
272 } else if thickness_mils <= 50.0 {
273 1.45
274 } else if thickness_mils <= 60.0 {
275 1.42
276 } else if thickness_mils <= 70.0 {
277 1.39
278 } else if thickness_mils <= 80.0 {
279 1.37
280 } else if thickness_mils <= 90.0 {
281 1.33
282 } else if thickness_mils <= 100.0 {
283 1.30
284 } else {
285 1.20
286 }
287 }
288}
289
290fn m_area_lookup(area: f64) -> f64 {
291 if area <= 20.0 {
292 3.0364 * area.powf(-0.145)
293 } else if area <= 60.0 {
294 2.9143 * area.powf(-0.129)
295 } else if area <= 100.0 {
296 2.7877 * area.powf(-0.114)
297 } else {
298 2.801 * area.powf(-0.111)
299 }
300}
301
302fn rho_base_lookup(temp: f64) -> f64 {
303 if temp <= -40.0 {
304 0.000519
305 } else if temp <= -20.0 {
306 0.000572
307 } else if temp <= 0.0 {
308 0.000625
309 } else if temp <= 20.0 {
310 0.0006787
311 } else if temp <= 40.0 {
312 0.000732
313 } else if temp <= 60.0 {
314 0.000785
315 } else {
316 0.000839
317 }
318}
319
320pub fn calculate_ipc2152(input: &Ipc2152Input) -> Result<Ipc2152Result, CalcError> {
325 let Ipc2152Input {
326 width,
327 thickness,
328 length,
329 temperature_rise,
330 ambient_temp,
331 frequency,
332 ref etch_factor,
333 is_internal,
334 board_thickness_mils,
335 has_copper_plane,
336 material_modifier,
337 user_modifier,
338 } = *input;
339
340 if width <= 0.0 {
341 return Err(CalcError::NegativeDimension { name: "width", value: width });
342 }
343 if thickness <= 0.0 {
344 return Err(CalcError::NegativeDimension { name: "thickness", value: thickness });
345 }
346 if length <= 0.0 {
347 return Err(CalcError::NegativeDimension { name: "length", value: length });
348 }
349 if temperature_rise <= 0.0 {
350 return Err(CalcError::OutOfRange {
351 name: "temperature_rise",
352 value: temperature_rise,
353 expected: "> 0",
354 });
355 }
356 if frequency < 0.0 {
357 return Err(CalcError::OutOfRange {
358 name: "frequency",
359 value: frequency,
360 expected: ">= 0",
361 });
362 }
363 if board_thickness_mils <= 0.0 {
364 return Err(CalcError::NegativeDimension {
365 name: "board_thickness_mils",
366 value: board_thickness_mils,
367 });
368 }
369
370 let cross_section = etch_factor.cross_section_sq_mils(width, thickness);
372
373 let k = if is_internal { K_INTERNAL } else { K_EXTERNAL };
375 let i_base = k * temperature_rise.powf(0.44) * cross_section.powf(0.725);
376
377 let m_area = m_area_lookup(cross_section);
379 let m_temp = m_temp_lookup(temperature_rise);
380 let m_board = m_board_lookup(board_thickness_mils, has_copper_plane);
381
382 let current_capacity = i_base * m_area * m_temp * m_board * material_modifier * user_modifier;
383
384 let rho_base = rho_base_lookup(ambient_temp);
386 let rho_adj = (1.0 + 0.00393 * (ambient_temp - 20.0)) * rho_base;
387 let resistance_dc = rho_adj * length / cross_section;
388
389 let voltage_drop = current_capacity * resistance_dc;
390 let power_dissipation = current_capacity * voltage_drop;
391 let current_density = current_capacity / cross_section;
392
393 let skin_depth_mils = if frequency > 0.0 {
394 let delta_m = (COPPER_RESISTIVITY_OHM_M
395 / (std::f64::consts::PI * frequency * MU_0))
396 .sqrt();
397 delta_m / crate::constants::MIL_TO_M
398 } else {
399 0.0
400 };
401
402 Ok(Ipc2152Result {
403 current_capacity,
404 cross_section,
405 resistance_dc,
406 voltage_drop,
407 power_dissipation,
408 current_density,
409 skin_depth_mils,
410 m_area,
411 m_temp,
412 m_board,
413 })
414}
415
416#[cfg(test)]
417mod tests {
418 use approx::assert_relative_eq;
419
420 use super::*;
421
422 #[test]
424 fn skin_depth_1mhz() {
425 let result = calculate(&CurrentInput {
426 width: 10.0,
427 thickness: 1.4,
428 length: 1000.0,
429 temperature_rise: 10.0,
430 ambient_temp: 25.0,
431 frequency: 1_000_000.0,
432 etch_factor: EtchFactor::None,
433 is_internal: false,
434 })
435 .unwrap();
436
437 assert_relative_eq!(result.skin_depth_mils, 2.599, max_relative = 0.005);
438 }
439
440 #[test]
446 fn ipc2221a_external_a100() {
447 let result = calculate(&CurrentInput {
449 width: 50.0,
450 thickness: 2.0,
451 length: 1000.0,
452 temperature_rise: 10.0,
453 ambient_temp: 25.0,
454 frequency: 0.0,
455 etch_factor: EtchFactor::None,
456 is_internal: false,
457 })
458 .unwrap();
459
460 assert_relative_eq!(result.cross_section, 100.0, max_relative = 1e-10);
461 assert_relative_eq!(result.current_capacity, 3.73, max_relative = 0.005);
462 }
463
464 #[test]
466 fn ipc2221a_internal_a100() {
467 let result = calculate(&CurrentInput {
468 width: 50.0,
469 thickness: 2.0,
470 length: 1000.0,
471 temperature_rise: 10.0,
472 ambient_temp: 25.0,
473 frequency: 0.0,
474 etch_factor: EtchFactor::None,
475 is_internal: true,
476 })
477 .unwrap();
478
479 assert_relative_eq!(result.current_capacity, 1.86, max_relative = 0.005);
480 }
481
482 #[test]
484 fn internal_lower_than_external() {
485 let ext = calculate(&CurrentInput {
486 width: 10.0,
487 thickness: 1.4,
488 length: 1000.0,
489 temperature_rise: 10.0,
490 ambient_temp: 25.0,
491 frequency: 0.0,
492 etch_factor: EtchFactor::None,
493 is_internal: false,
494 })
495 .unwrap();
496
497 let int = calculate(&CurrentInput {
498 width: 10.0,
499 thickness: 1.4,
500 length: 1000.0,
501 temperature_rise: 10.0,
502 ambient_temp: 25.0,
503 frequency: 0.0,
504 etch_factor: EtchFactor::None,
505 is_internal: true,
506 })
507 .unwrap();
508
509 assert_relative_eq!(int.current_capacity / ext.current_capacity, 0.5, max_relative = 1e-10);
511 }
512
513 #[test]
514 fn resistance_and_power() {
515 let result = calculate(&CurrentInput {
516 width: 10.0,
517 thickness: 1.4,
518 length: 1000.0,
519 temperature_rise: 10.0,
520 ambient_temp: 25.0,
521 frequency: 0.0,
522 etch_factor: EtchFactor::None,
523 is_internal: false,
524 })
525 .unwrap();
526
527 let expected_r = RESISTIVITY_OHM_MIL * 1000.0 / 14.0;
529 assert_relative_eq!(result.resistance_dc, expected_r, max_relative = 1e-10);
530
531 assert_relative_eq!(
533 result.voltage_drop,
534 result.current_capacity * result.resistance_dc,
535 max_relative = 1e-10
536 );
537
538 assert_relative_eq!(
540 result.power_dissipation,
541 result.current_capacity * result.voltage_drop,
542 max_relative = 1e-10
543 );
544 }
545
546 #[test]
547 fn rejects_negative_width() {
548 let result = calculate(&CurrentInput {
549 width: -1.0,
550 thickness: 1.4,
551 length: 1000.0,
552 temperature_rise: 10.0,
553 ambient_temp: 25.0,
554 frequency: 0.0,
555 etch_factor: EtchFactor::None,
556 is_internal: false,
557 });
558 assert!(result.is_err());
559 }
560
561 #[test]
562 fn rejects_zero_temperature_rise() {
563 let result = calculate(&CurrentInput {
564 width: 10.0,
565 thickness: 1.4,
566 length: 1000.0,
567 temperature_rise: 0.0,
568 ambient_temp: 25.0,
569 frequency: 0.0,
570 etch_factor: EtchFactor::None,
571 is_internal: false,
572 });
573 assert!(result.is_err());
574 }
575
576 #[test]
577 fn m_temp_boundary_values() {
578 assert_eq!(m_temp_lookup(10.0), 0.40);
579 assert_eq!(m_temp_lookup(10.1), 0.48);
580 assert_eq!(m_temp_lookup(80.0), 1.00);
581 assert_eq!(m_temp_lookup(100.0), 1.20);
582 assert_eq!(m_temp_lookup(101.0), 1.30);
583 }
584
585 #[test]
586 fn m_board_no_plane() {
587 assert_eq!(m_board_lookup(10.0, false), 1.59);
588 assert_eq!(m_board_lookup(50.0, false), 1.45);
589 assert_eq!(m_board_lookup(101.0, false), 1.20);
590 }
591
592 #[test]
593 fn m_board_with_plane() {
594 assert_eq!(m_board_lookup(10.0, true), 1.63);
595 assert_eq!(m_board_lookup(50.0, true), 1.49);
596 assert_eq!(m_board_lookup(101.0, true), 1.24);
597 }
598
599 #[test]
600 fn m_area_segments() {
601 let a20 = m_area_lookup(20.0);
602 let a60 = m_area_lookup(60.0);
603 let a100 = m_area_lookup(100.0);
604 assert!(a20 > a60);
605 assert!(a60 > a100);
606 assert_relative_eq!(a100, 2.7877 * 100.0_f64.powf(-0.114), max_relative = 1e-6);
607 }
608
609 #[test]
610 fn rho_base_lookup_values() {
611 assert_eq!(rho_base_lookup(-50.0), 0.000519);
612 assert_eq!(rho_base_lookup(20.0), 0.0006787);
613 assert_eq!(rho_base_lookup(90.0), 0.000839);
614 }
615
616 #[test]
617 fn ipc2152_modifiers_applied() {
618 let result = calculate_ipc2152(&Ipc2152Input {
619 width: 50.0,
620 thickness: 2.0,
621 length: 1000.0,
622 temperature_rise: 10.0,
623 ambient_temp: 25.0,
624 frequency: 0.0,
625 etch_factor: EtchFactor::None,
626 is_internal: false,
627 board_thickness_mils: 62.0,
628 has_copper_plane: false,
629 material_modifier: 1.0,
630 user_modifier: 1.0,
631 })
632 .unwrap();
633
634 assert_relative_eq!(result.cross_section, 100.0, max_relative = 1e-10);
635 let i_base = K_EXTERNAL * 10.0_f64.powf(0.44) * 100.0_f64.powf(0.725);
636 assert!(result.current_capacity != i_base, "IPC-2152 should differ from IPC-2221A base");
637 assert!(result.m_area > 0.0);
638 assert_relative_eq!(result.m_temp, 0.40, max_relative = 1e-10);
639 assert_relative_eq!(result.m_board, 1.39, max_relative = 1e-10);
640 }
641
642 #[test]
643 fn ipc2152_keeps_existing_tests_unaffected() {
644 let result = calculate(&CurrentInput {
645 width: 50.0,
646 thickness: 2.0,
647 length: 1000.0,
648 temperature_rise: 10.0,
649 ambient_temp: 25.0,
650 frequency: 0.0,
651 etch_factor: EtchFactor::None,
652 is_internal: false,
653 })
654 .unwrap();
655 assert_relative_eq!(result.current_capacity, 3.73, max_relative = 0.005);
656 }
657}