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,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
+ ])