use serde::{Deserialize, Serialize};
use std::f64::consts::PI;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SystemsTract {
LowstandST,
TransgressiveST,
HighstandST,
FallingStageST,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DepositionalEnvironment {
Fluvial,
Deltaic,
Shoreface,
Shelf,
DeepMarine,
Lacustrine,
Eolian,
Glacial,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SeaLevelCycle {
pub amplitude_m: f64,
pub period_years: f64,
}
impl SeaLevelCycle {
#[must_use]
pub fn sea_level_at(&self, time_years: f64) -> f64 {
self.amplitude_m * (2.0 * PI * time_years / self.period_years).sin()
}
#[must_use]
pub fn classify_systems_tract(phase: f64) -> SystemsTract {
match phase {
p if p < 0.25 => SystemsTract::LowstandST,
p if p < 0.50 => SystemsTract::TransgressiveST,
p if p < 0.75 => SystemsTract::HighstandST,
_ => SystemsTract::FallingStageST,
}
}
}
#[must_use]
pub fn accommodation_space(sea_level_change_m: f64, subsidence_m: f64) -> f64 {
sea_level_change_m + subsidence_m
}
#[must_use]
pub fn sediment_supply_ratio(accommodation: f64, sediment_supply: f64) -> f64 {
assert!(sediment_supply != 0.0, "sediment_supply must be non-zero");
accommodation / sediment_supply
}
pub struct WalthersLaw;
impl WalthersLaw {
#[must_use]
pub fn lateral_equivalent(
environment: DepositionalEnvironment,
) -> Vec<DepositionalEnvironment> {
use DepositionalEnvironment::*;
match environment {
Fluvial => vec![Fluvial, Deltaic, Eolian, Lacustrine],
Deltaic => vec![Fluvial, Deltaic, Shoreface, Lacustrine],
Shoreface => vec![Deltaic, Shoreface, Shelf],
Shelf => vec![Shoreface, Shelf, DeepMarine],
DeepMarine => vec![Shelf, DeepMarine],
Lacustrine => vec![Fluvial, Deltaic, Lacustrine],
Eolian => vec![Fluvial, Eolian],
Glacial => vec![Glacial, Fluvial, Lacustrine],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParasequenceBoundary {
pub depth_m: f64,
pub age_ma: f64,
pub flooding_surface: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sea_level_at_zero_time_is_zero() {
let cycle = SeaLevelCycle {
amplitude_m: 50.0,
period_years: 100_000.0,
};
let level = cycle.sea_level_at(0.0);
assert!(level.abs() < 1e-10, "expected ~0, got {level}");
}
#[test]
fn sea_level_peak_at_quarter_period() {
let cycle = SeaLevelCycle {
amplitude_m: 50.0,
period_years: 100_000.0,
};
let level = cycle.sea_level_at(25_000.0);
assert!((level - 50.0).abs() < 1e-10, "expected 50, got {level}");
}
#[test]
fn sea_level_trough_at_three_quarter_period() {
let cycle = SeaLevelCycle {
amplitude_m: 50.0,
period_years: 100_000.0,
};
let level = cycle.sea_level_at(75_000.0);
assert!((level + 50.0).abs() < 1e-10, "expected -50, got {level}");
}
#[test]
fn sea_level_full_period_returns_to_zero() {
let cycle = SeaLevelCycle {
amplitude_m: 30.0,
period_years: 40_000.0,
};
let level = cycle.sea_level_at(40_000.0);
assert!(level.abs() < 1e-10, "expected ~0, got {level}");
}
#[test]
fn classify_lowstand() {
assert_eq!(
SeaLevelCycle::classify_systems_tract(0.0),
SystemsTract::LowstandST
);
assert_eq!(
SeaLevelCycle::classify_systems_tract(0.1),
SystemsTract::LowstandST
);
assert_eq!(
SeaLevelCycle::classify_systems_tract(0.24),
SystemsTract::LowstandST
);
}
#[test]
fn classify_transgressive() {
assert_eq!(
SeaLevelCycle::classify_systems_tract(0.25),
SystemsTract::TransgressiveST
);
assert_eq!(
SeaLevelCycle::classify_systems_tract(0.49),
SystemsTract::TransgressiveST
);
}
#[test]
fn classify_highstand() {
assert_eq!(
SeaLevelCycle::classify_systems_tract(0.50),
SystemsTract::HighstandST
);
assert_eq!(
SeaLevelCycle::classify_systems_tract(0.74),
SystemsTract::HighstandST
);
}
#[test]
fn classify_falling_stage() {
assert_eq!(
SeaLevelCycle::classify_systems_tract(0.75),
SystemsTract::FallingStageST
);
assert_eq!(
SeaLevelCycle::classify_systems_tract(0.99),
SystemsTract::FallingStageST
);
}
#[test]
fn accommodation_positive_values() {
let acc = accommodation_space(10.0, 5.0);
assert!((acc - 15.0).abs() < 1e-10);
}
#[test]
fn accommodation_negative_sea_level() {
let acc = accommodation_space(-3.0, 8.0);
assert!((acc - 5.0).abs() < 1e-10);
}
#[test]
fn as_ratio_underfilled() {
let ratio = sediment_supply_ratio(100.0, 50.0);
assert!((ratio - 2.0).abs() < 1e-10, "expected 2.0, got {ratio}");
}
#[test]
fn as_ratio_overfilled() {
let ratio = sediment_supply_ratio(30.0, 60.0);
assert!((ratio - 0.5).abs() < 1e-10, "expected 0.5, got {ratio}");
}
#[test]
#[should_panic(expected = "sediment_supply must be non-zero")]
fn as_ratio_zero_supply_panics() {
let _ = sediment_supply_ratio(10.0, 0.0);
}
#[test]
fn walthers_law_shoreface_adjacency() {
let adj = WalthersLaw::lateral_equivalent(DepositionalEnvironment::Shoreface);
assert!(adj.contains(&DepositionalEnvironment::Deltaic));
assert!(adj.contains(&DepositionalEnvironment::Shoreface));
assert!(adj.contains(&DepositionalEnvironment::Shelf));
assert!(!adj.contains(&DepositionalEnvironment::DeepMarine));
}
#[test]
fn walthers_law_deep_marine_adjacency() {
let adj = WalthersLaw::lateral_equivalent(DepositionalEnvironment::DeepMarine);
assert!(adj.contains(&DepositionalEnvironment::Shelf));
assert!(adj.contains(&DepositionalEnvironment::DeepMarine));
assert_eq!(adj.len(), 2);
}
#[test]
fn walthers_law_glacial_adjacency() {
let adj = WalthersLaw::lateral_equivalent(DepositionalEnvironment::Glacial);
assert!(adj.contains(&DepositionalEnvironment::Glacial));
assert!(adj.contains(&DepositionalEnvironment::Fluvial));
assert!(adj.contains(&DepositionalEnvironment::Lacustrine));
}
#[test]
fn parasequence_boundary_serde_roundtrip() {
let boundary = ParasequenceBoundary {
depth_m: 123.4,
age_ma: 65.5,
flooding_surface: true,
};
let json = serde_json::to_string(&boundary).unwrap();
let back: ParasequenceBoundary = serde_json::from_str(&json).unwrap();
assert!((back.depth_m - 123.4).abs() < 1e-10);
assert!((back.age_ma - 65.5).abs() < 1e-10);
assert!(back.flooding_surface);
}
}