#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
pub mod prelude;
pub const SPEED_OF_LIGHT: f64 = 299_792_458.0;
const SPEED_OF_LIGHT_SQUARED: f64 = SPEED_OF_LIGHT * SPEED_OF_LIGHT;
fn finite(value: f64) -> Option<f64> {
value.is_finite().then_some(value)
}
fn is_nonnegative_finite(value: f64) -> bool {
value.is_finite() && value >= 0.0
}
fn is_subluminal_velocity(velocity: f64) -> bool {
velocity.is_finite() && velocity.abs() < SPEED_OF_LIGHT
}
fn signed_beta(velocity: f64) -> Option<f64> {
if !is_subluminal_velocity(velocity) {
return None;
}
let beta = velocity / SPEED_OF_LIGHT;
if beta.abs() >= 1.0 {
return None;
}
finite(beta)
}
fn gamma_from_signed_beta(beta: f64) -> Option<f64> {
if !beta.is_finite() || beta.abs() >= 1.0 {
return None;
}
let one_minus_beta_squared = (-beta).mul_add(beta, 1.0);
if !one_minus_beta_squared.is_finite() || one_minus_beta_squared <= 0.0 {
return None;
}
let gamma = one_minus_beta_squared.sqrt().recip();
if gamma < 1.0 {
return None;
}
finite(gamma)
}
fn signed_speed_from_beta(beta: f64) -> Option<f64> {
if !beta.is_finite() || beta.abs() >= 1.0 {
return None;
}
let velocity = beta * SPEED_OF_LIGHT;
if velocity.abs() >= SPEED_OF_LIGHT {
return None;
}
finite(velocity)
}
#[must_use]
pub fn beta(speed: f64) -> Option<f64> {
if !is_subluminal_speed(speed) {
return None;
}
let beta = speed / SPEED_OF_LIGHT;
if !(0.0..1.0).contains(&beta) {
return None;
}
finite(beta)
}
#[must_use]
pub fn speed_from_beta(beta: f64) -> Option<f64> {
if !beta.is_finite() || !(0.0..1.0).contains(&beta) {
return None;
}
let speed = beta * SPEED_OF_LIGHT;
if speed >= SPEED_OF_LIGHT {
return None;
}
finite(speed)
}
#[must_use]
pub fn is_subluminal_speed(speed: f64) -> bool {
is_nonnegative_finite(speed) && speed < SPEED_OF_LIGHT
}
#[must_use]
pub fn lorentz_factor_from_beta(beta: f64) -> Option<f64> {
if !beta.is_finite() || !(0.0..1.0).contains(&beta) {
return None;
}
gamma_from_signed_beta(beta)
}
#[must_use]
pub fn lorentz_factor(speed: f64) -> Option<f64> {
beta(speed).and_then(lorentz_factor_from_beta)
}
#[must_use]
pub fn dilated_time(proper_time: f64, speed: f64) -> Option<f64> {
if !is_nonnegative_finite(proper_time) {
return None;
}
let gamma = lorentz_factor(speed)?;
finite(gamma * proper_time)
}
#[must_use]
pub fn proper_time(dilated_time: f64, speed: f64) -> Option<f64> {
if !is_nonnegative_finite(dilated_time) {
return None;
}
let gamma = lorentz_factor(speed)?;
finite(dilated_time / gamma)
}
#[must_use]
pub fn contracted_length(proper_length: f64, speed: f64) -> Option<f64> {
if !is_nonnegative_finite(proper_length) {
return None;
}
let gamma = lorentz_factor(speed)?;
finite(proper_length / gamma)
}
#[must_use]
pub fn proper_length(contracted_length: f64, speed: f64) -> Option<f64> {
if !is_nonnegative_finite(contracted_length) {
return None;
}
let gamma = lorentz_factor(speed)?;
finite(contracted_length * gamma)
}
#[must_use]
pub fn rest_energy(mass: f64) -> Option<f64> {
if !is_nonnegative_finite(mass) {
return None;
}
finite(mass * SPEED_OF_LIGHT_SQUARED)
}
#[must_use]
pub fn mass_from_rest_energy(rest_energy: f64) -> Option<f64> {
if !is_nonnegative_finite(rest_energy) {
return None;
}
finite(rest_energy / SPEED_OF_LIGHT_SQUARED)
}
#[must_use]
pub fn total_energy(mass: f64, speed: f64) -> Option<f64> {
if !is_nonnegative_finite(mass) {
return None;
}
let gamma = lorentz_factor(speed)?;
finite(gamma * mass * SPEED_OF_LIGHT_SQUARED)
}
#[must_use]
pub fn relativistic_kinetic_energy(mass: f64, speed: f64) -> Option<f64> {
if !is_nonnegative_finite(mass) {
return None;
}
let gamma = lorentz_factor(speed)?;
let kinetic_energy = (gamma - 1.0) * mass * SPEED_OF_LIGHT_SQUARED;
if kinetic_energy < 0.0 {
return None;
}
finite(kinetic_energy)
}
#[must_use]
pub fn relativistic_momentum(mass: f64, velocity: f64) -> Option<f64> {
if !is_nonnegative_finite(mass) || !is_subluminal_velocity(velocity) {
return None;
}
let gamma = gamma_from_signed_beta(signed_beta(velocity)?)?;
finite(gamma * mass * velocity)
}
#[must_use]
pub fn rest_mass_from_momentum_speed(momentum: f64, velocity: f64) -> Option<f64> {
if !momentum.is_finite() || !is_subluminal_velocity(velocity) || velocity == 0.0 {
return None;
}
let gamma = gamma_from_signed_beta(signed_beta(velocity)?)?;
let mass = momentum / (gamma * velocity);
if mass < 0.0 {
return None;
}
finite(mass)
}
#[must_use]
pub fn energy_momentum_relation(rest_mass: f64, momentum: f64) -> Option<f64> {
if !is_nonnegative_finite(rest_mass) || !momentum.is_finite() {
return None;
}
let momentum_term = momentum * SPEED_OF_LIGHT;
let rest_energy = rest_mass * SPEED_OF_LIGHT_SQUARED;
let energy_squared = momentum_term.mul_add(momentum_term, rest_energy * rest_energy);
if !energy_squared.is_finite() || energy_squared < 0.0 {
return None;
}
finite(energy_squared.sqrt())
}
#[must_use]
pub fn rapidity_from_beta(beta: f64) -> Option<f64> {
if !beta.is_finite() || beta.abs() >= 1.0 {
return None;
}
finite(beta.atanh())
}
#[must_use]
pub fn beta_from_rapidity(rapidity: f64) -> Option<f64> {
if !rapidity.is_finite() {
return None;
}
let beta = rapidity.tanh();
if beta.abs() >= 1.0 {
return None;
}
finite(beta)
}
#[must_use]
pub fn speed_from_rapidity(rapidity: f64) -> Option<f64> {
beta_from_rapidity(rapidity).and_then(signed_speed_from_beta)
}
#[must_use]
pub fn velocity_addition(velocity_a: f64, velocity_b: f64) -> Option<f64> {
if !is_subluminal_velocity(velocity_a) || !is_subluminal_velocity(velocity_b) {
return None;
}
let denominator = 1.0 + ((velocity_a * velocity_b) / SPEED_OF_LIGHT_SQUARED);
if !denominator.is_finite() || denominator == 0.0 {
return None;
}
let velocity = (velocity_a + velocity_b) / denominator;
if velocity.abs() >= SPEED_OF_LIGHT {
return None;
}
finite(velocity)
}
#[must_use]
pub fn doppler_factor_longitudinal_from_beta(beta: f64) -> Option<f64> {
if !beta.is_finite() || beta <= -1.0 || beta >= 1.0 {
return None;
}
let numerator = 1.0 + beta;
let denominator = 1.0 - beta;
if numerator <= 0.0 || denominator <= 0.0 {
return None;
}
finite((numerator / denominator).sqrt())
}
#[must_use]
pub fn observed_frequency_longitudinal(emitted_frequency: f64, beta: f64) -> Option<f64> {
if !is_nonnegative_finite(emitted_frequency) {
return None;
}
let doppler_factor = doppler_factor_longitudinal_from_beta(beta)?;
finite(emitted_frequency * doppler_factor)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RelativisticBody {
pub rest_mass: f64,
pub velocity: f64,
}
impl RelativisticBody {
#[must_use]
pub fn new(rest_mass: f64, velocity: f64) -> Option<Self> {
if !is_nonnegative_finite(rest_mass) || !is_subluminal_velocity(velocity) {
return None;
}
Some(Self {
rest_mass,
velocity,
})
}
#[must_use]
pub fn beta(&self) -> Option<f64> {
beta(self.velocity.abs())
}
#[must_use]
pub fn lorentz_factor(&self) -> Option<f64> {
lorentz_factor(self.velocity.abs())
}
#[must_use]
pub fn rest_energy(&self) -> Option<f64> {
rest_energy(self.rest_mass)
}
#[must_use]
pub fn total_energy(&self) -> Option<f64> {
total_energy(self.rest_mass, self.velocity.abs())
}
#[must_use]
pub fn kinetic_energy(&self) -> Option<f64> {
relativistic_kinetic_energy(self.rest_mass, self.velocity.abs())
}
#[must_use]
pub fn momentum(&self) -> Option<f64> {
relativistic_momentum(self.rest_mass, self.velocity)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp)]
use super::*;
const EPSILON: f64 = 1.0e-12;
fn approx_eq(actual: f64, expected: f64) -> bool {
let scale = expected.abs().max(1.0);
(actual - expected).abs() <= EPSILON * scale
}
fn assert_option_approx_eq(actual: Option<f64>, expected: f64) {
let value = actual.expect("expected Some(value)");
assert!(
approx_eq(value, expected),
"expected {expected}, got {value}"
);
}
#[test]
fn beta_helpers_validate_speed_ranges() {
assert_option_approx_eq(beta(SPEED_OF_LIGHT * 0.5), 0.5);
assert_eq!(beta(0.0), Some(0.0));
assert_eq!(beta(-1.0), None);
assert_eq!(beta(SPEED_OF_LIGHT), None);
assert_option_approx_eq(speed_from_beta(0.5), SPEED_OF_LIGHT * 0.5);
assert_eq!(speed_from_beta(1.0), None);
assert_eq!(speed_from_beta(-0.1), None);
assert!(is_subluminal_speed(0.0));
assert!(is_subluminal_speed(SPEED_OF_LIGHT * 0.5));
assert!(!is_subluminal_speed(SPEED_OF_LIGHT));
assert!(!is_subluminal_speed(f64::NAN));
}
#[test]
fn lorentz_helpers_compute_expected_gamma() {
assert_eq!(lorentz_factor_from_beta(0.0), Some(1.0));
assert_option_approx_eq(lorentz_factor_from_beta(0.6), 1.25);
assert_eq!(lorentz_factor_from_beta(1.0), None);
assert_option_approx_eq(lorentz_factor(SPEED_OF_LIGHT * 0.6), 1.25);
}
#[test]
fn time_dilation_helpers_compute_expected_values() {
assert_option_approx_eq(dilated_time(10.0, SPEED_OF_LIGHT * 0.6), 12.5);
assert_eq!(dilated_time(-10.0, SPEED_OF_LIGHT * 0.6), None);
assert_option_approx_eq(proper_time(12.5, SPEED_OF_LIGHT * 0.6), 10.0);
assert_eq!(proper_time(-12.5, SPEED_OF_LIGHT * 0.6), None);
}
#[test]
fn length_helpers_compute_expected_values() {
assert_option_approx_eq(contracted_length(10.0, SPEED_OF_LIGHT * 0.6), 8.0);
assert_eq!(contracted_length(-10.0, SPEED_OF_LIGHT * 0.6), None);
assert_option_approx_eq(proper_length(8.0, SPEED_OF_LIGHT * 0.6), 10.0);
}
#[test]
fn mass_energy_helpers_compute_expected_values() {
assert_option_approx_eq(rest_energy(1.0), SPEED_OF_LIGHT_SQUARED);
assert_eq!(rest_energy(-1.0), None);
assert_option_approx_eq(mass_from_rest_energy(SPEED_OF_LIGHT_SQUARED), 1.0);
assert_option_approx_eq(
total_energy(1.0, SPEED_OF_LIGHT * 0.6),
1.25 * SPEED_OF_LIGHT_SQUARED,
);
assert_option_approx_eq(
relativistic_kinetic_energy(1.0, SPEED_OF_LIGHT * 0.6),
0.25 * SPEED_OF_LIGHT_SQUARED,
);
}
#[test]
fn momentum_helpers_compute_expected_values() {
let expected_momentum = 1.25 * SPEED_OF_LIGHT * 0.6;
assert_option_approx_eq(
relativistic_momentum(1.0, SPEED_OF_LIGHT * 0.6),
expected_momentum,
);
assert_option_approx_eq(
relativistic_momentum(1.0, -SPEED_OF_LIGHT * 0.6),
-expected_momentum,
);
assert_eq!(relativistic_momentum(-1.0, SPEED_OF_LIGHT * 0.6), None);
assert_option_approx_eq(
rest_mass_from_momentum_speed(expected_momentum, SPEED_OF_LIGHT * 0.6),
1.0,
);
assert_eq!(rest_mass_from_momentum_speed(1.0, 0.0), None);
assert_option_approx_eq(energy_momentum_relation(1.0, 0.0), SPEED_OF_LIGHT_SQUARED);
}
#[test]
fn rapidity_helpers_compute_expected_values() {
assert_eq!(rapidity_from_beta(0.0), Some(0.0));
assert_eq!(beta_from_rapidity(0.0), Some(0.0));
assert_eq!(speed_from_rapidity(0.0), Some(0.0));
}
#[test]
fn velocity_addition_stays_subluminal() {
assert_option_approx_eq(
velocity_addition(SPEED_OF_LIGHT * 0.5, SPEED_OF_LIGHT * 0.5),
SPEED_OF_LIGHT * 0.8,
);
assert_eq!(velocity_addition(SPEED_OF_LIGHT, 1.0), None);
}
#[test]
fn doppler_helpers_compute_expected_values() {
assert_eq!(doppler_factor_longitudinal_from_beta(0.0), Some(1.0));
assert_option_approx_eq(doppler_factor_longitudinal_from_beta(0.6), 2.0);
assert_eq!(doppler_factor_longitudinal_from_beta(1.0), None);
assert_option_approx_eq(observed_frequency_longitudinal(100.0, 0.6), 200.0);
assert_eq!(observed_frequency_longitudinal(-100.0, 0.6), None);
}
#[test]
fn relativistic_body_validates_and_delegates() {
let body = RelativisticBody::new(1.0, SPEED_OF_LIGHT * 0.6).expect("expected valid body");
assert_option_approx_eq(body.lorentz_factor(), 1.25);
assert_option_approx_eq(body.momentum(), 1.25 * SPEED_OF_LIGHT * 0.6);
assert_eq!(RelativisticBody::new(-1.0, SPEED_OF_LIGHT * 0.6), None);
assert_eq!(RelativisticBody::new(1.0, SPEED_OF_LIGHT), None);
}
}