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 {
188 match trajectory_func(inputs, look_angle_rad) {
190 Ok(bullet_height) => bullet_height - vert,
191 Err(_) => -999.0, }
193 };
194
195 let lower_bound = -10.0 * DEGREES_TO_RADIANS;
198 let upper_bound = 10.0 * DEGREES_TO_RADIANS;
199
200 match brent_root_find(height_diff, lower_bound, upper_bound, 1e-6, 100) {
202 Ok(result) if result.success => Ok(result),
203 _ => {
204 let wider_lower = -45.0 * DEGREES_TO_RADIANS;
206 let wider_upper = 45.0 * DEGREES_TO_RADIANS;
207
208 match brent_root_find(height_diff, wider_lower, wider_upper, 1e-5, 150) {
209 Ok(result) if result.success => Ok(result),
210 Ok(result) => {
211 Ok(AngleResult {
213 angle_rad: result.angle_rad,
214 iterations_used: result.iterations_used,
215 final_error: result.final_error,
216 success: false,
217 })
218 },
219 Err(_) => {
220 Ok(AngleResult {
222 angle_rad: 0.0,
223 iterations_used: 0,
224 final_error: f64::INFINITY,
225 success: false,
226 })
227 }
228 }
229 }
230 }
231}
232
233pub fn solve_muzzle_angle(
235 inputs: &InternalBallisticInputs,
236 zero_distance_los_m: f64,
237 trajectory_func: impl Fn(&InternalBallisticInputs) -> Result<f64, String> + Copy, angle_lower_deg: f64,
239 angle_upper_deg: f64,
240 rtol: f64,
241) -> Result<AngleResult, String> {
242 if angle_lower_deg >= angle_upper_deg {
243 return Err("angle_lower_deg must be less than angle_upper_deg".to_string());
244 }
245
246 let lower = angle_lower_deg * DEGREES_TO_RADIANS;
247 let mut upper = angle_upper_deg * DEGREES_TO_RADIANS;
248
249 let vertical_error = |angle_rad: f64| -> f64 {
251 let mut candidate = inputs.clone();
253 candidate.muzzle_angle = angle_rad * RADIANS_TO_DEGREES;
254 candidate.target_distance = zero_distance_los_m / YARDS_TO_METERS; trajectory_func(&candidate).unwrap_or(1e6)
257 };
258
259 let f_lower = vertical_error(lower);
261 if f_lower.abs() < 1e-9 {
262 return Ok(AngleResult {
263 angle_rad: lower,
264 iterations_used: 1,
265 final_error: f_lower.abs(),
266 success: true,
267 });
268 }
269
270 let f_upper = vertical_error(upper);
271 if f_upper.abs() < 1e-9 {
272 return Ok(AngleResult {
273 angle_rad: upper,
274 iterations_used: 1,
275 final_error: f_upper.abs(),
276 success: true,
277 });
278 }
279
280 if f_lower * f_upper > 0.0 {
282 let step = 5.0 * DEGREES_TO_RADIANS;
283 let max_angle = 45.0 * DEGREES_TO_RADIANS;
284 let mut current = upper;
285 let mut f_current = f_upper;
286
287 while current < max_angle && f_lower * f_current > 0.0 {
288 current += step;
289 f_current = vertical_error(current);
290 }
291
292 if f_lower * f_current > 0.0 {
293 return Err("Unable to bracket zero; widen angle bounds or check inputs".to_string());
294 }
295
296 upper = current;
297 }
298
299 let range = (upper - lower).abs();
301 let tolerance = if range > f64::EPSILON {
302 rtol * range
303 } else {
304 rtol * 1e-12 };
306 brent_root_find(vertical_error, lower, upper, tolerance, ZERO_FINDING_MAX_ITER)
307}
308
309pub fn quick_drop_estimate(
311 muzzle_velocity_fps: f64,
312 distance_yards: f64,
313 _bullet_mass_grains: f64,
314 bc: f64,
315) -> f64 {
316 let mv_mps = muzzle_velocity_fps * FPS_TO_MPS;
317 let distance_m = distance_yards * YARDS_TO_METERS;
318
319 if mv_mps <= 0.0 || distance_m <= 0.0 {
321 return 0.0; }
323
324 let time_of_flight = distance_m / mv_mps;
325 let gravity = 9.80665;
326
327 let bc_safe = bc.max(0.1);
329 let drag_factor = 1.0 / bc_safe;
330 let velocity_loss = drag_factor * time_of_flight * 0.1;
331 let effective_velocity = mv_mps * (1.0 - velocity_loss).max(0.1); let adjusted_time = distance_m / effective_velocity;
333
334 0.5 * gravity * adjusted_time * adjusted_time
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341
342 fn create_test_inputs() -> InternalBallisticInputs {
343 InternalBallisticInputs {
344 muzzle_velocity: 823.0, bc_value: 0.5,
346 bullet_mass: 0.0109, bullet_diameter: 0.00782, target_distance: 500.0,
349 temperature: 21.1, powder_temp: 21.1, ..Default::default()
352 }
353 }
354
355 #[test]
356 fn test_brent_root_find_quadratic() {
357 let f = |x: f64| x * x - 4.0;
359 let result = brent_root_find(f, 1.0, 3.0, 1e-6, 100).unwrap();
360
361 assert!(result.success);
362 assert!((result.angle_rad - 2.0).abs() < 1e-6);
363 assert!(result.iterations_used > 0);
364 assert!(result.final_error < 1e-6);
365 }
366
367 #[test]
368 fn test_brent_root_find_linear() {
369 let f = |x: f64| 2.0 * x - 6.0;
371 let result = brent_root_find(f, 0.0, 5.0, 1e-6, 100).unwrap();
372
373 assert!(result.success);
374 assert!((result.angle_rad - 3.0).abs() < 1e-6);
375 }
376
377 #[test]
378 fn test_brent_root_find_no_bracket() {
379 let f = |x: f64| x * x + 1.0; let result = brent_root_find(f, 1.0, 3.0, 1e-6, 100);
382
383 assert!(result.is_err());
384 assert!(result.unwrap_err().contains("Root not bracketed"));
385 }
386
387 #[test]
388 fn test_adjusted_muzzle_velocity_no_sensitivity() {
389 let inputs = create_test_inputs();
390
391 let result = adjusted_muzzle_velocity(&inputs);
392 assert_eq!(result, 823.0); }
394
395 #[test]
396 fn test_adjusted_muzzle_velocity_with_sensitivity() {
397 let mut inputs = create_test_inputs();
398 inputs.use_powder_sensitivity = true;
399 inputs.powder_temp_sensitivity = 1.0; inputs.temperature = 85.0; inputs.powder_temp = 70.0; let result = adjusted_muzzle_velocity(&inputs);
404 assert!((result - 1646.0).abs() < 1e-6);
406 }
407
408 #[test]
409 fn test_quick_drop_estimate() {
410 let drop = quick_drop_estimate(2700.0, 500.0, 168.0, 0.5);
411
412 assert!(drop > 0.0);
414 assert!(drop < 50.0); let drop_high_bc = quick_drop_estimate(2700.0, 500.0, 168.0, 0.8);
418 assert!(drop_high_bc < drop);
419 }
420
421 #[test]
422 fn test_zero_angle_bounds() {
423 let lower = -10.0 * DEGREES_TO_RADIANS;
425 let upper = 10.0 * DEGREES_TO_RADIANS;
426
427 assert!(lower < 0.0);
428 assert!(upper > 0.0);
429 assert!((upper - lower).abs() > 0.1); }
431}