solvax 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.
- solvax/__init__.py +89 -0
- solvax/banded.py +385 -0
- solvax/direct.py +344 -0
- solvax/implicit.py +149 -0
- solvax/krylov.py +431 -0
- solvax/native.py +119 -0
- solvax/operators.py +460 -0
- solvax/precond.py +416 -0
- solvax/refine.py +111 -0
- solvax-0.1.0.dist-info/METADATA +115 -0
- solvax-0.1.0.dist-info/RECORD +13 -0
- solvax-0.1.0.dist-info/WHEEL +4 -0
- solvax-0.1.0.dist-info/licenses/LICENSE +21 -0
solvax/__init__.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""solvax: differentiable structured linear solvers and matrix-free methods in JAX.
|
|
2
|
+
|
|
3
|
+
Generic solver infrastructure for kinetic and PDE codes: batched structured
|
|
4
|
+
direct solves, preconditioned/recycled Krylov methods, and implicit
|
|
5
|
+
differentiation — everything jit/vmap/grad-transparent unless explicitly
|
|
6
|
+
marked as a host-side native bridge.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from solvax.banded import (
|
|
10
|
+
BandedLUFactors,
|
|
11
|
+
PeriodicBandedLUFactors,
|
|
12
|
+
banded_matvec,
|
|
13
|
+
lu_factor_banded,
|
|
14
|
+
lu_factor_banded_periodic,
|
|
15
|
+
lu_solve_banded,
|
|
16
|
+
lu_solve_banded_periodic,
|
|
17
|
+
)
|
|
18
|
+
from solvax.direct import (
|
|
19
|
+
BlockTridiagFactors,
|
|
20
|
+
block_thomas,
|
|
21
|
+
block_thomas_factor,
|
|
22
|
+
block_thomas_solve,
|
|
23
|
+
block_thomas_truncated,
|
|
24
|
+
block_thomas_truncated_fn,
|
|
25
|
+
)
|
|
26
|
+
from solvax.implicit import linear_solve, root_solve
|
|
27
|
+
from solvax.krylov import KrylovSolution, gcrot, gmres
|
|
28
|
+
from solvax.native import SpluFactorization, splu_solve
|
|
29
|
+
from solvax.operators import (
|
|
30
|
+
BlockTridiagonalOperator,
|
|
31
|
+
BorderedOperator,
|
|
32
|
+
KroneckerOperator,
|
|
33
|
+
MatrixFreeOperator,
|
|
34
|
+
SumOperator,
|
|
35
|
+
schur_projected_precond,
|
|
36
|
+
)
|
|
37
|
+
from solvax.precond import (
|
|
38
|
+
block_jacobi,
|
|
39
|
+
coarse_operator,
|
|
40
|
+
jacobi,
|
|
41
|
+
kronecker_nkp,
|
|
42
|
+
line_smoother,
|
|
43
|
+
mixed_precision,
|
|
44
|
+
nearest_kronecker,
|
|
45
|
+
p_multigrid,
|
|
46
|
+
)
|
|
47
|
+
from solvax.refine import as_low_precision, iterative_refinement
|
|
48
|
+
|
|
49
|
+
__version__ = "0.1.0"
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"BandedLUFactors",
|
|
53
|
+
"PeriodicBandedLUFactors",
|
|
54
|
+
"banded_matvec",
|
|
55
|
+
"lu_factor_banded",
|
|
56
|
+
"lu_factor_banded_periodic",
|
|
57
|
+
"lu_solve_banded",
|
|
58
|
+
"lu_solve_banded_periodic",
|
|
59
|
+
"BlockTridiagFactors",
|
|
60
|
+
"block_thomas",
|
|
61
|
+
"block_thomas_factor",
|
|
62
|
+
"block_thomas_solve",
|
|
63
|
+
"block_thomas_truncated",
|
|
64
|
+
"block_thomas_truncated_fn",
|
|
65
|
+
"KrylovSolution",
|
|
66
|
+
"gmres",
|
|
67
|
+
"gcrot",
|
|
68
|
+
"linear_solve",
|
|
69
|
+
"root_solve",
|
|
70
|
+
"MatrixFreeOperator",
|
|
71
|
+
"SumOperator",
|
|
72
|
+
"KroneckerOperator",
|
|
73
|
+
"BlockTridiagonalOperator",
|
|
74
|
+
"BorderedOperator",
|
|
75
|
+
"schur_projected_precond",
|
|
76
|
+
"jacobi",
|
|
77
|
+
"block_jacobi",
|
|
78
|
+
"coarse_operator",
|
|
79
|
+
"line_smoother",
|
|
80
|
+
"p_multigrid",
|
|
81
|
+
"mixed_precision",
|
|
82
|
+
"kronecker_nkp",
|
|
83
|
+
"nearest_kronecker",
|
|
84
|
+
"iterative_refinement",
|
|
85
|
+
"as_low_precision",
|
|
86
|
+
"SpluFactorization",
|
|
87
|
+
"splu_solve",
|
|
88
|
+
"__version__",
|
|
89
|
+
]
|
solvax/banded.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Banded LU solvers: non-pivoted factorization and periodic (circulant-banded) systems.
|
|
2
|
+
|
|
3
|
+
Storage convention (identical to :func:`scipy.linalg.solve_banded`): a matrix
|
|
4
|
+
``A`` with ``lower_bw`` sub-diagonals and ``upper_bw`` super-diagonals is held
|
|
5
|
+
in a ``bands`` array of shape ``(n_diags, n)`` with
|
|
6
|
+
``n_diags = lower_bw + upper_bw + 1``, where row ``r`` holds the diagonal with
|
|
7
|
+
offset ``upper_bw - r``:
|
|
8
|
+
|
|
9
|
+
bands[upper_bw + i - j, j] = A[i, j] for max(0, j - upper_bw) <= i <= min(n - 1, j + lower_bw)
|
|
10
|
+
|
|
11
|
+
Entries of ``bands`` outside that range are ignored. The LU factorization is
|
|
12
|
+
Doolittle elimination *without pivoting*, carried out column by column in
|
|
13
|
+
banded storage with a ``jax.lax.scan`` (static shapes, so everything is
|
|
14
|
+
jit/vmap/grad-transparent — XLA handles row pivoting poorly, and avoiding it
|
|
15
|
+
is the point of this module). Two safeguards substitute for pivoting:
|
|
16
|
+
|
|
17
|
+
- *row equilibration*: each row is pre-scaled by ``1 / max|row|``;
|
|
18
|
+
- *static pivoting*: any pivot with ``|pivot| < floor`` is clamped to
|
|
19
|
+
``sign(pivot) * floor``, and the number of clamps is recorded in the factors
|
|
20
|
+
so callers can detect near-singularity and fall back to iterative
|
|
21
|
+
refinement (see ``solvax.refine``).
|
|
22
|
+
|
|
23
|
+
LU without pivoting is guaranteed backward-stable only for diagonally
|
|
24
|
+
dominant (or block-dominant) systems (Demmel, Higham & Schreiber, Numer.
|
|
25
|
+
Linear Algebra Appl. 2, 173 (1995)); with equilibration and static pivoting it
|
|
26
|
+
is a robust practical choice for the advection-dominated periodic 1-D
|
|
27
|
+
operators these routines target, but callers should monitor the clamp counter
|
|
28
|
+
in weakly-dominant regimes.
|
|
29
|
+
|
|
30
|
+
Periodic (circulant-banded) systems ``A = B + U V^T`` — a banded core ``B``
|
|
31
|
+
plus wrap-around corner blocks expressed as a low-rank update — are solved
|
|
32
|
+
with the Sherman-Morrison-Woodbury capacitance-matrix identity
|
|
33
|
+
|
|
34
|
+
(B + U V^T)^{-1} = B^{-1} - B^{-1} U (I + V^T B^{-1} U)^{-1} V^T B^{-1},
|
|
35
|
+
|
|
36
|
+
where the small dense capacitance matrix ``I + V^T B^{-1} U`` is LU-factored
|
|
37
|
+
once (with partial pivoting — it is tiny) and ``B^{-1} U`` is precomputed with
|
|
38
|
+
the banded factorization, so each periodic solve costs one banded solve plus
|
|
39
|
+
O(bw) dense work.
|
|
40
|
+
|
|
41
|
+
References
|
|
42
|
+
----------
|
|
43
|
+
- G. H. Golub & C. F. Van Loan, *Matrix Computations*, 4th ed., section 4.3
|
|
44
|
+
(banded Gaussian elimination) and section 2.1.4 (Sherman-Morrison-Woodbury).
|
|
45
|
+
- J. W. Demmel, N. J. Higham & R. S. Schreiber, "Stability of block LU
|
|
46
|
+
factorization", Numer. Linear Algebra Appl. 2, 173 (1995) — stability
|
|
47
|
+
caveats for elimination without pivoting.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
from typing import NamedTuple
|
|
53
|
+
|
|
54
|
+
import jax
|
|
55
|
+
import jax.numpy as jnp
|
|
56
|
+
from jax.scipy.linalg import lu_factor, lu_solve
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _band_indices(n_diags: int, n: int, upper_bw: int):
|
|
60
|
+
"""Row-index grid and validity mask for the banded layout.
|
|
61
|
+
|
|
62
|
+
Entry ``bands[r, j]`` represents ``A[i, j]`` with ``i = j + r - upper_bw``;
|
|
63
|
+
it is valid iff ``0 <= i < n``.
|
|
64
|
+
"""
|
|
65
|
+
r = jnp.arange(n_diags)[:, None]
|
|
66
|
+
j = jnp.arange(n)[None, :]
|
|
67
|
+
i = j + r - upper_bw
|
|
68
|
+
return i, (i >= 0) & (i < n)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _shift_rows(v: jax.Array, d: int) -> jax.Array:
|
|
72
|
+
"""Zero-padded shift along axis 0: ``out[i] = v[i + d]`` where defined."""
|
|
73
|
+
if d == 0:
|
|
74
|
+
return v
|
|
75
|
+
if d > 0:
|
|
76
|
+
return jnp.concatenate([v[d:], jnp.zeros_like(v[:d])], axis=0)
|
|
77
|
+
return jnp.concatenate([jnp.zeros_like(v[d:]), v[:d]], axis=0)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def banded_matvec(
|
|
81
|
+
bands: jax.Array, lower_bw: int, upper_bw: int, x: jax.Array
|
|
82
|
+
) -> jax.Array:
|
|
83
|
+
"""Matrix-vector product ``A @ x`` with ``A`` in banded storage.
|
|
84
|
+
|
|
85
|
+
Vectorized over the matrix dimension (the only Python loop is over the
|
|
86
|
+
``n_diags`` diagonals, which is static).
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
bands: banded storage of ``A``, shape ``(lower_bw + upper_bw + 1, n)``
|
|
90
|
+
(see module docstring); out-of-range entries are ignored.
|
|
91
|
+
lower_bw: number of sub-diagonals.
|
|
92
|
+
upper_bw: number of super-diagonals.
|
|
93
|
+
x: vector ``(n,)`` or block of vectors ``(n, k)``.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
``A @ x`` with the same shape as ``x``.
|
|
97
|
+
"""
|
|
98
|
+
bands = jnp.asarray(bands)
|
|
99
|
+
x = jnp.asarray(x)
|
|
100
|
+
n_diags, n = bands.shape
|
|
101
|
+
if n_diags != lower_bw + upper_bw + 1:
|
|
102
|
+
raise ValueError("bands must have lower_bw + upper_bw + 1 rows")
|
|
103
|
+
_, mask = _band_indices(n_diags, n, upper_bw)
|
|
104
|
+
ab = jnp.where(mask, bands, 0.0)
|
|
105
|
+
|
|
106
|
+
y = jnp.zeros(x.shape, dtype=jnp.result_type(ab.dtype, x.dtype))
|
|
107
|
+
for r in range(n_diags):
|
|
108
|
+
w = ab[r]
|
|
109
|
+
c = w[:, None] * x if x.ndim > 1 else w * x
|
|
110
|
+
# bands[r, j] multiplies x[j] and contributes to y[i], i = j - (upper_bw - r).
|
|
111
|
+
y = y + _shift_rows(c, upper_bw - r)
|
|
112
|
+
return y
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class BandedLUFactors(NamedTuple):
|
|
116
|
+
"""Packed non-pivoted banded LU from :func:`lu_factor_banded`.
|
|
117
|
+
|
|
118
|
+
The bandwidths are recovered from the array shapes, so the factors stay
|
|
119
|
+
jit/vmap-transparent (no static metadata crosses transform boundaries).
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
l_bands: unit-lower-triangular multipliers, shape ``(lower_bw, n)``;
|
|
123
|
+
``l_bands[t - 1, k]`` holds ``L[k + t, k]``.
|
|
124
|
+
u_bands: upper factor in banded storage, shape ``(upper_bw + 1, n)``;
|
|
125
|
+
``u_bands[upper_bw + i - j, j]`` holds ``U[i, j]``.
|
|
126
|
+
row_scale: equilibration scales applied to the rows of ``A`` (all ones
|
|
127
|
+
when ``equilibrate=False``), shape ``(n,)``.
|
|
128
|
+
n_clamped: int32 count of pivots clamped by static pivoting; a
|
|
129
|
+
nonzero value signals near-singularity of the (equilibrated) core.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
l_bands: jax.Array
|
|
133
|
+
u_bands: jax.Array
|
|
134
|
+
row_scale: jax.Array
|
|
135
|
+
n_clamped: jax.Array
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def lu_factor_banded(
|
|
139
|
+
bands: jax.Array,
|
|
140
|
+
lower_bw: int,
|
|
141
|
+
upper_bw: int,
|
|
142
|
+
*,
|
|
143
|
+
equilibrate: bool = True,
|
|
144
|
+
static_pivot_floor: float | None = None,
|
|
145
|
+
) -> BandedLUFactors:
|
|
146
|
+
"""Non-pivoted (Doolittle) LU of a banded matrix, in banded storage.
|
|
147
|
+
|
|
148
|
+
Columns are eliminated left to right with a ``jax.lax.scan``; the carry
|
|
149
|
+
holds the multipliers of the previous ``upper_bw`` steps, so shapes are
|
|
150
|
+
static and the factorization is jit/vmap/grad-transparent. Row
|
|
151
|
+
equilibration and static pivoting (see module docstring) substitute for
|
|
152
|
+
the row pivoting that XLA handles poorly.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
bands: banded storage of ``A``, shape ``(lower_bw + upper_bw + 1, n)``;
|
|
156
|
+
out-of-range entries are ignored.
|
|
157
|
+
lower_bw: number of sub-diagonals.
|
|
158
|
+
upper_bw: number of super-diagonals.
|
|
159
|
+
equilibrate: scale each row by ``1 / max|row|`` before factoring
|
|
160
|
+
(the scales are stored and applied to the right-hand side by
|
|
161
|
+
:func:`lu_solve_banded`).
|
|
162
|
+
static_pivot_floor: clamp threshold for pivots; ``None`` (default)
|
|
163
|
+
uses ``sqrt(machine eps) * max|bands|`` of the (equilibrated)
|
|
164
|
+
matrix.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Factors for :func:`lu_solve_banded`, including the clamp counter.
|
|
168
|
+
"""
|
|
169
|
+
bands = jnp.asarray(bands)
|
|
170
|
+
kl, ku = int(lower_bw), int(upper_bw)
|
|
171
|
+
n_diags, n = bands.shape
|
|
172
|
+
if n_diags != kl + ku + 1:
|
|
173
|
+
raise ValueError("bands must have lower_bw + upper_bw + 1 rows")
|
|
174
|
+
i_idx, mask = _band_indices(n_diags, n, ku)
|
|
175
|
+
ab = jnp.where(mask, bands, 0.0)
|
|
176
|
+
dtype = ab.dtype
|
|
177
|
+
|
|
178
|
+
if equilibrate:
|
|
179
|
+
# Row i of A is scattered across the diagonals: |A[i, j]| = |ab[r, i + ku - r]|.
|
|
180
|
+
rows = jnp.stack([_shift_rows(jnp.abs(ab[r]), ku - r) for r in range(n_diags)])
|
|
181
|
+
row_max = rows.max(axis=0)
|
|
182
|
+
safe = jnp.where(row_max > 0, row_max, 1.0)
|
|
183
|
+
row_scale = jnp.where(row_max > 0, 1.0 / safe, 1.0)
|
|
184
|
+
ab = jnp.where(mask, ab * row_scale[jnp.clip(i_idx, 0, n - 1)], 0.0)
|
|
185
|
+
else:
|
|
186
|
+
row_scale = jnp.ones(n, dtype)
|
|
187
|
+
|
|
188
|
+
if static_pivot_floor is None:
|
|
189
|
+
floor = jnp.sqrt(jnp.finfo(dtype).eps) * jnp.max(jnp.abs(ab))
|
|
190
|
+
else:
|
|
191
|
+
floor = jnp.asarray(static_pivot_floor, dtype)
|
|
192
|
+
|
|
193
|
+
def step(carry, c):
|
|
194
|
+
# c = ab[:, j]: c[r] holds A[j + r - ku, j]. m_hist[:, ku - t] holds the
|
|
195
|
+
# multipliers of elimination step k = j - t (zeros for k < 0).
|
|
196
|
+
m_hist, n_clamped = carry
|
|
197
|
+
for t in range(ku, 0, -1): # oldest step first
|
|
198
|
+
u_kj = c[ku - t] # U[j - t, j], final after older updates
|
|
199
|
+
c = c.at[ku - t + 1 : ku - t + 1 + kl].add(-m_hist[:, ku - t] * u_kj)
|
|
200
|
+
p = c[ku]
|
|
201
|
+
small = jnp.abs(p) < floor
|
|
202
|
+
p = jnp.where(small, jnp.where(p >= 0, floor, -floor), p)
|
|
203
|
+
mults = c[ku + 1 :] / p
|
|
204
|
+
c = c.at[ku].set(p).at[ku + 1 :].set(mults)
|
|
205
|
+
if ku > 0:
|
|
206
|
+
m_hist = jnp.concatenate([m_hist[:, 1:], mults[:, None]], axis=1)
|
|
207
|
+
return (m_hist, n_clamped + small.astype(jnp.int32)), c
|
|
208
|
+
|
|
209
|
+
carry0 = (jnp.zeros((kl, ku), dtype), jnp.int32(0))
|
|
210
|
+
(_, n_clamped), cols = jax.lax.scan(step, carry0, ab.T)
|
|
211
|
+
packed = cols.T # LAPACK-style packed LU in the input layout
|
|
212
|
+
return BandedLUFactors(packed[ku + 1 :], packed[: ku + 1], row_scale, n_clamped)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def lu_solve_banded(factors: BandedLUFactors, b: jax.Array) -> jax.Array:
|
|
216
|
+
"""Solve ``A x = b`` using precomputed banded LU factors.
|
|
217
|
+
|
|
218
|
+
Forward substitution with the unit-lower multipliers, then backward
|
|
219
|
+
substitution with the banded upper factor, each as a ``jax.lax.scan``
|
|
220
|
+
carrying a bandwidth-sized window of the solution.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
factors: output of :func:`lu_factor_banded`.
|
|
224
|
+
b: right-hand side, shape ``(n,)`` or ``(n, k)``.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Solution with the same shape as ``b``.
|
|
228
|
+
"""
|
|
229
|
+
l_bands, u_bands, row_scale, _ = factors
|
|
230
|
+
kl = l_bands.shape[0]
|
|
231
|
+
ku = u_bands.shape[0] - 1
|
|
232
|
+
b = jnp.asarray(b)
|
|
233
|
+
vector = b.ndim == 1
|
|
234
|
+
bb = b[:, None] if vector else b
|
|
235
|
+
bb = bb * row_scale[:, None]
|
|
236
|
+
|
|
237
|
+
# Forward: y[i] = b[i] - sum_t L[i, i - t] y[i - t], L[i, i - t] = l_bands[t - 1, i - t].
|
|
238
|
+
if kl > 0:
|
|
239
|
+
lcoef = jnp.stack(
|
|
240
|
+
[_shift_rows(l_bands[t - 1], -t) for t in range(1, kl + 1)], axis=1
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def fwd_step(y_win, inputs): # y_win[s] = y[i - kl + s]
|
|
244
|
+
coeffs, b_i = inputs
|
|
245
|
+
y_i = b_i - coeffs[::-1] @ y_win
|
|
246
|
+
return jnp.concatenate([y_win[1:], y_i[None]], axis=0), y_i
|
|
247
|
+
|
|
248
|
+
y0 = jnp.zeros((kl, bb.shape[1]), bb.dtype)
|
|
249
|
+
_, y = jax.lax.scan(fwd_step, y0, (lcoef, bb))
|
|
250
|
+
else:
|
|
251
|
+
y = bb
|
|
252
|
+
|
|
253
|
+
# Backward: x[i] = (y[i] - sum_t U[i, i + t] x[i + t]) / U[i, i].
|
|
254
|
+
diag = u_bands[ku]
|
|
255
|
+
if ku > 0:
|
|
256
|
+
ucoef = jnp.stack(
|
|
257
|
+
[_shift_rows(u_bands[ku - t], t) for t in range(1, ku + 1)], axis=1
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def bwd_step(x_win, inputs): # x_win[s] = x[i + 1 + s]
|
|
261
|
+
coeffs, y_i, d_i = inputs
|
|
262
|
+
x_i = (y_i - coeffs @ x_win) / d_i
|
|
263
|
+
return jnp.concatenate([x_i[None], x_win[:-1]], axis=0), x_i
|
|
264
|
+
|
|
265
|
+
x0 = jnp.zeros((ku, y.shape[1]), y.dtype)
|
|
266
|
+
_, x = jax.lax.scan(bwd_step, x0, (ucoef, y, diag), reverse=True)
|
|
267
|
+
else:
|
|
268
|
+
x = y / diag[:, None]
|
|
269
|
+
|
|
270
|
+
return x[:, 0] if vector else x
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class PeriodicBandedLUFactors(NamedTuple):
|
|
274
|
+
"""Woodbury factors for a periodic banded system, from
|
|
275
|
+
:func:`lu_factor_banded_periodic`.
|
|
276
|
+
|
|
277
|
+
Attributes:
|
|
278
|
+
core: banded LU of the (non-periodic) banded core ``B``.
|
|
279
|
+
z_ul: ``B^{-1} U`` columns generated by the top-right corner block,
|
|
280
|
+
shape ``(n, bw_ul)``.
|
|
281
|
+
z_lr: ``B^{-1} U`` columns generated by the bottom-left corner block,
|
|
282
|
+
shape ``(n, bw_lr)``.
|
|
283
|
+
cap_lu: dense LU of the capacitance matrix ``I + V^T B^{-1} U``,
|
|
284
|
+
shape ``(bw_ul + bw_lr, bw_ul + bw_lr)``.
|
|
285
|
+
cap_piv: matching pivot indices.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
core: BandedLUFactors
|
|
289
|
+
z_ul: jax.Array
|
|
290
|
+
z_lr: jax.Array
|
|
291
|
+
cap_lu: jax.Array
|
|
292
|
+
cap_piv: jax.Array
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def lu_factor_banded_periodic(
|
|
296
|
+
bands: jax.Array,
|
|
297
|
+
lower_bw: int,
|
|
298
|
+
upper_bw: int,
|
|
299
|
+
corner_ul: jax.Array,
|
|
300
|
+
corner_lr: jax.Array,
|
|
301
|
+
*,
|
|
302
|
+
equilibrate: bool = True,
|
|
303
|
+
static_pivot_floor: float | None = None,
|
|
304
|
+
) -> PeriodicBandedLUFactors:
|
|
305
|
+
"""Factor a periodic (circulant-banded) matrix via the capacitance method.
|
|
306
|
+
|
|
307
|
+
The matrix is ``A = B + U V^T``: a banded core ``B`` (given in ``bands``)
|
|
308
|
+
plus the periodic wrap-around corners as a rank-``(bw_ul + bw_lr)``
|
|
309
|
+
update, where ``U`` carries the corner blocks and ``V`` selects the
|
|
310
|
+
coupled columns. ``B`` is factored with the non-pivoted banded LU,
|
|
311
|
+
``Z = B^{-1} U`` is precomputed, and the small dense capacitance matrix
|
|
312
|
+
``I + V^T Z`` is LU-factored so :func:`lu_solve_banded_periodic` can apply
|
|
313
|
+
the Woodbury identity (module docstring) at the cost of one banded solve.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
bands: banded storage of the core ``B``, shape
|
|
317
|
+
``(lower_bw + upper_bw + 1, n)``.
|
|
318
|
+
lower_bw: number of sub-diagonals of ``B``.
|
|
319
|
+
upper_bw: number of super-diagonals of ``B``.
|
|
320
|
+
corner_ul: top-right corner block ``A[:bw, n - bw:]`` coupling the
|
|
321
|
+
first rows to the last columns, shape ``(bw, bw)``.
|
|
322
|
+
corner_lr: bottom-left corner block ``A[n - bw:, :bw]`` coupling the
|
|
323
|
+
last rows to the first columns, shape ``(bw, bw)``.
|
|
324
|
+
equilibrate: passed to :func:`lu_factor_banded` for the core.
|
|
325
|
+
static_pivot_floor: passed to :func:`lu_factor_banded` for the core.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Factors for :func:`lu_solve_banded_periodic`.
|
|
329
|
+
"""
|
|
330
|
+
bands = jnp.asarray(bands)
|
|
331
|
+
corner_ul = jnp.asarray(corner_ul)
|
|
332
|
+
corner_lr = jnp.asarray(corner_lr)
|
|
333
|
+
if corner_ul.ndim != 2 or corner_ul.shape[0] != corner_ul.shape[1]:
|
|
334
|
+
raise ValueError("corner_ul must be a square matrix")
|
|
335
|
+
if corner_lr.ndim != 2 or corner_lr.shape[0] != corner_lr.shape[1]:
|
|
336
|
+
raise ValueError("corner_lr must be a square matrix")
|
|
337
|
+
|
|
338
|
+
core = lu_factor_banded(
|
|
339
|
+
bands,
|
|
340
|
+
lower_bw,
|
|
341
|
+
upper_bw,
|
|
342
|
+
equilibrate=equilibrate,
|
|
343
|
+
static_pivot_floor=static_pivot_floor,
|
|
344
|
+
)
|
|
345
|
+
n = bands.shape[1]
|
|
346
|
+
r_ul, r_lr = corner_ul.shape[0], corner_lr.shape[0]
|
|
347
|
+
dtype = jnp.result_type(bands.dtype, corner_ul.dtype, corner_lr.dtype)
|
|
348
|
+
|
|
349
|
+
# U stacks the corner blocks; V^T picks the last r_ul then first r_lr rows.
|
|
350
|
+
u_cols = jnp.zeros((n, r_ul + r_lr), dtype)
|
|
351
|
+
u_cols = u_cols.at[:r_ul, :r_ul].set(corner_ul)
|
|
352
|
+
u_cols = u_cols.at[n - r_lr :, r_ul:].set(corner_lr)
|
|
353
|
+
z = lu_solve_banded(core, u_cols)
|
|
354
|
+
vt_z = jnp.concatenate([z[n - r_ul :], z[:r_lr]], axis=0)
|
|
355
|
+
cap_lu, cap_piv = lu_factor(jnp.eye(r_ul + r_lr, dtype=dtype) + vt_z)
|
|
356
|
+
return PeriodicBandedLUFactors(core, z[:, :r_ul], z[:, r_ul:], cap_lu, cap_piv)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def lu_solve_banded_periodic(
|
|
360
|
+
factors: PeriodicBandedLUFactors, b: jax.Array
|
|
361
|
+
) -> jax.Array:
|
|
362
|
+
"""Solve a periodic banded system using precomputed Woodbury factors.
|
|
363
|
+
|
|
364
|
+
Applies ``x = B^{-1} b - Z (I + V^T Z)^{-1} V^T B^{-1} b`` with
|
|
365
|
+
``Z = B^{-1} U`` from :func:`lu_factor_banded_periodic`.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
factors: output of :func:`lu_factor_banded_periodic`.
|
|
369
|
+
b: right-hand side, shape ``(n,)`` or ``(n, k)``.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Solution with the same shape as ``b``.
|
|
373
|
+
"""
|
|
374
|
+
core, z_ul, z_lr, cap_lu, cap_piv = factors
|
|
375
|
+
r_ul = z_ul.shape[1]
|
|
376
|
+
n = z_ul.shape[0]
|
|
377
|
+
b = jnp.asarray(b)
|
|
378
|
+
vector = b.ndim == 1
|
|
379
|
+
bb = b[:, None] if vector else b
|
|
380
|
+
|
|
381
|
+
y = lu_solve_banded(core, bb)
|
|
382
|
+
vt_y = jnp.concatenate([y[n - r_ul :], y[: z_lr.shape[1]]], axis=0)
|
|
383
|
+
t = lu_solve((cap_lu, cap_piv), vt_y)
|
|
384
|
+
x = y - z_ul @ t[:r_ul] - z_lr @ t[r_ul:]
|
|
385
|
+
return x[:, 0] if vector else x
|