holonomy-lib 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. holonomy_explorer/__init__.py +1 -0
  2. holonomy_explorer/__main__.py +79 -0
  3. holonomy_explorer/batching.py +182 -0
  4. holonomy_explorer/common.py +187 -0
  5. holonomy_explorer/curvature.py +154 -0
  6. holonomy_explorer/datasets.py +346 -0
  7. holonomy_explorer/graphs.py +286 -0
  8. holonomy_explorer/server.py +172 -0
  9. holonomy_explorer/static/app.js +435 -0
  10. holonomy_explorer/static/charts.js +114 -0
  11. holonomy_explorer/static/index.html +45 -0
  12. holonomy_explorer/static/layout.js +85 -0
  13. holonomy_explorer/static/style.css +121 -0
  14. holonomy_explorer/static/view.js +195 -0
  15. holonomy_explorer/topology.py +118 -0
  16. holonomy_lib/__init__.py +9 -0
  17. holonomy_lib/_graph_utils.py +52 -0
  18. holonomy_lib/_quadrature.py +74 -0
  19. holonomy_lib/algebra/__init__.py +32 -0
  20. holonomy_lib/algebra/eigsh_sparse.py +251 -0
  21. holonomy_lib/algebra/lanczos.py +361 -0
  22. holonomy_lib/algebra/linear.py +180 -0
  23. holonomy_lib/algebra/solvers.py +547 -0
  24. holonomy_lib/convex/__init__.py +27 -0
  25. holonomy_lib/convex/least_squares.py +184 -0
  26. holonomy_lib/discrete_geometry/__init__.py +38 -0
  27. holonomy_lib/discrete_geometry/forman.py +242 -0
  28. holonomy_lib/discrete_geometry/ricci.py +855 -0
  29. holonomy_lib/experimental/__init__.py +13 -0
  30. holonomy_lib/experimental/heterogeneous_kappa.py +361 -0
  31. holonomy_lib/experimental/laplacian_eigenmaps.py +200 -0
  32. holonomy_lib/hyperbolic/__init__.py +27 -0
  33. holonomy_lib/hyperbolic/frechet_mean.py +381 -0
  34. holonomy_lib/hyperbolic/heat_kernel.py +608 -0
  35. holonomy_lib/hyperbolic/manifold_inner.py +81 -0
  36. holonomy_lib/info_geometry/__init__.py +64 -0
  37. holonomy_lib/info_geometry/divergences.py +224 -0
  38. holonomy_lib/info_geometry/fisher.py +179 -0
  39. holonomy_lib/lie/__init__.py +54 -0
  40. holonomy_lib/lie/so3.py +385 -0
  41. holonomy_lib/lie/spherical_harmonics.py +189 -0
  42. holonomy_lib/manifolds/__init__.py +53 -0
  43. holonomy_lib/manifolds/comparison.py +343 -0
  44. holonomy_lib/manifolds/fixed_rank.py +453 -0
  45. holonomy_lib/manifolds/lorentz.py +699 -0
  46. holonomy_lib/manifolds/lorentzian.py +567 -0
  47. holonomy_lib/manifolds/product.py +425 -0
  48. holonomy_lib/manifolds/spd.py +504 -0
  49. holonomy_lib/manifolds/stereographic.py +680 -0
  50. holonomy_lib/optimization/__init__.py +42 -0
  51. holonomy_lib/optimization/base.py +65 -0
  52. holonomy_lib/optimization/sgd.py +112 -0
  53. holonomy_lib/py.typed +0 -0
  54. holonomy_lib/sheaf/__init__.py +74 -0
  55. holonomy_lib/sheaf/graph_sheaf.py +352 -0
  56. holonomy_lib/sheaf/laplacian.py +529 -0
  57. holonomy_lib/simplicial/__init__.py +55 -0
  58. holonomy_lib/simplicial/_face_lookup.py +99 -0
  59. holonomy_lib/simplicial/complex.py +168 -0
  60. holonomy_lib/simplicial/sparse_complex.py +145 -0
  61. holonomy_lib/simplicial/vietoris_rips.py +277 -0
  62. holonomy_lib/spectral/__init__.py +55 -0
  63. holonomy_lib/spectral/diffusion_map.py +169 -0
  64. holonomy_lib/spectral/effective_resistance.py +199 -0
  65. holonomy_lib/spectral/embedding.py +131 -0
  66. holonomy_lib/spectral/heat_kernel.py +236 -0
  67. holonomy_lib/spectral/laplacian.py +441 -0
  68. holonomy_lib/spectral/magnetic.py +351 -0
  69. holonomy_lib/spectral/spectral_dimension.py +104 -0
  70. holonomy_lib/tensor_calculus/__init__.py +23 -0
  71. holonomy_lib/tensor_calculus/decomposition.py +208 -0
  72. holonomy_lib/topology/__init__.py +28 -0
  73. holonomy_lib/topology/_filtration.py +147 -0
  74. holonomy_lib/topology/_reduction.py +227 -0
  75. holonomy_lib/topology/hodge.py +250 -0
  76. holonomy_lib/topology/persistent_homology.py +301 -0
  77. holonomy_lib-0.8.0.dist-info/METADATA +161 -0
  78. holonomy_lib-0.8.0.dist-info/RECORD +82 -0
  79. holonomy_lib-0.8.0.dist-info/WHEEL +5 -0
  80. holonomy_lib-0.8.0.dist-info/entry_points.txt +2 -0
  81. holonomy_lib-0.8.0.dist-info/licenses/LICENSE.md +105 -0
  82. holonomy_lib-0.8.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,567 @@
