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 };
118
119 let solution = fast_integrate(inputs, &wind_sock, params);
121
122 if solution.t.is_empty() {
123 return Err("Empty trajectory solution".to_string());
124 }
125
126 let final_idx = solution.t.len() - 1;
129
130 let final_downrange = solution.y[0][final_idx]; if final_downrange < target_distance_m * 0.999 {
136 return Err("trajectory did not reach target distance".to_string());
137 }
138
139 let final_y = solution.y[1][final_idx]; let final_lateral = solution.y[2][final_idx]; let final_vx = solution.y[3][final_idx];
143 let final_vy = solution.y[4][final_idx];
144 let final_vz = solution.y[5][final_idx];
145
146 let final_speed = (final_vx * final_vx + final_vy * final_vy + final_vz * final_vz).sqrt();
147 let final_mach = final_speed / speed_of_sound;
148 let final_energy = 0.5 * mass_kg * final_speed * final_speed;
149
150 let sight_height_m = inputs.sight_height; let los_y = sight_height_m + (0.0 - sight_height_m) * (final_downrange / target_distance_m);
153 let drop = los_y - final_y;
154
155 Ok(TrajectoryOutput {
156 drop,
157 wind_drift: final_lateral,
158 time: solution.t[final_idx],
159 velocity: final_speed,
160 energy: final_energy,
161 mach: final_mach,
162 spin_drift: final_lateral, distance: final_downrange,
164 })
165}
166
167pub fn calculate_cep(wind_drift_values: &[f64], drop_values: &[f64]) -> f64 {
172 if wind_drift_values.len() != drop_values.len() || wind_drift_values.is_empty() {
173 return 0.0;
174 }
175
176 let mean_x = wind_drift_values.iter().sum::<f64>() / wind_drift_values.len() as f64;
178 let mean_y = drop_values.iter().sum::<f64>() / drop_values.len() as f64;
179
180 let mut distances: Vec<f64> = wind_drift_values
182 .iter()
183 .zip(drop_values.iter())
184 .map(|(x, y)| {
185 let dx = x - mean_x;
186 let dy = y - mean_y;
187 (dx * dx + dy * dy).sqrt()
188 })
189 .collect();
190
191 distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
193
194 percentile(&distances, 0.50)
196}
197
198pub fn calculate_confidence_ellipse(
202 wind_drift_values: &[f64],
203 drop_values: &[f64],
204) -> (f64, f64, f64, f64, f64) {
205 if wind_drift_values.len() != drop_values.len() || wind_drift_values.len() < 2 {
206 return (0.0, 0.0, 0.0, 0.0, 0.0);
207 }
208
209 let n = wind_drift_values.len() as f64;
210
211 let mean_x = wind_drift_values.iter().sum::<f64>() / n;
213 let mean_y = drop_values.iter().sum::<f64>() / n;
214
215 let mut cov_xx = 0.0;
217 let mut cov_yy = 0.0;
218 let mut cov_xy = 0.0;
219
220 for (x, y) in wind_drift_values.iter().zip(drop_values.iter()) {
221 let dx = x - mean_x;
222 let dy = y - mean_y;
223 cov_xx += dx * dx;
224 cov_yy += dy * dy;
225 cov_xy += dx * dy;
226 }
227
228 cov_xx /= n - 1.0;
229 cov_yy /= n - 1.0;
230 cov_xy /= n - 1.0;
231
232 let trace = cov_xx + cov_yy;
235 let det = cov_xx * cov_yy - cov_xy * cov_xy;
236 let discriminant = (trace * trace / 4.0 - det).max(0.0).sqrt();
237
238 let lambda1 = trace / 2.0 + discriminant; let lambda2 = trace / 2.0 - discriminant; let scale_factor = 5.991_f64.sqrt();
243 let semi_major = lambda1.max(0.0).sqrt() * scale_factor;
244 let semi_minor = lambda2.max(0.0).sqrt() * scale_factor;
245
246 let rotation_rad = if cov_xy.abs() < 1e-10 {
248 if cov_xx >= cov_yy {
249 0.0
250 } else {
251 std::f64::consts::PI / 2.0
252 }
253 } else {
254 ((lambda1 - cov_xx) / cov_xy).atan()
255 };
256
257 let rotation_deg = rotation_rad.to_degrees();
258
259 (mean_x, mean_y, semi_major, semi_minor, rotation_deg)
260}
261
262pub fn sample_points_for_visualization(
264 wind_drift_values: &[f64],
265 drop_values: &[f64],
266 max_points: usize,
267) -> Vec<(f64, f64)> {
268 let n = wind_drift_values.len();
269 if n == 0 {
270 return Vec::new();
271 }
272
273 if n <= max_points {
274 wind_drift_values
276 .iter()
277 .zip(drop_values.iter())
278 .map(|(x, y)| (*x, *y))
279 .collect()
280 } else {
281 let step = n as f64 / max_points as f64;
283 (0..max_points)
284 .map(|i| {
285 let idx = (i as f64 * step) as usize;
286 (wind_drift_values[idx], drop_values[idx])
287 })
288 .collect()
289 }
290}
291
292pub fn percentile(sorted_values: &[f64], p: f64) -> f64 {
294 if sorted_values.is_empty() {
295 return 0.0;
296 }
297
298 if sorted_values.len() == 1 {
299 return sorted_values[0];
300 }
301
302 let p = p.clamp(0.0, 1.0);
305 let rank = p * (sorted_values.len() - 1) as f64;
306 let lower_idx = rank.floor() as usize;
307 let upper_idx = rank.ceil() as usize;
308 let fraction = rank - lower_idx as f64;
309
310 if lower_idx == upper_idx {
311 sorted_values[lower_idx]
312 } else {
313 sorted_values[lower_idx] * (1.0 - fraction) + sorted_values[upper_idx] * fraction
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_calculate_cep() {
323 let wind_drift = vec![0.0, 1.0, -1.0, 0.5, -0.5];
324 let drop = vec![0.0, 0.5, -0.5, 1.0, -1.0];
325
326 let cep = calculate_cep(&wind_drift, &drop);
327 assert!(cep > 0.0);
328 assert!(cep < 2.0); }
330
331 #[test]
332 fn test_calculate_confidence_ellipse() {
333 let wind_drift = vec![0.0, 1.0, -1.0, 0.5, -0.5];
334 let drop = vec![0.0, 0.5, -0.5, 1.0, -1.0];
335
336 let (cx, cy, major, minor, _rotation) = calculate_confidence_ellipse(&wind_drift, &drop);
337
338 assert!(cx.abs() < 0.5);
340 assert!(cy.abs() < 0.5);
341
342 assert!(major > 0.0);
344 assert!(minor > 0.0);
345 assert!(major >= minor); }
347
348 #[test]
349 fn test_sample_points() {
350 let wind_drift = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
351 let drop = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
352
353 let sampled = sample_points_for_visualization(&wind_drift, &drop, 3);
354 assert_eq!(sampled.len(), 3);
355 }
356
357 #[test]
358 fn test_percentile() {
359 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
360
361 assert_eq!(percentile(&values, 0.0), 1.0);
362 assert_eq!(percentile(&values, 0.5), 3.0);
363 assert_eq!(percentile(&values, 1.0), 5.0);
364 }
365}