1#![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
51const U: f64 = (constants::K - 1.0) / constants::K;
54
55const INV_U: f64 = 1.0 / U;
58
59const K_MINUS_1_OVER_2: f64 = (constants::K - 1.0) / 2.0;
60
61const PRESSURE_POWER: f64 = -constants::g.0 / (constants::TEMPERATURE_GRADIENT * constants::R);
64
65const TEMPERATURE_POWER: f64 = 1.0 / PRESSURE_POWER;
68
69const TROPOPAUSE_PRESSURE: Pascals = Pascals(22_632.040_095_007_81);
72
73const TROPOPAUSE_PRESSURE_FACTOR: f64 =
76 -constants::g.0 / (constants::R * constants::TROPOPAUSE_TEMPERATURE.0);
77
78#[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#[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#[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#[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#[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#[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#[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#[must_use]
192pub fn calculate_density(pressure: Pascals, temperature: Kelvin) -> KilogramsPerCubicMetre {
193 KilogramsPerCubicMetre(pressure.0 / (temperature.0 * constants::R))
194}
195
196#[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#[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#[must_use]
255pub fn speed_of_sound(temperature: Kelvin) -> MetresPerSecond {
256 MetresPerSecond(libm::sqrt(constants::K * constants::R * temperature.0))
257}
258
259#[must_use]
266pub fn mach_true_air_speed(mach: f64, temperature: Kelvin) -> MetresPerSecond {
267 MetresPerSecond(mach * speed_of_sound(temperature).0)
268}
269
270#[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#[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 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 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 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 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 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}