1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::f64::consts::TAU;
7
8pub mod prelude;
9
10pub const STANDARD_GRAVITY: f64 = 9.806_65;
15
16pub 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#[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#[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#[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#[must_use]
89pub fn standard_weight(mass: f64) -> Option<f64> {
90 weight(mass, STANDARD_GRAVITY)
91}
92
93#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq)]
184pub struct GravityBody {
185 pub mass: f64,
186 pub radius: f64,
187}
188
189impl GravityBody {
190 #[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 #[must_use]
216 pub fn surface_gravity(&self) -> Option<f64> {
217 gravitational_acceleration(self.mass, self.radius)
218 }
219
220 #[must_use]
222 pub fn escape_velocity(&self) -> Option<f64> {
223 escape_velocity(self.mass, self.radius)
224 }
225
226 #[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}