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.
- holonomy_explorer/__init__.py +1 -0
- holonomy_explorer/__main__.py +79 -0
- holonomy_explorer/batching.py +182 -0
- holonomy_explorer/common.py +187 -0
- holonomy_explorer/curvature.py +154 -0
- holonomy_explorer/datasets.py +346 -0
- holonomy_explorer/graphs.py +286 -0
- holonomy_explorer/server.py +172 -0
- holonomy_explorer/static/app.js +435 -0
- holonomy_explorer/static/charts.js +114 -0
- holonomy_explorer/static/index.html +45 -0
- holonomy_explorer/static/layout.js +85 -0
- holonomy_explorer/static/style.css +121 -0
- holonomy_explorer/static/view.js +195 -0
- holonomy_explorer/topology.py +118 -0
- holonomy_lib/__init__.py +9 -0
- holonomy_lib/_graph_utils.py +52 -0
- holonomy_lib/_quadrature.py +74 -0
- holonomy_lib/algebra/__init__.py +32 -0
- holonomy_lib/algebra/eigsh_sparse.py +251 -0
- holonomy_lib/algebra/lanczos.py +361 -0
- holonomy_lib/algebra/linear.py +180 -0
- holonomy_lib/algebra/solvers.py +547 -0
- holonomy_lib/convex/__init__.py +27 -0
- holonomy_lib/convex/least_squares.py +184 -0
- holonomy_lib/discrete_geometry/__init__.py +38 -0
- holonomy_lib/discrete_geometry/forman.py +242 -0
- holonomy_lib/discrete_geometry/ricci.py +855 -0
- holonomy_lib/experimental/__init__.py +13 -0
- holonomy_lib/experimental/heterogeneous_kappa.py +361 -0
- holonomy_lib/experimental/laplacian_eigenmaps.py +200 -0
- holonomy_lib/hyperbolic/__init__.py +27 -0
- holonomy_lib/hyperbolic/frechet_mean.py +381 -0
- holonomy_lib/hyperbolic/heat_kernel.py +608 -0
- holonomy_lib/hyperbolic/manifold_inner.py +81 -0
- holonomy_lib/info_geometry/__init__.py +64 -0
- holonomy_lib/info_geometry/divergences.py +224 -0
- holonomy_lib/info_geometry/fisher.py +179 -0
- holonomy_lib/lie/__init__.py +54 -0
- holonomy_lib/lie/so3.py +385 -0
- holonomy_lib/lie/spherical_harmonics.py +189 -0
- holonomy_lib/manifolds/__init__.py +53 -0
- holonomy_lib/manifolds/comparison.py +343 -0
- holonomy_lib/manifolds/fixed_rank.py +453 -0
- holonomy_lib/manifolds/lorentz.py +699 -0
- holonomy_lib/manifolds/lorentzian.py +567 -0
- holonomy_lib/manifolds/product.py +425 -0
- holonomy_lib/manifolds/spd.py +504 -0
- holonomy_lib/manifolds/stereographic.py +680 -0
- holonomy_lib/optimization/__init__.py +42 -0
- holonomy_lib/optimization/base.py +65 -0
- holonomy_lib/optimization/sgd.py +112 -0
- holonomy_lib/py.typed +0 -0
- holonomy_lib/sheaf/__init__.py +74 -0
- holonomy_lib/sheaf/graph_sheaf.py +352 -0
- holonomy_lib/sheaf/laplacian.py +529 -0
- holonomy_lib/simplicial/__init__.py +55 -0
- holonomy_lib/simplicial/_face_lookup.py +99 -0
- holonomy_lib/simplicial/complex.py +168 -0
- holonomy_lib/simplicial/sparse_complex.py +145 -0
- holonomy_lib/simplicial/vietoris_rips.py +277 -0
- holonomy_lib/spectral/__init__.py +55 -0
- holonomy_lib/spectral/diffusion_map.py +169 -0
- holonomy_lib/spectral/effective_resistance.py +199 -0
- holonomy_lib/spectral/embedding.py +131 -0
- holonomy_lib/spectral/heat_kernel.py +236 -0
- holonomy_lib/spectral/laplacian.py +441 -0
- holonomy_lib/spectral/magnetic.py +351 -0
- holonomy_lib/spectral/spectral_dimension.py +104 -0
- holonomy_lib/tensor_calculus/__init__.py +23 -0
- holonomy_lib/tensor_calculus/decomposition.py +208 -0
- holonomy_lib/topology/__init__.py +28 -0
- holonomy_lib/topology/_filtration.py +147 -0
- holonomy_lib/topology/_reduction.py +227 -0
- holonomy_lib/topology/hodge.py +250 -0
- holonomy_lib/topology/persistent_homology.py +301 -0
- holonomy_lib-0.8.0.dist-info/METADATA +161 -0
- holonomy_lib-0.8.0.dist-info/RECORD +82 -0
- holonomy_lib-0.8.0.dist-info/WHEEL +5 -0
- holonomy_lib-0.8.0.dist-info/entry_points.txt +2 -0
- holonomy_lib-0.8.0.dist-info/licenses/LICENSE.md +105 -0
- 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)
|