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; let (air_density, speed_of_sound) = calculate_atmosphere(
41 inputs.altitude, Some(inputs.temperature),
43 Some(inputs.pressure),
44 inputs.humidity,
45 );
46
47 let wind_segments = vec![(
50 inputs.wind_speed * 3.6, inputs.wind_angle.to_degrees(), target_distance_m * 2.0, )];
54 let wind_sock = WindSock::new(wind_segments);
55
56 let muzzle_angle_rad = inputs.muzzle_angle;
58 let initial_velocity = Vector3::new(
60 muzzle_velocity_mps * muzzle_angle_rad.cos(),
61 muzzle_velocity_mps * muzzle_angle_rad.sin(),
62 0.0,
63 );
64
65 let initial_position = Vector3::new(0.0, inputs.sight_height, 0.0); let mut initial_state_array = [0.0; 6];
67 initial_state_array[0..3].copy_from_slice(&[
68 initial_position.x,
69 initial_position.y,
70 initial_position.z,
71 ]);
72 initial_state_array[3..6].copy_from_slice(&[
73 initial_velocity.x,
74 initial_velocity.y,
75 initial_velocity.z,
76 ]);
77
78 let base_ratio = air_density / 1.225;
85 let params = FastIntegrationParams {
86 initial_state: initial_state_array,
87 t_span: (0.0, 30.0),
88 horiz: target_distance_m,
89 vert: 0.0, atmo_params: (inputs.altitude, inputs.temperature, inputs.pressure, base_ratio),
91 };
92
93 let solution = fast_integrate(inputs, &wind_sock, params);
95
96 if solution.t.is_empty() {
97 return Err("Empty trajectory solution".to_string());
98 }
99
100 let final_idx = solution.t.len() - 1;
103
104 let final_downrange = solution.y[0][final_idx]; let final_y = solution.y[1][final_idx]; let final_lateral = solution.y[2][final_idx]; let final_vx = solution.y[3][final_idx];
109 let final_vy = solution.y[4][final_idx];
110 let final_vz = solution.y[5][final_idx];
111
112 let final_speed = (final_vx * final_vx + final_vy * final_vy + final_vz * final_vz).sqrt();
113 let final_mach = final_speed / speed_of_sound;
114 let final_energy = 0.5 * mass_kg * final_speed * final_speed;
115
116 let sight_height_m = inputs.sight_height; let los_y = sight_height_m + (0.0 - sight_height_m) * (final_downrange / target_distance_m);
119 let drop = los_y - final_y;
120
121 Ok(TrajectoryOutput {
122 drop,
123 wind_drift: final_lateral,
124 time: solution.t[final_idx],
125 velocity: final_speed,
126 energy: final_energy,
127 mach: final_mach,
128 spin_drift: final_lateral, distance: final_downrange,
130 })
131}
132
133pub fn calculate_cep(wind_drift_values: &[f64], drop_values: &[f64]) -> f64 {
138 if wind_drift_values.len() != drop_values.len() || wind_drift_values.is_empty() {
139 return 0.0;
140 }
141
142 let mean_x = wind_drift_values.iter().sum::<f64>() / wind_drift_values.len() as f64;
144 let mean_y = drop_values.iter().sum::<f64>() / drop_values.len() as f64;
145
146 let mut distances: Vec<f64> = wind_drift_values
148 .iter()
149 .zip(drop_values.iter())
150 .map(|(x, y)| {
151 let dx = x - mean_x;
152 let dy = y - mean_y;
153 (dx * dx + dy * dy).sqrt()
154 })
155 .collect();
156
157 distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
159
160 percentile(&distances, 0.50)
162}
163
164pub fn calculate_confidence_ellipse(
168 wind_drift_values: &[f64],
169 drop_values: &[f64],
170) -> (f64, f64, f64, f64, f64) {
171 if wind_drift_values.len() != drop_values.len() || wind_drift_values.len() < 2 {
172 return (0.0, 0.0, 0.0, 0.0, 0.0);
173 }
174
175 let n = wind_drift_values.len() as f64;
176
177 let mean_x = wind_drift_values.iter().sum::<f64>() / n;
179 let mean_y = drop_values.iter().sum::<f64>() / n;
180
181 let mut cov_xx = 0.0;
183 let mut cov_yy = 0.0;
184 let mut cov_xy = 0.0;
185
186 for (x, y) in wind_drift_values.iter().zip(drop_values.iter()) {
187 let dx = x - mean_x;
188 let dy = y - mean_y;
189 cov_xx += dx * dx;
190 cov_yy += dy * dy;
191 cov_xy += dx * dy;
192 }
193
194 cov_xx /= n - 1.0;
195 cov_yy /= n - 1.0;
196 cov_xy /= n - 1.0;
197
198 let trace = cov_xx + cov_yy;
201 let det = cov_xx * cov_yy - cov_xy * cov_xy;
202 let discriminant = (trace * trace / 4.0 - det).max(0.0).sqrt();
203
204 let lambda1 = trace / 2.0 + discriminant; let lambda2 = trace / 2.0 - discriminant; let scale_factor = 5.991_f64.sqrt();
209 let semi_major = lambda1.max(0.0).sqrt() * scale_factor;
210 let semi_minor = lambda2.max(0.0).sqrt() * scale_factor;
211
212 let rotation_rad = if cov_xy.abs() < 1e-10 {
214 if cov_xx >= cov_yy {
215 0.0
216 } else {
217 std::f64::consts::PI / 2.0
218 }
219 } else {
220 ((lambda1 - cov_xx) / cov_xy).atan()
221 };
222
223 let rotation_deg = rotation_rad.to_degrees();
224
225 (mean_x, mean_y, semi_major, semi_minor, rotation_deg)
226}
227
228pub fn sample_points_for_visualization(
230 wind_drift_values: &[f64],
231 drop_values: &[f64],
232 max_points: usize,
233) -> Vec<(f64, f64)> {
234 let n = wind_drift_values.len();
235 if n == 0 {
236 return Vec::new();
237 }
238
239 if n <= max_points {
240 wind_drift_values
242 .iter()
243 .zip(drop_values.iter())
244 .map(|(x, y)| (*x, *y))
245 .collect()
246 } else {
247 let step = n as f64 / max_points as f64;
249 (0..max_points)
250 .map(|i| {
251 let idx = (i as f64 * step) as usize;
252 (wind_drift_values[idx], drop_values[idx])
253 })
254 .collect()
255 }
256}
257
258pub fn percentile(sorted_values: &[f64], p: f64) -> f64 {
260 if sorted_values.is_empty() {
261 return 0.0;
262 }
263
264 if sorted_values.len() == 1 {
265 return sorted_values[0];
266 }
267
268 let rank = p * (sorted_values.len() - 1) as f64;
269 let lower_idx = rank.floor() as usize;
270 let upper_idx = rank.ceil() as usize;
271 let fraction = rank - lower_idx as f64;
272
273 if lower_idx == upper_idx {
274 sorted_values[lower_idx]
275 } else {
276 sorted_values[lower_idx] * (1.0 - fraction) + sorted_values[upper_idx] * fraction
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_calculate_cep() {
286 let wind_drift = vec![0.0, 1.0, -1.0, 0.5, -0.5];
287 let drop = vec![0.0, 0.5, -0.5, 1.0, -1.0];
288
289 let cep = calculate_cep(&wind_drift, &drop);
290 assert!(cep > 0.0);
291 assert!(cep < 2.0); }
293
294 #[test]
295 fn test_calculate_confidence_ellipse() {
296 let wind_drift = vec![0.0, 1.0, -1.0, 0.5, -0.5];
297 let drop = vec![0.0, 0.5, -0.5, 1.0, -1.0];
298
299 let (cx, cy, major, minor, _rotation) = calculate_confidence_ellipse(&wind_drift, &drop);
300
301 assert!(cx.abs() < 0.5);
303 assert!(cy.abs() < 0.5);
304
305 assert!(major > 0.0);
307 assert!(minor > 0.0);
308 assert!(major >= minor); }
310
311 #[test]
312 fn test_sample_points() {
313 let wind_drift = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
314 let drop = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
315
316 let sampled = sample_points_for_visualization(&wind_drift, &drop, 3);
317 assert_eq!(sampled.len(), 3);
318 }
319
320 #[test]
321 fn test_percentile() {
322 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
323
324 assert_eq!(percentile(&values, 0.0), 1.0);
325 assert_eq!(percentile(&values, 0.5), 3.0);
326 assert_eq!(percentile(&values, 1.0), 5.0);
327 }
328}