use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Vei {
V0,
V1,
V2,
V3,
V4,
V5,
V6,
V7,
V8,
}
impl Vei {
#[must_use]
pub fn ejecta_volume_m3(&self) -> (f64, f64) {
match self {
Self::V0 => (0.0, 1e4),
Self::V1 => (1e4, 1e6),
Self::V2 => (1e6, 1e7),
Self::V3 => (1e7, 1e8),
Self::V4 => (1e8, 1e9),
Self::V5 => (1e9, 1e10),
Self::V6 => (1e10, 1e11),
Self::V7 => (1e11, 1e12),
Self::V8 => (1e12, 1e13),
}
}
#[must_use]
pub fn description(&self) -> &'static str {
match self {
Self::V0 => "non-explosive, effusive",
Self::V1 => "gentle",
Self::V2 => "explosive",
Self::V3 => "severe",
Self::V4 => "cataclysmic",
Self::V5 => "paroxysmal",
Self::V6 => "colossal",
Self::V7 => "super-colossal",
Self::V8 => "mega-colossal",
}
}
}
#[must_use]
pub fn classify_vei(ejecta_volume_m3: f64) -> Vei {
if ejecta_volume_m3 < 1e4 {
Vei::V0
} else if ejecta_volume_m3 < 1e6 {
Vei::V1
} else if ejecta_volume_m3 < 1e7 {
Vei::V2
} else if ejecta_volume_m3 < 1e8 {
Vei::V3
} else if ejecta_volume_m3 < 1e9 {
Vei::V4
} else if ejecta_volume_m3 < 1e10 {
Vei::V5
} else if ejecta_volume_m3 < 1e11 {
Vei::V6
} else if ejecta_volume_m3 < 1e12 {
Vei::V7
} else {
Vei::V8
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MagmaComposition {
pub sio2: f64,
pub al2o3: f64,
pub feo: f64,
pub mgo: f64,
pub cao: f64,
pub na2o: f64,
pub k2o: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MagmaType {
Ultramafic,
Mafic,
Intermediate,
Felsic,
}
#[must_use]
pub fn classify_magma(sio2: f64) -> MagmaType {
if sio2 < 45.0 {
MagmaType::Ultramafic
} else if sio2 < 52.0 {
MagmaType::Mafic
} else if sio2 < 63.0 {
MagmaType::Intermediate
} else {
MagmaType::Felsic
}
}
#[must_use]
pub fn magma_viscosity(sio2_percent: f64, temperature_c: f64) -> f64 {
let t_k = temperature_c + 273.15;
if t_k <= 0.0 {
return f64::INFINITY;
}
let ln_mu = -6.4 + 0.05 * sio2_percent + 26_000.0 / t_k;
ln_mu.exp()
}
#[must_use]
pub fn eruption_column_height(mass_flux_kg_s: f64) -> f64 {
if mass_flux_kg_s <= 0.0 {
return 0.0;
}
0.236 * mass_flux_kg_s.powf(0.25)
}
#[must_use]
pub fn pyroclastic_flow_runout(column_height_km: f64, slope_degrees: f64) -> f64 {
if column_height_km <= 0.0 {
return 0.0;
}
let slope = slope_degrees.clamp(0.5, 89.0);
let tan_slope = slope.to_radians().tan();
column_height_km / tan_slope
}
#[must_use]
pub fn lava_flow_velocity(
slope_degrees: f64,
viscosity: f64,
thickness_m: f64,
density: f64,
) -> f64 {
if viscosity <= 0.0 || thickness_m <= 0.0 || density <= 0.0 || slope_degrees <= 0.0 {
return 0.0;
}
let g = 9.80665; let sin_alpha = slope_degrees.to_radians().sin();
density * g * thickness_m * thickness_m * sin_alpha / (3.0 * viscosity)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_vei_zero() {
assert_eq!(classify_vei(500.0), Vei::V0);
}
#[test]
fn classify_vei_boundaries() {
assert_eq!(classify_vei(1e4), Vei::V1);
assert_eq!(classify_vei(1e6), Vei::V2);
assert_eq!(classify_vei(1e7), Vei::V3);
assert_eq!(classify_vei(5e8), Vei::V4);
assert_eq!(classify_vei(5e9), Vei::V5);
assert_eq!(classify_vei(5e10), Vei::V6);
assert_eq!(classify_vei(5e11), Vei::V7);
assert_eq!(classify_vei(1e13), Vei::V8);
}
#[test]
fn vei_ejecta_volume_range() {
let (lo, hi) = Vei::V5.ejecta_volume_m3();
assert!(lo < hi);
assert!((lo - 1e9).abs() < 1.0);
}
#[test]
fn vei_description_non_empty() {
assert!(!Vei::V0.description().is_empty());
assert!(!Vei::V8.description().is_empty());
}
#[test]
fn viscosity_increases_with_sio2() {
let v_basalt = magma_viscosity(50.0, 1200.0);
let v_rhyolite = magma_viscosity(75.0, 1200.0);
assert!(
v_rhyolite > v_basalt,
"rhyolite ({v_rhyolite}) should be more viscous than basalt ({v_basalt})"
);
}
#[test]
fn viscosity_decreases_with_temperature() {
let v_cool = magma_viscosity(65.0, 800.0);
let v_hot = magma_viscosity(65.0, 1200.0);
assert!(
v_cool > v_hot,
"cooler magma ({v_cool}) should be more viscous than hotter ({v_hot})"
);
}
#[test]
fn viscosity_at_absolute_zero() {
assert!(magma_viscosity(50.0, -274.0).is_infinite());
}
#[test]
fn column_height_plinian() {
let h = eruption_column_height(1e8);
assert!((h - 23.6).abs() < 0.1, "expected ~23.6 km, got {h}");
}
#[test]
fn column_height_zero_flux() {
assert_eq!(eruption_column_height(0.0), 0.0);
}
#[test]
fn runout_increases_with_height() {
let r1 = pyroclastic_flow_runout(5.0, 10.0);
let r2 = pyroclastic_flow_runout(20.0, 10.0);
assert!(r2 > r1);
}
#[test]
fn runout_zero_height() {
assert_eq!(pyroclastic_flow_runout(0.0, 10.0), 0.0);
}
#[test]
fn lava_flow_basic() {
let v = lava_flow_velocity(10.0, 100.0, 2.0, 2500.0);
let expected = 2500.0 * 9.80665 * 4.0 * (10.0_f64).to_radians().sin() / 300.0;
assert!((v - expected).abs() < 1e-6, "expected {expected}, got {v}");
}
#[test]
fn lava_flow_zero_viscosity() {
assert_eq!(lava_flow_velocity(10.0, 0.0, 2.0, 2500.0), 0.0);
}
#[test]
fn magma_classification() {
assert_eq!(classify_magma(40.0), MagmaType::Ultramafic);
assert_eq!(classify_magma(48.0), MagmaType::Mafic);
assert_eq!(classify_magma(57.0), MagmaType::Intermediate);
assert_eq!(classify_magma(72.0), MagmaType::Felsic);
}
#[test]
fn magma_classification_boundaries() {
assert_eq!(classify_magma(44.9), MagmaType::Ultramafic);
assert_eq!(classify_magma(45.0), MagmaType::Mafic);
assert_eq!(classify_magma(52.0), MagmaType::Intermediate);
assert_eq!(classify_magma(63.0), MagmaType::Felsic);
}
}