#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub const G0: f64 = 9.806_65;
pub const M_AIR: f64 = 0.028_964_4;
pub const R_STAR: f64 = 8.314_32;
pub const R_AIR: f64 = R_STAR / M_AIR;
pub const GMR: f64 = G0 * M_AIR / R_STAR;
pub const EARTH_RADIUS_M: f64 = 6_356_766.0;
pub const P0_PA: f64 = 101_325.0;
pub const T0_K: f64 = 288.15;
const FT_PER_M: f64 = 1.0 / 0.3048;
struct Layer {
base_geopotential_m: f64,
base_temp_k: f64,
lapse_k_per_m: f64,
base_pressure_pa: f64,
}
static LAYERS: [Layer; 7] = [
Layer { base_geopotential_m: 0.0, base_temp_k: 288.15, lapse_k_per_m: -0.006_5, base_pressure_pa: 101_325.0 },
Layer { base_geopotential_m: 11_000.0, base_temp_k: 216.65, lapse_k_per_m: 0.0, base_pressure_pa: 22_632.06 },
Layer { base_geopotential_m: 20_000.0, base_temp_k: 216.65, lapse_k_per_m: 0.001, base_pressure_pa: 5_474.889 },
Layer { base_geopotential_m: 32_000.0, base_temp_k: 228.65, lapse_k_per_m: 0.002_8, base_pressure_pa: 868.018_7 },
Layer { base_geopotential_m: 47_000.0, base_temp_k: 270.65, lapse_k_per_m: 0.0, base_pressure_pa: 110.906_3 },
Layer { base_geopotential_m: 51_000.0, base_temp_k: 270.65, lapse_k_per_m: -0.002_8, base_pressure_pa: 66.938_87 },
Layer { base_geopotential_m: 71_000.0, base_temp_k: 214.65, lapse_k_per_m: -0.002, base_pressure_pa: 3.956_420 },
];
fn layer_for_geopotential(h_m: f64) -> &'static Layer {
let mut chosen = &LAYERS[0];
for layer in LAYERS.iter() {
if h_m >= layer.base_geopotential_m {
chosen = layer;
} else {
break;
}
}
chosen
}
fn layer_for_pressure(p_pa: f64) -> &'static Layer {
let mut chosen = &LAYERS[0];
for layer in LAYERS.iter() {
if p_pa <= layer.base_pressure_pa {
chosen = layer;
} else {
break;
}
}
chosen
}
pub fn temperature_k_from_geopotential_m(h_m: f64) -> f64 {
let l = layer_for_geopotential(h_m);
l.base_temp_k + l.lapse_k_per_m * (h_m - l.base_geopotential_m)
}
pub fn pressure_pa_from_geopotential_m(h_m: f64) -> f64 {
let l = layer_for_geopotential(h_m);
let dh = h_m - l.base_geopotential_m;
if l.lapse_k_per_m.abs() < f64::EPSILON {
l.base_pressure_pa * (-GMR * dh / l.base_temp_k).exp()
} else {
let t = l.base_temp_k + l.lapse_k_per_m * dh;
l.base_pressure_pa * (l.base_temp_k / t).powf(GMR / l.lapse_k_per_m)
}
}
pub fn pressure_hpa_from_geopotential_m(h_m: f64) -> f64 {
pressure_pa_from_geopotential_m(h_m) / 100.0
}
pub fn geopotential_m_from_pressure_pa(p_pa: f64) -> f64 {
let l = layer_for_pressure(p_pa);
if l.lapse_k_per_m.abs() < f64::EPSILON {
l.base_geopotential_m - (l.base_temp_k / GMR) * (p_pa / l.base_pressure_pa).ln()
} else {
let t = l.base_temp_k * (p_pa / l.base_pressure_pa).powf(-l.lapse_k_per_m / GMR);
l.base_geopotential_m + (t - l.base_temp_k) / l.lapse_k_per_m
}
}
pub fn density_kg_m3_from_geopotential_m(h_m: f64) -> f64 {
let p = pressure_pa_from_geopotential_m(h_m);
let t = temperature_k_from_geopotential_m(h_m);
p / (R_AIR * t)
}
pub fn geopotential_from_geometric(z_m: f64) -> f64 {
EARTH_RADIUS_M * z_m / (EARTH_RADIUS_M + z_m)
}
pub fn geometric_from_geopotential(h_m: f64) -> f64 {
EARTH_RADIUS_M * h_m / (EARTH_RADIUS_M - h_m)
}
pub fn pressure_hpa_from_flight_level(fl: f64) -> f64 {
let h_m = fl * 100.0 / FT_PER_M; pressure_pa_from_geopotential_m(h_m) / 100.0
}
pub fn flight_level_from_pressure_hpa(p_hpa: f64) -> f64 {
let h_m = geopotential_m_from_pressure_pa(p_hpa * 100.0);
h_m * FT_PER_M / 100.0
}
pub fn pressure_altitude_ft_from_pressure_hpa(p_hpa: f64) -> f64 {
geopotential_m_from_pressure_pa(p_hpa * 100.0) * FT_PER_M
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64, tol: f64) {
assert!((a - b).abs() < tol, "{a} vs {b} (tol {tol})");
}
#[test]
fn sea_level_matches_the_standard() {
approx(pressure_hpa_from_geopotential_m(0.0), 1013.25, 0.01);
approx(temperature_k_from_geopotential_m(0.0), 288.15, 0.01);
approx(density_kg_m3_from_geopotential_m(0.0), 1.225, 0.001);
}
#[test]
fn flight_levels_are_isobars() {
approx(pressure_hpa_from_flight_level(300.0), 300.9, 0.3);
approx(pressure_hpa_from_flight_level(350.0), 238.4, 0.3);
approx(pressure_hpa_from_flight_level(400.0), 187.5, 0.3);
}
#[test]
fn flight_level_round_trips() {
for fl in [50.0, 180.0, 300.0, 410.0] {
let p = pressure_hpa_from_flight_level(fl);
approx(flight_level_from_pressure_hpa(p), fl, 0.01);
}
}
#[test]
fn known_pressure_heights() {
approx(geopotential_m_from_pressure_pa(50_000.0), 5575.0, 5.0);
approx(geopotential_m_from_pressure_pa(25_000.0), 10_363.0, 10.0);
}
#[test]
fn tropopause_is_piecewise() {
approx(pressure_hpa_from_geopotential_m(11_000.0), 226.32, 0.05);
approx(temperature_k_from_geopotential_m(11_000.0), 216.65, 0.01);
approx(temperature_k_from_geopotential_m(18_000.0), 216.65, 0.01);
}
#[test]
fn geopotential_geometric_correction_is_small_but_present() {
let z = geometric_from_geopotential(11_000.0);
assert!(z > 11_000.0 && z - 11_000.0 < 25.0); approx(geopotential_from_geometric(z), 11_000.0, 1e-6);
}
}