icao_isa/
lib.rs

1// Copyright (c) 2024 Via Technology Ltd.
2
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"),
5// to deal in the Software without restriction, including without limitation the
6// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7// sell copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19// THE SOFTWARE.
20
21//! [![crates.io](https://img.shields.io/crates/v/icao-isa.svg)](https://crates.io/crates/icao-isa)
22//! [![docs.io](https://docs.rs/icao-isa/badge.svg)](https://docs.rs/icao-isa/)
23//! [![License](https://img.shields.io/badge/License-MIT-blue)](https://opensource.org/license/mit/)
24//! [![Rust](https://github.com/kenba/icao-isa-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/kenba/icao-isa-rs/actions)
25//! [![codecov](https://codecov.io/gh/kenba/icao-isa-rs/graph/badge.svg?token=6DTOY9Y4BT)](https://codecov.io/gh/kenba/icao-isa-rs)
26//!
27//! An implementation of the [International Civil Aviation Organization](https://icao.int/) (ICAO)
28//! [International Standard Atmosphere](https://en.wikipedia.org/wiki/International_Standard_Atmosphere)
29//! (ISA), see [ICAO Doc 7488/3](https://standart.aero/en/icao/book/doc-7488-manual-of-the-icao-standard-atmosphere-extended-to-80-kilometres-262-500-feet-en-cons).
30//!
31//! The library also includes functions for calculating:
32//!
33//! - true airspeed ([TAS](https://en.wikipedia.org/wiki/True_airspeed)) from calibrated airspeed ([CAS](https://en.wikipedia.org/wiki/Calibrated_airspeed)), pressure and temperature;
34//! - CAS from TAS, pressure and temperature;
35//! - TAS from [Mach number](https://en.wikipedia.org/wiki/Mach_number) and temperature;
36//! - and the crossover altitude between CAS / MACH flight regimes.
37//!
38//! The equations for the functions above are from
39//! [BADA User Manual revision 3-12](https://www.scribd.com/document/289480324/1-User-Manual-Bada-3-12).
40//!
41//! The library is declared [no_std](https://docs.rust-embedded.org/book/intro/no-std.html)
42//! so it can be used in embedded applications.
43
44#![cfg_attr(not(test), no_std)]
45#![allow(clippy::suboptimal_flops)]
46
47pub mod constants;
48
49use icao_units::si::{Kelvin, KilogramsPerCubicMetre, Metres, MetresPerSecond, Pascals};
50
51/// The coefficient used in CAS / TAS conversions.
52/// See BADA Equation 3.2-14
53const U: f64 = (constants::K - 1.0) / constants::K;
54
55/// Another coefficient used in pressure conversions.
56/// See BADA Equation 3.2-14
57const INV_U: f64 = 1.0 / U;
58
59const K_MINUS_1_OVER_2: f64 = (constants::K - 1.0) / 2.0;
60
61/// The Power factor used in calculating the pressure below the Tropopause.
62/// See BADA Equation 3.1-18
63const PRESSURE_POWER: f64 = -constants::g.0 / (constants::TEMPERATURE_GRADIENT * constants::R);
64
65/// The Power factor used in calculating the altitude below the Tropopause.
66/// See BADA Equation 3.1-8
67const TEMPERATURE_POWER: f64 = 1.0 / PRESSURE_POWER;
68
69/// The pressure at `TROPOPAUSE_ALTITUDE`.
70/// See BADA Equation Eq 3.1-19
71const TROPOPAUSE_PRESSURE: Pascals = Pascals(22_632.040_095_007_81);
72
73/// The factor used in calculating the density and pressure above Tropopause.
74/// See BADA Equation 3.2-16
75const TROPOPAUSE_PRESSURE_FACTOR: f64 =
76    -constants::g.0 / (constants::R * constants::TROPOPAUSE_TEMPERATURE.0);
77
78/// Calculate the ISA pressure below the tropopause for the given altitude.  
79/// See BADA Rev 3.12, Eq 3.1-18
80/// * `altitude` the altitude in Metres: max tropopause - `11_000` metres.
81///
82/// returns the pressure in pascals.
83#[must_use]
84fn calculate_troposphere_pressure(altitude: Metres) -> Pascals {
85    Pascals(
86        constants::SEA_LEVEL_PRESSURE.0
87            * libm::pow(
88                1.0 + altitude.0 * constants::TEMPERATURE_GRADIENT
89                    / constants::SEA_LEVEL_TEMPERATURE.0,
90                PRESSURE_POWER,
91            ),
92    )
93}
94
95/// Calculate the ISA pressure in the tropopause for the given altitude.  
96/// See BADA Rev 3.12, Eq 3.1-20
97/// * `altitude` the altitude in Metres: min tropopause - `11_000` metres.
98///
99/// returns the pressure in Pascals.
100#[must_use]
101fn calculate_tropopause_pressure(altitude: Metres) -> Pascals {
102    Pascals(
103        TROPOPAUSE_PRESSURE.0
104            * libm::exp(
105                TROPOPAUSE_PRESSURE_FACTOR * (altitude.0 - constants::TROPOPAUSE_ALTITUDE.0),
106            ),
107    )
108}
109
110/// Calculate the ISA pressure corresponding to the given altitude.  
111/// Note: ISA pressure does **NOT** vary with temperature.  
112/// See BADA Rev 3.12, Eq 3.1-18 & Eq 3.1-20
113/// * `altitude` the pressure altitude in metres.
114///
115/// returns the pressure in Pascals.
116#[must_use]
117pub fn calculate_isa_pressure(altitude: Metres) -> Pascals {
118    if altitude < constants::TROPOPAUSE_ALTITUDE {
119        calculate_troposphere_pressure(altitude)
120    } else {
121        calculate_tropopause_pressure(altitude)
122    }
123}
124
125/// Calculate the altitude corresponding to the given pressure below the tropopause.  
126/// See BADA Rev 3.12, Eq 3.1-18
127/// * `pressure` the pressure in Pascals.
128///
129/// returns the altitude in metres.
130#[must_use]
131fn calculate_troposphere_altitude(pressure: Pascals) -> Metres {
132    let pressure_ratio = pressure.0 / constants::SEA_LEVEL_PRESSURE.0;
133    let altitude_ratio = libm::pow(pressure_ratio, TEMPERATURE_POWER) - 1.0;
134    Metres(altitude_ratio * constants::SEA_LEVEL_TEMPERATURE.0 / constants::TEMPERATURE_GRADIENT)
135}
136
137/// Calculate the altitude corresponding to the given pressure in the tropopause.  
138/// See BADA Rev 3.12, Eq 3.1-20
139/// * `pressure` the pressure in Pascals.
140///
141/// returns the altitude in metres.
142#[must_use]
143fn calculate_tropopause_altitude(pressure: Pascals) -> Metres {
144    let altitude_delta = libm::log(pressure.0 / TROPOPAUSE_PRESSURE.0) / TROPOPAUSE_PRESSURE_FACTOR;
145    Metres(constants::TROPOPAUSE_ALTITUDE.0 + altitude_delta)
146}
147
148/// Calculate the ISA altitude corresponding to the given pressure.  
149/// See BADA Rev 3.12, Eq 3.1-18 & Eq 3.1-20
150/// * `altitude` the pressure altitude in metres.
151///
152/// returns the pressure in Pascals.
153#[must_use]
154pub fn calculate_isa_altitude(pressure: Pascals) -> Metres {
155    if pressure > TROPOPAUSE_PRESSURE {
156        calculate_troposphere_altitude(pressure)
157    } else {
158        calculate_tropopause_altitude(pressure)
159    }
160}
161
162/// Calculate the ISA temperature corresponding to the given altitude and
163/// difference in Sea level temperature.  
164/// See ICAO Doc 7488/3, Eq (11)
165/// * `altitude` the altitude in Metres.
166/// * `delta_temperature` the difference from ISA temperature at Sea level.
167///
168/// returns the temperature in Kelvin.
169#[must_use]
170pub fn calculate_isa_temperature(altitude: Metres, delta_temperature: Kelvin) -> Kelvin {
171    let temperature = Kelvin(
172        constants::SEA_LEVEL_TEMPERATURE.0
173            + delta_temperature.0
174            + altitude.0 * constants::TEMPERATURE_GRADIENT,
175    );
176
177    if temperature > constants::TROPOPAUSE_TEMPERATURE {
178        temperature
179    } else {
180        constants::TROPOPAUSE_TEMPERATURE
181    }
182}
183
184/// Calculate the air density given the air temperature and pressure.  
185/// Uses the Ideal Gas Equation (Boyles law)  
186/// See See ICAO Doc 7488/3, Eq (3).
187/// * `pressure` the pressure in Pascals.
188/// * `temperature` the temperature in Kelvin.
189///
190/// returns the density in Kg per cubic metre.
191#[must_use]
192pub fn calculate_density(pressure: Pascals, temperature: Kelvin) -> KilogramsPerCubicMetre {
193    KilogramsPerCubicMetre(pressure.0 / (temperature.0 * constants::R))
194}
195
196/// Calculate the True Air Speed (TAS) from the Calibrated Air Speed (CAS)
197/// at the given pressure and temperature.  
198/// See BADA Rev 3.12, Eq 3.1.23
199/// * `cas` the Calibrated Air Speed in metres per second.
200/// * `pressure` the pressure in Pascals.
201/// * `temperature` the temperature in Kelvin.
202///
203/// returns the True Air Speed in metres per second.
204#[must_use]
205pub fn calculate_true_air_speed(
206    cas: MetresPerSecond,
207    pressure: Pascals,
208    temperature: Kelvin,
209) -> MetresPerSecond {
210    const INNER_FACTOR: f64 = U / (2.0 * constants::R * constants::SEA_LEVEL_TEMPERATURE.0);
211    const OUTER_FACTOR: f64 = 2.0 * constants::R / U;
212
213    let cas_factor = libm::pow(1.0 + INNER_FACTOR * cas.0 * cas.0, INV_U) - 1.0;
214    let cas_pressure_factor = libm::pow(
215        1.0 + constants::SEA_LEVEL_PRESSURE.0 * cas_factor / pressure.0,
216        U,
217    ) - 1.0;
218    MetresPerSecond(libm::sqrt(
219        OUTER_FACTOR * temperature.0 * cas_pressure_factor,
220    ))
221}
222
223/// Calculate the Calibrated Air Speed (CAS) from the True Air Speed (TAS)
224/// at the given pressure and temperature.  
225/// See BADA Rev 3.12, Eq 3.1.24
226/// * `tas` the True Air Speed in metres per second.
227/// * `pressure` the pressure in Pascals.
228/// * `temperature` the temperature in Kelvin.
229///
230/// * returns the Calibrated Air Speed in metres per second.
231#[must_use]
232pub fn calculate_calibrated_air_speed(
233    tas: MetresPerSecond,
234    pressure: Pascals,
235    temperature: Kelvin,
236) -> MetresPerSecond {
237    const INNER_FACTOR: f64 = U / (2.0 * constants::R);
238    const OUTER_FACTOR: f64 = 2.0 * constants::R * constants::SEA_LEVEL_TEMPERATURE.0 / U;
239
240    let tas_factor = libm::pow(1.0 + INNER_FACTOR * tas.0 * tas.0 / temperature.0, INV_U) - 1.0;
241    let tas_pressure_factor = libm::pow(
242        1.0 + pressure.0 * tas_factor / constants::SEA_LEVEL_PRESSURE.0,
243        U,
244    ) - 1.0;
245
246    MetresPerSecond(libm::sqrt(OUTER_FACTOR * tas_pressure_factor))
247}
248
249/// Calculate the speed of sound for the given temperature.  
250/// See ICAO Doc 7488/3, Eq 21
251/// * `temperature` the temperature in Kelvin.
252///
253/// returns the speed of sound in metres per second.
254#[must_use]
255pub fn speed_of_sound(temperature: Kelvin) -> MetresPerSecond {
256    MetresPerSecond(libm::sqrt(constants::K * constants::R * temperature.0))
257}
258
259/// Calculate the True Air Speed (TAS) from the Mach number at the given temperature.  
260/// See BADA Rev 3.12, Eq 3.1.22
261/// * `mach` the Mach number.
262/// * `temperature` the temperature in Kelvin.
263///
264/// returns the True Air Speed in metres per second.
265#[must_use]
266pub fn mach_true_air_speed(mach: f64, temperature: Kelvin) -> MetresPerSecond {
267    MetresPerSecond(mach * speed_of_sound(temperature).0)
268}
269
270/// This function calculates the crossover pressure ratio between the
271/// Calibrated Air Speed (CAS) and Mach number.  
272/// See BADA Rev 3.12, Eq 3.1-29
273/// * `cas` the Calibrated Air Speed in metres per second.
274/// * `mach` the Mach number.
275///
276/// returns the crossover pressure ratio.
277#[must_use]
278fn calculate_crossover_pressure_ratio(cas: MetresPerSecond, mach: f64) -> f64 {
279    let cas_mach = cas.0 / constants::SEA_LEVEL_SPEED_OF_SOUND.0;
280
281    let numerator = libm::pow(1.0 + K_MINUS_1_OVER_2 * cas_mach * cas_mach, INV_U) - 1.0;
282    let denominator = libm::pow(1.0 + K_MINUS_1_OVER_2 * mach * mach, INV_U) - 1.0;
283
284    numerator / denominator
285}
286
287/// Calculate the crossover altitude at which the True Air Speeds (TAS)
288/// corresponding to the given Calibrated Air Speed (CAS) and Mach number are
289/// the same.  
290/// See BADA Rev 3.12, Eq 3.1-27
291/// * `cas` the Calibrated Air Speed in metres per second.
292/// * `mach` the Mach number.
293///
294/// returns the altitude in metres.
295#[must_use]
296pub fn calculate_crossover_altitude(cas: MetresPerSecond, mach: f64) -> Metres {
297    let temperature_ratio = libm::pow(
298        calculate_crossover_pressure_ratio(cas, mach),
299        TEMPERATURE_POWER,
300    );
301
302    Metres(
303        constants::SEA_LEVEL_TEMPERATURE.0 * (1.0 - temperature_ratio)
304            / -constants::TEMPERATURE_GRADIENT,
305    )
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_calculate_isa_pressure() {
314        // calculate_troposphere_pressure
315        assert_eq!(
316            constants::SEA_LEVEL_PRESSURE,
317            calculate_isa_pressure(Metres(0.0))
318        );
319        assert!(libm::fabs(89874.563 - calculate_isa_pressure(Metres(1000.0)).0) < 0.001);
320        assert!(libm::fabs(79495.201 - calculate_isa_pressure(Metres(2000.0)).0) < 0.001);
321        assert!(libm::fabs(22635.609 - calculate_isa_pressure(Metres(10999.0)).0) < 0.001);
322
323        // calculate_tropopause_pressure
324        assert_eq!(
325            22632.04009500781,
326            calculate_isa_pressure(constants::TROPOPAUSE_ALTITUDE).0
327        );
328        assert!(libm::fabs(19330.383 - calculate_isa_pressure(Metres(12000.0)).0) < 0.001);
329    }
330
331    #[test]
332    fn test_calculate_isa_altitude() {
333        // calculate_troposphere_altitude
334        assert_eq!(0.0, calculate_isa_altitude(constants::SEA_LEVEL_PRESSURE).0);
335        assert!(libm::fabs(1000.0 - calculate_isa_altitude(Pascals(89874.563)).0) < 0.001);
336        assert!(libm::fabs(2000.0 - calculate_isa_altitude(Pascals(79495.201)).0) < 0.001);
337        assert!(libm::fabs(10999.0 - calculate_isa_altitude(Pascals(22635.609)).0) < 0.001);
338
339        // calculate_tropopause_altitude
340        assert_eq!(
341            constants::TROPOPAUSE_ALTITUDE.0,
342            calculate_isa_altitude(TROPOPAUSE_PRESSURE).0
343        );
344        assert!(libm::fabs(12000.0 - calculate_isa_altitude(Pascals(19330.383)).0) < 0.001);
345    }
346
347    #[test]
348    fn test_calculate_isa_temperature() {
349        assert_eq!(
350            constants::SEA_LEVEL_TEMPERATURE.0 - 3.25,
351            calculate_isa_temperature(Metres(500.0), Kelvin(0.0)).0
352        );
353        assert_eq!(
354            constants::SEA_LEVEL_TEMPERATURE.0 - 13.0,
355            calculate_isa_temperature(Metres(2000.0), Kelvin(0.0)).0
356        );
357        assert_eq!(
358            constants::TROPOPAUSE_TEMPERATURE.0,
359            calculate_isa_temperature(constants::TROPOPAUSE_ALTITUDE, Kelvin(0.0)).0
360        );
361        assert!(
362            libm::fabs(
363                constants::TROPOPAUSE_TEMPERATURE.0 + 10.
364                    - calculate_isa_temperature(constants::TROPOPAUSE_ALTITUDE, Kelvin(10.0)).0
365            ) < 1.0e-9
366        );
367        assert_eq!(
368            constants::TROPOPAUSE_TEMPERATURE.0,
369            calculate_isa_temperature(Metres(12000.0), Kelvin(-10.0)).0
370        );
371    }
372
373    #[test]
374    fn test_calculate_density() {
375        assert!(
376            libm::fabs(
377                constants::SEA_LEVEL_DENSITY.0
378                    - calculate_density(
379                        constants::SEA_LEVEL_PRESSURE,
380                        constants::SEA_LEVEL_TEMPERATURE
381                    )
382                    .0
383            ) < 2.0e-8
384        );
385        assert!(
386            libm::fabs(
387                0.3639176
388                    - calculate_density(TROPOPAUSE_PRESSURE, constants::TROPOPAUSE_TEMPERATURE).0
389            ) < 1.0e-6
390        );
391    }
392
393    #[test]
394    fn test_calculate_true_air_speed() {
395        assert!(
396            libm::fabs(
397                150.0
398                    - calculate_true_air_speed(
399                        MetresPerSecond(150.0),
400                        constants::SEA_LEVEL_PRESSURE,
401                        constants::SEA_LEVEL_TEMPERATURE
402                    )
403                    .0
404            ) < 1.0e-9
405        );
406        assert!(
407            libm::fabs(
408                164.458
409                    - calculate_true_air_speed(
410                        MetresPerSecond(150.0),
411                        Pascals(79495.201),
412                        Kelvin(constants::SEA_LEVEL_TEMPERATURE.0 - 13.0)
413                    )
414                    .0
415            ) < 0.001
416        );
417    }
418
419    #[test]
420    fn test_calculate_calibrated_air_speed() {
421        assert!(
422            libm::fabs(
423                150.0
424                    - calculate_calibrated_air_speed(
425                        MetresPerSecond(150.0),
426                        constants::SEA_LEVEL_PRESSURE,
427                        constants::SEA_LEVEL_TEMPERATURE
428                    )
429                    .0
430            ) < 1.0e-9
431        );
432        assert!(
433            libm::fabs(
434                150.0
435                    - calculate_calibrated_air_speed(
436                        MetresPerSecond(164.458),
437                        Pascals(79495.201),
438                        Kelvin(constants::SEA_LEVEL_TEMPERATURE.0 - 13.0)
439                    )
440                    .0
441            ) < 0.001
442        );
443    }
444
445    #[test]
446    fn test_speed_of_sound() {
447        assert_eq!(0.0, speed_of_sound(Kelvin(0.0)).0);
448        assert!(
449            libm::fabs(
450                constants::SEA_LEVEL_SPEED_OF_SOUND.0
451                    - speed_of_sound(constants::SEA_LEVEL_TEMPERATURE).0
452            ) < 0.001
453        );
454        assert!(libm::fabs(295.070 - speed_of_sound(constants::TROPOPAUSE_TEMPERATURE).0) < 0.001);
455    }
456
457    #[test]
458    fn test_mach_true_air_speed() {
459        assert!(
460            libm::fabs(
461                0.8 * constants::SEA_LEVEL_SPEED_OF_SOUND.0
462                    - mach_true_air_speed(0.8, constants::SEA_LEVEL_TEMPERATURE).0
463            ) < 0.001
464        );
465        assert!(
466            libm::fabs(250.809 - mach_true_air_speed(0.85, constants::TROPOPAUSE_TEMPERATURE).0)
467                < 0.001
468        );
469    }
470
471    #[test]
472    fn test_calculate_crossover_altitude() {
473        let cas = MetresPerSecond(155.0);
474        let crossover_altitude = calculate_crossover_altitude(cas, 0.79);
475        assert!(libm::fabs(9070.814 - crossover_altitude.0) < 0.001);
476
477        // The TAS should be the same from both CAS and MACH at the crossover_altitude
478        let pressure = calculate_isa_pressure(crossover_altitude);
479        let temperature = calculate_isa_temperature(crossover_altitude, Kelvin(0.0));
480        let tas_from_cas = calculate_true_air_speed(cas, pressure, temperature);
481        let tas_from_mach = mach_true_air_speed(0.79, temperature);
482        assert!(libm::fabs(tas_from_cas.0 - tas_from_mach.0) < 0.001);
483    }
484}