Skip to main content

use_gravity/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Gravity, orbit, and gravitational energy helpers.
5
6use core::f64::consts::TAU;
7
8pub mod prelude;
9
10/// Conventional standard gravity, in meters per second squared.
11///
12/// This crate keeps the value locally as a convenience for gravity-specific helpers.
13/// Broader physical constants belong in the top-level `use-constants` set.
14pub const STANDARD_GRAVITY: f64 = 9.806_65;
15
16/// Newtonian constant of gravitation, in cubic meters per kilogram second squared.
17///
18/// This crate keeps the value locally as a convenience for gravity-specific helpers.
19/// Broader physical constants belong in the top-level `use-constants` set.
20pub const GRAVITATIONAL_CONSTANT: f64 = 6.674_30e-11;
21
22fn finite(value: f64) -> Option<f64> {
23    value.is_finite().then_some(value)
24}
25
26/// Computes the gravitational attraction between two point masses.
27///
28/// Formula: `F = G * m1 * m2 / r^2`
29///
30/// Returns `None` when either mass is negative, when `distance` is less than or equal to zero,
31/// or when the computed result is not finite.
32///
33/// # Examples
34///
35/// ```rust
36/// use use_gravity::{GRAVITATIONAL_CONSTANT, gravitational_force};
37///
38/// assert_eq!(gravitational_force(1.0, 1.0, 1.0), Some(GRAVITATIONAL_CONSTANT));
39/// ```
40#[must_use]
41pub fn gravitational_force(mass_a: f64, mass_b: f64, distance: f64) -> Option<f64> {
42    if mass_a < 0.0 || mass_b < 0.0 || distance <= 0.0 {
43        return None;
44    }
45
46    finite(GRAVITATIONAL_CONSTANT * mass_a * mass_b / distance.powi(2))
47}
48
49/// Computes the gravitational acceleration caused by a source mass at a distance.
50///
51/// Formula: `g = G * M / r^2`
52///
53/// Returns `None` when `source_mass` is negative, when `distance` is less than or equal to
54/// zero, or when the computed result is not finite.
55///
56/// # Examples
57///
58/// ```rust
59/// use use_gravity::{GRAVITATIONAL_CONSTANT, gravitational_acceleration};
60///
61/// assert_eq!(gravitational_acceleration(1.0, 1.0), Some(GRAVITATIONAL_CONSTANT));
62/// ```
63#[must_use]
64pub fn gravitational_acceleration(source_mass: f64, distance: f64) -> Option<f64> {
65    if source_mass < 0.0 || distance <= 0.0 {
66        return None;
67    }
68
69    finite(GRAVITATIONAL_CONSTANT * source_mass / distance.powi(2))
70}
71
72/// Computes weight from mass and gravitational acceleration.
73///
74/// Formula: `W = m * g`
75///
76/// Returns `None` when `mass` is negative, when `gravitational_acceleration` is not finite, or
77/// when the computed result is not finite.
78#[must_use]
79pub fn weight(mass: f64, gravitational_acceleration: f64) -> Option<f64> {
80    if mass < 0.0 || !gravitational_acceleration.is_finite() {
81        return None;
82    }
83
84    finite(mass * gravitational_acceleration)
85}
86
87/// Computes weight under conventional standard gravity.
88#[must_use]
89pub fn standard_weight(mass: f64) -> Option<f64> {
90    weight(mass, STANDARD_GRAVITY)
91}
92
93/// Computes the circular orbital velocity around a source mass.
94///
95/// Formula: `v = sqrt(G * M / r)`
96///
97/// Returns `None` when `source_mass` is negative, when `orbital_radius` is less than or equal to
98/// zero, or when the computed result is not finite.
99#[must_use]
100pub fn circular_orbital_velocity(source_mass: f64, orbital_radius: f64) -> Option<f64> {
101    if source_mass < 0.0 || orbital_radius <= 0.0 {
102        return None;
103    }
104
105    finite((GRAVITATIONAL_CONSTANT * source_mass / orbital_radius).sqrt())
106}
107
108/// Computes the escape velocity from a source mass at a distance.
109///
110/// Formula: `v = sqrt(2 * G * M / r)`
111///
112/// Returns `None` when `source_mass` is negative, when `distance` is less than or equal to zero,
113/// or when the computed result is not finite.
114///
115/// # Examples
116///
117/// ```rust
118/// use use_gravity::escape_velocity;
119///
120/// let velocity = escape_velocity(5.972e24, 6.371e6).unwrap();
121///
122/// assert!((velocity - 11_186.0).abs() < 2.0);
123/// ```
124#[must_use]
125pub fn escape_velocity(source_mass: f64, distance: f64) -> Option<f64> {
126    if source_mass < 0.0 || distance <= 0.0 {
127        return None;
128    }
129
130    finite((2.0 * GRAVITATIONAL_CONSTANT * source_mass / distance).sqrt())
131}
132
133/// Computes the orbital period for a circular orbit.
134///
135/// Formula: `T = 2π * sqrt(r^3 / (G * M))`
136///
137/// Returns `None` when `source_mass` is less than or equal to zero, when `orbital_radius` is
138/// less than or equal to zero, or when the computed result is not finite.
139#[must_use]
140pub fn circular_orbital_period(source_mass: f64, orbital_radius: f64) -> Option<f64> {
141    if source_mass <= 0.0 || orbital_radius <= 0.0 {
142        return None;
143    }
144
145    finite(TAU * (orbital_radius.powi(3) / (GRAVITATIONAL_CONSTANT * source_mass)).sqrt())
146}
147
148/// Computes gravitational potential energy between two masses.
149///
150/// Formula: `U = -G * m1 * m2 / r`
151///
152/// Returns `None` when either mass is negative, when `distance` is less than or equal to zero,
153/// or when the computed result is not finite.
154#[must_use]
155pub fn gravitational_potential_energy(mass_a: f64, mass_b: f64, distance: f64) -> Option<f64> {
156    if mass_a < 0.0 || mass_b < 0.0 || distance <= 0.0 {
157        return None;
158    }
159
160    finite(-GRAVITATIONAL_CONSTANT * mass_a * mass_b / distance)
161}
162
163/// Computes near-surface potential energy from mass, height, and gravitational acceleration.
164///
165/// Formula: `U = m * g * h`
166///
167/// Returns `None` when `mass` is negative, when `gravitational_acceleration` is not finite, or
168/// when the computed result is not finite. Negative heights are allowed.
169#[must_use]
170pub fn near_surface_potential_energy(
171    mass: f64,
172    height: f64,
173    gravitational_acceleration: f64,
174) -> Option<f64> {
175    if mass < 0.0 || !gravitational_acceleration.is_finite() {
176        return None;
177    }
178
179    finite(mass * gravitational_acceleration * height)
180}
181
182/// Mass and radius for a body used in gravity calculations.
183#[derive(Debug, Clone, Copy, PartialEq)]
184pub struct GravityBody {
185    pub mass: f64,
186    pub radius: f64,
187}
188
189impl GravityBody {
190    /// Creates a gravity body from a mass and radius.
191    ///
192    /// Returns `None` when `mass` is negative, when `radius` is less than or equal to zero, or
193    /// when either input is not finite.
194    #[must_use]
195    pub fn new(mass: f64, radius: f64) -> Option<Self> {
196        if !mass.is_finite() || mass < 0.0 || !radius.is_finite() || radius <= 0.0 {
197            return None;
198        }
199
200        Some(Self { mass, radius })
201    }
202
203    /// Computes the surface gravity of the body.
204    ///
205    /// # Examples
206    ///
207    /// ```rust
208    /// use use_gravity::GravityBody;
209    ///
210    /// let earth = GravityBody::new(5.972e24, 6.371e6).unwrap();
211    /// let gravity = earth.surface_gravity().unwrap();
212    ///
213    /// assert!((gravity - 9.82).abs() < 0.05);
214    /// ```
215    #[must_use]
216    pub fn surface_gravity(&self) -> Option<f64> {
217        gravitational_acceleration(self.mass, self.radius)
218    }
219
220    /// Computes the escape velocity from the body's surface.
221    #[must_use]
222    pub fn escape_velocity(&self) -> Option<f64> {
223        escape_velocity(self.mass, self.radius)
224    }
225
226    /// Computes the circular orbital velocity at a given radius from the body's center.
227    #[must_use]
228    pub fn circular_orbital_velocity_at_radius(&self, orbital_radius: f64) -> Option<f64> {
229        circular_orbital_velocity(self.mass, orbital_radius)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::{
236        GRAVITATIONAL_CONSTANT, GravityBody, STANDARD_GRAVITY, circular_orbital_period,
237        circular_orbital_velocity, escape_velocity, gravitational_acceleration,
238        gravitational_force, gravitational_potential_energy, near_surface_potential_energy,
239        standard_weight, weight,
240    };
241
242    fn approx_eq(left: f64, right: f64, tolerance: f64) {
243        let delta = (left - right).abs();
244
245        assert!(
246            delta <= tolerance,
247            "left={left} right={right} delta={delta} tolerance={tolerance}"
248        );
249    }
250
251    #[test]
252    fn gravitational_force_handles_basic_cases() {
253        assert_eq!(
254            gravitational_force(1.0, 1.0, 1.0),
255            Some(GRAVITATIONAL_CONSTANT)
256        );
257        assert_eq!(gravitational_force(1.0, 1.0, 0.0), None);
258        assert_eq!(gravitational_force(-1.0, 1.0, 1.0), None);
259    }
260
261    #[test]
262    fn gravitational_acceleration_matches_earth_surface() {
263        let gravity = gravitational_acceleration(5.972e24, 6.371e6).unwrap();
264
265        approx_eq(gravity, 9.82, 0.05);
266    }
267
268    #[test]
269    fn weight_helpers_match_standard_gravity() {
270        approx_eq(weight(10.0, STANDARD_GRAVITY).unwrap(), 98.066_5, 1.0e-12);
271        approx_eq(standard_weight(10.0).unwrap(), 98.066_5, 1.0e-12);
272    }
273
274    #[test]
275    fn orbital_helpers_match_earth_scale_values() {
276        approx_eq(escape_velocity(5.972e24, 6.371e6).unwrap(), 11_186.0, 2.0);
277        approx_eq(
278            circular_orbital_velocity(5.972e24, 6.371e6).unwrap(),
279            7_909.0,
280            2.0,
281        );
282
283        let period = circular_orbital_period(5.972e24, 6.371e6).unwrap();
284
285        assert!(period.is_finite());
286        assert!(period > 0.0);
287    }
288
289    #[test]
290    fn potential_energy_helpers_match_expected_values() {
291        assert_eq!(
292            gravitational_potential_energy(1.0, 1.0, 1.0),
293            Some(-GRAVITATIONAL_CONSTANT)
294        );
295        approx_eq(
296            near_surface_potential_energy(2.0, 10.0, STANDARD_GRAVITY).unwrap(),
297            196.133,
298            1.0e-12,
299        );
300    }
301
302    #[test]
303    fn gravity_body_validates_inputs_and_delegates() {
304        let earth = GravityBody::new(5.972e24, 6.371e6).unwrap();
305
306        approx_eq(earth.surface_gravity().unwrap(), 9.82, 0.05);
307        assert_eq!(GravityBody::new(-1.0, 1.0), None);
308        assert_eq!(GravityBody::new(1.0, 0.0), None);
309    }
310}