use std::f64::consts::PI;
#[derive(Debug, Clone, PartialEq)]
pub struct ElectricField {
pub ex: f64,
pub ey: f64,
pub ez: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MagneticField {
pub bx: f64,
pub by: f64,
pub bz: f64,
}
#[derive(Debug, Clone)]
pub struct PointCharge {
pub charge_c: f64,
pub x: f64,
pub y: f64,
pub z: f64,
}
#[derive(Debug, Clone)]
pub struct CurrentLoop {
pub current_a: f64,
pub radius_m: f64,
pub center: (f64, f64, f64),
pub axis: (f64, f64, f64),
}
pub struct ElectromagneticsCalculator;
impl ElectromagneticsCalculator {
pub const K_E: f64 = 8.987_551_787e9;
pub const MU_0: f64 = 1.256_637_061_4e-6;
pub const EPSILON_0: f64 = 8.854_187_817e-12;
pub const C: f64 = 2.997_924_458e8;
pub fn electric_field(charge: &PointCharge, x: f64, y: f64, z: f64) -> ElectricField {
let dx = x - charge.x;
let dy = y - charge.y;
let dz = z - charge.z;
let r_sq = dx * dx + dy * dy + dz * dz;
if r_sq < f64::EPSILON {
return ElectricField::zero();
}
let r = r_sq.sqrt();
let factor = Self::K_E * charge.charge_c / (r_sq * r);
ElectricField {
ex: factor * dx,
ey: factor * dy,
ez: factor * dz,
}
}
pub fn coulomb_force(q1: f64, q2: f64, r: f64) -> f64 {
if r <= 0.0 {
return 0.0;
}
Self::K_E * q1.abs() * q2.abs() / (r * r)
}
pub fn superposition(charges: &[PointCharge], x: f64, y: f64, z: f64) -> ElectricField {
let mut total = ElectricField::zero();
for charge in charges {
let e = Self::electric_field(charge, x, y, z);
total = total.add(&e);
}
total
}
pub fn solenoid_field_axial(n_turns: f64, current_a: f64, length_m: f64) -> f64 {
if length_m <= 0.0 {
return 0.0;
}
let n = n_turns / length_m; Self::MU_0 * n * current_a
}
pub fn lorentz_force(
q: f64,
e: &ElectricField,
b: &MagneticField,
v: (f64, f64, f64),
) -> (f64, f64, f64) {
let (vx, vy, vz) = v;
let cross_x = vy * b.bz - vz * b.by;
let cross_y = vz * b.bx - vx * b.bz;
let cross_z = vx * b.by - vy * b.bx;
let fx = q * (e.ex + cross_x);
let fy = q * (e.ey + cross_y);
let fz = q * (e.ez + cross_z);
(fx, fy, fz)
}
pub fn electric_field_energy(e_magnitude: f64, volume_m3: f64) -> f64 {
0.5 * Self::EPSILON_0 * e_magnitude * e_magnitude * volume_m3
}
pub fn capacitor_voltage(v0: f64, c: f64, r: f64, t: f64) -> f64 {
if r <= 0.0 || c <= 0.0 {
return v0;
}
let tau = r * c;
v0 * (-t / tau).exp()
}
pub fn inductor_current(v: f64, l: f64, r: f64, t: f64) -> f64 {
if r <= 0.0 || l <= 0.0 {
return 0.0;
}
(v / r) * (1.0 - (-r * t / l).exp())
}
pub fn skin_depth(resistivity: f64, frequency_hz: f64) -> f64 {
if frequency_hz <= 0.0 || resistivity < 0.0 {
return f64::INFINITY;
}
let omega = 2.0 * PI * frequency_hz;
(2.0 * resistivity / (omega * Self::MU_0)).sqrt()
}
pub fn loop_field_axial(loop_: &CurrentLoop, axial_distance: f64) -> f64 {
let r = loop_.radius_m;
let i = loop_.current_a;
let r_sq = r * r;
let z_sq = axial_distance * axial_distance;
let denom = (r_sq + z_sq).powf(1.5);
if denom < f64::EPSILON {
return 0.0;
}
Self::MU_0 * i * r_sq / (2.0 * denom)
}
}
impl ElectricField {
pub fn magnitude(&self) -> f64 {
(self.ex * self.ex + self.ey * self.ey + self.ez * self.ez).sqrt()
}
pub fn zero() -> Self {
ElectricField {
ex: 0.0,
ey: 0.0,
ez: 0.0,
}
}
pub fn add(&self, other: &ElectricField) -> ElectricField {
ElectricField {
ex: self.ex + other.ex,
ey: self.ey + other.ey,
ez: self.ez + other.ez,
}
}
}
impl MagneticField {
pub fn magnitude(&self) -> f64 {
(self.bx * self.bx + self.by * self.by + self.bz * self.bz).sqrt()
}
pub fn zero() -> Self {
MagneticField {
bx: 0.0,
by: 0.0,
bz: 0.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-6;
#[test]
fn test_electric_field_magnitude_zero() {
let e = ElectricField::zero();
assert_eq!(e.magnitude(), 0.0);
}
#[test]
fn test_electric_field_magnitude_unit() {
let e = ElectricField { ex: 1.0, ey: 0.0, ez: 0.0 };
assert!((e.magnitude() - 1.0).abs() < TOL);
}
#[test]
fn test_electric_field_magnitude_3d() {
let e = ElectricField { ex: 3.0, ey: 4.0, ez: 0.0 };
assert!((e.magnitude() - 5.0).abs() < TOL);
}
#[test]
fn test_electric_field_add() {
let e1 = ElectricField { ex: 1.0, ey: 2.0, ez: 3.0 };
let e2 = ElectricField { ex: 4.0, ey: 5.0, ez: 6.0 };
let sum = e1.add(&e2);
assert!((sum.ex - 5.0).abs() < TOL);
assert!((sum.ey - 7.0).abs() < TOL);
assert!((sum.ez - 9.0).abs() < TOL);
}
#[test]
fn test_electric_field_add_zero() {
let e = ElectricField { ex: 3.0, ey: -1.0, ez: 2.5 };
let zero = ElectricField::zero();
let sum = e.add(&zero);
assert!((sum.ex - e.ex).abs() < TOL);
assert!((sum.ey - e.ey).abs() < TOL);
assert!((sum.ez - e.ez).abs() < TOL);
}
#[test]
fn test_magnetic_field_magnitude_zero() {
let b = MagneticField::zero();
assert_eq!(b.magnitude(), 0.0);
}
#[test]
fn test_magnetic_field_magnitude_345() {
let b = MagneticField { bx: 3.0, by: 4.0, bz: 0.0 };
assert!((b.magnitude() - 5.0).abs() < TOL);
}
#[test]
fn test_electric_field_single_charge_at_distance_1() {
let charge = PointCharge { charge_c: 1.0, x: 0.0, y: 0.0, z: 0.0 };
let e = ElectromagneticsCalculator::electric_field(&charge, 1.0, 0.0, 0.0);
let expected = ElectromagneticsCalculator::K_E;
assert!((e.magnitude() - expected).abs() / expected < 1e-9);
assert!((e.ex - expected).abs() / expected < 1e-9);
assert!(e.ey.abs() < TOL);
assert!(e.ez.abs() < TOL);
}
#[test]
fn test_electric_field_inverse_square_law() {
let charge = PointCharge { charge_c: 1.0e-6, x: 0.0, y: 0.0, z: 0.0 };
let e1 = ElectromagneticsCalculator::electric_field(&charge, 1.0, 0.0, 0.0);
let e2 = ElectromagneticsCalculator::electric_field(&charge, 2.0, 0.0, 0.0);
let ratio = e1.magnitude() / e2.magnitude();
assert!((ratio - 4.0).abs() < 1e-6);
}
#[test]
fn test_electric_field_negative_charge_direction() {
let charge = PointCharge { charge_c: -1.0, x: 0.0, y: 0.0, z: 0.0 };
let e = ElectromagneticsCalculator::electric_field(&charge, 1.0, 0.0, 0.0);
assert!(e.ex < 0.0, "field should point toward negative charge");
}
#[test]
fn test_electric_field_at_charge_location_is_zero() {
let charge = PointCharge { charge_c: 5.0, x: 3.0, y: 3.0, z: 3.0 };
let e = ElectromagneticsCalculator::electric_field(&charge, 3.0, 3.0, 3.0);
assert_eq!(e.magnitude(), 0.0);
}
#[test]
fn test_electric_field_symmetry() {
let charge = PointCharge { charge_c: 1.0, x: 0.0, y: 0.0, z: 0.0 };
let e_pos = ElectromagneticsCalculator::electric_field(&charge, 2.0, 0.0, 0.0);
let e_neg = ElectromagneticsCalculator::electric_field(&charge, -2.0, 0.0, 0.0);
assert!((e_pos.magnitude() - e_neg.magnitude()).abs() < TOL);
assert!((e_pos.ex + e_neg.ex).abs() < TOL);
}
#[test]
fn test_coulomb_force_unit_charges() {
let f = ElectromagneticsCalculator::coulomb_force(1.0, 1.0, 1.0);
assert!((f - ElectromagneticsCalculator::K_E).abs() / ElectromagneticsCalculator::K_E < 1e-9);
}
#[test]
fn test_coulomb_force_inverse_square() {
let f1 = ElectromagneticsCalculator::coulomb_force(1.0, 1.0, 1.0);
let f2 = ElectromagneticsCalculator::coulomb_force(1.0, 1.0, 2.0);
let ratio = f1 / f2;
assert!((ratio - 4.0).abs() < 1e-9);
}
#[test]
fn test_coulomb_force_zero_distance_returns_zero() {
let f = ElectromagneticsCalculator::coulomb_force(1.0, 1.0, 0.0);
assert_eq!(f, 0.0);
}
#[test]
fn test_coulomb_force_always_positive() {
let f = ElectromagneticsCalculator::coulomb_force(-2.0, -3.0, 0.5);
assert!(f > 0.0);
}
#[test]
fn test_superposition_two_opposite_charges_cancel_on_axis() {
let charges = vec![
PointCharge { charge_c: 1.0, x: -1.0, y: 0.0, z: 0.0 },
PointCharge { charge_c: -1.0, x: 1.0, y: 0.0, z: 0.0 },
];
let e = ElectromagneticsCalculator::superposition(&charges, 0.0, 0.0, 0.0);
assert!(e.ey.abs() < TOL);
assert!(e.ez.abs() < TOL);
}
#[test]
fn test_superposition_two_equal_charges_double_field() {
let single = PointCharge { charge_c: 1.0e-9, x: 0.0, y: 0.0, z: 0.0 };
let double = vec![
PointCharge { charge_c: 1.0e-9, x: 0.0, y: 0.0, z: 0.0 },
PointCharge { charge_c: 1.0e-9, x: 0.0, y: 0.0, z: 0.0 },
];
let e1 = ElectromagneticsCalculator::electric_field(&single, 1.0, 0.0, 0.0);
let e2 = ElectromagneticsCalculator::superposition(&double, 1.0, 0.0, 0.0);
assert!((e2.magnitude() - 2.0 * e1.magnitude()).abs() / e1.magnitude() < 1e-9);
}
#[test]
fn test_superposition_empty_charges() {
let charges: Vec<PointCharge> = vec![];
let e = ElectromagneticsCalculator::superposition(&charges, 1.0, 2.0, 3.0);
assert_eq!(e.magnitude(), 0.0);
}
#[test]
fn test_superposition_three_charges() {
let charges = vec![
PointCharge { charge_c: 1.0e-9, x: 0.0, y: 0.0, z: 0.0 },
PointCharge { charge_c: 2.0e-9, x: 1.0, y: 0.0, z: 0.0 },
PointCharge { charge_c: -1.0e-9, x: -1.0, y: 0.0, z: 0.0 },
];
let e = ElectromagneticsCalculator::superposition(&charges, 5.0, 0.0, 0.0);
assert!(e.magnitude().is_finite());
}
#[test]
fn test_electric_field_energy_zero_volume() {
let u = ElectromagneticsCalculator::electric_field_energy(1000.0, 0.0);
assert_eq!(u, 0.0);
}
#[test]
fn test_electric_field_energy_proportional_to_e_squared() {
let u1 = ElectromagneticsCalculator::electric_field_energy(100.0, 1.0);
let u2 = ElectromagneticsCalculator::electric_field_energy(200.0, 1.0);
assert!((u2 / u1 - 4.0).abs() < 1e-9);
}
#[test]
fn test_electric_field_energy_proportional_to_volume() {
let u1 = ElectromagneticsCalculator::electric_field_energy(100.0, 1.0);
let u2 = ElectromagneticsCalculator::electric_field_energy(100.0, 3.0);
assert!((u2 / u1 - 3.0).abs() < 1e-9);
}
#[test]
fn test_electric_field_energy_positive() {
let u = ElectromagneticsCalculator::electric_field_energy(1.0e3, 0.001);
assert!(u > 0.0);
}
#[test]
fn test_capacitor_voltage_at_t0() {
let v = ElectromagneticsCalculator::capacitor_voltage(12.0, 1.0e-3, 1000.0, 0.0);
assert!((v - 12.0).abs() < TOL);
}
#[test]
fn test_capacitor_voltage_at_one_tau() {
let v0 = 10.0;
let r = 1000.0;
let c = 1.0e-3;
let tau = r * c;
let v = ElectromagneticsCalculator::capacitor_voltage(v0, c, r, tau);
let expected = v0 * (-1.0f64).exp();
assert!((v - expected).abs() / expected < 1e-9);
}
#[test]
fn test_capacitor_voltage_decays_monotonically() {
let v0 = 5.0;
let r = 100.0;
let c = 1.0e-4;
let v1 = ElectromagneticsCalculator::capacitor_voltage(v0, c, r, 0.001);
let v2 = ElectromagneticsCalculator::capacitor_voltage(v0, c, r, 0.002);
assert!(v2 < v1);
}
#[test]
fn test_capacitor_voltage_approaches_zero() {
let v = ElectromagneticsCalculator::capacitor_voltage(100.0, 1.0e-3, 1.0, 1000.0);
assert!(v < 1.0e-10);
}
#[test]
fn test_capacitor_voltage_37_percent_at_tau() {
let v0 = 100.0;
let r = 1.0;
let c = 1.0;
let v = ElectromagneticsCalculator::capacitor_voltage(v0, c, r, r * c);
let pct = v / v0;
assert!((pct - 0.367879441).abs() < 1e-7);
}
#[test]
fn test_inductor_current_at_t0() {
let i = ElectromagneticsCalculator::inductor_current(12.0, 0.01, 100.0, 0.0);
assert!(i.abs() < TOL);
}
#[test]
fn test_inductor_current_at_one_tau() {
let v = 10.0;
let l = 0.1;
let r = 10.0;
let tau = l / r;
let i = ElectromagneticsCalculator::inductor_current(v, l, r, tau);
let expected = (v / r) * (1.0 - (-1.0f64).exp());
assert!((i - expected).abs() / expected < 1e-9);
}
#[test]
fn test_inductor_current_approaches_steady_state() {
let v = 5.0;
let l = 1.0;
let r = 1.0;
let i = ElectromagneticsCalculator::inductor_current(v, l, r, 1000.0);
assert!((i - v / r).abs() < 1.0e-9);
}
#[test]
fn test_inductor_current_monotone_rise() {
let v = 12.0;
let l = 0.05;
let r = 50.0;
let i1 = ElectromagneticsCalculator::inductor_current(v, l, r, 0.0005);
let i2 = ElectromagneticsCalculator::inductor_current(v, l, r, 0.001);
assert!(i2 > i1);
}
#[test]
fn test_inductor_current_63_percent_at_tau() {
let v = 100.0;
let l = 1.0;
let r = 1.0;
let tau = l / r;
let i = ElectromagneticsCalculator::inductor_current(v, l, r, tau);
let pct = i / (v / r);
assert!((pct - 0.632120559).abs() < 1e-7);
}
#[test]
fn test_skin_depth_copper_at_50hz() {
let delta = ElectromagneticsCalculator::skin_depth(1.7e-8, 50.0);
assert!(delta > 0.005 && delta < 0.02, "copper 50Hz skin depth should be ~9mm, got {:.4} m", delta);
}
#[test]
fn test_skin_depth_decreases_with_frequency() {
let d1 = ElectromagneticsCalculator::skin_depth(1.7e-8, 50.0);
let d2 = ElectromagneticsCalculator::skin_depth(1.7e-8, 50000.0);
assert!(d2 < d1, "higher frequency → smaller skin depth");
}
#[test]
fn test_skin_depth_increases_with_resistivity() {
let d1 = ElectromagneticsCalculator::skin_depth(1.7e-8, 1000.0); let d2 = ElectromagneticsCalculator::skin_depth(1.0e-6, 1000.0); assert!(d2 > d1);
}
#[test]
fn test_skin_depth_formula_ratio() {
let d1 = ElectromagneticsCalculator::skin_depth(1.0e-7, 100.0);
let d2 = ElectromagneticsCalculator::skin_depth(1.0e-7, 400.0);
let ratio = d1 / d2;
assert!((ratio - 2.0).abs() < 1e-6);
}
#[test]
fn test_skin_depth_zero_frequency_returns_infinity() {
let d = ElectromagneticsCalculator::skin_depth(1.0e-7, 0.0);
assert!(d.is_infinite());
}
#[test]
fn test_solenoid_field_proportional_to_n_times_i_over_l() {
let b1 = ElectromagneticsCalculator::solenoid_field_axial(100.0, 1.0, 0.1);
let b2 = ElectromagneticsCalculator::solenoid_field_axial(200.0, 1.0, 0.1);
assert!((b2 / b1 - 2.0).abs() < 1e-9);
}
#[test]
fn test_solenoid_field_proportional_to_current() {
let b1 = ElectromagneticsCalculator::solenoid_field_axial(1000.0, 1.0, 1.0);
let b2 = ElectromagneticsCalculator::solenoid_field_axial(1000.0, 3.0, 1.0);
assert!((b2 / b1 - 3.0).abs() < 1e-9);
}
#[test]
fn test_solenoid_field_1000_turns_1m_1a() {
let b = ElectromagneticsCalculator::solenoid_field_axial(1000.0, 1.0, 1.0);
let expected = ElectromagneticsCalculator::MU_0 * 1000.0;
assert!((b - expected).abs() / expected < 1e-9);
}
#[test]
fn test_solenoid_field_zero_length_returns_zero() {
let b = ElectromagneticsCalculator::solenoid_field_axial(1000.0, 1.0, 0.0);
assert_eq!(b, 0.0);
}
#[test]
fn test_lorentz_force_electric_only() {
let e = ElectricField { ex: 1000.0, ey: 0.0, ez: 0.0 };
let b = MagneticField::zero();
let (fx, fy, fz) = ElectromagneticsCalculator::lorentz_force(1.0e-6, &e, &b, (0.0, 0.0, 0.0));
assert!((fx - 1.0e-3).abs() < 1e-12);
assert!(fy.abs() < TOL);
assert!(fz.abs() < TOL);
}
#[test]
fn test_lorentz_force_magnetic_cross_product() {
let e = ElectricField::zero();
let b = MagneticField { bx: 0.0, by: 0.0, bz: 1.0 };
let q = 2.0;
let (fx, fy, fz) = ElectromagneticsCalculator::lorentz_force(q, &e, &b, (1.0, 0.0, 0.0));
assert!(fx.abs() < TOL);
assert!((fy - (-2.0)).abs() < TOL); assert!(fz.abs() < TOL);
}
#[test]
fn test_lorentz_force_both_fields() {
let e = ElectricField { ex: 100.0, ey: 0.0, ez: 0.0 };
let b = MagneticField { bx: 0.0, by: 0.0, bz: 1.0 };
let q = 1.0e-6;
let v = (1.0e4, 0.0, 0.0);
let (fx, fy, fz) = ElectromagneticsCalculator::lorentz_force(q, &e, &b, v);
assert!((fx - q * 100.0).abs() < 1e-18);
assert!((fy - q * (-1.0e4)).abs() < 1e-18);
assert!(fz.abs() < 1e-18);
}
#[test]
fn test_lorentz_force_zero_charge() {
let e = ElectricField { ex: 1.0e6, ey: 1.0e6, ez: 1.0e6 };
let b = MagneticField { bx: 1.0, by: 1.0, bz: 1.0 };
let (fx, fy, fz) = ElectromagneticsCalculator::lorentz_force(0.0, &e, &b, (1000.0, 1000.0, 1000.0));
assert_eq!(fx, 0.0);
assert_eq!(fy, 0.0);
assert_eq!(fz, 0.0);
}
#[test]
fn test_loop_field_at_center() {
let loop_ = CurrentLoop {
current_a: 1.0,
radius_m: 0.1,
center: (0.0, 0.0, 0.0),
axis: (0.0, 0.0, 1.0),
};
let b = ElectromagneticsCalculator::loop_field_axial(&loop_, 0.0);
let expected = ElectromagneticsCalculator::MU_0 * 1.0 / (2.0 * 0.1);
assert!((b - expected).abs() / expected < 1e-9);
}
#[test]
fn test_loop_field_decreases_with_distance() {
let loop_ = CurrentLoop {
current_a: 10.0,
radius_m: 0.05,
center: (0.0, 0.0, 0.0),
axis: (0.0, 0.0, 1.0),
};
let b0 = ElectromagneticsCalculator::loop_field_axial(&loop_, 0.0);
let b1 = ElectromagneticsCalculator::loop_field_axial(&loop_, 0.1);
assert!(b1 < b0);
}
#[test]
fn test_loop_field_proportional_to_current() {
let make_loop = |i: f64| CurrentLoop {
current_a: i,
radius_m: 0.05,
center: (0.0, 0.0, 0.0),
axis: (0.0, 0.0, 1.0),
};
let b1 = ElectromagneticsCalculator::loop_field_axial(&make_loop(1.0), 0.0);
let b2 = ElectromagneticsCalculator::loop_field_axial(&make_loop(5.0), 0.0);
assert!((b2 / b1 - 5.0).abs() < 1e-9);
}
}