1use crate::atmosphere::calculate_atmosphere;
9use crate::constants::{FPS_TO_MPS, GRAINS_TO_KG};
10use crate::fast_trajectory::{fast_integrate, FastIntegrationParams};
11use crate::wind::WindSock;
12use crate::BallisticInputs;
13use nalgebra::Vector3;
14
15const YARDS_TO_METERS: f64 = 0.9144;
16
17#[derive(Debug, Clone)]
19pub struct TrajectoryOutput {
20 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, }
29
30pub fn solve_trajectory_for_monte_carlo(
35 inputs: &BallisticInputs,
36) -> Result<TrajectoryOutput, String> {
37 let target_distance_m = inputs.target_distance * YARDS_TO_METERS;
39 let muzzle_velocity_mps = inputs.muzzle_velocity * FPS_TO_MPS;
40 let mass_kg = inputs.bullet_mass * GRAINS_TO_KG;
41
42 let (air_density, speed_of_sound) = calculate_atmosphere(
44 inputs.altitude * 0.3048, Some(inputs.temperature),
46 Some(inputs.pressure),
47 inputs.humidity,
48 );
49
50 let wind_segments = vec![(
54 inputs.wind_speed, inputs.wind_angle, target_distance_m * 2.0, )];
58 let wind_sock = WindSock::new(wind_segments);
59
60 let muzzle_angle_rad = inputs.muzzle_angle;
62 let initial_velocity = Vector3::new(
63 0.0,
64 muzzle_velocity_mps * muzzle_angle_rad.sin(),
65 muzzle_velocity_mps * muzzle_angle_rad.cos(),
66 );
67
68 let initial_position = Vector3::new(0.0, inputs.sight_height * 0.0254, 0.0);
69 let mut initial_state_array = [0.0; 6];
70 initial_state_array[0..3].copy_from_slice(&[
71 initial_position.x,
72 initial_position.y,
73 initial_position.z,
74 ]);
75 initial_state_array[3..6].copy_from_slice(&[
76 initial_velocity.x,
77 initial_velocity.y,
78 initial_velocity.z,
79 ]);
80
81 let temp_c = inputs.temperature;
83 let pressure_hpa = inputs.pressure;
84
85 let params = FastIntegrationParams {
87 initial_state: initial_state_array,
88 t_span: (0.0, 30.0),
89 horiz: target_distance_m,
90 vert: 0.0, atmo_params: (temp_c, pressure_hpa, air_density, speed_of_sound),
92 };
93
94 let solution = fast_integrate(inputs, &wind_sock, params);
96
97 if solution.t.is_empty() {
98 return Err("Empty trajectory solution".to_string());
99 }
100
101 let final_idx = solution.t.len() - 1;
104
105 let final_x = solution.y[0][final_idx]; let final_y = solution.y[1][final_idx]; let final_z = solution.y[2][final_idx]; let final_vx = solution.y[3][final_idx];
110 let final_vy = solution.y[4][final_idx];
111 let final_vz = solution.y[5][final_idx];
112
113 let final_speed = (final_vx * final_vx + final_vy * final_vy + final_vz * final_vz).sqrt();
114 let final_mach = final_speed / speed_of_sound;
115 let final_energy = 0.5 * mass_kg * final_speed * final_speed;
116
117 let sight_height_m = inputs.sight_height * 0.0254;
119 let los_y = sight_height_m + (0.0 - sight_height_m) * (final_z / target_distance_m);
120 let drop = los_y - final_y;
121
122 Ok(TrajectoryOutput {
123 drop,
124 wind_drift: final_x,
125 time: solution.t[final_idx],
126 velocity: final_speed,
127 energy: final_energy,
128 mach: final_mach,
129 spin_drift: final_x, distance: final_z,
131 })
132}
133
134pub fn calculate_cep(wind_drift_values: &[f64], drop_values: &[f64]) -> f64 {
139 if wind_drift_values.len() != drop_values.len() || wind_drift_values.is_empty() {
140 return 0.0;
141 }
142
143 let mean_x = wind_drift_values.iter().sum::<f64>() / wind_drift_values.len() as f64;
145 let mean_y = drop_values.iter().sum::<f64>() / drop_values.len() as f64;
146
147 let mut distances: Vec<f64> = wind_drift_values
149 .iter()
150 .zip(drop_values.iter())
151 .map(|(x, y)| {
152 let dx = x - mean_x;
153 let dy = y - mean_y;
154 (dx * dx + dy * dy).sqrt()
155 })
156 .collect();
157
158 distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
160
161 percentile(&distances, 0.50)
163}
164
165pub fn calculate_confidence_ellipse(
169 wind_drift_values: &[f64],
170 drop_values: &[f64],
171) -> (f64, f64, f64, f64, f64) {
172 if wind_drift_values.len() != drop_values.len() || wind_drift_values.len() < 2 {
173 return (0.0, 0.0, 0.0, 0.0, 0.0);
174 }
175
176 let n = wind_drift_values.len() as f64;
177
178 let mean_x = wind_drift_values.iter().sum::<f64>() / n;
180 let mean_y = drop_values.iter().sum::<f64>() / n;
181
182 let mut cov_xx = 0.0;
184 let mut cov_yy = 0.0;
185 let mut cov_xy = 0.0;
186
187 for (x, y) in wind_drift_values.iter().zip(drop_values.iter()) {
188 let dx = x - mean_x;
189 let dy = y - mean_y;
190 cov_xx += dx * dx;
191 cov_yy += dy * dy;
192 cov_xy += dx * dy;
193 }
194
195 cov_xx /= n - 1.0;
196 cov_yy /= n - 1.0;
197 cov_xy /= n - 1.0;
198
199 let trace = cov_xx + cov_yy;
202 let det = cov_xx * cov_yy - cov_xy * cov_xy;
203 let discriminant = (trace * trace / 4.0 - det).max(0.0).sqrt();
204
205 let lambda1 = trace / 2.0 + discriminant; let lambda2 = trace / 2.0 - discriminant; let scale_factor = 5.991_f64.sqrt();
210 let semi_major = lambda1.max(0.0).sqrt() * scale_factor;
211 let semi_minor = lambda2.max(0.0).sqrt() * scale_factor;
212
213 let rotation_rad = if cov_xy.abs() < 1e-10 {
215 if cov_xx >= cov_yy {
216 0.0
217 } else {
218 std::f64::consts::PI / 2.0
219 }
220 } else {
221 ((lambda1 - cov_xx) / cov_xy).atan()
222 };
223
224 let rotation_deg = rotation_rad.to_degrees();
225
226 (mean_x, mean_y, semi_major, semi_minor, rotation_deg)
227}
228
229pub fn sample_points_for_visualization(
231 wind_drift_values: &[f64],
232 drop_values: &[f64],
233 max_points: usize,
234) -> Vec<(f64, f64)> {
235 let n = wind_drift_values.len();
236 if n == 0 {
237 return Vec::new();
238 }
239
240 if n <= max_points {
241 wind_drift_values
243 .iter()
244 .zip(drop_values.iter())
245 .map(|(x, y)| (*x, *y))
246 .collect()
247 } else {
248 let step = n as f64 / max_points as f64;
250 (0..max_points)
251 .map(|i| {
252 let idx = (i as f64 * step) as usize;
253 (wind_drift_values[idx], drop_values[idx])
254 })
255 .collect()
256 }
257}
258
259pub fn percentile(sorted_values: &[f64], p: f64) -> f64 {
261 if sorted_values.is_empty() {
262 return 0.0;
263 }
264
265 if sorted_values.len() == 1 {
266 return sorted_values[0];
267 }
268
269 let rank = p * (sorted_values.len() - 1) as f64;
270 let lower_idx = rank.floor() as usize;
271 let upper_idx = rank.ceil() as usize;
272 let fraction = rank - lower_idx as f64;
273
274 if lower_idx == upper_idx {
275 sorted_values[lower_idx]
276 } else {
277 sorted_values[lower_idx] * (1.0 - fraction) + sorted_values[upper_idx] * fraction
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_calculate_cep() {
287 let wind_drift = vec![0.0, 1.0, -1.0, 0.5, -0.5];
288 let drop = vec![0.0, 0.5, -0.5, 1.0, -1.0];
289
290 let cep = calculate_cep(&wind_drift, &drop);
291 assert!(cep > 0.0);
292 assert!(cep < 2.0); }
294
295 #[test]
296 fn test_calculate_confidence_ellipse() {
297 let wind_drift = vec![0.0, 1.0, -1.0, 0.5, -0.5];
298 let drop = vec![0.0, 0.5, -0.5, 1.0, -1.0];
299
300 let (cx, cy, major, minor, _rotation) = calculate_confidence_ellipse(&wind_drift, &drop);
301
302 assert!(cx.abs() < 0.5);
304 assert!(cy.abs() < 0.5);
305
306 assert!(major > 0.0);
308 assert!(minor > 0.0);
309 assert!(major >= minor); }
311
312 #[test]
313 fn test_sample_points() {
314 let wind_drift = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
315 let drop = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
316
317 let sampled = sample_points_for_visualization(&wind_drift, &drop, 3);
318 assert_eq!(sampled.len(), 3);
319 }
320
321 #[test]
322 fn test_percentile() {
323 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
324
325 assert_eq!(percentile(&values, 0.0), 1.0);
326 assert_eq!(percentile(&values, 0.5), 3.0);
327 assert_eq!(percentile(&values, 1.0), 5.0);
328 }
329}