1use crate::atmosphere::calculate_atmosphere;
9use crate::fast_trajectory::{fast_integrate, FastIntegrationParams};
10use crate::wind::WindSock;
11use crate::BallisticInputs;
12use nalgebra::Vector3;
13
14#[derive(Debug, Clone)]
16pub struct TrajectoryOutput {
17 pub drop: f64, pub wind_drift: f64, pub time: f64, pub velocity: f64, pub energy: f64, pub mach: f64, pub spin_drift: f64, pub distance: f64, }
26
27pub fn solve_trajectory_for_monte_carlo(
32 inputs: &BallisticInputs,
33) -> Result<TrajectoryOutput, String> {
34 let target_distance_m = inputs.target_distance; let muzzle_velocity_mps = inputs.muzzle_velocity; let mass_kg = inputs.bullet_mass; if !(target_distance_m.is_finite() && target_distance_m > 0.0) {
45 return Err("target_distance must be a positive, finite distance".to_string());
46 }
47
48 let (resolved_temp_c, resolved_pressure_hpa) = crate::atmosphere::resolve_station_conditions(
54 inputs.temperature,
55 inputs.pressure,
56 inputs.altitude,
57 );
58 let (air_density, speed_of_sound) = calculate_atmosphere(
59 inputs.altitude, Some(resolved_temp_c),
61 Some(resolved_pressure_hpa),
62 inputs.humidity_percent(),
66 );
67
68 let wind_segments = vec![(
71 inputs.wind_speed * 3.6, inputs.wind_angle.to_degrees(), target_distance_m * 2.0, )];
75 let wind_sock = WindSock::new(wind_segments);
76
77 let muzzle_angle_rad = inputs.muzzle_angle;
79 let initial_velocity = Vector3::new(
81 muzzle_velocity_mps * muzzle_angle_rad.cos(),
82 muzzle_velocity_mps * muzzle_angle_rad.sin(),
83 0.0,
84 );
85
86 let initial_position = Vector3::new(0.0, inputs.sight_height, 0.0); let mut initial_state_array = [0.0; 6];
88 initial_state_array[0..3].copy_from_slice(&[
89 initial_position.x,
90 initial_position.y,
91 initial_position.z,
92 ]);
93 initial_state_array[3..6].copy_from_slice(&[
94 initial_velocity.x,
95 initial_velocity.y,
96 initial_velocity.z,
97 ]);
98
99 let base_ratio = air_density / 1.225;
106 let params = FastIntegrationParams {
107 initial_state: initial_state_array,
108 t_span: (0.0, 30.0),
109 horiz: target_distance_m,
110 vert: 0.0, atmo_params: (
112 inputs.altitude,
113 resolved_temp_c,
114 resolved_pressure_hpa,
115 base_ratio,
116 ),
117 atmo_sock: None,
121 };
122
123 let solution = fast_integrate(inputs, &wind_sock, params);
125
126 if solution.t.is_empty() {
127 return Err("Empty trajectory solution".to_string());
128 }
129
130 let final_idx = solution.t.len() - 1;
133
134 let final_downrange = solution.y[0][final_idx]; if final_downrange < target_distance_m * 0.999 {
140 return Err("trajectory did not reach target distance".to_string());
141 }
142
143 let final_y = solution.y[1][final_idx]; let final_lateral = solution.y[2][final_idx]; let final_vx = solution.y[3][final_idx];
147 let final_vy = solution.y[4][final_idx];
148 let final_vz = solution.y[5][final_idx];
149
150 let final_speed = (final_vx * final_vx + final_vy * final_vy + final_vz * final_vz).sqrt();
151 let final_mach = final_speed / speed_of_sound;
152 let final_energy = 0.5 * mass_kg * final_speed * final_speed;
153
154 let sight_height_m = inputs.sight_height; let los_y = sight_height_m + (0.0 - sight_height_m) * (final_downrange / target_distance_m);
157 let drop = los_y - final_y;
158
159 let spin_drift_m = if inputs.use_enhanced_spin_drift {
166 let sg = crate::spin_drift::effective_sg_from_inputs(
167 inputs,
168 resolved_temp_c,
169 resolved_pressure_hpa,
170 );
171 crate::spin_drift::litz_drift_meters(sg, solution.t[final_idx], inputs.is_twist_right)
172 } else {
173 0.0
174 };
175
176 Ok(TrajectoryOutput {
177 drop,
178 wind_drift: final_lateral,
180 time: solution.t[final_idx],
181 velocity: final_speed,
182 energy: final_energy,
183 mach: final_mach,
184 spin_drift: spin_drift_m,
185 distance: final_downrange,
186 })
187}
188
189pub fn calculate_cep(wind_drift_values: &[f64], drop_values: &[f64]) -> f64 {
194 if wind_drift_values.len() != drop_values.len() || wind_drift_values.is_empty() {
195 return 0.0;
196 }
197
198 let mean_x = wind_drift_values.iter().sum::<f64>() / wind_drift_values.len() as f64;
200 let mean_y = drop_values.iter().sum::<f64>() / drop_values.len() as f64;
201
202 let mut distances: Vec<f64> = wind_drift_values
204 .iter()
205 .zip(drop_values.iter())
206 .map(|(x, y)| {
207 let dx = x - mean_x;
208 let dy = y - mean_y;
209 (dx * dx + dy * dy).sqrt()
210 })
211 .collect();
212
213 distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
215
216 percentile(&distances, 0.50)
218}
219
220pub fn calculate_confidence_ellipse(
224 wind_drift_values: &[f64],
225 drop_values: &[f64],
226) -> (f64, f64, f64, f64, f64) {
227 if wind_drift_values.len() != drop_values.len() || wind_drift_values.len() < 2 {
228 return (0.0, 0.0, 0.0, 0.0, 0.0);
229 }
230
231 let n = wind_drift_values.len() as f64;
232
233 let mean_x = wind_drift_values.iter().sum::<f64>() / n;
235 let mean_y = drop_values.iter().sum::<f64>() / n;
236
237 let mut cov_xx = 0.0;
239 let mut cov_yy = 0.0;
240 let mut cov_xy = 0.0;
241
242 for (x, y) in wind_drift_values.iter().zip(drop_values.iter()) {
243 let dx = x - mean_x;
244 let dy = y - mean_y;
245 cov_xx += dx * dx;
246 cov_yy += dy * dy;
247 cov_xy += dx * dy;
248 }
249
250 cov_xx /= n - 1.0;
251 cov_yy /= n - 1.0;
252 cov_xy /= n - 1.0;
253
254 let trace = cov_xx + cov_yy;
257 let det = cov_xx * cov_yy - cov_xy * cov_xy;
258 let discriminant = (trace * trace / 4.0 - det).max(0.0).sqrt();
259
260 let lambda1 = trace / 2.0 + discriminant; let lambda2 = trace / 2.0 - discriminant; let scale_factor = 5.991_f64.sqrt();
265 let semi_major = lambda1.max(0.0).sqrt() * scale_factor;
266 let semi_minor = lambda2.max(0.0).sqrt() * scale_factor;
267
268 let rotation_rad = if cov_xy.abs() < 1e-10 {
270 if cov_xx >= cov_yy {
271 0.0
272 } else {
273 std::f64::consts::PI / 2.0
274 }
275 } else {
276 ((lambda1 - cov_xx) / cov_xy).atan()
277 };
278
279 let rotation_deg = rotation_rad.to_degrees();
280
281 (mean_x, mean_y, semi_major, semi_minor, rotation_deg)
282}
283
284pub fn sample_points_for_visualization(
286 wind_drift_values: &[f64],
287 drop_values: &[f64],
288 max_points: usize,
289) -> Vec<(f64, f64)> {
290 let n = wind_drift_values.len();
291 if n == 0 {
292 return Vec::new();
293 }
294
295 if n <= max_points {
296 wind_drift_values
298 .iter()
299 .zip(drop_values.iter())
300 .map(|(x, y)| (*x, *y))
301 .collect()
302 } else {
303 let step = n as f64 / max_points as f64;
305 (0..max_points)
306 .map(|i| {
307 let idx = (i as f64 * step) as usize;
308 (wind_drift_values[idx], drop_values[idx])
309 })
310 .collect()
311 }
312}
313
314pub fn percentile(sorted_values: &[f64], p: f64) -> f64 {
316 if sorted_values.is_empty() {
317 return 0.0;
318 }
319
320 if sorted_values.len() == 1 {
321 return sorted_values[0];
322 }
323
324 let p = p.clamp(0.0, 1.0);
327 let rank = p * (sorted_values.len() - 1) as f64;
328 let lower_idx = rank.floor() as usize;
329 let upper_idx = rank.ceil() as usize;
330 let fraction = rank - lower_idx as f64;
331
332 if lower_idx == upper_idx {
333 sorted_values[lower_idx]
334 } else {
335 sorted_values[lower_idx] * (1.0 - fraction) + sorted_values[upper_idx] * fraction
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn test_calculate_cep() {
345 let wind_drift = vec![0.0, 1.0, -1.0, 0.5, -0.5];
346 let drop = vec![0.0, 0.5, -0.5, 1.0, -1.0];
347
348 let cep = calculate_cep(&wind_drift, &drop);
349 assert!(cep > 0.0);
350 assert!(cep < 2.0); }
352
353 #[test]
354 fn test_calculate_confidence_ellipse() {
355 let wind_drift = vec![0.0, 1.0, -1.0, 0.5, -0.5];
356 let drop = vec![0.0, 0.5, -0.5, 1.0, -1.0];
357
358 let (cx, cy, major, minor, _rotation) = calculate_confidence_ellipse(&wind_drift, &drop);
359
360 assert!(cx.abs() < 0.5);
362 assert!(cy.abs() < 0.5);
363
364 assert!(major > 0.0);
366 assert!(minor > 0.0);
367 assert!(major >= minor); }
369
370 #[test]
371 fn test_sample_points() {
372 let wind_drift = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
373 let drop = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
374
375 let sampled = sample_points_for_visualization(&wind_drift, &drop, 3);
376 assert_eq!(sampled.len(), 3);
377 }
378
379 #[test]
380 fn test_percentile() {
381 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
382
383 assert_eq!(percentile(&values, 0.0), 1.0);
384 assert_eq!(percentile(&values, 0.5), 3.0);
385 assert_eq!(percentile(&values, 1.0), 5.0);
386 }
387}