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 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,6 @@
1
+ """Immutable Cholesky factor objects with matrix calculations and modifications."""
2
+
3
+ from .cholesky import Cholesky
4
+ from .permuted import PermutedCholesky
5
+
6
+ __all__ = ["Cholesky", "PermutedCholesky"]
@@ -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)