Skip to main content

proof_engine/relativistic/
lorentz.rs

1//! Lorentz contraction and transformations.
2
3use glam::{Vec2, Vec3, Vec4};
4
5/// Compute the Lorentz factor gamma = 1 / sqrt(1 - v^2/c^2).
6/// Clamps v to be strictly less than c.
7pub fn lorentz_factor(v: f64, c: f64) -> f64 {
8    let beta = (v / c).abs();
9    if beta >= 1.0 {
10        return f64::INFINITY;
11    }
12    1.0 / (1.0 - beta * beta).sqrt()
13}
14
15/// A four-vector in Minkowski spacetime with signature (+, -, -, -).
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct FourVector {
18    pub t: f64,
19    pub x: f64,
20    pub y: f64,
21    pub z: f64,
22}
23
24impl FourVector {
25    pub fn new(t: f64, x: f64, y: f64, z: f64) -> Self {
26        Self { t, x, y, z }
27    }
28
29    pub fn zero() -> Self {
30        Self { t: 0.0, x: 0.0, y: 0.0, z: 0.0 }
31    }
32
33    /// Minkowski dot product with signature (+, -, -, -).
34    pub fn dot(&self, other: &FourVector) -> f64 {
35        self.t * other.t - self.x * other.x - self.y * other.y - self.z * other.z
36    }
37
38    /// Minkowski norm squared (invariant interval).
39    pub fn norm_sq(&self) -> f64 {
40        self.dot(self)
41    }
42
43    /// Minkowski norm. Returns NaN for spacelike vectors if you take sqrt of negative.
44    pub fn norm(&self) -> f64 {
45        let ns = self.norm_sq();
46        if ns >= 0.0 {
47            ns.sqrt()
48        } else {
49            -(-ns).sqrt()
50        }
51    }
52
53    /// Spatial part as a Vec3.
54    pub fn spatial(&self) -> Vec3 {
55        Vec3::new(self.x as f32, self.y as f32, self.z as f32)
56    }
57
58    /// Spatial magnitude.
59    pub fn spatial_magnitude(&self) -> f64 {
60        (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
61    }
62
63    /// Apply a Lorentz boost along an arbitrary direction.
64    pub fn boost(&self, velocity: Vec3, c: f64) -> FourVector {
65        boost(self, velocity, c)
66    }
67
68    /// Scale by a scalar.
69    pub fn scale(&self, s: f64) -> FourVector {
70        FourVector::new(self.t * s, self.x * s, self.y * s, self.z * s)
71    }
72
73    /// Add two four-vectors.
74    pub fn add(&self, other: &FourVector) -> FourVector {
75        FourVector::new(
76            self.t + other.t,
77            self.x + other.x,
78            self.y + other.y,
79            self.z + other.z,
80        )
81    }
82
83    /// Subtract another four-vector.
84    pub fn sub(&self, other: &FourVector) -> FourVector {
85        FourVector::new(
86            self.t - other.t,
87            self.x - other.x,
88            self.y - other.y,
89            self.z - other.z,
90        )
91    }
92}
93
94impl std::ops::Add for FourVector {
95    type Output = FourVector;
96    fn add(self, rhs: FourVector) -> FourVector {
97        FourVector::new(self.t + rhs.t, self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
98    }
99}
100
101impl std::ops::Sub for FourVector {
102    type Output = FourVector;
103    fn sub(self, rhs: FourVector) -> FourVector {
104        FourVector::new(self.t - rhs.t, self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
105    }
106}
107
108impl std::ops::Mul<f64> for FourVector {
109    type Output = FourVector;
110    fn mul(self, rhs: f64) -> FourVector {
111        FourVector::new(self.t * rhs, self.x * rhs, self.y * rhs, self.z * rhs)
112    }
113}
114
115impl std::ops::Neg for FourVector {
116    type Output = FourVector;
117    fn neg(self) -> FourVector {
118        FourVector::new(-self.t, -self.x, -self.y, -self.z)
119    }
120}
121
122/// Lorentz boost parameters.
123#[derive(Debug, Clone)]
124pub struct LorentzBoost {
125    pub velocity: Vec3,
126    pub gamma: f64,
127}
128
129impl LorentzBoost {
130    pub fn new(velocity: Vec3, c: f64) -> Self {
131        let v = (velocity.length() as f64).min(c * 0.9999999);
132        Self {
133            velocity,
134            gamma: lorentz_factor(v, c),
135        }
136    }
137
138    pub fn beta(&self, c: f64) -> f64 {
139        self.velocity.length() as f64 / c
140    }
141
142    /// Apply this boost to a four-vector.
143    pub fn apply(&self, fv: &FourVector, c: f64) -> FourVector {
144        boost(fv, self.velocity, c)
145    }
146
147    /// Inverse boost (negate velocity).
148    pub fn inverse(&self) -> LorentzBoost {
149        LorentzBoost {
150            velocity: -self.velocity,
151            gamma: self.gamma,
152        }
153    }
154}
155
156/// General Lorentz boost of a four-vector along an arbitrary velocity direction.
157///
158/// Uses the standard formula for a boost along direction n = v/|v|:
159///   t' = gamma (t - (v . r) / c^2)
160///   r' = r + (gamma - 1)(r . n)n - gamma v t
161///
162/// where r = (x, y, z) spatial part.
163pub fn boost(four_vec: &FourVector, velocity: Vec3, c: f64) -> FourVector {
164    let vx = velocity.x as f64;
165    let vy = velocity.y as f64;
166    let vz = velocity.z as f64;
167    let v_mag = (vx * vx + vy * vy + vz * vz).sqrt();
168
169    if v_mag < 1e-15 {
170        return *four_vec;
171    }
172
173    let gamma = lorentz_factor(v_mag, c);
174    let nx = vx / v_mag;
175    let ny = vy / v_mag;
176    let nz = vz / v_mag;
177
178    // r dot n
179    let r_dot_n = four_vec.x * nx + four_vec.y * ny + four_vec.z * nz;
180    // r dot v
181    let r_dot_v = four_vec.x * vx + four_vec.y * vy + four_vec.z * vz;
182
183    let t_prime = gamma * (four_vec.t - r_dot_v / (c * c));
184    let coeff = (gamma - 1.0) * r_dot_n;
185    let x_prime = four_vec.x + coeff * nx - gamma * vx * four_vec.t;
186    let y_prime = four_vec.y + coeff * ny - gamma * vy * four_vec.t;
187    let z_prime = four_vec.z + coeff * nz - gamma * vz * four_vec.t;
188
189    FourVector::new(t_prime, x_prime, y_prime, z_prime)
190}
191
192/// Length contraction: L = L_0 / gamma.
193pub fn contract_length(proper_length: f64, v: f64, c: f64) -> f64 {
194    let gamma = lorentz_factor(v, c);
195    proper_length / gamma
196}
197
198/// Proper time: tau = t / gamma (coordinate time to proper time).
199pub fn proper_time(coordinate_time: f64, v: f64, c: f64) -> f64 {
200    let gamma = lorentz_factor(v, c);
201    coordinate_time / gamma
202}
203
204/// Relativistic velocity addition: w = (v1 + v2) / (1 + v1*v2/c^2).
205pub fn velocity_addition(v1: f64, v2: f64, c: f64) -> f64 {
206    (v1 + v2) / (1.0 + v1 * v2 / (c * c))
207}
208
209/// Rapidity: phi = atanh(v/c).
210pub fn rapidity(v: f64, c: f64) -> f64 {
211    (v / c).atanh()
212}
213
214/// Four-momentum from mass and 3-velocity.
215/// p^mu = (gamma*m*c, gamma*m*vx, gamma*m*vy, gamma*m*vz)
216pub fn four_momentum(mass: f64, velocity: Vec3, c: f64) -> FourVector {
217    let vx = velocity.x as f64;
218    let vy = velocity.y as f64;
219    let vz = velocity.z as f64;
220    let v_mag = (vx * vx + vy * vy + vz * vz).sqrt();
221    let gamma = lorentz_factor(v_mag, c);
222    FourVector::new(
223        gamma * mass * c,
224        gamma * mass * vx,
225        gamma * mass * vy,
226        gamma * mass * vz,
227    )
228}
229
230/// Relativistic energy: E = gamma * m * c^2.
231pub fn relativistic_energy(mass: f64, v: f64, c: f64) -> f64 {
232    lorentz_factor(v, c) * mass * c * c
233}
234
235/// Relativistic momentum magnitude: p = gamma * m * v.
236pub fn relativistic_momentum(mass: f64, v: f64, c: f64) -> f64 {
237    lorentz_factor(v, c) * mass * v
238}
239
240/// Invariant mass from energy and momentum: m^2 = E^2/c^4 - p^2/c^2.
241/// Returns the mass (taking sqrt, returns 0 for tachyonic cases).
242pub fn invariant_mass(energy: f64, momentum: f64, c: f64) -> f64 {
243    let m_sq = energy * energy / (c * c * c * c) - momentum * momentum / (c * c);
244    if m_sq >= 0.0 {
245        m_sq.sqrt()
246    } else {
247        0.0
248    }
249}
250
251/// Modifies entity visual scale based on velocity relative to observer.
252/// Contracts along the direction of motion by 1/gamma.
253#[derive(Debug, Clone)]
254pub struct LorentzContractor {
255    pub c: f64,
256}
257
258impl LorentzContractor {
259    pub fn new(c: f64) -> Self {
260        Self { c }
261    }
262
263    /// Compute contracted scale for an entity moving at `velocity` relative to observer.
264    /// Returns (scale_x, scale_y, scale_z) where the motion direction is contracted.
265    pub fn contracted_scale(&self, velocity: Vec3) -> Vec3 {
266        let v = velocity.length() as f64;
267        if v < 1e-12 {
268            return Vec3::ONE;
269        }
270        let gamma = lorentz_factor(v, self.c);
271        let contraction = (1.0 / gamma) as f32;
272        let dir = velocity.normalize();
273
274        // Contract along the motion direction, keep perpendicular unchanged.
275        // scale = I + (contraction - 1) * (dir outer dir)
276        // For a unit vector, the contracted component = contraction, others = 1
277        let sx = 1.0 + (contraction - 1.0) * dir.x * dir.x;
278        let sy = 1.0 + (contraction - 1.0) * dir.y * dir.y;
279        let sz = 1.0 + (contraction - 1.0) * dir.z * dir.z;
280
281        Vec3::new(sx, sy, sz)
282    }
283
284    /// Apply contraction to a list of vertex positions around a center.
285    pub fn contract_vertices(&self, vertices: &[Vec3], center: Vec3, velocity: Vec3) -> Vec<Vec3> {
286        let v = velocity.length() as f64;
287        if v < 1e-12 {
288            return vertices.to_vec();
289        }
290        let gamma = lorentz_factor(v, self.c);
291        let contraction = (1.0 / gamma) as f32;
292        let dir = velocity.normalize();
293
294        vertices.iter().map(|vert| {
295            let rel = *vert - center;
296            let along = rel.dot(dir) * dir;
297            let perp = rel - along;
298            center + along * contraction + perp
299        }).collect()
300    }
301}
302
303/// Render objects with velocity-dependent squish along motion direction.
304#[derive(Debug, Clone)]
305pub struct LorentzRenderer {
306    pub c: f64,
307    pub contraction_enabled: bool,
308    pub color_shift_enabled: bool,
309}
310
311impl LorentzRenderer {
312    pub fn new(c: f64) -> Self {
313        Self {
314            c,
315            contraction_enabled: true,
316            color_shift_enabled: false,
317        }
318    }
319
320    /// Compute the apparent position of a vertex given object velocity.
321    /// Contracts along the direction of motion.
322    pub fn apparent_vertex(
323        &self,
324        vertex: Vec3,
325        object_center: Vec3,
326        velocity: Vec3,
327    ) -> Vec3 {
328        if !self.contraction_enabled {
329            return vertex;
330        }
331        let v = velocity.length() as f64;
332        if v < 1e-12 {
333            return vertex;
334        }
335        let gamma = lorentz_factor(v, self.c);
336        let contraction = (1.0 / gamma) as f32;
337        let dir = velocity.normalize();
338        let rel = vertex - object_center;
339        let along = rel.dot(dir) * dir;
340        let perp = rel - along;
341        object_center + along * contraction + perp
342    }
343
344    /// Render a set of entity glyphs with Lorentz contraction applied.
345    pub fn render_glyphs(
346        &self,
347        positions: &[Vec3],
348        center: Vec3,
349        velocity: Vec3,
350    ) -> Vec<Vec3> {
351        positions.iter().map(|p| self.apparent_vertex(*p, center, velocity)).collect()
352    }
353
354    /// Compute velocity-dependent brightness factor.
355    /// Objects moving toward observer appear brighter due to beaming.
356    pub fn brightness_factor(&self, velocity: Vec3, observer_dir: Vec3) -> f32 {
357        let v = velocity.length() as f64;
358        if v < 1e-12 {
359            return 1.0;
360        }
361        let gamma = lorentz_factor(v, self.c);
362        let beta = v / self.c;
363        let dir = velocity.normalize();
364        let cos_theta = dir.dot(observer_dir.normalize()) as f64;
365        let doppler = gamma * (1.0 - beta * cos_theta);
366        if doppler > 1e-10 {
367            (1.0 / doppler).powi(3) as f32
368        } else {
369            1.0
370        }
371    }
372
373    /// Apply full Lorentz rendering transform to a set of glyph data.
374    /// Returns (contracted_positions, brightness_factors).
375    pub fn transform_entity(
376        &self,
377        positions: &[Vec3],
378        center: Vec3,
379        velocity: Vec3,
380        observer_pos: Vec3,
381    ) -> (Vec<Vec3>, Vec<f32>) {
382        let new_positions = self.render_glyphs(positions, center, velocity);
383        let observer_dir = (observer_pos - center).normalize_or_zero();
384        let brightness = positions.iter().map(|_| self.brightness_factor(velocity, observer_dir)).collect();
385        (new_positions, brightness)
386    }
387}
388
389/// Compute the kinetic energy: KE = (gamma - 1) * m * c^2.
390pub fn kinetic_energy(mass: f64, v: f64, c: f64) -> f64 {
391    (lorentz_factor(v, c) - 1.0) * mass * c * c
392}
393
394/// Convert between rapidity and velocity.
395pub fn velocity_from_rapidity(phi: f64, c: f64) -> f64 {
396    c * phi.tanh()
397}
398
399/// Compose two boosts by adding rapidities (for collinear boosts).
400pub fn compose_collinear_boosts(v1: f64, v2: f64, c: f64) -> f64 {
401    let phi1 = rapidity(v1, c);
402    let phi2 = rapidity(v2, c);
403    velocity_from_rapidity(phi1 + phi2, c)
404}
405
406/// Relativistic Doppler factor for radial motion.
407pub fn doppler_factor(v: f64, c: f64, approaching: bool) -> f64 {
408    let beta = v / c;
409    if approaching {
410        ((1.0 + beta) / (1.0 - beta)).sqrt()
411    } else {
412        ((1.0 - beta) / (1.0 + beta)).sqrt()
413    }
414}
415
416/// Four-velocity from 3-velocity.
417pub fn four_velocity(velocity: Vec3, c: f64) -> FourVector {
418    let vx = velocity.x as f64;
419    let vy = velocity.y as f64;
420    let vz = velocity.z as f64;
421    let v_mag = (vx * vx + vy * vy + vz * vz).sqrt();
422    let gamma = lorentz_factor(v_mag, c);
423    FourVector::new(gamma * c, gamma * vx, gamma * vy, gamma * vz)
424}
425
426/// Check if a four-vector is timelike (norm_sq > 0).
427pub fn is_timelike(fv: &FourVector) -> bool {
428    fv.norm_sq() > 0.0
429}
430
431/// Check if a four-vector is spacelike (norm_sq < 0).
432pub fn is_spacelike(fv: &FourVector) -> bool {
433    fv.norm_sq() < 0.0
434}
435
436/// Check if a four-vector is lightlike/null (norm_sq ~ 0).
437pub fn is_lightlike(fv: &FourVector, tolerance: f64) -> bool {
438    fv.norm_sq().abs() < tolerance
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    const C: f64 = 299_792_458.0; // speed of light in m/s
446
447    #[test]
448    fn test_lorentz_factor_zero() {
449        let g = lorentz_factor(0.0, C);
450        assert!((g - 1.0).abs() < 1e-10);
451    }
452
453    #[test]
454    fn test_lorentz_factor_high_v() {
455        let g = lorentz_factor(0.99 * C, C);
456        let expected = 1.0 / (1.0 - 0.99 * 0.99_f64).sqrt();
457        assert!((g - expected).abs() < 1e-6);
458    }
459
460    #[test]
461    fn test_lorentz_factor_approaches_infinity() {
462        let g = lorentz_factor(0.9999999 * C, C);
463        assert!(g > 1000.0);
464        let g2 = lorentz_factor(C, C);
465        assert!(g2.is_infinite());
466    }
467
468    #[test]
469    fn test_four_vector_minkowski_dot() {
470        let a = FourVector::new(5.0, 1.0, 2.0, 3.0);
471        let b = FourVector::new(3.0, 1.0, 1.0, 1.0);
472        // dot = 5*3 - 1*1 - 2*1 - 3*1 = 15 - 6 = 9
473        assert!((a.dot(&b) - 9.0).abs() < 1e-10);
474    }
475
476    #[test]
477    fn test_four_vector_norm_sq() {
478        let v = FourVector::new(5.0, 3.0, 0.0, 0.0);
479        // norm_sq = 25 - 9 = 16
480        assert!((v.norm_sq() - 16.0).abs() < 1e-10);
481    }
482
483    #[test]
484    fn test_boost_zero_velocity() {
485        let fv = FourVector::new(1.0, 2.0, 3.0, 4.0);
486        let boosted = boost(&fv, Vec3::ZERO, C);
487        assert!((boosted.t - fv.t).abs() < 1e-10);
488        assert!((boosted.x - fv.x).abs() < 1e-10);
489    }
490
491    #[test]
492    fn test_boost_preserves_interval() {
493        let fv = FourVector::new(10.0, 1.0, 2.0, 0.0);
494        let original_interval = fv.norm_sq();
495        let boosted = boost(&fv, Vec3::new(0.5 * C as f32, 0.0, 0.0), C);
496        let boosted_interval = boosted.norm_sq();
497        assert!(
498            (original_interval - boosted_interval).abs() < 1e-3,
499            "Interval not preserved: {} vs {}",
500            original_interval,
501            boosted_interval
502        );
503    }
504
505    #[test]
506    fn test_contract_length() {
507        let L0 = 10.0;
508        let L = contract_length(L0, 0.0, C);
509        assert!((L - 10.0).abs() < 1e-10);
510
511        let L2 = contract_length(L0, 0.866 * C, C);
512        // gamma ~ 2, so L ~ 5
513        assert!((L2 - 5.0).abs() < 0.1);
514    }
515
516    #[test]
517    fn test_velocity_addition_subluminal() {
518        let w = velocity_addition(0.5 * C, 0.5 * C, C);
519        assert!(w < C);
520        let expected = (0.5 * C + 0.5 * C) / (1.0 + 0.25);
521        assert!((w - expected).abs() < 1e-6);
522    }
523
524    #[test]
525    fn test_velocity_addition_light_speed() {
526        // Adding c to anything should give c
527        let w = velocity_addition(C, 0.5 * C, C);
528        assert!((w - C).abs() < 1e-6);
529    }
530
531    #[test]
532    fn test_rapidity() {
533        let phi = rapidity(0.0, C);
534        assert!(phi.abs() < 1e-10);
535
536        let phi2 = rapidity(0.5 * C, C);
537        assert!((phi2 - (0.5_f64).atanh()).abs() < 1e-10);
538    }
539
540    #[test]
541    fn test_energy_momentum_relation() {
542        // E^2 = p^2 c^2 + m^2 c^4
543        let mass = 1.0;
544        let v = 0.8 * C;
545        let E = relativistic_energy(mass, v, C);
546        let p = relativistic_momentum(mass, v, C);
547        let lhs = E * E;
548        let rhs = p * p * C * C + mass * mass * C * C * C * C;
549        assert!(
550            (lhs - rhs).abs() / lhs < 1e-10,
551            "E^2 = p^2 c^2 + m^2 c^4 failed: {} vs {}",
552            lhs, rhs
553        );
554    }
555
556    #[test]
557    fn test_invariant_mass_recovery() {
558        let mass = 2.5;
559        let v = 0.6 * C;
560        let E = relativistic_energy(mass, v, C);
561        let p = relativistic_momentum(mass, v, C);
562        let m_recovered = invariant_mass(E, p, C);
563        assert!((m_recovered - mass).abs() < 1e-6);
564    }
565
566    #[test]
567    fn test_four_momentum_norm() {
568        let mass = 1.0;
569        let vel = Vec3::new(0.3 * C as f32, 0.4 * C as f32, 0.0);
570        let pm = four_momentum(mass, vel, C);
571        // p^mu p_mu = m^2 c^2 (with our convention p0 = gamma*m*c)
572        let norm = pm.norm_sq();
573        let expected = mass * mass * C * C;
574        assert!(
575            (norm - expected).abs() / expected < 1e-6,
576            "Four-momentum norm: {} vs {}",
577            norm, expected
578        );
579    }
580
581    #[test]
582    fn test_proper_time() {
583        let t = 10.0;
584        let tau = proper_time(t, 0.866 * C, C);
585        // gamma ~ 2, tau ~ 5
586        assert!((tau - 5.0).abs() < 0.1);
587    }
588
589    #[test]
590    fn test_lorentz_contraction_renderer() {
591        let contactor = LorentzContractor::new(C);
592        let scale = contactor.contracted_scale(Vec3::new(0.866 * C as f32, 0.0, 0.0));
593        // gamma ~ 2, contraction along x ~ 0.5
594        assert!((scale.x - 0.5).abs() < 0.05);
595        assert!((scale.y - 1.0).abs() < 0.01);
596        assert!((scale.z - 1.0).abs() < 0.01);
597    }
598
599    #[test]
600    fn test_compose_collinear_boosts() {
601        let w = compose_collinear_boosts(0.5 * C, 0.5 * C, C);
602        let w2 = velocity_addition(0.5 * C, 0.5 * C, C);
603        assert!((w - w2).abs() < 1e-6);
604    }
605
606    #[test]
607    fn test_kinetic_energy_low_v() {
608        // At low velocities, KE ~ 0.5 * m * v^2
609        let mass = 1.0;
610        let v = 0.001 * C;
611        let ke_rel = kinetic_energy(mass, v, C);
612        let ke_classical = 0.5 * mass * v * v;
613        assert!(
614            (ke_rel - ke_classical).abs() / ke_classical < 0.01,
615            "Low v KE: rel={} classical={}",
616            ke_rel, ke_classical
617        );
618    }
619
620    #[test]
621    fn test_four_velocity_norm() {
622        let vel = Vec3::new(0.5 * C as f32, 0.0, 0.0);
623        let u = four_velocity(vel, C);
624        // u^mu u_mu = c^2
625        let norm = u.norm_sq();
626        assert!(
627            (norm - C * C).abs() / (C * C) < 1e-6,
628            "Four-velocity norm: {} vs {}",
629            norm, C * C
630        );
631    }
632}