standard_atmosphere/
lib.rs1#![forbid(unsafe_code)]
47#![warn(missing_docs)]
48
49pub const G0: f64 = 9.806_65;
51pub const M_AIR: f64 = 0.028_964_4;
53pub const R_STAR: f64 = 8.314_32;
55pub const R_AIR: f64 = R_STAR / M_AIR;
57pub const GMR: f64 = G0 * M_AIR / R_STAR;
59pub const EARTH_RADIUS_M: f64 = 6_356_766.0;
61pub const P0_PA: f64 = 101_325.0;
63pub const T0_K: f64 = 288.15;
65
66const FT_PER_M: f64 = 1.0 / 0.3048;
68
69struct Layer {
72 base_geopotential_m: f64,
73 base_temp_k: f64,
74 lapse_k_per_m: f64,
75 base_pressure_pa: f64,
76}
77
78static LAYERS: [Layer; 7] = [
80 Layer { base_geopotential_m: 0.0, base_temp_k: 288.15, lapse_k_per_m: -0.006_5, base_pressure_pa: 101_325.0 },
81 Layer { base_geopotential_m: 11_000.0, base_temp_k: 216.65, lapse_k_per_m: 0.0, base_pressure_pa: 22_632.06 },
82 Layer { base_geopotential_m: 20_000.0, base_temp_k: 216.65, lapse_k_per_m: 0.001, base_pressure_pa: 5_474.889 },
83 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 },
84 Layer { base_geopotential_m: 47_000.0, base_temp_k: 270.65, lapse_k_per_m: 0.0, base_pressure_pa: 110.906_3 },
85 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 },
86 Layer { base_geopotential_m: 71_000.0, base_temp_k: 214.65, lapse_k_per_m: -0.002, base_pressure_pa: 3.956_420 },
87];
88
89fn layer_for_geopotential(h_m: f64) -> &'static Layer {
90 let mut chosen = &LAYERS[0];
91 for layer in LAYERS.iter() {
92 if h_m >= layer.base_geopotential_m {
93 chosen = layer;
94 } else {
95 break;
96 }
97 }
98 chosen
99}
100
101fn layer_for_pressure(p_pa: f64) -> &'static Layer {
102 let mut chosen = &LAYERS[0];
105 for layer in LAYERS.iter() {
106 if p_pa <= layer.base_pressure_pa {
107 chosen = layer;
108 } else {
109 break;
110 }
111 }
112 chosen
113}
114
115pub fn temperature_k_from_geopotential_m(h_m: f64) -> f64 {
117 let l = layer_for_geopotential(h_m);
118 l.base_temp_k + l.lapse_k_per_m * (h_m - l.base_geopotential_m)
119}
120
121pub fn pressure_pa_from_geopotential_m(h_m: f64) -> f64 {
123 let l = layer_for_geopotential(h_m);
124 let dh = h_m - l.base_geopotential_m;
125 if l.lapse_k_per_m.abs() < f64::EPSILON {
126 l.base_pressure_pa * (-GMR * dh / l.base_temp_k).exp()
128 } else {
129 let t = l.base_temp_k + l.lapse_k_per_m * dh;
131 l.base_pressure_pa * (l.base_temp_k / t).powf(GMR / l.lapse_k_per_m)
132 }
133}
134
135pub fn pressure_hpa_from_geopotential_m(h_m: f64) -> f64 {
137 pressure_pa_from_geopotential_m(h_m) / 100.0
138}
139
140pub fn geopotential_m_from_pressure_pa(p_pa: f64) -> f64 {
143 let l = layer_for_pressure(p_pa);
144 if l.lapse_k_per_m.abs() < f64::EPSILON {
145 l.base_geopotential_m - (l.base_temp_k / GMR) * (p_pa / l.base_pressure_pa).ln()
146 } else {
147 let t = l.base_temp_k * (p_pa / l.base_pressure_pa).powf(-l.lapse_k_per_m / GMR);
148 l.base_geopotential_m + (t - l.base_temp_k) / l.lapse_k_per_m
149 }
150}
151
152pub fn density_kg_m3_from_geopotential_m(h_m: f64) -> f64 {
154 let p = pressure_pa_from_geopotential_m(h_m);
155 let t = temperature_k_from_geopotential_m(h_m);
156 p / (R_AIR * t)
157}
158
159pub fn geopotential_from_geometric(z_m: f64) -> f64 {
161 EARTH_RADIUS_M * z_m / (EARTH_RADIUS_M + z_m)
162}
163
164pub fn geometric_from_geopotential(h_m: f64) -> f64 {
166 EARTH_RADIUS_M * h_m / (EARTH_RADIUS_M - h_m)
167}
168
169pub fn pressure_hpa_from_flight_level(fl: f64) -> f64 {
176 let h_m = fl * 100.0 / FT_PER_M; pressure_pa_from_geopotential_m(h_m) / 100.0
178}
179
180pub fn flight_level_from_pressure_hpa(p_hpa: f64) -> f64 {
183 let h_m = geopotential_m_from_pressure_pa(p_hpa * 100.0);
184 h_m * FT_PER_M / 100.0
185}
186
187pub fn pressure_altitude_ft_from_pressure_hpa(p_hpa: f64) -> f64 {
190 geopotential_m_from_pressure_pa(p_hpa * 100.0) * FT_PER_M
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 fn approx(a: f64, b: f64, tol: f64) {
198 assert!((a - b).abs() < tol, "{a} vs {b} (tol {tol})");
199 }
200
201 #[test]
202 fn sea_level_matches_the_standard() {
203 approx(pressure_hpa_from_geopotential_m(0.0), 1013.25, 0.01);
204 approx(temperature_k_from_geopotential_m(0.0), 288.15, 0.01);
205 approx(density_kg_m3_from_geopotential_m(0.0), 1.225, 0.001);
206 }
207
208 #[test]
209 fn flight_levels_are_isobars() {
210 approx(pressure_hpa_from_flight_level(300.0), 300.9, 0.3);
211 approx(pressure_hpa_from_flight_level(350.0), 238.4, 0.3);
212 approx(pressure_hpa_from_flight_level(400.0), 187.5, 0.3);
213 }
214
215 #[test]
216 fn flight_level_round_trips() {
217 for fl in [50.0, 180.0, 300.0, 410.0] {
218 let p = pressure_hpa_from_flight_level(fl);
219 approx(flight_level_from_pressure_hpa(p), fl, 0.01);
220 }
221 }
222
223 #[test]
224 fn known_pressure_heights() {
225 approx(geopotential_m_from_pressure_pa(50_000.0), 5575.0, 5.0);
227 approx(geopotential_m_from_pressure_pa(25_000.0), 10_363.0, 10.0);
228 }
229
230 #[test]
231 fn tropopause_is_piecewise() {
232 approx(pressure_hpa_from_geopotential_m(11_000.0), 226.32, 0.05);
234 approx(temperature_k_from_geopotential_m(11_000.0), 216.65, 0.01);
235 approx(temperature_k_from_geopotential_m(18_000.0), 216.65, 0.01);
237 }
238
239 #[test]
240 fn geopotential_geometric_correction_is_small_but_present() {
241 let z = geometric_from_geopotential(11_000.0);
242 assert!(z > 11_000.0 && z - 11_000.0 < 25.0); approx(geopotential_from_geometric(z), 11_000.0, 1e-6);
244 }
245}