cholops 0.1.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.
- cholops/__init__.py +28 -0
- cholops/factors/__init__.py +6 -0
- cholops/factors/_consumers.py +28 -0
- cholops/factors/cholesky.py +133 -0
- cholops/factors/permuted.py +182 -0
- cholops/py.typed +0 -0
- cholops/tensors/__init__.py +25 -0
- cholops/tensors/_autodiff.py +78 -0
- cholops/tensors/_indexing.py +51 -0
- cholops/tensors/_validation.py +55 -0
- cholops/tensors/append.py +30 -0
- cholops/tensors/deletion.py +110 -0
- cholops/tensors/factorization.py +57 -0
- cholops/tensors/rank_one.py +199 -0
- cholops/tensors/replacement.py +212 -0
- cholops-0.1.0.dist-info/METADATA +149 -0
- cholops-0.1.0.dist-info/RECORD +19 -0
- cholops-0.1.0.dist-info/WHEEL +4 -0
- cholops-0.1.0.dist-info/licenses/LICENSE +21 -0
cholops/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Dense Cholesky modifications using ordinary JAX operations."""
|
|
2
|
+
|
|
3
|
+
from .factors import Cholesky, PermutedCholesky
|
|
4
|
+
from .tensors import (
|
|
5
|
+
chol_append,
|
|
6
|
+
chol_delete,
|
|
7
|
+
chol_delete_append,
|
|
8
|
+
chol_diag_update,
|
|
9
|
+
chol_rank1,
|
|
10
|
+
chol_rank1_positive,
|
|
11
|
+
chol_replace,
|
|
12
|
+
chol_replace_last,
|
|
13
|
+
cholesky,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Cholesky",
|
|
18
|
+
"PermutedCholesky",
|
|
19
|
+
"cholesky",
|
|
20
|
+
"chol_append",
|
|
21
|
+
"chol_delete",
|
|
22
|
+
"chol_diag_update",
|
|
23
|
+
"chol_rank1",
|
|
24
|
+
"chol_rank1_positive",
|
|
25
|
+
"chol_delete_append",
|
|
26
|
+
"chol_replace",
|
|
27
|
+
"chol_replace_last",
|
|
28
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Solves, log determinants and quadratic forms shared by the factor classes."""
|
|
2
|
+
|
|
3
|
+
import jax.numpy as jnp
|
|
4
|
+
from jax import Array
|
|
5
|
+
from jax.scipy.linalg import solve_triangular
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _rhs(L: Array, rhs: Array) -> Array:
|
|
9
|
+
rhs = jnp.asarray(rhs)
|
|
10
|
+
if rhs.ndim not in (1, 2) or rhs.shape[0] != L.shape[0]:
|
|
11
|
+
raise ValueError("rhs must have shape (n,) or (n,r)")
|
|
12
|
+
if rhs.dtype != L.dtype:
|
|
13
|
+
raise TypeError("rhs and the factor must have the same dtype")
|
|
14
|
+
return rhs
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _solve(L: Array, rhs: Array) -> Array:
|
|
18
|
+
z = solve_triangular(L, rhs, lower=True)
|
|
19
|
+
return solve_triangular(L, z, lower=True, trans="T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _logdet(L: Array) -> Array:
|
|
23
|
+
return 2 * jnp.sum(jnp.log(jnp.diag(L)))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _quadratic(L: Array, rhs: Array) -> Array:
|
|
27
|
+
z = solve_triangular(L, rhs, lower=True)
|
|
28
|
+
return jnp.sum(z * z, axis=0)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Cholesky factor objects using the tensor operations."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
import jax.numpy as jnp
|
|
7
|
+
from jax import Array
|
|
8
|
+
|
|
9
|
+
from ..tensors import (
|
|
10
|
+
chol_append,
|
|
11
|
+
chol_delete,
|
|
12
|
+
chol_delete_append,
|
|
13
|
+
chol_diag_update,
|
|
14
|
+
chol_rank1,
|
|
15
|
+
chol_rank1_positive,
|
|
16
|
+
chol_replace,
|
|
17
|
+
chol_replace_last,
|
|
18
|
+
cholesky,
|
|
19
|
+
)
|
|
20
|
+
from ..tensors._validation import _factor
|
|
21
|
+
from ._consumers import _logdet, _quadratic, _rhs, _solve
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@jax.tree_util.register_dataclass
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Cholesky:
|
|
27
|
+
"""Canonical lower factor, stored as one dynamic PyTree leaf ``factor``.
|
|
28
|
+
|
|
29
|
+
Construct with from_matrix or from_factor. All inputs and outputs follow
|
|
30
|
+
the matrix's row and column order. Floating arrays share a real float32 or
|
|
31
|
+
float64 dtype. The represented matrix must remain positive definite.
|
|
32
|
+
Modifications return new objects, leaving the original unchanged.
|
|
33
|
+
Supports jit, vmap and first-order differentiation.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
factor: Array
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_matrix(cls, matrix: Array) -> "Cholesky":
|
|
40
|
+
"""Factor real (n,n) input with the symmetry-preserving reverse rule.
|
|
41
|
+
|
|
42
|
+
The symmetric part must be positive definite. Batch with vmap.
|
|
43
|
+
"""
|
|
44
|
+
matrix = jnp.asarray(matrix)
|
|
45
|
+
if matrix.ndim != 2:
|
|
46
|
+
raise ValueError("matrix must be unbatched; batch with jax.vmap")
|
|
47
|
+
return cls(cholesky(matrix))
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_factor(cls, factor: Array) -> "Cholesky":
|
|
51
|
+
"""Wrap a real (n,n) lower factor, ignoring its unused upper triangle.
|
|
52
|
+
|
|
53
|
+
Does not change the derivative of the function that produced factor.
|
|
54
|
+
Supply a valid factor with a positive diagonal. Empty factors are supported.
|
|
55
|
+
"""
|
|
56
|
+
return cls(_factor(factor))
|
|
57
|
+
|
|
58
|
+
def solve(self, rhs: Array) -> Array:
|
|
59
|
+
"""Solve the covariance system for a canonical (n,) or (n,r) RHS."""
|
|
60
|
+
return _solve(self.factor, _rhs(self.factor, rhs))
|
|
61
|
+
|
|
62
|
+
def logdet(self) -> Array:
|
|
63
|
+
"""Scalar log determinant of the represented covariance."""
|
|
64
|
+
return _logdet(self.factor)
|
|
65
|
+
|
|
66
|
+
def quadratic(self, rhs: Array) -> Array:
|
|
67
|
+
"""Inverse quadratic form: scalar for (n,), per column for (n,r)."""
|
|
68
|
+
return _quadratic(self.factor, _rhs(self.factor, rhs))
|
|
69
|
+
|
|
70
|
+
def append(self, k: Array, kappa: Array | float) -> "Cholesky":
|
|
71
|
+
"""Append covariances k:(n,) and diagonal kappa; return size n+1.
|
|
72
|
+
|
|
73
|
+
The target covariance must be positive definite.
|
|
74
|
+
"""
|
|
75
|
+
return type(self)(chol_append(self.factor, k, kappa))
|
|
76
|
+
|
|
77
|
+
def delete(self, index: Array | int) -> "Cholesky":
|
|
78
|
+
"""Delete one variable, preserving survivor order; return size n-1.
|
|
79
|
+
|
|
80
|
+
index must be in [0,n). Check traced indices before calling this method,
|
|
81
|
+
since they are not checked at runtime.
|
|
82
|
+
"""
|
|
83
|
+
return type(self)(chol_delete(self.factor, index))
|
|
84
|
+
|
|
85
|
+
def replace(self, index: Array | int, k: Array, kappa: Array | float) -> "Cholesky":
|
|
86
|
+
"""Replace at index, preserving every other index and the (n,n) shape.
|
|
87
|
+
|
|
88
|
+
k:(n-1,) follows survivor order. kappa is the new diagonal.
|
|
89
|
+
The target must be positive definite and index must be in [0,n).
|
|
90
|
+
Traced indices are not checked at runtime.
|
|
91
|
+
"""
|
|
92
|
+
return type(self)(chol_replace(self.factor, index, k, kappa))
|
|
93
|
+
|
|
94
|
+
def delete_append(
|
|
95
|
+
self, index: Array | int, k: Array, kappa: Array | float
|
|
96
|
+
) -> "Cholesky":
|
|
97
|
+
"""Delete index and append its replacement at the end, keeping size n.
|
|
98
|
+
|
|
99
|
+
k:(n-1,) follows survivor order. kappa is the new diagonal.
|
|
100
|
+
The returned factor is ordered as survivors followed by the replacement.
|
|
101
|
+
The target must be positive definite and index must be in [0,n).
|
|
102
|
+
Traced indices are not checked at runtime.
|
|
103
|
+
"""
|
|
104
|
+
return type(self)(chol_delete_append(self.factor, index, k, kappa))
|
|
105
|
+
|
|
106
|
+
def replace_last(self, K: Array, C: Array) -> "Cholesky":
|
|
107
|
+
"""Replace the last r variables with K:(n-r,r), C:(r,r), keeping size n.
|
|
108
|
+
|
|
109
|
+
Uses a Schur complement with native JAX Cholesky differentiation.
|
|
110
|
+
C must be symmetric and the target must be positive definite.
|
|
111
|
+
r is static, inferred from C; r=0 and r=n are supported.
|
|
112
|
+
"""
|
|
113
|
+
return type(self)(chol_replace_last(self.factor, K, C))
|
|
114
|
+
|
|
115
|
+
def diag_update(self, index: Array | int, delta: Array | float) -> "Cholesky":
|
|
116
|
+
"""Add signed delta to covariance diagonal index, preserving order.
|
|
117
|
+
|
|
118
|
+
The derivative with respect to delta is retained at delta=0.
|
|
119
|
+
The target must be positive definite.
|
|
120
|
+
"""
|
|
121
|
+
return type(self)(chol_diag_update(self.factor, index, delta))
|
|
122
|
+
|
|
123
|
+
def rank1(self, u: Array, alpha: Array | float = 1.0) -> "Cholesky":
|
|
124
|
+
"""Add alpha * outer(u,u), u:(n,), preserving order and size.
|
|
125
|
+
|
|
126
|
+
alpha may be positive, negative or zero. The weight derivative is
|
|
127
|
+
retained at zero. The target must be positive definite.
|
|
128
|
+
"""
|
|
129
|
+
return type(self)(chol_rank1(self.factor, u, alpha))
|
|
130
|
+
|
|
131
|
+
def rank1_positive(self, u: Array) -> "Cholesky":
|
|
132
|
+
"""Add outer(u,u), u:(n,), using the optimized positive-update algorithm."""
|
|
133
|
+
return type(self)(chol_rank1_positive(self.factor, u))
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Cholesky factor objects that manage their internal variable ordering."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
import jax.numpy as jnp
|
|
7
|
+
from jax import Array
|
|
8
|
+
|
|
9
|
+
from ..tensors import (
|
|
10
|
+
chol_append,
|
|
11
|
+
chol_delete,
|
|
12
|
+
chol_delete_append,
|
|
13
|
+
chol_diag_update,
|
|
14
|
+
chol_rank1,
|
|
15
|
+
chol_rank1_positive,
|
|
16
|
+
cholesky,
|
|
17
|
+
)
|
|
18
|
+
from ..tensors._indexing import _keep
|
|
19
|
+
from ..tensors._validation import _factor, _index, _scalar, _vector
|
|
20
|
+
from ._consumers import _logdet, _quadratic, _rhs, _solve
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@jax.tree_util.register_dataclass
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class PermutedCholesky:
|
|
26
|
+
"""Physical lower factor and inverse permutations, all dynamic PyTree leaves.
|
|
27
|
+
|
|
28
|
+
order[physical] is logical; inverse_order[logical] is physical. Construct via
|
|
29
|
+
from_matrix or from_factor. All method inputs and outputs use logical order.
|
|
30
|
+
The stored factor is lower triangular in physical order. Floating arrays
|
|
31
|
+
share a real float32 or float64 dtype, and the represented matrix must remain
|
|
32
|
+
positive definite. Modifications return new objects.
|
|
33
|
+
Supports jit, vmap and first-order differentiation of continuous inputs.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
physical_factor: Array
|
|
37
|
+
order: Array
|
|
38
|
+
inverse_order: Array
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_matrix(cls, matrix: Array) -> "PermutedCholesky":
|
|
42
|
+
"""Factor a real (n,n) matrix with the symmetry-preserving reverse rule.
|
|
43
|
+
|
|
44
|
+
The symmetric part must be positive definite. Inputs use float32 or
|
|
45
|
+
float64. Batch with vmap.
|
|
46
|
+
"""
|
|
47
|
+
matrix = jnp.asarray(matrix)
|
|
48
|
+
if matrix.ndim != 2:
|
|
49
|
+
raise ValueError("matrix must be unbatched; batch with jax.vmap")
|
|
50
|
+
return cls.from_factor(cholesky(matrix))
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_factor(cls, factor: Array) -> "PermutedCholesky":
|
|
54
|
+
"""Wrap an existing (n,n) lower factor in identity logical order.
|
|
55
|
+
|
|
56
|
+
This does not change the derivative of the operation that produced
|
|
57
|
+
factor. Use from_matrix to apply cholops' Cholesky reverse rule.
|
|
58
|
+
"""
|
|
59
|
+
factor = _factor(factor)
|
|
60
|
+
order = jnp.arange(factor.shape[0])
|
|
61
|
+
return cls(factor, order, order)
|
|
62
|
+
|
|
63
|
+
def append(self, k: Array, kappa: Array | float) -> "PermutedCholesky":
|
|
64
|
+
"""Append a variable last in logical order, returning size n+1.
|
|
65
|
+
|
|
66
|
+
k:(n,) follows the existing logical order. kappa is the new diagonal.
|
|
67
|
+
"""
|
|
68
|
+
L = self.physical_factor
|
|
69
|
+
n = L.shape[0]
|
|
70
|
+
k = _vector(k, n, L, "k")
|
|
71
|
+
result = chol_append(L, k[self.order], kappa)
|
|
72
|
+
last = jnp.asarray([n], dtype=self.order.dtype)
|
|
73
|
+
return type(self)(
|
|
74
|
+
result,
|
|
75
|
+
jnp.concatenate((self.order, last)),
|
|
76
|
+
jnp.concatenate((self.inverse_order, last)),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def delete(self, index: Array | int) -> "PermutedCholesky":
|
|
80
|
+
"""Delete a logical variable, preserving survivor order and returning n-1.
|
|
81
|
+
|
|
82
|
+
Remaining logical indices are renumbered from zero.
|
|
83
|
+
"""
|
|
84
|
+
L = self.physical_factor
|
|
85
|
+
n = L.shape[0]
|
|
86
|
+
index = _index(index, n)
|
|
87
|
+
physical = self.inverse_order[index]
|
|
88
|
+
result = chol_delete(L, physical)
|
|
89
|
+
survivors = self.order[_keep(n, physical)]
|
|
90
|
+
order = survivors - (survivors > index)
|
|
91
|
+
inverse = self.inverse_order[_keep(n, index)]
|
|
92
|
+
inverse = inverse - (inverse > physical)
|
|
93
|
+
return type(self)(result, order, inverse)
|
|
94
|
+
|
|
95
|
+
def replace(
|
|
96
|
+
self, index: Array | int, k: Array, kappa: Array | float
|
|
97
|
+
) -> "PermutedCholesky":
|
|
98
|
+
"""Return a new factor after replacing logical slot index.
|
|
99
|
+
|
|
100
|
+
k has shape (n-1,) in logical order with index omitted; scalar kappa
|
|
101
|
+
is the new covariance diagonal. Typed arrays share the factor dtype.
|
|
102
|
+
The target must be positive definite and index must be in [0,n).
|
|
103
|
+
Traced indices are not checked at runtime.
|
|
104
|
+
Logical order and shape are preserved.
|
|
105
|
+
"""
|
|
106
|
+
L = self.physical_factor
|
|
107
|
+
n = L.shape[0]
|
|
108
|
+
index = _index(index, n)
|
|
109
|
+
k = _vector(k, n - 1, L, "k")
|
|
110
|
+
kappa = _scalar(kappa, L, "kappa")
|
|
111
|
+
physical = self.inverse_order[index]
|
|
112
|
+
keep = jnp.arange(n - 1) + (jnp.arange(n - 1) >= physical)
|
|
113
|
+
survivors = self.order[keep]
|
|
114
|
+
compact = survivors - (survivors > index)
|
|
115
|
+
result = chol_delete_append(L, physical, k[compact], kappa)
|
|
116
|
+
order = jnp.concatenate((survivors, index[None]))
|
|
117
|
+
inverse = self.inverse_order - (self.inverse_order > physical)
|
|
118
|
+
inverse = inverse.at[index].set(n - 1)
|
|
119
|
+
return type(self)(result, order, inverse)
|
|
120
|
+
|
|
121
|
+
def delete_append(
|
|
122
|
+
self, index: Array | int, k: Array, kappa: Array | float
|
|
123
|
+
) -> "PermutedCholesky":
|
|
124
|
+
"""Delete a logical variable and append its replacement last.
|
|
125
|
+
|
|
126
|
+
k:(n-1,) follows logical survivor order. kappa is the new diagonal.
|
|
127
|
+
Unlike replace, this changes logical order to survivors then replacement.
|
|
128
|
+
"""
|
|
129
|
+
L = self.physical_factor
|
|
130
|
+
n = L.shape[0]
|
|
131
|
+
index = _index(index, n)
|
|
132
|
+
k = _vector(k, n - 1, L, "k")
|
|
133
|
+
physical = self.inverse_order[index]
|
|
134
|
+
survivors = self.order[_keep(n, physical)]
|
|
135
|
+
compact = survivors - (survivors > index)
|
|
136
|
+
result = chol_delete_append(L, physical, k[compact], kappa)
|
|
137
|
+
last = jnp.asarray([n - 1], dtype=self.order.dtype)
|
|
138
|
+
order = jnp.concatenate((compact, last))
|
|
139
|
+
inverse = self.inverse_order[_keep(n, index)]
|
|
140
|
+
inverse = jnp.concatenate((inverse - (inverse > physical), last))
|
|
141
|
+
return type(self)(result, order, inverse)
|
|
142
|
+
|
|
143
|
+
def diag_update(
|
|
144
|
+
self, index: Array | int, delta: Array | float
|
|
145
|
+
) -> "PermutedCholesky":
|
|
146
|
+
"""Add signed delta to one logical covariance diagonal entry."""
|
|
147
|
+
index = _index(index, self.physical_factor.shape[0])
|
|
148
|
+
result = chol_diag_update(
|
|
149
|
+
self.physical_factor, self.inverse_order[index], delta
|
|
150
|
+
)
|
|
151
|
+
return type(self)(result, self.order, self.inverse_order)
|
|
152
|
+
|
|
153
|
+
def rank1(self, u: Array, alpha: Array | float = 1.0) -> "PermutedCholesky":
|
|
154
|
+
"""Add alpha * outer(u, u), with u:(n,) in logical order.
|
|
155
|
+
|
|
156
|
+
alpha may be positive, negative or zero. Logical ordering is unchanged.
|
|
157
|
+
"""
|
|
158
|
+
L = self.physical_factor
|
|
159
|
+
u = _vector(u, L.shape[0], L, "u")
|
|
160
|
+
result = chol_rank1(L, u[self.order], alpha)
|
|
161
|
+
return type(self)(result, self.order, self.inverse_order)
|
|
162
|
+
|
|
163
|
+
def rank1_positive(self, u: Array) -> "PermutedCholesky":
|
|
164
|
+
"""Add outer(u, u), with u:(n,) in logical order, using the fast update."""
|
|
165
|
+
L = self.physical_factor
|
|
166
|
+
u = _vector(u, L.shape[0], L, "u")
|
|
167
|
+
result = chol_rank1_positive(L, u[self.order])
|
|
168
|
+
return type(self)(result, self.order, self.inverse_order)
|
|
169
|
+
|
|
170
|
+
def solve(self, rhs: Array) -> Array:
|
|
171
|
+
"""Solve the covariance system for canonical vector or matrix RHS."""
|
|
172
|
+
rhs = _rhs(self.physical_factor, rhs)
|
|
173
|
+
return _solve(self.physical_factor, rhs[self.order])[self.inverse_order]
|
|
174
|
+
|
|
175
|
+
def logdet(self) -> Array:
|
|
176
|
+
"""Log determinant of the represented covariance."""
|
|
177
|
+
return _logdet(self.physical_factor)
|
|
178
|
+
|
|
179
|
+
def quadratic(self, rhs: Array) -> Array:
|
|
180
|
+
"""Covariance inverse quadratic form, separately per matrix RHS column."""
|
|
181
|
+
rhs = _rhs(self.physical_factor, rhs)
|
|
182
|
+
return _quadratic(self.physical_factor, rhs[self.order])
|
cholops/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Differentiable Cholesky operations on dense lower-triangular arrays.
|
|
2
|
+
|
|
3
|
+
Inputs use a common float32 or float64 dtype. Supply a valid lower factor and
|
|
4
|
+
choose modifications that keep the represented matrix positive definite.
|
|
5
|
+
Operations support JIT and vmap. Differentiation is supported only through
|
|
6
|
+
first order, in both forward and reverse mode.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .append import chol_append
|
|
10
|
+
from .deletion import chol_delete, chol_delete_append
|
|
11
|
+
from .factorization import cholesky
|
|
12
|
+
from .rank_one import chol_diag_update, chol_rank1, chol_rank1_positive
|
|
13
|
+
from .replacement import chol_replace, chol_replace_last
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"cholesky",
|
|
17
|
+
"chol_append",
|
|
18
|
+
"chol_delete",
|
|
19
|
+
"chol_diag_update",
|
|
20
|
+
"chol_rank1",
|
|
21
|
+
"chol_rank1_positive",
|
|
22
|
+
"chol_delete_append",
|
|
23
|
+
"chol_replace",
|
|
24
|
+
"chol_replace_last",
|
|
25
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Custom JAX rules for first-order differentiation.
|
|
2
|
+
|
|
3
|
+
Reuses each operation's reverse rule under JIT, batching and checkpointing.
|
|
4
|
+
Only first-order differentiation is supported.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import jax
|
|
8
|
+
import jax.numpy as jnp
|
|
9
|
+
from jax.experimental import hijax
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _TangentMap(hijax.HiPrim):
|
|
13
|
+
def __init__(self, pullback, residuals, tangents, out_aval, pushforward=None):
|
|
14
|
+
self.in_avals = jax.tree.map(jax.typeof, (residuals, tangents))
|
|
15
|
+
self.out_aval = out_aval
|
|
16
|
+
self.params = dict(
|
|
17
|
+
pullback=pullback,
|
|
18
|
+
pushforward=pushforward,
|
|
19
|
+
in_avals=self.in_avals,
|
|
20
|
+
out_aval=out_aval,
|
|
21
|
+
)
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
def expand(self, residuals, tangents):
|
|
25
|
+
if self.pushforward is not None:
|
|
26
|
+
return self.pushforward(residuals, tangents)
|
|
27
|
+
# Differentiating the linear pullback also handles masks introduced by
|
|
28
|
+
# batching runtime conditionals. Its primal at zero is discarded.
|
|
29
|
+
zero = jnp.zeros(self.out_aval.shape, self.out_aval.dtype)
|
|
30
|
+
_, pushforward = jax.vjp(lambda G: self.pullback(residuals, G), zero)
|
|
31
|
+
return pushforward(tangents)[0]
|
|
32
|
+
|
|
33
|
+
def transpose(self, cotangent, residuals, tangent_accums):
|
|
34
|
+
gradients = self.pullback(residuals, hijax.instantiate_zeros(cotangent))
|
|
35
|
+
|
|
36
|
+
def accumulate(accumulator, value):
|
|
37
|
+
if isinstance(accumulator, hijax.GradAccum):
|
|
38
|
+
accumulator.accum(value)
|
|
39
|
+
|
|
40
|
+
jax.tree.map(accumulate, tangent_accums, gradients)
|
|
41
|
+
|
|
42
|
+
def batch(self, axis_data, args, dims):
|
|
43
|
+
residuals, tangents = args
|
|
44
|
+
residual_dims, tangent_dims = dims
|
|
45
|
+
|
|
46
|
+
def pullback(residuals, G):
|
|
47
|
+
gradients = jax.vmap(
|
|
48
|
+
self.pullback, in_axes=(residual_dims, 0), axis_size=axis_data.size
|
|
49
|
+
)(residuals, G)
|
|
50
|
+
# A shared tangent is broadcast in the JVP, so its adjoint sums
|
|
51
|
+
# across the batch; mapped tangents retain their original axis.
|
|
52
|
+
return jax.tree.map(
|
|
53
|
+
lambda g, d: jnp.sum(g, axis=0) if d is None else jnp.moveaxis(g, 0, d),
|
|
54
|
+
gradients,
|
|
55
|
+
tangent_dims,
|
|
56
|
+
is_leaf=lambda x: x is None,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
out_aval = self.out_aval.update(shape=(axis_data.size, *self.out_aval.shape))
|
|
60
|
+
pushforward = None
|
|
61
|
+
if self.pushforward is not None:
|
|
62
|
+
|
|
63
|
+
def pushforward(residuals, tangents):
|
|
64
|
+
return jax.vmap(
|
|
65
|
+
self.pushforward,
|
|
66
|
+
in_axes=(residual_dims, tangent_dims),
|
|
67
|
+
axis_size=axis_data.size,
|
|
68
|
+
)(residuals, tangents)
|
|
69
|
+
|
|
70
|
+
operation = _TangentMap(pullback, residuals, tangents, out_aval, pushforward)
|
|
71
|
+
return operation(residuals, tangents), 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def tangent_map(pullback, residuals, tangents, output, pushforward=None):
|
|
75
|
+
"""Apply the transpose of a linear pullback, retaining its explicit adjoint."""
|
|
76
|
+
return _TangentMap(pullback, residuals, tangents, jax.typeof(output), pushforward)(
|
|
77
|
+
residuals, tangents
|
|
78
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Principal-submatrix indexing without quadratic Cartesian index tensors."""
|
|
2
|
+
|
|
3
|
+
import jax
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
|
|
6
|
+
from ._autodiff import tangent_map
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _keep(n, index):
|
|
10
|
+
positions = jnp.arange(n - 1)
|
|
11
|
+
return positions + (positions >= index)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _insert(matrix, index, row, column, diagonal):
|
|
15
|
+
"""Insert one row/column with linear-sized gather indices.
|
|
16
|
+
|
|
17
|
+
Used both for factor assembly and the transpose of principal extraction.
|
|
18
|
+
The inserted diagonal is supplied separately from the off-diagonal vectors.
|
|
19
|
+
"""
|
|
20
|
+
n = matrix.shape[0] + 1
|
|
21
|
+
if n == 1:
|
|
22
|
+
return jnp.reshape(diagonal, (1, 1))
|
|
23
|
+
positions = jnp.arange(n)
|
|
24
|
+
source = jnp.clip(positions - (positions > index), 0, n - 2)
|
|
25
|
+
result = jnp.take(jnp.take(matrix, source, axis=0), source, axis=1)
|
|
26
|
+
result = jnp.where((positions == index)[:, None], row[source][None, :], result)
|
|
27
|
+
result = jnp.where((positions == index)[None, :], column[source][:, None], result)
|
|
28
|
+
return jnp.where(
|
|
29
|
+
(positions == index)[:, None] & (positions == index)[None, :],
|
|
30
|
+
diagonal,
|
|
31
|
+
result,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@jax.custom_jvp
|
|
36
|
+
def _principal(matrix, index):
|
|
37
|
+
keep = _keep(matrix.shape[0], index)
|
|
38
|
+
return jnp.take(jnp.take(matrix, keep, axis=0), keep, axis=1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _principal_pullback(index, cotangent):
|
|
42
|
+
zeros = jnp.zeros((cotangent.shape[0],), dtype=cotangent.dtype)
|
|
43
|
+
return _insert(cotangent, index, zeros, zeros, jnp.zeros((), cotangent.dtype))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@_principal.defjvp
|
|
47
|
+
def _principal_jvp(primals, tangents):
|
|
48
|
+
matrix, index = primals
|
|
49
|
+
dmatrix, _ = tangents
|
|
50
|
+
result = _principal(matrix, index)
|
|
51
|
+
return result, tangent_map(_principal_pullback, index, dmatrix, result)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Shared shape, dtype and validity checks for tensor operations."""
|
|
2
|
+
|
|
3
|
+
import jax.numpy as jnp
|
|
4
|
+
from jax import Array
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _factor(L: Array) -> Array:
|
|
8
|
+
L = jnp.asarray(L)
|
|
9
|
+
if L.ndim != 2 or L.shape[0] != L.shape[1]:
|
|
10
|
+
raise ValueError("L must be a square, unbatched matrix; batch with jax.vmap")
|
|
11
|
+
if L.dtype not in (jnp.float32, jnp.float64):
|
|
12
|
+
raise TypeError("L must have a real float32 or float64 dtype")
|
|
13
|
+
return jnp.tril(L)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _vector(x: Array, n: int, L: Array, name: str) -> Array:
|
|
17
|
+
x = jnp.asarray(x)
|
|
18
|
+
if x.shape != (n,):
|
|
19
|
+
raise ValueError(f"{name} must have shape ({n},), got {x.shape}")
|
|
20
|
+
if x.dtype != L.dtype:
|
|
21
|
+
raise TypeError(f"{name} and L must have the same dtype")
|
|
22
|
+
return x
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _scalar(x: Array | float, L: Array, name: str) -> Array:
|
|
26
|
+
# Python scalars follow the factor precision; typed arrays are never cast.
|
|
27
|
+
x = jnp.asarray(x, dtype=L.dtype) if isinstance(x, (float, int)) else jnp.asarray(x)
|
|
28
|
+
if x.weak_type:
|
|
29
|
+
x = x.astype(L.dtype)
|
|
30
|
+
if x.shape != ():
|
|
31
|
+
raise ValueError(f"{name} must be a scalar")
|
|
32
|
+
if x.dtype != L.dtype:
|
|
33
|
+
raise TypeError(f"{name} and L must have the same dtype")
|
|
34
|
+
return x
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _index(index: Array | int, n: int) -> Array:
|
|
38
|
+
if n == 0:
|
|
39
|
+
raise ValueError("cannot index an empty factor")
|
|
40
|
+
if isinstance(index, int) and not 0 <= index < n:
|
|
41
|
+
raise ValueError(f"index must be in [0, {n})")
|
|
42
|
+
index = jnp.asarray(index)
|
|
43
|
+
if index.shape != () or not jnp.issubdtype(index.dtype, jnp.integer):
|
|
44
|
+
raise TypeError("index must be an integer scalar")
|
|
45
|
+
return index
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _valid_factor(L: Array) -> Array:
|
|
49
|
+
return jnp.all(jnp.isfinite(L)) & jnp.all(jnp.diag(L) > 0)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _finish(result: Array, valid: Array) -> Array:
|
|
53
|
+
valid = valid & _valid_factor(result)
|
|
54
|
+
# Keep the failure marker in arithmetic on the result, including its AD path.
|
|
55
|
+
return result * jnp.where(valid, 1, jnp.asarray(jnp.nan, dtype=result.dtype))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Append a row and column through a bordering solve."""
|
|
2
|
+
|
|
3
|
+
import jax.numpy as jnp
|
|
4
|
+
from jax import Array
|
|
5
|
+
from jax.scipy.linalg import solve_triangular
|
|
6
|
+
|
|
7
|
+
from ._validation import _factor, _finish, _scalar, _valid_factor, _vector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _append(L: Array, k: Array, kappa: Array) -> Array:
|
|
11
|
+
n = L.shape[0]
|
|
12
|
+
v = solve_triangular(L, k, lower=True) if n else k
|
|
13
|
+
diagonal = jnp.sqrt(kappa - jnp.vdot(v, v))
|
|
14
|
+
result = jnp.zeros((n + 1, n + 1), dtype=L.dtype)
|
|
15
|
+
return result.at[:n, :n].set(L).at[n, :n].set(v).at[n, n].set(diagonal)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def chol_append(L: Array, k: Array, kappa: Array | float) -> Array:
|
|
19
|
+
"""Factor [[L @ L.T, k], [k.T, kappa]], returning shape (n+1, n+1).
|
|
20
|
+
|
|
21
|
+
k has shape (n,) in the current physical ordering; kappa is scalar.
|
|
22
|
+
The Schur complement kappa - ||solve(L, k)||^2 must be strictly positive.
|
|
23
|
+
Empty (0, 0) factors are supported. Work is O(n^2).
|
|
24
|
+
Supports jit, vmap, JVP and reverse-mode AD. Numerical failure gives NaNs.
|
|
25
|
+
"""
|
|
26
|
+
L = _factor(L)
|
|
27
|
+
k = _vector(k, L.shape[0], L, "k")
|
|
28
|
+
kappa = _scalar(kappa, L, "kappa")
|
|
29
|
+
valid = _valid_factor(L) & jnp.all(jnp.isfinite(k)) & jnp.isfinite(kappa)
|
|
30
|
+
return _finish(_append(L, k, kappa), valid)
|