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,425 @@
|
|
|
1
|
+
# SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
# Copyright 2026 Synoros
|
|
3
|
+
|
|
4
|
+
"""Mixed-curvature product manifold M_1 × M_2 × … × M_k.
|
|
5
|
+
|
|
6
|
+
A point is a tuple of per-submanifold coordinates, stored as a
|
|
7
|
+
**flat concatenated tensor** for ergonomic batched ops:
|
|
8
|
+
|
|
9
|
+
`(B, sum(m.ambient_dim for m in manifolds))`
|
|
10
|
+
|
|
11
|
+
The combined Riemannian metric is the (optionally weighted) direct
|
|
12
|
+
sum of the submanifold metrics:
|
|
13
|
+
|
|
14
|
+
g((u_1, …, u_k), (v_1, …, v_k))_x = Σ_i w_i · g_i(u_i, v_i)_{x_i}
|
|
15
|
+
|
|
16
|
+
so geodesic distance is Pythagorean:
|
|
17
|
+
|
|
18
|
+
d²((x_1, …, x_k), (y_1, …, y_k)) = Σ_i w_i · d_i²(x_i, y_i).
|
|
19
|
+
|
|
20
|
+
Each manifold operation (exp, log, projection, retraction, …)
|
|
21
|
+
delegates to the corresponding submanifold method, slice by slice.
|
|
22
|
+
A point `(x_E, x_H, x_S)` with one Euclidean, one hyperbolic, and one
|
|
23
|
+
spherical component therefore works by calling the submanifold classes
|
|
24
|
+
(`KappaStereographicManifold`, `LorentzManifold`) on each slice.
|
|
25
|
+
|
|
26
|
+
References:
|
|
27
|
+
Skopek, O., Ganea, O.-E., Bécigneul, G. (2019). Mixed-curvature
|
|
28
|
+
Variational Autoencoders. ICLR. (mixed-curvature embedding for
|
|
29
|
+
deep generative models)
|
|
30
|
+
Gu, A., Sala, F., Gunel, B., Ré, C. (2019). Learning mixed-
|
|
31
|
+
curvature representations in product spaces. ICLR.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import math
|
|
37
|
+
from collections.abc import Sequence
|
|
38
|
+
|
|
39
|
+
import torch
|
|
40
|
+
|
|
41
|
+
from holonomy_lib.manifolds.lorentz import _safe_sqrt
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ProductManifold:
|
|
45
|
+
"""Riemannian product of one or more manifolds.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
manifolds: ordered list of submanifolds. Each must expose
|
|
49
|
+
`.dim`, `.ambient_dim`, `random_point`, `origin`,
|
|
50
|
+
`is_on_manifold`, `distance`, `exp`, `log`, `inner`,
|
|
51
|
+
`projection`, `retraction`, `egrad_to_rgrad`. `exp_0` and `log_0`
|
|
52
|
+
are needed only by the tangent-at-origin methods. Heterogeneous
|
|
53
|
+
mixes are fine, e.g.
|
|
54
|
+
`[KappaStereographicManifold(kappa=-1.0, n=4), LorentzManifold(n=4)]`.
|
|
55
|
+
weights: optional per-submanifold positive finite scalars `w_i` on
|
|
56
|
+
the squared-distance contribution. Default uniform (`w_i = 1`).
|
|
57
|
+
Use `weights` to up-weight one geometry relative to others in
|
|
58
|
+
the combined metric.
|
|
59
|
+
device, dtype: tensor placement / precision for `random_point`
|
|
60
|
+
/ `origin`. (Sub-manifold operations use the input tensor's
|
|
61
|
+
own device.)
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
>>> from holonomy_lib.manifolds import (
|
|
65
|
+
... KappaStereographicManifold, LorentzManifold, ProductManifold,
|
|
66
|
+
... )
|
|
67
|
+
>>> mfd = ProductManifold([
|
|
68
|
+
... KappaStereographicManifold(n=4, kappa=0.0), # Euclidean part
|
|
69
|
+
... LorentzManifold(n=4, k=-1.0), # Hyperbolic part
|
|
70
|
+
... ])
|
|
71
|
+
>>> x = mfd.random_point(batch_size=3)
|
|
72
|
+
>>> x.shape # 4 (Euclidean ambient) + 5 (Lorentz ambient) = 9
|
|
73
|
+
torch.Size([3, 9])
|
|
74
|
+
>>> mfd.distance(x, mfd.random_point(batch_size=3)).shape
|
|
75
|
+
torch.Size([3])
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
manifolds: Sequence,
|
|
81
|
+
weights: Sequence[float] | None = None,
|
|
82
|
+
device: str | torch.device = "cpu",
|
|
83
|
+
dtype: torch.dtype = torch.float64,
|
|
84
|
+
):
|
|
85
|
+
if not manifolds:
|
|
86
|
+
raise ValueError("manifolds must contain at least one submanifold")
|
|
87
|
+
self.manifolds = list(manifolds)
|
|
88
|
+
if weights is None:
|
|
89
|
+
self.weights = [1.0] * len(self.manifolds)
|
|
90
|
+
else:
|
|
91
|
+
if len(weights) != len(self.manifolds):
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"weights length {len(weights)} != "
|
|
94
|
+
f"manifolds length {len(self.manifolds)}"
|
|
95
|
+
)
|
|
96
|
+
if any(w <= 0 or not math.isfinite(w) for w in weights):
|
|
97
|
+
raise ValueError("weights must be positive and finite")
|
|
98
|
+
self.weights = [float(w) for w in weights]
|
|
99
|
+
self.device = torch.device(device)
|
|
100
|
+
self.dtype = dtype
|
|
101
|
+
|
|
102
|
+
# Each submanifold owns a contiguous block of the flat tensor. The
|
|
103
|
+
# running sum of block sizes gives where each block starts, and pairing
|
|
104
|
+
# adjacent entries gives (start, end) for every slice.
|
|
105
|
+
self._ambient_dims = [m.ambient_dim for m in self.manifolds]
|
|
106
|
+
offsets = [0]
|
|
107
|
+
for d in self._ambient_dims:
|
|
108
|
+
offsets.append(offsets[-1] + d)
|
|
109
|
+
self._ambient_slices = list(zip(offsets[:-1], offsets[1:], strict=True))
|
|
110
|
+
|
|
111
|
+
# Same bookkeeping for intrinsic coordinates, which exp_0 and log_0
|
|
112
|
+
# use. They are shorter than the ambient points for components that
|
|
113
|
+
# embed, like Lorentz (n intrinsic, n + 1 ambient).
|
|
114
|
+
self._intrinsic_dims = [m.dim for m in self.manifolds]
|
|
115
|
+
i_offsets = [0]
|
|
116
|
+
for d in self._intrinsic_dims:
|
|
117
|
+
i_offsets.append(i_offsets[-1] + d)
|
|
118
|
+
self._intrinsic_slices = list(zip(i_offsets[:-1], i_offsets[1:], strict=True))
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def dim(self) -> int:
|
|
122
|
+
"""Intrinsic manifold dimension Σ dim_i."""
|
|
123
|
+
return sum(self._intrinsic_dims)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def ambient_dim(self) -> int:
|
|
127
|
+
"""Ambient dimension Σ ambient_dim_i."""
|
|
128
|
+
return sum(self._ambient_dims)
|
|
129
|
+
|
|
130
|
+
# ----------------------------------------------------------------
|
|
131
|
+
# Slicing helpers (used by every op)
|
|
132
|
+
# ----------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def _split_ambient(self, x: torch.Tensor) -> list[torch.Tensor]:
|
|
135
|
+
"""Split a flat ambient tensor into one view per submanifold.
|
|
136
|
+
|
|
137
|
+
Input: `x` of shape `(..., ambient_dim)`. Output: list of views in
|
|
138
|
+
submanifold order. Raises ValueError on the wrong shape.
|
|
139
|
+
"""
|
|
140
|
+
if x.ndim < 2 or x.shape[-1] != self.ambient_dim:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"x must have shape (..., {self.ambient_dim}); got {tuple(x.shape)}"
|
|
143
|
+
)
|
|
144
|
+
return [x[..., s:e] for s, e in self._ambient_slices]
|
|
145
|
+
|
|
146
|
+
def _split_intrinsic(self, v: torch.Tensor) -> list[torch.Tensor]:
|
|
147
|
+
"""Split a flat intrinsic tangent into one view per submanifold.
|
|
148
|
+
|
|
149
|
+
Input: `v` of shape `(..., dim)`. Output: list of views in submanifold
|
|
150
|
+
order. Raises ValueError on the wrong shape.
|
|
151
|
+
"""
|
|
152
|
+
if v.ndim < 2 or v.shape[-1] != self.dim:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"v must have shape (..., {self.dim}); got {tuple(v.shape)}"
|
|
155
|
+
)
|
|
156
|
+
return [v[..., s:e] for s, e in self._intrinsic_slices]
|
|
157
|
+
|
|
158
|
+
def _join(self, pieces: list[torch.Tensor]) -> torch.Tensor:
|
|
159
|
+
"""Concatenate per-submanifold tensors back into flat form along the last
|
|
160
|
+
axis. The pieces must share their leading (batch) dims.
|
|
161
|
+
"""
|
|
162
|
+
return torch.cat(pieces, dim=-1)
|
|
163
|
+
|
|
164
|
+
def component(self, x: torch.Tensor, index: int) -> torch.Tensor:
|
|
165
|
+
"""Extract the `index`-th submanifold's coordinates from a
|
|
166
|
+
flat point tensor `x`. Public convenience for inspection /
|
|
167
|
+
diagnostics."""
|
|
168
|
+
return self._split_ambient(x)[index]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ----------------------------------------------------------------
|
|
172
|
+
# Construction
|
|
173
|
+
# ----------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def origin(self, batch_size: int = 1) -> torch.Tensor:
|
|
176
|
+
"""Concatenated origins of every submanifold.
|
|
177
|
+
|
|
178
|
+
References:
|
|
179
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
180
|
+
The component formulas are cited in their manifold methods.
|
|
181
|
+
"""
|
|
182
|
+
if batch_size < 0:
|
|
183
|
+
raise ValueError(f"batch_size must be >= 0, got {batch_size}")
|
|
184
|
+
return self._join([
|
|
185
|
+
m.origin(batch_size=batch_size).to(device=self.device, dtype=self.dtype)
|
|
186
|
+
for m in self.manifolds
|
|
187
|
+
])
|
|
188
|
+
|
|
189
|
+
def random_point(
|
|
190
|
+
self,
|
|
191
|
+
batch_size: int = 1,
|
|
192
|
+
generator: torch.Generator | None = None,
|
|
193
|
+
) -> torch.Tensor:
|
|
194
|
+
"""Sample each submanifold and concatenate the results. Each
|
|
195
|
+
submanifold's `random_point` decides its own distribution (uniform,
|
|
196
|
+
Gaussian tangent, ...). One `generator` is passed to all of them in
|
|
197
|
+
order, so a single seed fixes the whole draw.
|
|
198
|
+
|
|
199
|
+
References:
|
|
200
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
201
|
+
The component formulas are cited in their manifold methods.
|
|
202
|
+
"""
|
|
203
|
+
if batch_size < 0:
|
|
204
|
+
raise ValueError(f"batch_size must be >= 0, got {batch_size}")
|
|
205
|
+
return self._join([
|
|
206
|
+
m.random_point(batch_size=batch_size, generator=generator).to(
|
|
207
|
+
device=self.device, dtype=self.dtype,
|
|
208
|
+
)
|
|
209
|
+
for m in self.manifolds
|
|
210
|
+
])
|
|
211
|
+
|
|
212
|
+
def is_on_manifold(
|
|
213
|
+
self, x: torch.Tensor, atol: float = 1e-9,
|
|
214
|
+
) -> torch.Tensor:
|
|
215
|
+
"""A product point is on the manifold iff every component is
|
|
216
|
+
on its submanifold.
|
|
217
|
+
|
|
218
|
+
References:
|
|
219
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
220
|
+
The component formulas are cited in their manifold methods.
|
|
221
|
+
"""
|
|
222
|
+
parts = self._split_ambient(x)
|
|
223
|
+
on_each = torch.stack(
|
|
224
|
+
[
|
|
225
|
+
m.is_on_manifold(p, atol=atol)
|
|
226
|
+
for m, p in zip(self.manifolds, parts, strict=True)
|
|
227
|
+
],
|
|
228
|
+
dim=0,
|
|
229
|
+
)
|
|
230
|
+
return on_each.all(dim=0)
|
|
231
|
+
|
|
232
|
+
# ----------------------------------------------------------------
|
|
233
|
+
# Tangent operations
|
|
234
|
+
# ----------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
def projection(
|
|
237
|
+
self, x: torch.Tensor, w: torch.Tensor,
|
|
238
|
+
) -> torch.Tensor:
|
|
239
|
+
"""Project the ambient direction `w` (flat) onto the tangent
|
|
240
|
+
space at `x`, per-submanifold.
|
|
241
|
+
|
|
242
|
+
References:
|
|
243
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
244
|
+
The component formulas are cited in their manifold methods.
|
|
245
|
+
"""
|
|
246
|
+
x_parts = self._split_ambient(x)
|
|
247
|
+
w_parts = self._split_ambient(w)
|
|
248
|
+
return self._join([
|
|
249
|
+
m.projection(x_i, w_i)
|
|
250
|
+
for m, x_i, w_i in zip(self.manifolds, x_parts, w_parts, strict=True)
|
|
251
|
+
])
|
|
252
|
+
|
|
253
|
+
def egrad_to_rgrad(self, point: torch.Tensor, egrad: torch.Tensor) -> torch.Tensor:
|
|
254
|
+
"""Apply each component's metric inverse, including its weight.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
point: (B, ambient_dim) base point.
|
|
258
|
+
egrad: (B, ambient_dim) Euclidean gradient.
|
|
259
|
+
Returns:
|
|
260
|
+
(B, ambient_dim) metric gradient in tangent coordinates.
|
|
261
|
+
|
|
262
|
+
References:
|
|
263
|
+
Absil, Mahony, Sepulchre (2008), §3.6, direct-sum metric duality.
|
|
264
|
+
"""
|
|
265
|
+
points = self._split_ambient(point)
|
|
266
|
+
gradients = self._split_ambient(egrad)
|
|
267
|
+
# The weighted metric on component i is w_i · g_i, whose inverse is
|
|
268
|
+
# g_i^{-1} / w_i. Dividing each slice by its weight gives the gradient.
|
|
269
|
+
return self._join([
|
|
270
|
+
m.egrad_to_rgrad(p, g) / weight
|
|
271
|
+
for m, p, g, weight in zip(
|
|
272
|
+
self.manifolds, points, gradients, self.weights, strict=True,
|
|
273
|
+
)
|
|
274
|
+
])
|
|
275
|
+
|
|
276
|
+
def inner(
|
|
277
|
+
self, x: torch.Tensor, u: torch.Tensor, v: torch.Tensor,
|
|
278
|
+
) -> torch.Tensor:
|
|
279
|
+
"""Weighted direct-sum inner product:
|
|
280
|
+
`⟨u, v⟩_x = Σ_i w_i · ⟨u_i, v_i⟩_{x_i}`.
|
|
281
|
+
|
|
282
|
+
References:
|
|
283
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
284
|
+
The component formulas are cited in their manifold methods.
|
|
285
|
+
"""
|
|
286
|
+
x_parts = self._split_ambient(x)
|
|
287
|
+
u_parts = self._split_ambient(u)
|
|
288
|
+
v_parts = self._split_ambient(v)
|
|
289
|
+
terms = [
|
|
290
|
+
weight * m.inner(x_i, u_i, v_i)
|
|
291
|
+
for m, x_i, u_i, v_i, weight in zip(
|
|
292
|
+
self.manifolds, x_parts, u_parts, v_parts, self.weights, strict=True,
|
|
293
|
+
)
|
|
294
|
+
]
|
|
295
|
+
return torch.stack(terms).sum(dim=0)
|
|
296
|
+
|
|
297
|
+
def norm(self, x: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
|
|
298
|
+
"""`sqrt(⟨v, v⟩_x)`. Autograd-safe at v=0.
|
|
299
|
+
|
|
300
|
+
References:
|
|
301
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
302
|
+
The component formulas are cited in their manifold methods.
|
|
303
|
+
"""
|
|
304
|
+
return _safe_sqrt(self.inner(x, v, v))
|
|
305
|
+
|
|
306
|
+
# ----------------------------------------------------------------
|
|
307
|
+
# Distance, exp, log
|
|
308
|
+
# ----------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
def distance(
|
|
311
|
+
self, x: torch.Tensor, y: torch.Tensor,
|
|
312
|
+
) -> torch.Tensor:
|
|
313
|
+
"""Pythagorean: `d² = Σ_i w_i · d_i²(x_i, y_i)`.
|
|
314
|
+
|
|
315
|
+
References:
|
|
316
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
317
|
+
The component formulas are cited in their manifold methods.
|
|
318
|
+
"""
|
|
319
|
+
x_parts = self._split_ambient(x)
|
|
320
|
+
y_parts = self._split_ambient(y)
|
|
321
|
+
# Scaling each distance by sqrt(w_i) makes its square w_i * d_i^2, so
|
|
322
|
+
# the vector norm over components is the weighted Pythagorean sum.
|
|
323
|
+
distances = [
|
|
324
|
+
math.sqrt(weight) * m.distance(x_i, y_i)
|
|
325
|
+
for m, x_i, y_i, weight in zip(
|
|
326
|
+
self.manifolds, x_parts, y_parts, self.weights, strict=True,
|
|
327
|
+
)
|
|
328
|
+
]
|
|
329
|
+
return torch.linalg.vector_norm(torch.stack(distances, dim=-1), dim=-1)
|
|
330
|
+
|
|
331
|
+
def exp(
|
|
332
|
+
self, x: torch.Tensor, v: torch.Tensor,
|
|
333
|
+
) -> torch.Tensor:
|
|
334
|
+
"""Per-component exponential map.
|
|
335
|
+
|
|
336
|
+
References:
|
|
337
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
338
|
+
The component formulas are cited in their manifold methods.
|
|
339
|
+
"""
|
|
340
|
+
x_parts = self._split_ambient(x)
|
|
341
|
+
v_parts = self._split_ambient(v)
|
|
342
|
+
return self._join([
|
|
343
|
+
m.exp(x_i, v_i)
|
|
344
|
+
for m, x_i, v_i in zip(self.manifolds, x_parts, v_parts, strict=True)
|
|
345
|
+
])
|
|
346
|
+
|
|
347
|
+
def log(
|
|
348
|
+
self, x: torch.Tensor, y: torch.Tensor,
|
|
349
|
+
) -> torch.Tensor:
|
|
350
|
+
"""Per-component logarithmic map.
|
|
351
|
+
|
|
352
|
+
References:
|
|
353
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
354
|
+
The component formulas are cited in their manifold methods.
|
|
355
|
+
"""
|
|
356
|
+
x_parts = self._split_ambient(x)
|
|
357
|
+
y_parts = self._split_ambient(y)
|
|
358
|
+
return self._join([
|
|
359
|
+
m.log(x_i, y_i)
|
|
360
|
+
for m, x_i, y_i in zip(self.manifolds, x_parts, y_parts, strict=True)
|
|
361
|
+
])
|
|
362
|
+
|
|
363
|
+
def retraction(
|
|
364
|
+
self, x: torch.Tensor, v: torch.Tensor,
|
|
365
|
+
) -> torch.Tensor:
|
|
366
|
+
"""Per-component retraction. A component whose retraction is its
|
|
367
|
+
exponential map gets exp here.
|
|
368
|
+
|
|
369
|
+
References:
|
|
370
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
371
|
+
The component formulas are cited in their manifold methods.
|
|
372
|
+
"""
|
|
373
|
+
x_parts = self._split_ambient(x)
|
|
374
|
+
v_parts = self._split_ambient(v)
|
|
375
|
+
return self._join([
|
|
376
|
+
m.retraction(x_i, v_i)
|
|
377
|
+
for m, x_i, v_i in zip(self.manifolds, x_parts, v_parts, strict=True)
|
|
378
|
+
])
|
|
379
|
+
|
|
380
|
+
# ----------------------------------------------------------------
|
|
381
|
+
# Tangent-at-origin convenience (exp_0 / log_0)
|
|
382
|
+
# ----------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
def exp_0(self, v: torch.Tensor) -> torch.Tensor:
|
|
385
|
+
"""Embed a flat Euclidean tangent `(B, Σ dim_i)` onto the
|
|
386
|
+
product manifold via per-submanifold `exp_0`. Result is the
|
|
387
|
+
flat ambient form `(B, Σ ambient_dim_i)`.
|
|
388
|
+
|
|
389
|
+
A submanifold without `exp_0` raises AttributeError, which is the
|
|
390
|
+
intended failure. Typical products (Euclidean, hyperbolic,
|
|
391
|
+
κ-stereographic) all provide it.
|
|
392
|
+
|
|
393
|
+
References:
|
|
394
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
395
|
+
The component formulas are cited in their manifold methods.
|
|
396
|
+
"""
|
|
397
|
+
if v.ndim < 2 or v.shape[-1] != self.dim:
|
|
398
|
+
raise ValueError(
|
|
399
|
+
f"v last dim must be Σ intrinsic dims = {self.dim}, "
|
|
400
|
+
f"got {tuple(v.shape)}"
|
|
401
|
+
)
|
|
402
|
+
v_parts = self._split_intrinsic(v)
|
|
403
|
+
return self._join([
|
|
404
|
+
m.exp_0(v_i)
|
|
405
|
+
for m, v_i in zip(self.manifolds, v_parts, strict=True)
|
|
406
|
+
])
|
|
407
|
+
|
|
408
|
+
def log_0(self, y: torch.Tensor) -> torch.Tensor:
|
|
409
|
+
"""Inverse of `exp_0`: flat-ambient point → flat-intrinsic
|
|
410
|
+
tangent. Per-submanifold `log_0` concatenated.
|
|
411
|
+
|
|
412
|
+
References:
|
|
413
|
+
Lee (2018), Introduction to Riemannian Manifolds, product metrics.
|
|
414
|
+
The component formulas are cited in their manifold methods.
|
|
415
|
+
"""
|
|
416
|
+
if y.ndim < 2 or y.shape[-1] != self.ambient_dim:
|
|
417
|
+
raise ValueError(
|
|
418
|
+
f"y last dim must be Σ ambient dims = {self.ambient_dim}, "
|
|
419
|
+
f"got {tuple(y.shape)}"
|
|
420
|
+
)
|
|
421
|
+
y_parts = self._split_ambient(y)
|
|
422
|
+
return self._join([
|
|
423
|
+
m.log_0(y_i)
|
|
424
|
+
for m, y_i in zip(self.manifolds, y_parts, strict=True)
|
|
425
|
+
])
|