use serde::{Deserialize, Serialize};
use ushma::state;
use ushma::transfer;
#[must_use]
pub fn heat_flux(
conductivity: f64,
area: f64,
t_deep: f64,
t_surface: f64,
depth: f64,
) -> Option<f64> {
transfer::conduction(conductivity, area, t_deep, t_surface, depth).ok()
}
#[must_use]
pub fn temperature_at_depth(surface_temp_k: f64, gradient_k_per_m: f64, depth_m: f64) -> f64 {
surface_temp_k + gradient_k_per_m * depth_m
}
#[must_use]
pub fn rock_thermal_diffusivity(
conductivity: f64,
density: f64,
specific_heat: f64,
) -> Option<f64> {
transfer::thermal_diffusivity(conductivity, density, specific_heat).ok()
}
#[must_use]
pub fn heat_stored(mass_kg: f64, specific_heat: f64, delta_t: f64) -> f64 {
transfer::heat_stored(mass_kg, specific_heat, delta_t)
}
#[must_use]
pub fn lithostatic_pressure(density: f64, gravity: f64, depth_m: f64) -> f64 {
density * gravity * depth_m
}
#[must_use]
pub fn gibbs_energy(enthalpy: f64, temperature: f64, entropy: f64) -> f64 {
ushma::entropy::gibbs(enthalpy, temperature, entropy)
}
#[must_use]
pub fn is_spontaneous(delta_h: f64, temperature: f64, delta_s: f64) -> bool {
gibbs_energy(delta_h, temperature, delta_s) < 0.0
}
#[must_use]
pub fn volatile_pressure(moles: f64, temperature_k: f64, volume_m3: f64) -> Option<f64> {
state::ideal_gas_pressure(moles, temperature_k, volume_m3).ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MetamorphicFacies {
Zeolite,
Greenschist,
Amphibolite,
Granulite,
Blueschist,
Eclogite,
ContactHornfels,
}
#[must_use]
pub fn classify_facies(temperature_c: f64, pressure_gpa: f64) -> MetamorphicFacies {
if pressure_gpa > 1.2 && temperature_c > 450.0 {
MetamorphicFacies::Eclogite
}
else if pressure_gpa > 0.6 && temperature_c < 500.0 {
MetamorphicFacies::Blueschist
}
else if temperature_c > 500.0 && pressure_gpa < 0.3 {
MetamorphicFacies::ContactHornfels
}
else if temperature_c > 700.0 {
MetamorphicFacies::Granulite
}
else if temperature_c > 450.0 {
MetamorphicFacies::Amphibolite
}
else if temperature_c > 200.0 {
MetamorphicFacies::Greenschist
}
else {
MetamorphicFacies::Zeolite
}
}
#[must_use]
pub fn facies_at_depth(
depth_km: f64,
gradient_c_per_km: f64,
surface_temp_c: f64,
rock_density: f64,
) -> MetamorphicFacies {
let temp_c = surface_temp_c + gradient_c_per_km * depth_km;
let pressure_pa = lithostatic_pressure(rock_density, 9.81, depth_km * 1000.0);
let pressure_gpa = pressure_pa / 1e9;
classify_facies(temp_c, pressure_gpa)
}
#[must_use]
pub fn intrusion_cooling(
magma_temp_k: f64,
country_temp_k: f64,
half_width_m: f64,
diffusivity_m2_s: f64,
time_seconds: f64,
) -> f64 {
let decay = (-std::f64::consts::PI.powi(2) * diffusivity_m2_s * time_seconds
/ half_width_m.powi(2))
.exp();
country_temp_k + (magma_temp_k - country_temp_k) * decay
}
#[must_use]
pub fn intrusion_cooling_time(
magma_temp_k: f64,
country_temp_k: f64,
target_temp_k: f64,
half_width_m: f64,
diffusivity_m2_s: f64,
) -> Option<f64> {
if target_temp_k <= country_temp_k || target_temp_k >= magma_temp_k {
return None;
}
let ratio = (target_temp_k - country_temp_k) / (magma_temp_k - country_temp_k);
let t = -half_width_m.powi(2) * ratio.ln() / (std::f64::consts::PI.powi(2) * diffusivity_m2_s);
Some(t)
}
#[must_use]
pub fn contact_aureole_temperature(
distance_m: f64,
half_width_m: f64,
magma_temp_k: f64,
country_temp_k: f64,
) -> f64 {
country_temp_k + (magma_temp_k - country_temp_k) * (-distance_m / half_width_m).exp()
}
pub mod conductivity {
pub const GRANITE: f64 = 2.5;
pub const BASALT: f64 = 1.7;
pub const SANDSTONE: f64 = 2.3;
pub const LIMESTONE: f64 = 2.5;
pub const MARBLE: f64 = 2.9;
pub const SHALE: f64 = 1.5;
pub const GNEISS: f64 = 2.7;
pub const QUARTZITE: f64 = 5.0;
}
pub mod specific_heat {
pub const GRANITE: f64 = 790.0;
pub const BASALT: f64 = 840.0;
pub const SANDSTONE: f64 = 920.0;
pub const LIMESTONE: f64 = 840.0;
pub const MARBLE: f64 = 880.0;
pub const SHALE: f64 = 760.0;
pub const GNEISS: f64 = 800.0;
pub const QUARTZITE: f64 = 740.0;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn temperature_increases_with_depth() {
let surface = 288.15; let gradient = 0.025; let t_1km = temperature_at_depth(surface, gradient, 1000.0);
let t_5km = temperature_at_depth(surface, gradient, 5000.0);
assert!(t_1km > surface);
assert!(t_5km > t_1km);
assert!((t_1km - 313.15).abs() < 0.01); }
#[test]
fn lithostatic_pressure_at_depth() {
let p = lithostatic_pressure(2700.0, 9.81, 1000.0);
assert!((p - 26_487_000.0).abs() < 1000.0);
}
#[test]
fn heat_flux_positive_downward() {
let q = heat_flux(conductivity::GRANITE, 1.0, 373.15, 288.15, 1000.0);
assert!(q.is_some());
assert!(q.unwrap() > 0.0);
}
#[test]
fn granite_thermal_diffusivity() {
let d = rock_thermal_diffusivity(conductivity::GRANITE, 2700.0, specific_heat::GRANITE);
assert!(d.is_some());
let alpha = d.unwrap();
assert!(alpha > 1e-7 && alpha < 1e-5);
}
#[test]
fn heat_storage() {
let q = heat_stored(1.0, specific_heat::GRANITE, 100.0);
assert!((q - 79_000.0).abs() < 1000.0);
}
#[test]
fn gibbs_spontaneity() {
assert!(is_spontaneous(-50_000.0, 298.15, 100.0));
assert!(!is_spontaneous(50_000.0, 298.15, 10.0));
}
#[test]
fn volatile_pressure_in_pore() {
let p = volatile_pressure(1.0, 473.15, 0.001); assert!(p.is_some());
let pa = p.unwrap();
assert!(pa > 3_000_000.0 && pa < 5_000_000.0);
}
#[test]
fn facies_zeolite_shallow() {
assert_eq!(classify_facies(150.0, 0.1), MetamorphicFacies::Zeolite);
}
#[test]
fn facies_greenschist() {
assert_eq!(classify_facies(350.0, 0.4), MetamorphicFacies::Greenschist);
}
#[test]
fn facies_amphibolite() {
assert_eq!(classify_facies(550.0, 0.6), MetamorphicFacies::Amphibolite);
}
#[test]
fn facies_granulite() {
assert_eq!(classify_facies(800.0, 0.8), MetamorphicFacies::Granulite);
}
#[test]
fn facies_blueschist_high_p_low_t() {
assert_eq!(classify_facies(300.0, 1.0), MetamorphicFacies::Blueschist);
}
#[test]
fn facies_eclogite_high_p_high_t() {
assert_eq!(classify_facies(600.0, 1.5), MetamorphicFacies::Eclogite);
}
#[test]
fn facies_contact_hornfels_high_t_low_p() {
assert_eq!(
classify_facies(600.0, 0.2),
MetamorphicFacies::ContactHornfels
);
}
#[test]
fn facies_at_depth_shallow_is_zeolite() {
let f = facies_at_depth(5.0, 25.0, 15.0, 2700.0);
assert_eq!(f, MetamorphicFacies::Zeolite);
}
#[test]
fn facies_at_depth_deep_is_higher_grade() {
let shallow = facies_at_depth(5.0, 25.0, 15.0, 2700.0);
let deep = facies_at_depth(20.0, 25.0, 15.0, 2700.0);
assert_ne!(deep, shallow);
}
#[test]
fn intrusion_starts_at_magma_temp() {
let t = intrusion_cooling(1473.0, 573.0, 50.0, 1e-6, 0.0);
assert!((t - 1473.0).abs() < 0.01);
}
#[test]
fn intrusion_cools_over_time() {
let early = intrusion_cooling(1473.0, 573.0, 50.0, 1e-6, 1_000_000.0);
let late = intrusion_cooling(1473.0, 573.0, 50.0, 1e-6, 100_000_000.0);
assert!(late < early);
assert!(late >= 573.0); }
#[test]
fn intrusion_approaches_country_rock() {
let t = intrusion_cooling(1473.0, 573.0, 50.0, 1e-6, 1e12);
assert!((t - 573.0).abs() < 1.0);
}
#[test]
fn cooling_time_roundtrip() {
let target = 800.0;
let time = intrusion_cooling_time(1473.0, 573.0, target, 50.0, 1e-6).unwrap();
let recovered = intrusion_cooling(1473.0, 573.0, 50.0, 1e-6, time);
assert!((recovered - target).abs() < 0.1);
}
#[test]
fn cooling_time_invalid_target() {
assert!(intrusion_cooling_time(1473.0, 573.0, 500.0, 50.0, 1e-6).is_none());
assert!(intrusion_cooling_time(1473.0, 573.0, 1500.0, 50.0, 1e-6).is_none());
}
#[test]
fn contact_aureole_at_contact() {
let t = contact_aureole_temperature(0.0, 50.0, 1473.0, 573.0);
assert!((t - 1473.0).abs() < 0.01);
}
#[test]
fn contact_aureole_decays_with_distance() {
let near = contact_aureole_temperature(10.0, 50.0, 1473.0, 573.0);
let far = contact_aureole_temperature(100.0, 50.0, 1473.0, 573.0);
assert!(near > far);
assert!(far > 573.0);
}
}