1use crate::error::StreamError;
46
47#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct SpacetimePoint {
55 pub t: f64,
57 pub x: f64,
59}
60
61impl SpacetimePoint {
62 pub fn new(t: f64, x: f64) -> Self {
64 Self { t, x }
65 }
66
67 pub fn displacement(self, other: Self) -> Self {
71 Self {
72 t: other.t - self.t,
73 x: other.x - self.x,
74 }
75 }
76
77 pub fn norm_sq(self) -> f64 {
84 self.t * self.t - self.x * self.x
85 }
86
87 pub fn is_timelike(self) -> bool {
89 self.norm_sq() > 0.0
90 }
91
92 pub fn is_lightlike(self) -> bool {
96 self.norm_sq().abs() < 1e-10
97 }
98
99 pub fn is_spacelike(self) -> bool {
101 self.norm_sq() < 0.0
102 }
103
104 pub fn distance_to(self, other: Self) -> f64 {
111 let d = self.displacement(other);
112 d.norm_sq().abs().sqrt()
113 }
114}
115
116#[derive(Debug, Clone, Copy)]
139pub struct LorentzTransform {
140 beta: f64,
142 gamma: f64,
144}
145
146impl LorentzTransform {
147 pub fn new(beta: f64) -> Result<Self, StreamError> {
155 if beta.is_nan() || beta < 0.0 || beta >= 1.0 {
156 return Err(StreamError::LorentzConfigError {
157 reason: format!(
158 "beta must be in [0.0, 1.0) but got {beta}; \
159 beta >= 1 produces a division by zero in the Lorentz factor"
160 ),
161 });
162 }
163 let gamma = 1.0 / (1.0 - beta * beta).sqrt();
164 Ok(Self { beta, gamma })
165 }
166
167 pub fn from_rapidity(eta: f64) -> Result<Self, StreamError> {
177 if !eta.is_finite() {
178 return Err(StreamError::LorentzConfigError {
179 reason: format!("rapidity must be finite, got {eta}"),
180 });
181 }
182 let beta = eta.tanh();
183 Self::new(beta)
184 }
185
186 pub fn from_velocity(v: f64, c: f64) -> Result<Self, StreamError> {
195 if !v.is_finite() || !c.is_finite() {
196 return Err(StreamError::LorentzConfigError {
197 reason: format!("v and c must be finite, got v={v}, c={c}"),
198 });
199 }
200 if c == 0.0 {
201 return Err(StreamError::LorentzConfigError {
202 reason: "speed of light c must be non-zero".into(),
203 });
204 }
205 Self::new(v / c)
206 }
207
208 pub fn gamma_at(beta: f64) -> f64 {
213 (1.0 - beta * beta).sqrt().recip()
214 }
215
216 pub fn relativistic_momentum(&self, mass: f64) -> f64 {
222 mass * self.beta_times_gamma()
223 }
224
225 pub fn kinetic_energy_ratio(&self) -> f64 {
231 self.gamma() - 1.0
232 }
233
234 pub fn time_dilation_factor(&self) -> f64 {
240 self.gamma()
241 }
242
243 pub fn length_contraction_factor(&self) -> f64 {
247 1.0 / self.gamma()
248 }
249
250 pub fn beta_from_gamma(gamma: f64) -> Result<f64, StreamError> {
252 if gamma.is_nan() || gamma < 1.0 {
253 return Err(StreamError::LorentzConfigError {
254 reason: format!("gamma must be >= 1.0, got {gamma}"),
255 });
256 }
257 Ok((1.0 - 1.0 / (gamma * gamma)).sqrt())
258 }
259
260 pub fn rapidity(&self) -> f64 {
265 self.beta.atanh()
266 }
267
268 pub fn beta_times_gamma(&self) -> f64 {
273 self.beta * self.gamma
274 }
275
276 pub fn beta_from_rapidity(eta: f64) -> f64 {
280 eta.tanh()
281 }
282
283 pub fn proper_velocity(&self) -> f64 {
289 self.beta_times_gamma()
290 }
291
292 pub fn beta(&self) -> f64 {
294 self.beta
295 }
296
297 pub fn gamma(&self) -> f64 {
302 self.gamma
303 }
304
305 #[must_use]
315 pub fn transform(&self, p: SpacetimePoint) -> SpacetimePoint {
316 let t_prime = self.gamma * (p.t - self.beta * p.x);
317 let x_prime = self.gamma * (p.x - self.beta * p.t);
318 SpacetimePoint {
319 t: t_prime,
320 x: x_prime,
321 }
322 }
323
324 #[must_use]
337 pub fn inverse_transform(&self, p: SpacetimePoint) -> SpacetimePoint {
338 let t_orig = self.gamma * (p.t + self.beta * p.x);
339 let x_orig = self.gamma * (p.x + self.beta * p.t);
340 SpacetimePoint {
341 t: t_orig,
342 x: x_orig,
343 }
344 }
345
346 pub fn transform_batch(&self, points: &[SpacetimePoint]) -> Vec<SpacetimePoint> {
353 points.iter().map(|&p| self.transform(p)).collect()
354 }
355
356 pub fn inverse_transform_batch(&self, points: &[SpacetimePoint]) -> Vec<SpacetimePoint> {
364 points.iter().map(|&p| self.inverse_transform(p)).collect()
365 }
366
367 pub fn dilate_time(&self, t: f64) -> f64 {
375 self.gamma() * t
376 }
377
378 pub fn contract_length(&self, x: f64) -> f64 {
386 self.gamma() * x
387 }
388
389 pub fn time_contraction(&self, coordinate_time: f64) -> f64 {
398 coordinate_time / self.gamma()
399 }
400
401 pub fn is_ultrarelativistic(&self) -> bool {
407 self.beta > 0.9
408 }
409
410 pub fn momentum_factor(&self) -> f64 {
417 self.beta_times_gamma()
418 }
419
420 pub fn momentum(&self, mass: f64) -> f64 {
429 self.relativistic_momentum(mass)
430 }
431
432 pub fn relativistic_energy(&self, rest_mass: f64) -> f64 {
439 self.gamma() * rest_mass
440 }
441
442 pub fn kinetic_energy(&self, rest_mass: f64) -> f64 {
448 self.kinetic_energy_ratio() * rest_mass
449 }
450
451 pub fn energy_momentum_invariant(&self, rest_mass: f64) -> f64 {
456 let e = self.relativistic_energy(rest_mass);
457 let p = self.relativistic_momentum(rest_mass);
458 e * e - p * p
459 }
460
461 pub fn four_velocity_time(&self) -> f64 {
467 self.gamma()
468 }
469
470 pub fn proper_time_dilation(&self, dt: f64) -> f64 {
475 self.time_contraction(dt)
476 }
477
478 pub fn spacetime_interval(p1: SpacetimePoint, p2: SpacetimePoint) -> f64 {
489 p1.displacement(p2).norm_sq()
490 }
491
492 pub fn velocity_addition(beta1: f64, beta2: f64) -> Result<f64, StreamError> {
503 if beta1.is_nan() || beta1 < 0.0 || beta1 >= 1.0 {
504 return Err(StreamError::LorentzConfigError {
505 reason: format!("beta1 must be in [0.0, 1.0), got {beta1}"),
506 });
507 }
508 if beta2.is_nan() || beta2 < 0.0 || beta2 >= 1.0 {
509 return Err(StreamError::LorentzConfigError {
510 reason: format!("beta2 must be in [0.0, 1.0), got {beta2}"),
511 });
512 }
513 let composed = (beta1 + beta2) / (1.0 + beta1 * beta2);
514 if composed >= 1.0 {
515 return Err(StreamError::LorentzConfigError {
516 reason: format!("composed velocity {composed} >= 1.0 (speed of light)"),
517 });
518 }
519 Ok(composed)
520 }
521
522 pub fn proper_time(&self, coordinate_time: f64) -> f64 {
530 self.time_contraction(coordinate_time)
531 }
532
533 pub fn inverse(&self) -> Self {
543 Self { beta: -self.beta, gamma: self.gamma }
545 }
546
547 #[deprecated(since = "2.2.0", note = "Use `dilate_time` instead")]
551 pub fn time_dilation(&self, proper_time: f64) -> f64 {
552 self.dilate_time(proper_time)
553 }
554
555 pub fn compose(&self, other: &LorentzTransform) -> Result<Self, StreamError> {
572 self.composition(other)
573 }
574
575 pub fn boost_chain(betas: &[f64]) -> Result<Self, StreamError> {
585 let mut result = LorentzTransform::new(0.0)?;
586 for &beta in betas {
587 let next = LorentzTransform::new(beta)?;
588 result = result.compose(&next)?;
589 }
590 Ok(result)
591 }
592
593 pub fn is_identity(&self) -> bool {
598 self.beta.abs() < 1e-10
599 }
600
601 pub fn composition(&self, other: &Self) -> Result<Self, StreamError> {
611 let b1 = self.beta;
612 let b2 = other.beta;
613 let denom = 1.0 + b1 * b2;
614 if denom.abs() < 1e-15 {
615 return Err(StreamError::LorentzConfigError {
616 reason: "boost composition denominator too small (near-singular)".into(),
617 });
618 }
619 Self::new((b1 + b2) / denom)
620 }
621
622 #[deprecated(since = "2.2.0", note = "Use `doppler_ratio` instead")]
626 pub fn doppler_factor(&self) -> f64 {
627 self.doppler_ratio()
628 }
629
630 pub fn aberration_angle(&self, cos_theta: f64) -> f64 {
635 let cos_prime = (cos_theta + self.beta) / (1.0 + self.beta * cos_theta);
636 cos_prime.clamp(-1.0, 1.0).acos()
637 }
638
639 #[deprecated(since = "2.2.0", note = "Use `relativistic_energy` instead")]
644 pub fn relativistic_mass(&self, rest_mass: f64) -> f64 {
645 self.relativistic_energy(rest_mass)
646 }
647
648 #[deprecated(since = "2.2.0", note = "Use `kinetic_energy_ratio` instead")]
652 pub fn energy_ratio(&self) -> f64 {
653 self.kinetic_energy_ratio()
654 }
655
656 pub fn warp_factor(&self) -> f64 {
661 self.gamma().cbrt()
662 }
663
664 pub fn four_momentum(&self, mass: f64) -> (f64, f64) {
669 let g = self.gamma();
670 (g * mass, g * mass * self.beta)
671 }
672
673 #[deprecated(since = "2.2.0", note = "Use `beta_times_gamma` instead")]
677 pub fn momentum_ratio(&self) -> f64 {
678 self.beta_times_gamma()
679 }
680
681 #[deprecated(since = "2.2.0", note = "Use `is_ultrarelativistic` instead")]
685 pub fn is_ultra_relativistic(&self) -> bool {
686 self.is_ultrarelativistic()
687 }
688
689 pub fn lorentz_factor_approx(&self) -> f64 {
693 1.0 + 0.5 * self.beta * self.beta
694 }
695
696 pub fn velocity_ratio(&self, other: &Self) -> f64 {
701 self.beta / other.beta
702 }
703
704 pub fn proper_length(&self, observed: f64) -> f64 {
709 self.dilate_time(observed)
710 }
711
712 pub fn length_contraction(&self, rest_length: f64) -> f64 {
716 self.time_contraction(rest_length)
717 }
718
719 pub fn light_cone_check(dt: f64, dx: f64) -> &'static str {
726 let s_sq = dt * dt - dx * dx;
727 if s_sq > 1e-12 {
728 "timelike"
729 } else if s_sq < -1e-12 {
730 "spacelike"
731 } else {
732 "lightlike"
733 }
734 }
735
736 pub fn lorentz_invariant_mass(energy: f64, momentum: f64) -> Option<f64> {
740 let m_sq = energy * energy - momentum * momentum;
741 if m_sq < 0.0 { return None; }
742 Some(m_sq.sqrt())
743 }
744
745 pub fn aberration_correction(&self, cos_theta: f64) -> Option<f64> {
750 let denom = 1.0 - self.beta * cos_theta;
751 if denom.abs() < 1e-15 { return None; }
752 Some((cos_theta - self.beta) / denom)
753 }
754
755 pub fn doppler_ratio(&self) -> f64 {
760 ((1.0 + self.beta) / (1.0 - self.beta)).sqrt()
761 }
762
763 pub fn time_dilation_ms(&self, proper_ms: f64) -> f64 {
767 self.dilate_time(proper_ms)
768 }
769
770 #[deprecated(since = "2.2.0", note = "Use `length_contraction` instead")]
774 pub fn space_contraction(&self, proper_length: f64) -> f64 {
775 self.length_contraction(proper_length)
776 }
777
778 pub fn proper_acceleration(&self, force: f64, mass: f64) -> Option<f64> {
783 if mass == 0.0 { return None; }
784 Some(force / (self.gamma.powi(3) * mass))
785 }
786
787 pub fn momentum_rapidity(&self) -> f64 {
791 self.beta_times_gamma()
792 }
793
794 pub fn inverse_gamma(&self) -> f64 {
798 self.length_contraction_factor()
799 }
800
801 pub fn boost_composition(beta1: f64, beta2: f64) -> Result<f64, crate::error::StreamError> {
806 let denom = 1.0 + beta1 * beta2;
807 if denom.abs() < 1e-15 {
808 return Err(crate::error::StreamError::LorentzConfigError {
809 reason: "degenerate boost composition".into(),
810 });
811 }
812 let result = (beta1 + beta2) / denom;
813 if result.abs() >= 1.0 {
814 return Err(crate::error::StreamError::LorentzConfigError {
815 reason: "boost exceeds c".into(),
816 });
817 }
818 Ok(result)
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825
826 const EPS: f64 = 1e-10;
827
828 fn approx_eq(a: f64, b: f64) -> bool {
829 (a - b).abs() < EPS
830 }
831
832 fn point_approx_eq(a: SpacetimePoint, b: SpacetimePoint) -> bool {
833 approx_eq(a.t, b.t) && approx_eq(a.x, b.x)
834 }
835
836 #[test]
839 fn test_new_valid_beta() {
840 let lt = LorentzTransform::new(0.5).unwrap();
841 assert!((lt.beta() - 0.5).abs() < EPS);
842 }
843
844 #[test]
845 fn test_new_beta_zero() {
846 let lt = LorentzTransform::new(0.0).unwrap();
847 assert_eq!(lt.beta(), 0.0);
848 assert!((lt.gamma() - 1.0).abs() < EPS);
849 }
850
851 #[test]
852 fn test_new_beta_one_returns_error() {
853 let err = LorentzTransform::new(1.0).unwrap_err();
854 assert!(matches!(err, StreamError::LorentzConfigError { .. }));
855 }
856
857 #[test]
858 fn test_new_beta_above_one_returns_error() {
859 let err = LorentzTransform::new(1.5).unwrap_err();
860 assert!(matches!(err, StreamError::LorentzConfigError { .. }));
861 }
862
863 #[test]
864 fn test_new_beta_negative_returns_error() {
865 let err = LorentzTransform::new(-0.1).unwrap_err();
866 assert!(matches!(err, StreamError::LorentzConfigError { .. }));
867 }
868
869 #[test]
870 fn test_new_beta_nan_returns_error() {
871 let err = LorentzTransform::new(f64::NAN).unwrap_err();
872 assert!(matches!(err, StreamError::LorentzConfigError { .. }));
873 }
874
875 #[test]
878 fn test_beta_zero_is_identity_transform() {
879 let lt = LorentzTransform::new(0.0).unwrap();
880 let p = SpacetimePoint::new(3.0, 4.0);
881 let q = lt.transform(p);
882 assert!(point_approx_eq(p, q), "beta=0 must be identity, got {q:?}");
883 }
884
885 #[test]
889 fn test_time_dilation_at_x_zero() {
890 let lt = LorentzTransform::new(0.6).unwrap();
891 let p = SpacetimePoint::new(5.0, 0.0);
892 let q = lt.transform(p);
893 let expected_t = lt.gamma() * 5.0;
894 assert!(approx_eq(q.t, expected_t));
895 }
896
897 #[test]
898 fn test_dilate_time_helper() {
899 let lt = LorentzTransform::new(0.6).unwrap();
900 assert!(approx_eq(lt.dilate_time(1.0), lt.gamma()));
901 }
902
903 #[test]
907 fn test_length_contraction_at_t_zero() {
908 let lt = LorentzTransform::new(0.6).unwrap();
909 let p = SpacetimePoint::new(0.0, 5.0);
910 let q = lt.transform(p);
911 let expected_x = lt.gamma() * 5.0;
912 assert!(approx_eq(q.x, expected_x));
913 }
914
915 #[test]
916 fn test_contract_length_helper() {
917 let lt = LorentzTransform::new(0.6).unwrap();
918 assert!(approx_eq(lt.contract_length(1.0), lt.gamma()));
919 }
920
921 #[test]
925 fn test_known_beta_0_6_gamma_is_1_25() {
926 let lt = LorentzTransform::new(0.6).unwrap();
927 assert!(
928 (lt.gamma() - 1.25).abs() < 1e-9,
929 "gamma should be 1.25, got {}",
930 lt.gamma()
931 );
932 }
933
934 #[test]
936 fn test_known_beta_0_8_gamma() {
937 let lt = LorentzTransform::new(0.8).unwrap();
938 let expected_gamma = 5.0 / 3.0;
939 assert!((lt.gamma() - expected_gamma).abs() < 1e-9);
940 }
941
942 #[test]
944 fn test_known_beta_0_5_gamma() {
945 let lt = LorentzTransform::new(0.5).unwrap();
946 let expected_gamma = 2.0 / 3.0f64.sqrt();
947 assert!((lt.gamma() - expected_gamma).abs() < 1e-9);
948 }
949
950 #[test]
954 fn test_transform_known_point_beta_0_6() {
955 let lt = LorentzTransform::new(0.6).unwrap();
956 let p = SpacetimePoint::new(1.0, 0.0);
957 let q = lt.transform(p);
958 assert!((q.t - 1.25).abs() < 1e-9);
959 assert!((q.x - (-0.75)).abs() < 1e-9);
960 }
961
962 #[test]
965 fn test_inverse_transform_roundtrip() {
966 let lt = LorentzTransform::new(0.7).unwrap();
967 let p = SpacetimePoint::new(3.0, 1.5);
968 let q = lt.transform(p);
969 let r = lt.inverse_transform(q);
970 assert!(
971 point_approx_eq(r, p),
972 "round-trip failed: expected {p:?}, got {r:?}"
973 );
974 }
975
976 #[test]
979 fn test_transform_batch_length_preserved() {
980 let lt = LorentzTransform::new(0.3).unwrap();
981 let pts = vec![
982 SpacetimePoint::new(0.0, 1.0),
983 SpacetimePoint::new(1.0, 2.0),
984 SpacetimePoint::new(2.0, 3.0),
985 ];
986 let out = lt.transform_batch(&pts);
987 assert_eq!(out.len(), pts.len());
988 }
989
990 #[test]
991 fn test_transform_batch_matches_individual() {
992 let lt = LorentzTransform::new(0.4).unwrap();
993 let pts = vec![SpacetimePoint::new(1.0, 0.5), SpacetimePoint::new(2.0, 1.5)];
994 let batch = lt.transform_batch(&pts);
995 for (i, &p) in pts.iter().enumerate() {
996 let individual = lt.transform(p);
997 assert!(
998 point_approx_eq(batch[i], individual),
999 "batch[{i}] differs from individual transform"
1000 );
1001 }
1002 }
1003
1004 #[test]
1005 fn test_inverse_transform_batch_roundtrip() {
1006 let lt = LorentzTransform::new(0.5).unwrap();
1007 let pts = vec![
1008 SpacetimePoint::new(1.0, 0.0),
1009 SpacetimePoint::new(2.0, 1.0),
1010 SpacetimePoint::new(0.0, 3.0),
1011 ];
1012 let transformed = lt.transform_batch(&pts);
1013 let restored = lt.inverse_transform_batch(&transformed);
1014 for (i, (&orig, &rest)) in pts.iter().zip(restored.iter()).enumerate() {
1015 assert!(
1016 point_approx_eq(orig, rest),
1017 "round-trip failed at index {i}: expected {orig:?}, got {rest:?}"
1018 );
1019 }
1020 }
1021
1022 #[test]
1023 fn test_inverse_transform_batch_length_preserved() {
1024 let lt = LorentzTransform::new(0.3).unwrap();
1025 let pts = vec![SpacetimePoint::new(0.0, 0.0), SpacetimePoint::new(1.0, 1.0)];
1026 assert_eq!(lt.inverse_transform_batch(&pts).len(), pts.len());
1027 }
1028
1029 #[test]
1032 fn test_spacetime_point_fields() {
1033 let p = SpacetimePoint::new(1.5, 2.5);
1034 assert_eq!(p.t, 1.5);
1035 assert_eq!(p.x, 2.5);
1036 }
1037
1038 #[test]
1039 fn test_spacetime_point_equality() {
1040 let p = SpacetimePoint::new(1.0, 2.0);
1041 let q = SpacetimePoint::new(1.0, 2.0);
1042 assert_eq!(p, q);
1043 }
1044
1045 #[test]
1049 fn test_compose_identity_with_identity() {
1050 let lt = LorentzTransform::new(0.0).unwrap();
1051 let composed = lt.compose(<).unwrap();
1052 assert!(approx_eq(composed.beta(), 0.0));
1053 }
1054
1055 #[test]
1057 fn test_compose_0_5_and_0_5() {
1058 let lt = LorentzTransform::new(0.5).unwrap();
1059 let composed = lt.compose(<).unwrap();
1060 let expected = (0.5 + 0.5) / (1.0 + 0.5 * 0.5); assert!((composed.beta() - expected).abs() < EPS);
1062 }
1063
1064 #[test]
1066 fn test_compose_with_negative_is_identity() {
1067 let lt_fwd = LorentzTransform::new(0.3).unwrap();
1069 let lt_bwd = LorentzTransform::new(0.3).unwrap();
1070 let composed = lt_fwd.compose(<_bwd).unwrap();
1074 assert!(composed.beta() < 1.0);
1075 }
1076
1077 #[test]
1081 fn test_rapidity_zero_beta() {
1082 let lt = LorentzTransform::new(0.0).unwrap();
1083 assert!(approx_eq(lt.rapidity(), 0.0));
1084 }
1085
1086 #[test]
1088 fn test_rapidity_known_value() {
1089 let lt = LorentzTransform::new(0.5).unwrap();
1090 let expected = (0.5f64).atanh();
1091 assert!((lt.rapidity() - expected).abs() < EPS);
1092 }
1093
1094 #[test]
1096 fn test_rapidity_is_additive_under_composition() {
1097 let lt1 = LorentzTransform::new(0.3).unwrap();
1098 let lt2 = LorentzTransform::new(0.4).unwrap();
1099 let composed = lt1.compose(<2).unwrap();
1100 let sum_rapidities = lt1.rapidity() + lt2.rapidity();
1101 assert!(
1102 (composed.rapidity() - sum_rapidities).abs() < 1e-9,
1103 "rapidity should be additive: {} vs {}",
1104 composed.rapidity(),
1105 sum_rapidities
1106 );
1107 }
1108
1109 #[test]
1112 fn test_velocity_addition_known_values() {
1113 let result = LorentzTransform::velocity_addition(0.5, 0.5).unwrap();
1115 assert!((result - 0.8).abs() < EPS);
1116 }
1117
1118 #[test]
1119 fn test_velocity_addition_identity_with_zero() {
1120 let result = LorentzTransform::velocity_addition(0.0, 0.6).unwrap();
1121 assert!((result - 0.6).abs() < EPS);
1122 }
1123
1124 #[test]
1125 fn test_velocity_addition_invalid_beta_rejected() {
1126 assert!(LorentzTransform::velocity_addition(1.0, 0.5).is_err());
1127 assert!(LorentzTransform::velocity_addition(0.5, 1.0).is_err());
1128 assert!(LorentzTransform::velocity_addition(-0.1, 0.5).is_err());
1129 }
1130
1131 #[test]
1132 fn test_velocity_addition_matches_compose() {
1133 let b1 = 0.3;
1134 let b2 = 0.4;
1135 let static_result = LorentzTransform::velocity_addition(b1, b2).unwrap();
1136 let lt1 = LorentzTransform::new(b1).unwrap();
1137 let lt2 = LorentzTransform::new(b2).unwrap();
1138 let composed = lt1.compose(<2).unwrap();
1139 assert!((static_result - composed.beta()).abs() < EPS);
1140 }
1141
1142 #[test]
1146 fn test_proper_time_identity_at_zero_beta() {
1147 let lt = LorentzTransform::new(0.0).unwrap();
1148 assert!(approx_eq(lt.proper_time(10.0), 10.0));
1149 }
1150
1151 #[test]
1153 fn test_proper_time_less_than_coordinate_time() {
1154 let lt = LorentzTransform::new(0.6).unwrap(); let tau = lt.proper_time(5.0);
1156 assert!((tau - 4.0).abs() < EPS);
1158 assert!(tau < 5.0);
1159 }
1160
1161 #[test]
1163 fn test_proper_time_roundtrip_with_dilate_time() {
1164 let lt = LorentzTransform::new(0.8).unwrap();
1165 let t = 3.0;
1166 let dilated = lt.dilate_time(t);
1167 let recovered = lt.proper_time(dilated);
1168 assert!(approx_eq(recovered, t));
1169 }
1170
1171 #[test]
1173 fn test_compose_equals_sequential_transforms() {
1174 let lt1 = LorentzTransform::new(0.3).unwrap();
1175 let lt2 = LorentzTransform::new(0.4).unwrap();
1176 let composed = lt1.compose(<2).unwrap();
1177
1178 let p = SpacetimePoint::new(2.0, 1.0);
1179 let sequential = lt2.transform(lt1.transform(p));
1180 let single = composed.transform(p);
1181 assert!(
1182 point_approx_eq(sequential, single),
1183 "composed boost must equal sequential: {sequential:?} vs {single:?}"
1184 );
1185 }
1186
1187 #[test]
1190 fn test_displacement_gives_correct_deltas() {
1191 let a = SpacetimePoint::new(1.0, 2.0);
1192 let b = SpacetimePoint::new(4.0, 6.0);
1193 let d = a.displacement(b);
1194 assert!(approx_eq(d.t, 3.0));
1195 assert!(approx_eq(d.x, 4.0));
1196 }
1197
1198 #[test]
1199 fn test_displacement_to_self_is_zero() {
1200 let a = SpacetimePoint::new(3.0, 7.0);
1201 let d = a.displacement(a);
1202 assert!(approx_eq(d.t, 0.0));
1203 assert!(approx_eq(d.x, 0.0));
1204 }
1205
1206 #[test]
1209 fn test_boost_chain_empty_is_identity() {
1210 let chain = LorentzTransform::boost_chain(&[]).unwrap();
1211 assert!(approx_eq(chain.beta(), 0.0));
1212 assert!(approx_eq(chain.gamma(), 1.0));
1213 }
1214
1215 #[test]
1216 fn test_boost_chain_single_equals_new() {
1217 let chain = LorentzTransform::boost_chain(&[0.5]).unwrap();
1218 let direct = LorentzTransform::new(0.5).unwrap();
1219 assert!(approx_eq(chain.beta(), direct.beta()));
1220 }
1221
1222 #[test]
1223 fn test_boost_chain_two_equals_compose() {
1224 let chain = LorentzTransform::boost_chain(&[0.3, 0.4]).unwrap();
1225 let manual = LorentzTransform::new(0.3)
1226 .unwrap()
1227 .compose(&LorentzTransform::new(0.4).unwrap())
1228 .unwrap();
1229 assert!(approx_eq(chain.beta(), manual.beta()));
1230 }
1231
1232 #[test]
1233 fn test_boost_chain_invalid_beta_returns_error() {
1234 assert!(LorentzTransform::boost_chain(&[1.0]).is_err());
1235 assert!(LorentzTransform::boost_chain(&[0.3, -0.1]).is_err());
1236 }
1237
1238 #[test]
1241 fn test_time_dilation_identity_at_zero_beta() {
1242 let lt = LorentzTransform::new(0.0).unwrap();
1244 assert!(approx_eq(lt.time_dilation(10.0), 10.0));
1245 }
1246
1247 #[test]
1248 fn test_time_dilation_inverse_of_proper_time() {
1249 let lt = LorentzTransform::new(0.6).unwrap();
1251 let t = 5.0_f64;
1252 let tau = lt.proper_time(t);
1253 assert!(approx_eq(lt.time_dilation(tau), t));
1254 }
1255
1256 #[test]
1257 fn test_time_dilation_greater_than_proper_time() {
1258 let lt = LorentzTransform::new(0.8).unwrap();
1260 let proper = 3.0_f64;
1261 let coord = lt.time_dilation(proper);
1262 assert!(coord > proper);
1263 }
1264
1265 #[test]
1268 fn test_inverse_negates_beta() {
1269 let lt = LorentzTransform::new(0.6).unwrap();
1270 let inv = lt.inverse();
1271 assert!(approx_eq(inv.beta(), -0.6));
1272 }
1273
1274 #[test]
1275 fn test_inverse_preserves_gamma_magnitude() {
1276 let lt = LorentzTransform::new(0.6).unwrap();
1277 let inv = lt.inverse();
1278 assert!(approx_eq(inv.gamma(), lt.gamma()));
1280 }
1281
1282 #[test]
1283 fn test_inverse_roundtrips_point() {
1284 let lt = LorentzTransform::new(0.5).unwrap();
1285 let p = SpacetimePoint::new(3.0, 1.0);
1286 let boosted = lt.transform(p);
1287 let recovered = lt.inverse().transform(boosted);
1288 assert!(point_approx_eq(recovered, p));
1289 }
1290
1291 #[test]
1294 fn test_norm_sq_timelike() {
1295 let p = SpacetimePoint::new(3.0, 1.0);
1297 assert!((p.norm_sq() - 8.0).abs() < EPS);
1298 assert!(p.is_timelike());
1299 assert!(!p.is_spacelike());
1300 assert!(!p.is_lightlike());
1301 }
1302
1303 #[test]
1304 fn test_norm_sq_spacelike() {
1305 let p = SpacetimePoint::new(1.0, 3.0);
1307 assert!((p.norm_sq() - (-8.0)).abs() < EPS);
1308 assert!(p.is_spacelike());
1309 assert!(!p.is_timelike());
1310 assert!(!p.is_lightlike());
1311 }
1312
1313 #[test]
1314 fn test_norm_sq_lightlike() {
1315 let p = SpacetimePoint::new(1.0, 1.0);
1317 assert!(p.norm_sq().abs() < EPS);
1318 assert!(p.is_lightlike());
1319 assert!(!p.is_timelike());
1320 assert!(!p.is_spacelike());
1321 }
1322
1323 #[test]
1324 fn test_norm_sq_origin_is_lightlike() {
1325 let p = SpacetimePoint::new(0.0, 0.0);
1326 assert!(p.is_lightlike());
1327 }
1328
1329 #[test]
1332 fn test_is_identity_beta_zero() {
1333 let lt = LorentzTransform::new(0.0).unwrap();
1334 assert!(lt.is_identity());
1335 }
1336
1337 #[test]
1338 fn test_is_not_identity_nonzero_beta() {
1339 let lt = LorentzTransform::new(0.5).unwrap();
1340 assert!(!lt.is_identity());
1341 }
1342
1343 #[test]
1344 fn test_is_identity_empty_boost_chain() {
1345 let lt = LorentzTransform::boost_chain(&[]).unwrap();
1346 assert!(lt.is_identity());
1347 }
1348
1349 #[test]
1352 fn test_beta_from_gamma_identity() {
1353 let beta = LorentzTransform::beta_from_gamma(1.0).unwrap();
1355 assert!(approx_eq(beta, 0.0));
1356 }
1357
1358 #[test]
1359 fn test_beta_from_gamma_roundtrip() {
1360 let lt = LorentzTransform::new(0.6).unwrap();
1361 let recovered = LorentzTransform::beta_from_gamma(lt.gamma()).unwrap();
1362 assert!(approx_eq(recovered, 0.6));
1363 }
1364
1365 #[test]
1366 fn test_beta_from_gamma_invalid_rejected() {
1367 assert!(LorentzTransform::beta_from_gamma(0.5).is_err()); assert!(LorentzTransform::beta_from_gamma(f64::NAN).is_err());
1369 assert!(LorentzTransform::beta_from_gamma(-1.0).is_err());
1370 }
1371
1372 #[test]
1375 fn test_from_rapidity_zero_is_identity() {
1376 let lt = LorentzTransform::from_rapidity(0.0).unwrap();
1377 assert!(lt.is_identity());
1378 }
1379
1380 #[test]
1381 fn test_from_rapidity_positive_gives_valid_beta() {
1382 let lt = LorentzTransform::from_rapidity(0.5).unwrap();
1384 assert!((lt.beta() - 0.5_f64.tanh()).abs() < EPS);
1385 }
1386
1387 #[test]
1388 fn test_from_rapidity_infinite_rejected() {
1389 assert!(LorentzTransform::from_rapidity(f64::INFINITY).is_err());
1390 assert!(LorentzTransform::from_rapidity(f64::NEG_INFINITY).is_err());
1391 assert!(LorentzTransform::from_rapidity(f64::NAN).is_err());
1392 }
1393
1394 #[test]
1397 fn test_distance_to_timelike_separation() {
1398 let a = SpacetimePoint::new(0.0, 0.0);
1400 let b = SpacetimePoint::new(3.0, 0.0);
1401 assert!((a.distance_to(b) - 3.0).abs() < EPS);
1402 }
1403
1404 #[test]
1405 fn test_distance_to_spacelike_separation() {
1406 let a = SpacetimePoint::new(0.0, 0.0);
1408 let b = SpacetimePoint::new(0.0, 4.0);
1409 assert!((a.distance_to(b) - 4.0).abs() < EPS);
1410 }
1411
1412 #[test]
1413 fn test_distance_to_same_point_is_zero() {
1414 let p = SpacetimePoint::new(2.0, 3.0);
1415 assert!(p.distance_to(p).abs() < EPS);
1416 }
1417
1418 #[test]
1421 fn test_gamma_at_zero_is_one() {
1422 assert!((LorentzTransform::gamma_at(0.0) - 1.0).abs() < EPS);
1423 }
1424
1425 #[test]
1426 fn test_gamma_at_matches_constructor_gamma() {
1427 let lt = LorentzTransform::new(0.6).unwrap();
1428 let expected = lt.gamma();
1429 let computed = LorentzTransform::gamma_at(0.6);
1430 assert!((computed - expected).abs() < EPS);
1431 }
1432
1433 #[test]
1434 fn test_gamma_at_one_is_infinite() {
1435 assert!(LorentzTransform::gamma_at(1.0).is_infinite());
1436 }
1437
1438 #[test]
1439 fn test_gamma_at_above_one_is_nan() {
1440 assert!(LorentzTransform::gamma_at(1.1).is_nan());
1441 }
1442
1443 #[test]
1444 fn test_from_velocity_matches_beta_ratio() {
1445 let t = LorentzTransform::from_velocity(0.6, 1.0).unwrap();
1447 assert!((t.beta() - 0.6).abs() < 1e-12);
1448 }
1449
1450 #[test]
1451 fn test_from_velocity_zero_c_returns_error() {
1452 assert!(matches!(
1453 LorentzTransform::from_velocity(0.5, 0.0),
1454 Err(StreamError::LorentzConfigError { .. })
1455 ));
1456 }
1457
1458 #[test]
1459 fn test_from_velocity_non_finite_returns_error() {
1460 assert!(LorentzTransform::from_velocity(f64::INFINITY, 1.0).is_err());
1461 assert!(LorentzTransform::from_velocity(0.5, f64::NAN).is_err());
1462 }
1463
1464 #[test]
1465 fn test_from_velocity_superluminal_returns_error() {
1466 assert!(LorentzTransform::from_velocity(1.5, 1.0).is_err());
1468 }
1469
1470 #[test]
1473 fn test_relativistic_momentum_zero_beta_is_zero() {
1474 let lt = LorentzTransform::new(0.0).unwrap();
1475 assert!(approx_eq(lt.relativistic_momentum(1.0), 0.0));
1476 }
1477
1478 #[test]
1479 fn test_relativistic_momentum_known_value() {
1480 let lt = LorentzTransform::new(0.6).unwrap();
1482 assert!((lt.relativistic_momentum(2.0) - 1.5).abs() < 1e-9);
1483 }
1484
1485 #[test]
1486 fn test_relativistic_momentum_scales_with_mass() {
1487 let lt = LorentzTransform::new(0.6).unwrap();
1488 let p1 = lt.relativistic_momentum(1.0);
1489 let p2 = lt.relativistic_momentum(2.0);
1490 assert!((p2 - 2.0 * p1).abs() < 1e-9);
1491 }
1492
1493 #[test]
1496 fn test_kinetic_energy_ratio_zero_at_rest() {
1497 let lt = LorentzTransform::new(0.0).unwrap();
1498 assert!(approx_eq(lt.kinetic_energy_ratio(), 0.0));
1499 }
1500
1501 #[test]
1502 fn test_kinetic_energy_ratio_known_value() {
1503 let lt = LorentzTransform::new(0.6).unwrap();
1505 assert!((lt.kinetic_energy_ratio() - 0.25).abs() < 1e-9);
1506 }
1507
1508 #[test]
1509 fn test_kinetic_energy_ratio_positive_for_nonzero_beta() {
1510 let lt = LorentzTransform::new(0.8).unwrap();
1511 assert!(lt.kinetic_energy_ratio() > 0.0);
1512 }
1513
1514 #[test]
1515 fn test_composition_zero_with_zero_is_zero() {
1516 let t1 = LorentzTransform::new(0.0).unwrap();
1517 let t2 = LorentzTransform::new(0.0).unwrap();
1518 let composed = t1.composition(&t2).unwrap();
1519 assert!(composed.beta().abs() < 1e-12);
1520 }
1521
1522 #[test]
1523 fn test_composition_with_opposite_is_near_zero() {
1524 let t1 = LorentzTransform::new(0.6).unwrap();
1525 let t2 = t1.inverse(); let composed = t1.composition(&t2).unwrap();
1527 assert!(composed.beta().abs() < 1e-12);
1529 }
1530
1531 #[test]
1532 fn test_composition_velocity_addition_known_value() {
1533 let t1 = LorentzTransform::new(0.5).unwrap();
1535 let t2 = LorentzTransform::new(0.5).unwrap();
1536 let composed = t1.composition(&t2).unwrap();
1537 assert!((composed.beta() - 0.8).abs() < 1e-12);
1538 }
1539
1540 #[test]
1543 fn test_time_dilation_factor_one_at_rest() {
1544 let t = LorentzTransform::new(0.0).unwrap();
1545 assert!((t.time_dilation_factor() - 1.0).abs() < 1e-12);
1546 }
1547
1548 #[test]
1549 fn test_time_dilation_factor_equals_gamma() {
1550 let t = LorentzTransform::new(0.6).unwrap();
1551 assert!((t.time_dilation_factor() - t.gamma()).abs() < 1e-12);
1552 }
1553
1554 #[test]
1555 fn test_time_dilation_factor_greater_than_one_for_nonzero_beta() {
1556 let t = LorentzTransform::new(0.8).unwrap();
1557 assert!(t.time_dilation_factor() > 1.0);
1558 }
1559
1560 #[test]
1563 fn test_time_contraction_inverse_of_dilate_time() {
1564 let t = LorentzTransform::new(0.6).unwrap();
1565 let original = 100.0_f64;
1566 let dilated = t.dilate_time(original);
1567 let contracted = t.time_contraction(dilated);
1568 assert!((contracted - original).abs() < 1e-10);
1569 }
1570
1571 #[test]
1572 fn test_time_contraction_at_zero_beta_equals_input() {
1573 let t = LorentzTransform::new(0.0).unwrap();
1574 assert!((t.time_contraction(42.0) - 42.0).abs() < 1e-12);
1575 }
1576
1577 #[test]
1578 fn test_time_contraction_less_than_input_for_nonzero_beta() {
1579 let t = LorentzTransform::new(0.8).unwrap();
1580 assert!(t.time_contraction(100.0) < 100.0);
1581 }
1582
1583 #[test]
1586 fn test_length_contraction_factor_one_at_rest() {
1587 let t = LorentzTransform::new(0.0).unwrap();
1588 assert!((t.length_contraction_factor() - 1.0).abs() < 1e-12);
1589 }
1590
1591 #[test]
1592 fn test_length_contraction_factor_is_reciprocal_of_gamma() {
1593 let t = LorentzTransform::new(0.6).unwrap();
1594 assert!((t.length_contraction_factor() - 1.0 / t.gamma()).abs() < 1e-12);
1595 }
1596
1597 #[test]
1598 fn test_length_contraction_factor_less_than_one_for_nonzero_beta() {
1599 let t = LorentzTransform::new(0.9).unwrap();
1600 assert!(t.length_contraction_factor() < 1.0);
1601 }
1602
1603 #[test]
1606 fn test_proper_velocity_zero_at_rest() {
1607 let t = LorentzTransform::new(0.0).unwrap();
1608 assert!((t.proper_velocity() - 0.0).abs() < 1e-12);
1609 }
1610
1611 #[test]
1612 fn test_proper_velocity_equals_beta_times_gamma() {
1613 let t = LorentzTransform::new(0.6).unwrap();
1614 assert!((t.proper_velocity() - t.beta() * t.gamma()).abs() < 1e-12);
1615 }
1616
1617 #[test]
1618 fn test_proper_velocity_greater_than_beta_for_high_speed() {
1619 let t = LorentzTransform::new(0.8).unwrap();
1621 assert!(t.proper_velocity() > t.beta());
1622 }
1623
1624 #[test]
1627 fn test_is_ultrarelativistic_true_above_0_9() {
1628 let t = LorentzTransform::new(0.95).unwrap();
1629 assert!(t.is_ultrarelativistic());
1630 }
1631
1632 #[test]
1633 fn test_is_ultrarelativistic_false_below_0_9() {
1634 let t = LorentzTransform::new(0.5).unwrap();
1635 assert!(!t.is_ultrarelativistic());
1636 }
1637
1638 #[test]
1639 fn test_is_ultrarelativistic_false_at_exactly_0_9() {
1640 let t = LorentzTransform::new(0.9).unwrap();
1641 assert!(!t.is_ultrarelativistic()); }
1643
1644 #[test]
1645 fn test_momentum_factor_zero_at_rest() {
1646 let t = LorentzTransform::new(0.0).unwrap();
1647 assert!(t.momentum_factor().abs() < 1e-12);
1648 }
1649
1650 #[test]
1651 fn test_momentum_factor_equals_gamma_times_beta() {
1652 let t = LorentzTransform::new(0.6).unwrap();
1653 assert!((t.momentum_factor() - t.gamma() * t.beta()).abs() < 1e-12);
1654 }
1655
1656 #[test]
1659 fn test_relativistic_energy_equals_rest_mass_at_rest() {
1660 let t = LorentzTransform::new(0.0).unwrap();
1661 assert!((t.relativistic_energy(1.0) - 1.0).abs() < 1e-12);
1662 }
1663
1664 #[test]
1665 fn test_relativistic_energy_greater_than_rest_mass_for_moving_particle() {
1666 let t = LorentzTransform::new(0.6).unwrap();
1667 assert!(t.relativistic_energy(1.0) > 1.0);
1668 }
1669
1670 #[test]
1671 fn test_relativistic_energy_equals_gamma_times_rest_mass() {
1672 let t = LorentzTransform::new(0.8).unwrap();
1673 let m = 2.5;
1674 assert!((t.relativistic_energy(m) - t.gamma() * m).abs() < 1e-12);
1675 }
1676
1677 #[test]
1680 fn test_kinetic_energy_zero_at_rest() {
1681 let t = LorentzTransform::new(0.0).unwrap();
1682 assert!(t.kinetic_energy(1.0).abs() < 1e-12);
1683 }
1684
1685 #[test]
1686 fn test_kinetic_energy_positive_for_moving_particle() {
1687 let t = LorentzTransform::new(0.6).unwrap();
1688 assert!(t.kinetic_energy(1.0) > 0.0);
1689 }
1690
1691 #[test]
1692 fn test_kinetic_energy_equals_total_minus_rest() {
1693 let t = LorentzTransform::new(0.8).unwrap();
1694 let m = 3.0;
1695 let ke = t.kinetic_energy(m);
1696 let total = t.relativistic_energy(m);
1697 assert!((ke - (total - m)).abs() < 1e-12);
1698 }
1699
1700 #[test]
1703 fn test_four_velocity_time_equals_gamma() {
1704 let t = LorentzTransform::new(0.6).unwrap();
1705 assert!((t.four_velocity_time() - t.gamma()).abs() < 1e-12);
1706 }
1707
1708 #[test]
1709 fn test_four_velocity_time_one_at_rest() {
1710 let t = LorentzTransform::new(0.0).unwrap();
1711 assert!((t.four_velocity_time() - 1.0).abs() < 1e-12);
1712 }
1713
1714 #[test]
1715 fn test_four_velocity_time_greater_than_one_when_moving() {
1716 let t = LorentzTransform::new(0.5).unwrap();
1717 assert!(t.four_velocity_time() > 1.0);
1718 }
1719
1720 #[test]
1722 fn test_proper_time_dilation_at_rest_equals_dt() {
1723 let t = LorentzTransform::new(0.0).unwrap(); assert!((t.proper_time_dilation(10.0) - 10.0).abs() < 1e-10);
1725 }
1726
1727 #[test]
1728 fn test_proper_time_dilation_less_than_dt_when_moving() {
1729 let t = LorentzTransform::new(0.6).unwrap(); let proper = t.proper_time_dilation(10.0);
1731 assert!(proper < 10.0, "moving clock should run slow: {proper}");
1732 }
1733
1734 #[test]
1735 fn test_proper_time_dilation_approaches_zero_at_high_speed() {
1736 let t = LorentzTransform::new(0.9999).unwrap();
1737 let proper = t.proper_time_dilation(1.0);
1738 assert!(proper < 0.02, "expected near-zero proper time at 0.9999c: {proper}");
1739 }
1740
1741 #[test]
1743 fn test_beta_from_rapidity_zero_rapidity_gives_zero_beta() {
1744 assert!((LorentzTransform::beta_from_rapidity(0.0)).abs() < 1e-12);
1745 }
1746
1747 #[test]
1748 fn test_beta_from_rapidity_roundtrip_with_rapidity() {
1749 let t = LorentzTransform::new(0.7).unwrap();
1750 let eta = t.rapidity();
1751 let beta = LorentzTransform::beta_from_rapidity(eta);
1752 assert!((beta - 0.7).abs() < 1e-10, "roundtrip failed: {beta}");
1753 }
1754
1755 #[test]
1756 fn test_beta_from_rapidity_bounded_below_one() {
1757 let beta = LorentzTransform::beta_from_rapidity(10.0);
1759 assert!(beta < 1.0 && beta > 0.9999);
1760 }
1761
1762 #[test]
1765 fn test_rapidity_zero_at_rest() {
1766 let t = LorentzTransform::new(0.0).unwrap();
1767 assert!(t.rapidity().abs() < 1e-12);
1768 }
1769
1770 #[test]
1771 fn test_rapidity_equals_atanh_beta() {
1772 let t = LorentzTransform::new(0.5).unwrap();
1773 assert!((t.rapidity() - 0.5_f64.atanh()).abs() < 1e-12);
1774 }
1775
1776 #[test]
1777 fn test_rapidity_positive_for_nonzero_beta() {
1778 let t = LorentzTransform::new(0.8).unwrap();
1779 assert!(t.rapidity() > 0.0);
1780 }
1781
1782 #[test]
1785 fn test_doppler_factor_one_at_rest() {
1786 let t = LorentzTransform::new(0.0).unwrap();
1787 assert!((t.doppler_factor() - 1.0).abs() < 1e-12);
1788 }
1789
1790 #[test]
1791 fn test_doppler_factor_greater_than_one_when_approaching() {
1792 let t = LorentzTransform::new(0.6).unwrap();
1793 assert!((t.doppler_factor() - 2.0).abs() < 1e-10);
1795 }
1796
1797 #[test]
1798 fn test_doppler_factor_inverse_of_receding() {
1799 let t1 = LorentzTransform::new(0.6).unwrap();
1800 let t2 = LorentzTransform::new(0.6).unwrap();
1801 let fwd = t1.doppler_factor();
1803 let rev = 1.0 / t2.doppler_factor();
1804 assert!((fwd - 1.0 / rev).abs() < 1e-10);
1805 }
1806
1807 #[test]
1810 fn test_aberration_angle_at_rest_unchanged() {
1811 let t = LorentzTransform::new(0.0).unwrap();
1812 let angle_in = std::f64::consts::PI / 3.0; let cos_in = angle_in.cos();
1814 let angle_out = t.aberration_angle(cos_in);
1815 assert!((angle_out - angle_in).abs() < 1e-12);
1816 }
1817
1818 #[test]
1819 fn test_aberration_angle_head_on_unchanged() {
1820 let t = LorentzTransform::new(0.5).unwrap();
1821 let angle_out = t.aberration_angle(1.0);
1823 assert!(angle_out.abs() < 1e-12);
1824 }
1825
1826 #[test]
1827 fn test_aberration_angle_range_zero_to_pi() {
1828 let t = LorentzTransform::new(0.8).unwrap();
1829 let angle = t.aberration_angle(0.0);
1830 assert!(angle >= 0.0 && angle <= std::f64::consts::PI);
1831 }
1832
1833 #[test]
1836 fn test_relativistic_mass_at_rest_equals_rest_mass() {
1837 let t = LorentzTransform::new(0.0).unwrap();
1838 assert!((t.relativistic_mass(1.0) - 1.0).abs() < 1e-12);
1839 }
1840
1841 #[test]
1842 fn test_relativistic_mass_increases_with_speed() {
1843 let t = LorentzTransform::new(0.6).unwrap();
1844 assert!((t.relativistic_mass(1.0) - 1.25).abs() < 1e-10);
1846 }
1847
1848 #[test]
1849 fn test_energy_ratio_zero_at_rest() {
1850 let t = LorentzTransform::new(0.0).unwrap();
1851 assert!(t.energy_ratio().abs() < 1e-12);
1852 }
1853
1854 #[test]
1855 fn test_energy_ratio_positive_for_nonzero_beta() {
1856 let t = LorentzTransform::new(0.6).unwrap();
1857 assert!((t.energy_ratio() - 0.25).abs() < 1e-10);
1859 }
1860
1861 #[test]
1864 fn test_momentum_ratio_zero_at_rest() {
1865 let t = LorentzTransform::new(0.0).unwrap();
1866 assert!(t.momentum_ratio().abs() < 1e-12);
1867 }
1868
1869 #[test]
1870 fn test_momentum_ratio_correct_at_0_6() {
1871 let t = LorentzTransform::new(0.6).unwrap();
1872 assert!((t.momentum_ratio() - 0.75).abs() < 1e-10);
1874 }
1875
1876 #[test]
1877 fn test_is_ultra_relativistic_true_above_0_9() {
1878 let t = LorentzTransform::new(0.95).unwrap();
1879 assert!(t.is_ultra_relativistic());
1880 }
1881
1882 #[test]
1883 fn test_is_ultra_relativistic_false_below_0_9() {
1884 let t = LorentzTransform::new(0.8).unwrap();
1885 assert!(!t.is_ultra_relativistic());
1886 }
1887
1888 #[test]
1889 fn test_is_ultra_relativistic_false_at_rest() {
1890 let t = LorentzTransform::new(0.0).unwrap();
1891 assert!(!t.is_ultra_relativistic());
1892 }
1893
1894 #[test]
1897 fn test_lorentz_factor_approx_equals_one_at_rest() {
1898 let t = LorentzTransform::new(0.0).unwrap();
1899 assert!((t.lorentz_factor_approx() - 1.0).abs() < 1e-12);
1900 }
1901
1902 #[test]
1903 fn test_lorentz_factor_approx_close_for_small_beta() {
1904 let t = LorentzTransform::new(0.1).unwrap();
1905 let approx = t.lorentz_factor_approx();
1907 let exact = t.gamma();
1908 assert!((approx - exact).abs() < 0.001);
1909 }
1910
1911 #[test]
1912 fn test_lorentz_factor_approx_always_gte_one() {
1913 let t = LorentzTransform::new(0.5).unwrap();
1914 assert!(t.lorentz_factor_approx() >= 1.0);
1915 }
1916
1917 #[test]
1920 fn test_velocity_ratio_equals_one_for_same_beta() {
1921 let t = LorentzTransform::new(0.6).unwrap();
1922 assert!((t.velocity_ratio(&t) - 1.0).abs() < 1e-12);
1923 }
1924
1925 #[test]
1926 fn test_velocity_ratio_correct() {
1927 let a = LorentzTransform::new(0.6).unwrap();
1928 let b = LorentzTransform::new(0.3).unwrap();
1929 assert!((a.velocity_ratio(&b) - 2.0).abs() < 1e-12);
1930 }
1931
1932 #[test]
1933 fn test_velocity_ratio_less_than_one_when_slower() {
1934 let a = LorentzTransform::new(0.3).unwrap();
1935 let b = LorentzTransform::new(0.6).unwrap();
1936 assert!(a.velocity_ratio(&b) < 1.0);
1937 }
1938
1939 #[test]
1942 fn test_proper_length_at_rest_equals_observed() {
1943 let t = LorentzTransform::new(0.0).unwrap();
1944 assert!((t.proper_length(10.0) - 10.0).abs() < 1e-10);
1946 }
1947
1948 #[test]
1949 fn test_proper_length_greater_than_observed_when_moving() {
1950 let t = LorentzTransform::new(0.6).unwrap();
1951 assert!((t.proper_length(8.0) - 10.0).abs() < 1e-9);
1953 }
1954
1955 #[test]
1958 fn test_length_contraction_at_rest_equals_rest_length() {
1959 let t = LorentzTransform::new(0.0).unwrap();
1960 assert!((t.length_contraction(10.0) - 10.0).abs() < 1e-10);
1961 }
1962
1963 #[test]
1964 fn test_length_contraction_shorter_when_moving() {
1965 let t = LorentzTransform::new(0.6).unwrap();
1966 assert!((t.length_contraction(10.0) - 8.0).abs() < 1e-9);
1968 }
1969
1970 #[test]
1971 fn test_length_contraction_proper_length_roundtrip() {
1972 let t = LorentzTransform::new(0.8).unwrap();
1973 let rest = 100.0;
1974 let contracted = t.length_contraction(rest);
1975 let recovered = t.proper_length(contracted);
1976 assert!((recovered - rest).abs() < 1e-9);
1977 }
1978
1979 #[test]
1982 fn test_beta_times_gamma_zero_at_rest() {
1983 let t = LorentzTransform::new(0.0).unwrap();
1984 assert_eq!(t.beta_times_gamma(), 0.0);
1985 }
1986
1987 #[test]
1988 fn test_beta_times_gamma_positive_for_nonzero_velocity() {
1989 let t = LorentzTransform::new(0.6).unwrap();
1990 let bg = t.beta_times_gamma();
1992 assert!((bg - 0.75).abs() < 1e-9);
1993 }
1994
1995 #[test]
1996 fn test_beta_times_gamma_increases_with_velocity() {
1997 let t1 = LorentzTransform::new(0.5).unwrap();
1998 let t2 = LorentzTransform::new(0.9).unwrap();
1999 assert!(t2.beta_times_gamma() > t1.beta_times_gamma());
2000 }
2001
2002 #[test]
2005 fn test_energy_momentum_invariant_equals_mass_squared_at_rest() {
2006 let t = LorentzTransform::new(0.0).unwrap();
2007 let mass = 2.0;
2008 let inv = t.energy_momentum_invariant(mass);
2009 assert!((inv - mass * mass).abs() < 1e-9);
2010 }
2011
2012 #[test]
2013 fn test_energy_momentum_invariant_equals_mass_squared_at_velocity() {
2014 let t = LorentzTransform::new(0.8).unwrap();
2015 let mass = 3.0;
2016 let inv = t.energy_momentum_invariant(mass);
2017 assert!((inv - mass * mass).abs() < 1e-9);
2018 }
2019}