1use crate::InternalBallisticInputs;
2use std::f64;
3
4const FPS_TO_MPS: f64 = 0.3048;
6const YARDS_TO_METERS: f64 = 0.9144;
7const DEGREES_TO_RADIANS: f64 = std::f64::consts::PI / 180.0;
8const RADIANS_TO_DEGREES: f64 = 180.0 / std::f64::consts::PI;
9
10const ZERO_FINDING_MAX_ITER: usize = 100;
12
13#[derive(Debug, Clone)]
15pub struct AngleResult {
16 pub angle_rad: f64,
17 pub iterations_used: usize,
18 pub final_error: f64,
19 pub success: bool,
20}
21
22pub fn brent_root_find<F>(
24 f: F,
25 mut a: f64,
26 mut b: f64,
27 tolerance: f64,
28 max_iterations: usize,
29) -> Result<AngleResult, String>
30where
31 F: Fn(f64) -> f64,
32{
33 let mut fa = f(a);
34 let mut fb = f(b);
35 let mut iterations = 0;
36
37 if fa * fb > 0.0 {
39 return Err(format!("Root not bracketed: f({a}) = {fa}, f({b}) = {fb}"));
40 }
41
42 if fa.abs() < fb.abs() {
44 std::mem::swap(&mut a, &mut b);
45 std::mem::swap(&mut fa, &mut fb);
46 }
47
48 let mut c = a;
49 let mut fc = fa;
50 let mut d = b - a;
51 let mut e = d;
52
53 while iterations < max_iterations {
54 iterations += 1;
55
56 if fb.abs() < tolerance {
57 return Ok(AngleResult {
58 angle_rad: b,
59 iterations_used: iterations,
60 final_error: fb.abs(),
61 success: true,
62 });
63 }
64
65 if fa.abs() < fb.abs() {
66 a = b;
67 b = c;
68 c = a;
69 fa = fb;
70 fb = fc;
71 fc = fa;
72 }
73
74 let tolerance_scaled = 2.0 * f64::EPSILON * b.abs() + 0.5 * tolerance;
75 let m = 0.5 * (c - b);
76
77 if m.abs() <= tolerance_scaled {
78 return Ok(AngleResult {
79 angle_rad: b,
80 iterations_used: iterations,
81 final_error: fb.abs(),
82 success: true,
83 });
84 }
85
86 if e.abs() >= tolerance_scaled && fc.abs() > fb.abs() {
87 if fc.abs() < f64::EPSILON || fa.abs() < f64::EPSILON {
89 d = m;
91 e = m;
92 } else {
93 let s = fb / fc;
94 let mut p;
95 let mut q;
96
97 if (a - c).abs() < f64::EPSILON {
98 p = 2.0 * m * s;
100 q = 1.0 - s;
101 } else {
102 q = fc / fa;
104 let r = fb / fa;
105 p = s * (2.0 * m * q * (q - r) - (b - a) * (r - 1.0));
106 q = (q - 1.0) * (r - 1.0) * (s - 1.0);
107 }
108
109 if p > 0.0 {
110 q = -q;
111 } else {
112 p = -p;
113 }
114
115 let s = e;
116 e = d;
117
118 if q.abs() > f64::EPSILON && 2.0 * p < 3.0 * m * q - (tolerance_scaled * q).abs()
120 && p < (0.5 * s * q).abs() {
121 d = p / q;
122 } else {
123 d = m;
124 e = d;
125 }
126 }
127 } else {
128 d = m;
129 e = d;
130 }
131
132 a = b;
133 fa = fb;
134
135 if d.abs() > tolerance_scaled {
136 b += d;
137 } else if m > 0.0 {
138 b += tolerance_scaled;
139 } else {
140 b -= tolerance_scaled;
141 }
142
143 fb = f(b);
144
145 if (fc * fb) > 0.0 {
146 c = a;
147 fc = fa;
148 e = b - a;
149 d = e;
150 }
151 }
152
153 Ok(AngleResult {
154 angle_rad: b,
155 iterations_used: iterations,
156 final_error: fb.abs(),
157 success: fb.abs() < tolerance * 10.0, })
159}
160
161pub fn adjusted_muzzle_velocity(inputs: &InternalBallisticInputs) -> f64 {
163 let mut mv = inputs.muzzle_velocity;
164
165 if inputs.use_powder_sensitivity {
166 mv *= 1.0 + inputs.powder_temp_sensitivity
167 * (inputs.temperature - inputs.powder_temp) / 15.0;
168 }
169
170 mv
171}
172
173pub fn zero_angle(
175 inputs: &InternalBallisticInputs,
176 trajectory_func: impl Fn(&InternalBallisticInputs, f64) -> Result<f64, String> + Copy,
177) -> Result<AngleResult, String> {
178 let vert = if inputs.shooting_angle.abs() > 1e-6 {
180 let angle_rad = inputs.shooting_angle * DEGREES_TO_RADIANS;
181 (inputs.target_distance * YARDS_TO_METERS) * angle_rad.sin()
182 } else {
183 0.0
184 };
185
186 let height_diff = |look_angle_rad: f64| -> f64 {
189 match trajectory_func(inputs, look_angle_rad) {
191 Ok(bullet_height) => bullet_height - vert,
192 Err(_) => f64::NAN, }
194 };
195
196 let lower_bound = -10.0 * DEGREES_TO_RADIANS;
199 let upper_bound = 10.0 * DEGREES_TO_RADIANS;
200
201 match brent_root_find(height_diff, lower_bound, upper_bound, 1e-6, 100) {
203 Ok(result) if result.success => Ok(result),
204 _ => {
205 let wider_lower = -45.0 * DEGREES_TO_RADIANS;
207 let wider_upper = 45.0 * DEGREES_TO_RADIANS;
208
209 match brent_root_find(height_diff, wider_lower, wider_upper, 1e-5, 150) {
210 Ok(result) if result.success => Ok(result),
211 Ok(result) => {
212 Ok(AngleResult {
214 angle_rad: result.angle_rad,
215 iterations_used: result.iterations_used,
216 final_error: result.final_error,
217 success: false,
218 })
219 },
220 Err(_) => {
221 Ok(AngleResult {
223 angle_rad: 0.0,
224 iterations_used: 0,
225 final_error: f64::INFINITY,
226 success: false,
227 })
228 }
229 }
230 }
231 }
232}
233
234pub fn solve_muzzle_angle(
236 inputs: &InternalBallisticInputs,
237 zero_distance_los_m: f64,
238 trajectory_func: impl Fn(&InternalBallisticInputs) -> Result<f64, String> + Copy, angle_lower_deg: f64,
240 angle_upper_deg: f64,
241 rtol: f64,
242) -> Result<AngleResult, String> {
243 if angle_lower_deg >= angle_upper_deg {
244 return Err("angle_lower_deg must be less than angle_upper_deg".to_string());
245 }
246
247 let lower = angle_lower_deg * DEGREES_TO_RADIANS;
248 let mut upper = angle_upper_deg * DEGREES_TO_RADIANS;
249
250 let vertical_error = |angle_rad: f64| -> f64 {
252 let mut candidate = inputs.clone();
254 candidate.muzzle_angle = angle_rad * RADIANS_TO_DEGREES;
255 candidate.target_distance = zero_distance_los_m / YARDS_TO_METERS; trajectory_func(&candidate).unwrap_or(1e6)
258 };
259
260 let f_lower = vertical_error(lower);
262 if f_lower.abs() < 1e-9 {
263 return Ok(AngleResult {
264 angle_rad: lower,
265 iterations_used: 1,
266 final_error: f_lower.abs(),
267 success: true,
268 });
269 }
270
271 let f_upper = vertical_error(upper);
272 if f_upper.abs() < 1e-9 {
273 return Ok(AngleResult {
274 angle_rad: upper,
275 iterations_used: 1,
276 final_error: f_upper.abs(),
277 success: true,
278 });
279 }
280
281 if f_lower * f_upper > 0.0 {
283 let step = 5.0 * DEGREES_TO_RADIANS;
284 let max_angle = 45.0 * DEGREES_TO_RADIANS;
285 let mut current = upper;
286 let mut f_current = f_upper;
287
288 while current < max_angle && f_lower * f_current > 0.0 {
289 current += step;
290 f_current = vertical_error(current);
291 }
292
293 if f_lower * f_current > 0.0 {
294 return Err("Unable to bracket zero; widen angle bounds or check inputs".to_string());
295 }
296
297 upper = current;
298 }
299
300 let range = (upper - lower).abs();
302 let tolerance = if range > f64::EPSILON {
303 rtol * range
304 } else {
305 rtol * 1e-12 };
307 brent_root_find(vertical_error, lower, upper, tolerance, ZERO_FINDING_MAX_ITER)
308}
309
310pub fn quick_drop_estimate(
312 muzzle_velocity_fps: f64,
313 distance_yards: f64,
314 _bullet_mass_grains: f64,
315 bc: f64,
316) -> f64 {
317 let mv_mps = muzzle_velocity_fps * FPS_TO_MPS;
318 let distance_m = distance_yards * YARDS_TO_METERS;
319
320 if mv_mps <= 0.0 || distance_m <= 0.0 {
322 return 0.0; }
324
325 let time_of_flight = distance_m / mv_mps;
326 let gravity = 9.80665;
327
328 let bc_safe = bc.max(0.1);
330 let drag_factor = 1.0 / bc_safe;
331 let velocity_loss = drag_factor * time_of_flight * 0.1;
332 let effective_velocity = mv_mps * (1.0 - velocity_loss).max(0.1); let adjusted_time = distance_m / effective_velocity;
334
335 0.5 * gravity * adjusted_time * adjusted_time
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342
343 fn create_test_inputs() -> InternalBallisticInputs {
344 InternalBallisticInputs {
345 muzzle_velocity: 823.0, bc_value: 0.5,
347 bullet_mass: 0.0109, bullet_diameter: 0.00782, target_distance: 500.0,
350 temperature: 21.1, powder_temp: 21.1, ..Default::default()
353 }
354 }
355
356 #[test]
357 fn test_brent_root_find_quadratic() {
358 let f = |x: f64| x * x - 4.0;
360 let result = brent_root_find(f, 1.0, 3.0, 1e-6, 100).unwrap();
361
362 assert!(result.success);
363 assert!((result.angle_rad - 2.0).abs() < 1e-6);
364 assert!(result.iterations_used > 0);
365 assert!(result.final_error < 1e-6);
366 }
367
368 #[test]
369 fn test_brent_root_find_linear() {
370 let f = |x: f64| 2.0 * x - 6.0;
372 let result = brent_root_find(f, 0.0, 5.0, 1e-6, 100).unwrap();
373
374 assert!(result.success);
375 assert!((result.angle_rad - 3.0).abs() < 1e-6);
376 }
377
378 #[test]
379 fn test_brent_root_find_no_bracket() {
380 let f = |x: f64| x * x + 1.0; let result = brent_root_find(f, 1.0, 3.0, 1e-6, 100);
383
384 assert!(result.is_err());
385 assert!(result.unwrap_err().contains("Root not bracketed"));
386 }
387
388 #[test]
389 fn test_adjusted_muzzle_velocity_no_sensitivity() {
390 let inputs = create_test_inputs();
391
392 let result = adjusted_muzzle_velocity(&inputs);
393 assert_eq!(result, 823.0); }
395
396 #[test]
397 fn test_adjusted_muzzle_velocity_with_sensitivity() {
398 let mut inputs = create_test_inputs();
399 inputs.use_powder_sensitivity = true;
400 inputs.powder_temp_sensitivity = 1.0; inputs.temperature = 85.0; inputs.powder_temp = 70.0; let result = adjusted_muzzle_velocity(&inputs);
405 assert!((result - 1646.0).abs() < 1e-6);
407 }
408
409 #[test]
410 fn test_quick_drop_estimate() {
411 let drop = quick_drop_estimate(2700.0, 500.0, 168.0, 0.5);
412
413 assert!(drop > 0.0);
415 assert!(drop < 50.0); let drop_high_bc = quick_drop_estimate(2700.0, 500.0, 168.0, 0.8);
419 assert!(drop_high_bc < drop);
420 }
421
422 #[test]
423 fn test_zero_angle_bounds() {
424 let lower = -10.0 * DEGREES_TO_RADIANS;
426 let upper = 10.0 * DEGREES_TO_RADIANS;
427
428 assert!(lower < 0.0);
429 assert!(upper > 0.0);
430 assert!((upper - lower).abs() > 0.1); }
432
433 #[test]
434 fn test_brent_root_find_near_zero_function_values() {
435 let f = |x: f64| (x - 1.0) * 1e-10; let result = brent_root_find(f, 0.0, 2.0, 1e-12, 100).unwrap();
440
441 assert!(result.success || result.iterations_used > 0);
444 assert!((result.angle_rad - 1.0).abs() < 0.1);
446 }
447
448 #[test]
449 fn test_brent_root_find_steep_function() {
450 let f = |x: f64| (x - 0.5).powi(3) * 1e6;
452 let result = brent_root_find(f, 0.0, 1.0, 1e-9, 100).unwrap();
453
454 assert!(result.success);
455 assert!((result.angle_rad - 0.5).abs() < 1e-6);
456 }
457
458 #[test]
459 fn test_brent_root_find_oscillating_convergence() {
460 let f = |x: f64| x.sin() - 0.5;
463 let result = brent_root_find(f, 0.0, 1.0, 1e-10, 100).unwrap();
464
465 assert!(result.success);
466 assert!((result.angle_rad - 0.5235987755982989).abs() < 1e-6);
468 }
469
470 #[test]
471 fn test_brent_root_find_flat_region() {
472 let f = |x: f64| (x - 2.0).powi(5);
475 let result = brent_root_find(f, 1.0, 3.0, 1e-8, 100).unwrap();
476
477 assert!(result.success);
478 assert!((result.angle_rad - 2.0).abs() < 1e-4);
479 }
480}