Skip to main content

rockets_engine/
ballistics.rs

1//! Adapter to ballistics-engine 0.48's public derivative kernel.
2//!
3//! Uses the body's own Cd-versus-Mach curve, fixed diameter, standard gravity and
4//! a level world frame. SI mass and its imperial mirror are updated at every stage,
5//! so aerodynamic deceleration responds to fuel consumption. Bullet-specific spin,
6//! Magnus, yaw, reference-BC and postprocessing effects are intentionally disabled.
7
8use crate::{finite_vec, positive, Error, FlightState, ForceModel, Result, Vec3};
9use ballistics_engine::{
10    atmosphere::calculate_air_density_cimp, constants::GRAINS_TO_KG,
11    derivatives::compute_derivatives, drag::DragTable, BallisticInputs,
12};
13
14/// Atmospheric inputs supported by the upstream derivative kernel.
15#[derive(Debug, Clone, Copy)]
16pub enum Atmosphere {
17    /// Station measurements; the upstream model applies its local altitude lapse.
18    /// Humidity affects station density; this derivative path uses dry-air sound speed.
19    Standard {
20        /// Elevation of the weather station above sea level, in metres.
21        station_altitude_m: f64,
22        /// Station temperature in degrees Celsius (greater than absolute zero).
23        temperature_c: f64,
24        /// Positive station pressure in hectopascals.
25        pressure_hpa: f64,
26        /// Relative humidity as a percentage in [0, 100].
27        humidity_percent: f64,
28    },
29    /// Constant density and sound speed, independent of position.
30    /// Upstream's direct-mode sentinel requires 0 <= density < 2 and sound speed > 200.
31    Uniform {
32        /// Air density in kilograms per cubic metre; zero disables drag.
33        density_kg_m3: f64,
34        /// Sound speed in metres per second.
35        speed_of_sound_mps: f64,
36    },
37}
38
39impl Default for Atmosphere {
40    fn default() -> Self {
41        Self::Standard {
42            station_altitude_m: 0.0,
43            temperature_c: 15.0,
44            pressure_hpa: 1013.25,
45            humidity_percent: 0.0,
46        }
47    }
48}
49
50impl Atmosphere {
51    fn parameters(self) -> Result<(f64, f64, f64, f64)> {
52        match self {
53            Self::Standard {
54                station_altitude_m,
55                temperature_c,
56                pressure_hpa,
57                humidity_percent,
58            } => {
59                if !station_altitude_m.is_finite()
60                    || !temperature_c.is_finite()
61                    || temperature_c <= -273.15
62                    || !humidity_percent.is_finite()
63                    || !(0.0..=100.0).contains(&humidity_percent)
64                {
65                    return Err(Error::invalid("standard atmosphere", "requires finite altitude, temperature above absolute zero and humidity in [0, 100]"));
66                }
67                positive(pressure_hpa, "pressure_hpa")?;
68                let ratio =
69                    calculate_air_density_cimp(temperature_c, pressure_hpa, humidity_percent)
70                        / 1.225;
71                positive(ratio, "station density ratio")?;
72                Ok((station_altitude_m, temperature_c, pressure_hpa, ratio))
73            }
74            Self::Uniform {
75                density_kg_m3,
76                speed_of_sound_mps,
77            } => {
78                if !density_kg_m3.is_finite()
79                    || !(0.0..2.0).contains(&density_kg_m3)
80                    || !speed_of_sound_mps.is_finite()
81                    || speed_of_sound_mps <= 200.0
82                {
83                    return Err(Error::invalid(
84                        "uniform atmosphere",
85                        "upstream requires density in [0, 2) and sound speed > 200 m/s",
86                    ));
87                }
88                Ok((density_kg_m3, speed_of_sound_mps, 0.0, 0.0))
89            }
90        }
91    }
92}
93
94/// Gravity and aerodynamic drag delegated to ballistics-engine's force kernel.
95#[derive(Debug, Clone)]
96pub struct BallisticForces {
97    inputs: BallisticInputs,
98    atmos_params: (f64, f64, f64, f64),
99    wind_mps: Vec3,
100}
101
102impl BallisticForces {
103    /// Build using the vehicle's actual Cd curve, not a G-model reference curve or BC.
104    /// At least two ascending, nonnegative Mach values and positive Cd values are required.
105    /// Wind is a world-frame air velocity vector (positive X is a tailwind).
106    pub fn new(
107        diameter_m: f64,
108        cd_vs_mach: Vec<(f64, f64)>,
109        atmosphere: Atmosphere,
110        wind_mps: Vec3,
111    ) -> Result<Self> {
112        positive(diameter_m, "diameter_m")?;
113        let diameter_in = diameter_m / 0.0254;
114        positive(diameter_in * diameter_in, "diameter area")?;
115        if !finite_vec(&wind_mps) {
116            return Err(Error::invalid("wind_mps", "must be finite"));
117        }
118        let (mach, cd) = cd_vs_mach.into_iter().unzip();
119        let drag = DragTable::try_new(mach, cd).map_err(Error::Model)?;
120        let inputs = BallisticInputs {
121            bullet_diameter: diameter_m,
122            caliber_inches: diameter_in,
123            bullet_length: diameter_m,
124            custom_drag_table: Some(drag),
125            custom_drag_reference: false,
126            altitude: 0.0,
127            shooting_angle: 0.0,
128            enable_advanced_effects: false,
129            enable_magnus: false,
130            enable_coriolis: false,
131            use_enhanced_spin_drift: false,
132            tipoff_yaw: 0.0,
133            ..BallisticInputs::default()
134        };
135        Ok(Self {
136            inputs,
137            atmos_params: atmosphere.parameters()?,
138            wind_mps,
139        })
140    }
141
142    /// Apply a positive multiplier to the entire Cd curve. Default is 1.0.
143    pub fn with_drag_scale(mut self, scale: f64) -> Result<Self> {
144        positive(scale, "drag scale")?;
145        if self
146            .inputs
147            .custom_drag_table
148            .as_ref()
149            .unwrap()
150            .cd_values
151            .iter()
152            .any(|cd| !(cd * scale).is_finite())
153        {
154            return Err(Error::invalid("drag scale", "scaled Cd must remain finite"));
155        }
156        self.inputs.cd_scale = scale;
157        Ok(self)
158    }
159
160    /// Change uniform world-frame wind between public integration steps.
161    pub fn set_wind(&mut self, wind_mps: Vec3) -> Result<()> {
162        if !finite_vec(&wind_mps) {
163            return Err(Error::invalid("wind_mps", "must be finite"));
164        }
165        self.wind_mps = wind_mps;
166        Ok(())
167    }
168
169    /// Evaluate using externally sampled wind, for a spatial or time-varying force wrapper.
170    pub fn acceleration_in_wind(
171        &self,
172        state: &FlightState,
173        mass_kg: f64,
174        wind_mps: Vec3,
175    ) -> Result<Vec3> {
176        state.validate()?;
177        positive(mass_kg, "mass_kg")?;
178        if !finite_vec(&wind_mps) {
179            return Err(Error::invalid("wind_mps", "must be finite"));
180        }
181        let mut inputs = self.inputs.clone();
182        inputs.bullet_mass = mass_kg;
183        inputs.weight_grains = mass_kg / GRAINS_TO_KG;
184        positive(inputs.weight_grains, "mass in grains")?;
185        let derivatives = compute_derivatives(
186            state.position_m,
187            state.velocity_mps,
188            &inputs,
189            wind_mps,
190            self.atmos_params,
191            inputs.bc_value,
192            None,
193            state.time_s,
194            None,
195        );
196        let acceleration = Vec3::new(derivatives[3], derivatives[4], derivatives[5]);
197        if !finite_vec(&acceleration) {
198            return Err(Error::NumericalFailure(
199                "ballistics force model returned non-finite acceleration",
200            ));
201        }
202        Ok(acceleration)
203    }
204}
205
206impl ForceModel for BallisticForces {
207    fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3> {
208        self.acceleration_in_wind(state, mass_kg, self.wind_mps)
209    }
210}