use core::f64::consts::PI;
const MAS_TO_RAD: f64 = PI / (180.0 * 3_600_000.0);
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PlateAngularVelocity {
pub omega_x: f64,
pub omega_y: f64,
pub omega_z: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Plate {
Eurasian,
NorthAmerican,
Pacific,
African,
Antarctic,
Australian,
SouthAmerican,
Arabian,
Caribbean,
Cocos,
Nazca,
Philippine,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum PlateMotionModel {
Itrf2014,
Itrf2020,
NnrMorvel56,
}
impl PlateMotionModel {
#[must_use]
pub fn angular_velocity(&self, plate: Plate) -> Option<PlateAngularVelocity> {
match self {
PlateMotionModel::Itrf2014 => itrf2014_angular_velocity(plate),
PlateMotionModel::Itrf2020 => itrf2020_angular_velocity(plate),
PlateMotionModel::NnrMorvel56 => nnr_morvel56_angular_velocity(plate),
}
}
#[must_use]
pub fn velocity_at(&self, plate: Plate, x: f64, y: f64, z: f64) -> Option<(f64, f64, f64)> {
let av = self.angular_velocity(plate)?;
let wx = av.omega_x * MAS_TO_RAD;
let wy = av.omega_y * MAS_TO_RAD;
let wz = av.omega_z * MAS_TO_RAD;
let vx = wy * z - wz * y;
let vy = wz * x - wx * z;
let vz = wx * y - wy * x;
Some((vx * 1000.0, vy * 1000.0, vz * 1000.0))
}
#[must_use]
pub fn coordinate_shift(
&self,
plate: Plate,
x: f64,
y: f64,
z: f64,
dt_years: f64,
) -> Option<(f64, f64, f64)> {
let (vx, vy, vz) = self.velocity_at(plate, x, y, z)?;
Some((vx * dt_years, vy * dt_years, vz * dt_years))
}
#[must_use]
pub fn propagate(
&self,
plate: Plate,
x: f64,
y: f64,
z: f64,
from_epoch: f64,
to_epoch: f64,
) -> Option<(f64, f64, f64)> {
let dt = to_epoch - from_epoch;
let (dx_mm, dy_mm, dz_mm) = self.coordinate_shift(plate, x, y, z, dt)?;
let dx_m = dx_mm / 1000.0;
let dy_m = dy_mm / 1000.0;
let dz_m = dz_mm / 1000.0;
Some((x + dx_m, y + dy_m, z + dz_m))
}
}
fn itrf2014_angular_velocity(plate: Plate) -> Option<PlateAngularVelocity> {
let (ox, oy, oz) = match plate {
Plate::Eurasian => (-0.085, -0.531, 0.770),
Plate::NorthAmerican => (0.024, -0.694, -0.033),
Plate::Pacific => (-0.409, 1.047, -2.169),
Plate::African => (0.000, 0.310, 0.550),
Plate::Antarctic => (-0.252, -0.302, 0.643),
Plate::Australian => (1.504, 1.172, 1.228),
Plate::SouthAmerican => (-0.270, -0.301, -0.140),
Plate::Arabian => (1.154, -0.136, 1.444),
Plate::Caribbean | Plate::Cocos | Plate::Nazca | Plate::Philippine => return None,
};
Some(PlateAngularVelocity {
omega_x: ox,
omega_y: oy,
omega_z: oz,
})
}
fn itrf2020_angular_velocity(plate: Plate) -> Option<PlateAngularVelocity> {
let (ox, oy, oz) = match plate {
Plate::Eurasian => (-0.083, -0.534, 0.750),
Plate::NorthAmerican => (0.029, -0.698, -0.030),
Plate::Pacific => (-0.411, 1.049, -2.162),
Plate::African => (0.002, 0.316, 0.546),
Plate::Antarctic => (-0.247, -0.294, 0.635),
Plate::Australian => (1.511, 1.166, 1.218),
Plate::SouthAmerican => (-0.265, -0.308, -0.132),
Plate::Arabian => (1.156, -0.136, 1.451),
Plate::Caribbean | Plate::Cocos | Plate::Nazca | Plate::Philippine => return None,
};
Some(PlateAngularVelocity {
omega_x: ox,
omega_y: oy,
omega_z: oz,
})
}
fn nnr_morvel56_angular_velocity(plate: Plate) -> Option<PlateAngularVelocity> {
let (ox, oy, oz) = match plate {
Plate::Eurasian => (-0.083, -0.534, 0.750),
Plate::NorthAmerican => (0.035, -0.662, -0.100),
Plate::Pacific => (-0.444, 1.049, -2.093),
Plate::African => (0.048, 0.280, 0.564),
Plate::Antarctic => (-0.223, -0.310, 0.594),
Plate::Australian => (1.504, 1.172, 1.228),
Plate::SouthAmerican => (-0.270, -0.301, -0.140),
Plate::Arabian | Plate::Caribbean | Plate::Cocos | Plate::Nazca | Plate::Philippine => {
return None
}
};
Some(PlateAngularVelocity {
omega_x: ox,
omega_y: oy,
omega_z: oz,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pacific_plate_velocity_non_zero() {
let omega = PlateMotionModel::Itrf2014
.angular_velocity(Plate::Pacific)
.unwrap();
assert!(omega.omega_z.abs() > 0.0);
}
#[test]
fn eurasian_velocity_at_origin_is_zero() {
let (vx, vy, vz) = PlateMotionModel::Itrf2014
.velocity_at(Plate::Eurasian, 0.0, 0.0, 0.0)
.unwrap();
assert_eq!(vx, 0.0);
assert_eq!(vy, 0.0);
assert_eq!(vz, 0.0);
}
#[test]
fn north_american_propagation_makes_sense() {
let (x, y, z) = (-2_000_000.0_f64, -4_500_000.0_f64, 4_000_000.0_f64);
let result = PlateMotionModel::Itrf2014
.propagate(Plate::NorthAmerican, x, y, z, 2010.0, 2020.0)
.unwrap();
let shift =
((result.0 - x).powi(2) + (result.1 - y).powi(2) + (result.2 - z).powi(2)).sqrt();
assert!(
shift < 0.5,
"10-year plate motion shift > 0.5m is unrealistic: {}",
shift
);
assert!(
shift > 0.001,
"10-year plate motion shift should be detectable"
);
}
#[test]
fn all_itrf2014_plates_have_velocity() {
let plates = [
Plate::Eurasian,
Plate::NorthAmerican,
Plate::Pacific,
Plate::African,
Plate::Antarctic,
Plate::Australian,
Plate::SouthAmerican,
Plate::Arabian,
];
for plate in plates {
assert!(
PlateMotionModel::Itrf2014.angular_velocity(plate).is_some(),
"{:?} missing from ITRF2014",
plate
);
}
}
#[test]
fn itrf2020_eurasian_slightly_different_from_itrf2014() {
let v14 = PlateMotionModel::Itrf2014
.angular_velocity(Plate::Eurasian)
.unwrap();
let v20 = PlateMotionModel::Itrf2020
.angular_velocity(Plate::Eurasian)
.unwrap();
assert_ne!(v14.omega_z, v20.omega_z);
}
}