use arika::earth::ellipsoid::{WGS84_A, WGS84_E2};
use nalgebra::Vector3;
use super::{MagneticFieldInput, MagneticFieldModel};
const EARTH_DIPOLE_STRENGTH: f64 = 7.94e15;
pub struct TiltedDipole {
dipole_strength: f64,
axis_ecef: Vector3<f64>,
}
impl TiltedDipole {
pub fn new(dipole_strength: f64, axis_ecef: Vector3<f64>) -> Self {
let norm = axis_ecef.magnitude();
assert!(norm > 1e-15, "Dipole axis must be non-zero");
Self {
dipole_strength,
axis_ecef: axis_ecef / norm,
}
}
pub fn earth() -> Self {
let tilt = 11.5_f64.to_radians();
Self {
dipole_strength: EARTH_DIPOLE_STRENGTH,
axis_ecef: Vector3::new(tilt.sin(), 0.0, tilt.cos()).normalize(),
}
}
fn compute_field_ecef(&self, position_ecef: &Vector3<f64>) -> Vector3<f64> {
let r_km = position_ecef.magnitude();
if r_km < 1.0 {
return Vector3::zeros();
}
let r_m = r_km * 1000.0;
let r3 = r_m * r_m * r_m;
let r_hat = position_ecef / r_km;
let m_hat = &self.axis_ecef;
let m_dot_r = m_hat.dot(&r_hat);
self.dipole_strength * (3.0 * m_dot_r * r_hat - m_hat) / r3
}
}
impl MagneticFieldModel for TiltedDipole {
fn field_ecef(&self, input: &MagneticFieldInput<'_>) -> [f64; 3] {
let lat = input.geodetic.latitude;
let lon = input.geodetic.longitude;
let h = input.geodetic.altitude;
let sin_lat = lat.sin();
let cos_lat = lat.cos();
let n = WGS84_A / (1.0 - WGS84_E2 * sin_lat * sin_lat).sqrt();
let pos_ecef = Vector3::new(
(n + h) * cos_lat * lon.cos(),
(n + h) * cos_lat * lon.sin(),
(n * (1.0 - WGS84_E2) + h) * sin_lat,
);
let b = self.compute_field_ecef(&pos_ecef);
[b.x, b.y, b.z]
}
}
#[cfg(test)]
mod tests {
use super::*;
use arika::earth::geodetic::Geodetic;
use arika::epoch::Epoch;
fn j2000_epoch() -> Epoch {
Epoch::j2000()
}
fn make_input(geodetic: Geodetic, epoch: &Epoch) -> MagneticFieldInput<'_> {
MagneticFieldInput {
geodetic,
utc: epoch,
}
}
fn b_magnitude(b: &[f64; 3]) -> f64 {
(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]).sqrt()
}
#[test]
fn equatorial_field_magnitude_at_leo() {
let dipole = TiltedDipole::earth();
let epoch = j2000_epoch();
let input = make_input(
Geodetic {
latitude: 0.0,
longitude: 0.0,
altitude: 7000.0 - WGS84_A,
},
&epoch,
);
let b = dipole.field_ecef(&input);
let b_micro_t = b_magnitude(&b) * 1e6;
assert!(
b_micro_t > 20.0 && b_micro_t < 50.0,
"Equatorial LEO field should be ~25-35 uT, got {b_micro_t:.2} uT"
);
}
#[test]
fn inverse_cube_scaling() {
let dipole = TiltedDipole::earth();
let epoch = j2000_epoch();
let b1 = b_magnitude(&dipole.field_ecef(&make_input(
Geodetic {
latitude: 0.0,
longitude: 0.0,
altitude: 7000.0 - WGS84_A,
},
&epoch,
)));
let b2 = b_magnitude(&dipole.field_ecef(&make_input(
Geodetic {
latitude: 0.0,
longitude: 0.0,
altitude: 14000.0 - WGS84_A,
},
&epoch,
)));
let ratio = b1 / b2;
assert!(
(ratio - 8.0).abs() < 0.1,
"Expected ~1/r^3 scaling (ratio ~8.0), got {ratio:.4}"
);
}
#[test]
fn polar_field_stronger_than_equatorial() {
let dipole = TiltedDipole::new(7.94e15, Vector3::new(0.0, 0.0, 1.0));
let r = 7000.0;
let epoch = j2000_epoch();
let b_pole = b_magnitude(&dipole.field_ecef(&make_input(
Geodetic {
latitude: std::f64::consts::FRAC_PI_2,
longitude: 0.0,
altitude: r - arika::earth::ellipsoid::WGS84_B,
},
&epoch,
)));
let b_eq = b_magnitude(&dipole.field_ecef(&make_input(
Geodetic {
latitude: 0.0,
longitude: 0.0,
altitude: r - WGS84_A,
},
&epoch,
)));
let ratio = b_pole / b_eq;
assert!(
(ratio - 2.0).abs() < 0.15,
"Polar/equatorial ratio should be ~2.0, got {ratio:.4}"
);
}
#[test]
fn zero_inside_earth_guard() {
let dipole = TiltedDipole::earth();
let b = dipole.compute_field_ecef(&Vector3::new(0.5, 0.0, 0.0));
assert_eq!(b, Vector3::zeros());
}
#[test]
fn zero_at_origin() {
let dipole = TiltedDipole::earth();
let epoch = j2000_epoch();
let input = make_input(
Geodetic {
latitude: 0.0,
longitude: 0.0,
altitude: -WGS84_A,
},
&epoch,
);
let b = dipole.field_ecef(&input);
assert_eq!(b, [0.0, 0.0, 0.0]);
}
#[test]
fn field_is_finite() {
let dipole = TiltedDipole::earth();
let epoch = j2000_epoch();
let input = make_input(
Geodetic {
latitude: 0.0,
longitude: 0.0,
altitude: 6778.0 - WGS84_A,
},
&epoch,
);
let b = dipole.field_ecef(&input);
assert!(
b[0].is_finite() && b[1].is_finite() && b[2].is_finite(),
"Field must be finite: {b:?}"
);
}
#[test]
fn field_ecef_varies_with_longitude() {
let dipole = TiltedDipole::earth();
let epoch = j2000_epoch();
let b1 = dipole.field_ecef(&make_input(
Geodetic {
latitude: 0.0,
longitude: 0.0,
altitude: 7000.0 - WGS84_A,
},
&epoch,
));
let b2 = dipole.field_ecef(&make_input(
Geodetic {
latitude: 0.0,
longitude: std::f64::consts::FRAC_PI_2,
altitude: 7000.0 - WGS84_A,
},
&epoch,
));
let diff =
((b1[0] - b2[0]).powi(2) + (b1[1] - b2[1]).powi(2) + (b1[2] - b2[2]).powi(2)).sqrt();
assert!(
diff > 1e-10,
"Field should differ at different longitudes, diff={diff:.3e}"
);
let mag_ratio = b_magnitude(&b1) / b_magnitude(&b2);
assert!(
(mag_ratio - 1.0).abs() < 0.5,
"Magnitudes should be similar, ratio={mag_ratio:.3}"
);
}
}