Skip to main content

use_relativity/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Small special relativity scalar helpers.
5
6/// Re-exports for ergonomic glob imports.
7pub mod prelude;
8
9/// Speed of light in vacuum, in meters per second.
10///
11/// This crate keeps the value locally as a convenience for scalar special relativity helpers.
12/// Broader physical constants belong in the top-level `use-constants` set.
13pub const SPEED_OF_LIGHT: f64 = 299_792_458.0;
14
15const SPEED_OF_LIGHT_SQUARED: f64 = SPEED_OF_LIGHT * SPEED_OF_LIGHT;
16
17fn finite(value: f64) -> Option<f64> {
18    value.is_finite().then_some(value)
19}
20
21fn is_nonnegative_finite(value: f64) -> bool {
22    value.is_finite() && value >= 0.0
23}
24
25fn is_subluminal_velocity(velocity: f64) -> bool {
26    velocity.is_finite() && velocity.abs() < SPEED_OF_LIGHT
27}
28
29fn signed_beta(velocity: f64) -> Option<f64> {
30    if !is_subluminal_velocity(velocity) {
31        return None;
32    }
33
34    let beta = velocity / SPEED_OF_LIGHT;
35    if beta.abs() >= 1.0 {
36        return None;
37    }
38
39    finite(beta)
40}
41
42fn gamma_from_signed_beta(beta: f64) -> Option<f64> {
43    if !beta.is_finite() || beta.abs() >= 1.0 {
44        return None;
45    }
46
47    let one_minus_beta_squared = (-beta).mul_add(beta, 1.0);
48    if !one_minus_beta_squared.is_finite() || one_minus_beta_squared <= 0.0 {
49        return None;
50    }
51
52    let gamma = one_minus_beta_squared.sqrt().recip();
53    if gamma < 1.0 {
54        return None;
55    }
56
57    finite(gamma)
58}
59
60fn signed_speed_from_beta(beta: f64) -> Option<f64> {
61    if !beta.is_finite() || beta.abs() >= 1.0 {
62        return None;
63    }
64
65    let velocity = beta * SPEED_OF_LIGHT;
66    if velocity.abs() >= SPEED_OF_LIGHT {
67        return None;
68    }
69
70    finite(velocity)
71}
72
73/// Computes the dimensionless speed ratio `β = v / c`.
74///
75/// The `speed` input is treated as a magnitude in meters per second.
76///
77/// Returns `None` when `speed` is negative, not finite, greater than or equal to the speed of
78/// light, or when the computed ratio is not finite.
79///
80/// # Examples
81///
82/// ```rust
83/// use use_relativity::{SPEED_OF_LIGHT, beta};
84///
85/// assert_eq!(beta(SPEED_OF_LIGHT * 0.5), Some(0.5));
86/// ```
87#[must_use]
88pub fn beta(speed: f64) -> Option<f64> {
89    if !is_subluminal_speed(speed) {
90        return None;
91    }
92
93    let beta = speed / SPEED_OF_LIGHT;
94    if !(0.0..1.0).contains(&beta) {
95        return None;
96    }
97
98    finite(beta)
99}
100
101/// Computes the speed magnitude `v = βc` in meters per second.
102///
103/// Returns `None` when `beta` is negative, not finite, greater than or equal to `1.0`, or when
104/// the computed speed is not finite.
105#[must_use]
106pub fn speed_from_beta(beta: f64) -> Option<f64> {
107    if !beta.is_finite() || !(0.0..1.0).contains(&beta) {
108        return None;
109    }
110
111    let speed = beta * SPEED_OF_LIGHT;
112    if speed >= SPEED_OF_LIGHT {
113        return None;
114    }
115
116    finite(speed)
117}
118
119/// Returns `true` when `speed` is finite, non-negative, and strictly less than the speed of light.
120#[must_use]
121pub fn is_subluminal_speed(speed: f64) -> bool {
122    is_nonnegative_finite(speed) && speed < SPEED_OF_LIGHT
123}
124
125/// Computes the Lorentz factor `γ = 1 / sqrt(1 - β²)` from a non-negative `beta` magnitude.
126///
127/// Returns `None` when `beta` is negative, not finite, greater than or equal to `1.0`, or when
128/// the computed factor is not finite.
129///
130/// # Examples
131///
132/// ```rust
133/// use use_relativity::lorentz_factor_from_beta;
134///
135/// assert_eq!(lorentz_factor_from_beta(0.0), Some(1.0));
136/// ```
137#[must_use]
138pub fn lorentz_factor_from_beta(beta: f64) -> Option<f64> {
139    if !beta.is_finite() || !(0.0..1.0).contains(&beta) {
140        return None;
141    }
142
143    gamma_from_signed_beta(beta)
144}
145
146/// Computes the Lorentz factor `γ` from a speed magnitude in meters per second.
147///
148/// This helper delegates to [`beta`] and then to [`lorentz_factor_from_beta`].
149///
150/// # Examples
151///
152/// ```rust
153/// use use_relativity::{SPEED_OF_LIGHT, lorentz_factor};
154///
155/// assert!((lorentz_factor(SPEED_OF_LIGHT * 0.6).unwrap() - 1.25).abs() < 1.0e-12);
156/// ```
157#[must_use]
158pub fn lorentz_factor(speed: f64) -> Option<f64> {
159    beta(speed).and_then(lorentz_factor_from_beta)
160}
161
162/// Computes dilated coordinate time `t = γτ` from proper time `τ`.
163///
164/// Returns `None` when `proper_time` is negative or not finite, when `speed` is invalid, or when
165/// the computed time is not finite.
166///
167/// # Examples
168///
169/// ```rust
170/// use use_relativity::{SPEED_OF_LIGHT, dilated_time};
171///
172/// assert!((dilated_time(10.0, SPEED_OF_LIGHT * 0.6).unwrap() - 12.5).abs() < 1.0e-12);
173/// ```
174#[must_use]
175pub fn dilated_time(proper_time: f64, speed: f64) -> Option<f64> {
176    if !is_nonnegative_finite(proper_time) {
177        return None;
178    }
179
180    let gamma = lorentz_factor(speed)?;
181    finite(gamma * proper_time)
182}
183
184/// Computes proper time `τ = t / γ` from dilated coordinate time `t`.
185///
186/// Returns `None` when `dilated_time` is negative or not finite, when `speed` is invalid, or when
187/// the computed proper time is not finite.
188#[must_use]
189pub fn proper_time(dilated_time: f64, speed: f64) -> Option<f64> {
190    if !is_nonnegative_finite(dilated_time) {
191        return None;
192    }
193
194    let gamma = lorentz_factor(speed)?;
195    finite(dilated_time / gamma)
196}
197
198/// Computes contracted length `L = L0 / γ` from proper length `L0`.
199///
200/// Returns `None` when `proper_length` is negative or not finite, when `speed` is invalid, or
201/// when the computed contracted length is not finite.
202///
203/// # Examples
204///
205/// ```rust
206/// use use_relativity::{SPEED_OF_LIGHT, contracted_length};
207///
208/// assert!((contracted_length(10.0, SPEED_OF_LIGHT * 0.6).unwrap() - 8.0).abs() < 1.0e-12);
209/// ```
210#[must_use]
211pub fn contracted_length(proper_length: f64, speed: f64) -> Option<f64> {
212    if !is_nonnegative_finite(proper_length) {
213        return None;
214    }
215
216    let gamma = lorentz_factor(speed)?;
217    finite(proper_length / gamma)
218}
219
220/// Computes proper length `L0 = Lγ` from a contracted length `L`.
221///
222/// Returns `None` when `contracted_length` is negative or not finite, when `speed` is invalid,
223/// or when the computed proper length is not finite.
224#[must_use]
225pub fn proper_length(contracted_length: f64, speed: f64) -> Option<f64> {
226    if !is_nonnegative_finite(contracted_length) {
227        return None;
228    }
229
230    let gamma = lorentz_factor(speed)?;
231    finite(contracted_length * gamma)
232}
233
234/// Computes rest energy `E0 = mc²` in joules.
235///
236/// Returns `None` when `mass` is negative or not finite, or when the computed energy is not
237/// finite.
238///
239/// # Examples
240///
241/// ```rust
242/// use use_relativity::{SPEED_OF_LIGHT, rest_energy};
243///
244/// assert!((rest_energy(1.0).unwrap() - (SPEED_OF_LIGHT * SPEED_OF_LIGHT)).abs() < 1.0e-3);
245/// ```
246#[must_use]
247pub fn rest_energy(mass: f64) -> Option<f64> {
248    if !is_nonnegative_finite(mass) {
249        return None;
250    }
251
252    finite(mass * SPEED_OF_LIGHT_SQUARED)
253}
254
255/// Computes rest mass `m = E0 / c²` from rest energy in joules.
256///
257/// Returns `None` when `rest_energy` is negative or not finite, or when the computed mass is not
258/// finite.
259#[must_use]
260pub fn mass_from_rest_energy(rest_energy: f64) -> Option<f64> {
261    if !is_nonnegative_finite(rest_energy) {
262        return None;
263    }
264
265    finite(rest_energy / SPEED_OF_LIGHT_SQUARED)
266}
267
268/// Computes total relativistic energy `E = γmc²` in joules.
269///
270/// Returns `None` when `mass` is negative or not finite, when `speed` is invalid, or when the
271/// computed energy is not finite.
272#[must_use]
273pub fn total_energy(mass: f64, speed: f64) -> Option<f64> {
274    if !is_nonnegative_finite(mass) {
275        return None;
276    }
277
278    let gamma = lorentz_factor(speed)?;
279    finite(gamma * mass * SPEED_OF_LIGHT_SQUARED)
280}
281
282/// Computes relativistic kinetic energy `KE = (γ - 1)mc²` in joules.
283///
284/// Returns `None` when `mass` is negative or not finite, when `speed` is invalid, or when the
285/// computed energy is negative or not finite.
286#[must_use]
287pub fn relativistic_kinetic_energy(mass: f64, speed: f64) -> Option<f64> {
288    if !is_nonnegative_finite(mass) {
289        return None;
290    }
291
292    let gamma = lorentz_factor(speed)?;
293    let kinetic_energy = (gamma - 1.0) * mass * SPEED_OF_LIGHT_SQUARED;
294    if kinetic_energy < 0.0 {
295        return None;
296    }
297
298    finite(kinetic_energy)
299}
300
301/// Computes relativistic momentum `p = γmv`.
302///
303/// Returns `None` when `mass` is negative or not finite, when `velocity` is not finite or has a
304/// magnitude greater than or equal to the speed of light, or when the computed momentum is not
305/// finite.
306///
307/// # Examples
308///
309/// ```rust
310/// use use_relativity::{SPEED_OF_LIGHT, relativistic_momentum};
311///
312/// let expected = 1.25 * SPEED_OF_LIGHT * 0.6;
313///
314/// assert!((relativistic_momentum(1.0, SPEED_OF_LIGHT * 0.6).unwrap() - expected).abs() < 1.0e-3);
315/// ```
316#[must_use]
317pub fn relativistic_momentum(mass: f64, velocity: f64) -> Option<f64> {
318    if !is_nonnegative_finite(mass) || !is_subluminal_velocity(velocity) {
319        return None;
320    }
321
322    let gamma = gamma_from_signed_beta(signed_beta(velocity)?)?;
323    finite(gamma * mass * velocity)
324}
325
326/// Computes rest mass from relativistic momentum and velocity using `m = p / (γv)`.
327///
328/// Returns `None` when `velocity` is zero, when `velocity` is not finite or has a magnitude
329/// greater than or equal to the speed of light, when `momentum` is not finite, or when the
330/// computed rest mass is negative or not finite.
331#[must_use]
332pub fn rest_mass_from_momentum_speed(momentum: f64, velocity: f64) -> Option<f64> {
333    if !momentum.is_finite() || !is_subluminal_velocity(velocity) || velocity == 0.0 {
334        return None;
335    }
336
337    let gamma = gamma_from_signed_beta(signed_beta(velocity)?)?;
338    let mass = momentum / (gamma * velocity);
339    if mass < 0.0 {
340        return None;
341    }
342
343    finite(mass)
344}
345
346/// Computes total energy from rest mass and momentum using `E = sqrt((pc)² + (mc²)²)`.
347///
348/// Returns `None` when `rest_mass` is negative or not finite, when `momentum` is not finite, or
349/// when the computed energy is not finite.
350#[must_use]
351pub fn energy_momentum_relation(rest_mass: f64, momentum: f64) -> Option<f64> {
352    if !is_nonnegative_finite(rest_mass) || !momentum.is_finite() {
353        return None;
354    }
355
356    let momentum_term = momentum * SPEED_OF_LIGHT;
357    let rest_energy = rest_mass * SPEED_OF_LIGHT_SQUARED;
358    let energy_squared = momentum_term.mul_add(momentum_term, rest_energy * rest_energy);
359    if !energy_squared.is_finite() || energy_squared < 0.0 {
360        return None;
361    }
362
363    finite(energy_squared.sqrt())
364}
365
366/// Computes rapidity `φ = atanh(β)` from a signed `beta`.
367///
368/// Returns `None` when `beta` is not finite, has an absolute value greater than or equal to
369/// `1.0`, or when the computed rapidity is not finite.
370#[must_use]
371pub fn rapidity_from_beta(beta: f64) -> Option<f64> {
372    if !beta.is_finite() || beta.abs() >= 1.0 {
373        return None;
374    }
375
376    finite(beta.atanh())
377}
378
379/// Computes `β = tanh(φ)` from rapidity.
380///
381/// Returns `None` when `rapidity` is not finite or when the computed beta is not finite.
382#[must_use]
383pub fn beta_from_rapidity(rapidity: f64) -> Option<f64> {
384    if !rapidity.is_finite() {
385        return None;
386    }
387
388    let beta = rapidity.tanh();
389    if beta.abs() >= 1.0 {
390        return None;
391    }
392
393    finite(beta)
394}
395
396/// Computes signed velocity `v = c * tanh(φ)` from rapidity.
397///
398/// Returns `None` when `rapidity` is not finite or when the computed velocity is not finite.
399#[must_use]
400pub fn speed_from_rapidity(rapidity: f64) -> Option<f64> {
401    beta_from_rapidity(rapidity).and_then(signed_speed_from_beta)
402}
403
404/// Computes relativistic velocity addition `u = (v + w) / (1 + vw / c²)`.
405///
406/// Returns `None` when either velocity is not finite or has a magnitude greater than or equal to
407/// the speed of light, when the denominator is zero, when the computed result is not finite, or
408/// when the result has a magnitude greater than or equal to the speed of light.
409///
410/// # Examples
411///
412/// ```rust
413/// use use_relativity::{SPEED_OF_LIGHT, velocity_addition};
414///
415/// let expected = SPEED_OF_LIGHT * 0.8;
416///
417/// assert!((velocity_addition(SPEED_OF_LIGHT * 0.5, SPEED_OF_LIGHT * 0.5).unwrap() - expected).abs() < 1.0e-3);
418/// ```
419#[must_use]
420pub fn velocity_addition(velocity_a: f64, velocity_b: f64) -> Option<f64> {
421    if !is_subluminal_velocity(velocity_a) || !is_subluminal_velocity(velocity_b) {
422        return None;
423    }
424
425    let denominator = 1.0 + ((velocity_a * velocity_b) / SPEED_OF_LIGHT_SQUARED);
426    if !denominator.is_finite() || denominator == 0.0 {
427        return None;
428    }
429
430    let velocity = (velocity_a + velocity_b) / denominator;
431    if velocity.abs() >= SPEED_OF_LIGHT {
432        return None;
433    }
434
435    finite(velocity)
436}
437
438/// Computes the longitudinal relativistic Doppler factor `D = sqrt((1 + β) / (1 - β))`.
439///
440/// Positive `beta` means the source is approaching the observer. Negative `beta` means the source
441/// is receding.
442///
443/// Returns `None` when `beta` is not finite, when `beta <= -1.0` or `beta >= 1.0`, or when the
444/// computed factor is not finite.
445#[must_use]
446pub fn doppler_factor_longitudinal_from_beta(beta: f64) -> Option<f64> {
447    if !beta.is_finite() || beta <= -1.0 || beta >= 1.0 {
448        return None;
449    }
450
451    let numerator = 1.0 + beta;
452    let denominator = 1.0 - beta;
453    if numerator <= 0.0 || denominator <= 0.0 {
454        return None;
455    }
456
457    finite((numerator / denominator).sqrt())
458}
459
460/// Computes observed longitudinal Doppler-shifted frequency `f_observed = f_emitted * D`.
461///
462/// Positive `beta` means the source is approaching the observer.
463///
464/// Returns `None` when `emitted_frequency` is negative or not finite, when `beta` is invalid, or
465/// when the computed frequency is not finite.
466///
467/// # Examples
468///
469/// ```rust
470/// use use_relativity::observed_frequency_longitudinal;
471///
472/// assert!((observed_frequency_longitudinal(100.0, 0.6).unwrap() - 200.0).abs() < 1.0e-12);
473/// ```
474#[must_use]
475pub fn observed_frequency_longitudinal(emitted_frequency: f64, beta: f64) -> Option<f64> {
476    if !is_nonnegative_finite(emitted_frequency) {
477        return None;
478    }
479
480    let doppler_factor = doppler_factor_longitudinal_from_beta(beta)?;
481    finite(emitted_frequency * doppler_factor)
482}
483
484/// A body with scalar rest mass and signed velocity.
485#[derive(Debug, Clone, Copy, PartialEq)]
486pub struct RelativisticBody {
487    /// Rest mass in kilograms.
488    pub rest_mass: f64,
489    /// Signed velocity in meters per second.
490    pub velocity: f64,
491}
492
493impl RelativisticBody {
494    /// Creates a relativistic body when `rest_mass` is finite and non-negative and `velocity` is
495    /// finite with a magnitude strictly less than the speed of light.
496    #[must_use]
497    pub fn new(rest_mass: f64, velocity: f64) -> Option<Self> {
498        if !is_nonnegative_finite(rest_mass) || !is_subluminal_velocity(velocity) {
499            return None;
500        }
501
502        Some(Self {
503            rest_mass,
504            velocity,
505        })
506    }
507
508    /// Computes the speed ratio magnitude `β` for the body's current velocity.
509    #[must_use]
510    pub fn beta(&self) -> Option<f64> {
511        beta(self.velocity.abs())
512    }
513
514    /// Computes the Lorentz factor `γ` for the body's current speed magnitude.
515    #[must_use]
516    pub fn lorentz_factor(&self) -> Option<f64> {
517        lorentz_factor(self.velocity.abs())
518    }
519
520    /// Computes the body's rest energy in joules.
521    #[must_use]
522    pub fn rest_energy(&self) -> Option<f64> {
523        rest_energy(self.rest_mass)
524    }
525
526    /// Computes the body's total relativistic energy in joules.
527    ///
528    /// # Examples
529    ///
530    /// ```rust
531    /// use use_relativity::{RelativisticBody, SPEED_OF_LIGHT};
532    ///
533    /// let body = RelativisticBody::new(1.0, SPEED_OF_LIGHT * 0.6).unwrap();
534    /// let expected = 1.25 * SPEED_OF_LIGHT * SPEED_OF_LIGHT;
535    ///
536    /// assert!((body.total_energy().unwrap() - expected).abs() < 1.0e-3);
537    /// ```
538    #[must_use]
539    pub fn total_energy(&self) -> Option<f64> {
540        total_energy(self.rest_mass, self.velocity.abs())
541    }
542
543    /// Computes the body's relativistic kinetic energy in joules.
544    #[must_use]
545    pub fn kinetic_energy(&self) -> Option<f64> {
546        relativistic_kinetic_energy(self.rest_mass, self.velocity.abs())
547    }
548
549    /// Computes the body's relativistic momentum in kilogram meters per second.
550    #[must_use]
551    pub fn momentum(&self) -> Option<f64> {
552        relativistic_momentum(self.rest_mass, self.velocity)
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    #![allow(clippy::float_cmp)]
559
560    use super::*;
561
562    const EPSILON: f64 = 1.0e-12;
563
564    fn approx_eq(actual: f64, expected: f64) -> bool {
565        let scale = expected.abs().max(1.0);
566        (actual - expected).abs() <= EPSILON * scale
567    }
568
569    fn assert_option_approx_eq(actual: Option<f64>, expected: f64) {
570        let value = actual.expect("expected Some(value)");
571        assert!(
572            approx_eq(value, expected),
573            "expected {expected}, got {value}"
574        );
575    }
576
577    #[test]
578    fn beta_helpers_validate_speed_ranges() {
579        assert_option_approx_eq(beta(SPEED_OF_LIGHT * 0.5), 0.5);
580        assert_eq!(beta(0.0), Some(0.0));
581        assert_eq!(beta(-1.0), None);
582        assert_eq!(beta(SPEED_OF_LIGHT), None);
583
584        assert_option_approx_eq(speed_from_beta(0.5), SPEED_OF_LIGHT * 0.5);
585        assert_eq!(speed_from_beta(1.0), None);
586        assert_eq!(speed_from_beta(-0.1), None);
587
588        assert!(is_subluminal_speed(0.0));
589        assert!(is_subluminal_speed(SPEED_OF_LIGHT * 0.5));
590        assert!(!is_subluminal_speed(SPEED_OF_LIGHT));
591        assert!(!is_subluminal_speed(f64::NAN));
592    }
593
594    #[test]
595    fn lorentz_helpers_compute_expected_gamma() {
596        assert_eq!(lorentz_factor_from_beta(0.0), Some(1.0));
597        assert_option_approx_eq(lorentz_factor_from_beta(0.6), 1.25);
598        assert_eq!(lorentz_factor_from_beta(1.0), None);
599
600        assert_option_approx_eq(lorentz_factor(SPEED_OF_LIGHT * 0.6), 1.25);
601    }
602
603    #[test]
604    fn time_dilation_helpers_compute_expected_values() {
605        assert_option_approx_eq(dilated_time(10.0, SPEED_OF_LIGHT * 0.6), 12.5);
606        assert_eq!(dilated_time(-10.0, SPEED_OF_LIGHT * 0.6), None);
607
608        assert_option_approx_eq(proper_time(12.5, SPEED_OF_LIGHT * 0.6), 10.0);
609        assert_eq!(proper_time(-12.5, SPEED_OF_LIGHT * 0.6), None);
610    }
611
612    #[test]
613    fn length_helpers_compute_expected_values() {
614        assert_option_approx_eq(contracted_length(10.0, SPEED_OF_LIGHT * 0.6), 8.0);
615        assert_eq!(contracted_length(-10.0, SPEED_OF_LIGHT * 0.6), None);
616
617        assert_option_approx_eq(proper_length(8.0, SPEED_OF_LIGHT * 0.6), 10.0);
618    }
619
620    #[test]
621    fn mass_energy_helpers_compute_expected_values() {
622        assert_option_approx_eq(rest_energy(1.0), SPEED_OF_LIGHT_SQUARED);
623        assert_eq!(rest_energy(-1.0), None);
624
625        assert_option_approx_eq(mass_from_rest_energy(SPEED_OF_LIGHT_SQUARED), 1.0);
626        assert_option_approx_eq(
627            total_energy(1.0, SPEED_OF_LIGHT * 0.6),
628            1.25 * SPEED_OF_LIGHT_SQUARED,
629        );
630        assert_option_approx_eq(
631            relativistic_kinetic_energy(1.0, SPEED_OF_LIGHT * 0.6),
632            0.25 * SPEED_OF_LIGHT_SQUARED,
633        );
634    }
635
636    #[test]
637    fn momentum_helpers_compute_expected_values() {
638        let expected_momentum = 1.25 * SPEED_OF_LIGHT * 0.6;
639
640        assert_option_approx_eq(
641            relativistic_momentum(1.0, SPEED_OF_LIGHT * 0.6),
642            expected_momentum,
643        );
644        assert_option_approx_eq(
645            relativistic_momentum(1.0, -SPEED_OF_LIGHT * 0.6),
646            -expected_momentum,
647        );
648        assert_eq!(relativistic_momentum(-1.0, SPEED_OF_LIGHT * 0.6), None);
649
650        assert_option_approx_eq(
651            rest_mass_from_momentum_speed(expected_momentum, SPEED_OF_LIGHT * 0.6),
652            1.0,
653        );
654        assert_eq!(rest_mass_from_momentum_speed(1.0, 0.0), None);
655
656        assert_option_approx_eq(energy_momentum_relation(1.0, 0.0), SPEED_OF_LIGHT_SQUARED);
657    }
658
659    #[test]
660    fn rapidity_helpers_compute_expected_values() {
661        assert_eq!(rapidity_from_beta(0.0), Some(0.0));
662        assert_eq!(beta_from_rapidity(0.0), Some(0.0));
663        assert_eq!(speed_from_rapidity(0.0), Some(0.0));
664    }
665
666    #[test]
667    fn velocity_addition_stays_subluminal() {
668        assert_option_approx_eq(
669            velocity_addition(SPEED_OF_LIGHT * 0.5, SPEED_OF_LIGHT * 0.5),
670            SPEED_OF_LIGHT * 0.8,
671        );
672        assert_eq!(velocity_addition(SPEED_OF_LIGHT, 1.0), None);
673    }
674
675    #[test]
676    fn doppler_helpers_compute_expected_values() {
677        assert_eq!(doppler_factor_longitudinal_from_beta(0.0), Some(1.0));
678        assert_option_approx_eq(doppler_factor_longitudinal_from_beta(0.6), 2.0);
679        assert_eq!(doppler_factor_longitudinal_from_beta(1.0), None);
680
681        assert_option_approx_eq(observed_frequency_longitudinal(100.0, 0.6), 200.0);
682        assert_eq!(observed_frequency_longitudinal(-100.0, 0.6), None);
683    }
684
685    #[test]
686    fn relativistic_body_validates_and_delegates() {
687        let body = RelativisticBody::new(1.0, SPEED_OF_LIGHT * 0.6).expect("expected valid body");
688
689        assert_option_approx_eq(body.lorentz_factor(), 1.25);
690        assert_option_approx_eq(body.momentum(), 1.25 * SPEED_OF_LIGHT * 0.6);
691        assert_eq!(RelativisticBody::new(-1.0, SPEED_OF_LIGHT * 0.6), None);
692        assert_eq!(RelativisticBody::new(1.0, SPEED_OF_LIGHT), None);
693    }
694}