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,301 @@
1
+ # SPDX-License-Identifier: FSL-1.1-ALv2
2
+ # Copyright 2026 Synoros
3
+
4
+ """Persistent homology of Vietoris-Rips filtrations.
5
+
6
+ For a point cloud `X`, the Vietoris-Rips filtration is the family of
7
+ simplicial complexes `{VR(X, r) : r ≥ 0}` indexed by scale `r`. As
8
+ `r` grows, simplices are born; the persistent-homology pipeline
9
+ tracks which topological features (connected components, loops,
10
+ voids, ...) survive across scales.
11
+
12
+ This module exports a single public function
13
+
14
+ persistence_diagrams(points_or_distances, max_dim=2, max_radius=inf,
15
+ input_is_distance=False, reduction_backend="python")
16
+ → (diagrams, masks)
17
+
18
+ where `diagrams[k]` is a `(B, max_pairs_k, 2)` batched padded tensor
19
+ of `(birth, death)` pairs for the k-th persistence diagram, and
20
+ `masks[k]` is the matching `(B, max_pairs_k)` validity mask. Bars
21
+ that survive to the end of the filtration have `death = +inf`. With a finite
22
+ `max_radius` the filtration ends at that radius, so bars still alive there
23
+ get `death = +inf` as well.
24
+
25
+ Two computational paths:
26
+
27
+ H₀: sequential host union-find on sorted filtration edges, dispatched
28
+ per batch member. All components are born at zero; each merge records
29
+ one death and every remaining component gets a `(0, +inf)` bar.
30
+
31
+ H_{1..max_dim}: Z/2 left-to-right reduction of the boundary
32
+ matrix in filtration order (Edelsbrunner-Letscher-Zomorodian
33
+ 2002), via `_reduction.reduce_filtration`. Sequential within one
34
+ filtration. Simplex enumeration is batched across point clouds, but the
35
+ public function runs one reduction per batch element.
36
+
37
+ References:
38
+ Edelsbrunner, H., Letscher, D., Zomorodian, A. (2002). Topological
39
+ persistence and simplification. Discrete & Computational
40
+ Geometry 28:511-533. Foundational PH paper.
41
+ Cohen-Steiner, D., Edelsbrunner, H., Harer, J. (2007). Stability
42
+ of persistence diagrams. Discrete & Computational Geometry
43
+ 37(1):103-120. Stability under perturbation of input data.
44
+ Edelsbrunner, H., Harer, J. (2010). Computational Topology: An
45
+ Introduction. AMS. Reference textbook.
46
+ Bauer, U. (2021). Ripser: efficient computation of Vietoris-Rips
47
+ persistence barcodes. J. Appl. Comput. Topology 5:391-423.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ from typing import Literal
53
+
54
+ import torch
55
+
56
+ from holonomy_lib.simplicial import (
57
+ SparseSimplicialComplex,
58
+ pairwise_distances,
59
+ vietoris_rips_dense,
60
+ )
61
+ from holonomy_lib.topology._filtration import build_filtration
62
+ from holonomy_lib.topology._reduction import reduce_filtration
63
+
64
+
65
+ def persistence_diagrams(
66
+ points_or_distances: torch.Tensor,
67
+ max_dim: int = 2,
68
+ max_radius: float = float("inf"),
69
+ input_is_distance: bool = False,
70
+ reduction_backend: Literal["python", "torch"] = "python",
71
+ ) -> tuple[list[torch.Tensor], list[torch.Tensor]]:
72
+ """Persistence diagrams for dims 0..max_dim of a Vietoris-Rips filtration.
73
+
74
+ Args:
75
+ points_or_distances: either `(B, n, d)` point clouds or
76
+ `(B, n, n)` distance matrices. Set `input_is_distance=True`
77
+ for the latter.
78
+ max_dim: highest dim to compute. Common choices: 1 (clusters +
79
+ loops) or 2 (clusters + loops + voids).
80
+ max_radius: build the filtration only up to this scale. Bars
81
+ born after this radius are not recorded, and bars still alive at
82
+ this radius get `death = +inf`. Default `+inf` builds the full
83
+ filtration.
84
+ input_is_distance: if True, `points_or_distances` is read as a
85
+ distance matrix directly; otherwise pairwise Euclidean
86
+ distances are computed.
87
+ reduction_backend: `"python"` (default; CPython set columns)
88
+ or `"torch"` (LongTensor columns on the filtration's device).
89
+ See `_reduction.reduce_filtration` for the tradeoff.
90
+
91
+ Returns:
92
+ `(diagrams, masks)` where:
93
+ diagrams[k]: `(B, max_pairs_k, 2)` float Tensor of
94
+ `(birth, death)` pairs. Bars surviving to infinity have
95
+ `death = +inf`.
96
+ masks[k]: `(B, max_pairs_k)` bool Tensor; True for real
97
+ pairs, False for padding.
98
+
99
+ Both lists have length `max_dim + 1`. H_0 retains one bar per input
100
+ vertex, including zero-length bars when coincident vertices merge.
101
+ Higher-dimensional diagrams omit zero-length bars.
102
+
103
+ Notes:
104
+ Cost is dominated by the matrix reduction for `max_dim ≥ 1`:
105
+ O(B · m³) worst case where `m` is the number of simplices in
106
+ the filtration, but typically much faster due to sparsity. For
107
+ large point clouds, lower `max_radius` or `max_dim` to keep
108
+ the simplex count manageable.
109
+
110
+ Simplex construction is batched. Z/2 pivot reduction and H_0 union-find
111
+ are sequential within each filtration: every pivot or component merge
112
+ depends on previous ones. Ragged filtrations are dispatched separately.
113
+ Python reduction is an explicit host boundary; torch reduction keeps
114
+ columns on device but synchronizes for dependent pivot decisions. Bar
115
+ coordinates are returned without an autograd connection to the inputs.
116
+
117
+ Comparison vs `ripser`: tie-breaking on equal birth/death
118
+ values may differ; the persistence diagrams agree under
119
+ bottleneck distance.
120
+
121
+ References:
122
+ Edelsbrunner-Letscher-Zomorodian (2002).
123
+ Cohen-Steiner-Edelsbrunner-Harer (2007) stability.
124
+ """
125
+ if points_or_distances.ndim != 3:
126
+ raise ValueError(
127
+ f"points_or_distances must be 3-D (B, ...); got "
128
+ f"{tuple(points_or_distances.shape)}"
129
+ )
130
+ if not points_or_distances.is_floating_point():
131
+ raise ValueError("points_or_distances must have real floating dtype")
132
+ if max_dim < 0:
133
+ raise ValueError(f"max_dim must be >= 0, got {max_dim}")
134
+ if max_radius < 0:
135
+ raise ValueError(f"max_radius must be >= 0, got {max_radius}")
136
+ if reduction_backend not in ("python", "torch"):
137
+ raise ValueError(f"unknown reduction_backend={reduction_backend!r}")
138
+ B = points_or_distances.shape[0]
139
+
140
+ if input_is_distance:
141
+ if points_or_distances.shape[-1] != points_or_distances.shape[-2]:
142
+ raise ValueError(
143
+ f"input_is_distance=True requires (B, n, n); got "
144
+ f"{tuple(points_or_distances.shape)}"
145
+ )
146
+ distances = points_or_distances
147
+ else:
148
+ distances = pairwise_distances(points_or_distances)
149
+
150
+ # pairs_per_batch_per_dim[b][k] collects the (birth, death) pairs of batch
151
+ # member b in dimension k.
152
+ pairs_per_batch_per_dim: list[list[list[tuple[float, float]]]] = [
153
+ [[] for _ in range(max_dim + 1)] for _ in range(B)
154
+ ]
155
+
156
+ # All batch members share one tensorized simplex enumeration. The reduction
157
+ # is sequential, so it runs once per member on that member's filtration.
158
+ complexes = vietoris_rips_dense(
159
+ distances, max_radius=max_radius, max_dim=max_dim + 1,
160
+ ) if max_dim >= 1 else None
161
+ for b in range(B):
162
+ d_b = distances[b]
163
+
164
+ # H_0: union-find on sorted filtration edges.
165
+ h0_pairs = _persistent_h0(d_b, max_radius)
166
+ pairs_per_batch_per_dim[b][0] = h0_pairs
167
+
168
+ # H_{1..max_dim}: build the VR complex and reduce its boundary matrix.
169
+ # With max_dim == 0 there is no complex, so only union-find runs.
170
+ # Otherwise the reduction runs on whatever simplices exist, and a
171
+ # dimension with none adds no bars, so small clouds need no extra guard.
172
+ if complexes is not None:
173
+ complex = SparseSimplicialComplex(
174
+ {k: table[b][complexes.valid_mask[k][b]]
175
+ for k, table in complexes.simplices_by_dim.items()},
176
+ n_vertices=complexes.n_vertices, device=complexes.device,
177
+ )
178
+ filt = build_filtration(d_b, complex)
179
+ pairs_by_dim = reduce_filtration(filt, backend=reduction_backend)
180
+ # H_0 already came from union-find, so only dims 1..max_dim are
181
+ # taken from this reduction.
182
+ for k in range(1, max_dim + 1):
183
+ pairs_per_batch_per_dim[b][k] = pairs_by_dim.get(k, [])
184
+
185
+ # Pad per-dim into batched tensors.
186
+ diagrams: list[torch.Tensor] = []
187
+ masks: list[torch.Tensor] = []
188
+ for k in range(max_dim + 1):
189
+ max_pairs_k = max(
190
+ len(pairs_per_batch_per_dim[b][k]) for b in range(B)
191
+ ) if B > 0 else 0
192
+ # No member has a bar in this dimension. Return an empty (B, 0, 2)
193
+ # tensor so callers can index every dimension the same way.
194
+ if max_pairs_k == 0:
195
+ diagrams.append(
196
+ torch.zeros(B, 0, 2, dtype=distances.dtype,
197
+ device=distances.device)
198
+ )
199
+ masks.append(
200
+ torch.zeros(B, 0, dtype=torch.bool, device=distances.device)
201
+ )
202
+ continue
203
+ diag = torch.zeros(
204
+ B, max_pairs_k, 2,
205
+ dtype=distances.dtype, device=distances.device,
206
+ )
207
+ # counts[b] is how many real pairs member b has in this dimension.
208
+ counts = [len(member[k]) for member in pairs_per_batch_per_dim]
209
+ # One host-to-device copy per dimension. Pairs are flattened in batch
210
+ # order, so each (birth, death) stays whole.
211
+ values = torch.tensor(
212
+ [pair for member in pairs_per_batch_per_dim for pair in member[k]],
213
+ dtype=distances.dtype, device=distances.device,
214
+ )
215
+ count_tensor = torch.tensor(counts, device=distances.device)
216
+ # Row b's first counts[b] slots are real and the rest are padding.
217
+ positions = torch.arange(max_pairs_k, device=distances.device)
218
+ mask = positions < count_tensor[:, None]
219
+ # Boolean-mask assignment fills the True slots row by row, which matches
220
+ # the batch-order flattening of `values`.
221
+ diag[mask] = values
222
+ diagrams.append(diag)
223
+ masks.append(mask)
224
+
225
+ return diagrams, masks
226
+
227
+
228
+ # ============================================================
229
+ # H_0 via batched union-find on sorted filtration edges
230
+ # ============================================================
231
+
232
+
233
+ def _persistent_h0(
234
+ distances: torch.Tensor, max_radius: float,
235
+ ) -> list[tuple[float, float]]:
236
+ """Persistent H_0 for a single distance matrix.
237
+
238
+ Takes an `(n, n)` distance matrix and the radius cap. Returns `n`
239
+ `(birth, death)` pairs, one per vertex. Every component is born at 0.
240
+ Each union-find merge kills one component at that edge length. A graph
241
+ that is connected at the cap leaves one component alive to infinity, and
242
+ a disconnected one leaves one essential bar per component. Edges are
243
+ copied to the host in one bulk transfer. Union-find runs sequentially
244
+ because each merge changes the partition that later edges see.
245
+ """
246
+ n = distances.shape[0]
247
+ if n == 0:
248
+ return []
249
+ if n == 1:
250
+ # Single vertex: one infinite bar.
251
+ return [(0.0, float("inf"))]
252
+
253
+ # Strictly upper-triangular edges (i, j) with i < j and d <= max_radius.
254
+ iu, ju = torch.triu_indices(n, n, offset=1, device=distances.device)
255
+ edge_lengths = distances[iu, ju]
256
+ if max_radius != float("inf"):
257
+ keep = edge_lengths <= max_radius
258
+ iu, ju, edge_lengths = iu[keep], ju[keep], edge_lengths[keep]
259
+
260
+ # Sort edges by length. The stable sort breaks ties by upper-triangle
261
+ # order, so the barcode is the same on every run.
262
+ sort_idx = torch.argsort(edge_lengths, stable=True)
263
+ sorted_u = iu[sort_idx].tolist()
264
+ sorted_v = ju[sort_idx].tolist()
265
+ sorted_lengths = edge_lengths[sort_idx].tolist()
266
+
267
+ # Disjoint-set union with path compression + union-by-rank.
268
+ parent = list(range(n))
269
+ rank = [0] * n
270
+
271
+ def find(x: int) -> int:
272
+ while parent[x] != x:
273
+ parent[x] = parent[parent[x]] # path compression
274
+ x = parent[x]
275
+ return x
276
+
277
+ pairs: list[tuple[float, float]] = []
278
+ for u, v, length in zip(sorted_u, sorted_v, sorted_lengths, strict=True):
279
+ ru, rv = find(u), find(v)
280
+ if ru == rv:
281
+ continue
282
+ # All vertices are born at zero, so either representative gives
283
+ # the same barcode. Rank only keeps the parent trees shallow.
284
+ if rank[ru] < rank[rv]:
285
+ parent[ru] = rv
286
+ elif rank[ru] > rank[rv]:
287
+ parent[rv] = ru
288
+ else:
289
+ parent[rv] = ru
290
+ rank[ru] += 1
291
+ # A successful merge kills one zero-born connected component.
292
+ pairs.append((0.0, length))
293
+
294
+ # Components that never merged survive to infinity. For a graph connected
295
+ # at the cap, that is one component.
296
+ n_merged = len(pairs)
297
+ n_surviving = n - n_merged
298
+ for _ in range(n_surviving):
299
+ pairs.append((0.0, float("inf")))
300
+
301
+ return pairs
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: holonomy-lib
3
+ Version: 0.8.0
4
+ Summary: Batched PyTorch primitives for Riemannian geometry, spectral graph theory, discrete curvature, topology, and tensor decompositions.
5
+ Author: Synoros
6
+ License-Expression: FSL-1.1-ALv2
7
+ Project-URL: Homepage, https://github.com/Synoros-io/holonomy_lib
8
+ Project-URL: Repository, https://github.com/Synoros-io/holonomy_lib
9
+ Project-URL: Issues, https://github.com/Synoros-io/holonomy_lib/issues
10
+ Keywords: pytorch,differential-geometry,riemannian-manifolds,hyperbolic-geometry,riemannian-optimization,spectral-graph-theory,graph-laplacian,magnetic-laplacian,heat-kernel,diffusion-maps,effective-resistance,ricci-curvature,ricci-flow,cellular-sheaves,simplicial-complex,hodge-laplacian,persistent-homology,topological-data-analysis,information-geometry,tensor-decomposition,numerical-linear-algebra
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.12
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE.md
23
+ Requires-Dist: torch>=2.2
24
+ Requires-Dist: numpy>=1.26
25
+ Requires-Dist: scipy>=1.11.2
26
+ Provides-Extra: comparison
27
+ Requires-Dist: pymanopt; extra == "comparison"
28
+ Requires-Dist: geoopt; extra == "comparison"
29
+ Requires-Dist: geomstats; extra == "comparison"
30
+ Requires-Dist: autograd; extra == "comparison"
31
+ Requires-Dist: tensorly; extra == "comparison"
32
+ Requires-Dist: networkx; extra == "comparison"
33
+ Requires-Dist: gudhi; extra == "comparison"
34
+ Requires-Dist: ripser; extra == "comparison"
35
+ Requires-Dist: GraphRicciCurvature; extra == "comparison"
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest; extra == "dev"
38
+ Requires-Dist: ruff; extra == "dev"
39
+ Requires-Dist: mypy; extra == "dev"
40
+ Requires-Dist: scipy-stubs; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # holonomy_lib
44
+
45
+ Geometry, graph and topology primitives for PyTorch. Most functions take a batch
46
+ of inputs (a leading batch dimension) and run on the device and dtype you pass in.
47
+
48
+ ## Install
49
+
50
+ Python 3.12 or later. Install PyTorch for your hardware first, using the
51
+ [PyTorch selector](https://pytorch.org/get-started/locally/), then:
52
+
53
+ ```bash
54
+ pip install holonomy-lib
55
+ ```
56
+
57
+ ## Example
58
+
59
+ Effective resistance between the ends of a three-node path and of a triangle,
60
+ computed as one batch:
61
+
62
+ ```python
63
+ import torch
64
+ from holonomy_lib.spectral import effective_resistance
65
+
66
+ device = "cuda" if torch.cuda.is_available() else "cpu" # ROCm builds also use "cuda"
67
+ adjacency = torch.tensor([
68
+ [[0, 1, 0], [1, 0, 1], [0, 1, 0]], # path 0-1-2
69
+ [[0, 1, 1], [1, 0, 1], [1, 1, 0]], # triangle
70
+ ], dtype=torch.float64, device=device)
71
+
72
+ R = effective_resistance(adjacency)
73
+ print(R.shape) # torch.Size([2, 3, 3])
74
+ print(R[:, 0, 2]) # tensor([2.0000, 0.6667], dtype=torch.float64)
75
+ ```
76
+
77
+ ## What's in it
78
+
79
+ [CONTENTS.md](https://github.com/Synoros-io/holonomy_lib/blob/master/CONTENTS.md) lists every function with its signature and the
80
+ paper it implements.
81
+
82
+ | Subpackage | Contents |
83
+ |---|---|
84
+ | `manifolds` | Fixed-rank, SPD, Lorentz, Lorentzian, stereographic and product manifolds; model-space volumes. |
85
+ | `optimization` | Riemannian SGD. |
86
+ | `hyperbolic` | Fréchet means, manifold-aware inner products, hyperbolic heat kernels. |
87
+ | `lie` | SO(3) exponential and logarithm, rotations, real spherical harmonics. |
88
+ | `info_geometry` | Bregman and KL divergences, Fisher information, natural gradients. |
89
+ | `tensor_calculus` | Mode unfolding, mode products, HOSVD. |
90
+ | `algebra` | Truncated SVD, dense and sparse eigensolvers, conjugate gradient, LSQR. |
91
+ | `convex` | Regularized least squares. |
92
+ | `spectral` | Graph Laplacians, embeddings, heat diffusion, effective resistance, spectral dimension. |
93
+ | `discrete_geometry` | Ollivier and Forman curvature, Ricci flow with surgery. |
94
+ | `sheaf` | Graph sheaves, coboundaries, sheaf Laplacians and their spectra. |
95
+ | `simplicial` | Dense and sparse simplicial complexes, boundary operators, Vietoris-Rips. |
96
+ | `topology` | Hodge Laplacians, Betti numbers, persistent homology. |
97
+
98
+ ## Explorer
99
+
100
+ `holonomy-explorer` opens a local page that runs the library live: curvature
101
+ and diffusion on graphs, geodesics as curvature passes through zero,
102
+ persistent homology of point clouds, and batch-size timings on your hardware.
103
+ Options: `--device`, `--dtype`, `--port`, `--no-browser`.
104
+
105
+ ![The explorer's graph view](https://raw.githubusercontent.com/Synoros-io/holonomy_lib/master/docs/explorer.png)
106
+
107
+ ## Experimental
108
+
109
+ `holonomy_lib.experimental` holds two unfinished pieces that are not part of
110
+ the stable API:
111
+
112
+ - `hyperbolic_laplacian_eigenmaps`: nothing in its objective keeps nodes apart,
113
+ so an embedding can collapse to a point.
114
+ - `HeterogeneousKappaManifold`: a curvature per point. Its `dissimilarity` is
115
+ not a metric.
116
+
117
+ ## Limits
118
+
119
+ - Use `float64` when accuracy matters.
120
+ - Sparse eigensolvers, sparse simplicial complexes and sheaves handle one
121
+ instance at a time. Sparse eigensolvers are not differentiable.
122
+ - Persistent homology runs its reduction one batch member at a time on the CPU.
123
+ - Ollivier curvature uses entropy-regularized transport, so it is close to the
124
+ exact value rather than equal to it, and it warns if the solver did not
125
+ converge. Edge weights act as both lengths and random-walk weights.
126
+ - Gradients through `cg` can be wrong when it converges in very few iterations.
127
+ - Effective resistance and commute time are `inf` between disconnected components.
128
+
129
+ ## Development
130
+
131
+ ```bash
132
+ uv sync --extra dev
133
+ python -m pytest -q
134
+ python -m ruff check src/ tests/
135
+ python -m mypy src/
136
+ python tools/audit.py src/holonomy_lib --strict
137
+ ```
138
+
139
+ [CONVENTIONS.md](https://github.com/Synoros-io/holonomy_lib/blob/master/CONVENTIONS.md) has the coding and numerical rules;
140
+ [CHANGELOG.md](https://github.com/Synoros-io/holonomy_lib/blob/master/CHANGELOG.md) has the release history.
141
+
142
+ ## Citation
143
+
144
+ ```bibtex
145
+ @software{holonomy_lib,
146
+ author = {John Vaught},
147
+ title = {holonomy\_lib: Batched PyTorch primitives for geometry and topology},
148
+ version = {0.8.0},
149
+ year = {2026},
150
+ url = {https://github.com/Synoros-io/holonomy_lib},
151
+ }
152
+ ```
153
+
154
+ Please also cite the papers named in the docstrings of the functions you use.
155
+
156
+ ## License
157
+
158
+ [FSL-1.1-ALv2](https://github.com/Synoros-io/holonomy_lib/blob/master/LICENSE.md). You can use and modify it freely; competing
159
+ commercial use is reserved to Synoros. Each release becomes Apache-2.0 two
160
+ years after it ships. Releases up to 0.7.0 remain AGPL-3.0-or-later. For other
161
+ terms, contact [contact@synoros.io](mailto:contact@synoros.io).
@@ -0,0 +1,82 @@
1
+ holonomy_explorer/__init__.py,sha256=TbPP-kKgdgN9bxB_XvZOmHg13uDwgdJw33PRD5FuEeU,70
2
+ holonomy_explorer/__main__.py,sha256=J8vfIKNMtZ9CyTFEJpcCOQnOUyN3nYZ6DyxaiFT2IDM,2759
3
+ holonomy_explorer/batching.py,sha256=k8jCkk6YBHxzuAmnuQRAECTTPM1pCqV18QonrHVUZMw,6720
4
+ holonomy_explorer/common.py,sha256=MnyVIinZkxvPFjA9bzMrdFsRqZctFhV4jxLqcjK807U,6749
5
+ holonomy_explorer/curvature.py,sha256=PojOi0VGM_XTde8KrAxygvmOFoXpQIc8CEgUhgkSGHw,5837
6
+ holonomy_explorer/datasets.py,sha256=E8ELvVSrTLKtwtgt5WSycajH1iXAJ_tF8Mu4gt1lT7c,10152
7
+ holonomy_explorer/graphs.py,sha256=W8ubZde8XMOzocRcWnVcgSghyMFuhTchJB3EKUUQ3mM,11740
8
+ holonomy_explorer/server.py,sha256=es-4SkL2WLPWV-v0KBn-lVkaN7dk3K3-67zc-789sEY,7687
9
+ holonomy_explorer/topology.py,sha256=6c0Avw4dpqwXgivugCDw0b7V_rYv4DaVSCOqOo_H22w,4769
10
+ holonomy_explorer/static/app.js,sha256=IZABPlg9iPjvFBbATyrKzycsO_aLO2V66zrUYR3VWOI,31163
11
+ holonomy_explorer/static/charts.js,sha256=_kq6QJjf19qRBrJGPF9zRXcr_9zjptoziQaBglOH0T4,6711
12
+ holonomy_explorer/static/index.html,sha256=igEQMy8IDD9-6_nDNSNvYwwtiNTOB8imz34G84tnPRo,2571
13
+ holonomy_explorer/static/layout.js,sha256=8lGLY3kO8gDPXHvS1veEbW3lki0FPkpGV6XyIEM-_sI,4689
14
+ holonomy_explorer/static/style.css,sha256=52OPsb7buVqhHVNiVKxE94rmaMO1R-0ykQVn3wWWnyU,10208
15
+ holonomy_explorer/static/view.js,sha256=FZA6WGFq8FkGMzoGxJBQsHq8s0gvwnYP2QBo-Jcuqw8,10630
16
+ holonomy_lib/__init__.py,sha256=_J5vdotTpz2rDD8pdM9MlVxjiTEE1ov9RUcEpY3z0d4,327
17
+ holonomy_lib/_graph_utils.py,sha256=wTkJZeAEQJ_Q1f9SDgRxi_TtPw4ZpoHyddGIjT5CFo4,1899
18
+ holonomy_lib/_quadrature.py,sha256=nrh6oAS8hItOUPEjHjJ3jN9nfvMhZEQIbUJkH0myaPY,2820
19
+ holonomy_lib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ holonomy_lib/algebra/__init__.py,sha256=1uzdf2qHyq-r7WZ80xMTFS2FjwQf9dBu7CZFfTlGiFA,1516
21
+ holonomy_lib/algebra/eigsh_sparse.py,sha256=0t-GcNel1rL_wb60o8-G61zBooXoNeBNE2TRpo1URYE,10737
22
+ holonomy_lib/algebra/lanczos.py,sha256=v-iQlTDdahoeY1LifzIk8W1U46XV-NxDVwMOABaWLms,17002
23
+ holonomy_lib/algebra/linear.py,sha256=SV9Tb6F6dhE9pout4FDIDAbeHVxMCnjt68v4e9llvkk,7555
24
+ holonomy_lib/algebra/solvers.py,sha256=1o9d_yRVVEMdLtQJsOYNMWXvNMxt4mZJFGCU76vuw08,24104
25
+ holonomy_lib/convex/__init__.py,sha256=-rRTZPEGDVCvMm1S5-dVrXcJ_3lU_6hR7ioCaHmX5co,1138
26
+ holonomy_lib/convex/least_squares.py,sha256=DkxmDqJNbegrOfyB45JDfJXU0MqrvziFVVE9g_bBvIo,7609
27
+ holonomy_lib/discrete_geometry/__init__.py,sha256=6ZaTTMbdSKDALMdYawUsLs7bnt-M-o62gfO3fqutOTU,1277
28
+ holonomy_lib/discrete_geometry/forman.py,sha256=O8A_0amiRlnQ3gt1FygEz40GszgnKAkm8rsNpgM7bWA,10567
29
+ holonomy_lib/discrete_geometry/ricci.py,sha256=BhJmyACa0M7Aqcj97GYTRdgmyb_rwL7ySRTp1bXYXGQ,39850
30
+ holonomy_lib/experimental/__init__.py,sha256=p8WqCl2OWfRHt9iVvHZm0HN2eQn1sXl7dp7LXM-i1DQ,477
31
+ holonomy_lib/experimental/heterogeneous_kappa.py,sha256=6-QHmB4rNCALuL2MxYzZ2oLqua-hkh9Qo_uVCsuStQ4,15282
32
+ holonomy_lib/experimental/laplacian_eigenmaps.py,sha256=fGqmoqMIbeJuZndS62r2wPnN5n8bfUZWS9d615n2ghk,8733
33
+ holonomy_lib/hyperbolic/__init__.py,sha256=0g8V___z89RsjwLbXda4gkZ1_ko8WOXQk7ffRQbR5j0,944
34
+ holonomy_lib/hyperbolic/frechet_mean.py,sha256=qw1C0KcSUNLesFLnUu-zoHtTyfj4O28WM5KwFy-kFQA,19281
35
+ holonomy_lib/hyperbolic/heat_kernel.py,sha256=RWmQpOqIxcNmn1R9DN-3X-UKGLLtb2-2KvQG_Lo1EhA,25248
36
+ holonomy_lib/hyperbolic/manifold_inner.py,sha256=9ax5jeu_NaLNvyyGMhvcPQBAwl0XvJ3W7AR_lwQa8WY,3301
37
+ holonomy_lib/info_geometry/__init__.py,sha256=T0d6Ie8d7aUlfuCMtmzflDd4DLRKdOkbzF4Jb_uLO64,2533
38
+ holonomy_lib/info_geometry/divergences.py,sha256=fE3Xc6DBfFQVnbIojTTw97i2xY_TSGENVbdgWD4CElA,8577
39
+ holonomy_lib/info_geometry/fisher.py,sha256=qOgmQnmKs8ldMBOalCQddgJkBm-9BS6RINZqu6U-81U,7067
40
+ holonomy_lib/lie/__init__.py,sha256=Vw2iSSqMFgjZeURD1isMW1LjlZ4uLaVwQ70ENrRstLU,2409
41
+ holonomy_lib/lie/so3.py,sha256=CmA1ULGgMiXyPGOaMbGNfFwYqlNgsiiHPzxC29ebmns,15503
42
+ holonomy_lib/lie/spherical_harmonics.py,sha256=9Ggx8OHFUgLW3IgiY6dojmhCJqTL4AAvxOyHfVsV-Ew,7299
43
+ holonomy_lib/manifolds/__init__.py,sha256=tCGwNUA-qlSCAxwSvVebUjAUpIFqNy8H7P7AGiAzUeY,2237
44
+ holonomy_lib/manifolds/comparison.py,sha256=VmvqxupDTYMxmPbgdhhcSvZCu69mV6FRwAiscYVrpMQ,15380
45
+ holonomy_lib/manifolds/fixed_rank.py,sha256=3tk3fhdbtlIzG04qef22QxYUi1NV1m7tlsfmHZGy0Y4,19485
46
+ holonomy_lib/manifolds/lorentz.py,sha256=lL7KP0B2MxVhPheLcEwFsK34aSoebw-OYLn9jcCXJhE,29004
47
+ holonomy_lib/manifolds/lorentzian.py,sha256=s4LNUUxefxJLsIHDcCHEcr9rvlzb7NojOOHevdrASwI,21944
48
+ holonomy_lib/manifolds/product.py,sha256=9xK3ailR5tKBgckW3SvMtFeA-ZZzfJVjhBnTCgR10Oc,16635
49
+ holonomy_lib/manifolds/spd.py,sha256=quOFTz_FVTjJt2TgRJ-p30Hxz03fIzotlsDSFAW5PGo,21834
50
+ holonomy_lib/manifolds/stereographic.py,sha256=XS1di52kWnl0Rp8IbeETUg0hSvlYnPQhLWrBL2_oBiU,31047
51
+ holonomy_lib/optimization/__init__.py,sha256=Km60vtVvlDrkHqanRysA0lkMDxwnsvvgG1IRsxA92oA,1639
52
+ holonomy_lib/optimization/base.py,sha256=-e2DUkvbIqZi3Hazz9m2Yc08CVOQSK5QJu4WScXUSXA,2434
53
+ holonomy_lib/optimization/sgd.py,sha256=hxuBGB_TCup_KW8axB9ADHiMzRzGIxPmCUUIaXN7Du4,4116
54
+ holonomy_lib/sheaf/__init__.py,sha256=hoHivoy_zqUOC0PuJnuzm1CPPcFxLwW1bZKvBvBN2Xw,3185
55
+ holonomy_lib/sheaf/graph_sheaf.py,sha256=FElURHbnqXZrFEJmmaA6C4HYXimGAFA0LMcDNWCULkU,15423
56
+ holonomy_lib/sheaf/laplacian.py,sha256=_18wabK8tfSBnBagquYNaUQeAjlWSrrDy1MfApyqSts,23144
57
+ holonomy_lib/simplicial/__init__.py,sha256=ImONeXhqF3jQjNwpJGJ79oW9dPIDA9iwO1FmpypEok0,1933
58
+ holonomy_lib/simplicial/_face_lookup.py,sha256=BIAPUeqJKgNTO6N4YF-4pBolodGmjz-IsrUbEWTcbC8,3801
59
+ holonomy_lib/simplicial/complex.py,sha256=RESjE1EslkYfqzGwl35uEJTK6K75MUSFVwgpdIdvvgY,6887
60
+ holonomy_lib/simplicial/sparse_complex.py,sha256=_F7V2Nt7xWs_f_rW906MsrCyiF_ouH2Ai6XSCqjVdro,5552
61
+ holonomy_lib/simplicial/vietoris_rips.py,sha256=PQjlukGWoOOyZ9sI6OLlLjKVvRwI5kJAqsPHvkFimas,12133
62
+ holonomy_lib/spectral/__init__.py,sha256=CyHxGWo3DB4VT4QpJHm4w-8GmJb95H_Ed6A16NwZy8c,2581
63
+ holonomy_lib/spectral/diffusion_map.py,sha256=Zdzr0Y5jTao5lJTaeAMEkivsros-P3UZl_qX4Rha6ic,7799
64
+ holonomy_lib/spectral/effective_resistance.py,sha256=vu9Tyzk5si-FiI7ORpzLDazhh9AtsIXe7JMjU5AsFH8,9202
65
+ holonomy_lib/spectral/embedding.py,sha256=zOqUMUZjsv2rZlFrxw7ZMMpFDOzOrBDozc1cKAxZTvk,5613
66
+ holonomy_lib/spectral/heat_kernel.py,sha256=d7V2-ovSpw3Na-DRIC7AaGDn7EhHFhhYQmiMB4o3vjY,10252
67
+ holonomy_lib/spectral/laplacian.py,sha256=0wD4r6Mp2CR6BDwY_PGKEFG9n7eQh_MdfiCrc5SSIpE,17776
68
+ holonomy_lib/spectral/magnetic.py,sha256=OSoH68ONc_0mOLKnRb-hk5axdBDMJv-IxCfCUM1-mCU,14360
69
+ holonomy_lib/spectral/spectral_dimension.py,sha256=Qf-hF_wqZ0QTZR-uHBHJIBVgtqJJfs6s8pxJdRoc3-E,4810
70
+ holonomy_lib/tensor_calculus/__init__.py,sha256=aa9Cr-Ye36PMUdRhCjaklbjPr1skdk8_K5YfI7n7ogc,654
71
+ holonomy_lib/tensor_calculus/decomposition.py,sha256=aX_Wglha1ZcrK-9fn6UXDJHdJ2OO6LPe9qoJseURTr8,7795
72
+ holonomy_lib/topology/__init__.py,sha256=_F6hKEBLSnRgfXB6qKq6E8T5GaFCqKZdunGrdiwVo6s,978
73
+ holonomy_lib/topology/_filtration.py,sha256=sYhq28chXVtnQQGuEWi4pJtlvFuvVZHSPSex2ejBYlo,5875
74
+ holonomy_lib/topology/_reduction.py,sha256=6MS6S4wHZIgQRAx6OVk0xu7MhDKFVl_xPO6bXyq-edI,10442
75
+ holonomy_lib/topology/hodge.py,sha256=sEuORngp21M4TyZiVz1hZmcaIvJFKUn7Y7pzHs7JthY,9714
76
+ holonomy_lib/topology/persistent_homology.py,sha256=J-pyKA-kUVmEvYlvBTgQQdc-NVSl9GGfjDbpAfVlgt8,12733
77
+ holonomy_lib-0.8.0.dist-info/licenses/LICENSE.md,sha256=-VpEBq5bW92sgS2cMsqa_s9oSKcPt5VXYQB0RLXQ0s4,3739
78
+ holonomy_lib-0.8.0.dist-info/METADATA,sha256=RTDbUKKihUFxAEkfwXMzYmuLFO7nvaxaB3FnOOEeDZc,6822
79
+ holonomy_lib-0.8.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
80
+ holonomy_lib-0.8.0.dist-info/entry_points.txt,sha256=DHAZ9YwjmV1pfrfQELNg7JnepSdBE63efm4z1owaa_4,70
81
+ holonomy_lib-0.8.0.dist-info/top_level.txt,sha256=8CFDkax9Y2qRF7jqMxpEzk53ODPnQdBmG1MuGQVj5c8,31
82
+ holonomy_lib-0.8.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ holonomy-explorer = holonomy_explorer.__main__:main
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-ALv2
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Synoros
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
@@ -0,0 +1,2 @@
1
+ holonomy_explorer
2
+ holonomy_lib