1use crate::{finite_vec, positive, Error, FlightState, ForceModel, Result, Vec3};
11use ballistics_engine::{
12 atmosphere::calculate_air_density_cimp, constants::GRAINS_TO_KG,
13 derivatives::compute_derivatives, drag::DragTable, BallisticInputs,
14};
15
16#[derive(Debug, Clone, Copy)]
18pub enum Atmosphere {
19 Standard {
22 station_altitude_m: f64,
24 temperature_c: f64,
26 pressure_hpa: f64,
28 humidity_percent: f64,
30 },
31 Uniform {
34 density_kg_m3: f64,
36 speed_of_sound_mps: f64,
38 },
39}
40
41impl Default for Atmosphere {
42 fn default() -> Self {
43 Self::Standard {
44 station_altitude_m: 0.0,
45 temperature_c: 15.0,
46 pressure_hpa: 1013.25,
47 humidity_percent: 0.0,
48 }
49 }
50}
51
52impl Atmosphere {
53 fn parameters(self) -> Result<(f64, f64, f64, f64)> {
54 match self {
55 Self::Standard {
56 station_altitude_m,
57 temperature_c,
58 pressure_hpa,
59 humidity_percent,
60 } => {
61 if !station_altitude_m.is_finite()
62 || !temperature_c.is_finite()
63 || temperature_c <= -273.15
64 || !humidity_percent.is_finite()
65 || !(0.0..=100.0).contains(&humidity_percent)
66 {
67 return Err(Error::invalid("standard atmosphere", "requires finite altitude, temperature above absolute zero and humidity in [0, 100]"));
68 }
69 positive(pressure_hpa, "pressure_hpa")?;
70 let ratio =
71 calculate_air_density_cimp(temperature_c, pressure_hpa, humidity_percent)
72 / 1.225;
73 positive(ratio, "station density ratio")?;
74 Ok((station_altitude_m, temperature_c, pressure_hpa, ratio))
75 }
76 Self::Uniform {
77 density_kg_m3,
78 speed_of_sound_mps,
79 } => {
80 if !density_kg_m3.is_finite()
81 || !(0.0..2.0).contains(&density_kg_m3)
82 || !speed_of_sound_mps.is_finite()
83 || speed_of_sound_mps <= 200.0
84 {
85 return Err(Error::invalid(
86 "uniform atmosphere",
87 "upstream requires density in [0, 2) and sound speed > 200 m/s",
88 ));
89 }
90 Ok((density_kg_m3, speed_of_sound_mps, 0.0, 0.0))
91 }
92 }
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct BallisticForces {
99 inputs: BallisticInputs,
100 atmos_params: (f64, f64, f64, f64),
101 wind_mps: Vec3,
102 omega_rps: Option<Vec3>,
103}
104
105const OMEGA_EARTH_RPS: f64 = 7.2921159e-5;
107
108impl BallisticForces {
109 pub fn new(
113 diameter_m: f64,
114 cd_vs_mach: Vec<(f64, f64)>,
115 atmosphere: Atmosphere,
116 wind_mps: Vec3,
117 ) -> Result<Self> {
118 positive(diameter_m, "diameter_m")?;
119 let diameter_in = diameter_m / 0.0254;
120 positive(diameter_in * diameter_in, "diameter area")?;
121 if !finite_vec(&wind_mps) {
122 return Err(Error::invalid("wind_mps", "must be finite"));
123 }
124 let (mach, cd) = cd_vs_mach.into_iter().unzip();
125 let drag = DragTable::try_new(mach, cd).map_err(Error::Model)?;
126 let inputs = BallisticInputs {
127 bullet_diameter: diameter_m,
128 caliber_inches: diameter_in,
129 bullet_length: diameter_m,
130 custom_drag_table: Some(drag),
131 custom_drag_reference: false,
132 altitude: 0.0,
133 shooting_angle: 0.0,
134 enable_advanced_effects: false,
135 enable_magnus: false,
136 enable_coriolis: false,
137 use_enhanced_spin_drift: false,
138 tipoff_yaw: 0.0,
139 ..BallisticInputs::default()
140 };
141 Ok(Self {
142 inputs,
143 atmos_params: atmosphere.parameters()?,
144 wind_mps,
145 omega_rps: None,
146 })
147 }
148
149 pub fn with_drag_scale(mut self, scale: f64) -> Result<Self> {
151 positive(scale, "drag scale")?;
152 if self
153 .inputs
154 .custom_drag_table
155 .as_ref()
156 .unwrap()
157 .cd_values
158 .iter()
159 .any(|cd| !(cd * scale).is_finite())
160 {
161 return Err(Error::invalid("drag scale", "scaled Cd must remain finite"));
162 }
163 self.inputs.cd_scale = scale;
164 Ok(self)
165 }
166
167 pub fn set_wind(&mut self, wind_mps: Vec3) -> Result<()> {
169 if !finite_vec(&wind_mps) {
170 return Err(Error::invalid("wind_mps", "must be finite"));
171 }
172 self.wind_mps = wind_mps;
173 Ok(())
174 }
175
176 pub fn with_spin(
186 mut self,
187 twist_rate_in: f64,
188 body_length_m: f64,
189 right_hand: bool,
190 ) -> Result<Self> {
191 positive(twist_rate_in, "twist_rate_in")?;
192 positive(body_length_m, "body_length_m")?;
193 self.inputs.twist_rate = twist_rate_in;
194 self.inputs.is_twist_right = right_hand;
195 self.inputs.bullet_length = body_length_m;
196 self.inputs.enable_magnus = true;
197 Ok(self)
198 }
199
200 pub fn with_coriolis(mut self, latitude_deg: f64, shot_azimuth_deg: f64) -> Result<Self> {
206 if !latitude_deg.is_finite() || !(-90.0..=90.0).contains(&latitude_deg) {
207 return Err(Error::invalid(
208 "latitude_deg",
209 "must be finite and in [-90, 90]",
210 ));
211 }
212 if !shot_azimuth_deg.is_finite() {
213 return Err(Error::invalid("shot_azimuth_deg", "must be finite"));
214 }
215 let lat = latitude_deg.to_radians();
216 let az = shot_azimuth_deg.to_radians();
217 self.omega_rps = Some(Vec3::new(
218 OMEGA_EARTH_RPS * lat.cos() * az.cos(), OMEGA_EARTH_RPS * lat.sin(), -OMEGA_EARTH_RPS * lat.cos() * az.sin(), ));
222 self.inputs.latitude = Some(latitude_deg);
223 self.inputs.shot_azimuth = az;
224 self.inputs.enable_coriolis = true;
225 Ok(self)
226 }
227
228 pub fn with_tipoff_yaw(mut self, yaw_rad: f64, decay_distance_m: f64) -> Result<Self> {
232 if !yaw_rad.is_finite() {
233 return Err(Error::invalid("tipoff yaw_rad", "must be finite"));
234 }
235 positive(decay_distance_m, "tipoff decay_distance_m")?;
236 self.inputs.tipoff_yaw = yaw_rad;
237 self.inputs.tipoff_decay_distance = decay_distance_m;
238 Ok(self)
239 }
240
241 pub fn acceleration_in_wind(
243 &self,
244 state: &FlightState,
245 mass_kg: f64,
246 wind_mps: Vec3,
247 ) -> Result<Vec3> {
248 state.validate()?;
249 positive(mass_kg, "mass_kg")?;
250 if !finite_vec(&wind_mps) {
251 return Err(Error::invalid("wind_mps", "must be finite"));
252 }
253 let mut inputs = self.inputs.clone();
254 inputs.bullet_mass = mass_kg;
255 inputs.weight_grains = mass_kg / GRAINS_TO_KG;
256 positive(inputs.weight_grains, "mass in grains")?;
257 let derivatives = compute_derivatives(
258 state.position_m,
259 state.velocity_mps,
260 &inputs,
261 wind_mps,
262 self.atmos_params,
263 inputs.bc_value,
264 self.omega_rps,
265 state.time_s,
266 None,
267 );
268 let acceleration = Vec3::new(derivatives[3], derivatives[4], derivatives[5]);
269 if !finite_vec(&acceleration) {
270 return Err(Error::NumericalFailure(
271 "ballistics force model returned non-finite acceleration",
272 ));
273 }
274 Ok(acceleration)
275 }
276}
277
278impl ForceModel for BallisticForces {
279 fn acceleration(&self, state: &FlightState, mass_kg: f64) -> Result<Vec3> {
280 self.acceleration_in_wind(state, mass_kg, self.wind_mps)
281 }
282}