1use 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#[derive(Debug, Clone, Copy)]
16pub enum Atmosphere {
17 Standard {
20 station_altitude_m: f64,
22 temperature_c: f64,
24 pressure_hpa: f64,
26 humidity_percent: f64,
28 },
29 Uniform {
32 density_kg_m3: f64,
34 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#[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 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 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 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 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}