1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub mod prelude;
8
9pub 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#[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#[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#[must_use]
121pub fn is_subluminal_speed(speed: f64) -> bool {
122 is_nonnegative_finite(speed) && speed < SPEED_OF_LIGHT
123}
124
125#[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#[must_use]
158pub fn lorentz_factor(speed: f64) -> Option<f64> {
159 beta(speed).and_then(lorentz_factor_from_beta)
160}
161
162#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq)]
486pub struct RelativisticBody {
487 pub rest_mass: f64,
489 pub velocity: f64,
491}
492
493impl RelativisticBody {
494 #[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 #[must_use]
510 pub fn beta(&self) -> Option<f64> {
511 beta(self.velocity.abs())
512 }
513
514 #[must_use]
516 pub fn lorentz_factor(&self) -> Option<f64> {
517 lorentz_factor(self.velocity.abs())
518 }
519
520 #[must_use]
522 pub fn rest_energy(&self) -> Option<f64> {
523 rest_energy(self.rest_mass)
524 }
525
526 #[must_use]
539 pub fn total_energy(&self) -> Option<f64> {
540 total_energy(self.rest_mass, self.velocity.abs())
541 }
542
543 #[must_use]
545 pub fn kinetic_energy(&self) -> Option<f64> {
546 relativistic_kinetic_energy(self.rest_mass, self.velocity.abs())
547 }
548
549 #[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}