use pantometry_core::{Domain, Exchange};
use pantometry_electrical::{Winding, COPPER_ALPHA, COPPER_RESISTIVITY_20C, HEAT};
use pantometry_units::{Conductance, Current, Length, Resistance, Temperature, Time, Voltage};
#[test]
fn resistance_is_resistivity_times_length_over_area() {
let (length, area) = (Length::m(24.0), 0.35e-6);
let coil = Winding::of_copper("coil", length, area, Temperature::celsius(20.0));
let want = COPPER_RESISTIVITY_20C * 24.0 / 0.35e-6;
assert!(
(coil.resistance().to_si() / want - 1.0).abs() < 1e-12,
"{} against {want}",
coil.resistance().to_si()
);
let twice_as_long = Winding::of_copper("b", Length::m(48.0), area, Temperature::celsius(20.0));
let twice_as_fat = Winding::of_copper("c", length, 2.0 * area, Temperature::celsius(20.0));
assert!((twice_as_long.resistance().to_si() / want - 2.0).abs() < 1e-12);
assert!((twice_as_fat.resistance().to_si() / want - 0.5).abs() < 1e-12);
}
#[test]
fn copper_gains_four_tenths_of_a_percent_per_kelvin() {
let make = |c: f64| {
Winding::of_copper("coil", Length::m(10.0), 1e-6, Temperature::celsius(c))
.resistance()
.to_si()
};
let r20 = make(20.0);
assert_eq!(make(20.0), r20, "20 C is the reference point exactly");
assert!((make(21.0) / r20 - (1.0 + COPPER_ALPHA)).abs() < 1e-12);
assert!(
(make(120.0) / r20 - 1.393).abs() < 1e-12,
"at 120 C: {}",
make(120.0) / r20
);
assert!(make(-30.0) < r20);
}
#[test]
fn the_two_drive_modes_meet_at_the_same_operating_point() {
let build = || Winding::of_copper("coil", Length::m(24.0), 0.35e-6, Temperature::celsius(75.0));
let by_current = build().driven_at(Current::a(3.0));
let by_voltage = build().driven_from(by_current.voltage());
let (i, v) = (
by_current.dissipation().to_si(),
by_voltage.dissipation().to_si(),
);
assert!(
(i / v - 1.0).abs() < 1e-15,
"{i} against {v}, a relative difference of {:e}",
(i / v - 1.0).abs()
);
assert!((by_voltage.current().to_si() - 3.0).abs() < 1e-12);
let p = by_current.voltage().to_si() * by_current.current().to_si();
assert!((by_current.dissipation().to_si() / p - 1.0).abs() < 1e-12);
}
#[test]
fn current_drive_rises_with_temperature_and_voltage_drive_falls() {
let mut hot_i = Winding::of_copper("i", Length::m(10.0), 1e-6, Temperature::celsius(20.0))
.driven_at(Current::a(2.0));
let cold_watts = hot_i.dissipation().to_si();
hot_i.at_temperature(Temperature::celsius(120.0));
assert!(
(hot_i.dissipation().to_si() / cold_watts - 1.393).abs() < 1e-12,
"constant current should rise by exactly the resistance ratio"
);
let mut hot_v = Winding::of_copper("v", Length::m(10.0), 1e-6, Temperature::celsius(20.0))
.driven_from(Voltage::v(5.0));
let cold_watts = hot_v.dissipation().to_si();
hot_v.at_temperature(Temperature::celsius(120.0));
assert!(
(hot_v.dissipation().to_si() / cold_watts - 1.0 / 1.393).abs() < 1e-12,
"constant voltage should fall by exactly the resistance ratio"
);
}
#[test]
fn an_undriven_winding_publishes_nothing_at_all() {
let mut coil = Winding::of_copper("coil", Length::m(10.0), 1e-6, Temperature::celsius(20.0));
assert_eq!(coil.dissipation().to_si(), 0.0);
let mut bus = Exchange::new();
coil.step(Time::s(0.0), Time::s(1.0), &mut bus).unwrap();
assert_eq!(bus.peek(HEAT), 0.0);
let shorted = Winding::of_resistance(
"short",
Resistance::ohm(0.0),
0.0,
Temperature::celsius(20.0),
)
.driven_from(Voltage::v(12.0));
assert_eq!(shorted.dissipation().to_si(), 0.0);
assert_eq!(shorted.current().to_si(), 0.0);
}
#[test]
fn the_runaway_current_is_where_the_feedback_overtakes_the_heat_path() {
let coil = Winding::of_copper("coil", Length::m(62.0), 0.35e-6, Temperature::celsius(20.0))
.driven_at(Current::a(1.0));
let g = Conductance::w_per_k(0.203);
let crit = coil.runaway_current(g).expect("a current drive has one");
let r_20 = 1.724e-8 * 62.0 / 0.35e-6;
let want = (0.203_f64 / (r_20 * 0.00393)).sqrt();
assert!(
(crit.to_si() / want - 1.0).abs() < 1e-12,
"{} A against {want} A",
crit.to_si()
);
let slope_at = |amps: f64| {
let w = Winding::of_copper("c", Length::m(62.0), 0.35e-6, Temperature::celsius(20.0))
.driven_at(Current::a(amps));
(w.dissipation_at(Temperature::celsius(21.0)).to_si()
- w.dissipation_at(Temperature::celsius(20.0)).to_si())
/ 1.0
};
let below = crit.to_si() * 0.9;
let above = crit.to_si() * 1.1;
assert!(slope_at(below) < 0.203, "below: {} W/K", slope_at(below));
assert!(slope_at(above) > 0.203, "above: {} W/K", slope_at(above));
assert!(
(slope_at(crit.to_si()) / 0.203 - 1.0).abs() < 1e-12,
"at the threshold: {} W/K",
slope_at(crit.to_si())
);
let surface_only = coil
.runaway_current(Conductance::w_per_k(0.294))
.expect("still a current drive");
let series = 1.0 / (1.0 / 0.9 + 1.0 / 2.4 + 1.0 / 0.294);
let with_joints = coil
.runaway_current(Conductance::w_per_k(series))
.expect("still a current drive");
assert!(
(surface_only.to_si() - 4.949).abs() < 1e-3,
"{}",
surface_only.to_si()
);
assert!(
(with_joints.to_si() - 4.111).abs() < 1e-3,
"{}",
with_joints.to_si()
);
assert!(with_joints.to_si() < surface_only.to_si());
let from_volts = Winding::of_copper("v", Length::m(62.0), 0.35e-6, Temperature::celsius(20.0))
.driven_from(Voltage::v(12.0));
assert!(from_volts.runaway_current(g).is_none());
assert!(
from_volts
.dissipation_at(Temperature::celsius(120.0))
.to_si()
< from_volts
.dissipation_at(Temperature::celsius(20.0))
.to_si()
);
}
#[test]
fn the_pure_function_and_the_method_are_the_same_arithmetic() {
for c in [-40.0, 20.0, 75.0, 180.0] {
let w = Winding::of_copper("coil", Length::m(24.0), 0.35e-6, Temperature::celsius(c))
.driven_at(Current::a(3.0));
assert_eq!(
w.dissipation().to_si().to_bits(),
w.dissipation_at(Temperature::celsius(c)).to_si().to_bits()
);
assert_eq!(
w.resistance().to_si().to_bits(),
w.resistance_at(Temperature::celsius(c)).to_si().to_bits()
);
}
}