1
+ # SPDX-License-Identifier: FSL-1.1-ALv2
2
+ # Copyright 2026 Synoros
3
+
4
+ """Lorentzian (pseudo-Riemannian, signature (1, n-1)) manifold,
5
+ flat Minkowski spacetime `R^{1, n-1}`.
6
+
7
+ Distinct from `LorentzManifold` (which is the *unit hyperboloid*, a
8
+ Riemannian submanifold of a Minkowski space one dimension larger). `LorentzianManifold`
9
+ **is** the ambient Minkowski space:
10
+
11
+ - The manifold itself is `R^n` (no constraint surface).
12
+ - The metric is `g(u, v) = ⟨u, v⟩_M = -u_0·v_0 + Σ_{i ≥ 1} u_i·v_i`,
13
+ **indefinite signature (1, n-1)**: one timelike direction, `n - 1`
14
+ spacelike directions.
15
+ - Tangent space at any point is the full `R^n`; tangent vectors carry
16
+ a signed norm-squared `⟨v, v⟩_M ∈ R` (negative = timelike,
17
+ zero = null/lightlike, positive = spacelike).
18
+ - Geodesics are straight lines (the manifold is flat); `exp_x(v) = x + v`
19
+ and `log_x(y) = y - x`, regardless of causal type.
20
+
21
+ The interesting structure isn't the geodesics (trivial) but the **causal
22
+ classification**: which point pairs are timelike-separated, lightlike-
23
+ separated, or spacelike-separated. This is the substrate-as-spacetime
24
+ primitive; Lorentzian-geometry-aware operations that respect the
25
+ sign-indefinite metric.
26
+
27
+ API summary (compared to a Riemannian manifold):
28
+
29
+ - `minkowski_inner(u, v)`: SIGNED inner product; can be any real
30
+ number (vs `LorentzManifold.minkowski_inner` which is the same
31
+ function used purely as a helper, and `LorentzManifold.inner`
32
+ which is the *induced* positive-definite tangent-space metric).
33
+ - `norm_sq(v)`: signed `⟨v, v⟩_M`. **There is no `norm`**; the
34
+ square root would be complex for timelike vectors.
35
+ - `interval_sq(x, y) := ⟨y - x, y - x⟩_M`: signed squared spacetime
36
+ interval. Negative for timelike-separated pairs, zero for null,
37
+ positive for spacelike.
38
+ - `causal_type(x, y) -> int`: integer label per pair:
39
+ 0 = spacelike, 1 = future-timelike, -1 = past-timelike,
40
+ 2 = future-null, -2 = past-null.
41
+ - `proper_time(x, y)`: `sqrt(-interval_sq)` for timelike pairs only;
42
+ returns NaN element-wise for non-timelike pairs.
43
+ - `proper_distance(x, y)`: `sqrt(interval_sq)` for spacelike pairs
44
+ only.
45
+ - `exp`, `log`, `retraction`: the trivial flat-space forms.
46
+
47
+ References:
48
+ Misner, C. W., Thorne, K. S., Wheeler, J. A. (1973). *Gravitation*.
49
+ W. H. Freeman. Chapters 1–5 (Lorentzian-geometry foundations).
50
+ Hawking, S. W., Ellis, G. F. R. (1973). *The Large Scale Structure of
51
+ Space-Time*. Cambridge University Press, Ch. 4 (causal structure).
52
+ O'Neill, B. (1983). *Semi-Riemannian Geometry With Applications to
53
+ Relativity*. Academic Press, Ch. 5 (formal pseudo-Riemannian
54
+ geometry).
55
+ """
56
+
57
+ from __future__ import annotations
58
+
59
+ import torch
60
+
61
+
62
+ class LorentzianManifold:
63
+ """Flat pseudo-Riemannian manifold `R^{1, n-1}`.
64
+
65
+ Args:
66
+ n: ambient dimension. The signature is `(1, n-1)`; one timelike
67
+ component (index 0) and `n - 1` spacelike components.
68
+ device, dtype: tensor placement / precision.
69
+
70
+ Example:
71
+ >>> mfd = LorentzianManifold(n=4) # 4D spacetime
72
+ >>> x = mfd.random_point(batch_size=3)
73
+ >>> x.shape
74
+ torch.Size([3, 4])
75
+ >>> # Future-timelike pair: y lies inside the future light cone of x
76
+ >>> y = x + torch.tensor([2.0, 0.5, 0.0, 0.0])
77
+ >>> mfd.causal_type(x, y).tolist() # 1 = future-timelike
78
+ [1, 1, 1]
79
+ """
80
+
81
+ # Causal-type integer codes (returned by `causal_type`). The
82
+ # mathematical sign agrees with the temporal-component sign of
83
+ # `y - x` for non-spacelike pairs.
84
+ SPACELIKE: int = 0
85
+ FUTURE_TIMELIKE: int = 1
86
+ PAST_TIMELIKE: int = -1
87
+ FUTURE_NULL: int = 2
88
+ PAST_NULL: int = -2
89
+
90
+ def __init__(self, n: int,
91
+ device: str | torch.device = "cpu",
92
+ dtype: torch.dtype = torch.float64):
93
+ if n < 2:
94
+ raise ValueError(
95
+ f"n must be >= 2 for a meaningful Lorentzian signature, "
96
+ f"got n={n}"
97
+ )
98
+ self.n = n
99
+ self.device = torch.device(device)
100
+ self.dtype = dtype
101
+
102
+ @property
103
+ def dim(self) -> int:
104
+ """Manifold dimension `n`. Same as ambient; flat space, no
105
+ constraint surface.
106
+ """
107
+ return self.n
108
+
109
+ @property
110
+ def ambient_dim(self) -> int:
111
+ """Ambient dimension `n`. Same as `.dim` for flat space."""
112
+ return self.n
113
+
114
+
115
+ # ----------------------------------------------------------------
116
+ # Minkowski form
117
+ # ----------------------------------------------------------------
118
+
119
+ def minkowski_inner(
120
+ self, u: torch.Tensor, v: torch.Tensor,
121
+ ) -> torch.Tensor:
122
+ """Signed Minkowski inner product
123
+ `⟨u, v⟩_M = -u_0·v_0 + Σ_{i≥1} u_i·v_i`.
124
+
125
+ Can be any real number; negative on timelike, zero on null,
126
+ positive on spacelike directions.
127
+
128
+ Args:
129
+ u, v: `(B, n)` vectors.
130
+ Returns:
131
+ `(B,)` signed scalars.
132
+
133
+ References:
134
+ O'Neill (1983), Semi-Riemannian Geometry, Ch. 3, flat metric
135
+ and its Levi-Civita connection.
136
+ """
137
+ return (u[..., 1:] * v[..., 1:]).sum(dim=-1) - u[..., 0] * v[..., 0]
138
+
139
+ def inner(
140
+ self, x: torch.Tensor, u: torch.Tensor, v: torch.Tensor,
141
+ ) -> torch.Tensor:
142
+ """Pseudo-Riemannian inner product `⟨u, v⟩_x = ⟨u, v⟩_M`.
143
+
144
+ **Signed**: the Minkowski form restricted to ambient vectors,
145
+ with the (1, n-1) signature. NOT positive-definite; `inner(x,
146
+ v, v)` can be any sign depending on `v`'s causal type. This
147
+ differs from the conventional Riemannian `inner` (which is
148
+ always positive); for `LorentzianManifold` the indefinite
149
+ metric IS the geometry.
150
+
151
+ Provided primarily for API uniformity with the other manifold
152
+ classes; so `manifold_aware_inner`, `ProductManifold.inner`
153
+ and similar manifold-generic primitives don't crash when
154
+ passed a `LorentzianManifold`. Callers that compute Riemannian
155
+ norms via `sqrt(inner(...))` should be aware that the square
156
+ root is not real on timelike inputs; prefer `proper_time` /
157
+ `proper_distance` for type-appropriate magnitudes.
158
+
159
+ Args:
160
+ x: base point (unused; flat-space metric is point-independent).
161
+ u, v: ambient `(B, n)` vectors.
162
+ Returns:
163
+ `(B,)` signed inner products.
164
+
165
+ References:
166
+ O'Neill (1983), Semi-Riemannian Geometry, Ch. 3, flat metric
167
+ and its Levi-Civita connection.
168
+ """
169
+ del x
170
+ return self.minkowski_inner(u, v)
171
+
172
+ def norm_sq(self, v: torch.Tensor) -> torch.Tensor:
173
+ """Signed `⟨v, v⟩_M`. Negative = timelike, zero = null,
174
+ positive = spacelike.
175
+
176
+ **No `norm` method exists**; `√⟨v, v⟩_M` is imaginary for
177
+ timelike `v`. Use `proper_time` or `proper_distance` for the
178
+ type-appropriate magnitude.
179
+
180
+ References:
181
+ MTW (1973) §2.3 (Minkowski metric); O'Neill (1983) §3.1.
182
+ """
183
+ return self.minkowski_inner(v, v)
184
+
185
+ def interval_sq(
186
+ self, x: torch.Tensor, y: torch.Tensor,
187
+ ) -> torch.Tensor:
188
+ """Signed squared spacetime interval `⟨y - x, y - x⟩_M`.
189
+
190
+ - Negative: `x` and `y` are timelike-separated (one is in the
191
+ past or future light cone of the other).
192
+ - Zero: `x` and `y` are null-separated (on each other's light
193
+ cone; photon path).
194
+ - Positive: `x` and `y` are spacelike-separated (no signal
195
+ can connect them).
196
+
197
+ References:
198
+ MTW (1973) §1.5; Hawking-Ellis (1973) §4.1.
199
+ """
200
+ return self.norm_sq(y - x)
201
+
202
+ # ----------------------------------------------------------------
203
+ # Causal classification
204
+ # ----------------------------------------------------------------
205
+
206
+ def causal_type(
207
+ self,
208
+ x: torch.Tensor,
209
+ y: torch.Tensor,
210
+ null_atol: float = 1e-9,
211
+ ) -> torch.Tensor:
212
+ """Causal classification of point pairs `(x_i, y_i)`.
213
+
214
+ Returns an integer label per batch element:
215
+ - `SPACELIKE` (= 0): `(y - x)` is spacelike
216
+ - `FUTURE_TIMELIKE` (= 1): timelike with `(y - x)_0 > 0`
217
+ - `PAST_TIMELIKE` (= -1): timelike with `(y - x)_0 < 0`
218
+ - `FUTURE_NULL` (= 2): null/lightlike with `(y - x)_0 > 0`
219
+ - `PAST_NULL` (= -2): null/lightlike with `(y - x)_0 <= 0`
220
+
221
+ Coincident events have no time orientation. Null pairs with
222
+ `dt <= 0`, including coincident events, get PAST_NULL.
223
+
224
+ A pair is null when |interval_sq| <= null_atol and timelike when
225
+ interval_sq < -null_atol. Everything else is spacelike, so the two
226
+ bands never overlap. The default tolerance is the library's
227
+ `numerical_floor_convention`.
228
+
229
+ References:
230
+ Hawking-Ellis (1973) §4.1 (causal structure).
231
+ O'Neill (1983) §5.1 (timelike / null / spacelike vectors).
232
+
233
+ Args:
234
+ x, y: `(B, n)` points.
235
+ null_atol: |interval_sq| floor below which the pair is
236
+ classified as null. Default 1e-9.
237
+ Returns:
238
+ `(B,)` int64 tensor of causal-type codes.
239
+ """
240
+ diff = y - x
241
+ i_sq = self.norm_sq(diff)
242
+ dt = diff[..., 0]
243
+ # Null is a band around zero, |interval_sq| <= null_atol. Timelike
244
+ # needs interval_sq below the negative edge of that band, so the two
245
+ # masks never overlap. A coincident pair has dt = 0, so it is not
246
+ # "future" and falls into the past branch.
247
+ is_null = i_sq.abs() <= null_atol
248
+ is_timelike = i_sq < -null_atol
249
+ is_future = dt > 0
250
+ # Everything starts as SPACELIKE. The four masks below are disjoint,
251
+ # so the order of the where calls does not change the result.
252
+ out = torch.zeros(diff.shape[:-1], dtype=torch.int64,
253
+ device=diff.device)
254
+ out = torch.where(
255
+ is_timelike & is_future,
256
+ torch.full_like(out, self.FUTURE_TIMELIKE), out,
257
+ )
258
+ out = torch.where(
259
+ is_timelike & ~is_future,
260
+ torch.full_like(out, self.PAST_TIMELIKE), out,
261
+ )
262
+ out = torch.where(
263
+ is_null & is_future,
264
+ torch.full_like(out, self.FUTURE_NULL), out,
265
+ )
266
+ out = torch.where(
267
+ is_null & ~is_future,
268
+ torch.full_like(out, self.PAST_NULL), out,
269
+ )
270
+ return out
271
+
272
+ # ----------------------------------------------------------------
273
+ # Proper time / distance
274
+ # ----------------------------------------------------------------
275
+
276
+ def proper_time(
277
+ self, x: torch.Tensor, y: torch.Tensor,
278
+ ) -> torch.Tensor:
279
+ """Proper time `τ = √(-⟨y - x, y - x⟩_M)` for timelike-
280
+ separated pairs. Returns NaN element-wise for non-timelike
281
+ pairs (where the square root would be of a non-negative
282
+ quantity, making the operation type-inappropriate).
283
+
284
+ Physically: proper time along the straight-line world-line
285
+ from `x` to `y`, the time elapsed for an observer travelling
286
+ directly between the events.
287
+
288
+ References:
289
+ MTW (1973) §1.4 (proper time, "wristwatch time").
290
+ """
291
+ i_sq = self.interval_sq(x, y)
292
+ # Timelike means interval_sq < 0. Anything else becomes NaN, so a caller
293
+ # that passes a spacelike pair sees NaN and not a number that looks like
294
+ # a proper time. For spacelike pairs the same square root is a proper
295
+ # distance, which is different physics.
296
+ is_timelike = i_sq < 0
297
+ # The sqrt only sees the timelike entries. Elsewhere `safe` holds 1, so
298
+ # sqrt never gets a negative input, and the NaN fill is applied after.
299
+ safe = torch.where(is_timelike, -i_sq, torch.ones_like(i_sq))
300
+ tau = torch.where(
301
+ is_timelike,
302
+ torch.sqrt(safe),
303
+ torch.full_like(i_sq, float("nan")),
304
+ )
305
+ return tau
306
+
307
+ def proper_distance(
308
+ self, x: torch.Tensor, y: torch.Tensor,
309
+ ) -> torch.Tensor:
310
+ """Proper distance `s = √⟨y - x, y - x⟩_M` for spacelike-
311
+ separated pairs. Returns NaN element-wise for non-spacelike
312
+ pairs.
313
+
314
+ Physically: spatial distance in the reference frame where the
315
+ events are simultaneous.
316
+
317
+ References:
318
+ MTW (1973) §1.4 (proper distance / spatial interval).
319
+ """
320
+ i_sq = self.interval_sq(x, y)
321
+ is_spacelike = i_sq > 0
322
+ safe = torch.where(is_spacelike, i_sq, torch.ones_like(i_sq))
323
+ return torch.where(
324
+ is_spacelike,
325
+ torch.sqrt(safe),
326
+ torch.full_like(i_sq, float("nan")),
327
+ )
328
+
329
+ # ----------------------------------------------------------------
330
+ # Curvature-tensor primitives
331
+ # ----------------------------------------------------------------
332
+ #
333
+ # Flat Minkowski spacetime is identically Ricci-flat: the metric is
334
+ # constant, all Christoffel symbols vanish, and the full Riemann
335
+ # tensor is zero. These methods exist for API parity with curved
336
+ # Lorentzian backgrounds (Schwarzschild, FLRW, etc.) that may be
337
+ # added later as subclasses or sibling manifolds.
338
+ #
339
+ # Shape convention follows physics literature:
340
+ # g_μν (n, n) metric tensor, stored as a plain matrix
341
+ # Γ^σ_μν (n, n, n) Christoffel symbol of the 2nd kind,
342
+ # with the upper index σ first
343
+ # R^σ_ρμν (n, n, n, n) Riemann curvature tensor
344
+ # R_μν (n, n) Ricci tensor
345
+ # R ( ) Ricci scalar (scalar curvature)
346
+
347
+ def metric_tensor(self, x: torch.Tensor) -> torch.Tensor:
348
+ """Minkowski metric `g_μν = diag(-1, +1, +1, …, +1)`.
349
+
350
+ Constant on flat spacetime; the `x` argument is unused but
351
+ included for API parity with curved Lorentzian backgrounds.
352
+
353
+ Args:
354
+ x: `(..., n)` base point (unused).
355
+ Returns:
356
+ `(..., n, n)` metric tensor.
357
+
358
+ References:
359
+ O'Neill (1983), Semi-Riemannian Geometry, Ch. 3, flat metric
360
+ and its Levi-Civita connection.
361
+ """
362
+ # Diagonal of the metric. Index 0 is time, so it gets the minus sign.
363
+ eta = torch.ones(self.n, device=x.device, dtype=x.dtype)
364
+ eta[0] = -1.0
365
+ # Broadcast diagonal to (..., n, n)
366
+ out_shape = x.shape[:-1] + (self.n, self.n)
367
+ return torch.diag_embed(eta).expand(out_shape).contiguous()
368
+
369
+ def christoffel_symbols(self, x: torch.Tensor) -> torch.Tensor:
370
+ """Christoffel symbols `Γ^σ_μν` of the Levi-Civita connection.
371
+
372
+ Identically zero on flat Minkowski spacetime (the metric has
373
+ zero derivatives). Returned as a `(..., n, n, n)` tensor with
374
+ the upper index first.
375
+
376
+ Subclasses representing curved Lorentzian backgrounds should
377
+ override this with the appropriate non-zero expression.
378
+
379
+ References:
380
+ MTW (1973) §8.5 (Christoffel symbols in flat spacetime);
381
+ O'Neill (1983) §3.5 (Levi-Civita connection on
382
+ semi-Riemannian manifolds).
383
+ """
384
+ return torch.zeros(
385
+ *x.shape[:-1], self.n, self.n, self.n,
386
+ device=x.device, dtype=x.dtype,
387
+ )
388
+
389
+ def riemann_tensor(self, x: torch.Tensor) -> torch.Tensor:
390
+ """Riemann curvature tensor `R^σ_ρμν`.
391
+
392
+ Identically zero on flat Minkowski (the spacetime is flat:
393
+ parallel transport around any closed loop returns the original
394
+ vector). Shape `(..., n, n, n, n)`.
395
+
396
+ References:
397
+ MTW (1973) §11.3 (Riemann tensor in flat spacetime);
398
+ O'Neill (1983) §3.5 (curvature tensor formulas).
399
+ """
400
+ return torch.zeros(
401
+ *x.shape[:-1], self.n, self.n, self.n, self.n,
402
+ device=x.device, dtype=x.dtype,
403
+ )
404
+
405
+ def ricci_tensor(self, x: torch.Tensor) -> torch.Tensor:
406
+ """Ricci tensor `R_μν = R^σ_μσν`.
407
+
408
+ Identically zero on flat Minkowski. Shape `(..., n, n)`.
409
+
410
+ Physics relevance: Einstein's equations equate `R_μν − (1/2) R
411
+ g_μν + Λ g_μν` to the stress-energy tensor (up to constants);
412
+ in vacuum without cosmological constant, `R_μν = 0`; flat
413
+ Minkowski is the simplest such solution.
414
+
415
+ References:
416
+ MTW (1973) §17.2.
417
+ """
418
+ return torch.zeros(
419
+ *x.shape[:-1], self.n, self.n,
420
+ device=x.device, dtype=x.dtype,
421
+ )
422
+
423
+ def scalar_curvature(self, x: torch.Tensor) -> torch.Tensor:
424
+ """Ricci scalar `R = g^μν R_μν`.
425
+
426
+ Identically zero on flat Minkowski. Shape `(...,)` (a scalar
427
+ per batch element).
428
+
429
+ References:
430
+ MTW (1973) §17.4 (scalar curvature).
431
+ """
432
+ return torch.zeros(
433
+ *x.shape[:-1], device=x.device, dtype=x.dtype,
434
+ )
435
+
436
+ # ----------------------------------------------------------------
437
+ # Construction
438
+ # ----------------------------------------------------------------
439
+
440
+ def origin(self, batch_size: int = 1) -> torch.Tensor:
441
+ """Origin `(0, 0, …, 0) ∈ R^{1, n-1}`; convention for the
442
+ spacetime origin event.
443
+
444
+ References:
445
+ O'Neill (1983), Semi-Riemannian Geometry, Ch. 3, flat metric
446
+ and its Levi-Civita connection.
447
+ """
448
+ if batch_size < 0:
449
+ raise ValueError(f"batch_size must be >= 0, got {batch_size}")
450
+ return torch.zeros(batch_size, self.n,
451
+ device=self.device, dtype=self.dtype)
452
+
453
+ def random_point(
454
+ self,
455
+ batch_size: int = 1,
456
+ generator: torch.Generator | None = None,
457
+ ) -> torch.Tensor:
458
+ """Sample B random points in spacetime; Gaussian in each of
459
+ the `n` coordinates. The manifold is `R^n` so no rejection
460
+ needed.
461
+
462
+ References:
463
+ O'Neill (1983), Semi-Riemannian Geometry, Ch. 3, flat metric
464
+ and its Levi-Civita connection.
465
+ """
466
+ if batch_size < 0:
467
+ raise ValueError(f"batch_size must be >= 0, got {batch_size}")
468
+ # A torch.Generator belongs to one device, so the draw runs on the
469
+ # generator's device and is moved to the manifold's device afterward.
470
+ return torch.randn(
471
+ batch_size,
472
+ self.n,
473
+ generator=generator,
474
+ device=generator.device if generator is not None else self.device,
475
+ dtype=self.dtype,
476
+ ).to(self.device)
477
+
478
+ # ----------------------------------------------------------------
479
+ # Trivial flat-space geodesic primitives (included for API parity
480
+ # with the Riemannian manifolds)
481
+ # ----------------------------------------------------------------
482
+
483
+ def projection(
484
+ self, x: torch.Tensor, w: torch.Tensor,
485
+ ) -> torch.Tensor:
486
+ """Tangent-space projection. For flat space the tangent at
487
+ every point is the full `R^n`, so projection is the identity.
488
+ Included for API parity with curved Riemannian manifolds.
489
+
490
+ References:
491
+ O'Neill (1983), Semi-Riemannian Geometry, Ch. 3, flat metric
492
+ and its Levi-Civita connection.
493
+ """
494
+ del x
495
+ return w
496
+
497
+ def egrad_to_rgrad(self, point: torch.Tensor, egrad: torch.Tensor) -> torch.Tensor:
498
+ """Metric dual of an ambient covector in flat spacetime.
499
+
500
+ The Lorentzian metric is indefinite: this is not generally a descent
501
+ direction, unlike a Riemannian metric gradient.
502
+
503
+ Args:
504
+ point: (B, ambient_dim) base point.
505
+ egrad: (B, ambient_dim) Euclidean gradient.
506
+ Returns:
507
+ (B, ambient_dim) metric gradient in tangent coordinates.
508
+
509
+ References:
510
+ O'Neill (1983), Semi-Riemannian Geometry, Ch. 3, metric duality.
511
+ """
512
+ # The metric diag(-1, 1, ..., 1) is its own inverse, so raising the
513
+ # index flips the sign of the time component.
514
+ return torch.cat((-egrad[..., :1], egrad[..., 1:]), dim=-1)
515
+
516
+ def exp(
517
+ self, x: torch.Tensor, v: torch.Tensor,
518
+ ) -> torch.Tensor:
519
+ """Exponential map. In flat space all geodesics are straight
520
+ lines, so `exp_x(v) = x + v` regardless of `v`'s causal type.
521
+
522
+ References:
523
+ O'Neill (1983) §3.5 (flat semi-Riemannian geodesics).
524
+ """
525
+ return x + v
526
+
527
+ def log(
528
+ self, x: torch.Tensor, y: torch.Tensor,
529
+ ) -> torch.Tensor:
530
+ """Logarithmic map = `y - x` (the geodesic from `x` to `y` is
531
+ a straight line; its tangent at `x` is the difference vector).
532
+
533
+ References:
534
+ O'Neill (1983), Semi-Riemannian Geometry, Ch. 3, flat metric
535
+ and its Levi-Civita connection.
536
+ """
537
+ return y - x
538
+
539
+ def retraction(
540
+ self, x: torch.Tensor, v: torch.Tensor,
541
+ ) -> torch.Tensor:
542
+ """Retraction = exponential map for flat space.
543
+
544
+ References:
545
+ O'Neill (1983), Semi-Riemannian Geometry, Ch. 3, flat metric
546
+ and its Levi-Civita connection.
547
+ """
548
+ return self.exp(x, v)
549
+
550
+ # ----------------------------------------------------------------
551
+ # is_on_manifold: trivially True, since flat space has no constraint
552
+ # ----------------------------------------------------------------
553
+
554
+ def is_on_manifold(
555
+ self, x: torch.Tensor, atol: float = 1e-9,
556
+ ) -> torch.Tensor:
557
+ """Always True per-batch element; `R^n` has no constraint
558
+ surface. Included for API parity. `atol` is accepted for
559
+ signature compatibility but unused.
560
+
561
+ References:
562
+ O'Neill (1983), Semi-Riemannian Geometry, Ch. 3, flat metric
563
+ and its Levi-Civita connection.
564
+ """
565
+ del atol
566
+ return torch.ones(x.shape[:-1], dtype=torch.bool,
567
+ device=x.device)