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,
19 pub wind_drift: f64, pub time: f64, pub velocity: f64, pub energy: f64, pub mach: f64, pub spin_drift: f64, pub distance: f64, }
27
28pub fn solve_trajectory_for_monte_carlo(
37 inputs: &BallisticInputs,
38) -> Result<TrajectoryOutput, String> {
39 solve_trajectory_for_monte_carlo_with_station_presence(
40 inputs,
41 crate::atmosphere::StationAtmospherePresence::NEITHER,
42 )
43}
44
45pub fn solve_trajectory_for_monte_carlo_with_station_presence(
49 inputs: &BallisticInputs,
50 station_presence: crate::atmosphere::StationAtmospherePresence,
51) -> Result<TrajectoryOutput, String> {
52 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) {
62 return Err("target_distance must be a positive, finite distance".to_string());
63 }
64
65 let (resolved_temp_c, resolved_pressure_hpa) =
71 crate::atmosphere::resolve_station_conditions_with_presence(
72 inputs.temperature,
73 inputs.pressure,
74 inputs.altitude,
75 crate::atmosphere::PressureReferenceMode::Absolute,
76 station_presence,
77 );
78 let (air_density, speed_of_sound) = calculate_atmosphere(
79 inputs.altitude, Some(resolved_temp_c),
81 Some(resolved_pressure_hpa),
82 inputs.humidity_percent(),
86 );
87
88 let wind_segments = vec![crate::wind::WindSegment::new(
91 inputs.wind_speed * 3.6, inputs.wind_angle.to_degrees(), target_distance_m * 2.0, )];
95 let wind_sock = WindSock::new(wind_segments);
96
97 let muzzle_angle_rad = inputs.muzzle_angle;
99 let initial_velocity = Vector3::new(
101 muzzle_velocity_mps * muzzle_angle_rad.cos(),
102 muzzle_velocity_mps * muzzle_angle_rad.sin(),
103 0.0,
104 );
105
106 let bore_y = inputs.muzzle_height;
109 let line_of_sight_y = bore_y + inputs.sight_height;
110 let initial_position = Vector3::new(0.0, bore_y, 0.0); let mut initial_state_array = [0.0; 6];
112 initial_state_array[0..3].copy_from_slice(&[
113 initial_position.x,
114 initial_position.y,
115 initial_position.z,
116 ]);
117 initial_state_array[3..6].copy_from_slice(&[
118 initial_velocity.x,
119 initial_velocity.y,
120 initial_velocity.z,
121 ]);
122
123 let base_ratio = air_density / 1.225;
130 let params = FastIntegrationParams {
131 initial_state: initial_state_array,
132 t_span: (0.0, 30.0),
133 horiz: target_distance_m,
134 vert: line_of_sight_y,
135 atmo_params: (
136 inputs.altitude,
137 resolved_temp_c,
138 resolved_pressure_hpa,
139 base_ratio,
140 ),
141 atmo_sock: None,
145 };
146
147 let solution = fast_integrate(inputs, &wind_sock, params);
149
150 if solution.t.is_empty() {
151 return Err("Empty trajectory solution".to_string());
152 }
153
154 let final_idx = solution.t.len() - 1;
157
158 let final_downrange = solution.y[0][final_idx]; if final_downrange < target_distance_m * 0.999 {
164 return Err("trajectory did not reach target distance".to_string());
165 }
166
167 let final_y = solution.y[1][final_idx]; let final_lateral = solution.y[2][final_idx]; let final_vx = solution.y[3][final_idx];
171 let final_vy = solution.y[4][final_idx];
172 let final_vz = solution.y[5][final_idx];
173
174 let final_speed = (final_vx * final_vx + final_vy * final_vy + final_vz * final_vz).sqrt();
175 let final_mach = final_speed / speed_of_sound;
176 let final_energy = 0.5 * mass_kg * final_speed * final_speed;
177
178 let drop = line_of_sight_y - final_y;
181
182 let spin_drift_m = if inputs.use_enhanced_spin_drift {
189 let sg = crate::spin_drift::effective_sg_from_inputs(
190 inputs,
191 resolved_temp_c,
192 resolved_pressure_hpa,
193 );
194 crate::spin_drift::litz_drift_meters(sg, solution.t[final_idx], inputs.is_twist_right)
195 } else {
196 0.0
197 };
198
199 Ok(TrajectoryOutput {
200 drop,
201 wind_drift: final_lateral,
203 time: solution.t[final_idx],
204 velocity: final_speed,
205 energy: final_energy,
206 mach: final_mach,
207 spin_drift: spin_drift_m,
208 distance: final_downrange,
209 })
210}
211
212pub fn calculate_cep(wind_drift_values: &[f64], drop_values: &[f64]) -> f64 {
217 if wind_drift_values.len() != drop_values.len() || wind_drift_values.is_empty() {
218 return 0.0;
219 }
220
221 let mean_x = wind_drift_values.iter().sum::<f64>() / wind_drift_values.len() as f64;
223 let mean_y = drop_values.iter().sum::<f64>() / drop_values.len() as f64;
224
225 let mut distances: Vec<f64> = wind_drift_values
227 .iter()
228 .zip(drop_values.iter())
229 .map(|(x, y)| {
230 let dx = x - mean_x;
231 let dy = y - mean_y;
232 (dx * dx + dy * dy).sqrt()
233 })
234 .collect();
235
236 distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
238
239 percentile(&distances, 0.50)
241}
242
243pub fn calculate_confidence_ellipse(
247 wind_drift_values: &[f64],
248 drop_values: &[f64],
249) -> (f64, f64, f64, f64, f64) {
250 if wind_drift_values.len() != drop_values.len() || wind_drift_values.len() < 2 {
251 return (0.0, 0.0, 0.0, 0.0, 0.0);
252 }
253
254 let n = wind_drift_values.len() as f64;
255
256 let mean_x = wind_drift_values.iter().sum::<f64>() / n;
258 let mean_y = drop_values.iter().sum::<f64>() / n;
259
260 let mut cov_xx = 0.0;
262 let mut cov_yy = 0.0;
263 let mut cov_xy = 0.0;
264
265 for (x, y) in wind_drift_values.iter().zip(drop_values.iter()) {
266 let dx = x - mean_x;
267 let dy = y - mean_y;
268 cov_xx += dx * dx;
269 cov_yy += dy * dy;
270 cov_xy += dx * dy;
271 }
272
273 cov_xx /= n - 1.0;
274 cov_yy /= n - 1.0;
275 cov_xy /= n - 1.0;
276
277 let trace = cov_xx + cov_yy;
280 let det = cov_xx * cov_yy - cov_xy * cov_xy;
281 let discriminant = (trace * trace / 4.0 - det).max(0.0).sqrt();
282
283 let lambda1 = trace / 2.0 + discriminant; let lambda2 = trace / 2.0 - discriminant; let scale_factor = 5.991_f64.sqrt();
288 let semi_major = lambda1.max(0.0).sqrt() * scale_factor;
289 let semi_minor = lambda2.max(0.0).sqrt() * scale_factor;
290
291 let rotation_rad = if cov_xy.abs() < 1e-10 {
293 if cov_xx >= cov_yy {
294 0.0
295 } else {
296 std::f64::consts::PI / 2.0
297 }
298 } else {
299 ((lambda1 - cov_xx) / cov_xy).atan()
300 };
301
302 let rotation_deg = rotation_rad.to_degrees();
303
304 (mean_x, mean_y, semi_major, semi_minor, rotation_deg)
305}
306
307pub fn sample_points_for_visualization(
309 wind_drift_values: &[f64],
310 drop_values: &[f64],
311 max_points: usize,
312) -> Vec<(f64, f64)> {
313 let n = wind_drift_values.len();
314 if n == 0 {
315 return Vec::new();
316 }
317
318 if n <= max_points {
319 wind_drift_values
321 .iter()
322 .zip(drop_values.iter())
323 .map(|(x, y)| (*x, *y))
324 .collect()
325 } else {
326 let step = n as f64 / max_points as f64;
328 (0..max_points)
329 .map(|i| {
330 let idx = (i as f64 * step) as usize;
331 (wind_drift_values[idx], drop_values[idx])
332 })
333 .collect()
334 }
335}
336
337pub fn percentile(sorted_values: &[f64], p: f64) -> f64 {
339 if sorted_values.is_empty() {
340 return 0.0;
341 }
342
343 if sorted_values.len() == 1 {
344 return sorted_values[0];
345 }
346
347 let p = p.clamp(0.0, 1.0);
350 let rank = p * (sorted_values.len() - 1) as f64;
351 let lower_idx = rank.floor() as usize;
352 let upper_idx = rank.ceil() as usize;
353 let fraction = rank - lower_idx as f64;
354
355 if lower_idx == upper_idx {
356 sorted_values[lower_idx]
357 } else {
358 sorted_values[lower_idx] * (1.0 - fraction) + sorted_values[upper_idx] * fraction
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_calculate_cep() {
368 let wind_drift = vec![0.0, 1.0, -1.0, 0.5, -0.5];
369 let drop = vec![0.0, 0.5, -0.5, 1.0, -1.0];
370
371 let cep = calculate_cep(&wind_drift, &drop);
372 assert!(cep > 0.0);
373 assert!(cep < 2.0); }
375
376 #[test]
377 fn test_calculate_confidence_ellipse() {
378 let wind_drift = vec![0.0, 1.0, -1.0, 0.5, -0.5];
379 let drop = vec![0.0, 0.5, -0.5, 1.0, -1.0];
380
381 let (cx, cy, major, minor, _rotation) = calculate_confidence_ellipse(&wind_drift, &drop);
382
383 assert!(cx.abs() < 0.5);
385 assert!(cy.abs() < 0.5);
386
387 assert!(major > 0.0);
389 assert!(minor > 0.0);
390 assert!(major >= minor); }
392
393 #[test]
394 fn test_sample_points() {
395 let wind_drift = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
396 let drop = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
397
398 let sampled = sample_points_for_visualization(&wind_drift, &drop, 3);
399 assert_eq!(sampled.len(), 3);
400 }
401
402 #[test]
403 fn test_percentile() {
404 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
405
406 assert_eq!(percentile(&values, 0.0), 1.0);
407 assert_eq!(percentile(&values, 0.5), 3.0);
408 assert_eq!(percentile(&values, 1.0), 5.0);
409 }
410}