use serde::{Deserialize, Serialize};
const MW_AL2O3: f64 = 101.961;
const MW_CAO: f64 = 56.077;
const MW_NA2O: f64 = 61.979;
const MW_K2O: f64 = 94.196;
const MW_MGO: f64 = 40.304;
const MW_FEO: f64 = 71.844;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MajorOxides {
pub sio2: f64,
pub tio2: f64,
pub al2o3: f64,
pub fe2o3: f64,
pub feo: f64,
pub mno: f64,
pub mgo: f64,
pub cao: f64,
pub na2o: f64,
pub k2o: f64,
pub p2o5: f64,
pub h2o: f64,
}
impl MajorOxides {
#[must_use]
pub fn total(&self) -> f64 {
self.sio2
+ self.tio2
+ self.al2o3
+ self.fe2o3
+ self.feo
+ self.mno
+ self.mgo
+ self.cao
+ self.na2o
+ self.k2o
+ self.p2o5
+ self.h2o
}
#[must_use]
pub fn is_valid(&self) -> bool {
let t = self.total();
(98.0..=102.0).contains(&t)
}
#[must_use]
pub fn total_alkali(&self) -> f64 {
self.na2o + self.k2o
}
#[must_use]
pub fn tas_classification(&self) -> TasClassification {
classify_tas(self.sio2, self.total_alkali())
}
#[must_use]
pub fn mg_number(&self) -> f64 {
mg_number(self.feo, self.mgo)
}
#[must_use]
pub fn asi(&self) -> f64 {
alumina_saturation_index(self.al2o3, self.cao, self.na2o, self.k2o)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum TasClassification {
Picrite,
Picrobasalt,
Basalt,
BasalticAndesite,
Andesite,
Dacite,
Rhyolite,
Trachybasalt,
BasalticTrachyandesite,
Trachyandesite,
Trachyte,
Phonolite,
Tephrite,
Phonotephrite,
Tephriphonolite,
Foidite,
}
#[must_use]
pub fn classify_tas(sio2: f64, total_alkali: f64) -> TasClassification {
if total_alkali > sio2 - 25.0 && sio2 < 41.0 {
return TasClassification::Foidite;
}
if sio2 < 41.0 {
if total_alkali >= 3.0 {
return TasClassification::Foidite;
}
return TasClassification::Picrite;
}
let alkaline_boundary = 0.37 * sio2 - 14.43;
let is_alkaline = total_alkali > alkaline_boundary;
match sio2 {
s if s < 45.0 => {
if is_alkaline {
if total_alkali >= 9.0 {
TasClassification::Phonotephrite
} else {
TasClassification::Tephrite
}
} else if total_alkali < 2.0 {
TasClassification::Picrite
} else {
TasClassification::Picrobasalt
}
}
s if s < 52.0 => {
if is_alkaline {
if total_alkali >= 9.5 {
TasClassification::Tephriphonolite
} else if total_alkali >= 5.0 {
TasClassification::Trachybasalt
} else {
TasClassification::Basalt
}
} else {
TasClassification::Basalt
}
}
s if s < 57.0 => {
if is_alkaline {
if total_alkali >= 11.0 {
TasClassification::Phonolite
} else if total_alkali >= 7.0 {
TasClassification::BasalticTrachyandesite
} else {
TasClassification::BasalticAndesite
}
} else {
TasClassification::BasalticAndesite
}
}
s if s < 63.0 => {
if is_alkaline {
if total_alkali >= 11.5 {
TasClassification::Phonolite
} else if total_alkali >= 7.5 {
TasClassification::Trachyandesite
} else {
TasClassification::Andesite
}
} else {
TasClassification::Andesite
}
}
s if s < 69.0 => {
if total_alkali >= 11.0 {
TasClassification::Trachyte
} else {
TasClassification::Dacite
}
}
_ => {
if total_alkali >= 12.0 {
TasClassification::Trachyte
} else {
TasClassification::Rhyolite
}
}
}
}
#[must_use]
pub fn mg_number(feo: f64, mgo: f64) -> f64 {
let mgo_mol = mgo / MW_MGO;
let feo_mol = feo / MW_FEO;
let denom = mgo_mol + feo_mol;
if denom <= 0.0 {
return 0.0;
}
mgo_mol / denom
}
#[must_use]
pub fn alumina_saturation_index(al2o3: f64, cao: f64, na2o: f64, k2o: f64) -> f64 {
let al_mol = al2o3 / MW_AL2O3;
let denom = (cao / MW_CAO) + (na2o / MW_NA2O) + (k2o / MW_K2O);
if denom <= 0.0 {
return f64::INFINITY;
}
al_mol / denom
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AsiClassification {
Peraluminous,
Metaluminous,
Peralkaline,
}
#[must_use]
pub fn classify_asi(asi: f64) -> AsiClassification {
if asi > 1.0 {
AsiClassification::Peraluminous
} else if asi >= 0.5 {
AsiClassification::Metaluminous
} else {
AsiClassification::Peralkaline
}
}
#[must_use]
pub fn fractional_crystallization(c0: f64, f_remaining: f64, partition_coeff: f64) -> Option<f64> {
if f_remaining <= 0.0 || f_remaining > 1.0 {
return None;
}
Some(c0 * f_remaining.powf(partition_coeff - 1.0))
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, eps: f64) -> bool {
(a - b).abs() < eps
}
fn basalt_oxides() -> MajorOxides {
MajorOxides {
sio2: 49.5,
tio2: 1.5,
al2o3: 15.5,
fe2o3: 2.5,
feo: 7.5,
mno: 0.17,
mgo: 8.0,
cao: 11.0,
na2o: 2.5,
k2o: 0.5,
p2o5: 0.2,
h2o: 0.5,
}
}
fn rhyolite_oxides() -> MajorOxides {
MajorOxides {
sio2: 73.0,
tio2: 0.2,
al2o3: 13.5,
fe2o3: 0.8,
feo: 1.0,
mno: 0.05,
mgo: 0.3,
cao: 1.2,
na2o: 3.5,
k2o: 5.0,
p2o5: 0.05,
h2o: 0.5,
}
}
#[test]
fn oxide_total_basalt() {
let ox = basalt_oxides();
let t = ox.total();
assert!(approx_eq(t, 99.37, 0.01), "total = {t}");
}
#[test]
fn oxide_is_valid_basalt() {
assert!(basalt_oxides().is_valid());
}
#[test]
fn oxide_invalid_low_total() {
let mut ox = basalt_oxides();
ox.sio2 = 30.0; assert!(!ox.is_valid());
}
#[test]
fn oxide_invalid_high_total() {
let mut ox = basalt_oxides();
ox.sio2 = 80.0; assert!(!ox.is_valid());
}
#[test]
fn tas_basalt() {
assert_eq!(classify_tas(50.0, 3.0), TasClassification::Basalt);
}
#[test]
fn tas_basalt_from_struct() {
assert_eq!(
basalt_oxides().tas_classification(),
TasClassification::Basalt
);
}
#[test]
fn tas_rhyolite() {
assert_eq!(classify_tas(73.0, 8.5), TasClassification::Rhyolite);
}
#[test]
fn tas_rhyolite_from_struct() {
assert_eq!(
rhyolite_oxides().tas_classification(),
TasClassification::Rhyolite
);
}
#[test]
fn tas_andesite() {
assert_eq!(classify_tas(58.0, 4.0), TasClassification::Andesite);
}
#[test]
fn tas_dacite() {
assert_eq!(classify_tas(66.0, 5.0), TasClassification::Dacite);
}
#[test]
fn mg_number_primitive_basalt() {
let mg = mg_number(7.5, 8.0);
assert!(mg > 0.5 && mg < 0.8, "Mg# = {mg}");
}
#[test]
fn mg_number_zero_inputs() {
assert_eq!(mg_number(0.0, 0.0), 0.0);
}
#[test]
fn asi_peraluminous_granite() {
let asi = alumina_saturation_index(16.0, 1.0, 3.0, 4.0);
assert!(asi > 1.0, "ASI = {asi}");
assert_eq!(classify_asi(asi), AsiClassification::Peraluminous);
}
#[test]
fn asi_metaluminous() {
let asi = alumina_saturation_index(14.0, 5.0, 3.5, 4.5);
assert!((0.5..1.0).contains(&asi), "ASI = {asi}");
assert_eq!(classify_asi(asi), AsiClassification::Metaluminous);
}
#[test]
fn rayleigh_compatible_element() {
let c = fractional_crystallization(100.0, 0.5, 2.0).unwrap();
assert!(c < 100.0, "C_l = {c}");
}
#[test]
fn rayleigh_incompatible_element() {
let c = fractional_crystallization(100.0, 0.5, 0.1).unwrap();
assert!(c > 100.0, "C_l = {c}");
}
#[test]
fn rayleigh_no_crystallization() {
let c = fractional_crystallization(42.0, 1.0, 3.0).unwrap();
assert!(approx_eq(c, 42.0, 1e-10));
}
#[test]
fn rayleigh_d_equals_one() {
let c = fractional_crystallization(100.0, 0.3, 1.0).unwrap();
assert!(approx_eq(c, 100.0, 1e-10));
}
#[test]
fn rayleigh_zero_f_returns_none() {
assert!(fractional_crystallization(100.0, 0.0, 1.0).is_none());
}
#[test]
fn serde_major_oxides_round_trip() {
let ox = basalt_oxides();
let json = serde_json::to_string(&ox).unwrap();
let deser: MajorOxides = serde_json::from_str(&json).unwrap();
assert!(approx_eq(ox.total(), deser.total(), 1e-10));
}
#[test]
fn serde_tas_round_trip() {
let cls = TasClassification::Basalt;
let json = serde_json::to_string(&cls).unwrap();
let deser: TasClassification = serde_json::from_str(&json).unwrap();
assert_eq!(cls, deser);
}
}