ballistics_engine/
aerodynamic_jump.rs

1use crate::constants::{AIR_DENSITY_SEA_LEVEL, SPEED_OF_SOUND_MPS};
2
3/// Components of aerodynamic jump calculation
4#[derive(Debug, Clone, Copy)]
5pub struct AerodynamicJumpComponents {
6    pub vertical_jump_moa: f64,    // Vertical displacement in MOA at 100 yards
7    pub horizontal_jump_moa: f64,  // Horizontal displacement in MOA at 100 yards
8    pub jump_angle_rad: f64,       // Total angular displacement in radians
9    pub magnus_component_moa: f64, // Magnus effect contribution
10    pub yaw_component_moa: f64,    // Initial yaw contribution
11    pub stabilization_factor: f64, // How quickly projectile stabilizes (0-1)
12}
13
14/// Calculate aerodynamic jump for a spinning projectile.
15///
16/// Aerodynamic jump is the displacement of the projectile's trajectory
17/// as it transitions from constrained motion in the barrel to free flight.
18pub fn calculate_aerodynamic_jump(
19    muzzle_velocity_mps: f64,
20    spin_rate_rad_s: f64,
21    crosswind_mps: f64,
22    caliber_m: f64,
23    mass_kg: f64,
24    barrel_length_m: f64,
25    twist_rate_calibers: f64,
26    is_right_twist: bool,
27    initial_yaw_rad: f64,
28    air_density_kg_m3: f64,
29) -> AerodynamicJumpComponents {
30    if muzzle_velocity_mps <= 0.0 || caliber_m <= 0.0 {
31        return AerodynamicJumpComponents {
32            vertical_jump_moa: 0.0,
33            horizontal_jump_moa: 0.0,
34            jump_angle_rad: 0.0,
35            magnus_component_moa: 0.0,
36            yaw_component_moa: 0.0,
37            stabilization_factor: 0.0,
38        };
39    }
40
41    // Calculate Magnus force coefficient
42    let mach = muzzle_velocity_mps / SPEED_OF_SOUND_MPS;
43    let magnus_coeff = if mach < 0.8 {
44        0.25
45    } else if mach < 1.2 {
46        0.15 // Reduced in transonic
47    } else {
48        0.20
49    };
50
51    // Spin parameter (non-dimensional)
52    let spin_param = (spin_rate_rad_s * caliber_m / 2.0) / muzzle_velocity_mps;
53
54    // Effective yaw angle during muzzle exit
55    let crosswind_yaw = if crosswind_mps != 0.0 {
56        (crosswind_mps / muzzle_velocity_mps).atan()
57    } else {
58        0.0
59    };
60
61    let total_yaw_rad = crosswind_yaw + initial_yaw_rad;
62
63    // Magnus force during barrel exit
64    let area = std::f64::consts::PI * (caliber_m / 2.0).powi(2);
65    let magnus_force = 0.5
66        * air_density_kg_m3
67        * muzzle_velocity_mps.powi(2)
68        * area
69        * magnus_coeff
70        * spin_param
71        * total_yaw_rad.sin();
72
73    // Time for projectile to clear muzzle
74    let exit_time = 2.0 * barrel_length_m / muzzle_velocity_mps;
75
76    // Stabilization distance
77    let stabilization_calibers = 20.0 / (twist_rate_calibers / 10.0).sqrt();
78    let stabilization_distance = stabilization_calibers * caliber_m;
79    let stabilization_time = stabilization_distance / muzzle_velocity_mps;
80
81    // Total effective time
82    let effective_time = exit_time + stabilization_time;
83
84    // Calculate jump displacement
85    let vertical_sign = if is_right_twist {
86        crosswind_mps.signum()
87    } else {
88        -crosswind_mps.signum()
89    };
90
91    // Magnus acceleration
92    let magnus_accel = magnus_force / mass_kg;
93
94    // Enhanced calculation accounting for barrel exit dynamics
95    let lever_factor = (barrel_length_m / caliber_m) * 0.1;
96    let magnus_enhancement = 50.0; // Calibrated to match empirical data
97
98    // Vertical displacement
99    let mut vertical_jump_m = magnus_enhancement
100        * lever_factor
101        * vertical_sign
102        * magnus_accel.abs()
103        * effective_time.powi(2);
104
105    // Add yaw-induced component
106    if total_yaw_rad != 0.0 {
107        let yaw_contribution = total_yaw_rad.abs() * barrel_length_m * 0.5;
108        vertical_jump_m += vertical_sign * yaw_contribution;
109    }
110
111    // Horizontal component (smaller effect)
112    let horizontal_jump_m = 0.25 * vertical_jump_m * (2.0 * total_yaw_rad).sin();
113
114    // Convert to MOA at 100 yards
115    const YARDS_TO_M: f64 = 0.9144;
116    const MOA_PER_RADIAN: f64 = 3437.7467707849; // 1 / 0.0002908882
117
118    let range_100y = 100.0 * YARDS_TO_M;
119    let vertical_angle_rad = vertical_jump_m / range_100y;
120    let horizontal_angle_rad = horizontal_jump_m / range_100y;
121
122    let vertical_jump_moa = vertical_angle_rad * MOA_PER_RADIAN;
123    let horizontal_jump_moa = horizontal_angle_rad * MOA_PER_RADIAN;
124
125    // Total jump angle
126    let total_jump_rad = (vertical_angle_rad.powi(2) + horizontal_angle_rad.powi(2)).sqrt();
127
128    // Component breakdown
129    let magnus_component_moa = vertical_jump_moa.abs() * 0.8;
130    let yaw_component_moa = vertical_jump_moa.abs() * 0.2;
131
132    // Stabilization factor
133    let caliber_in = caliber_m / 0.0254;
134    let sg_approx = 30.0 * mass_kg * 15.432 / (twist_rate_calibers.powi(2) * caliber_in.powi(3));
135    let stabilization_factor = (sg_approx / 1.5).min(1.0);
136
137    AerodynamicJumpComponents {
138        vertical_jump_moa,
139        horizontal_jump_moa,
140        jump_angle_rad: total_jump_rad,
141        magnus_component_moa,
142        yaw_component_moa,
143        stabilization_factor,
144    }
145}
146
147/// Calculate sight corrections needed to compensate for aerodynamic jump
148pub fn calculate_sight_correction_for_jump(
149    jump_components: &AerodynamicJumpComponents,
150    zero_range_m: f64,
151    sight_height_m: f64,
152) -> (f64, f64) {
153    // Range factor
154    let range_factor = 91.44 / zero_range_m; // 100 yards / zero range
155
156    // Sight corrections (opposite of jump direction)
157    let mut vertical_correction = -jump_components.vertical_jump_moa * range_factor;
158    let horizontal_correction = -jump_components.horizontal_jump_moa * range_factor;
159
160    // Account for sight height
161    let sight_factor = 1.0 + (sight_height_m / 0.05);
162    vertical_correction *= sight_factor;
163
164    (vertical_correction, horizontal_correction)
165}
166
167/// Calculate sensitivity to crosswind for aerodynamic jump (MOA per mph)
168pub fn calculate_crosswind_jump_sensitivity(
169    muzzle_velocity_mps: f64,
170    spin_rate_rad_s: f64,
171    caliber_m: f64,
172    mass_kg: f64,
173    twist_rate_calibers: f64,
174    is_right_twist: bool,
175) -> f64 {
176    const MPH_TO_MPS: f64 = 0.44704;
177    let crosswind_1mph = MPH_TO_MPS;
178
179    let jump = calculate_aerodynamic_jump(
180        muzzle_velocity_mps,
181        spin_rate_rad_s,
182        crosswind_1mph,
183        caliber_m,
184        mass_kg,
185        0.6, // Typical 24" barrel
186        twist_rate_calibers,
187        is_right_twist,
188        0.0, // No initial yaw
189        AIR_DENSITY_SEA_LEVEL,
190    );
191
192    jump.vertical_jump_moa.abs()
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_aerodynamic_jump_zero_conditions() {
201        // Test with no crosswind
202        let jump = calculate_aerodynamic_jump(
203            800.0,   // velocity
204            1000.0,  // spin rate
205            0.0,     // no crosswind
206            0.00762, // .30 cal
207            0.01134, // 175gr
208            0.6,     // barrel length
209            32.47,   // twist rate in calibers
210            true,    // right twist
211            0.0,     // no initial yaw
212            1.225,   // air density
213        );
214
215        assert_eq!(jump.vertical_jump_moa, 0.0);
216        assert!(jump.horizontal_jump_moa.abs() < 0.001);
217    }
218
219    #[test]
220    fn test_aerodynamic_jump_with_crosswind() {
221        // Test with 10 mph right crosswind
222        let jump = calculate_aerodynamic_jump(
223            800.0,   // velocity
224            17593.0, // spin rate for 1:10 twist
225            4.4704,  // 10 mph crosswind
226            0.00782, // .308 cal
227            0.01134, // 175gr
228            0.6096,  // 24" barrel
229            32.47,   // twist rate in calibers
230            true,    // right twist
231            0.0,     // no initial yaw
232            1.225,   // air density
233        );
234
235        // Right twist + right wind should give positive (upward) jump
236        assert!(jump.vertical_jump_moa > 0.0);
237        // Just check that we have some stabilization
238        assert!(jump.stabilization_factor > 0.0);
239    }
240
241    #[test]
242    fn test_opposite_twist_direction() {
243        let crosswind = 4.4704; // 10 mph
244
245        // Right twist
246        let jump_right = calculate_aerodynamic_jump(
247            800.0, 17593.0, crosswind, 0.00782, 0.01134, 0.6096, 32.47, true, 0.0, 1.225,
248        );
249
250        // Left twist
251        let jump_left = calculate_aerodynamic_jump(
252            800.0, 17593.0, crosswind, 0.00782, 0.01134, 0.6096, 32.47, false, 0.0, 1.225,
253        );
254
255        // Opposite twist should give opposite vertical jump
256        assert!((jump_right.vertical_jump_moa + jump_left.vertical_jump_moa).abs() < 0.001);
257    }
258
259    #[test]
260    fn test_sight_correction() {
261        let jump = AerodynamicJumpComponents {
262            vertical_jump_moa: 0.5,
263            horizontal_jump_moa: 0.1,
264            jump_angle_rad: 0.0001,
265            magnus_component_moa: 0.4,
266            yaw_component_moa: 0.1,
267            stabilization_factor: 0.9,
268        };
269
270        let (vert, horiz) = calculate_sight_correction_for_jump(
271            &jump, 274.32, // 300 yards
272            0.05,   // 2" sight height
273        );
274
275        // Corrections should be opposite of jump
276        assert!(vert < 0.0);
277        assert!(horiz < 0.0);
278    }
279
280    #[test]
281    fn test_crosswind_sensitivity() {
282        let sensitivity = calculate_crosswind_jump_sensitivity(
283            800.0,   // velocity
284            17593.0, // spin rate
285            0.00782, // caliber
286            0.01134, // mass
287            32.47,   // twist rate
288            true,    // right twist
289        );
290
291        // Should be positive and reasonable (typically 0.01-0.1 MOA/mph)
292        assert!(sensitivity > 0.0);
293        assert!(sensitivity < 0.5);
294    }
295}