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
120 && 2.0 * p < 3.0 * m * q - (tolerance_scaled * q).abs()
121 && p < (0.5 * s * q).abs()
122 {
123 d = p / q;
124 } else {
125 d = m;
126 e = d;
127 }
128 }
129 } else {
130 d = m;
131 e = d;
132 }
133
134 a = b;
135 fa = fb;
136
137 if d.abs() > tolerance_scaled {
138 b += d;
139 } else if m > 0.0 {
140 b += tolerance_scaled;
141 } else {
142 b -= tolerance_scaled;
143 }
144
145 fb = f(b);
146
147 if (fc * fb) > 0.0 {
148 c = a;
149 fc = fa;
150 e = b - a;
151 d = e;
152 }
153 }
154
155 Ok(AngleResult {
156 angle_rad: b,
157 iterations_used: iterations,
158 final_error: fb.abs(),
159 success: fb.abs() < tolerance * 10.0, })
161}
162
163pub fn adjusted_muzzle_velocity(inputs: &InternalBallisticInputs) -> f64 {
165 let mut mv = inputs.muzzle_velocity;
166
167 if inputs.use_powder_sensitivity {
168 mv *=
169 1.0 + inputs.powder_temp_sensitivity * (inputs.temperature - inputs.powder_temp) / 15.0;
170 }
171
172 mv
173}
174
175pub fn zero_angle(
177 inputs: &InternalBallisticInputs,
178 trajectory_func: impl Fn(&InternalBallisticInputs, f64) -> Result<f64, String> + Copy,
179) -> Result<AngleResult, String> {
180 let vert = if inputs.shooting_angle.abs() > 1e-6 {
182 let angle_rad = inputs.shooting_angle * DEGREES_TO_RADIANS;
183 (inputs.target_distance * YARDS_TO_METERS) * angle_rad.sin()
184 } else {
185 0.0
186 };
187
188 let height_diff = |look_angle_rad: f64| -> f64 {
191 match trajectory_func(inputs, look_angle_rad) {
193 Ok(bullet_height) => bullet_height - vert,
194 Err(_) => f64::NAN, }
196 };
197
198 let lower_bound = -10.0 * DEGREES_TO_RADIANS;
201 let upper_bound = 10.0 * DEGREES_TO_RADIANS;
202
203 match brent_root_find(height_diff, lower_bound, upper_bound, 1e-6, 100) {
205 Ok(result) if result.success => Ok(result),
206 _ => {
207 let wider_lower = -45.0 * DEGREES_TO_RADIANS;
209 let wider_upper = 45.0 * DEGREES_TO_RADIANS;
210
211 match brent_root_find(height_diff, wider_lower, wider_upper, 1e-5, 150) {
212 Ok(result) if result.success => Ok(result),
213 Ok(result) => {
214 Ok(AngleResult {
216 angle_rad: result.angle_rad,
217 iterations_used: result.iterations_used,
218 final_error: result.final_error,
219 success: false,
220 })
221 }
222 Err(_) => {
223 Ok(AngleResult {
225 angle_rad: 0.0,
226 iterations_used: 0,
227 final_error: f64::INFINITY,
228 success: false,
229 })
230 }
231 }
232 }
233 }
234}
235
236pub fn solve_muzzle_angle(
238 inputs: &InternalBallisticInputs,
239 zero_distance_los_m: f64,
240 trajectory_func: impl Fn(&InternalBallisticInputs) -> Result<f64, String> + Copy, angle_lower_deg: f64,
242 angle_upper_deg: f64,
243 rtol: f64,
244) -> Result<AngleResult, String> {
245 if angle_lower_deg >= angle_upper_deg {
246 return Err("angle_lower_deg must be less than angle_upper_deg".to_string());
247 }
248
249 let lower = angle_lower_deg * DEGREES_TO_RADIANS;
250 let mut upper = angle_upper_deg * DEGREES_TO_RADIANS;
251
252 let vertical_error = |angle_rad: f64| -> f64 {
254 let mut candidate = inputs.clone();
256 candidate.muzzle_angle = angle_rad * RADIANS_TO_DEGREES;
257 candidate.target_distance = zero_distance_los_m / YARDS_TO_METERS; trajectory_func(&candidate).unwrap_or(f64::NAN)
264 };
265
266 let f_lower = vertical_error(lower);
268 if f_lower.abs() < 1e-9 {
269 return Ok(AngleResult {
270 angle_rad: lower,
271 iterations_used: 1,
272 final_error: f_lower.abs(),
273 success: true,
274 });
275 }
276
277 let f_upper = vertical_error(upper);
278 if f_upper.abs() < 1e-9 {
279 return Ok(AngleResult {
280 angle_rad: upper,
281 iterations_used: 1,
282 final_error: f_upper.abs(),
283 success: true,
284 });
285 }
286
287 if f_lower * f_upper > 0.0 {
289 let step = 5.0 * DEGREES_TO_RADIANS;
290 let max_angle = 45.0 * DEGREES_TO_RADIANS;
291 let mut current = upper;
292 let mut f_current = f_upper;
293
294 while current < max_angle && f_lower * f_current > 0.0 {
295 current += step;
296 f_current = vertical_error(current);
297 }
298
299 if f_lower * f_current > 0.0 {
300 return Err("Unable to bracket zero; widen angle bounds or check inputs".to_string());
301 }
302
303 upper = current;
304 }
305
306 let range = (upper - lower).abs();
308 let tolerance = if range > f64::EPSILON {
309 rtol * range
310 } else {
311 rtol * 1e-12 };
313 brent_root_find(
314 vertical_error,
315 lower,
316 upper,
317 tolerance,
318 ZERO_FINDING_MAX_ITER,
319 )
320}
321
322pub fn quick_drop_estimate(
324 muzzle_velocity_fps: f64,
325 distance_yards: f64,
326 _bullet_mass_grains: f64,
327 bc: f64,
328) -> f64 {
329 let mv_mps = muzzle_velocity_fps * FPS_TO_MPS;
330 let distance_m = distance_yards * YARDS_TO_METERS;
331
332 if mv_mps <= 0.0 || distance_m <= 0.0 {
334 return 0.0; }
336
337 let time_of_flight = distance_m / mv_mps;
338 let gravity = 9.80665;
339
340 let bc_safe = bc.max(0.1);
342 let drag_factor = 1.0 / bc_safe;
343 let velocity_loss = drag_factor * time_of_flight * 0.1;
344 let effective_velocity = mv_mps * (1.0 - velocity_loss).max(0.1); let adjusted_time = distance_m / effective_velocity;
346
347 0.5 * gravity * adjusted_time * adjusted_time
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 fn create_test_inputs() -> InternalBallisticInputs {
355 InternalBallisticInputs {
356 muzzle_velocity: 823.0, bc_value: 0.5,
358 bullet_mass: 0.0109, bullet_diameter: 0.00782, target_distance: 500.0,
361 temperature: 21.1, powder_temp: 21.1, ..Default::default()
364 }
365 }
366
367 #[test]
368 fn test_brent_root_find_quadratic() {
369 let f = |x: f64| x * x - 4.0;
371 let result = brent_root_find(f, 1.0, 3.0, 1e-6, 100).unwrap();
372
373 assert!(result.success);
374 assert!((result.angle_rad - 2.0).abs() < 1e-6);
375 assert!(result.iterations_used > 0);
376 assert!(result.final_error < 1e-6);
377 }
378
379 #[test]
380 fn test_brent_root_find_linear() {
381 let f = |x: f64| 2.0 * x - 6.0;
383 let result = brent_root_find(f, 0.0, 5.0, 1e-6, 100).unwrap();
384
385 assert!(result.success);
386 assert!((result.angle_rad - 3.0).abs() < 1e-6);
387 }
388
389 #[test]
390 fn test_brent_root_find_no_bracket() {
391 let f = |x: f64| x * x + 1.0; let result = brent_root_find(f, 1.0, 3.0, 1e-6, 100);
394
395 assert!(result.is_err());
396 assert!(result.unwrap_err().contains("Root not bracketed"));
397 }
398
399 #[test]
400 fn test_adjusted_muzzle_velocity_no_sensitivity() {
401 let inputs = create_test_inputs();
402
403 let result = adjusted_muzzle_velocity(&inputs);
404 assert_eq!(result, 823.0); }
406
407 #[test]
408 fn test_adjusted_muzzle_velocity_with_sensitivity() {
409 let mut inputs = create_test_inputs();
410 inputs.use_powder_sensitivity = true;
411 inputs.powder_temp_sensitivity = 1.0; inputs.temperature = 85.0; inputs.powder_temp = 70.0; let result = adjusted_muzzle_velocity(&inputs);
416 assert!((result - 1646.0).abs() < 1e-6);
418 }
419
420 #[test]
421 fn test_quick_drop_estimate() {
422 let drop = quick_drop_estimate(2700.0, 500.0, 168.0, 0.5);
423
424 assert!(drop > 0.0);
426 assert!(drop < 50.0); let drop_high_bc = quick_drop_estimate(2700.0, 500.0, 168.0, 0.8);
430 assert!(drop_high_bc < drop);
431 }
432
433 #[test]
434 fn test_zero_angle_bounds() {
435 let lower = -10.0 * DEGREES_TO_RADIANS;
437 let upper = 10.0 * DEGREES_TO_RADIANS;
438
439 assert!(lower < 0.0);
440 assert!(upper > 0.0);
441 assert!((upper - lower).abs() > 0.1); }
443
444 #[test]
445 fn test_brent_root_find_near_zero_function_values() {
446 let f = |x: f64| (x - 1.0) * 1e-10; let result = brent_root_find(f, 0.0, 2.0, 1e-12, 100).unwrap();
451
452 assert!(result.success || result.iterations_used > 0);
455 assert!((result.angle_rad - 1.0).abs() < 0.1);
457 }
458
459 #[test]
460 fn test_brent_root_find_steep_function() {
461 let f = |x: f64| (x - 0.5).powi(3) * 1e6;
463 let result = brent_root_find(f, 0.0, 1.0, 1e-9, 100).unwrap();
464
465 assert!(result.success);
466 assert!((result.angle_rad - 0.5).abs() < 1e-6);
467 }
468
469 #[test]
470 fn test_brent_root_find_oscillating_convergence() {
471 let f = |x: f64| x.sin() - 0.5;
474 let result = brent_root_find(f, 0.0, 1.0, 1e-10, 100).unwrap();
475
476 assert!(result.success);
477 assert!((result.angle_rad - 0.5235987755982989).abs() < 1e-6);
479 }
480
481 #[test]
482 fn test_brent_root_find_flat_region() {
483 let f = |x: f64| (x - 2.0).powi(5);
486 let result = brent_root_find(f, 1.0, 3.0, 1e-8, 100).unwrap();
487
488 assert!(result.success);
489 assert!((result.angle_rad - 2.0).abs() < 1e-4);
490 }
491}