Skip to main content

proof_engine/relativistic/
spacetime.rs

1//! Spacetime diagrams: Minkowski, Penrose, worldlines, causal structure.
2
3use glam::{Vec2, Vec3, Vec4};
4use super::lorentz;
5
6/// An event in spacetime.
7#[derive(Debug, Clone)]
8pub struct SpacetimeEvent {
9    pub t: f64,
10    pub x: f64,
11    pub label: String,
12}
13
14impl SpacetimeEvent {
15    pub fn new(t: f64, x: f64, label: &str) -> Self {
16        Self { t, x, label: label.to_string() }
17    }
18
19    /// Spacetime interval squared between two events: ds^2 = c^2 dt^2 - dx^2.
20    pub fn interval_sq(&self, other: &SpacetimeEvent, c: f64) -> f64 {
21        let dt = self.t - other.t;
22        let dx = self.x - other.x;
23        c * c * dt * dt - dx * dx
24    }
25
26    /// As a 2D point (x, t) for plotting.
27    pub fn as_point(&self) -> Vec2 {
28        Vec2::new(self.x as f32, self.t as f32)
29    }
30}
31
32/// A worldline: a sequence of spacetime events.
33#[derive(Debug, Clone)]
34pub struct Worldline {
35    pub events: Vec<(f64, f64)>, // (t, x)
36    pub color: Vec4,
37    pub label: String,
38}
39
40impl Worldline {
41    pub fn new(label: &str, color: Vec4) -> Self {
42        Self {
43            events: Vec::new(),
44            color,
45            label: label.to_string(),
46        }
47    }
48
49    pub fn add_event(&mut self, t: f64, x: f64) {
50        self.events.push((t, x));
51    }
52
53    /// Create a worldline for an object at rest at position x.
54    pub fn at_rest(x: f64, t_range: (f64, f64), steps: usize, label: &str) -> Self {
55        let mut wl = Self::new(label, Vec4::new(1.0, 1.0, 1.0, 1.0));
56        let dt = (t_range.1 - t_range.0) / steps.max(1) as f64;
57        for i in 0..=steps {
58            wl.add_event(t_range.0 + dt * i as f64, x);
59        }
60        wl
61    }
62
63    /// Create a worldline for constant velocity motion.
64    pub fn constant_velocity(x0: f64, v: f64, t_range: (f64, f64), steps: usize, label: &str) -> Self {
65        let mut wl = Self::new(label, Vec4::new(0.5, 1.0, 0.5, 1.0));
66        let dt = (t_range.1 - t_range.0) / steps.max(1) as f64;
67        for i in 0..=steps {
68            let t = t_range.0 + dt * i as f64;
69            wl.add_event(t, x0 + v * t);
70        }
71        wl
72    }
73
74    /// Get the worldline as plotting points.
75    pub fn as_points(&self) -> Vec<Vec2> {
76        self.events.iter().map(|(t, x)| Vec2::new(*x as f32, *t as f32)).collect()
77    }
78
79    /// Velocity at a given segment index.
80    pub fn velocity_at(&self, index: usize) -> f64 {
81        if index + 1 >= self.events.len() {
82            return 0.0;
83        }
84        let (t1, x1) = self.events[index];
85        let (t2, x2) = self.events[index + 1];
86        let dt = t2 - t1;
87        if dt.abs() < 1e-15 { return 0.0; }
88        (x2 - x1) / dt
89    }
90}
91
92/// Minkowski diagram containing events and worldlines.
93#[derive(Debug, Clone)]
94pub struct MinkowskiDiagram {
95    pub events: Vec<SpacetimeEvent>,
96    pub worldlines: Vec<Worldline>,
97}
98
99impl MinkowskiDiagram {
100    pub fn new() -> Self {
101        Self {
102            events: Vec::new(),
103            worldlines: Vec::new(),
104        }
105    }
106
107    pub fn add_event(&mut self, event: SpacetimeEvent) {
108        self.events.push(event);
109    }
110
111    pub fn add_worldline(&mut self, wl: Worldline) {
112        self.worldlines.push(wl);
113    }
114
115    /// Get all event positions as plotting points.
116    pub fn event_points(&self) -> Vec<Vec2> {
117        self.events.iter().map(|e| e.as_point()).collect()
118    }
119}
120
121/// Compute the light cone from an event.
122/// Returns (future_cone, past_cone) as line segments in (x, t) space.
123pub fn light_cone(
124    event: &SpacetimeEvent,
125    c: f64,
126) -> (Vec<(f64, f64)>, Vec<(f64, f64)>) {
127    let extent = 10.0; // how far to draw the cone
128    let steps = 50;
129    let dt = extent / steps as f64;
130
131    let mut future = Vec::with_capacity(steps * 2);
132    let mut past = Vec::with_capacity(steps * 2);
133
134    for i in 0..=steps {
135        let t_off = dt * i as f64;
136        // Future: rightward and leftward at speed c
137        future.push((event.x + c * t_off, event.t + t_off));
138        future.push((event.x - c * t_off, event.t + t_off));
139        // Past:
140        past.push((event.x + c * t_off, event.t - t_off));
141        past.push((event.x - c * t_off, event.t - t_off));
142    }
143
144    (future, past)
145}
146
147/// Check if two events are timelike separated (ds^2 > 0).
148pub fn is_timelike(event_a: &SpacetimeEvent, event_b: &SpacetimeEvent, c: f64) -> bool {
149    event_a.interval_sq(event_b, c) > 0.0
150}
151
152/// Check if two events are spacelike separated (ds^2 < 0).
153pub fn is_spacelike(event_a: &SpacetimeEvent, event_b: &SpacetimeEvent, c: f64) -> bool {
154    event_a.interval_sq(event_b, c) < 0.0
155}
156
157/// Check if two events are lightlike (null) separated (ds^2 ~ 0).
158pub fn is_lightlike(event_a: &SpacetimeEvent, event_b: &SpacetimeEvent, c: f64) -> bool {
159    event_a.interval_sq(event_b, c).abs() < 1e-10
160}
161
162/// Compute proper time along a worldline.
163/// tau = integral of sqrt(c^2 dt^2 - dx^2) / c.
164pub fn proper_time_along_worldline(worldline: &Worldline, c: f64) -> f64 {
165    let mut tau = 0.0;
166    for i in 1..worldline.events.len() {
167        let (t1, x1) = worldline.events[i - 1];
168        let (t2, x2) = worldline.events[i];
169        let dt = t2 - t1;
170        let dx = x2 - x1;
171        let ds2 = c * c * dt * dt - dx * dx;
172        if ds2 > 0.0 {
173            tau += ds2.sqrt() / c;
174        }
175    }
176    tau
177}
178
179/// Lorentz-transform all events in a diagram.
180pub fn boost_diagram(diagram: &MinkowskiDiagram, velocity: f64, c: f64) -> MinkowskiDiagram {
181    let gamma = lorentz::lorentz_factor(velocity, c);
182    let beta = velocity / c;
183
184    let transform = |t: f64, x: f64| -> (f64, f64) {
185        let t_new = gamma * (t - beta * x / c);
186        let x_new = gamma * (x - velocity * t);
187        (t_new, x_new)
188    };
189
190    let mut new_diagram = MinkowskiDiagram::new();
191
192    for event in &diagram.events {
193        let (t_new, x_new) = transform(event.t, event.x);
194        new_diagram.add_event(SpacetimeEvent::new(t_new, x_new, &event.label));
195    }
196
197    for wl in &diagram.worldlines {
198        let mut new_wl = Worldline::new(&wl.label, wl.color);
199        for &(t, x) in &wl.events {
200            let (t_new, x_new) = transform(t, x);
201            new_wl.add_event(t_new, x_new);
202        }
203        new_diagram.add_worldline(new_wl);
204    }
205
206    new_diagram
207}
208
209/// Penrose diagram: conformal compactification of spacetime.
210#[derive(Debug, Clone)]
211pub struct PenroseDiagram {
212    pub events: Vec<(f64, f64)>, // (T, X) in Penrose coordinates
213    pub worldlines: Vec<Vec<(f64, f64)>>,
214}
215
216impl PenroseDiagram {
217    pub fn new() -> Self {
218        Self {
219            events: Vec::new(),
220            worldlines: Vec::new(),
221        }
222    }
223
224    /// Add an event in Minkowski coordinates; automatically transforms to Penrose.
225    pub fn add_event(&mut self, t: f64, r: f64) {
226        let (pt, px) = penrose_transform(t, r);
227        self.events.push((pt, px));
228    }
229
230    /// Add a worldline in Minkowski coordinates.
231    pub fn add_worldline(&mut self, points: &[(f64, f64)]) {
232        let transformed: Vec<(f64, f64)> = points.iter()
233            .map(|&(t, r)| penrose_transform(t, r))
234            .collect();
235        self.worldlines.push(transformed);
236    }
237
238    /// Get the boundary of the Penrose diagram (diamond shape).
239    pub fn boundary(&self, n_points: usize) -> Vec<(f64, f64)> {
240        let pi = std::f64::consts::PI;
241        let mut boundary = Vec::new();
242        let dp = pi / n_points as f64;
243
244        // Right boundary: future null infinity to i+
245        for i in 0..=n_points {
246            let T = -pi / 2.0 + dp * i as f64;
247            let X = pi / 2.0 - T.abs();
248            boundary.push((T, X));
249        }
250        // Left boundary
251        for i in (0..=n_points).rev() {
252            let T = -pi / 2.0 + dp * i as f64;
253            let X = -(pi / 2.0 - T.abs());
254            boundary.push((T, X));
255        }
256
257        boundary
258    }
259}
260
261/// Transform Minkowski coordinates (t, r) to Penrose coordinates.
262/// Uses the conformal compactification:
263///   T = arctan(t + r) + arctan(t - r)
264///   X = arctan(t + r) - arctan(t - r)
265pub fn penrose_transform(t: f64, r: f64) -> (f64, f64) {
266    let u = (t + r).atan();
267    let v = (t - r).atan();
268    let big_t = u + v;
269    let big_x = u - v;
270    (big_t, big_x)
271}
272
273/// Spacetime renderer for Minkowski diagrams.
274#[derive(Debug, Clone)]
275pub struct SpacetimeRenderer {
276    pub c: f64,
277    pub show_light_cones: bool,
278    pub show_simultaneity: bool,
279    pub x_range: (f32, f32),
280    pub t_range: (f32, f32),
281    pub grid_color: Vec4,
282    pub light_cone_color: Vec4,
283}
284
285impl SpacetimeRenderer {
286    pub fn new(c: f64) -> Self {
287        Self {
288            c,
289            show_light_cones: true,
290            show_simultaneity: true,
291            x_range: (-10.0, 10.0),
292            t_range: (-10.0, 10.0),
293            grid_color: Vec4::new(0.2, 0.2, 0.3, 1.0),
294            light_cone_color: Vec4::new(1.0, 1.0, 0.0, 0.5),
295        }
296    }
297
298    /// Render the diagram: returns lines and points.
299    pub fn render_diagram(
300        &self,
301        diagram: &MinkowskiDiagram,
302    ) -> (Vec<(Vec2, Vec2, Vec4)>, Vec<(Vec2, Vec4)>) {
303        let mut lines = Vec::new();
304        let mut points = Vec::new();
305
306        // Event points
307        for event in &diagram.events {
308            points.push((event.as_point(), Vec4::new(1.0, 0.3, 0.3, 1.0)));
309
310            // Light cones
311            if self.show_light_cones {
312                let p = event.as_point();
313                let extent = 5.0;
314                let c = self.c as f32;
315                // Future
316                lines.push((p, p + Vec2::new(extent, extent / c), self.light_cone_color));
317                lines.push((p, p + Vec2::new(-extent, extent / c), self.light_cone_color));
318                // Past
319                lines.push((p, p + Vec2::new(extent, -extent / c), self.light_cone_color));
320                lines.push((p, p + Vec2::new(-extent, -extent / c), self.light_cone_color));
321            }
322        }
323
324        // Worldlines
325        for wl in &diagram.worldlines {
326            let pts = wl.as_points();
327            for i in 1..pts.len() {
328                lines.push((pts[i - 1], pts[i], wl.color));
329            }
330        }
331
332        (lines, points)
333    }
334
335    /// Render simultaneity surfaces for a boosted observer.
336    pub fn simultaneity_lines(
337        &self,
338        velocity: f64,
339        n_lines: usize,
340    ) -> Vec<(Vec2, Vec2)> {
341        let beta = velocity / self.c;
342        let mut lines = Vec::new();
343        let dt = (self.t_range.1 - self.t_range.0) / n_lines as f32;
344
345        for i in 0..n_lines {
346            let t0 = self.t_range.0 + dt * i as f32;
347            // Simultaneity surface has slope beta (in x-t diagram)
348            let x1 = self.x_range.0;
349            let x2 = self.x_range.1;
350            let t1 = t0 + beta as f32 * x1;
351            let t2 = t0 + beta as f32 * x2;
352            lines.push((Vec2::new(x1, t1), Vec2::new(x2, t2)));
353        }
354        lines
355    }
356}
357
358/// Compute the causal structure matrix.
359/// Returns a matrix where entry [i][j] is true if event i can causally influence event j.
360pub fn causal_structure(events: &[SpacetimeEvent], c: f64) -> Vec<Vec<bool>> {
361    let n = events.len();
362    let mut matrix = vec![vec![false; n]; n];
363
364    for i in 0..n {
365        for j in 0..n {
366            if i == j {
367                matrix[i][j] = true;
368                continue;
369            }
370            let dt = events[j].t - events[i].t;
371            if dt < 0.0 {
372                continue; // j is in the past of i, so i cannot influence j
373            }
374            let dx = (events[j].x - events[i].x).abs();
375            // Causal if the separation is timelike or lightlike (and j is in i's future)
376            if c * dt >= dx {
377                matrix[i][j] = true;
378            }
379        }
380    }
381
382    matrix
383}
384
385/// Compute the invariant interval between two events.
386pub fn invariant_interval(a: &SpacetimeEvent, b: &SpacetimeEvent, c: f64) -> f64 {
387    a.interval_sq(b, c)
388}
389
390/// Generate a grid of light cones for visualization.
391pub fn light_cone_grid(
392    x_range: (f64, f64),
393    t_range: (f64, f64),
394    c: f64,
395    nx: usize,
396    nt: usize,
397) -> Vec<(SpacetimeEvent, Vec<(f64, f64)>, Vec<(f64, f64)>)> {
398    let dx = (x_range.1 - x_range.0) / nx.max(1) as f64;
399    let dt = (t_range.1 - t_range.0) / nt.max(1) as f64;
400    let mut grid = Vec::new();
401
402    for it in 0..=nt {
403        for ix in 0..=nx {
404            let x = x_range.0 + dx * ix as f64;
405            let t = t_range.0 + dt * it as f64;
406            let event = SpacetimeEvent::new(t, x, "");
407            let (future, past) = light_cone(&event, c);
408            grid.push((event, future, past));
409        }
410    }
411    grid
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    const C: f64 = 1.0; // use natural units for simplicity
419
420    #[test]
421    fn test_timelike_separation() {
422        let a = SpacetimeEvent::new(0.0, 0.0, "A");
423        let b = SpacetimeEvent::new(2.0, 1.0, "B");
424        // ds^2 = 4 - 1 = 3 > 0
425        assert!(is_timelike(&a, &b, C));
426        assert!(!is_spacelike(&a, &b, C));
427    }
428
429    #[test]
430    fn test_spacelike_separation() {
431        let a = SpacetimeEvent::new(0.0, 0.0, "A");
432        let b = SpacetimeEvent::new(1.0, 3.0, "B");
433        // ds^2 = 1 - 9 = -8 < 0
434        assert!(is_spacelike(&a, &b, C));
435        assert!(!is_timelike(&a, &b, C));
436    }
437
438    #[test]
439    fn test_lightlike_separation() {
440        let a = SpacetimeEvent::new(0.0, 0.0, "A");
441        let b = SpacetimeEvent::new(5.0, 5.0, "B");
442        // ds^2 = 25 - 25 = 0
443        assert!(is_lightlike(&a, &b, C));
444    }
445
446    #[test]
447    fn test_light_cone_structure() {
448        let event = SpacetimeEvent::new(0.0, 0.0, "O");
449        let (future, past) = light_cone(&event, C);
450        assert!(!future.is_empty());
451        assert!(!past.is_empty());
452        // First future point should be at origin
453        assert!((future[0].0).abs() < 1e-10);
454        assert!((future[0].1).abs() < 1e-10);
455    }
456
457    #[test]
458    fn test_proper_time_at_rest() {
459        let wl = Worldline::at_rest(0.0, (0.0, 10.0), 100, "rest");
460        let tau = proper_time_along_worldline(&wl, C);
461        // At rest, proper time = coordinate time
462        assert!((tau - 10.0).abs() < 0.1, "Proper time at rest: {}", tau);
463    }
464
465    #[test]
466    fn test_proper_time_moving() {
467        let v = 0.866; // gamma ~ 2
468        let wl = Worldline::constant_velocity(0.0, v, (0.0, 10.0), 1000, "moving");
469        let tau = proper_time_along_worldline(&wl, C);
470        // tau = t / gamma ~ 10 / 2 = 5
471        assert!((tau - 5.0).abs() < 0.1, "Moving proper time: {}", tau);
472    }
473
474    #[test]
475    fn test_boost_diagram_preserves_interval() {
476        let mut diagram = MinkowskiDiagram::new();
477        let a = SpacetimeEvent::new(0.0, 0.0, "A");
478        let b = SpacetimeEvent::new(3.0, 1.0, "B");
479        let interval_before = a.interval_sq(&b, C);
480        diagram.add_event(a);
481        diagram.add_event(b);
482
483        let boosted = boost_diagram(&diagram, 0.5, C);
484        let interval_after = boosted.events[0].interval_sq(&boosted.events[1], C);
485
486        assert!(
487            (interval_before - interval_after).abs() < 1e-6,
488            "Interval not preserved: {} vs {}",
489            interval_before, interval_after
490        );
491    }
492
493    #[test]
494    fn test_causal_structure() {
495        let events = vec![
496            SpacetimeEvent::new(0.0, 0.0, "A"),
497            SpacetimeEvent::new(1.0, 0.5, "B"), // timelike future of A
498            SpacetimeEvent::new(0.5, 3.0, "C"), // spacelike from A
499        ];
500        let matrix = causal_structure(&events, C);
501
502        // A can influence B (timelike, B in future)
503        assert!(matrix[0][1], "A should causally influence B");
504        // A cannot influence C (spacelike)
505        assert!(!matrix[0][2], "A should not influence C (spacelike)");
506        // Self-influence
507        assert!(matrix[0][0]);
508        assert!(matrix[1][1]);
509    }
510
511    #[test]
512    fn test_causal_structure_no_backward() {
513        let events = vec![
514            SpacetimeEvent::new(5.0, 0.0, "A"),
515            SpacetimeEvent::new(0.0, 0.0, "B"), // B is in the past of A
516        ];
517        let matrix = causal_structure(&events, C);
518        // A cannot influence B (B is in the past)
519        assert!(!matrix[0][1]);
520        // B can influence A
521        assert!(matrix[1][0]);
522    }
523
524    #[test]
525    fn test_penrose_transform_origin() {
526        let (t, x) = penrose_transform(0.0, 0.0);
527        assert!(t.abs() < 1e-10);
528        assert!(x.abs() < 1e-10);
529    }
530
531    #[test]
532    fn test_penrose_transform_finite() {
533        // Even large coordinates map to finite values
534        let (t, x) = penrose_transform(1e10, 0.0);
535        assert!(t.is_finite());
536        assert!(x.is_finite());
537        assert!(t.abs() < std::f64::consts::PI);
538    }
539
540    #[test]
541    fn test_invariant_interval() {
542        let a = SpacetimeEvent::new(0.0, 0.0, "A");
543        let b = SpacetimeEvent::new(3.0, 4.0, "B");
544        let ds2 = invariant_interval(&a, &b, C);
545        // c=1: ds^2 = 9 - 16 = -7
546        assert!((ds2 - (-7.0)).abs() < 1e-10);
547    }
548
549    #[test]
550    fn test_spacetime_renderer() {
551        let renderer = SpacetimeRenderer::new(C);
552        let mut diagram = MinkowskiDiagram::new();
553        diagram.add_event(SpacetimeEvent::new(0.0, 0.0, "O"));
554        diagram.add_worldline(Worldline::at_rest(0.0, (-5.0, 5.0), 10, "static"));
555
556        let (lines, points) = renderer.render_diagram(&diagram);
557        assert!(!points.is_empty());
558        assert!(!lines.is_empty()); // light cone lines
559    }
560
561    #[test]
562    fn test_penrose_diagram() {
563        let mut pd = PenroseDiagram::new();
564        pd.add_event(0.0, 0.0);
565        pd.add_event(5.0, 3.0);
566        assert_eq!(pd.events.len(), 2);
567
568        let boundary = pd.boundary(10);
569        assert!(!boundary.is_empty());
570    }
571
572    #[test]
573    fn test_worldline_velocity() {
574        let wl = Worldline::constant_velocity(0.0, 0.5, (0.0, 10.0), 100, "v=0.5");
575        let v = wl.velocity_at(50);
576        assert!((v - 0.5).abs() < 0.01, "Velocity: {}", v);
577    }
578}