use serde::{Deserialize, Serialize};
use crate::CalcError;
use crate::copper::EtchFactor;
const K_EXTERNAL: f64 = 0.048;
const K_INTERNAL: f64 = 0.024;
const RESISTIVITY_OHM_MIL: f64 = 6.787e-4;
const COPPER_RESISTIVITY_OHM_M: f64 = 1.724e-8;
const MU_0: f64 = 1.256_637_061_435_9e-6;
pub struct CurrentInput {
pub width: f64,
pub thickness: f64,
pub length: f64,
pub temperature_rise: f64,
pub ambient_temp: f64,
pub frequency: f64,
pub etch_factor: EtchFactor,
pub is_internal: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CurrentResult {
pub current_capacity: f64,
pub cross_section: f64,
pub resistance_dc: f64,
pub voltage_drop: f64,
pub power_dissipation: f64,
pub current_density: f64,
pub skin_depth_mils: f64,
}
pub fn calculate(input: &CurrentInput) -> Result<CurrentResult, CalcError> {
let CurrentInput {
width,
thickness,
length,
temperature_rise,
etch_factor,
is_internal,
frequency,
..
} = *input;
if width <= 0.0 {
return Err(CalcError::NegativeDimension { name: "width", value: width });
}
if thickness <= 0.0 {
return Err(CalcError::NegativeDimension { name: "thickness", value: thickness });
}
if length <= 0.0 {
return Err(CalcError::NegativeDimension { name: "length", value: length });
}
if temperature_rise <= 0.0 {
return Err(CalcError::OutOfRange {
name: "temperature_rise",
value: temperature_rise,
expected: "> 0",
});
}
if frequency < 0.0 {
return Err(CalcError::OutOfRange {
name: "frequency",
value: frequency,
expected: ">= 0",
});
}
let cross_section = etch_factor.cross_section_sq_mils(width, thickness);
let k = if is_internal { K_INTERNAL } else { K_EXTERNAL };
let current_capacity = k * temperature_rise.powf(0.44) * cross_section.powf(0.725);
let resistance_dc = RESISTIVITY_OHM_MIL * length / cross_section;
let voltage_drop = current_capacity * resistance_dc;
let power_dissipation = current_capacity * voltage_drop;
let current_density = current_capacity / cross_section;
let skin_depth_mils = if frequency > 0.0 {
let delta_m = (COPPER_RESISTIVITY_OHM_M
/ (std::f64::consts::PI * frequency * MU_0))
.sqrt();
delta_m / crate::constants::MIL_TO_M
} else {
0.0
};
Ok(CurrentResult {
current_capacity,
cross_section,
resistance_dc,
voltage_drop,
power_dissipation,
current_density,
skin_depth_mils,
})
}
pub struct Ipc2152Input {
pub width: f64,
pub thickness: f64,
pub length: f64,
pub temperature_rise: f64,
pub ambient_temp: f64,
pub frequency: f64,
pub etch_factor: EtchFactor,
pub is_internal: bool,
pub board_thickness_mils: f64,
pub has_copper_plane: bool,
pub material_modifier: f64,
pub user_modifier: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Ipc2152Result {
pub current_capacity: f64,
pub cross_section: f64,
pub resistance_dc: f64,
pub voltage_drop: f64,
pub power_dissipation: f64,
pub current_density: f64,
pub skin_depth_mils: f64,
pub m_area: f64,
pub m_temp: f64,
pub m_board: f64,
}
fn m_temp_lookup(dt: f64) -> f64 {
if dt <= 10.0 {
0.40
} else if dt <= 20.0 {
0.48
} else if dt <= 30.0 {
0.58
} else if dt <= 40.0 {
0.67
} else if dt <= 50.0 {
0.75
} else if dt <= 60.0 {
0.85
} else if dt <= 70.0 {
0.95
} else if dt <= 80.0 {
1.00
} else if dt <= 90.0 {
1.10
} else if dt <= 100.0 {
1.20
} else {
1.30
}
}
fn m_board_lookup(thickness_mils: f64, has_plane: bool) -> f64 {
if has_plane {
if thickness_mils <= 10.0 {
1.63
} else if thickness_mils <= 20.0 {
1.59
} else if thickness_mils <= 30.0 {
1.56
} else if thickness_mils <= 40.0 {
1.52
} else if thickness_mils <= 50.0 {
1.49
} else if thickness_mils <= 60.0 {
1.46
} else if thickness_mils <= 70.0 {
1.43
} else if thickness_mils <= 80.0 {
1.41
} else if thickness_mils <= 90.0 {
1.37
} else if thickness_mils <= 100.0 {
1.34
} else {
1.24
}
} else {
if thickness_mils <= 10.0 {
1.59
} else if thickness_mils <= 20.0 {
1.55
} else if thickness_mils <= 30.0 {
1.52
} else if thickness_mils <= 40.0 {
1.48
} else if thickness_mils <= 50.0 {
1.45
} else if thickness_mils <= 60.0 {
1.42
} else if thickness_mils <= 70.0 {
1.39
} else if thickness_mils <= 80.0 {
1.37
} else if thickness_mils <= 90.0 {
1.33
} else if thickness_mils <= 100.0 {
1.30
} else {
1.20
}
}
}
fn m_area_lookup(area: f64) -> f64 {
if area <= 20.0 {
3.0364 * area.powf(-0.145)
} else if area <= 60.0 {
2.9143 * area.powf(-0.129)
} else if area <= 100.0 {
2.7877 * area.powf(-0.114)
} else {
2.801 * area.powf(-0.111)
}
}
fn rho_base_lookup(temp: f64) -> f64 {
if temp <= -40.0 {
0.000519
} else if temp <= -20.0 {
0.000572
} else if temp <= 0.0 {
0.000625
} else if temp <= 20.0 {
0.0006787
} else if temp <= 40.0 {
0.000732
} else if temp <= 60.0 {
0.000785
} else {
0.000839
}
}
pub fn calculate_ipc2152(input: &Ipc2152Input) -> Result<Ipc2152Result, CalcError> {
let Ipc2152Input {
width,
thickness,
length,
temperature_rise,
ambient_temp,
frequency,
ref etch_factor,
is_internal,
board_thickness_mils,
has_copper_plane,
material_modifier,
user_modifier,
} = *input;
if width <= 0.0 {
return Err(CalcError::NegativeDimension { name: "width", value: width });
}
if thickness <= 0.0 {
return Err(CalcError::NegativeDimension { name: "thickness", value: thickness });
}
if length <= 0.0 {
return Err(CalcError::NegativeDimension { name: "length", value: length });
}
if temperature_rise <= 0.0 {
return Err(CalcError::OutOfRange {
name: "temperature_rise",
value: temperature_rise,
expected: "> 0",
});
}
if frequency < 0.0 {
return Err(CalcError::OutOfRange {
name: "frequency",
value: frequency,
expected: ">= 0",
});
}
if board_thickness_mils <= 0.0 {
return Err(CalcError::NegativeDimension {
name: "board_thickness_mils",
value: board_thickness_mils,
});
}
let cross_section = etch_factor.cross_section_sq_mils(width, thickness);
let k = if is_internal { K_INTERNAL } else { K_EXTERNAL };
let i_base = k * temperature_rise.powf(0.44) * cross_section.powf(0.725);
let m_area = m_area_lookup(cross_section);
let m_temp = m_temp_lookup(temperature_rise);
let m_board = m_board_lookup(board_thickness_mils, has_copper_plane);
let current_capacity = i_base * m_area * m_temp * m_board * material_modifier * user_modifier;
let rho_base = rho_base_lookup(ambient_temp);
let rho_adj = (1.0 + 0.00393 * (ambient_temp - 20.0)) * rho_base;
let resistance_dc = rho_adj * length / cross_section;
let voltage_drop = current_capacity * resistance_dc;
let power_dissipation = current_capacity * voltage_drop;
let current_density = current_capacity / cross_section;
let skin_depth_mils = if frequency > 0.0 {
let delta_m = (COPPER_RESISTIVITY_OHM_M
/ (std::f64::consts::PI * frequency * MU_0))
.sqrt();
delta_m / crate::constants::MIL_TO_M
} else {
0.0
};
Ok(Ipc2152Result {
current_capacity,
cross_section,
resistance_dc,
voltage_drop,
power_dissipation,
current_density,
skin_depth_mils,
m_area,
m_temp,
m_board,
})
}
#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
use super::*;
#[test]
fn skin_depth_1mhz() {
let result = calculate(&CurrentInput {
width: 10.0,
thickness: 1.4,
length: 1000.0,
temperature_rise: 10.0,
ambient_temp: 25.0,
frequency: 1_000_000.0,
etch_factor: EtchFactor::None,
is_internal: false,
})
.unwrap();
assert_relative_eq!(result.skin_depth_mils, 2.599, max_relative = 0.005);
}
#[test]
fn ipc2221a_external_a100() {
let result = calculate(&CurrentInput {
width: 50.0,
thickness: 2.0,
length: 1000.0,
temperature_rise: 10.0,
ambient_temp: 25.0,
frequency: 0.0,
etch_factor: EtchFactor::None,
is_internal: false,
})
.unwrap();
assert_relative_eq!(result.cross_section, 100.0, max_relative = 1e-10);
assert_relative_eq!(result.current_capacity, 3.73, max_relative = 0.005);
}
#[test]
fn ipc2221a_internal_a100() {
let result = calculate(&CurrentInput {
width: 50.0,
thickness: 2.0,
length: 1000.0,
temperature_rise: 10.0,
ambient_temp: 25.0,
frequency: 0.0,
etch_factor: EtchFactor::None,
is_internal: true,
})
.unwrap();
assert_relative_eq!(result.current_capacity, 1.86, max_relative = 0.005);
}
#[test]
fn internal_lower_than_external() {
let ext = calculate(&CurrentInput {
width: 10.0,
thickness: 1.4,
length: 1000.0,
temperature_rise: 10.0,
ambient_temp: 25.0,
frequency: 0.0,
etch_factor: EtchFactor::None,
is_internal: false,
})
.unwrap();
let int = calculate(&CurrentInput {
width: 10.0,
thickness: 1.4,
length: 1000.0,
temperature_rise: 10.0,
ambient_temp: 25.0,
frequency: 0.0,
etch_factor: EtchFactor::None,
is_internal: true,
})
.unwrap();
assert_relative_eq!(int.current_capacity / ext.current_capacity, 0.5, max_relative = 1e-10);
}
#[test]
fn resistance_and_power() {
let result = calculate(&CurrentInput {
width: 10.0,
thickness: 1.4,
length: 1000.0,
temperature_rise: 10.0,
ambient_temp: 25.0,
frequency: 0.0,
etch_factor: EtchFactor::None,
is_internal: false,
})
.unwrap();
let expected_r = RESISTIVITY_OHM_MIL * 1000.0 / 14.0;
assert_relative_eq!(result.resistance_dc, expected_r, max_relative = 1e-10);
assert_relative_eq!(
result.voltage_drop,
result.current_capacity * result.resistance_dc,
max_relative = 1e-10
);
assert_relative_eq!(
result.power_dissipation,
result.current_capacity * result.voltage_drop,
max_relative = 1e-10
);
}
#[test]
fn rejects_negative_width() {
let result = calculate(&CurrentInput {
width: -1.0,
thickness: 1.4,
length: 1000.0,
temperature_rise: 10.0,
ambient_temp: 25.0,
frequency: 0.0,
etch_factor: EtchFactor::None,
is_internal: false,
});
assert!(result.is_err());
}
#[test]
fn rejects_zero_temperature_rise() {
let result = calculate(&CurrentInput {
width: 10.0,
thickness: 1.4,
length: 1000.0,
temperature_rise: 0.0,
ambient_temp: 25.0,
frequency: 0.0,
etch_factor: EtchFactor::None,
is_internal: false,
});
assert!(result.is_err());
}
#[test]
fn m_temp_boundary_values() {
assert_eq!(m_temp_lookup(10.0), 0.40);
assert_eq!(m_temp_lookup(10.1), 0.48);
assert_eq!(m_temp_lookup(80.0), 1.00);
assert_eq!(m_temp_lookup(100.0), 1.20);
assert_eq!(m_temp_lookup(101.0), 1.30);
}
#[test]
fn m_board_no_plane() {
assert_eq!(m_board_lookup(10.0, false), 1.59);
assert_eq!(m_board_lookup(50.0, false), 1.45);
assert_eq!(m_board_lookup(101.0, false), 1.20);
}
#[test]
fn m_board_with_plane() {
assert_eq!(m_board_lookup(10.0, true), 1.63);
assert_eq!(m_board_lookup(50.0, true), 1.49);
assert_eq!(m_board_lookup(101.0, true), 1.24);
}
#[test]
fn m_area_segments() {
let a20 = m_area_lookup(20.0);
let a60 = m_area_lookup(60.0);
let a100 = m_area_lookup(100.0);
assert!(a20 > a60);
assert!(a60 > a100);
assert_relative_eq!(a100, 2.7877 * 100.0_f64.powf(-0.114), max_relative = 1e-6);
}
#[test]
fn rho_base_lookup_values() {
assert_eq!(rho_base_lookup(-50.0), 0.000519);
assert_eq!(rho_base_lookup(20.0), 0.0006787);
assert_eq!(rho_base_lookup(90.0), 0.000839);
}
#[test]
fn ipc2152_modifiers_applied() {
let result = calculate_ipc2152(&Ipc2152Input {
width: 50.0,
thickness: 2.0,
length: 1000.0,
temperature_rise: 10.0,
ambient_temp: 25.0,
frequency: 0.0,
etch_factor: EtchFactor::None,
is_internal: false,
board_thickness_mils: 62.0,
has_copper_plane: false,
material_modifier: 1.0,
user_modifier: 1.0,
})
.unwrap();
assert_relative_eq!(result.cross_section, 100.0, max_relative = 1e-10);
let i_base = K_EXTERNAL * 10.0_f64.powf(0.44) * 100.0_f64.powf(0.725);
assert!(result.current_capacity != i_base, "IPC-2152 should differ from IPC-2221A base");
assert!(result.m_area > 0.0);
assert_relative_eq!(result.m_temp, 0.40, max_relative = 1e-10);
assert_relative_eq!(result.m_board, 1.39, max_relative = 1e-10);
}
#[test]
fn ipc2152_keeps_existing_tests_unaffected() {
let result = calculate(&CurrentInput {
width: 50.0,
thickness: 2.0,
length: 1000.0,
temperature_rise: 10.0,
ambient_temp: 25.0,
frequency: 0.0,
etch_factor: EtchFactor::None,
is_internal: false,
})
.unwrap();
assert_relative_eq!(result.current_capacity, 3.73, max_relative = 0.005);
}
}