use crate::error::StreamError;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SpacetimePoint {
pub t: f64,
pub x: f64,
}
impl SpacetimePoint {
pub fn new(t: f64, x: f64) -> Self {
Self { t, x }
}
pub fn displacement(self, other: Self) -> Self {
Self {
t: other.t - self.t,
x: other.x - self.x,
}
}
pub fn norm_sq(self) -> f64 {
self.t * self.t - self.x * self.x
}
pub fn is_timelike(self) -> bool {
self.norm_sq() > 0.0
}
pub fn is_lightlike(self) -> bool {
self.norm_sq().abs() < 1e-10
}
pub fn is_spacelike(self) -> bool {
self.norm_sq() < 0.0
}
pub fn distance_to(self, other: Self) -> f64 {
let d = self.displacement(other);
d.norm_sq().abs().sqrt()
}
}
#[derive(Debug, Clone, Copy)]
pub struct LorentzTransform {
beta: f64,
gamma: f64,
}
impl LorentzTransform {
pub fn new(beta: f64) -> Result<Self, StreamError> {
if beta.is_nan() || beta < 0.0 || beta >= 1.0 {
return Err(StreamError::LorentzConfigError {
reason: format!(
"beta must be in [0.0, 1.0) but got {beta}; \
beta >= 1 produces a division by zero in the Lorentz factor"
),
});
}
let gamma = 1.0 / (1.0 - beta * beta).sqrt();
Ok(Self { beta, gamma })
}
pub fn from_rapidity(eta: f64) -> Result<Self, StreamError> {
if !eta.is_finite() {
return Err(StreamError::LorentzConfigError {
reason: format!("rapidity must be finite, got {eta}"),
});
}
let beta = eta.tanh();
Self::new(beta)
}
pub fn from_velocity(v: f64, c: f64) -> Result<Self, StreamError> {
if !v.is_finite() || !c.is_finite() {
return Err(StreamError::LorentzConfigError {
reason: format!("v and c must be finite, got v={v}, c={c}"),
});
}
if c == 0.0 {
return Err(StreamError::LorentzConfigError {
reason: "speed of light c must be non-zero".into(),
});
}
Self::new(v / c)
}
pub fn gamma_at(beta: f64) -> f64 {
(1.0 - beta * beta).sqrt().recip()
}
pub fn relativistic_momentum(&self, mass: f64) -> f64 {
mass * self.beta_times_gamma()
}
pub fn kinetic_energy_ratio(&self) -> f64 {
self.gamma() - 1.0
}
pub fn time_dilation_factor(&self) -> f64 {
self.gamma()
}
pub fn length_contraction_factor(&self) -> f64 {
1.0 / self.gamma()
}
pub fn beta_from_gamma(gamma: f64) -> Result<f64, StreamError> {
if gamma.is_nan() || gamma < 1.0 {
return Err(StreamError::LorentzConfigError {
reason: format!("gamma must be >= 1.0, got {gamma}"),
});
}
Ok((1.0 - 1.0 / (gamma * gamma)).sqrt())
}
pub fn rapidity(&self) -> f64 {
self.beta.atanh()
}
pub fn beta_times_gamma(&self) -> f64 {
self.beta * self.gamma
}
pub fn beta_from_rapidity(eta: f64) -> f64 {
eta.tanh()
}
pub fn proper_velocity(&self) -> f64 {
self.beta_times_gamma()
}
pub fn beta(&self) -> f64 {
self.beta
}
pub fn gamma(&self) -> f64 {
self.gamma
}
#[must_use]
pub fn transform(&self, p: SpacetimePoint) -> SpacetimePoint {
let t_prime = self.gamma * (p.t - self.beta * p.x);
let x_prime = self.gamma * (p.x - self.beta * p.t);
SpacetimePoint {
t: t_prime,
x: x_prime,
}
}
#[must_use]
pub fn inverse_transform(&self, p: SpacetimePoint) -> SpacetimePoint {
let t_orig = self.gamma * (p.t + self.beta * p.x);
let x_orig = self.gamma * (p.x + self.beta * p.t);
SpacetimePoint {
t: t_orig,
x: x_orig,
}
}
pub fn transform_batch(&self, points: &[SpacetimePoint]) -> Vec<SpacetimePoint> {
points.iter().map(|&p| self.transform(p)).collect()
}
pub fn inverse_transform_batch(&self, points: &[SpacetimePoint]) -> Vec<SpacetimePoint> {
points.iter().map(|&p| self.inverse_transform(p)).collect()
}
pub fn dilate_time(&self, t: f64) -> f64 {
self.gamma() * t
}
pub fn contract_length(&self, x: f64) -> f64 {
self.gamma() * x
}
pub fn time_contraction(&self, coordinate_time: f64) -> f64 {
coordinate_time / self.gamma()
}
pub fn is_ultrarelativistic(&self) -> bool {
self.beta > 0.9
}
pub fn momentum_factor(&self) -> f64 {
self.beta_times_gamma()
}
pub fn momentum(&self, mass: f64) -> f64 {
self.relativistic_momentum(mass)
}
pub fn relativistic_energy(&self, rest_mass: f64) -> f64 {
self.gamma() * rest_mass
}
pub fn kinetic_energy(&self, rest_mass: f64) -> f64 {
self.kinetic_energy_ratio() * rest_mass
}
pub fn energy_momentum_invariant(&self, rest_mass: f64) -> f64 {
let e = self.relativistic_energy(rest_mass);
let p = self.relativistic_momentum(rest_mass);
e * e - p * p
}
pub fn four_velocity_time(&self) -> f64 {
self.gamma()
}
pub fn proper_time_dilation(&self, dt: f64) -> f64 {
self.time_contraction(dt)
}
pub fn spacetime_interval(p1: SpacetimePoint, p2: SpacetimePoint) -> f64 {
p1.displacement(p2).norm_sq()
}
pub fn velocity_addition(beta1: f64, beta2: f64) -> Result<f64, StreamError> {
if beta1.is_nan() || beta1 < 0.0 || beta1 >= 1.0 {
return Err(StreamError::LorentzConfigError {
reason: format!("beta1 must be in [0.0, 1.0), got {beta1}"),
});
}
if beta2.is_nan() || beta2 < 0.0 || beta2 >= 1.0 {
return Err(StreamError::LorentzConfigError {
reason: format!("beta2 must be in [0.0, 1.0), got {beta2}"),
});
}
let composed = (beta1 + beta2) / (1.0 + beta1 * beta2);
if composed >= 1.0 {
return Err(StreamError::LorentzConfigError {
reason: format!("composed velocity {composed} >= 1.0 (speed of light)"),
});
}
Ok(composed)
}
pub fn proper_time(&self, coordinate_time: f64) -> f64 {
self.time_contraction(coordinate_time)
}
pub fn inverse(&self) -> Self {
Self { beta: -self.beta, gamma: self.gamma }
}
#[deprecated(since = "2.2.0", note = "Use `dilate_time` instead")]
pub fn time_dilation(&self, proper_time: f64) -> f64 {
self.dilate_time(proper_time)
}
pub fn compose(&self, other: &LorentzTransform) -> Result<Self, StreamError> {
self.composition(other)
}
pub fn boost_chain(betas: &[f64]) -> Result<Self, StreamError> {
let mut result = LorentzTransform::new(0.0)?;
for &beta in betas {
let next = LorentzTransform::new(beta)?;
result = result.compose(&next)?;
}
Ok(result)
}
pub fn is_identity(&self) -> bool {
self.beta.abs() < 1e-10
}
pub fn composition(&self, other: &Self) -> Result<Self, StreamError> {
let b1 = self.beta;
let b2 = other.beta;
let denom = 1.0 + b1 * b2;
if denom.abs() < 1e-15 {
return Err(StreamError::LorentzConfigError {
reason: "boost composition denominator too small (near-singular)".into(),
});
}
Self::new((b1 + b2) / denom)
}
#[deprecated(since = "2.2.0", note = "Use `doppler_ratio` instead")]
pub fn doppler_factor(&self) -> f64 {
self.doppler_ratio()
}
pub fn aberration_angle(&self, cos_theta: f64) -> f64 {
let cos_prime = (cos_theta + self.beta) / (1.0 + self.beta * cos_theta);
cos_prime.clamp(-1.0, 1.0).acos()
}
#[deprecated(since = "2.2.0", note = "Use `relativistic_energy` instead")]
pub fn relativistic_mass(&self, rest_mass: f64) -> f64 {
self.relativistic_energy(rest_mass)
}
#[deprecated(since = "2.2.0", note = "Use `kinetic_energy_ratio` instead")]
pub fn energy_ratio(&self) -> f64 {
self.kinetic_energy_ratio()
}
pub fn warp_factor(&self) -> f64 {
self.gamma().cbrt()
}
pub fn four_momentum(&self, mass: f64) -> (f64, f64) {
let g = self.gamma();
(g * mass, g * mass * self.beta)
}
#[deprecated(since = "2.2.0", note = "Use `beta_times_gamma` instead")]
pub fn momentum_ratio(&self) -> f64 {
self.beta_times_gamma()
}
#[deprecated(since = "2.2.0", note = "Use `is_ultrarelativistic` instead")]
pub fn is_ultra_relativistic(&self) -> bool {
self.is_ultrarelativistic()
}
pub fn lorentz_factor_approx(&self) -> f64 {
1.0 + 0.5 * self.beta * self.beta
}
pub fn velocity_ratio(&self, other: &Self) -> f64 {
self.beta / other.beta
}
pub fn proper_length(&self, observed: f64) -> f64 {
self.dilate_time(observed)
}
pub fn length_contraction(&self, rest_length: f64) -> f64 {
self.time_contraction(rest_length)
}
pub fn light_cone_check(dt: f64, dx: f64) -> &'static str {
let s_sq = dt * dt - dx * dx;
if s_sq > 1e-12 {
"timelike"
} else if s_sq < -1e-12 {
"spacelike"
} else {
"lightlike"
}
}
pub fn lorentz_invariant_mass(energy: f64, momentum: f64) -> Option<f64> {
let m_sq = energy * energy - momentum * momentum;
if m_sq < 0.0 { return None; }
Some(m_sq.sqrt())
}
pub fn aberration_correction(&self, cos_theta: f64) -> Option<f64> {
let denom = 1.0 - self.beta * cos_theta;
if denom.abs() < 1e-15 { return None; }
Some((cos_theta - self.beta) / denom)
}
pub fn doppler_ratio(&self) -> f64 {
((1.0 + self.beta) / (1.0 - self.beta)).sqrt()
}
pub fn time_dilation_ms(&self, proper_ms: f64) -> f64 {
self.dilate_time(proper_ms)
}
#[deprecated(since = "2.2.0", note = "Use `length_contraction` instead")]
pub fn space_contraction(&self, proper_length: f64) -> f64 {
self.length_contraction(proper_length)
}
pub fn proper_acceleration(&self, force: f64, mass: f64) -> Option<f64> {
if mass == 0.0 { return None; }
Some(force / (self.gamma.powi(3) * mass))
}
pub fn momentum_rapidity(&self) -> f64 {
self.beta_times_gamma()
}
pub fn inverse_gamma(&self) -> f64 {
self.length_contraction_factor()
}
pub fn boost_composition(beta1: f64, beta2: f64) -> Result<f64, crate::error::StreamError> {
let denom = 1.0 + beta1 * beta2;
if denom.abs() < 1e-15 {
return Err(crate::error::StreamError::LorentzConfigError {
reason: "degenerate boost composition".into(),
});
}
let result = (beta1 + beta2) / denom;
if result.abs() >= 1.0 {
return Err(crate::error::StreamError::LorentzConfigError {
reason: "boost exceeds c".into(),
});
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-10;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < EPS
}
fn point_approx_eq(a: SpacetimePoint, b: SpacetimePoint) -> bool {
approx_eq(a.t, b.t) && approx_eq(a.x, b.x)
}
#[test]
fn test_new_valid_beta() {
let lt = LorentzTransform::new(0.5).unwrap();
assert!((lt.beta() - 0.5).abs() < EPS);
}
#[test]
fn test_new_beta_zero() {
let lt = LorentzTransform::new(0.0).unwrap();
assert_eq!(lt.beta(), 0.0);
assert!((lt.gamma() - 1.0).abs() < EPS);
}
#[test]
fn test_new_beta_one_returns_error() {
let err = LorentzTransform::new(1.0).unwrap_err();
assert!(matches!(err, StreamError::LorentzConfigError { .. }));
}
#[test]
fn test_new_beta_above_one_returns_error() {
let err = LorentzTransform::new(1.5).unwrap_err();
assert!(matches!(err, StreamError::LorentzConfigError { .. }));
}
#[test]
fn test_new_beta_negative_returns_error() {
let err = LorentzTransform::new(-0.1).unwrap_err();
assert!(matches!(err, StreamError::LorentzConfigError { .. }));
}
#[test]
fn test_new_beta_nan_returns_error() {
let err = LorentzTransform::new(f64::NAN).unwrap_err();
assert!(matches!(err, StreamError::LorentzConfigError { .. }));
}
#[test]
fn test_beta_zero_is_identity_transform() {
let lt = LorentzTransform::new(0.0).unwrap();
let p = SpacetimePoint::new(3.0, 4.0);
let q = lt.transform(p);
assert!(point_approx_eq(p, q), "beta=0 must be identity, got {q:?}");
}
#[test]
fn test_time_dilation_at_x_zero() {
let lt = LorentzTransform::new(0.6).unwrap();
let p = SpacetimePoint::new(5.0, 0.0);
let q = lt.transform(p);
let expected_t = lt.gamma() * 5.0;
assert!(approx_eq(q.t, expected_t));
}
#[test]
fn test_dilate_time_helper() {
let lt = LorentzTransform::new(0.6).unwrap();
assert!(approx_eq(lt.dilate_time(1.0), lt.gamma()));
}
#[test]
fn test_length_contraction_at_t_zero() {
let lt = LorentzTransform::new(0.6).unwrap();
let p = SpacetimePoint::new(0.0, 5.0);
let q = lt.transform(p);
let expected_x = lt.gamma() * 5.0;
assert!(approx_eq(q.x, expected_x));
}
#[test]
fn test_contract_length_helper() {
let lt = LorentzTransform::new(0.6).unwrap();
assert!(approx_eq(lt.contract_length(1.0), lt.gamma()));
}
#[test]
fn test_known_beta_0_6_gamma_is_1_25() {
let lt = LorentzTransform::new(0.6).unwrap();
assert!(
(lt.gamma() - 1.25).abs() < 1e-9,
"gamma should be 1.25, got {}",
lt.gamma()
);
}
#[test]
fn test_known_beta_0_8_gamma() {
let lt = LorentzTransform::new(0.8).unwrap();
let expected_gamma = 5.0 / 3.0;
assert!((lt.gamma() - expected_gamma).abs() < 1e-9);
}
#[test]
fn test_known_beta_0_5_gamma() {
let lt = LorentzTransform::new(0.5).unwrap();
let expected_gamma = 2.0 / 3.0f64.sqrt();
assert!((lt.gamma() - expected_gamma).abs() < 1e-9);
}
#[test]
fn test_transform_known_point_beta_0_6() {
let lt = LorentzTransform::new(0.6).unwrap();
let p = SpacetimePoint::new(1.0, 0.0);
let q = lt.transform(p);
assert!((q.t - 1.25).abs() < 1e-9);
assert!((q.x - (-0.75)).abs() < 1e-9);
}
#[test]
fn test_inverse_transform_roundtrip() {
let lt = LorentzTransform::new(0.7).unwrap();
let p = SpacetimePoint::new(3.0, 1.5);
let q = lt.transform(p);
let r = lt.inverse_transform(q);
assert!(
point_approx_eq(r, p),
"round-trip failed: expected {p:?}, got {r:?}"
);
}
#[test]
fn test_transform_batch_length_preserved() {
let lt = LorentzTransform::new(0.3).unwrap();
let pts = vec![
SpacetimePoint::new(0.0, 1.0),
SpacetimePoint::new(1.0, 2.0),
SpacetimePoint::new(2.0, 3.0),
];
let out = lt.transform_batch(&pts);
assert_eq!(out.len(), pts.len());
}
#[test]
fn test_transform_batch_matches_individual() {
let lt = LorentzTransform::new(0.4).unwrap();
let pts = vec![SpacetimePoint::new(1.0, 0.5), SpacetimePoint::new(2.0, 1.5)];
let batch = lt.transform_batch(&pts);
for (i, &p) in pts.iter().enumerate() {
let individual = lt.transform(p);
assert!(
point_approx_eq(batch[i], individual),
"batch[{i}] differs from individual transform"
);
}
}
#[test]
fn test_inverse_transform_batch_roundtrip() {
let lt = LorentzTransform::new(0.5).unwrap();
let pts = vec![
SpacetimePoint::new(1.0, 0.0),
SpacetimePoint::new(2.0, 1.0),
SpacetimePoint::new(0.0, 3.0),
];
let transformed = lt.transform_batch(&pts);
let restored = lt.inverse_transform_batch(&transformed);
for (i, (&orig, &rest)) in pts.iter().zip(restored.iter()).enumerate() {
assert!(
point_approx_eq(orig, rest),
"round-trip failed at index {i}: expected {orig:?}, got {rest:?}"
);
}
}
#[test]
fn test_inverse_transform_batch_length_preserved() {
let lt = LorentzTransform::new(0.3).unwrap();
let pts = vec![SpacetimePoint::new(0.0, 0.0), SpacetimePoint::new(1.0, 1.0)];
assert_eq!(lt.inverse_transform_batch(&pts).len(), pts.len());
}
#[test]
fn test_spacetime_point_fields() {
let p = SpacetimePoint::new(1.5, 2.5);
assert_eq!(p.t, 1.5);
assert_eq!(p.x, 2.5);
}
#[test]
fn test_spacetime_point_equality() {
let p = SpacetimePoint::new(1.0, 2.0);
let q = SpacetimePoint::new(1.0, 2.0);
assert_eq!(p, q);
}
#[test]
fn test_compose_identity_with_identity() {
let lt = LorentzTransform::new(0.0).unwrap();
let composed = lt.compose(<).unwrap();
assert!(approx_eq(composed.beta(), 0.0));
}
#[test]
fn test_compose_0_5_and_0_5() {
let lt = LorentzTransform::new(0.5).unwrap();
let composed = lt.compose(<).unwrap();
let expected = (0.5 + 0.5) / (1.0 + 0.5 * 0.5); assert!((composed.beta() - expected).abs() < EPS);
}
#[test]
fn test_compose_with_negative_is_identity() {
let lt_fwd = LorentzTransform::new(0.3).unwrap();
let lt_bwd = LorentzTransform::new(0.3).unwrap();
let composed = lt_fwd.compose(<_bwd).unwrap();
assert!(composed.beta() < 1.0);
}
#[test]
fn test_rapidity_zero_beta() {
let lt = LorentzTransform::new(0.0).unwrap();
assert!(approx_eq(lt.rapidity(), 0.0));
}
#[test]
fn test_rapidity_known_value() {
let lt = LorentzTransform::new(0.5).unwrap();
let expected = (0.5f64).atanh();
assert!((lt.rapidity() - expected).abs() < EPS);
}
#[test]
fn test_rapidity_is_additive_under_composition() {
let lt1 = LorentzTransform::new(0.3).unwrap();
let lt2 = LorentzTransform::new(0.4).unwrap();
let composed = lt1.compose(<2).unwrap();
let sum_rapidities = lt1.rapidity() + lt2.rapidity();
assert!(
(composed.rapidity() - sum_rapidities).abs() < 1e-9,
"rapidity should be additive: {} vs {}",
composed.rapidity(),
sum_rapidities
);
}
#[test]
fn test_velocity_addition_known_values() {
let result = LorentzTransform::velocity_addition(0.5, 0.5).unwrap();
assert!((result - 0.8).abs() < EPS);
}
#[test]
fn test_velocity_addition_identity_with_zero() {
let result = LorentzTransform::velocity_addition(0.0, 0.6).unwrap();
assert!((result - 0.6).abs() < EPS);
}
#[test]
fn test_velocity_addition_invalid_beta_rejected() {
assert!(LorentzTransform::velocity_addition(1.0, 0.5).is_err());
assert!(LorentzTransform::velocity_addition(0.5, 1.0).is_err());
assert!(LorentzTransform::velocity_addition(-0.1, 0.5).is_err());
}
#[test]
fn test_velocity_addition_matches_compose() {
let b1 = 0.3;
let b2 = 0.4;
let static_result = LorentzTransform::velocity_addition(b1, b2).unwrap();
let lt1 = LorentzTransform::new(b1).unwrap();
let lt2 = LorentzTransform::new(b2).unwrap();
let composed = lt1.compose(<2).unwrap();
assert!((static_result - composed.beta()).abs() < EPS);
}
#[test]
fn test_proper_time_identity_at_zero_beta() {
let lt = LorentzTransform::new(0.0).unwrap();
assert!(approx_eq(lt.proper_time(10.0), 10.0));
}
#[test]
fn test_proper_time_less_than_coordinate_time() {
let lt = LorentzTransform::new(0.6).unwrap(); let tau = lt.proper_time(5.0);
assert!((tau - 4.0).abs() < EPS);
assert!(tau < 5.0);
}
#[test]
fn test_proper_time_roundtrip_with_dilate_time() {
let lt = LorentzTransform::new(0.8).unwrap();
let t = 3.0;
let dilated = lt.dilate_time(t);
let recovered = lt.proper_time(dilated);
assert!(approx_eq(recovered, t));
}
#[test]
fn test_compose_equals_sequential_transforms() {
let lt1 = LorentzTransform::new(0.3).unwrap();
let lt2 = LorentzTransform::new(0.4).unwrap();
let composed = lt1.compose(<2).unwrap();
let p = SpacetimePoint::new(2.0, 1.0);
let sequential = lt2.transform(lt1.transform(p));
let single = composed.transform(p);
assert!(
point_approx_eq(sequential, single),
"composed boost must equal sequential: {sequential:?} vs {single:?}"
);
}
#[test]
fn test_displacement_gives_correct_deltas() {
let a = SpacetimePoint::new(1.0, 2.0);
let b = SpacetimePoint::new(4.0, 6.0);
let d = a.displacement(b);
assert!(approx_eq(d.t, 3.0));
assert!(approx_eq(d.x, 4.0));
}
#[test]
fn test_displacement_to_self_is_zero() {
let a = SpacetimePoint::new(3.0, 7.0);
let d = a.displacement(a);
assert!(approx_eq(d.t, 0.0));
assert!(approx_eq(d.x, 0.0));
}
#[test]
fn test_boost_chain_empty_is_identity() {
let chain = LorentzTransform::boost_chain(&[]).unwrap();
assert!(approx_eq(chain.beta(), 0.0));
assert!(approx_eq(chain.gamma(), 1.0));
}
#[test]
fn test_boost_chain_single_equals_new() {
let chain = LorentzTransform::boost_chain(&[0.5]).unwrap();
let direct = LorentzTransform::new(0.5).unwrap();
assert!(approx_eq(chain.beta(), direct.beta()));
}
#[test]
fn test_boost_chain_two_equals_compose() {
let chain = LorentzTransform::boost_chain(&[0.3, 0.4]).unwrap();
let manual = LorentzTransform::new(0.3)
.unwrap()
.compose(&LorentzTransform::new(0.4).unwrap())
.unwrap();
assert!(approx_eq(chain.beta(), manual.beta()));
}
#[test]
fn test_boost_chain_invalid_beta_returns_error() {
assert!(LorentzTransform::boost_chain(&[1.0]).is_err());
assert!(LorentzTransform::boost_chain(&[0.3, -0.1]).is_err());
}
#[test]
fn test_time_dilation_identity_at_zero_beta() {
let lt = LorentzTransform::new(0.0).unwrap();
assert!(approx_eq(lt.time_dilation(10.0), 10.0));
}
#[test]
fn test_time_dilation_inverse_of_proper_time() {
let lt = LorentzTransform::new(0.6).unwrap();
let t = 5.0_f64;
let tau = lt.proper_time(t);
assert!(approx_eq(lt.time_dilation(tau), t));
}
#[test]
fn test_time_dilation_greater_than_proper_time() {
let lt = LorentzTransform::new(0.8).unwrap();
let proper = 3.0_f64;
let coord = lt.time_dilation(proper);
assert!(coord > proper);
}
#[test]
fn test_inverse_negates_beta() {
let lt = LorentzTransform::new(0.6).unwrap();
let inv = lt.inverse();
assert!(approx_eq(inv.beta(), -0.6));
}
#[test]
fn test_inverse_preserves_gamma_magnitude() {
let lt = LorentzTransform::new(0.6).unwrap();
let inv = lt.inverse();
assert!(approx_eq(inv.gamma(), lt.gamma()));
}
#[test]
fn test_inverse_roundtrips_point() {
let lt = LorentzTransform::new(0.5).unwrap();
let p = SpacetimePoint::new(3.0, 1.0);
let boosted = lt.transform(p);
let recovered = lt.inverse().transform(boosted);
assert!(point_approx_eq(recovered, p));
}
#[test]
fn test_norm_sq_timelike() {
let p = SpacetimePoint::new(3.0, 1.0);
assert!((p.norm_sq() - 8.0).abs() < EPS);
assert!(p.is_timelike());
assert!(!p.is_spacelike());
assert!(!p.is_lightlike());
}
#[test]
fn test_norm_sq_spacelike() {
let p = SpacetimePoint::new(1.0, 3.0);
assert!((p.norm_sq() - (-8.0)).abs() < EPS);
assert!(p.is_spacelike());
assert!(!p.is_timelike());
assert!(!p.is_lightlike());
}
#[test]
fn test_norm_sq_lightlike() {
let p = SpacetimePoint::new(1.0, 1.0);
assert!(p.norm_sq().abs() < EPS);
assert!(p.is_lightlike());
assert!(!p.is_timelike());
assert!(!p.is_spacelike());
}
#[test]
fn test_norm_sq_origin_is_lightlike() {
let p = SpacetimePoint::new(0.0, 0.0);
assert!(p.is_lightlike());
}
#[test]
fn test_is_identity_beta_zero() {
let lt = LorentzTransform::new(0.0).unwrap();
assert!(lt.is_identity());
}
#[test]
fn test_is_not_identity_nonzero_beta() {
let lt = LorentzTransform::new(0.5).unwrap();
assert!(!lt.is_identity());
}
#[test]
fn test_is_identity_empty_boost_chain() {
let lt = LorentzTransform::boost_chain(&[]).unwrap();
assert!(lt.is_identity());
}
#[test]
fn test_beta_from_gamma_identity() {
let beta = LorentzTransform::beta_from_gamma(1.0).unwrap();
assert!(approx_eq(beta, 0.0));
}
#[test]
fn test_beta_from_gamma_roundtrip() {
let lt = LorentzTransform::new(0.6).unwrap();
let recovered = LorentzTransform::beta_from_gamma(lt.gamma()).unwrap();
assert!(approx_eq(recovered, 0.6));
}
#[test]
fn test_beta_from_gamma_invalid_rejected() {
assert!(LorentzTransform::beta_from_gamma(0.5).is_err()); assert!(LorentzTransform::beta_from_gamma(f64::NAN).is_err());
assert!(LorentzTransform::beta_from_gamma(-1.0).is_err());
}
#[test]
fn test_from_rapidity_zero_is_identity() {
let lt = LorentzTransform::from_rapidity(0.0).unwrap();
assert!(lt.is_identity());
}
#[test]
fn test_from_rapidity_positive_gives_valid_beta() {
let lt = LorentzTransform::from_rapidity(0.5).unwrap();
assert!((lt.beta() - 0.5_f64.tanh()).abs() < EPS);
}
#[test]
fn test_from_rapidity_infinite_rejected() {
assert!(LorentzTransform::from_rapidity(f64::INFINITY).is_err());
assert!(LorentzTransform::from_rapidity(f64::NEG_INFINITY).is_err());
assert!(LorentzTransform::from_rapidity(f64::NAN).is_err());
}
#[test]
fn test_distance_to_timelike_separation() {
let a = SpacetimePoint::new(0.0, 0.0);
let b = SpacetimePoint::new(3.0, 0.0);
assert!((a.distance_to(b) - 3.0).abs() < EPS);
}
#[test]
fn test_distance_to_spacelike_separation() {
let a = SpacetimePoint::new(0.0, 0.0);
let b = SpacetimePoint::new(0.0, 4.0);
assert!((a.distance_to(b) - 4.0).abs() < EPS);
}
#[test]
fn test_distance_to_same_point_is_zero() {
let p = SpacetimePoint::new(2.0, 3.0);
assert!(p.distance_to(p).abs() < EPS);
}
#[test]
fn test_gamma_at_zero_is_one() {
assert!((LorentzTransform::gamma_at(0.0) - 1.0).abs() < EPS);
}
#[test]
fn test_gamma_at_matches_constructor_gamma() {
let lt = LorentzTransform::new(0.6).unwrap();
let expected = lt.gamma();
let computed = LorentzTransform::gamma_at(0.6);
assert!((computed - expected).abs() < EPS);
}
#[test]
fn test_gamma_at_one_is_infinite() {
assert!(LorentzTransform::gamma_at(1.0).is_infinite());
}
#[test]
fn test_gamma_at_above_one_is_nan() {
assert!(LorentzTransform::gamma_at(1.1).is_nan());
}
#[test]
fn test_from_velocity_matches_beta_ratio() {
let t = LorentzTransform::from_velocity(0.6, 1.0).unwrap();
assert!((t.beta() - 0.6).abs() < 1e-12);
}
#[test]
fn test_from_velocity_zero_c_returns_error() {
assert!(matches!(
LorentzTransform::from_velocity(0.5, 0.0),
Err(StreamError::LorentzConfigError { .. })
));
}
#[test]
fn test_from_velocity_non_finite_returns_error() {
assert!(LorentzTransform::from_velocity(f64::INFINITY, 1.0).is_err());
assert!(LorentzTransform::from_velocity(0.5, f64::NAN).is_err());
}
#[test]
fn test_from_velocity_superluminal_returns_error() {
assert!(LorentzTransform::from_velocity(1.5, 1.0).is_err());
}
#[test]
fn test_relativistic_momentum_zero_beta_is_zero() {
let lt = LorentzTransform::new(0.0).unwrap();
assert!(approx_eq(lt.relativistic_momentum(1.0), 0.0));
}
#[test]
fn test_relativistic_momentum_known_value() {
let lt = LorentzTransform::new(0.6).unwrap();
assert!((lt.relativistic_momentum(2.0) - 1.5).abs() < 1e-9);
}
#[test]
fn test_relativistic_momentum_scales_with_mass() {
let lt = LorentzTransform::new(0.6).unwrap();
let p1 = lt.relativistic_momentum(1.0);
let p2 = lt.relativistic_momentum(2.0);
assert!((p2 - 2.0 * p1).abs() < 1e-9);
}
#[test]
fn test_kinetic_energy_ratio_zero_at_rest() {
let lt = LorentzTransform::new(0.0).unwrap();
assert!(approx_eq(lt.kinetic_energy_ratio(), 0.0));
}
#[test]
fn test_kinetic_energy_ratio_known_value() {
let lt = LorentzTransform::new(0.6).unwrap();
assert!((lt.kinetic_energy_ratio() - 0.25).abs() < 1e-9);
}
#[test]
fn test_kinetic_energy_ratio_positive_for_nonzero_beta() {
let lt = LorentzTransform::new(0.8).unwrap();
assert!(lt.kinetic_energy_ratio() > 0.0);
}
#[test]
fn test_composition_zero_with_zero_is_zero() {
let t1 = LorentzTransform::new(0.0).unwrap();
let t2 = LorentzTransform::new(0.0).unwrap();
let composed = t1.composition(&t2).unwrap();
assert!(composed.beta().abs() < 1e-12);
}
#[test]
fn test_composition_with_opposite_is_near_zero() {
let t1 = LorentzTransform::new(0.6).unwrap();
let t2 = t1.inverse(); let composed = t1.composition(&t2).unwrap();
assert!(composed.beta().abs() < 1e-12);
}
#[test]
fn test_composition_velocity_addition_known_value() {
let t1 = LorentzTransform::new(0.5).unwrap();
let t2 = LorentzTransform::new(0.5).unwrap();
let composed = t1.composition(&t2).unwrap();
assert!((composed.beta() - 0.8).abs() < 1e-12);
}
#[test]
fn test_time_dilation_factor_one_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!((t.time_dilation_factor() - 1.0).abs() < 1e-12);
}
#[test]
fn test_time_dilation_factor_equals_gamma() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.time_dilation_factor() - t.gamma()).abs() < 1e-12);
}
#[test]
fn test_time_dilation_factor_greater_than_one_for_nonzero_beta() {
let t = LorentzTransform::new(0.8).unwrap();
assert!(t.time_dilation_factor() > 1.0);
}
#[test]
fn test_time_contraction_inverse_of_dilate_time() {
let t = LorentzTransform::new(0.6).unwrap();
let original = 100.0_f64;
let dilated = t.dilate_time(original);
let contracted = t.time_contraction(dilated);
assert!((contracted - original).abs() < 1e-10);
}
#[test]
fn test_time_contraction_at_zero_beta_equals_input() {
let t = LorentzTransform::new(0.0).unwrap();
assert!((t.time_contraction(42.0) - 42.0).abs() < 1e-12);
}
#[test]
fn test_time_contraction_less_than_input_for_nonzero_beta() {
let t = LorentzTransform::new(0.8).unwrap();
assert!(t.time_contraction(100.0) < 100.0);
}
#[test]
fn test_length_contraction_factor_one_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!((t.length_contraction_factor() - 1.0).abs() < 1e-12);
}
#[test]
fn test_length_contraction_factor_is_reciprocal_of_gamma() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.length_contraction_factor() - 1.0 / t.gamma()).abs() < 1e-12);
}
#[test]
fn test_length_contraction_factor_less_than_one_for_nonzero_beta() {
let t = LorentzTransform::new(0.9).unwrap();
assert!(t.length_contraction_factor() < 1.0);
}
#[test]
fn test_proper_velocity_zero_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!((t.proper_velocity() - 0.0).abs() < 1e-12);
}
#[test]
fn test_proper_velocity_equals_beta_times_gamma() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.proper_velocity() - t.beta() * t.gamma()).abs() < 1e-12);
}
#[test]
fn test_proper_velocity_greater_than_beta_for_high_speed() {
let t = LorentzTransform::new(0.8).unwrap();
assert!(t.proper_velocity() > t.beta());
}
#[test]
fn test_is_ultrarelativistic_true_above_0_9() {
let t = LorentzTransform::new(0.95).unwrap();
assert!(t.is_ultrarelativistic());
}
#[test]
fn test_is_ultrarelativistic_false_below_0_9() {
let t = LorentzTransform::new(0.5).unwrap();
assert!(!t.is_ultrarelativistic());
}
#[test]
fn test_is_ultrarelativistic_false_at_exactly_0_9() {
let t = LorentzTransform::new(0.9).unwrap();
assert!(!t.is_ultrarelativistic()); }
#[test]
fn test_momentum_factor_zero_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!(t.momentum_factor().abs() < 1e-12);
}
#[test]
fn test_momentum_factor_equals_gamma_times_beta() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.momentum_factor() - t.gamma() * t.beta()).abs() < 1e-12);
}
#[test]
fn test_relativistic_energy_equals_rest_mass_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!((t.relativistic_energy(1.0) - 1.0).abs() < 1e-12);
}
#[test]
fn test_relativistic_energy_greater_than_rest_mass_for_moving_particle() {
let t = LorentzTransform::new(0.6).unwrap();
assert!(t.relativistic_energy(1.0) > 1.0);
}
#[test]
fn test_relativistic_energy_equals_gamma_times_rest_mass() {
let t = LorentzTransform::new(0.8).unwrap();
let m = 2.5;
assert!((t.relativistic_energy(m) - t.gamma() * m).abs() < 1e-12);
}
#[test]
fn test_kinetic_energy_zero_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!(t.kinetic_energy(1.0).abs() < 1e-12);
}
#[test]
fn test_kinetic_energy_positive_for_moving_particle() {
let t = LorentzTransform::new(0.6).unwrap();
assert!(t.kinetic_energy(1.0) > 0.0);
}
#[test]
fn test_kinetic_energy_equals_total_minus_rest() {
let t = LorentzTransform::new(0.8).unwrap();
let m = 3.0;
let ke = t.kinetic_energy(m);
let total = t.relativistic_energy(m);
assert!((ke - (total - m)).abs() < 1e-12);
}
#[test]
fn test_four_velocity_time_equals_gamma() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.four_velocity_time() - t.gamma()).abs() < 1e-12);
}
#[test]
fn test_four_velocity_time_one_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!((t.four_velocity_time() - 1.0).abs() < 1e-12);
}
#[test]
fn test_four_velocity_time_greater_than_one_when_moving() {
let t = LorentzTransform::new(0.5).unwrap();
assert!(t.four_velocity_time() > 1.0);
}
#[test]
fn test_proper_time_dilation_at_rest_equals_dt() {
let t = LorentzTransform::new(0.0).unwrap(); assert!((t.proper_time_dilation(10.0) - 10.0).abs() < 1e-10);
}
#[test]
fn test_proper_time_dilation_less_than_dt_when_moving() {
let t = LorentzTransform::new(0.6).unwrap(); let proper = t.proper_time_dilation(10.0);
assert!(proper < 10.0, "moving clock should run slow: {proper}");
}
#[test]
fn test_proper_time_dilation_approaches_zero_at_high_speed() {
let t = LorentzTransform::new(0.9999).unwrap();
let proper = t.proper_time_dilation(1.0);
assert!(proper < 0.02, "expected near-zero proper time at 0.9999c: {proper}");
}
#[test]
fn test_beta_from_rapidity_zero_rapidity_gives_zero_beta() {
assert!((LorentzTransform::beta_from_rapidity(0.0)).abs() < 1e-12);
}
#[test]
fn test_beta_from_rapidity_roundtrip_with_rapidity() {
let t = LorentzTransform::new(0.7).unwrap();
let eta = t.rapidity();
let beta = LorentzTransform::beta_from_rapidity(eta);
assert!((beta - 0.7).abs() < 1e-10, "roundtrip failed: {beta}");
}
#[test]
fn test_beta_from_rapidity_bounded_below_one() {
let beta = LorentzTransform::beta_from_rapidity(10.0);
assert!(beta < 1.0 && beta > 0.9999);
}
#[test]
fn test_rapidity_zero_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!(t.rapidity().abs() < 1e-12);
}
#[test]
fn test_rapidity_equals_atanh_beta() {
let t = LorentzTransform::new(0.5).unwrap();
assert!((t.rapidity() - 0.5_f64.atanh()).abs() < 1e-12);
}
#[test]
fn test_rapidity_positive_for_nonzero_beta() {
let t = LorentzTransform::new(0.8).unwrap();
assert!(t.rapidity() > 0.0);
}
#[test]
fn test_doppler_factor_one_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!((t.doppler_factor() - 1.0).abs() < 1e-12);
}
#[test]
fn test_doppler_factor_greater_than_one_when_approaching() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.doppler_factor() - 2.0).abs() < 1e-10);
}
#[test]
fn test_doppler_factor_inverse_of_receding() {
let t1 = LorentzTransform::new(0.6).unwrap();
let t2 = LorentzTransform::new(0.6).unwrap();
let fwd = t1.doppler_factor();
let rev = 1.0 / t2.doppler_factor();
assert!((fwd - 1.0 / rev).abs() < 1e-10);
}
#[test]
fn test_aberration_angle_at_rest_unchanged() {
let t = LorentzTransform::new(0.0).unwrap();
let angle_in = std::f64::consts::PI / 3.0; let cos_in = angle_in.cos();
let angle_out = t.aberration_angle(cos_in);
assert!((angle_out - angle_in).abs() < 1e-12);
}
#[test]
fn test_aberration_angle_head_on_unchanged() {
let t = LorentzTransform::new(0.5).unwrap();
let angle_out = t.aberration_angle(1.0);
assert!(angle_out.abs() < 1e-12);
}
#[test]
fn test_aberration_angle_range_zero_to_pi() {
let t = LorentzTransform::new(0.8).unwrap();
let angle = t.aberration_angle(0.0);
assert!(angle >= 0.0 && angle <= std::f64::consts::PI);
}
#[test]
fn test_relativistic_mass_at_rest_equals_rest_mass() {
let t = LorentzTransform::new(0.0).unwrap();
assert!((t.relativistic_mass(1.0) - 1.0).abs() < 1e-12);
}
#[test]
fn test_relativistic_mass_increases_with_speed() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.relativistic_mass(1.0) - 1.25).abs() < 1e-10);
}
#[test]
fn test_energy_ratio_zero_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!(t.energy_ratio().abs() < 1e-12);
}
#[test]
fn test_energy_ratio_positive_for_nonzero_beta() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.energy_ratio() - 0.25).abs() < 1e-10);
}
#[test]
fn test_momentum_ratio_zero_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!(t.momentum_ratio().abs() < 1e-12);
}
#[test]
fn test_momentum_ratio_correct_at_0_6() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.momentum_ratio() - 0.75).abs() < 1e-10);
}
#[test]
fn test_is_ultra_relativistic_true_above_0_9() {
let t = LorentzTransform::new(0.95).unwrap();
assert!(t.is_ultra_relativistic());
}
#[test]
fn test_is_ultra_relativistic_false_below_0_9() {
let t = LorentzTransform::new(0.8).unwrap();
assert!(!t.is_ultra_relativistic());
}
#[test]
fn test_is_ultra_relativistic_false_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!(!t.is_ultra_relativistic());
}
#[test]
fn test_lorentz_factor_approx_equals_one_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert!((t.lorentz_factor_approx() - 1.0).abs() < 1e-12);
}
#[test]
fn test_lorentz_factor_approx_close_for_small_beta() {
let t = LorentzTransform::new(0.1).unwrap();
let approx = t.lorentz_factor_approx();
let exact = t.gamma();
assert!((approx - exact).abs() < 0.001);
}
#[test]
fn test_lorentz_factor_approx_always_gte_one() {
let t = LorentzTransform::new(0.5).unwrap();
assert!(t.lorentz_factor_approx() >= 1.0);
}
#[test]
fn test_velocity_ratio_equals_one_for_same_beta() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.velocity_ratio(&t) - 1.0).abs() < 1e-12);
}
#[test]
fn test_velocity_ratio_correct() {
let a = LorentzTransform::new(0.6).unwrap();
let b = LorentzTransform::new(0.3).unwrap();
assert!((a.velocity_ratio(&b) - 2.0).abs() < 1e-12);
}
#[test]
fn test_velocity_ratio_less_than_one_when_slower() {
let a = LorentzTransform::new(0.3).unwrap();
let b = LorentzTransform::new(0.6).unwrap();
assert!(a.velocity_ratio(&b) < 1.0);
}
#[test]
fn test_proper_length_at_rest_equals_observed() {
let t = LorentzTransform::new(0.0).unwrap();
assert!((t.proper_length(10.0) - 10.0).abs() < 1e-10);
}
#[test]
fn test_proper_length_greater_than_observed_when_moving() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.proper_length(8.0) - 10.0).abs() < 1e-9);
}
#[test]
fn test_length_contraction_at_rest_equals_rest_length() {
let t = LorentzTransform::new(0.0).unwrap();
assert!((t.length_contraction(10.0) - 10.0).abs() < 1e-10);
}
#[test]
fn test_length_contraction_shorter_when_moving() {
let t = LorentzTransform::new(0.6).unwrap();
assert!((t.length_contraction(10.0) - 8.0).abs() < 1e-9);
}
#[test]
fn test_length_contraction_proper_length_roundtrip() {
let t = LorentzTransform::new(0.8).unwrap();
let rest = 100.0;
let contracted = t.length_contraction(rest);
let recovered = t.proper_length(contracted);
assert!((recovered - rest).abs() < 1e-9);
}
#[test]
fn test_beta_times_gamma_zero_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
assert_eq!(t.beta_times_gamma(), 0.0);
}
#[test]
fn test_beta_times_gamma_positive_for_nonzero_velocity() {
let t = LorentzTransform::new(0.6).unwrap();
let bg = t.beta_times_gamma();
assert!((bg - 0.75).abs() < 1e-9);
}
#[test]
fn test_beta_times_gamma_increases_with_velocity() {
let t1 = LorentzTransform::new(0.5).unwrap();
let t2 = LorentzTransform::new(0.9).unwrap();
assert!(t2.beta_times_gamma() > t1.beta_times_gamma());
}
#[test]
fn test_energy_momentum_invariant_equals_mass_squared_at_rest() {
let t = LorentzTransform::new(0.0).unwrap();
let mass = 2.0;
let inv = t.energy_momentum_invariant(mass);
assert!((inv - mass * mass).abs() < 1e-9);
}
#[test]
fn test_energy_momentum_invariant_equals_mass_squared_at_velocity() {
let t = LorentzTransform::new(0.8).unwrap();
let mass = 3.0;
let inv = t.energy_momentum_invariant(mass);
assert!((inv - mass * mass).abs() < 1e-9);
}
}