#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::f64::consts::TAU;
pub mod prelude;
pub const STANDARD_GRAVITY: f64 = 9.806_65;
pub const GRAVITATIONAL_CONSTANT: f64 = 6.674_30e-11;
fn finite(value: f64) -> Option<f64> {
value.is_finite().then_some(value)
}
#[must_use]
pub fn gravitational_force(mass_a: f64, mass_b: f64, distance: f64) -> Option<f64> {
if mass_a < 0.0 || mass_b < 0.0 || distance <= 0.0 {
return None;
}
finite(GRAVITATIONAL_CONSTANT * mass_a * mass_b / distance.powi(2))
}
#[must_use]
pub fn gravitational_acceleration(source_mass: f64, distance: f64) -> Option<f64> {
if source_mass < 0.0 || distance <= 0.0 {
return None;
}
finite(GRAVITATIONAL_CONSTANT * source_mass / distance.powi(2))
}
#[must_use]
pub fn weight(mass: f64, gravitational_acceleration: f64) -> Option<f64> {
if mass < 0.0 || !gravitational_acceleration.is_finite() {
return None;
}
finite(mass * gravitational_acceleration)
}
#[must_use]
pub fn standard_weight(mass: f64) -> Option<f64> {
weight(mass, STANDARD_GRAVITY)
}
#[must_use]
pub fn circular_orbital_velocity(source_mass: f64, orbital_radius: f64) -> Option<f64> {
if source_mass < 0.0 || orbital_radius <= 0.0 {
return None;
}
finite((GRAVITATIONAL_CONSTANT * source_mass / orbital_radius).sqrt())
}
#[must_use]
pub fn escape_velocity(source_mass: f64, distance: f64) -> Option<f64> {
if source_mass < 0.0 || distance <= 0.0 {
return None;
}
finite((2.0 * GRAVITATIONAL_CONSTANT * source_mass / distance).sqrt())
}
#[must_use]
pub fn circular_orbital_period(source_mass: f64, orbital_radius: f64) -> Option<f64> {
if source_mass <= 0.0 || orbital_radius <= 0.0 {
return None;
}
finite(TAU * (orbital_radius.powi(3) / (GRAVITATIONAL_CONSTANT * source_mass)).sqrt())
}
#[must_use]
pub fn gravitational_potential_energy(mass_a: f64, mass_b: f64, distance: f64) -> Option<f64> {
if mass_a < 0.0 || mass_b < 0.0 || distance <= 0.0 {
return None;
}
finite(-GRAVITATIONAL_CONSTANT * mass_a * mass_b / distance)
}
#[must_use]
pub fn near_surface_potential_energy(
mass: f64,
height: f64,
gravitational_acceleration: f64,
) -> Option<f64> {
if mass < 0.0 || !gravitational_acceleration.is_finite() {
return None;
}
finite(mass * gravitational_acceleration * height)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GravityBody {
pub mass: f64,
pub radius: f64,
}
impl GravityBody {
#[must_use]
pub fn new(mass: f64, radius: f64) -> Option<Self> {
if !mass.is_finite() || mass < 0.0 || !radius.is_finite() || radius <= 0.0 {
return None;
}
Some(Self { mass, radius })
}
#[must_use]
pub fn surface_gravity(&self) -> Option<f64> {
gravitational_acceleration(self.mass, self.radius)
}
#[must_use]
pub fn escape_velocity(&self) -> Option<f64> {
escape_velocity(self.mass, self.radius)
}
#[must_use]
pub fn circular_orbital_velocity_at_radius(&self, orbital_radius: f64) -> Option<f64> {
circular_orbital_velocity(self.mass, orbital_radius)
}
}
#[cfg(test)]
mod tests {
use super::{
GRAVITATIONAL_CONSTANT, GravityBody, STANDARD_GRAVITY, circular_orbital_period,
circular_orbital_velocity, escape_velocity, gravitational_acceleration,
gravitational_force, gravitational_potential_energy, near_surface_potential_energy,
standard_weight, weight,
};
fn approx_eq(left: f64, right: f64, tolerance: f64) {
let delta = (left - right).abs();
assert!(
delta <= tolerance,
"left={left} right={right} delta={delta} tolerance={tolerance}"
);
}
#[test]
fn gravitational_force_handles_basic_cases() {
assert_eq!(
gravitational_force(1.0, 1.0, 1.0),
Some(GRAVITATIONAL_CONSTANT)
);
assert_eq!(gravitational_force(1.0, 1.0, 0.0), None);
assert_eq!(gravitational_force(-1.0, 1.0, 1.0), None);
}
#[test]
fn gravitational_acceleration_matches_earth_surface() {
let gravity = gravitational_acceleration(5.972e24, 6.371e6).unwrap();
approx_eq(gravity, 9.82, 0.05);
}
#[test]
fn weight_helpers_match_standard_gravity() {
approx_eq(weight(10.0, STANDARD_GRAVITY).unwrap(), 98.066_5, 1.0e-12);
approx_eq(standard_weight(10.0).unwrap(), 98.066_5, 1.0e-12);
}
#[test]
fn orbital_helpers_match_earth_scale_values() {
approx_eq(escape_velocity(5.972e24, 6.371e6).unwrap(), 11_186.0, 2.0);
approx_eq(
circular_orbital_velocity(5.972e24, 6.371e6).unwrap(),
7_909.0,
2.0,
);
let period = circular_orbital_period(5.972e24, 6.371e6).unwrap();
assert!(period.is_finite());
assert!(period > 0.0);
}
#[test]
fn potential_energy_helpers_match_expected_values() {
assert_eq!(
gravitational_potential_energy(1.0, 1.0, 1.0),
Some(-GRAVITATIONAL_CONSTANT)
);
approx_eq(
near_surface_potential_energy(2.0, 10.0, STANDARD_GRAVITY).unwrap(),
196.133,
1.0e-12,
);
}
#[test]
fn gravity_body_validates_inputs_and_delegates() {
let earth = GravityBody::new(5.972e24, 6.371e6).unwrap();
approx_eq(earth.surface_gravity().unwrap(), 9.82, 0.05);
assert_eq!(GravityBody::new(-1.0, 1.0), None);
assert_eq!(GravityBody::new(1.0, 0.0), None);
}
}