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() }
}
}
pub const NATIVE_RATE_HZ: f64 = 500.0;
const DELTA_TIME: f64 = 1.0 / NATIVE_RATE_HZ;
const BALL_DIAMETER: f64 = 0.04267; const BALL_MASS: f64 = 0.04593; const GRAVITY: f64 = 9.81;
fn dynamic_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(spin_param: f64) -> f64 {
const S_TRANSITION: f64 = 0.25;
const CL_TRANSITION: f64 = 0.2941; const SLOPE_TRANSITION: f64 = 0.3625;
if spin_param <= S_TRANSITION {
1.99 * spin_param - 3.255 * spin_param * spin_param
} else {
CL_TRANSITION + SLOPE_TRANSITION * (spin_param - S_TRANSITION)
}
}
fn drag_coefficient(re: f64, spin_param: f64) -> f64 {
const CD_SUBCRIT: f64 = 0.5; const CD_SUPERCRIT: f64 = 0.225; const RE_CRIT: f64 = 80_000.0; const K: f64 = 0.0002;
let stationary_drag = CD_SUPERCRIT + (CD_SUBCRIT - CD_SUPERCRIT) / (1.0 + f64::exp(K * (re - RE_CRIT)));
let spin_modifier = 0.18 * (1.0 - f64::exp(-3.0 * spin_param * spin_param));
stationary_drag + spin_modifier
}
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 K_SPIN_DECAY: f64 = 0.001;
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);
let kinematic_viscosity = dynamic_viscosity_of_air(temperature_c) / air_density;
while (position.z >= 0.0) && iteration < MAX_ITERATIONS {
let current_speed = velocity.magnitude();
let re = current_speed * BALL_DIAMETER / kinematic_viscosity;
let spin_param = total_spin * (BALL_DIAMETER / 2.0) / current_speed;
let lift_coeff = lift_coefficient(spin_param);
let drag_coeff = drag_coefficient(re, spin_param);
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 *= (-K_SPIN_DECAY * current_speed * 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
}