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,64 @@
1
+ # SPDX-License-Identifier: FSL-1.1-ALv2
2
+ # Copyright 2026 Synoros
3
+
4
+ """holonomy_lib.info_geometry: information-geometric primitives.
5
+
6
+ Information geometry studies probability distributions as points on a
7
+ Riemannian manifold whose metric is the Fisher information. Three
8
+ families of primitives appear here:
9
+
10
+ Bregman divergences: `bregman_divergence(p, q, potential)`.
11
+ The asymmetric "distance" induced by a convex potential function.
12
+ Generalizes squared-Euclidean (potential = ½‖·‖²) and Itakura-
13
+ Saito (potential = −log(·)) and Kullback-Leibler (potential =
14
+ Σ p log p, the negative entropy). Banerjee et al. (2005) is the
15
+ canonical reference.
16
+
17
+ KL divergences: closed forms for the common exponential
18
+ families:
19
+ `kl_divergence_categorical(p, q)`: discrete distributions.
20
+ `kl_divergence_gaussian(mu_p, Sigma_p, mu_q, Sigma_q)`: multi-
21
+ variate Gaussians.
22
+
23
+ Fisher metric: the Riemannian inner product on the statistical
24
+ manifold. For q near p, KL(p || q) is half the squared length of
25
+ q - p in this metric. Plus `natural_gradient`, the steepest-descent
26
+ direction under this metric. See Amari (1998).
27
+ `fisher_information_categorical(p)`: diagonal Fisher on the simplex.
28
+ `fisher_information_gaussian_mean(Sigma)`: Fisher for the mean parameter.
29
+ `natural_gradient(grad, fisher_matrix)`: F^{-1} · grad.
30
+
31
+ Each primitive takes a leading batch dimension and cites its source.
32
+
33
+ References:
34
+ Amari, S.-I. (1998). Natural gradient works efficiently in learning.
35
+ Neural Computation 10(2):251–276.
36
+ Amari, S.-I. (2016). Information Geometry and Its Applications.
37
+ Applied Mathematical Sciences 194, Springer. The textbook.
38
+ Banerjee, A., Merugu, S., Dhillon, I. S., Ghosh, J. (2005).
39
+ Clustering with Bregman divergences. JMLR 6:1705–1749.
40
+ Martens, J. (2020). New insights and perspectives on the natural
41
+ gradient method. JMLR 21(146):1–76.
42
+ Nielsen, F. (2020). An elementary introduction to information
43
+ geometry. Entropy 22(10):1100.
44
+ """
45
+
46
+ from holonomy_lib.info_geometry.divergences import (
47
+ bregman_divergence,
48
+ kl_divergence_categorical,
49
+ kl_divergence_gaussian,
50
+ )
51
+ from holonomy_lib.info_geometry.fisher import (
52
+ fisher_information_categorical,
53
+ fisher_information_gaussian_mean,
54
+ natural_gradient,
55
+ )
56
+
57
+ __all__ = [
58
+ "bregman_divergence",
59
+ "fisher_information_categorical",
60
+ "fisher_information_gaussian_mean",
61
+ "kl_divergence_categorical",
62
+ "kl_divergence_gaussian",
63
+ "natural_gradient",
64
+ ]
@@ -0,0 +1,224 @@
1
+ # SPDX-License-Identifier: FSL-1.1-ALv2
2
+ # Copyright 2026 Synoros
3
+
4
+ """Bregman and KL divergences, batched over leading dimensions on any device.
5
+
6
+ A Bregman divergence is the asymmetric "distance" induced by a strictly
7
+ convex generator F : Ω ⊂ ℝᵈ → ℝ:
8
+
9
+ D_F(p ‖ q) = F(p) − F(q) − ⟨∇F(q), p − q⟩.
10
+
11
+ It is the gap between the function value at `p` and its first-order
12
+ Taylor approximation at `q`. Choices of `F` recover familiar
13
+ divergences:
14
+
15
+ F(x) = ½ ⟨x, x⟩ → squared Euclidean ½‖p−q‖²
16
+ F(x) = Σ x_i log x_i − x_i → generalized KL (Csiszár 1991)
17
+ (= KL on the simplex)
18
+ F(x) = − Σ log x_i → Itakura-Saito
19
+ F(X) = −log det X (for SPD matrices) → LogDet / Burg matrix divergence
20
+
21
+ This module provides:
22
+
23
+ bregman_divergence(p, q, potential)
24
+ The fully general form, parameterized by a callable `potential`
25
+ that returns (F(x), ∇F(x)) given x. Caller supplies the convex
26
+ function; we evaluate the Bregman formula and respect the leading
27
+ batch dimensions of `p`/`q`.
28
+
29
+ kl_divergence_categorical(p, q)
30
+ KL(p ‖ q) for discrete distributions on the simplex, with the
31
+ 0·log 0 = 0 convention. On the simplex this equals `bregman_divergence`
32
+ with `potential = (x · log x − x).sum()`, and it is cheaper. The
33
+ Bregman form also adds Σq − Σp, which vanishes only when both sum to one.
34
+
35
+ Autograd note: `xlogy` preserves zero-mass terms and returns
36
+ `+inf` when `p_i > 0` and `q_i = 0` (support mismatch).
37
+ Gradients through near-zero bins are not numerically reliable;
38
+ use only for inference unless you're sure about the support.
39
+
40
+ kl_divergence_gaussian(mu_p, Sigma_p, mu_q, Sigma_q)
41
+ KL between two multivariate Gaussians:
42
+
43
+ KL = ½ ( tr(Σ_q⁻¹ Σ_p) + (μ_q − μ_p)ᵀ Σ_q⁻¹ (μ_q − μ_p)
44
+ − d + log det Σ_q − log det Σ_p ).
45
+
46
+ Uses Cholesky-based solves for stability.
47
+
48
+ References:
49
+ Bregman, L. M. (1967). The relaxation method of finding the common
50
+ point of convex sets ... USSR Comp. Math. Math. Phys. 7(3):200-217.
51
+ The original definition.
52
+ Banerjee, A., Merugu, S., Dhillon, I. S., Ghosh, J. (2005).
53
+ Clustering with Bregman divergences. JMLR 6:1705-1749, §2.
54
+ Csiszár, I. (1991). Why least squares and maximum entropy?
55
+ An axiomatic approach to inference for linear inverse problems.
56
+ Ann. Stat. 19(4):2032-2066. Bregman ↔ generalized KL connection.
57
+ Amari, S.-I. (2016). Information Geometry and Its Applications, §1.5.
58
+ """
59
+
60
+ from __future__ import annotations
61
+
62
+ from collections.abc import Callable
63
+
64
+ import torch
65
+
66
+
67
+ def bregman_divergence(
68
+ p: torch.Tensor,
69
+ q: torch.Tensor,
70
+ potential: Callable[[torch.Tensor], tuple[torch.Tensor, torch.Tensor]],
71
+ ) -> torch.Tensor:
72
+ """Bregman divergence `D_F(p ‖ q) = F(p) − F(q) − ⟨∇F(q), p − q⟩`.
73
+
74
+ Args:
75
+ p, q: (B, d) tensors. Must broadcast against each other; output
76
+ has the broadcast batch shape.
77
+ potential: callable taking `x: (..., d)` and returning
78
+ `(F(x): (...,), grad_F(x): (..., d))`. Both outputs must
79
+ preserve the leading dims of the input. The caller is
80
+ responsible for choosing a strictly convex `F` and confirming
81
+ `p`, `q` live in its domain.
82
+
83
+ Returns:
84
+ D_F(p ‖ q): (B,) divergence values. Non-negative when F is
85
+ strictly convex (Bregman 1967); equals zero iff p = q.
86
+
87
+ Example (squared Euclidean):
88
+ >>> def half_norm_sq(x):
89
+ ... return 0.5 * (x * x).sum(dim=-1), x
90
+ >>> bregman_divergence(p, q, half_norm_sq)
91
+ # = ½ ‖p − q‖²
92
+
93
+ References:
94
+ Bregman (1967).
95
+ Banerjee et al. (2005), §2.
96
+ """
97
+ if p.shape[-1] != q.shape[-1]:
98
+ raise ValueError(
99
+ f"p and q must have matching last dim; got p.shape[-1]="
100
+ f"{p.shape[-1]} vs q.shape[-1]={q.shape[-1]}"
101
+ )
102
+ # Only the gradient at q enters the formula, so the potential's gradient
103
+ # at p is discarded.
104
+ F_p, _ = potential(p)
105
+ F_q, grad_F_q = potential(q)
106
+ # ⟨∇F(q), p − q⟩, summed over the feature dim so it has the batch shape.
107
+ inner = (grad_F_q * (p - q)).sum(dim=-1)
108
+ return F_p - F_q - inner
109
+
110
+
111
+ def kl_divergence_categorical(
112
+ p: torch.Tensor,
113
+ q: torch.Tensor,
114
+ ) -> torch.Tensor:
115
+ """KL divergence between two discrete distributions on the simplex.
116
+
117
+ KL(p ‖ q) = Σ_i p_i · (log p_i − log q_i).
118
+
119
+ Uses the convention 0 · log(0/x) = 0 (continuous limit). Both `p`
120
+ and `q` must be on the simplex (non-negative, sum to 1 along the
121
+ last dim); we do not enforce this, so it is a precondition.
122
+ Gradients at zero probabilities need not be finite; differentiate
123
+ only where both distributions have strictly positive entries.
124
+
125
+ Args:
126
+ p, q: (B, k) probability tensors. Last dim is the support size.
127
+
128
+ Returns:
129
+ KL: (B,) divergence values, non-negative (Gibbs inequality).
130
+
131
+ References:
132
+ Cover, T. M., Thomas, J. A. (2006). Elements of Information
133
+ Theory, 2nd ed., §2.3.
134
+ Amari (2016), §2.4.
135
+ """
136
+ if p.shape[-1] != q.shape[-1]:
137
+ raise ValueError(
138
+ f"p and q must have matching last dim; got {p.shape[-1]} vs "
139
+ f"{q.shape[-1]}"
140
+ )
141
+ # p log(p / q) is split into two xlogy terms. xlogy(p, x) is 0 wherever
142
+ # p is 0, so zero-mass bins drop out without a 0 * log 0 nan. No clamp
143
+ # is applied, so tiny positive p keep their exact p log p. A bin with
144
+ # p_i > 0 and q_i = 0 gives +inf, as the module note says.
145
+ return (torch.special.xlogy(p, p) - torch.special.xlogy(p, q)).sum(dim=-1)
146
+
147
+
148
+ def kl_divergence_gaussian(
149
+ mu_p: torch.Tensor,
150
+ Sigma_p: torch.Tensor,
151
+ mu_q: torch.Tensor,
152
+ Sigma_q: torch.Tensor,
153
+ ) -> torch.Tensor:
154
+ """KL divergence between two multivariate Gaussians.
155
+
156
+ KL(N(μ_p, Σ_p) ‖ N(μ_q, Σ_q))
157
+ = ½ ( tr(Σ_q⁻¹ Σ_p)
158
+ + (μ_q − μ_p)ᵀ Σ_q⁻¹ (μ_q − μ_p)
159
+ − d
160
+ + log det Σ_q − log det Σ_p ).
161
+
162
+ Stability: we factorize Σ_q via Cholesky once and reuse the
163
+ factorization for both the trace term `tr(Σ_q⁻¹ Σ_p)` and the
164
+ Mahalanobis term `(μ_q − μ_p)ᵀ Σ_q⁻¹ (μ_q − μ_p)`. `log det Σ_q`
165
+ comes for free from the Cholesky diagonal. For `Σ_p` we just need
166
+ `log det Σ_p`, also via Cholesky. A covariance that is not SPD makes
167
+ `torch.linalg.cholesky` raise, so bad input fails instead of returning
168
+ a number.
169
+
170
+ Args:
171
+ mu_p, mu_q: (..., d) means.
172
+ Sigma_p, Sigma_q: (..., d, d) covariances. Must be SPD.
173
+
174
+ Returns:
175
+ KL: (...,) divergence values, non-negative.
176
+
177
+ References:
178
+ Amari (2016), §2.4; exponential families.
179
+ Petersen, K. B., Pedersen, M. S. (2012). The Matrix Cookbook,
180
+ eq. 380; Gaussian-Gaussian KL.
181
+ """
182
+ d = mu_p.shape[-1]
183
+ if mu_q.shape[-1] != d:
184
+ raise ValueError(
185
+ f"mu_q's last dim must match mu_p's d={d}; "
186
+ f"got mu_q.shape={tuple(mu_q.shape)}"
187
+ )
188
+ if Sigma_p.shape[-1] != d or Sigma_p.shape[-2] != d:
189
+ raise ValueError(
190
+ f"Sigma_p must be (..., d, d) with d={d}; "
191
+ f"got {tuple(Sigma_p.shape)}"
192
+ )
193
+ if Sigma_q.shape[-1] != d or Sigma_q.shape[-2] != d:
194
+ raise ValueError(
195
+ f"Sigma_q must be (..., d, d) with d={d}; "
196
+ f"got {tuple(Sigma_q.shape)}"
197
+ )
198
+
199
+ L_q = torch.linalg.cholesky(Sigma_q)
200
+ # Σ_q⁻¹ Σ_p through the Cholesky factor of Σ_q: two triangular solves,
201
+ # so no explicit inverse of Σ_q is formed.
202
+ Sigma_q_inv_Sigma_p = torch.cholesky_solve(Sigma_p, L_q)
203
+ trace_term = torch.diagonal(
204
+ Sigma_q_inv_Sigma_p, dim1=-2, dim2=-1,
205
+ ).sum(dim=-1)
206
+
207
+ diff = (mu_q - mu_p).unsqueeze(dim=-1) # (..., d, 1)
208
+ # Σ_q⁻¹ Δμ via cholesky_solve; then ⟨Δμ, ·⟩ is a dot product.
209
+ Sigma_q_inv_diff = torch.cholesky_solve(diff, L_q).squeeze(dim=-1)
210
+ mahalanobis = ((mu_q - mu_p) * Sigma_q_inv_diff).sum(dim=-1)
211
+
212
+ # log det Σ = 2 Σ log diag(L) for the Cholesky factor.
213
+ log_det_Sigma_q = 2.0 * torch.log(
214
+ torch.diagonal(L_q, dim1=-2, dim2=-1)
215
+ ).sum(dim=-1)
216
+ L_p = torch.linalg.cholesky(Sigma_p)
217
+ log_det_Sigma_p = 2.0 * torch.log(
218
+ torch.diagonal(L_p, dim1=-2, dim2=-1)
219
+ ).sum(dim=-1)
220
+
221
+ return 0.5 * (
222
+ trace_term + mahalanobis - float(d)
223
+ + log_det_Sigma_q - log_det_Sigma_p
224
+ )
@@ -0,0 +1,179 @@
1
+ # SPDX-License-Identifier: FSL-1.1-ALv2
2
+ # Copyright 2026 Synoros
3
+
4
+ """Fisher information metric + natural-gradient primitives.
5
+
6
+ The Fisher information matrix is the canonical Riemannian metric on a
7
+ parametric statistical manifold:
8
+
9
+ g_{ij}(θ) = E_{p_θ}[ (∂_i log p_θ)(∂_j log p_θ) ]
10
+ = − E_{p_θ}[ ∂_i ∂_j log p_θ ].
11
+
12
+ It is the second-order Taylor coefficient of KL near the diagonal:
13
+
14
+ KL(p_θ ‖ p_{θ + dθ}) = ½ dθᵀ F(θ) dθ + O(‖dθ‖³).
15
+
16
+ Information geometry studies the geodesics of this metric. In
17
+ optimization, the natural gradient `F(θ)^{−1} ∇L` is the steepest-descent
18
+ direction under this metric. The metric does not depend on how θ is
19
+ parameterized, so the direction does not either (Amari 1998).
20
+
21
+ Two closed-form Fisher matrices are exposed for the most common
22
+ families; users with a custom likelihood can compute their own Fisher
23
+ either analytically or via `torch.autograd` over `log p`.
24
+
25
+ fisher_information_categorical(p)
26
+ Diagonal Fisher on the simplex, g_ii(p) = 1/p_i. Recovers the
27
+ Hellinger-Bhattacharyya geometry whose geodesic distance is twice
28
+ the Hellinger angle.
29
+
30
+ fisher_information_gaussian_mean(Sigma)
31
+ The Fisher information for the mean of N(μ, Σ) at fixed Σ. The
32
+ closed form is g(μ) = Σ^{−1}, which makes the Fisher-metric
33
+ geodesics between two means coincide with the Mahalanobis-distance
34
+ straight lines for that fixed Σ.
35
+
36
+ natural_gradient(grad, fisher_matrix)
37
+ Apply the Fisher inverse to a Euclidean gradient via a Cholesky
38
+ solve (no explicit inverse). The output is the steepest-descent
39
+ direction under the Fisher metric.
40
+
41
+ References:
42
+ Amari, S.-I. (1998). Natural gradient works efficiently in learning.
43
+ Neural Computation 10(2):251–276. The natural-gradient paper.
44
+ Amari, S.-I. (2016). Information Geometry and Its Applications.
45
+ Applied Mathematical Sciences 194, Springer. §2.5, §4.
46
+ Martens, J. (2020). New insights and perspectives on the natural
47
+ gradient method. JMLR 21(146):1–76. Modern revisit.
48
+ Petersen, K. B., Pedersen, M. S. (2012). The Matrix Cookbook,
49
+ §6 (Statistics); Fisher information for the Gaussian family.
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ import torch
55
+
56
+
57
+ def fisher_information_categorical(p: torch.Tensor) -> torch.Tensor:
58
+ """Fisher information metric on the simplex Δ^{k−1}.
59
+
60
+ For p in the open simplex, the Fisher information metric is the
61
+ diagonal matrix
62
+
63
+ g_ij(p) = δ_ij / p_i.
64
+
65
+ This is the metric induced by the KL divergence's second-order
66
+ Taylor expansion: KL(p ‖ p + δ) ≈ ½ Σ_i δ_i² / p_i.
67
+
68
+ Args:
69
+ p: (B, k) probability vectors on the open simplex. Entries should
70
+ be positive. Metric coefficients diverge as p_i approaches zero,
71
+ but the boundary is at finite Fisher distance. For example, the
72
+ Bernoulli distance from p=1/2 to p=0 is pi/2.
73
+ Entries below `1e-9` are clamped up to `1e-9` in the input dtype
74
+ (float16 is a special case, see the body).
75
+
76
+ Returns:
77
+ Fisher matrix: (B, k, k) diagonal SPD.
78
+
79
+ References:
80
+ Amari (2016), §2.4 example 4.
81
+ Nielsen, F. (2020). An elementary introduction to information
82
+ geometry. Entropy 22(10):1100.
83
+ """
84
+ if p.ndim < 1:
85
+ raise ValueError(f"p must have at least 1 dim; got p.shape={tuple(p.shape)}")
86
+ # The clamp is what keeps 1/p finite at the boundary. The floor is the
87
+ # library-wide convention `numerical_floor_convention` in
88
+ # notes/magic_numbers.md, not a per-call knob. Keeping p in the interior
89
+ # is the caller's job; a clamped entry gives a large but finite value.
90
+ # float16 cannot represent 1e-9 (it rounds to 0), so the floor is raised
91
+ # to the dtype's smallest normal number when that is larger. Every other
92
+ # float dtype keeps the 1e-9 convention.
93
+ p_safe = p.clamp(min=max(1e-9, torch.finfo(p.dtype).tiny))
94
+ inv_p = torch.reciprocal(p_safe)
95
+ return torch.diag_embed(inv_p)
96
+
97
+
98
+ def fisher_information_gaussian_mean(Sigma: torch.Tensor) -> torch.Tensor:
99
+ """Fisher information for the mean μ of N(μ, Σ), at fixed Σ.
100
+
101
+ Standard result (Amari 2016, eq. 2.43): for the Gaussian family
102
+ parameterized by the mean with covariance held fixed,
103
+
104
+ g(μ) = Σ^{−1}.
105
+
106
+ The Fisher-geodesic distance between two means μ_1, μ_2 is then
107
+ `sqrt((μ_1 − μ_2)ᵀ Σ^{−1} (μ_1 − μ_2))`, i.e. the Mahalanobis
108
+ distance for the given Σ.
109
+
110
+ Computed from a Cholesky factor with `cholesky_inverse`, not
111
+ `linalg.inv`. A matrix that is not SPD makes `cholesky` raise.
112
+
113
+ Args:
114
+ Sigma: (B, d, d) SPD covariance matrices.
115
+
116
+ Returns:
117
+ Fisher matrix: (B, d, d) SPD.
118
+
119
+ References:
120
+ Amari (2016), §2.4 example 5.
121
+ Petersen-Pedersen (2012), §6.
122
+ """
123
+ if Sigma.ndim < 2 or Sigma.shape[-1] != Sigma.shape[-2]:
124
+ raise ValueError(
125
+ f"Sigma must be (..., d, d); got Sigma.shape={tuple(Sigma.shape)}"
126
+ )
127
+ # Σ is SPD, so factor it once and invert from the factor. This skips
128
+ # the pivoting an LU inverse does, and kl_divergence_gaussian uses the
129
+ # same factorization.
130
+ L = torch.linalg.cholesky(Sigma)
131
+ return torch.cholesky_inverse(L)
132
+
133
+
134
+ def natural_gradient(
135
+ grad: torch.Tensor,
136
+ fisher_matrix: torch.Tensor,
137
+ ) -> torch.Tensor:
138
+ """Natural gradient: F(θ)^{−1} · ∇L.
139
+
140
+ The natural gradient (Amari 1998) is the steepest-descent direction
141
+ on a statistical manifold under the Fisher metric. Unlike the
142
+ Euclidean gradient, the direction does not change under a smooth
143
+ reparameterization of θ, since the metric is the same geometric
144
+ object in every coordinate system. That removes one source of
145
+ parameterization-dependent behavior. It does not make an
146
+ ill-conditioned F any better conditioned.
147
+
148
+ Implementation: solve `F · d = grad` through a Cholesky factor of F,
149
+ rather than forming `inv(F) @ grad`. The solve inherits whatever
150
+ conditioning F has, without the extra rounding from an explicit inverse.
151
+
152
+ Args:
153
+ grad: (B, d) Euclidean gradient at θ.
154
+ fisher_matrix: (B, d, d) Fisher information matrix at θ, SPD.
155
+
156
+ Returns:
157
+ (B, d) natural gradient.
158
+
159
+ References:
160
+ Amari (1998).
161
+ Martens (2020), §2; modern derivation.
162
+ """
163
+ if fisher_matrix.ndim < 2:
164
+ raise ValueError(
165
+ f"fisher_matrix must be (..., d, d) with at least 2 dims; "
166
+ f"got fisher_matrix.shape={tuple(fisher_matrix.shape)}"
167
+ )
168
+ if grad.shape[-1] != fisher_matrix.shape[-1]:
169
+ raise ValueError(
170
+ f"grad last dim {grad.shape[-1]} must match fisher_matrix "
171
+ f"last dim {fisher_matrix.shape[-1]}"
172
+ )
173
+ if fisher_matrix.shape[-1] != fisher_matrix.shape[-2]:
174
+ raise ValueError(
175
+ f"fisher_matrix must be (..., d, d); got "
176
+ f"fisher_matrix.shape={tuple(fisher_matrix.shape)}"
177
+ )
178
+ L = torch.linalg.cholesky(fisher_matrix)
179
+ return torch.cholesky_solve(grad.unsqueeze(dim=-1), L).squeeze(dim=-1)
@@ -0,0 +1,54 @@
1
+ # SPDX-License-Identifier: FSL-1.1-ALv2
2
+ # Copyright 2026 Synoros
3
+
4
+ """holonomy_lib.lie : Lie group primitives for SO(3) and the 2-sphere.
5
+
6
+ The Lie group `SO(3)` is the group of orientation-preserving
7
+ rotations of R³; its Lie algebra `so(3)` is the 3-dim space of
8
+ skew-symmetric matrices (equivalently, angular-velocity vectors).
9
+ This module ships the standard exp/log/random/compose primitives
10
+ plus real spherical harmonics, the standard basis for functions on the
11
+ unit sphere that rotate in a known way under SO(3).
12
+
13
+ Currently exposed:
14
+ so3.axis_angle_to_matrix(axis, angle) : Rodrigues formula.
15
+ so3.matrix_to_axis_angle(R) : inverse log map.
16
+ so3.so3_exp(omega) : matrix exp on so(3).
17
+ so3.so3_log(R) : matrix log on SO(3).
18
+ so3.random_so3(batch_size, generator) : Haar-uniform via
19
+ quaternion sampling.
20
+ so3.compose(R1, R2) : group product (matmul).
21
+ real_spherical_harmonics(directions, l_max)
22
+ : closed-form Y_lm for
23
+ l ≤ 4 evaluated at
24
+ (B, 3) direction vectors
25
+ (normalized internally).
26
+
27
+ Planned, not implemented yet:
28
+ Wigner-D matrices (real basis) for higher-l rotation actions on
29
+ spherical-harmonic features.
30
+ Clebsch-Gordan coefficients for SO(3) tensor products (l₁ ⊗ l₂ → L).
31
+ SE(3), SU(2), SL(n) primitives.
32
+
33
+ References:
34
+ Hall, B. C. (2015). Lie Groups, Lie Algebras, and Representations,
35
+ 2nd ed. Springer GTM 222. The canonical reference for matrix Lie
36
+ groups; §3.1 covers SO(3) and its Lie algebra.
37
+ Edmonds, A. R. (1957). Angular Momentum in Quantum Mechanics.
38
+ Princeton University Press. Real and complex spherical harmonics
39
+ + Wigner D-matrices.
40
+ Shoemake, K. (1992). Uniform random rotations. In Graphics Gems
41
+ III, pp. 124–132. Academic Press. The quaternion-from-3-uniforms
42
+ construction used by `random_so3`.
43
+ Cohen, T. S., Geiger, M., Köhler, J., Welling, M. (2018).
44
+ Spherical CNNs. ICLR. Modern application of these primitives in
45
+ SO(3)-equivariant deep learning.
46
+ """
47
+
48
+ from holonomy_lib.lie import so3
49
+ from holonomy_lib.lie.spherical_harmonics import real_spherical_harmonics
50
+
51
+ __all__ = [
52
+ "real_spherical_harmonics",
53
+ "so3",
54
+ ]