const BALL_MASS: f64 = 0.04593; const CLUBHEAD_MASS: f64 = 0.200; const DRIVER_COR_LIMIT: f64 = 0.83; const MIN_EFFECTIVE_COR: f64 = 0.52;
#[derive(Clone, Copy)]
struct ImpactBand {
max_ball_speed_mps: f64,
base_cor: f64,
optimal_launch_deg: f64,
launch_tolerance_deg: f64,
optimal_spin_rpm: f64,
spin_tolerance_rpm: f64,
face_influence_ratio: f64,
spin_axis_gain: f64,
}
const IMPACT_BANDS: [ImpactBand; 4] = [
ImpactBand {
max_ball_speed_mps: 40.0, base_cor: 0.55,
optimal_launch_deg: 28.0,
launch_tolerance_deg: 15.0,
optimal_spin_rpm: 9000.0,
spin_tolerance_rpm: 4000.0,
face_influence_ratio: 0.65,
spin_axis_gain: 1.7,
},
ImpactBand {
max_ball_speed_mps: 50.0, base_cor: 0.66,
optimal_launch_deg: 20.0,
launch_tolerance_deg: 12.0,
optimal_spin_rpm: 7000.0,
spin_tolerance_rpm: 2500.0,
face_influence_ratio: 0.72,
spin_axis_gain: 2.1,
},
ImpactBand {
max_ball_speed_mps: 60.0, base_cor: 0.72,
optimal_launch_deg: 16.0,
launch_tolerance_deg: 10.0,
optimal_spin_rpm: 5000.0,
spin_tolerance_rpm: 2000.0,
face_influence_ratio: 0.78,
spin_axis_gain: 2.4,
},
ImpactBand {
max_ball_speed_mps: f64::INFINITY,
base_cor: DRIVER_COR_LIMIT,
optimal_launch_deg: 12.0,
launch_tolerance_deg: 8.0,
optimal_spin_rpm: 2500.0,
spin_tolerance_rpm: 1500.0,
face_influence_ratio: 0.85,
spin_axis_gain: 2.8,
},
];
fn band_for_ball_speed(ball_speed_mps: f64) -> ImpactBand {
for band in IMPACT_BANDS {
if ball_speed_mps <= band.max_ball_speed_mps {
return band;
}
}
IMPACT_BANDS[IMPACT_BANDS.len() - 1]
}
pub struct ClubFacePathEstimates {
pub club_path_degrees: f64,
pub club_face_to_target_degrees: f64,
pub club_face_to_path_degrees: f64,
}
pub fn estimate_club_face_path(
ball_speed_mps: f64,
horizontal_launch_angle_deg: f64,
spin_axis_degrees: f64,
) -> ClubFacePathEstimates {
let band = band_for_ball_speed(ball_speed_mps.max(5.0));
let face_to_path = spin_axis_degrees / band.spin_axis_gain;
let club_path = horizontal_launch_angle_deg - band.face_influence_ratio * face_to_path;
let club_face = club_path + face_to_path;
ClubFacePathEstimates {
club_path_degrees: club_path,
club_face_to_target_degrees: club_face,
club_face_to_path_degrees: face_to_path,
}
}
pub fn get_smash_factor(ball_speed_mps: f64, clubhead_speed_mps: f64) -> f64 {
if clubhead_speed_mps == 0.0 {
return f64::NAN;
}
ball_speed_mps / clubhead_speed_mps
}
pub fn estimate_clubhead_speed(
ball_speed_mps: f64,
vertical_launch_angle_deg: f64,
total_spin_rpm: f64,
) -> f64 {
let launch_angle = vertical_launch_angle_deg.clamp(-5.0, 70.0);
let spin_rpm = total_spin_rpm.max(0.0);
let band = band_for_ball_speed(ball_speed_mps.max(5.0));
let launch_deviation = (launch_angle - band.optimal_launch_deg).abs();
let normalized_launch = (launch_deviation / band.launch_tolerance_deg).min(3.0);
let launch_penalty = normalized_launch.powf(1.25) * 0.06;
let spin_tolerance = band.spin_tolerance_rpm.max(1.0);
let normalized_spin = if spin_rpm >= band.optimal_spin_rpm {
((spin_rpm - band.optimal_spin_rpm) / spin_tolerance).min(3.0)
} else {
((band.optimal_spin_rpm - spin_rpm) / (spin_tolerance * 1.5)).min(3.0)
};
let spin_penalty = normalized_spin.powf(1.15) * 0.08;
let knuckle_penalty = if spin_rpm < 1200.0 {
((1200.0 - spin_rpm) / 1200.0).powf(1.3) * 0.05
} else {
0.0
};
let mut effective_cor = band.base_cor - launch_penalty - spin_penalty - knuckle_penalty;
effective_cor = effective_cor.clamp(MIN_EFFECTIVE_COR, DRIVER_COR_LIMIT);
let mass_ratio = BALL_MASS / CLUBHEAD_MASS;
let smash_factor = (1.0 + effective_cor) / (1.0 + mass_ratio);
ball_speed_mps / smash_factor
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clubhead_speed_estimation_driver() {
let ball_speed = 71.5;
let launch_angle = 11.5;
let spin = 2500.0;
let club_speed = estimate_clubhead_speed(ball_speed, launch_angle, spin);
assert!(
club_speed > 45.0 && club_speed < 50.0,
"Clubhead speed {} m/s ({} mph) should be reasonable for driver",
club_speed,
club_speed * 2.23694
);
assert!(
ball_speed > club_speed,
"Ball speed should exceed clubhead speed"
);
}
#[test]
fn test_clubhead_speed_estimation_iron() {
let ball_speed = 53.6;
let launch_angle = 16.0;
let spin = 7000.0;
let club_speed = estimate_clubhead_speed(ball_speed, launch_angle, spin);
assert!(
club_speed > 36.0 && club_speed < 43.0,
"Clubhead speed {} m/s ({} mph) should be reasonable for iron",
club_speed,
club_speed * 2.23694
);
}
#[test]
fn test_clubhead_speed_estimation_wedge() {
let ball_speed = 40.0;
let launch_angle = 32.0;
let spin = 9000.0;
let club_speed = estimate_clubhead_speed(ball_speed, launch_angle, spin);
assert!(
club_speed > 30.0 && club_speed < 35.5,
"Clubhead speed {} m/s ({} mph) should be reasonable for wedge",
club_speed,
club_speed * 2.23694
);
}
#[test]
fn test_spin_effect_on_clubhead_estimate() {
let ball_speed = 60.0;
let launch_angle = 12.0;
let optimal_spin_club_speed = estimate_clubhead_speed(ball_speed, launch_angle, 5000.0);
let low_spin_club_speed = estimate_clubhead_speed(ball_speed, launch_angle, 2000.0);
let high_spin_club_speed = estimate_clubhead_speed(ball_speed, launch_angle, 8000.0);
assert!(
low_spin_club_speed > optimal_spin_club_speed,
"Low spin should require higher club speed than optimal spin: {} vs {}",
low_spin_club_speed,
optimal_spin_club_speed
);
assert!(
high_spin_club_speed > optimal_spin_club_speed,
"High spin should require higher club speed than optimal spin: {} vs {}",
high_spin_club_speed,
optimal_spin_club_speed
);
}
#[test]
fn test_face_path_estimation_driver_cut() {
let estimates = estimate_club_face_path(70.0, -2.0, 15.0);
assert!(
estimates.club_face_to_path_degrees > 4.0 && estimates.club_face_to_path_degrees < 8.0
);
assert!(
estimates.club_face_to_target_degrees > -2.0
&& estimates.club_face_to_target_degrees < 1.0
);
assert!(estimates.club_path_degrees < -4.5);
}
#[test]
fn test_face_path_estimation_wedge() {
let estimates = estimate_club_face_path(35.0, 3.0, -6.0);
assert!(estimates.club_face_to_path_degrees < -2.0);
assert!(
estimates.club_face_to_target_degrees > 1.0
&& estimates.club_face_to_target_degrees < 5.0
);
assert!(estimates.club_path_degrees > 2.0);
}
#[test]
fn test_smash_factor_driver() {
let ball_speed = 71.5; let club_speed = 47.8; let smash = get_smash_factor(ball_speed, club_speed);
assert!((smash - 1.50).abs() < 0.01, "Smash factor should be ~1.50");
}
#[test]
fn test_smash_factor_iron() {
let ball_speed = 53.6; let club_speed = 38.9; let smash = get_smash_factor(ball_speed, club_speed);
assert!((smash - 1.38).abs() < 0.01, "Smash factor should be ~1.38");
}
#[test]
fn test_smash_factor_zero_clubhead_speed() {
let smash = get_smash_factor(70.0, 0.0);
assert!(
smash.is_nan(),
"Smash factor should be NaN for zero clubhead speed"
);
}
}