use std::f64::consts::PI;
use super::error::{PhysicsError, PhysicsResult};
#[derive(Debug, Clone, PartialEq)]
pub struct ProjectileResult {
pub max_height: f64,
pub range: f64,
pub time_of_flight: f64,
pub vx: f64,
pub vy0: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OscillatorResult {
pub angular_frequency: f64,
pub period: f64,
pub frequency: f64,
}
pub fn kinetic_energy(mass: f64, velocity: f64) -> PhysicsResult<f64> {
if mass <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "mass",
reason: format!("mass must be positive, got {mass}"),
});
}
Ok(0.5 * mass * velocity * velocity)
}
pub fn potential_energy_gravity(mass: f64, height: f64, g: f64) -> PhysicsResult<f64> {
if mass <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "mass",
reason: format!("mass must be positive, got {mass}"),
});
}
if g <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "g",
reason: format!("gravitational acceleration must be positive, got {g}"),
});
}
Ok(mass * g * height)
}
pub fn momentum(mass: f64, velocity: f64) -> PhysicsResult<f64> {
if mass <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "mass",
reason: format!("mass must be positive, got {mass}"),
});
}
Ok(mass * velocity)
}
pub fn angular_momentum(mass: f64, velocity: f64, radius: f64) -> PhysicsResult<f64> {
if mass <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "mass",
reason: format!("mass must be positive, got {mass}"),
});
}
if radius <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "radius",
reason: format!("radius must be positive, got {radius}"),
});
}
Ok(mass * velocity * radius)
}
pub fn centripetal_acceleration(velocity: f64, radius: f64) -> PhysicsResult<f64> {
if radius <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "radius",
reason: format!("radius must be positive, got {radius}"),
});
}
Ok(velocity * velocity / radius)
}
pub fn projectile_motion(v0: f64, angle_deg: f64, g: f64) -> PhysicsResult<ProjectileResult> {
if v0 < 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "v0",
reason: format!("initial speed must be non-negative, got {v0}"),
});
}
if g <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "g",
reason: format!("gravitational acceleration must be positive, got {g}"),
});
}
if !(0.0..=90.0).contains(&angle_deg) {
return Err(PhysicsError::DomainError(format!(
"launch angle must be in [0, 90] degrees, got {angle_deg}"
)));
}
let angle_rad = angle_deg * PI / 180.0;
let vx = v0 * angle_rad.cos();
let vy0 = v0 * angle_rad.sin();
let time_of_flight = 2.0 * vy0 / g;
let max_height = vy0 * vy0 / (2.0 * g);
let range = vx * time_of_flight;
Ok(ProjectileResult {
max_height,
range,
time_of_flight,
vx,
vy0,
})
}
pub fn projectile_position(v0: f64, angle_deg: f64, g: f64, t: f64) -> PhysicsResult<(f64, f64)> {
if t < 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "t",
reason: format!("time must be non-negative, got {t}"),
});
}
let proj = projectile_motion(v0, angle_deg, g)?;
let x = proj.vx * t;
let y = proj.vy0 * t - 0.5 * g * t * t;
Ok((x, y))
}
pub fn simple_harmonic_oscillator(
mass: f64,
spring_constant: f64,
) -> PhysicsResult<OscillatorResult> {
if mass <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "mass",
reason: format!("mass must be positive, got {mass}"),
});
}
if spring_constant <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "spring_constant",
reason: format!("spring constant must be positive, got {spring_constant}"),
});
}
let angular_frequency = (spring_constant / mass).sqrt();
let period = 2.0 * PI / angular_frequency;
let frequency = angular_frequency / (2.0 * PI);
Ok(OscillatorResult {
angular_frequency,
period,
frequency,
})
}
pub fn sho_displacement(
amplitude: f64,
angular_freq: f64,
time: f64,
phase: f64,
) -> PhysicsResult<f64> {
if amplitude < 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "amplitude",
reason: format!("amplitude must be non-negative, got {amplitude}"),
});
}
if angular_freq <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "angular_freq",
reason: format!("angular frequency must be positive, got {angular_freq}"),
});
}
if time < 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "time",
reason: format!("time must be non-negative, got {time}"),
});
}
Ok(amplitude * (angular_freq * time + phase).cos())
}
pub fn gravitational_force(m1: f64, m2: f64, distance: f64) -> PhysicsResult<f64> {
use crate::constants::physical::GRAVITATIONAL_CONSTANT;
if m1 <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "m1",
reason: format!("mass m1 must be positive, got {m1}"),
});
}
if m2 <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "m2",
reason: format!("mass m2 must be positive, got {m2}"),
});
}
if distance <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "distance",
reason: format!("distance must be positive, got {distance}"),
});
}
Ok(GRAVITATIONAL_CONSTANT * m1 * m2 / (distance * distance))
}
pub fn escape_velocity(mass: f64, radius: f64) -> PhysicsResult<f64> {
use crate::constants::physical::GRAVITATIONAL_CONSTANT;
if mass <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "mass",
reason: format!("mass must be positive, got {mass}"),
});
}
if radius <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "radius",
reason: format!("radius must be positive, got {radius}"),
});
}
Ok((2.0 * GRAVITATIONAL_CONSTANT * mass / radius).sqrt())
}
pub fn orbital_period(orbital_radius: f64, central_mass: f64) -> PhysicsResult<f64> {
use crate::constants::physical::GRAVITATIONAL_CONSTANT;
if orbital_radius <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "orbital_radius",
reason: format!("orbital radius must be positive, got {orbital_radius}"),
});
}
if central_mass <= 0.0 {
return Err(PhysicsError::InvalidParameter {
param: "central_mass",
reason: format!("central mass must be positive, got {central_mass}"),
});
}
Ok(2.0 * PI * (orbital_radius.powi(3) / (GRAVITATIONAL_CONSTANT * central_mass)).sqrt())
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-9;
#[test]
fn test_kinetic_energy_basic() {
let ke = kinetic_energy(2.0, 3.0).expect("should succeed");
assert!((ke - 9.0).abs() < TOL, "KE = {ke}");
}
#[test]
fn test_kinetic_energy_zero_velocity() {
let ke = kinetic_energy(5.0, 0.0).expect("should succeed");
assert_eq!(ke, 0.0);
}
#[test]
fn test_kinetic_energy_invalid_mass() {
assert!(kinetic_energy(-1.0, 1.0).is_err());
assert!(kinetic_energy(0.0, 1.0).is_err());
}
#[test]
fn test_potential_energy_gravity() {
let pe = potential_energy_gravity(2.0, 10.0, 9.81).expect("should succeed");
assert!((pe - 196.2).abs() < 1e-10);
}
#[test]
fn test_potential_energy_negative_height() {
let pe = potential_energy_gravity(1.0, -5.0, 9.81).expect("should succeed");
assert!(pe < 0.0);
}
#[test]
fn test_momentum() {
let p = momentum(3.0, 4.0).expect("should succeed");
assert!((p - 12.0).abs() < TOL);
}
#[test]
fn test_angular_momentum() {
let l = angular_momentum(1.0, 2.0, 3.0).expect("should succeed");
assert!((l - 6.0).abs() < TOL);
}
#[test]
fn test_centripetal_acceleration() {
let a = centripetal_acceleration(2.0, 2.0).expect("should succeed");
assert!((a - 2.0).abs() < TOL);
}
#[test]
fn test_projectile_45_degrees() {
let v0 = 20.0;
let g = 9.81;
let result = projectile_motion(v0, 45.0, g).expect("should succeed");
let expected_range = v0 * v0 / g;
assert!(
(result.range - expected_range).abs() < 1e-10,
"range = {}, expected = {}",
result.range,
expected_range
);
}
#[test]
fn test_projectile_90_degrees_zero_range() {
let v0 = 10.0;
let g = 9.81;
let result = projectile_motion(v0, 90.0, g).expect("should succeed");
assert!(result.range.abs() < 1e-10, "range should be ~0");
let expected_height = v0 * v0 / (2.0 * g);
assert!((result.max_height - expected_height).abs() < 1e-10);
}
#[test]
fn test_projectile_invalid_angle() {
assert!(projectile_motion(10.0, 95.0, 9.81).is_err());
assert!(projectile_motion(10.0, -1.0, 9.81).is_err());
}
#[test]
fn test_simple_harmonic_oscillator() {
let result = simple_harmonic_oscillator(1.0, 4.0).expect("should succeed");
assert!((result.angular_frequency - 2.0).abs() < TOL);
assert!((result.period - PI).abs() < TOL);
assert!((result.frequency - 1.0 / PI).abs() < TOL);
}
#[test]
fn test_sho_displacement_at_zero() {
let d = sho_displacement(3.0, 2.0, 0.0, 0.0).expect("should succeed");
assert!((d - 3.0).abs() < TOL);
}
#[test]
fn test_escape_velocity_earth() {
use crate::constants::physical::{EARTH_MASS, EARTH_RADIUS};
let ve = escape_velocity(EARTH_MASS, EARTH_RADIUS).expect("should succeed");
assert!(
(ve - 11_186.0).abs() < 200.0,
"escape velocity = {ve:.1} m/s"
);
}
#[test]
fn test_gravitational_force_known() {
use crate::constants::physical::GRAVITATIONAL_CONSTANT;
let f = gravitational_force(1.0, 1.0, 1.0).expect("should succeed");
assert!((f - GRAVITATIONAL_CONSTANT).abs() < 1e-20);
}
#[test]
fn test_orbital_period_earth_approx() {
use crate::constants::physical::{EARTH_RADIUS, SOLAR_MASS};
use crate::constants::physical::ASTRONOMICAL_UNIT;
let period = orbital_period(ASTRONOMICAL_UNIT, SOLAR_MASS).expect("should succeed");
let one_year = 3.156e7_f64;
assert!(
(period - one_year).abs() / one_year < 0.01,
"orbital period = {period:.3e} s"
);
assert!(orbital_period(0.0, SOLAR_MASS).is_err());
assert!(orbital_period(EARTH_RADIUS, 0.0).is_err());
}
}