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> {
186 if !eta.is_finite() {
187 return Err(StreamError::LorentzConfigError {
188 reason: format!("rapidity must be finite, got {eta}"),
189 });
190 }
191 let beta = eta.tanh();
192 Self::new(beta)
193 }
194
195 pub fn from_velocity(v: f64, c: f64) -> Result<Self, StreamError> {
204 if !v.is_finite() || !c.is_finite() {
205 return Err(StreamError::LorentzConfigError {
206 reason: format!("v and c must be finite, got v={v}, c={c}"),
207 });
208 }
209 if c == 0.0 {
210 return Err(StreamError::LorentzConfigError {
211 reason: "speed of light c must be non-zero".into(),
212 });
213 }
214 Self::new(v / c)
215 }
216
217 pub fn gamma_at(beta: f64) -> f64 {
222 (1.0 - beta * beta).sqrt().recip()
223 }
224
225 pub fn relativistic_momentum(&self, mass: f64) -> f64 {
231 mass * self.gamma * self.beta
232 }
233
234 pub fn kinetic_energy_ratio(&self) -> f64 {
240 self.gamma - 1.0
241 }
242
243 pub fn time_dilation_factor(&self) -> f64 {
249 self.gamma
250 }
251
252 pub fn length_contraction_factor(&self) -> f64 {
256 1.0 / self.gamma
257 }
258
259 pub fn beta_from_gamma(gamma: f64) -> Result<f64, StreamError> {
261 if gamma.is_nan() || gamma < 1.0 {
262 return Err(StreamError::LorentzConfigError {
263 reason: format!("gamma must be >= 1.0, got {gamma}"),
264 });
265 }
266 Ok((1.0 - 1.0 / (gamma * gamma)).sqrt())
267 }
268
269 pub fn rapidity(&self) -> f64 {
274 self.beta.atanh()
275 }
276
277 pub fn beta_times_gamma(&self) -> f64 {
282 self.beta * self.gamma
283 }
284
285 pub fn beta_from_rapidity(eta: f64) -> f64 {
289 eta.tanh()
290 }
291
292 pub fn proper_velocity(&self) -> f64 {
297 self.beta * self.gamma
298 }
299
300 pub fn beta(&self) -> f64 {
302 self.beta
303 }
304
305 pub fn gamma(&self) -> f64 {
310 self.gamma
311 }
312
313 #[must_use]
323 pub fn transform(&self, p: SpacetimePoint) -> SpacetimePoint {
324 let t_prime = self.gamma * (p.t - self.beta * p.x);
325 let x_prime = self.gamma * (p.x - self.beta * p.t);
326 SpacetimePoint {
327 t: t_prime,
328 x: x_prime,
329 }
330 }
331
332 #[must_use]
345 pub fn inverse_transform(&self, p: SpacetimePoint) -> SpacetimePoint {
346 let t_orig = self.gamma * (p.t + self.beta * p.x);
347 let x_orig = self.gamma * (p.x + self.beta * p.t);
348 SpacetimePoint {
349 t: t_orig,
350 x: x_orig,
351 }
352 }
353
354 pub fn transform_batch(&self, points: &[SpacetimePoint]) -> Vec<SpacetimePoint> {
361 points.iter().map(|&p| self.transform(p)).collect()
362 }
363
364 pub fn inverse_transform_batch(&self, points: &[SpacetimePoint]) -> Vec<SpacetimePoint> {
372 points.iter().map(|&p| self.inverse_transform(p)).collect()
373 }
374
375 pub fn dilate_time(&self, t: f64) -> f64 {
383 self.gamma * t
384 }
385
386 pub fn contract_length(&self, x: f64) -> f64 {
394 self.gamma * x
395 }
396
397 pub fn time_contraction(&self, coordinate_time: f64) -> f64 {
406 coordinate_time / self.gamma
407 }
408
409 pub fn is_ultrarelativistic(&self) -> bool {
415 self.beta > 0.9
416 }
417
418 pub fn momentum_factor(&self) -> f64 {
425 self.gamma * self.beta
426 }
427
428 pub fn momentum(&self, mass: f64) -> f64 {
437 self.gamma * mass * self.beta
438 }
439
440 pub fn relativistic_energy(&self, rest_mass: f64) -> f64 {
447 self.gamma * rest_mass
448 }
449
450 pub fn kinetic_energy(&self, rest_mass: f64) -> f64 {
456 (self.gamma - 1.0) * rest_mass
457 }
458
459 pub fn energy_momentum_invariant(&self, rest_mass: f64) -> f64 {
464 let e = self.gamma * rest_mass;
465 let p = self.relativistic_momentum(rest_mass);
466 e * e - p * p
467 }
468
469 pub fn four_velocity_time(&self) -> f64 {
475 self.gamma
476 }
477
478 pub fn proper_time_dilation(&self, dt: f64) -> f64 {
483 dt / self.gamma
484 }
485
486 pub fn spacetime_interval(p1: SpacetimePoint, p2: SpacetimePoint) -> f64 {
497 let dt = p2.t - p1.t;
498 let dx = p2.x - p1.x;
499 dt * dt - dx * dx
500 }
501
502 pub fn velocity_addition(beta1: f64, beta2: f64) -> Result<f64, StreamError> {
513 if beta1.is_nan() || beta1 < 0.0 || beta1 >= 1.0 {
514 return Err(StreamError::LorentzConfigError {
515 reason: format!("beta1 must be in [0.0, 1.0), got {beta1}"),
516 });
517 }
518 if beta2.is_nan() || beta2 < 0.0 || beta2 >= 1.0 {
519 return Err(StreamError::LorentzConfigError {
520 reason: format!("beta2 must be in [0.0, 1.0), got {beta2}"),
521 });
522 }
523 let composed = (beta1 + beta2) / (1.0 + beta1 * beta2);
524 if composed >= 1.0 {
525 return Err(StreamError::LorentzConfigError {
526 reason: format!("composed velocity {composed} >= 1.0 (speed of light)"),
527 });
528 }
529 Ok(composed)
530 }
531
532 pub fn proper_time(&self, coordinate_time: f64) -> f64 {
540 coordinate_time / self.gamma
541 }
542
543 pub fn inverse(&self) -> Self {
553 let beta = -self.beta;
556 let gamma = 1.0 / (1.0 - beta * beta).sqrt();
557 Self { beta, gamma }
558 }
559
560 pub fn time_dilation(&self, proper_time: f64) -> f64 {
569 proper_time * self.gamma
570 }
571
572 pub fn compose(&self, other: &LorentzTransform) -> Result<Self, StreamError> {
589 let b1 = self.beta;
590 let b2 = other.beta;
591 let composed = (b1 + b2) / (1.0 + b1 * b2);
592 LorentzTransform::new(composed)
593 }
594
595 pub fn boost_chain(betas: &[f64]) -> Result<Self, StreamError> {
605 let mut result = LorentzTransform::new(0.0)?;
606 for &beta in betas {
607 let next = LorentzTransform::new(beta)?;
608 result = result.compose(&next)?;
609 }
610 Ok(result)
611 }
612
613 pub fn is_identity(&self) -> bool {
618 self.beta.abs() < 1e-10
619 }
620
621 pub fn composition(&self, other: &Self) -> Result<Self, StreamError> {
631 let b1 = self.beta;
632 let b2 = other.beta;
633 let denom = 1.0 + b1 * b2;
634 if denom.abs() < 1e-15 {
635 return Err(StreamError::LorentzConfigError {
636 reason: "boost composition denominator too small (near-singular)".into(),
637 });
638 }
639 Self::new((b1 + b2) / denom)
640 }
641
642 pub fn doppler_factor(&self) -> f64 {
648 ((1.0 + self.beta) / (1.0 - self.beta)).sqrt()
649 }
650
651 pub fn aberration_angle(&self, cos_theta: f64) -> f64 {
656 let cos_prime = (cos_theta + self.beta) / (1.0 + self.beta * cos_theta);
657 cos_prime.clamp(-1.0, 1.0).acos()
658 }
659
660 pub fn relativistic_mass(&self, rest_mass: f64) -> f64 {
664 rest_mass * self.gamma()
665 }
666
667 pub fn energy_ratio(&self) -> f64 {
671 self.gamma() - 1.0
672 }
673
674 pub fn warp_factor(&self) -> f64 {
679 self.gamma().cbrt()
680 }
681
682 pub fn four_momentum(&self, mass: f64) -> (f64, f64) {
687 let g = self.gamma();
688 (g * mass, g * mass * self.beta)
689 }
690
691 pub fn momentum_ratio(&self) -> f64 {
695 self.gamma() * self.beta
696 }
697
698 pub fn is_ultra_relativistic(&self) -> bool {
700 self.beta > 0.9
701 }
702
703 pub fn lorentz_factor_approx(&self) -> f64 {
707 1.0 + 0.5 * self.beta * self.beta
708 }
709
710 pub fn velocity_ratio(&self, other: &Self) -> f64 {
715 self.beta / other.beta
716 }
717
718 pub fn proper_length(&self, observed: f64) -> f64 {
723 observed * self.gamma()
724 }
725
726 pub fn length_contraction(&self, rest_length: f64) -> f64 {
730 rest_length / self.gamma()
731 }
732
733 pub fn light_cone_check(dt: f64, dx: f64) -> &'static str {
740 let s_sq = dt * dt - dx * dx;
741 if s_sq > 1e-12 {
742 "timelike"
743 } else if s_sq < -1e-12 {
744 "spacelike"
745 } else {
746 "lightlike"
747 }
748 }
749
750 pub fn lorentz_invariant_mass(energy: f64, momentum: f64) -> Option<f64> {
754 let m_sq = energy * energy - momentum * momentum;
755 if m_sq < 0.0 { return None; }
756 Some(m_sq.sqrt())
757 }
758
759 pub fn aberration_correction(&self, cos_theta: f64) -> Option<f64> {
764 let denom = 1.0 - self.beta * cos_theta;
765 if denom.abs() < 1e-15 { return None; }
766 Some((cos_theta - self.beta) / denom)
767 }
768
769 pub fn doppler_ratio(&self) -> f64 {
774 ((1.0 + self.beta) / (1.0 - self.beta)).sqrt()
775 }
776
777 pub fn time_dilation_ms(&self, proper_ms: f64) -> f64 {
781 self.gamma * proper_ms
782 }
783
784 pub fn space_contraction(&self, proper_length: f64) -> f64 {
788 proper_length / self.gamma
789 }
790
791 pub fn proper_acceleration(&self, force: f64, mass: f64) -> Option<f64> {
796 if mass == 0.0 { return None; }
797 Some(force / (self.gamma.powi(3) * mass))
798 }
799
800 pub fn momentum_rapidity(&self) -> f64 {
804 self.gamma * self.beta
805 }
806
807 pub fn inverse_gamma(&self) -> f64 {
811 1.0 / self.gamma
812 }
813
814 pub fn boost_composition(beta1: f64, beta2: f64) -> Result<f64, crate::error::StreamError> {
819 let denom = 1.0 + beta1 * beta2;
820 if denom.abs() < 1e-15 {
821 return Err(crate::error::StreamError::LorentzConfigError {
822 reason: "degenerate boost composition".into(),
823 });
824 }
825 let result = (beta1 + beta2) / denom;
826 if result.abs() >= 1.0 {
827 return Err(crate::error::StreamError::LorentzConfigError {
828 reason: "boost exceeds c".into(),
829 });
830 }
831 Ok(result)
832 }
833}
834
835#[cfg(test)]
836mod tests {
837 use super::*;
838
839 const EPS: f64 = 1e-10;
840
841 fn approx_eq(a: f64, b: f64) -> bool {
842 (a - b).abs() < EPS
843 }
844
845 fn point_approx_eq(a: SpacetimePoint, b: SpacetimePoint) -> bool {
846 approx_eq(a.t, b.t) && approx_eq(a.x, b.x)
847 }
848
849 #[test]
852 fn test_new_valid_beta() {
853 let lt = LorentzTransform::new(0.5).unwrap();
854 assert!((lt.beta() - 0.5).abs() < EPS);
855 }
856
857 #[test]
858 fn test_new_beta_zero() {
859 let lt = LorentzTransform::new(0.0).unwrap();
860 assert_eq!(lt.beta(), 0.0);
861 assert!((lt.gamma() - 1.0).abs() < EPS);
862 }
863
864 #[test]
865 fn test_new_beta_one_returns_error() {
866 let err = LorentzTransform::new(1.0).unwrap_err();
867 assert!(matches!(err, StreamError::LorentzConfigError { .. }));
868 }
869
870 #[test]
871 fn test_new_beta_above_one_returns_error() {
872 let err = LorentzTransform::new(1.5).unwrap_err();
873 assert!(matches!(err, StreamError::LorentzConfigError { .. }));
874 }
875
876 #[test]
877 fn test_new_beta_negative_returns_error() {
878 let err = LorentzTransform::new(-0.1).unwrap_err();
879 assert!(matches!(err, StreamError::LorentzConfigError { .. }));
880 }
881
882 #[test]
883 fn test_new_beta_nan_returns_error() {
884 let err = LorentzTransform::new(f64::NAN).unwrap_err();
885 assert!(matches!(err, StreamError::LorentzConfigError { .. }));
886 }
887
888 #[test]
891 fn test_beta_zero_is_identity_transform() {
892 let lt = LorentzTransform::new(0.0).unwrap();
893 let p = SpacetimePoint::new(3.0, 4.0);
894 let q = lt.transform(p);
895 assert!(point_approx_eq(p, q), "beta=0 must be identity, got {q:?}");
896 }
897
898 #[test]
902 fn test_time_dilation_at_x_zero() {
903 let lt = LorentzTransform::new(0.6).unwrap();
904 let p = SpacetimePoint::new(5.0, 0.0);
905 let q = lt.transform(p);
906 let expected_t = lt.gamma() * 5.0;
907 assert!(approx_eq(q.t, expected_t));
908 }
909
910 #[test]
911 fn test_dilate_time_helper() {
912 let lt = LorentzTransform::new(0.6).unwrap();
913 assert!(approx_eq(lt.dilate_time(1.0), lt.gamma()));
914 }
915
916 #[test]
920 fn test_length_contraction_at_t_zero() {
921 let lt = LorentzTransform::new(0.6).unwrap();
922 let p = SpacetimePoint::new(0.0, 5.0);
923 let q = lt.transform(p);
924 let expected_x = lt.gamma() * 5.0;
925 assert!(approx_eq(q.x, expected_x));
926 }
927
928 #[test]
929 fn test_contract_length_helper() {
930 let lt = LorentzTransform::new(0.6).unwrap();
931 assert!(approx_eq(lt.contract_length(1.0), lt.gamma()));
932 }
933
934 #[test]
938 fn test_known_beta_0_6_gamma_is_1_25() {
939 let lt = LorentzTransform::new(0.6).unwrap();
940 assert!(
941 (lt.gamma() - 1.25).abs() < 1e-9,
942 "gamma should be 1.25, got {}",
943 lt.gamma()
944 );
945 }
946
947 #[test]
949 fn test_known_beta_0_8_gamma() {
950 let lt = LorentzTransform::new(0.8).unwrap();
951 let expected_gamma = 5.0 / 3.0;
952 assert!((lt.gamma() - expected_gamma).abs() < 1e-9);
953 }
954
955 #[test]
957 fn test_known_beta_0_5_gamma() {
958 let lt = LorentzTransform::new(0.5).unwrap();
959 let expected_gamma = 2.0 / 3.0f64.sqrt();
960 assert!((lt.gamma() - expected_gamma).abs() < 1e-9);
961 }
962
963 #[test]
967 fn test_transform_known_point_beta_0_6() {
968 let lt = LorentzTransform::new(0.6).unwrap();
969 let p = SpacetimePoint::new(1.0, 0.0);
970 let q = lt.transform(p);
971 assert!((q.t - 1.25).abs() < 1e-9);
972 assert!((q.x - (-0.75)).abs() < 1e-9);
973 }
974
975 #[test]
978 fn test_inverse_transform_roundtrip() {
979 let lt = LorentzTransform::new(0.7).unwrap();
980 let p = SpacetimePoint::new(3.0, 1.5);
981 let q = lt.transform(p);
982 let r = lt.inverse_transform(q);
983 assert!(
984 point_approx_eq(r, p),
985 "round-trip failed: expected {p:?}, got {r:?}"
986 );
987 }
988
989 #[test]
992 fn test_transform_batch_length_preserved() {
993 let lt = LorentzTransform::new(0.3).unwrap();
994 let pts = vec![
995 SpacetimePoint::new(0.0, 1.0),
996 SpacetimePoint::new(1.0, 2.0),
997 SpacetimePoint::new(2.0, 3.0),
998 ];
999 let out = lt.transform_batch(&pts);
1000 assert_eq!(out.len(), pts.len());
1001 }
1002
1003 #[test]
1004 fn test_transform_batch_matches_individual() {
1005 let lt = LorentzTransform::new(0.4).unwrap();
1006 let pts = vec![SpacetimePoint::new(1.0, 0.5), SpacetimePoint::new(2.0, 1.5)];
1007 let batch = lt.transform_batch(&pts);
1008 for (i, &p) in pts.iter().enumerate() {
1009 let individual = lt.transform(p);
1010 assert!(
1011 point_approx_eq(batch[i], individual),
1012 "batch[{i}] differs from individual transform"
1013 );
1014 }
1015 }
1016
1017 #[test]
1018 fn test_inverse_transform_batch_roundtrip() {
1019 let lt = LorentzTransform::new(0.5).unwrap();
1020 let pts = vec![
1021 SpacetimePoint::new(1.0, 0.0),
1022 SpacetimePoint::new(2.0, 1.0),
1023 SpacetimePoint::new(0.0, 3.0),
1024 ];
1025 let transformed = lt.transform_batch(&pts);
1026 let restored = lt.inverse_transform_batch(&transformed);
1027 for (i, (&orig, &rest)) in pts.iter().zip(restored.iter()).enumerate() {
1028 assert!(
1029 point_approx_eq(orig, rest),
1030 "round-trip failed at index {i}: expected {orig:?}, got {rest:?}"
1031 );
1032 }
1033 }
1034
1035 #[test]
1036 fn test_inverse_transform_batch_length_preserved() {
1037 let lt = LorentzTransform::new(0.3).unwrap();
1038 let pts = vec![SpacetimePoint::new(0.0, 0.0), SpacetimePoint::new(1.0, 1.0)];
1039 assert_eq!(lt.inverse_transform_batch(&pts).len(), pts.len());
1040 }
1041
1042 #[test]
1045 fn test_spacetime_point_fields() {
1046 let p = SpacetimePoint::new(1.5, 2.5);
1047 assert_eq!(p.t, 1.5);
1048 assert_eq!(p.x, 2.5);
1049 }
1050
1051 #[test]
1052 fn test_spacetime_point_equality() {
1053 let p = SpacetimePoint::new(1.0, 2.0);
1054 let q = SpacetimePoint::new(1.0, 2.0);
1055 assert_eq!(p, q);
1056 }
1057
1058 #[test]
1062 fn test_compose_identity_with_identity() {
1063 let lt = LorentzTransform::new(0.0).unwrap();
1064 let composed = lt.compose(<).unwrap();
1065 assert!(approx_eq(composed.beta(), 0.0));
1066 }
1067
1068 #[test]
1070 fn test_compose_0_5_and_0_5() {
1071 let lt = LorentzTransform::new(0.5).unwrap();
1072 let composed = lt.compose(<).unwrap();
1073 let expected = (0.5 + 0.5) / (1.0 + 0.5 * 0.5); assert!((composed.beta() - expected).abs() < EPS);
1075 }
1076
1077 #[test]
1079 fn test_compose_with_negative_is_identity() {
1080 let lt_fwd = LorentzTransform::new(0.3).unwrap();
1082 let lt_bwd = LorentzTransform::new(0.3).unwrap();
1083 let composed = lt_fwd.compose(<_bwd).unwrap();
1087 assert!(composed.beta() < 1.0);
1088 }
1089
1090 #[test]
1094 fn test_rapidity_zero_beta() {
1095 let lt = LorentzTransform::new(0.0).unwrap();
1096 assert!(approx_eq(lt.rapidity(), 0.0));
1097 }
1098
1099 #[test]
1101 fn test_rapidity_known_value() {
1102 let lt = LorentzTransform::new(0.5).unwrap();
1103 let expected = (0.5f64).atanh();
1104 assert!((lt.rapidity() - expected).abs() < EPS);
1105 }
1106
1107 #[test]
1109 fn test_rapidity_is_additive_under_composition() {
1110 let lt1 = LorentzTransform::new(0.3).unwrap();
1111 let lt2 = LorentzTransform::new(0.4).unwrap();
1112 let composed = lt1.compose(<2).unwrap();
1113 let sum_rapidities = lt1.rapidity() + lt2.rapidity();
1114 assert!(
1115 (composed.rapidity() - sum_rapidities).abs() < 1e-9,
1116 "rapidity should be additive: {} vs {}",
1117 composed.rapidity(),
1118 sum_rapidities
1119 );
1120 }
1121
1122 #[test]
1125 fn test_velocity_addition_known_values() {
1126 let result = LorentzTransform::velocity_addition(0.5, 0.5).unwrap();
1128 assert!((result - 0.8).abs() < EPS);
1129 }
1130
1131 #[test]
1132 fn test_velocity_addition_identity_with_zero() {
1133 let result = LorentzTransform::velocity_addition(0.0, 0.6).unwrap();
1134 assert!((result - 0.6).abs() < EPS);
1135 }
1136
1137 #[test]
1138 fn test_velocity_addition_invalid_beta_rejected() {
1139 assert!(LorentzTransform::velocity_addition(1.0, 0.5).is_err());
1140 assert!(LorentzTransform::velocity_addition(0.5, 1.0).is_err());
1141 assert!(LorentzTransform::velocity_addition(-0.1, 0.5).is_err());
1142 }
1143
1144 #[test]
1145 fn test_velocity_addition_matches_compose() {
1146 let b1 = 0.3;
1147 let b2 = 0.4;
1148 let static_result = LorentzTransform::velocity_addition(b1, b2).unwrap();
1149 let lt1 = LorentzTransform::new(b1).unwrap();
1150 let lt2 = LorentzTransform::new(b2).unwrap();
1151 let composed = lt1.compose(<2).unwrap();
1152 assert!((static_result - composed.beta()).abs() < EPS);
1153 }
1154
1155 #[test]
1159 fn test_proper_time_identity_at_zero_beta() {
1160 let lt = LorentzTransform::new(0.0).unwrap();
1161 assert!(approx_eq(lt.proper_time(10.0), 10.0));
1162 }
1163
1164 #[test]
1166 fn test_proper_time_less_than_coordinate_time() {
1167 let lt = LorentzTransform::new(0.6).unwrap(); let tau = lt.proper_time(5.0);
1169 assert!((tau - 4.0).abs() < EPS);
1171 assert!(tau < 5.0);
1172 }
1173
1174 #[test]
1176 fn test_proper_time_roundtrip_with_dilate_time() {
1177 let lt = LorentzTransform::new(0.8).unwrap();
1178 let t = 3.0;
1179 let dilated = lt.dilate_time(t);
1180 let recovered = lt.proper_time(dilated);
1181 assert!(approx_eq(recovered, t));
1182 }
1183
1184 #[test]
1186 fn test_compose_equals_sequential_transforms() {
1187 let lt1 = LorentzTransform::new(0.3).unwrap();
1188 let lt2 = LorentzTransform::new(0.4).unwrap();
1189 let composed = lt1.compose(<2).unwrap();
1190
1191 let p = SpacetimePoint::new(2.0, 1.0);
1192 let sequential = lt2.transform(lt1.transform(p));
1193 let single = composed.transform(p);
1194 assert!(
1195 point_approx_eq(sequential, single),
1196 "composed boost must equal sequential: {sequential:?} vs {single:?}"
1197 );
1198 }
1199
1200 #[test]
1203 fn test_displacement_gives_correct_deltas() {
1204 let a = SpacetimePoint::new(1.0, 2.0);
1205 let b = SpacetimePoint::new(4.0, 6.0);
1206 let d = a.displacement(b);
1207 assert!(approx_eq(d.t, 3.0));
1208 assert!(approx_eq(d.x, 4.0));
1209 }
1210
1211 #[test]
1212 fn test_displacement_to_self_is_zero() {
1213 let a = SpacetimePoint::new(3.0, 7.0);
1214 let d = a.displacement(a);
1215 assert!(approx_eq(d.t, 0.0));
1216 assert!(approx_eq(d.x, 0.0));
1217 }
1218
1219 #[test]
1222 fn test_boost_chain_empty_is_identity() {
1223 let chain = LorentzTransform::boost_chain(&[]).unwrap();
1224 assert!(approx_eq(chain.beta(), 0.0));
1225 assert!(approx_eq(chain.gamma(), 1.0));
1226 }
1227
1228 #[test]
1229 fn test_boost_chain_single_equals_new() {
1230 let chain = LorentzTransform::boost_chain(&[0.5]).unwrap();
1231 let direct = LorentzTransform::new(0.5).unwrap();
1232 assert!(approx_eq(chain.beta(), direct.beta()));
1233 }
1234
1235 #[test]
1236 fn test_boost_chain_two_equals_compose() {
1237 let chain = LorentzTransform::boost_chain(&[0.3, 0.4]).unwrap();
1238 let manual = LorentzTransform::new(0.3)
1239 .unwrap()
1240 .compose(&LorentzTransform::new(0.4).unwrap())
1241 .unwrap();
1242 assert!(approx_eq(chain.beta(), manual.beta()));
1243 }
1244
1245 #[test]
1246 fn test_boost_chain_invalid_beta_returns_error() {
1247 assert!(LorentzTransform::boost_chain(&[1.0]).is_err());
1248 assert!(LorentzTransform::boost_chain(&[0.3, -0.1]).is_err());
1249 }
1250
1251 #[test]
1254 fn test_time_dilation_identity_at_zero_beta() {
1255 let lt = LorentzTransform::new(0.0).unwrap();
1257 assert!(approx_eq(lt.time_dilation(10.0), 10.0));
1258 }
1259
1260 #[test]
1261 fn test_time_dilation_inverse_of_proper_time() {
1262 let lt = LorentzTransform::new(0.6).unwrap();
1264 let t = 5.0_f64;
1265 let tau = lt.proper_time(t);
1266 assert!(approx_eq(lt.time_dilation(tau), t));
1267 }
1268
1269 #[test]
1270 fn test_time_dilation_greater_than_proper_time() {
1271 let lt = LorentzTransform::new(0.8).unwrap();
1273 let proper = 3.0_f64;
1274 let coord = lt.time_dilation(proper);
1275 assert!(coord > proper);
1276 }
1277
1278 #[test]
1281 fn test_inverse_negates_beta() {
1282 let lt = LorentzTransform::new(0.6).unwrap();
1283 let inv = lt.inverse();
1284 assert!(approx_eq(inv.beta(), -0.6));
1285 }
1286
1287 #[test]
1288 fn test_inverse_preserves_gamma_magnitude() {
1289 let lt = LorentzTransform::new(0.6).unwrap();
1290 let inv = lt.inverse();
1291 assert!(approx_eq(inv.gamma(), lt.gamma()));
1293 }
1294
1295 #[test]
1296 fn test_inverse_roundtrips_point() {
1297 let lt = LorentzTransform::new(0.5).unwrap();
1298 let p = SpacetimePoint::new(3.0, 1.0);
1299 let boosted = lt.transform(p);
1300 let recovered = lt.inverse().transform(boosted);
1301 assert!(point_approx_eq(recovered, p));
1302 }
1303
1304 #[test]
1307 fn test_norm_sq_timelike() {
1308 let p = SpacetimePoint::new(3.0, 1.0);
1310 assert!((p.norm_sq() - 8.0).abs() < EPS);
1311 assert!(p.is_timelike());
1312 assert!(!p.is_spacelike());
1313 assert!(!p.is_lightlike());
1314 }
1315
1316 #[test]
1317 fn test_norm_sq_spacelike() {
1318 let p = SpacetimePoint::new(1.0, 3.0);
1320 assert!((p.norm_sq() - (-8.0)).abs() < EPS);
1321 assert!(p.is_spacelike());
1322 assert!(!p.is_timelike());
1323 assert!(!p.is_lightlike());
1324 }
1325
1326 #[test]
1327 fn test_norm_sq_lightlike() {
1328 let p = SpacetimePoint::new(1.0, 1.0);
1330 assert!(p.norm_sq().abs() < EPS);
1331 assert!(p.is_lightlike());
1332 assert!(!p.is_timelike());
1333 assert!(!p.is_spacelike());
1334 }
1335
1336 #[test]
1337 fn test_norm_sq_origin_is_lightlike() {
1338 let p = SpacetimePoint::new(0.0, 0.0);
1339 assert!(p.is_lightlike());
1340 }
1341
1342 #[test]
1345 fn test_is_identity_beta_zero() {
1346 let lt = LorentzTransform::new(0.0).unwrap();
1347 assert!(lt.is_identity());
1348 }
1349
1350 #[test]
1351 fn test_is_not_identity_nonzero_beta() {
1352 let lt = LorentzTransform::new(0.5).unwrap();
1353 assert!(!lt.is_identity());
1354 }
1355
1356 #[test]
1357 fn test_is_identity_empty_boost_chain() {
1358 let lt = LorentzTransform::boost_chain(&[]).unwrap();
1359 assert!(lt.is_identity());
1360 }
1361
1362 #[test]
1365 fn test_beta_from_gamma_identity() {
1366 let beta = LorentzTransform::beta_from_gamma(1.0).unwrap();
1368 assert!(approx_eq(beta, 0.0));
1369 }
1370
1371 #[test]
1372 fn test_beta_from_gamma_roundtrip() {
1373 let lt = LorentzTransform::new(0.6).unwrap();
1374 let recovered = LorentzTransform::beta_from_gamma(lt.gamma()).unwrap();
1375 assert!(approx_eq(recovered, 0.6));
1376 }
1377
1378 #[test]
1379 fn test_beta_from_gamma_invalid_rejected() {
1380 assert!(LorentzTransform::beta_from_gamma(0.5).is_err()); assert!(LorentzTransform::beta_from_gamma(f64::NAN).is_err());
1382 assert!(LorentzTransform::beta_from_gamma(-1.0).is_err());
1383 }
1384
1385 #[test]
1388 fn test_from_rapidity_zero_is_identity() {
1389 let lt = LorentzTransform::from_rapidity(0.0).unwrap();
1390 assert!(lt.is_identity());
1391 }
1392
1393 #[test]
1394 fn test_from_rapidity_positive_gives_valid_beta() {
1395 let lt = LorentzTransform::from_rapidity(0.5).unwrap();
1397 assert!((lt.beta() - 0.5_f64.tanh()).abs() < EPS);
1398 }
1399
1400 #[test]
1401 fn test_from_rapidity_infinite_rejected() {
1402 assert!(LorentzTransform::from_rapidity(f64::INFINITY).is_err());
1403 assert!(LorentzTransform::from_rapidity(f64::NEG_INFINITY).is_err());
1404 assert!(LorentzTransform::from_rapidity(f64::NAN).is_err());
1405 }
1406
1407 #[test]
1410 fn test_distance_to_timelike_separation() {
1411 let a = SpacetimePoint::new(0.0, 0.0);
1413 let b = SpacetimePoint::new(3.0, 0.0);
1414 assert!((a.distance_to(b) - 3.0).abs() < EPS);
1415 }
1416
1417 #[test]
1418 fn test_distance_to_spacelike_separation() {
1419 let a = SpacetimePoint::new(0.0, 0.0);
1421 let b = SpacetimePoint::new(0.0, 4.0);
1422 assert!((a.distance_to(b) - 4.0).abs() < EPS);
1423 }
1424
1425 #[test]
1426 fn test_distance_to_same_point_is_zero() {
1427 let p = SpacetimePoint::new(2.0, 3.0);
1428 assert!(p.distance_to(p).abs() < EPS);
1429 }
1430
1431 #[test]
1434 fn test_gamma_at_zero_is_one() {
1435 assert!((LorentzTransform::gamma_at(0.0) - 1.0).abs() < EPS);
1436 }
1437
1438 #[test]
1439 fn test_gamma_at_matches_constructor_gamma() {
1440 let lt = LorentzTransform::new(0.6).unwrap();
1441 let expected = lt.gamma();
1442 let computed = LorentzTransform::gamma_at(0.6);
1443 assert!((computed - expected).abs() < EPS);
1444 }
1445
1446 #[test]
1447 fn test_gamma_at_one_is_infinite() {
1448 assert!(LorentzTransform::gamma_at(1.0).is_infinite());
1449 }
1450
1451 #[test]
1452 fn test_gamma_at_above_one_is_nan() {
1453 assert!(LorentzTransform::gamma_at(1.1).is_nan());
1454 }
1455
1456 #[test]
1457 fn test_from_velocity_matches_beta_ratio() {
1458 let t = LorentzTransform::from_velocity(0.6, 1.0).unwrap();
1460 assert!((t.beta() - 0.6).abs() < 1e-12);
1461 }
1462
1463 #[test]
1464 fn test_from_velocity_zero_c_returns_error() {
1465 assert!(matches!(
1466 LorentzTransform::from_velocity(0.5, 0.0),
1467 Err(StreamError::LorentzConfigError { .. })
1468 ));
1469 }
1470
1471 #[test]
1472 fn test_from_velocity_non_finite_returns_error() {
1473 assert!(LorentzTransform::from_velocity(f64::INFINITY, 1.0).is_err());
1474 assert!(LorentzTransform::from_velocity(0.5, f64::NAN).is_err());
1475 }
1476
1477 #[test]
1478 fn test_from_velocity_superluminal_returns_error() {
1479 assert!(LorentzTransform::from_velocity(1.5, 1.0).is_err());
1481 }
1482
1483 #[test]
1486 fn test_relativistic_momentum_zero_beta_is_zero() {
1487 let lt = LorentzTransform::new(0.0).unwrap();
1488 assert!(approx_eq(lt.relativistic_momentum(1.0), 0.0));
1489 }
1490
1491 #[test]
1492 fn test_relativistic_momentum_known_value() {
1493 let lt = LorentzTransform::new(0.6).unwrap();
1495 assert!((lt.relativistic_momentum(2.0) - 1.5).abs() < 1e-9);
1496 }
1497
1498 #[test]
1499 fn test_relativistic_momentum_scales_with_mass() {
1500 let lt = LorentzTransform::new(0.6).unwrap();
1501 let p1 = lt.relativistic_momentum(1.0);
1502 let p2 = lt.relativistic_momentum(2.0);
1503 assert!((p2 - 2.0 * p1).abs() < 1e-9);
1504 }
1505
1506 #[test]
1509 fn test_kinetic_energy_ratio_zero_at_rest() {
1510 let lt = LorentzTransform::new(0.0).unwrap();
1511 assert!(approx_eq(lt.kinetic_energy_ratio(), 0.0));
1512 }
1513
1514 #[test]
1515 fn test_kinetic_energy_ratio_known_value() {
1516 let lt = LorentzTransform::new(0.6).unwrap();
1518 assert!((lt.kinetic_energy_ratio() - 0.25).abs() < 1e-9);
1519 }
1520
1521 #[test]
1522 fn test_kinetic_energy_ratio_positive_for_nonzero_beta() {
1523 let lt = LorentzTransform::new(0.8).unwrap();
1524 assert!(lt.kinetic_energy_ratio() > 0.0);
1525 }
1526
1527 #[test]
1528 fn test_composition_zero_with_zero_is_zero() {
1529 let t1 = LorentzTransform::new(0.0).unwrap();
1530 let t2 = LorentzTransform::new(0.0).unwrap();
1531 let composed = t1.composition(&t2).unwrap();
1532 assert!(composed.beta().abs() < 1e-12);
1533 }
1534
1535 #[test]
1536 fn test_composition_with_opposite_is_near_zero() {
1537 let t1 = LorentzTransform::new(0.6).unwrap();
1538 let t2 = t1.inverse(); let composed = t1.composition(&t2).unwrap();
1540 assert!(composed.beta().abs() < 1e-12);
1542 }
1543
1544 #[test]
1545 fn test_composition_velocity_addition_known_value() {
1546 let t1 = LorentzTransform::new(0.5).unwrap();
1548 let t2 = LorentzTransform::new(0.5).unwrap();
1549 let composed = t1.composition(&t2).unwrap();
1550 assert!((composed.beta() - 0.8).abs() < 1e-12);
1551 }
1552
1553 #[test]
1556 fn test_time_dilation_factor_one_at_rest() {
1557 let t = LorentzTransform::new(0.0).unwrap();
1558 assert!((t.time_dilation_factor() - 1.0).abs() < 1e-12);
1559 }
1560
1561 #[test]
1562 fn test_time_dilation_factor_equals_gamma() {
1563 let t = LorentzTransform::new(0.6).unwrap();
1564 assert!((t.time_dilation_factor() - t.gamma()).abs() < 1e-12);
1565 }
1566
1567 #[test]
1568 fn test_time_dilation_factor_greater_than_one_for_nonzero_beta() {
1569 let t = LorentzTransform::new(0.8).unwrap();
1570 assert!(t.time_dilation_factor() > 1.0);
1571 }
1572
1573 #[test]
1576 fn test_time_contraction_inverse_of_dilate_time() {
1577 let t = LorentzTransform::new(0.6).unwrap();
1578 let original = 100.0_f64;
1579 let dilated = t.dilate_time(original);
1580 let contracted = t.time_contraction(dilated);
1581 assert!((contracted - original).abs() < 1e-10);
1582 }
1583
1584 #[test]
1585 fn test_time_contraction_at_zero_beta_equals_input() {
1586 let t = LorentzTransform::new(0.0).unwrap();
1587 assert!((t.time_contraction(42.0) - 42.0).abs() < 1e-12);
1588 }
1589
1590 #[test]
1591 fn test_time_contraction_less_than_input_for_nonzero_beta() {
1592 let t = LorentzTransform::new(0.8).unwrap();
1593 assert!(t.time_contraction(100.0) < 100.0);
1594 }
1595
1596 #[test]
1599 fn test_length_contraction_factor_one_at_rest() {
1600 let t = LorentzTransform::new(0.0).unwrap();
1601 assert!((t.length_contraction_factor() - 1.0).abs() < 1e-12);
1602 }
1603
1604 #[test]
1605 fn test_length_contraction_factor_is_reciprocal_of_gamma() {
1606 let t = LorentzTransform::new(0.6).unwrap();
1607 assert!((t.length_contraction_factor() - 1.0 / t.gamma()).abs() < 1e-12);
1608 }
1609
1610 #[test]
1611 fn test_length_contraction_factor_less_than_one_for_nonzero_beta() {
1612 let t = LorentzTransform::new(0.9).unwrap();
1613 assert!(t.length_contraction_factor() < 1.0);
1614 }
1615
1616 #[test]
1619 fn test_proper_velocity_zero_at_rest() {
1620 let t = LorentzTransform::new(0.0).unwrap();
1621 assert!((t.proper_velocity() - 0.0).abs() < 1e-12);
1622 }
1623
1624 #[test]
1625 fn test_proper_velocity_equals_beta_times_gamma() {
1626 let t = LorentzTransform::new(0.6).unwrap();
1627 assert!((t.proper_velocity() - t.beta() * t.gamma()).abs() < 1e-12);
1628 }
1629
1630 #[test]
1631 fn test_proper_velocity_greater_than_beta_for_high_speed() {
1632 let t = LorentzTransform::new(0.8).unwrap();
1634 assert!(t.proper_velocity() > t.beta());
1635 }
1636
1637 #[test]
1640 fn test_is_ultrarelativistic_true_above_0_9() {
1641 let t = LorentzTransform::new(0.95).unwrap();
1642 assert!(t.is_ultrarelativistic());
1643 }
1644
1645 #[test]
1646 fn test_is_ultrarelativistic_false_below_0_9() {
1647 let t = LorentzTransform::new(0.5).unwrap();
1648 assert!(!t.is_ultrarelativistic());
1649 }
1650
1651 #[test]
1652 fn test_is_ultrarelativistic_false_at_exactly_0_9() {
1653 let t = LorentzTransform::new(0.9).unwrap();
1654 assert!(!t.is_ultrarelativistic()); }
1656
1657 #[test]
1658 fn test_momentum_factor_zero_at_rest() {
1659 let t = LorentzTransform::new(0.0).unwrap();
1660 assert!(t.momentum_factor().abs() < 1e-12);
1661 }
1662
1663 #[test]
1664 fn test_momentum_factor_equals_gamma_times_beta() {
1665 let t = LorentzTransform::new(0.6).unwrap();
1666 assert!((t.momentum_factor() - t.gamma() * t.beta()).abs() < 1e-12);
1667 }
1668
1669 #[test]
1672 fn test_relativistic_energy_equals_rest_mass_at_rest() {
1673 let t = LorentzTransform::new(0.0).unwrap();
1674 assert!((t.relativistic_energy(1.0) - 1.0).abs() < 1e-12);
1675 }
1676
1677 #[test]
1678 fn test_relativistic_energy_greater_than_rest_mass_for_moving_particle() {
1679 let t = LorentzTransform::new(0.6).unwrap();
1680 assert!(t.relativistic_energy(1.0) > 1.0);
1681 }
1682
1683 #[test]
1684 fn test_relativistic_energy_equals_gamma_times_rest_mass() {
1685 let t = LorentzTransform::new(0.8).unwrap();
1686 let m = 2.5;
1687 assert!((t.relativistic_energy(m) - t.gamma() * m).abs() < 1e-12);
1688 }
1689
1690 #[test]
1693 fn test_kinetic_energy_zero_at_rest() {
1694 let t = LorentzTransform::new(0.0).unwrap();
1695 assert!(t.kinetic_energy(1.0).abs() < 1e-12);
1696 }
1697
1698 #[test]
1699 fn test_kinetic_energy_positive_for_moving_particle() {
1700 let t = LorentzTransform::new(0.6).unwrap();
1701 assert!(t.kinetic_energy(1.0) > 0.0);
1702 }
1703
1704 #[test]
1705 fn test_kinetic_energy_equals_total_minus_rest() {
1706 let t = LorentzTransform::new(0.8).unwrap();
1707 let m = 3.0;
1708 let ke = t.kinetic_energy(m);
1709 let total = t.relativistic_energy(m);
1710 assert!((ke - (total - m)).abs() < 1e-12);
1711 }
1712
1713 #[test]
1716 fn test_four_velocity_time_equals_gamma() {
1717 let t = LorentzTransform::new(0.6).unwrap();
1718 assert!((t.four_velocity_time() - t.gamma()).abs() < 1e-12);
1719 }
1720
1721 #[test]
1722 fn test_four_velocity_time_one_at_rest() {
1723 let t = LorentzTransform::new(0.0).unwrap();
1724 assert!((t.four_velocity_time() - 1.0).abs() < 1e-12);
1725 }
1726
1727 #[test]
1728 fn test_four_velocity_time_greater_than_one_when_moving() {
1729 let t = LorentzTransform::new(0.5).unwrap();
1730 assert!(t.four_velocity_time() > 1.0);
1731 }
1732
1733 #[test]
1735 fn test_proper_time_dilation_at_rest_equals_dt() {
1736 let t = LorentzTransform::new(0.0).unwrap(); assert!((t.proper_time_dilation(10.0) - 10.0).abs() < 1e-10);
1738 }
1739
1740 #[test]
1741 fn test_proper_time_dilation_less_than_dt_when_moving() {
1742 let t = LorentzTransform::new(0.6).unwrap(); let proper = t.proper_time_dilation(10.0);
1744 assert!(proper < 10.0, "moving clock should run slow: {proper}");
1745 }
1746
1747 #[test]
1748 fn test_proper_time_dilation_approaches_zero_at_high_speed() {
1749 let t = LorentzTransform::new(0.9999).unwrap();
1750 let proper = t.proper_time_dilation(1.0);
1751 assert!(proper < 0.02, "expected near-zero proper time at 0.9999c: {proper}");
1752 }
1753
1754 #[test]
1756 fn test_beta_from_rapidity_zero_rapidity_gives_zero_beta() {
1757 assert!((LorentzTransform::beta_from_rapidity(0.0)).abs() < 1e-12);
1758 }
1759
1760 #[test]
1761 fn test_beta_from_rapidity_roundtrip_with_rapidity() {
1762 let t = LorentzTransform::new(0.7).unwrap();
1763 let eta = t.rapidity();
1764 let beta = LorentzTransform::beta_from_rapidity(eta);
1765 assert!((beta - 0.7).abs() < 1e-10, "roundtrip failed: {beta}");
1766 }
1767
1768 #[test]
1769 fn test_beta_from_rapidity_bounded_below_one() {
1770 let beta = LorentzTransform::beta_from_rapidity(10.0);
1772 assert!(beta < 1.0 && beta > 0.9999);
1773 }
1774
1775 #[test]
1778 fn test_rapidity_zero_at_rest() {
1779 let t = LorentzTransform::new(0.0).unwrap();
1780 assert!(t.rapidity().abs() < 1e-12);
1781 }
1782
1783 #[test]
1784 fn test_rapidity_equals_atanh_beta() {
1785 let t = LorentzTransform::new(0.5).unwrap();
1786 assert!((t.rapidity() - 0.5_f64.atanh()).abs() < 1e-12);
1787 }
1788
1789 #[test]
1790 fn test_rapidity_positive_for_nonzero_beta() {
1791 let t = LorentzTransform::new(0.8).unwrap();
1792 assert!(t.rapidity() > 0.0);
1793 }
1794
1795 #[test]
1798 fn test_doppler_factor_one_at_rest() {
1799 let t = LorentzTransform::new(0.0).unwrap();
1800 assert!((t.doppler_factor() - 1.0).abs() < 1e-12);
1801 }
1802
1803 #[test]
1804 fn test_doppler_factor_greater_than_one_when_approaching() {
1805 let t = LorentzTransform::new(0.6).unwrap();
1806 assert!((t.doppler_factor() - 2.0).abs() < 1e-10);
1808 }
1809
1810 #[test]
1811 fn test_doppler_factor_inverse_of_receding() {
1812 let t1 = LorentzTransform::new(0.6).unwrap();
1813 let t2 = LorentzTransform::new(0.6).unwrap();
1814 let fwd = t1.doppler_factor();
1816 let rev = 1.0 / t2.doppler_factor();
1817 assert!((fwd - 1.0 / rev).abs() < 1e-10);
1818 }
1819
1820 #[test]
1823 fn test_aberration_angle_at_rest_unchanged() {
1824 let t = LorentzTransform::new(0.0).unwrap();
1825 let angle_in = std::f64::consts::PI / 3.0; let cos_in = angle_in.cos();
1827 let angle_out = t.aberration_angle(cos_in);
1828 assert!((angle_out - angle_in).abs() < 1e-12);
1829 }
1830
1831 #[test]
1832 fn test_aberration_angle_head_on_unchanged() {
1833 let t = LorentzTransform::new(0.5).unwrap();
1834 let angle_out = t.aberration_angle(1.0);
1836 assert!(angle_out.abs() < 1e-12);
1837 }
1838
1839 #[test]
1840 fn test_aberration_angle_range_zero_to_pi() {
1841 let t = LorentzTransform::new(0.8).unwrap();
1842 let angle = t.aberration_angle(0.0);
1843 assert!(angle >= 0.0 && angle <= std::f64::consts::PI);
1844 }
1845
1846 #[test]
1849 fn test_relativistic_mass_at_rest_equals_rest_mass() {
1850 let t = LorentzTransform::new(0.0).unwrap();
1851 assert!((t.relativistic_mass(1.0) - 1.0).abs() < 1e-12);
1852 }
1853
1854 #[test]
1855 fn test_relativistic_mass_increases_with_speed() {
1856 let t = LorentzTransform::new(0.6).unwrap();
1857 assert!((t.relativistic_mass(1.0) - 1.25).abs() < 1e-10);
1859 }
1860
1861 #[test]
1862 fn test_energy_ratio_zero_at_rest() {
1863 let t = LorentzTransform::new(0.0).unwrap();
1864 assert!(t.energy_ratio().abs() < 1e-12);
1865 }
1866
1867 #[test]
1868 fn test_energy_ratio_positive_for_nonzero_beta() {
1869 let t = LorentzTransform::new(0.6).unwrap();
1870 assert!((t.energy_ratio() - 0.25).abs() < 1e-10);
1872 }
1873
1874 #[test]
1877 fn test_momentum_ratio_zero_at_rest() {
1878 let t = LorentzTransform::new(0.0).unwrap();
1879 assert!(t.momentum_ratio().abs() < 1e-12);
1880 }
1881
1882 #[test]
1883 fn test_momentum_ratio_correct_at_0_6() {
1884 let t = LorentzTransform::new(0.6).unwrap();
1885 assert!((t.momentum_ratio() - 0.75).abs() < 1e-10);
1887 }
1888
1889 #[test]
1890 fn test_is_ultra_relativistic_true_above_0_9() {
1891 let t = LorentzTransform::new(0.95).unwrap();
1892 assert!(t.is_ultra_relativistic());
1893 }
1894
1895 #[test]
1896 fn test_is_ultra_relativistic_false_below_0_9() {
1897 let t = LorentzTransform::new(0.8).unwrap();
1898 assert!(!t.is_ultra_relativistic());
1899 }
1900
1901 #[test]
1902 fn test_is_ultra_relativistic_false_at_rest() {
1903 let t = LorentzTransform::new(0.0).unwrap();
1904 assert!(!t.is_ultra_relativistic());
1905 }
1906
1907 #[test]
1910 fn test_lorentz_factor_approx_equals_one_at_rest() {
1911 let t = LorentzTransform::new(0.0).unwrap();
1912 assert!((t.lorentz_factor_approx() - 1.0).abs() < 1e-12);
1913 }
1914
1915 #[test]
1916 fn test_lorentz_factor_approx_close_for_small_beta() {
1917 let t = LorentzTransform::new(0.1).unwrap();
1918 let approx = t.lorentz_factor_approx();
1920 let exact = t.gamma();
1921 assert!((approx - exact).abs() < 0.001);
1922 }
1923
1924 #[test]
1925 fn test_lorentz_factor_approx_always_gte_one() {
1926 let t = LorentzTransform::new(0.5).unwrap();
1927 assert!(t.lorentz_factor_approx() >= 1.0);
1928 }
1929
1930 #[test]
1933 fn test_velocity_ratio_equals_one_for_same_beta() {
1934 let t = LorentzTransform::new(0.6).unwrap();
1935 assert!((t.velocity_ratio(&t) - 1.0).abs() < 1e-12);
1936 }
1937
1938 #[test]
1939 fn test_velocity_ratio_correct() {
1940 let a = LorentzTransform::new(0.6).unwrap();
1941 let b = LorentzTransform::new(0.3).unwrap();
1942 assert!((a.velocity_ratio(&b) - 2.0).abs() < 1e-12);
1943 }
1944
1945 #[test]
1946 fn test_velocity_ratio_less_than_one_when_slower() {
1947 let a = LorentzTransform::new(0.3).unwrap();
1948 let b = LorentzTransform::new(0.6).unwrap();
1949 assert!(a.velocity_ratio(&b) < 1.0);
1950 }
1951
1952 #[test]
1955 fn test_proper_length_at_rest_equals_observed() {
1956 let t = LorentzTransform::new(0.0).unwrap();
1957 assert!((t.proper_length(10.0) - 10.0).abs() < 1e-10);
1959 }
1960
1961 #[test]
1962 fn test_proper_length_greater_than_observed_when_moving() {
1963 let t = LorentzTransform::new(0.6).unwrap();
1964 assert!((t.proper_length(8.0) - 10.0).abs() < 1e-9);
1966 }
1967
1968 #[test]
1971 fn test_length_contraction_at_rest_equals_rest_length() {
1972 let t = LorentzTransform::new(0.0).unwrap();
1973 assert!((t.length_contraction(10.0) - 10.0).abs() < 1e-10);
1974 }
1975
1976 #[test]
1977 fn test_length_contraction_shorter_when_moving() {
1978 let t = LorentzTransform::new(0.6).unwrap();
1979 assert!((t.length_contraction(10.0) - 8.0).abs() < 1e-9);
1981 }
1982
1983 #[test]
1984 fn test_length_contraction_proper_length_roundtrip() {
1985 let t = LorentzTransform::new(0.8).unwrap();
1986 let rest = 100.0;
1987 let contracted = t.length_contraction(rest);
1988 let recovered = t.proper_length(contracted);
1989 assert!((recovered - rest).abs() < 1e-9);
1990 }
1991
1992 #[test]
1995 fn test_beta_times_gamma_zero_at_rest() {
1996 let t = LorentzTransform::new(0.0).unwrap();
1997 assert_eq!(t.beta_times_gamma(), 0.0);
1998 }
1999
2000 #[test]
2001 fn test_beta_times_gamma_positive_for_nonzero_velocity() {
2002 let t = LorentzTransform::new(0.6).unwrap();
2003 let bg = t.beta_times_gamma();
2005 assert!((bg - 0.75).abs() < 1e-9);
2006 }
2007
2008 #[test]
2009 fn test_beta_times_gamma_increases_with_velocity() {
2010 let t1 = LorentzTransform::new(0.5).unwrap();
2011 let t2 = LorentzTransform::new(0.9).unwrap();
2012 assert!(t2.beta_times_gamma() > t1.beta_times_gamma());
2013 }
2014
2015 #[test]
2018 fn test_energy_momentum_invariant_equals_mass_squared_at_rest() {
2019 let t = LorentzTransform::new(0.0).unwrap();
2020 let mass = 2.0;
2021 let inv = t.energy_momentum_invariant(mass);
2022 assert!((inv - mass * mass).abs() < 1e-9);
2023 }
2024
2025 #[test]
2026 fn test_energy_momentum_invariant_equals_mass_squared_at_velocity() {
2027 let t = LorentzTransform::new(0.8).unwrap();
2028 let mass = 3.0;
2029 let inv = t.energy_momentum_invariant(mass);
2030 assert!((inv - mass * mass).abs() < 1e-9);
2031 }
2032}