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