ballistics_engine/
atmosphere.rs

1//! Enhanced atmospheric calculations for ballistics.
2//!
3//! This module provides Rust-accelerated implementations of atmospheric calculations
4//! with full ICAO Standard Atmosphere support for improved accuracy at all altitudes.
5
6/// ICAO Standard Atmosphere layer definitions
7#[derive(Debug, Clone)]
8struct AtmosphereLayer {
9    /// Base altitude of this layer (m)
10    base_altitude: f64,
11    /// Base temperature at layer start (K)
12    base_temperature: f64,
13    /// Base pressure at layer start (Pa)
14    base_pressure: f64,
15    /// Temperature lapse rate (K/m)
16    lapse_rate: f64,
17}
18
19/// ICAO Standard Atmosphere constants
20const G_ACCEL_MPS2: f64 = 9.80665;
21const R_AIR: f64 = 287.0531; // Specific gas constant for dry air (J/(kg·K))
22const GAMMA: f64 = 1.4; // Heat capacity ratio for air
23const R_DRY: f64 = 287.05; // Gas constant for dry air
24const R_VAPOR: f64 = 461.495; // Gas constant for water vapor
25
26/// CIPM constants for precise air density calculation
27const R: f64 = 8.314472; // Universal gas constant
28const M_A: f64 = 28.96546e-3; // Molar mass of dry air (kg/mol)
29const M_V: f64 = 18.01528e-3; // Molar mass of water vapor (kg/mol)
30
31/// ICAO Standard Atmosphere layer data up to 84 km
32/// Pressures calculated using barometric formula between layers
33const ICAO_LAYERS: &[AtmosphereLayer] = &[
34    // Troposphere (0 - 11 km)
35    AtmosphereLayer {
36        base_altitude: 0.0,
37        base_temperature: 288.15, // 15°C
38        base_pressure: 101325.0,  // 1013.25 hPa
39        lapse_rate: -0.0065,      // -6.5 K/km
40    },
41    // Tropopause (11 - 20 km)
42    AtmosphereLayer {
43        base_altitude: 11000.0,
44        base_temperature: 216.65, // -56.5°C
45        base_pressure: 22632.1,   // 226.32 hPa
46        lapse_rate: 0.0,          // Isothermal
47    },
48    // Stratosphere 1 (20 - 32 km)
49    AtmosphereLayer {
50        base_altitude: 20000.0,
51        base_temperature: 216.65, // -56.5°C
52        base_pressure: 5474.89,   // 54.75 hPa
53        lapse_rate: 0.001,        // +1 K/km
54    },
55    // Stratosphere 2 (32 - 47 km)
56    AtmosphereLayer {
57        base_altitude: 32000.0,
58        base_temperature: 228.65, // -44.5°C
59        base_pressure: 868.02,    // 8.68 hPa
60        lapse_rate: 0.0028,       // +2.8 K/km
61    },
62    // Stratopause (47 - 51 km)
63    AtmosphereLayer {
64        base_altitude: 47000.0,
65        base_temperature: 270.65, // -2.5°C
66        base_pressure: 110.91,    // 1.11 hPa
67        lapse_rate: 0.0,          // Isothermal
68    },
69    // Mesosphere 1 (51 - 71 km)
70    AtmosphereLayer {
71        base_altitude: 51000.0,
72        base_temperature: 270.65, // -2.5°C
73        base_pressure: 66.94,     // 0.67 hPa
74        lapse_rate: -0.0028,      // -2.8 K/km
75    },
76    // Mesosphere 2 (71 - 84 km)
77    AtmosphereLayer {
78        base_altitude: 71000.0,
79        base_temperature: 214.65, // -58.5°C
80        base_pressure: 3.96,      // 0.04 hPa
81        lapse_rate: -0.002,       // -2.0 K/km
82    },
83];
84
85/// Calculate ICAO Standard Atmosphere conditions at any altitude.
86///
87/// This function implements the full ICAO Standard Atmosphere model with all
88/// atmospheric layers up to 84 km altitude.
89///
90/// # Arguments
91/// * `altitude_m` - Altitude in meters (0 to 84000)
92///
93/// # Returns
94/// Tuple of (temperature_k, pressure_pa)
95fn calculate_icao_standard_atmosphere(altitude_m: f64) -> (f64, f64) {
96    // Clamp altitude to valid range
97    let altitude = altitude_m.clamp(0.0, 84000.0);
98
99    // Find the appropriate atmospheric layer
100    let layer = ICAO_LAYERS
101        .iter()
102        .rev()
103        .find(|layer| altitude >= layer.base_altitude)
104        .unwrap_or(&ICAO_LAYERS[0]);
105
106    let height_diff = altitude - layer.base_altitude;
107    let temperature = layer.base_temperature + layer.lapse_rate * height_diff;
108
109    let pressure = if layer.lapse_rate.abs() < 1e-10 {
110        // Isothermal layer
111        layer.base_pressure * (-G_ACCEL_MPS2 * height_diff / (R_AIR * layer.base_temperature)).exp()
112    } else {
113        // Non-isothermal layer
114        let temp_ratio = temperature / layer.base_temperature;
115        layer.base_pressure * temp_ratio.powf(-G_ACCEL_MPS2 / (layer.lapse_rate * R_AIR))
116    };
117
118    (temperature, pressure)
119}
120
121/// Enhanced atmospheric calculation with ICAO Standard Atmosphere.
122///
123/// # Arguments
124/// * `altitude_m` - Altitude in meters
125/// * `temp_override_c` - Temperature override in Celsius (None for standard)
126/// * `press_override_hpa` - Pressure override in hPa (None for standard)
127/// * `humidity_percent` - Humidity percentage (0-100)
128///
129/// # Returns
130/// Tuple of (air_density_kg_m3, speed_of_sound_mps)
131pub fn calculate_atmosphere(
132    altitude_m: f64,
133    temp_override_c: Option<f64>,
134    press_override_hpa: Option<f64>,
135    humidity_percent: f64,
136) -> (f64, f64) {
137    // Get standard atmosphere conditions or use overrides
138    let (temp_k, pressure_pa) = if temp_override_c.is_some() && press_override_hpa.is_some() {
139        // Both overrides provided
140        (
141            temp_override_c.unwrap() + 273.15,
142            press_override_hpa.unwrap() * 100.0,
143        )
144    } else {
145        // Get ICAO standard conditions
146        let (std_temp_k, std_pressure_pa) = calculate_icao_standard_atmosphere(altitude_m);
147
148        let final_temp_k = if let Some(temp_c) = temp_override_c {
149            temp_c + 273.15
150        } else {
151            std_temp_k
152        };
153
154        let final_pressure_pa = if let Some(press_hpa) = press_override_hpa {
155            press_hpa * 100.0
156        } else {
157            std_pressure_pa
158        };
159
160        (final_temp_k, final_pressure_pa)
161    };
162
163    // Enhanced humidity effects on air density and speed of sound
164    let humidity_clamped = humidity_percent.clamp(0.0, 100.0);
165
166    // Calculate saturation vapor pressure (enhanced Magnus formula)
167    let temp_c = temp_k - 273.15;
168    let es_hpa = if temp_c >= 0.0 {
169        // Over water (Arden Buck equation)
170        6.1121 * (18.678 - temp_c / 234.5) * (temp_c / (257.14 + temp_c)).exp()
171    } else {
172        // Over ice (Arden Buck equation)
173        6.1115 * (23.036 - temp_c / 333.7) * (temp_c / (279.82 + temp_c)).exp()
174    };
175
176    // Calculate actual vapor pressure
177    let vapor_pressure_pa = humidity_clamped / 100.0 * es_hpa * 100.0;
178    let dry_pressure_pa = (pressure_pa - vapor_pressure_pa).max(0.0);
179
180    // Calculate air density with humidity effects
181    let density = dry_pressure_pa / (R_DRY * temp_k) + vapor_pressure_pa / (R_VAPOR * temp_k);
182
183    // Enhanced speed of sound calculation with humidity effects
184    // Speed of sound in moist air (Cramer, 1993)
185    let mole_fraction_vapor = vapor_pressure_pa / pressure_pa;
186    let temp_c_abs = temp_k;
187
188    // Heat capacity ratio for moist air
189    let gamma_moist = GAMMA * (1.0 - mole_fraction_vapor * 0.11);
190
191    // Gas constant for moist air
192    let r_moist = R_AIR * (1.0 + 0.6078 * mole_fraction_vapor);
193
194    // Speed of sound with enhanced humidity correction
195    let speed_of_sound_base = (gamma_moist * r_moist * temp_c_abs).sqrt();
196
197    // Additional humidity correction for molecular effects
198    let humidity_correction = 1.0 + 0.0001 * humidity_clamped * (temp_c / 20.0);
199    let speed_of_sound = speed_of_sound_base * humidity_correction;
200
201    (density, speed_of_sound)
202}
203
204/// Enhanced air density calculation using CIPM formula with ICAO atmosphere.
205///
206/// # Arguments
207/// * `temp_c` - Temperature in Celsius
208/// * `pressure_hpa` - Pressure in hPa
209/// * `humidity_percent` - Humidity percentage (0-100)
210///
211/// # Returns
212/// Air density in kg/m³
213pub fn calculate_air_density_cimp(temp_c: f64, pressure_hpa: f64, humidity_percent: f64) -> f64 {
214    let t_k = temp_c + 273.15;
215
216    // Enhanced saturation vapor pressure calculation
217    let p_sv = enhanced_saturation_vapor_pressure(t_k);
218
219    // Enhanced enhancement factor with temperature dependence
220    let f = enhanced_enhancement_factor(pressure_hpa, temp_c);
221
222    // Vapor pressure with clamping
223    let p_v = humidity_percent.clamp(0.0, 100.0) / 100.0 * f * p_sv;
224
225    // Mole fraction of water vapor
226    let x_v = p_v / pressure_hpa;
227
228    // Enhanced compressibility factor
229    let z = enhanced_compressibility_factor(pressure_hpa, t_k, x_v);
230
231    // Calculate density with enhanced precision
232    // Note: parentheses are important here for correct operator precedence
233    let density = ((pressure_hpa * M_A) / (z * R * t_k)) * (1.0 - x_v * (1.0 - M_V / M_A));
234
235    // Convert from SI units (pressure in Pa) to final density in kg/m³
236    // pressure_hpa is in hPa, so multiply by 100 to get Pa
237    density * 100.0
238}
239
240/// Enhanced saturation vapor pressure calculation.
241/// Uses the IAPWS-IF97 formulation for high precision.
242#[inline(always)]
243fn enhanced_saturation_vapor_pressure(t_k: f64) -> f64 {
244    // IAPWS-IF97 coefficients for better accuracy
245    const A: [f64; 6] = [
246        -7.85951783,
247        1.84408259,
248        -11.7866497,
249        22.6807411,
250        -15.9618719,
251        1.80122502,
252    ];
253
254    // Ensure temperature is positive and reasonable
255    let t_k_safe = t_k.max(173.15); // -100°C minimum
256
257    let tau = 1.0 - t_k_safe / 647.096; // Critical temperature of water
258    let ln_p_ratio = (647.096 / t_k_safe)
259        * (A[0] * tau
260            + A[1] * tau.powf(1.5)
261            + A[2] * tau.powf(3.0)
262            + A[3] * tau.powf(3.5)
263            + A[4] * tau.powf(4.0)
264            + A[5] * tau.powf(7.5));
265
266    220640.0 * ln_p_ratio.exp() // Critical pressure in hPa (22.064 MPa)
267}
268
269/// Enhanced enhancement factor with altitude and temperature dependence.
270#[inline(always)]
271fn enhanced_enhancement_factor(p: f64, t: f64) -> f64 {
272    const ALPHA: f64 = 1.00062;
273    const BETA: f64 = 3.14e-8;
274    const GAMMA: f64 = 5.6e-7;
275    const DELTA: f64 = 1.2e-10; // Additional temperature term
276
277    ALPHA + BETA * p + GAMMA * t * t + DELTA * p * t
278}
279
280/// Enhanced compressibility factor with improved accuracy.
281#[inline(always)]
282fn enhanced_compressibility_factor(p: f64, t_k: f64, x_v: f64) -> f64 {
283    // Enhanced virial coefficients for better accuracy
284    const A0: f64 = 1.58123e-6;
285    const A1: f64 = -2.9331e-8;
286    const A2: f64 = 1.1043e-10;
287    const B0: f64 = 5.707e-6;
288    const B1: f64 = -2.051e-8;
289    const C0: f64 = 1.9898e-4;
290    const C1: f64 = -2.376e-6;
291    const D: f64 = 1.83e-11;
292    const E: f64 = -0.765e-8;
293
294    // Additional third-order terms for enhanced accuracy
295    const F0: f64 = 2.1e-12;
296    const F1: f64 = -1.1e-14;
297
298    // Ensure temperature is positive
299    let t_k_safe = t_k.max(173.15); // -100°C minimum
300    let t = t_k_safe - 273.15;
301    let p_t = p / t_k_safe;
302
303    let z_second_order =
304        1.0 - p_t * (A0 + A1 * t + A2 * t * t + (B0 + B1 * t) * x_v + (C0 + C1 * t) * x_v * x_v);
305
306    let z_third_order = p_t * p_t * (D + E * x_v * x_v);
307
308    // Enhanced fourth-order correction
309    let z_fourth_order = p_t * p_t * p_t * (F0 + F1 * x_v * x_v * x_v);
310
311    z_second_order + z_third_order + z_fourth_order
312}
313
314/// Enhanced local atmospheric calculation with variable lapse rates.
315///
316/// # Arguments
317/// * `altitude_m` - Altitude in meters
318/// * `base_alt` - Base altitude for calculation
319/// * `base_temp_c` - Base temperature in Celsius
320/// * `base_press_hpa` - Base pressure in hPa
321/// * `base_ratio` - Base density ratio
322///
323/// # Returns
324/// Tuple of (air_density_kg_m3, speed_of_sound_mps)
325pub fn get_local_atmosphere(
326    altitude_m: f64,
327    base_alt: f64,
328    base_temp_c: f64,
329    base_press_hpa: f64,
330    base_ratio: f64,
331) -> (f64, f64) {
332    // Round altitude to the nearest meter for caching in Python
333    let altitude_m_rounded = altitude_m.round();
334    let height_diff = altitude_m_rounded - base_alt;
335
336    // Determine appropriate lapse rate based on altitude
337    let lapse_rate = determine_local_lapse_rate(altitude_m_rounded);
338
339    // Calculate temperature with variable lapse rate
340    let temp_c = base_temp_c + lapse_rate * height_diff;
341    let temp_k = temp_c + 273.15;
342    let base_temp_k = base_temp_c + 273.15;
343
344    // Calculate pressure using barometric formula
345    let pressure_hpa = if lapse_rate.abs() < 1e-10 {
346        // Isothermal atmosphere
347        base_press_hpa * (-G_ACCEL_MPS2 * height_diff / (R_AIR * base_temp_k)).exp()
348    } else {
349        // Non-isothermal atmosphere
350        let temp_ratio = temp_k / base_temp_k;
351        base_press_hpa * temp_ratio.powf(-G_ACCEL_MPS2 / (lapse_rate * R_AIR))
352    };
353
354    // Enhanced density calculation
355    let density_ratio = base_ratio * (base_temp_k * pressure_hpa) / (base_press_hpa * temp_k);
356    let density = density_ratio * 1.225;
357
358    // Enhanced speed of sound calculation
359    let speed_of_sound = (temp_k * 401.874).sqrt(); // More precise constant
360
361    (density, speed_of_sound)
362}
363
364/// Determine local lapse rate based on altitude and atmospheric layer.
365#[inline(always)]
366fn determine_local_lapse_rate(altitude_m: f64) -> f64 {
367    // Find the current atmospheric layer to get appropriate lapse rate
368    let layer = ICAO_LAYERS
369        .iter()
370        .rev()
371        .find(|layer| altitude_m >= layer.base_altitude)
372        .unwrap_or(&ICAO_LAYERS[0]);
373
374    layer.lapse_rate
375}
376
377/// Direct atmosphere calculation for simple cases.
378///
379/// # Arguments
380/// * `density` - Pre-computed air density
381/// * `speed_of_sound` - Pre-computed speed of sound
382///
383/// # Returns
384/// Tuple of (air_density, speed_of_sound) - just passes through the values
385#[inline(always)]
386pub fn get_direct_atmosphere(density: f64, speed_of_sound: f64) -> (f64, f64) {
387    (density, speed_of_sound)
388}
389
390/// Legacy function name for backwards compatibility
391pub fn calculate_air_density_cipm(temp_c: f64, pressure_hpa: f64, humidity_percent: f64) -> f64 {
392    calculate_air_density_cimp(temp_c, pressure_hpa, humidity_percent)
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    #[test]
400    fn test_icao_standard_atmosphere() {
401        // Test sea level
402        let (temp, press) = calculate_icao_standard_atmosphere(0.0);
403        assert!((temp - 288.15).abs() < 0.01);
404        assert!((press - 101325.0).abs() < 1.0);
405
406        // Test tropopause
407        let (temp_11km, press_11km) = calculate_icao_standard_atmosphere(11000.0);
408        assert!((temp_11km - 216.65).abs() < 0.01);
409        assert!(press_11km < 101325.0);
410
411        // Test stratosphere
412        let (temp_25km, _) = calculate_icao_standard_atmosphere(25000.0);
413        assert!(temp_25km > 216.65); // Temperature increases in stratosphere
414    }
415
416    #[test]
417    fn test_enhanced_atmosphere_sea_level() {
418        let (density, speed) = calculate_atmosphere(0.0, None, None, 0.0);
419        assert!((density - 1.225).abs() < 0.01);
420        assert!((speed - 340.0).abs() < 1.0);
421    }
422
423    #[test]
424    fn test_enhanced_atmosphere_with_humidity() {
425        let (density_dry, speed_dry) = calculate_atmosphere(0.0, None, None, 0.0);
426        let (density_humid, speed_humid) = calculate_atmosphere(0.0, None, None, 80.0);
427
428        // Humid air should be less dense
429        assert!(density_humid < density_dry);
430        // Humid air should have slightly higher speed of sound
431        assert!(speed_humid > speed_dry);
432    }
433
434    #[test]
435    fn test_enhanced_atmosphere_stratosphere() {
436        // Test in stratosphere where temperature increases
437        let (density_20km, speed_20km) = calculate_atmosphere(20000.0, None, None, 0.0);
438        let (density_30km, speed_30km) = calculate_atmosphere(30000.0, None, None, 0.0);
439
440        // Density should decrease with altitude
441        assert!(density_30km < density_20km);
442        // Speed of sound should increase due to temperature increase
443        assert!(speed_30km > speed_20km);
444    }
445
446    #[test]
447    fn test_enhanced_cimp_density() {
448        let density = calculate_air_density_cimp(15.0, 1013.25, 0.0);
449        assert!((density - 1.225).abs() < 0.01);
450
451        // Test with humidity
452        let density_humid = calculate_air_density_cimp(15.0, 1013.25, 50.0);
453        assert!(density_humid < density);
454    }
455
456    #[test]
457    fn test_variable_lapse_rates() {
458        // Test that lapse rates change appropriately with altitude
459        let lapse_tropo = determine_local_lapse_rate(5000.0);
460        let lapse_strato = determine_local_lapse_rate(25000.0);
461
462        assert!((lapse_tropo - (-0.0065)).abs() < 0.0001);
463        assert!(lapse_strato > 0.0); // Positive lapse rate in stratosphere
464    }
465}