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(1e6)
260 };
261
262 let f_lower = vertical_error(lower);
264 if f_lower.abs() < 1e-9 {
265 return Ok(AngleResult {
266 angle_rad: lower,
267 iterations_used: 1,
268 final_error: f_lower.abs(),
269 success: true,
270 });
271 }
272
273 let f_upper = vertical_error(upper);
274 if f_upper.abs() < 1e-9 {
275 return Ok(AngleResult {
276 angle_rad: upper,
277 iterations_used: 1,
278 final_error: f_upper.abs(),
279 success: true,
280 });
281 }
282
283 if f_lower * f_upper > 0.0 {
285 let step = 5.0 * DEGREES_TO_RADIANS;
286 let max_angle = 45.0 * DEGREES_TO_RADIANS;
287 let mut current = upper;
288 let mut f_current = f_upper;
289
290 while current < max_angle && f_lower * f_current > 0.0 {
291 current += step;
292 f_current = vertical_error(current);
293 }
294
295 if f_lower * f_current > 0.0 {
296 return Err("Unable to bracket zero; widen angle bounds or check inputs".to_string());
297 }
298
299 upper = current;
300 }
301
302 let range = (upper - lower).abs();
304 let tolerance = if range > f64::EPSILON {
305 rtol * range
306 } else {
307 rtol * 1e-12 };
309 brent_root_find(
310 vertical_error,
311 lower,
312 upper,
313 tolerance,
314 ZERO_FINDING_MAX_ITER,
315 )
316}
317
318pub fn quick_drop_estimate(
320 muzzle_velocity_fps: f64,
321 distance_yards: f64,
322 _bullet_mass_grains: f64,
323 bc: f64,
324) -> f64 {
325 let mv_mps = muzzle_velocity_fps * FPS_TO_MPS;
326 let distance_m = distance_yards * YARDS_TO_METERS;
327
328 if mv_mps <= 0.0 || distance_m <= 0.0 {
330 return 0.0; }
332
333 let time_of_flight = distance_m / mv_mps;
334 let gravity = 9.80665;
335
336 let bc_safe = bc.max(0.1);
338 let drag_factor = 1.0 / bc_safe;
339 let velocity_loss = drag_factor * time_of_flight * 0.1;
340 let effective_velocity = mv_mps * (1.0 - velocity_loss).max(0.1); let adjusted_time = distance_m / effective_velocity;
342
343 0.5 * gravity * adjusted_time * adjusted_time
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 fn create_test_inputs() -> InternalBallisticInputs {
351 InternalBallisticInputs {
352 muzzle_velocity: 823.0, bc_value: 0.5,
354 bullet_mass: 0.0109, bullet_diameter: 0.00782, target_distance: 500.0,
357 temperature: 21.1, powder_temp: 21.1, ..Default::default()
360 }
361 }
362
363 #[test]
364 fn test_brent_root_find_quadratic() {
365 let f = |x: f64| x * x - 4.0;
367 let result = brent_root_find(f, 1.0, 3.0, 1e-6, 100).unwrap();
368
369 assert!(result.success);
370 assert!((result.angle_rad - 2.0).abs() < 1e-6);
371 assert!(result.iterations_used > 0);
372 assert!(result.final_error < 1e-6);
373 }
374
375 #[test]
376 fn test_brent_root_find_linear() {
377 let f = |x: f64| 2.0 * x - 6.0;
379 let result = brent_root_find(f, 0.0, 5.0, 1e-6, 100).unwrap();
380
381 assert!(result.success);
382 assert!((result.angle_rad - 3.0).abs() < 1e-6);
383 }
384
385 #[test]
386 fn test_brent_root_find_no_bracket() {
387 let f = |x: f64| x * x + 1.0; let result = brent_root_find(f, 1.0, 3.0, 1e-6, 100);
390
391 assert!(result.is_err());
392 assert!(result.unwrap_err().contains("Root not bracketed"));
393 }
394
395 #[test]
396 fn test_adjusted_muzzle_velocity_no_sensitivity() {
397 let inputs = create_test_inputs();
398
399 let result = adjusted_muzzle_velocity(&inputs);
400 assert_eq!(result, 823.0); }
402
403 #[test]
404 fn test_adjusted_muzzle_velocity_with_sensitivity() {
405 let mut inputs = create_test_inputs();
406 inputs.use_powder_sensitivity = true;
407 inputs.powder_temp_sensitivity = 1.0; inputs.temperature = 85.0; inputs.powder_temp = 70.0; let result = adjusted_muzzle_velocity(&inputs);
412 assert!((result - 1646.0).abs() < 1e-6);
414 }
415
416 #[test]
417 fn test_quick_drop_estimate() {
418 let drop = quick_drop_estimate(2700.0, 500.0, 168.0, 0.5);
419
420 assert!(drop > 0.0);
422 assert!(drop < 50.0); let drop_high_bc = quick_drop_estimate(2700.0, 500.0, 168.0, 0.8);
426 assert!(drop_high_bc < drop);
427 }
428
429 #[test]
430 fn test_zero_angle_bounds() {
431 let lower = -10.0 * DEGREES_TO_RADIANS;
433 let upper = 10.0 * DEGREES_TO_RADIANS;
434
435 assert!(lower < 0.0);
436 assert!(upper > 0.0);
437 assert!((upper - lower).abs() > 0.1); }
439
440 #[test]
441 fn test_brent_root_find_near_zero_function_values() {
442 let f = |x: f64| (x - 1.0) * 1e-10; let result = brent_root_find(f, 0.0, 2.0, 1e-12, 100).unwrap();
447
448 assert!(result.success || result.iterations_used > 0);
451 assert!((result.angle_rad - 1.0).abs() < 0.1);
453 }
454
455 #[test]
456 fn test_brent_root_find_steep_function() {
457 let f = |x: f64| (x - 0.5).powi(3) * 1e6;
459 let result = brent_root_find(f, 0.0, 1.0, 1e-9, 100).unwrap();
460
461 assert!(result.success);
462 assert!((result.angle_rad - 0.5).abs() < 1e-6);
463 }
464
465 #[test]
466 fn test_brent_root_find_oscillating_convergence() {
467 let f = |x: f64| x.sin() - 0.5;
470 let result = brent_root_find(f, 0.0, 1.0, 1e-10, 100).unwrap();
471
472 assert!(result.success);
473 assert!((result.angle_rad - 0.5235987755982989).abs() < 1e-6);
475 }
476
477 #[test]
478 fn test_brent_root_find_flat_region() {
479 let f = |x: f64| (x - 2.0).powi(5);
482 let result = brent_root_find(f, 1.0, 3.0, 1e-8, 100).unwrap();
483
484 assert!(result.success);
485 assert!((result.angle_rad - 2.0).abs() < 1e-4);
486 }
487}