use crate::Vector3;
use serde::{Deserialize, Serialize};
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct TrajectoryPoint {
pub x: f64,
pub y: f64,
pub z: f64,
pub vx: f64,
pub vy: f64,
pub vz: f64,
pub t: f64, }
impl TrajectoryPoint {
fn new(position: Vector3, velocity: Vector3, time: f64) -> Self {
TrajectoryPoint {
x: position.x,
y: position.y,
z: position.z,
vx: velocity.x,
vy: velocity.y,
vz: velocity.z,
t: time,
}
}
pub fn position(&self) -> Vector3 {
Vector3::new(self.x, self.y, self.z)
}
pub fn velocity(&self) -> Vector3 {
Vector3::new(self.vx, self.vy, self.vz)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trajectory {
pub points: Vec<TrajectoryPoint>,
}
impl Trajectory {
fn new() -> Self {
Trajectory { points: Vec::new() }
}
}
const DELTA_TIME: f64 = 1.0 / 500.0; const BALL_DIAMETER: f64 = 0.04267; const BALL_MASS: f64 = 0.04593; const GRAVITY: f64 = 9.81;
fn kinematic_viscosity_of_air(temp_c: f64) -> f64 {
let t = temp_c + 273.15;
1.458e-6 * t.powf(1.5) / (t + 110.4)
}
fn pressure_hpa_at_elevation(elevation_m: f64) -> f64 {
let p0 = 1013.25; let t0 = 288.15; let g = 9.80665; let l = 0.0065; let r = 8.3144598; let m = 0.0289644;
p0 * (1.0 - (l * elevation_m) / t0).powf((g * m) / (r * l))
}
fn air_density_humid(p_hpa: f64, temp_c: f64, rel_humidity: f64) -> f64 {
let t_k = temp_c + 273.15;
let e_s = 6.112 * f64::exp((17.67 * temp_c) / (temp_c + 243.5));
let e = rel_humidity * e_s; let p_dry = (p_hpa - e) * 100.0;
let p_vapor = e * 100.0;
let r_dry = 287.058;
let r_vapor = 461.495;
(p_dry / (r_dry * t_k)) + (p_vapor / (r_vapor * t_k))
}
fn lift_coefficient(omega: f64, speed: f64) -> f64 {
let spin_number = omega * BALL_DIAMETER / (2.0 * speed);
if spin_number > 0.306153 {
(0.33 - 0.23) * (spin_number - 0.20) / (0.40 - 0.20) + 0.23
} else {
-3.25 * spin_number.powi(2) + 1.99 * spin_number
}
}
fn drag_coefficient(omega: f64, speed: f64, temp_c: f64) -> f64 {
let spin_number = omega * BALL_DIAMETER / (2.0 * speed);
let reynolds = speed * BALL_DIAMETER / kinematic_viscosity_of_air(temp_c);
let mut spin_modifier = -0.255;
if spin_number < 0.15 {
spin_modifier += (0.28 - 0.255) * (spin_number - 0.00) / (0.15 - 0.00) + 0.255;
} else if spin_number < 0.25 {
spin_modifier += (0.33 - 0.28) * (spin_number - 0.15) / (0.25 - 0.15) + 0.28;
} else if spin_number <= 0.35 {
spin_modifier += (0.355 - 0.33) * (spin_number - 0.25) / (0.35 - 0.25) + 0.33;
} else {
spin_modifier += (0.38 - 0.355) * (spin_number - 0.35) / (0.45 - 0.35) + 0.355;
}
if reynolds < 38000.0 {
0.50 + spin_modifier
} else if reynolds < 45000.0 {
(0.35 - 0.48) * (reynolds - 38000.0) / (45000.0 - 38000.0) + 0.48 + spin_modifier
} else if reynolds < 50000.0 {
(0.30 - 0.35) * (reynolds - 45000.0) / (50000.0 - 45000.0) + 0.35 + spin_modifier
} else if reynolds < 60000.0 {
(0.24 + 0.8 * spin_modifier - 0.30 + spin_modifier) * (reynolds - 50000.0)
/ (60000.0 - 50000.0)
+ 0.30
+ spin_modifier
} else if reynolds < 240000.0 {
(0.26 - 0.24 + 0.8 * spin_modifier) * (reynolds - 60000.0) / (240000.0 - 60000.0)
+ 0.24
+ 0.8 * spin_modifier
} else if reynolds <= 4000000.0 {
(0.30 - 0.26) * (reynolds - 240000.0) / (4000000.0 - 240000.0) + 0.26
} else {
(0.30 - 0.26) * (reynolds - 240000.0) / (4000000.0 - 240000.0) + 0.26
}
}
pub fn calculate_trajectory(
ball_speed_mps: f64,
v_launch_deg: f64,
h_launch_deg: f64,
backspin_rpm: f64,
sidespin_rpm: f64,
elevation_m: f64,
temperature_k: f64,
humidity_percent: f64,
pressure_pa: Option<f64>,
) -> Trajectory {
let v_launch_rad = v_launch_deg * PI / 180.0;
let h_launch_rad = h_launch_deg * PI / 180.0;
let backspin_rad_s = backspin_rpm * 0.10472;
let sidespin_rad_s = sidespin_rpm * 0.10472;
let total_spin_rad_s = (backspin_rad_s.powi(2) + sidespin_rad_s.powi(2)).sqrt();
let spin_axis = sidespin_rad_s.atan2(backspin_rad_s);
let mut trajectory = Trajectory::new();
let mut position = Vector3::new(0.0, 0.0, 0.0);
let v_horizontal = ball_speed_mps * v_launch_rad.cos();
let mut velocity = Vector3::new(
v_horizontal * h_launch_rad.cos(), v_horizontal * h_launch_rad.sin(), ball_speed_mps * v_launch_rad.sin(), );
let mut time = 0.0;
trajectory
.points
.push(TrajectoryPoint::new(position, velocity, time));
let mut total_spin = total_spin_rad_s;
const SPIN_DECAY_RATE: f64 = 0.04;
let mut iteration = 0;
const MAX_ITERATIONS: i32 = 10000;
let p_hpa = if let Some(pressure_pascals) = pressure_pa {
pressure_pascals / 100.0 } else {
pressure_hpa_at_elevation(elevation_m)
};
let temperature_c = temperature_k - 273.15;
let humidity_fraction = humidity_percent / 100.0;
let air_density = air_density_humid(p_hpa, temperature_c, humidity_fraction);
let cross_sectional_area = PI * (BALL_DIAMETER / 2.0).powi(2);
while (position.z >= 0.0) && iteration < MAX_ITERATIONS {
let current_speed = velocity.magnitude();
let lift_coeff = lift_coefficient(total_spin, current_speed);
let drag_coeff = drag_coefficient(total_spin, current_speed, temperature_c);
let dynamic_pressure = 0.5 * air_density * cross_sectional_area * current_speed.powi(2);
let drag_force_mag = dynamic_pressure * drag_coeff;
let vector_drag_force = Vector3::new(
-drag_force_mag * (velocity.x / current_speed),
-drag_force_mag * (velocity.y / current_speed),
-drag_force_mag * (velocity.z / current_speed),
);
let v_hat = velocity.normalize();
let spin_axis_vec = Vector3::new(
0.0,
-spin_axis.cos(), spin_axis.sin(), )
.normalize();
let lift_dir = spin_axis_vec.cross(&v_hat).normalize();
let lift_force_mag =
0.5 * air_density * current_speed.powi(2) * cross_sectional_area * lift_coeff;
let vector_lift_force = Vector3::new(
lift_dir.x * lift_force_mag,
lift_dir.y * lift_force_mag,
lift_dir.z * lift_force_mag,
);
let total_force = vector_drag_force.add(&vector_lift_force);
let accel = Vector3::new(
total_force.x / BALL_MASS,
total_force.y / BALL_MASS,
total_force.z / BALL_MASS - GRAVITY, );
let new_velocity = Vector3::new(
velocity.x + accel.x * DELTA_TIME,
velocity.y + accel.y * DELTA_TIME,
velocity.z + accel.z * DELTA_TIME,
);
let avg_velocity = Vector3::new(
(velocity.x + new_velocity.x) / 2.0,
(velocity.y + new_velocity.y) / 2.0,
(velocity.z + new_velocity.z) / 2.0,
);
position.x += avg_velocity.x * DELTA_TIME;
position.y += avg_velocity.y * DELTA_TIME;
position.z += avg_velocity.z * DELTA_TIME;
velocity = new_velocity;
total_spin *= (-SPIN_DECAY_RATE * DELTA_TIME).exp();
time += DELTA_TIME;
trajectory
.points
.push(TrajectoryPoint::new(position, velocity, time));
iteration += 1;
}
if iteration >= MAX_ITERATIONS {
trajectory.points.clear();
trajectory.points.push(TrajectoryPoint::new(
Vector3::new(f64::NAN, f64::NAN, f64::NAN),
Vector3::new(f64::NAN, f64::NAN, f64::NAN),
0.0,
));
}
trajectory
}