torchsolve 0.0.1__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.
- torchsolve/__init__.py +41 -0
- torchsolve/_cg.py +556 -0
- torchsolve/_irgnm.py +270 -0
- torchsolve/_problem.py +89 -0
- torchsolve/_solvers.py +304 -0
- torchsolve-0.0.1.dist-info/METADATA +223 -0
- torchsolve-0.0.1.dist-info/RECORD +10 -0
- torchsolve-0.0.1.dist-info/WHEEL +5 -0
- torchsolve-0.0.1.dist-info/licenses/LICENSE +21 -0
- torchsolve-0.0.1.dist-info/top_level.txt +1 -0
torchsolve/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Memory-lean iterative solvers for inverse problems in PyTorch.
|
|
2
|
+
|
|
3
|
+
The solvers here take the normal operator rather than the forward one, because
|
|
4
|
+
that is what a non-Cartesian reconstruction can afford to apply repeatedly, and
|
|
5
|
+
they are written so that an iteration allocates nothing it does not have to.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from importlib.metadata import PackageNotFoundError
|
|
11
|
+
from importlib.metadata import version as _distribution_version
|
|
12
|
+
|
|
13
|
+
from ._cg import CGResult, Regularizer, conjugate_gradient
|
|
14
|
+
from ._irgnm import (
|
|
15
|
+
GaussNewtonResult,
|
|
16
|
+
NonlinearOperator,
|
|
17
|
+
autodiff,
|
|
18
|
+
gauss_newton,
|
|
19
|
+
)
|
|
20
|
+
from ._problem import InnerSolver, LinearProblem
|
|
21
|
+
from ._solvers import CGSolver, LstsqSolver
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
__version__ = _distribution_version(__name__)
|
|
25
|
+
except PackageNotFoundError: # a source tree that was never installed
|
|
26
|
+
__version__ = "0.0.0.dev0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"CGResult",
|
|
30
|
+
"CGSolver",
|
|
31
|
+
"GaussNewtonResult",
|
|
32
|
+
"InnerSolver",
|
|
33
|
+
"LinearProblem",
|
|
34
|
+
"LstsqSolver",
|
|
35
|
+
"NonlinearOperator",
|
|
36
|
+
"Regularizer",
|
|
37
|
+
"__version__",
|
|
38
|
+
"autodiff",
|
|
39
|
+
"conjugate_gradient",
|
|
40
|
+
"gauss_newton",
|
|
41
|
+
]
|
torchsolve/_cg.py
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
r"""Conjugate gradient for the regularised normal equations.
|
|
2
|
+
|
|
3
|
+
Solves, for the general least-squares problem
|
|
4
|
+
|
|
5
|
+
.. math::
|
|
6
|
+
|
|
7
|
+
\min_x \; \|A x - y\|^2 + \sum_k \lambda_k \|R_k x - c_k\|^2
|
|
8
|
+
|
|
9
|
+
the normal equations that stationarity gives,
|
|
10
|
+
|
|
11
|
+
.. math::
|
|
12
|
+
|
|
13
|
+
\Big(A^H A + \sum_k \lambda_k R_k^H R_k\Big) x
|
|
14
|
+
= A^H y + \sum_k \lambda_k R_k^H c_k,
|
|
15
|
+
|
|
16
|
+
by conjugate gradient, optionally preconditioned. The caller supplies
|
|
17
|
+
:math:`A^H A` and :math:`A^H y` rather than :math:`A`, because for a
|
|
18
|
+
non-Cartesian acquisition the normal operator is a convolution that costs far
|
|
19
|
+
less than a transform pair, and because that is the object a Toeplitz
|
|
20
|
+
factorisation stands for.
|
|
21
|
+
|
|
22
|
+
Two properties this implementation is careful about, both learned the hard way.
|
|
23
|
+
|
|
24
|
+
**It steps through negative curvature.** A compressed Toeplitz normal carries
|
|
25
|
+
eigenvalues just below zero -- the transfer values its support left out -- and
|
|
26
|
+
the iteration keeps reducing the residual through them. Refusing the step,
|
|
27
|
+
which is what a ``pAp > 0`` guard does, freezes the iteration well short of
|
|
28
|
+
the answer. BART's ``conjgrad`` stops only on an exactly zero curvature, and
|
|
29
|
+
so does this.
|
|
30
|
+
|
|
31
|
+
**It allocates nothing per iteration that it can avoid.** The updates are
|
|
32
|
+
in place and the inner products are fused reductions: ``torch.vdot`` and a
|
|
33
|
+
batched ``einsum`` take a dot without materialising the product, where
|
|
34
|
+
``(a.conj() * b).real.sum()`` costs two whole volumes and
|
|
35
|
+
``torch.linalg.vecdot`` costs the same. What is left is whatever the operator
|
|
36
|
+
itself returns.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import warnings
|
|
42
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from typing import Any, cast
|
|
45
|
+
|
|
46
|
+
import torch
|
|
47
|
+
from torch.autograd.function import once_differentiable
|
|
48
|
+
|
|
49
|
+
__all__ = ["CGResult", "Regularizer", "conjugate_gradient"]
|
|
50
|
+
|
|
51
|
+
Operator = Callable[[torch.Tensor], torch.Tensor]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class Regularizer:
|
|
56
|
+
r"""One term :math:`\lambda \|R x - c\|^2` of the objective.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
weight
|
|
61
|
+
The term's :math:`\lambda`. Must not be negative.
|
|
62
|
+
operator
|
|
63
|
+
:math:`R`. ``None`` means the identity, which is the common case and
|
|
64
|
+
is folded into a single scalar rather than applied.
|
|
65
|
+
adjoint
|
|
66
|
+
:math:`R^H`. Needed when ``operator`` is given and does not carry its
|
|
67
|
+
own ``adjoint`` or ``H`` attribute.
|
|
68
|
+
bias
|
|
69
|
+
:math:`c`, the term's target. ``None`` means zero, so the term pulls
|
|
70
|
+
towards the origin.
|
|
71
|
+
|
|
72
|
+
Examples
|
|
73
|
+
--------
|
|
74
|
+
Pull towards zero, which is Tikhonov regularisation:
|
|
75
|
+
|
|
76
|
+
>>> from torchsolve import Regularizer
|
|
77
|
+
>>> Regularizer(1e-3).operator is None
|
|
78
|
+
True
|
|
79
|
+
|
|
80
|
+
Pull towards a previous estimate, which is what an outer iteration wants:
|
|
81
|
+
|
|
82
|
+
>>> import torch
|
|
83
|
+
>>> previous = torch.zeros(4)
|
|
84
|
+
>>> term = Regularizer(1e-2, bias=previous)
|
|
85
|
+
>>> term.weight
|
|
86
|
+
0.01
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
weight: float
|
|
90
|
+
operator: Operator | None = None
|
|
91
|
+
adjoint: Operator | None = None
|
|
92
|
+
bias: torch.Tensor | None = None
|
|
93
|
+
|
|
94
|
+
def __post_init__(self) -> None:
|
|
95
|
+
if self.weight < 0:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"regularizer weight must not be negative, got {self.weight}"
|
|
98
|
+
)
|
|
99
|
+
if self.operator is None:
|
|
100
|
+
if self.adjoint is not None:
|
|
101
|
+
raise ValueError("an identity regularizer takes no adjoint")
|
|
102
|
+
return
|
|
103
|
+
if self.adjoint is not None:
|
|
104
|
+
return
|
|
105
|
+
for name in ("adjoint", "H"):
|
|
106
|
+
found = getattr(self.operator, name, None)
|
|
107
|
+
if callable(found):
|
|
108
|
+
object.__setattr__(self, "adjoint", found)
|
|
109
|
+
return
|
|
110
|
+
raise ValueError(
|
|
111
|
+
"a regularizer with an operator needs its adjoint: pass adjoint=, "
|
|
112
|
+
"or give the operator an 'adjoint' or 'H' attribute"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def is_identity(self) -> bool:
|
|
117
|
+
"""Whether this term regularises towards a target without transforming."""
|
|
118
|
+
return self.operator is None
|
|
119
|
+
|
|
120
|
+
def normal(self, vector: torch.Tensor) -> torch.Tensor:
|
|
121
|
+
r"""Apply :math:`R^H R` to a vector."""
|
|
122
|
+
forward = cast("Operator", self.operator)
|
|
123
|
+
backward = cast("Operator", self.adjoint)
|
|
124
|
+
return backward(forward(vector))
|
|
125
|
+
|
|
126
|
+
def project(self, vector: torch.Tensor) -> torch.Tensor:
|
|
127
|
+
r"""Apply :math:`R^H` to a vector, which is what a bias needs."""
|
|
128
|
+
return cast("Operator", self.adjoint)(vector)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class CGResult:
|
|
133
|
+
"""What the iteration reached, and what it met on the way.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
solution
|
|
138
|
+
The final iterate.
|
|
139
|
+
iterations
|
|
140
|
+
How many steps were taken.
|
|
141
|
+
residual_norm
|
|
142
|
+
Norm of the final residual of the regularised normal equations.
|
|
143
|
+
converged
|
|
144
|
+
Whether the residual met the requested tolerance. Always ``False``
|
|
145
|
+
when no tolerance was requested, since nothing was checked.
|
|
146
|
+
definite
|
|
147
|
+
Whether the recurrence stayed positive throughout. ``False`` means the
|
|
148
|
+
operator is not positive definite and the answer is not a minimiser,
|
|
149
|
+
though it is still the best the iteration reached.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
solution: torch.Tensor
|
|
153
|
+
iterations: int
|
|
154
|
+
residual_norm: torch.Tensor
|
|
155
|
+
converged: bool
|
|
156
|
+
definite: bool = True
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class _System:
|
|
161
|
+
"""The regularised normal operator, and the right-hand side it acts on."""
|
|
162
|
+
|
|
163
|
+
normal: Operator
|
|
164
|
+
tikhonov: float
|
|
165
|
+
terms: Sequence[Regularizer]
|
|
166
|
+
rhs: torch.Tensor = field(repr=False)
|
|
167
|
+
|
|
168
|
+
def __call__(self, vector: torch.Tensor) -> torch.Tensor:
|
|
169
|
+
result = self.normal(vector)
|
|
170
|
+
if self.tikhonov:
|
|
171
|
+
result = result.add(vector, alpha=self.tikhonov)
|
|
172
|
+
for term in self.terms:
|
|
173
|
+
result = result.add(term.normal(vector), alpha=term.weight)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _assemble(
|
|
178
|
+
normal: Operator,
|
|
179
|
+
rhs: torch.Tensor,
|
|
180
|
+
regularizers: Iterable[Regularizer],
|
|
181
|
+
) -> _System:
|
|
182
|
+
"""Fold the identity terms into a scalar and add every bias to the rhs."""
|
|
183
|
+
tikhonov = 0.0
|
|
184
|
+
shaped: list[Regularizer] = []
|
|
185
|
+
augmented = rhs
|
|
186
|
+
for term in regularizers:
|
|
187
|
+
if term.weight == 0:
|
|
188
|
+
continue
|
|
189
|
+
if term.is_identity:
|
|
190
|
+
tikhonov += term.weight
|
|
191
|
+
if term.bias is not None:
|
|
192
|
+
augmented = augmented.add(term.bias, alpha=term.weight)
|
|
193
|
+
continue
|
|
194
|
+
shaped.append(term)
|
|
195
|
+
if term.bias is not None:
|
|
196
|
+
augmented = augmented.add(term.project(term.bias), alpha=term.weight)
|
|
197
|
+
return _System(normal, tikhonov, tuple(shaped), augmented)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _inner(left: torch.Tensor, right: torch.Tensor, batch: int | None) -> torch.Tensor:
|
|
201
|
+
"""Real part of the inner product, without materialising the product.
|
|
202
|
+
|
|
203
|
+
``torch.vdot`` is a fused dot and ``einsum`` reduces without a temporary;
|
|
204
|
+
the obvious ``(left.conj() * right).real.sum()`` costs two whole tensors,
|
|
205
|
+
and so does ``torch.linalg.vecdot``.
|
|
206
|
+
"""
|
|
207
|
+
if batch is None:
|
|
208
|
+
flat_left, flat_right = left.reshape(-1), right.reshape(-1)
|
|
209
|
+
if left.is_complex():
|
|
210
|
+
return torch.vdot(flat_left, flat_right).real
|
|
211
|
+
return torch.dot(flat_left, flat_right)
|
|
212
|
+
moved_left = left.movedim(batch, 0).reshape(left.shape[batch], -1)
|
|
213
|
+
moved_right = right.movedim(batch, 0).reshape(right.shape[batch], -1)
|
|
214
|
+
product = torch.einsum("bi,bi->b", moved_left.conj(), moved_right)
|
|
215
|
+
shape = [1] * left.ndim
|
|
216
|
+
shape[batch] = left.shape[batch]
|
|
217
|
+
return (product.real if left.is_complex() else product).reshape(shape)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _norm(value: torch.Tensor, batch: int | None) -> torch.Tensor:
|
|
221
|
+
"""Euclidean norm, fused."""
|
|
222
|
+
if batch is None:
|
|
223
|
+
return torch.linalg.vector_norm(value)
|
|
224
|
+
moved = value.movedim(batch, 0).reshape(value.shape[batch], -1)
|
|
225
|
+
shape = [1] * value.ndim
|
|
226
|
+
shape[batch] = value.shape[batch]
|
|
227
|
+
return torch.linalg.vector_norm(moved, dim=1).reshape(shape)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _iterate(
|
|
231
|
+
system: _System,
|
|
232
|
+
x0: torch.Tensor | None,
|
|
233
|
+
*,
|
|
234
|
+
preconditioner: Operator | None,
|
|
235
|
+
max_iter: int,
|
|
236
|
+
rtol: float,
|
|
237
|
+
atol: float,
|
|
238
|
+
batch_dim: int | None,
|
|
239
|
+
) -> CGResult:
|
|
240
|
+
"""Run the recurrence. See :func:`conjugate_gradient` for the arguments."""
|
|
241
|
+
target = system.rhs
|
|
242
|
+
|
|
243
|
+
if x0 is None:
|
|
244
|
+
solution = torch.zeros_like(target)
|
|
245
|
+
residual = target.clone()
|
|
246
|
+
else:
|
|
247
|
+
solution = x0.clone()
|
|
248
|
+
residual = target - system(solution)
|
|
249
|
+
|
|
250
|
+
preconditioned = residual if preconditioner is None else preconditioner(residual)
|
|
251
|
+
direction = preconditioned.clone()
|
|
252
|
+
rho = _inner(residual, preconditioned, batch_dim)
|
|
253
|
+
|
|
254
|
+
checking = rtol > 0.0 or atol > 0.0
|
|
255
|
+
threshold = atol + rtol * _norm(target, batch_dim) if checking else None
|
|
256
|
+
|
|
257
|
+
definite = True
|
|
258
|
+
converged = False
|
|
259
|
+
iterations = 0
|
|
260
|
+
for step_index in range(max_iter):
|
|
261
|
+
if threshold is not None and bool(
|
|
262
|
+
torch.all(_norm(residual, batch_dim) <= threshold)
|
|
263
|
+
):
|
|
264
|
+
converged = True
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
# An exactly zero rho is exact convergence, not a failure: there is no
|
|
268
|
+
# residual left to reduce and every further step would be a no-op.
|
|
269
|
+
remaining = rho != 0
|
|
270
|
+
if not bool(torch.any(remaining)):
|
|
271
|
+
converged = True
|
|
272
|
+
break
|
|
273
|
+
|
|
274
|
+
applied = system(direction)
|
|
275
|
+
curvature = _inner(direction, applied, batch_dim)
|
|
276
|
+
# Only an exactly zero curvature has no step to take. A negative one
|
|
277
|
+
# does, and taking it is what keeps the residual falling.
|
|
278
|
+
usable = remaining & (curvature != 0)
|
|
279
|
+
if not bool(torch.any(usable)):
|
|
280
|
+
break
|
|
281
|
+
# Negative curvature is what indefiniteness looks like. A vanishing rho
|
|
282
|
+
# is convergence and must not be mistaken for it.
|
|
283
|
+
if definite and bool(torch.any(remaining & (curvature < 0))):
|
|
284
|
+
definite = False
|
|
285
|
+
|
|
286
|
+
alpha = torch.where(usable, rho / torch.where(usable, curvature, 1.0), 0.0)
|
|
287
|
+
solution.addcmul_(direction, alpha)
|
|
288
|
+
residual.addcmul_(applied, alpha, value=-1)
|
|
289
|
+
del applied
|
|
290
|
+
|
|
291
|
+
preconditioned = (
|
|
292
|
+
residual if preconditioner is None else preconditioner(residual)
|
|
293
|
+
)
|
|
294
|
+
updated = _inner(residual, preconditioned, batch_dim)
|
|
295
|
+
beta = torch.where(rho != 0, updated / torch.where(rho != 0, rho, 1.0), 0.0)
|
|
296
|
+
rho = updated
|
|
297
|
+
iterations = step_index + 1
|
|
298
|
+
|
|
299
|
+
if step_index + 1 < max_iter:
|
|
300
|
+
direction.mul_(beta).add_(preconditioned)
|
|
301
|
+
|
|
302
|
+
return CGResult(
|
|
303
|
+
solution=solution,
|
|
304
|
+
iterations=iterations,
|
|
305
|
+
residual_norm=_norm(residual, batch_dim).detach(),
|
|
306
|
+
converged=converged,
|
|
307
|
+
definite=definite,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class _ImplicitSolve(torch.autograd.Function):
|
|
312
|
+
"""A solve that differentiates itself rather than its iterations.
|
|
313
|
+
|
|
314
|
+
The iterates are not a graph to walk back through: for a self-adjoint
|
|
315
|
+
system, if ``x`` solves ``M x = b`` then a gradient arriving at ``x``
|
|
316
|
+
reaches ``b`` as ``M^-1`` of itself, which is another solve. Memory is
|
|
317
|
+
therefore flat in the iteration count, and the backward pass costs one more
|
|
318
|
+
solve rather than one stored volume per step.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
def forward( # type: ignore[override]
|
|
323
|
+
ctx: Any,
|
|
324
|
+
target: torch.Tensor,
|
|
325
|
+
x0: torch.Tensor | None,
|
|
326
|
+
settings: _Settings,
|
|
327
|
+
record: dict[str, Any],
|
|
328
|
+
*parameters: torch.Tensor,
|
|
329
|
+
) -> torch.Tensor:
|
|
330
|
+
system = _System(settings.normal, settings.tikhonov, settings.terms, target)
|
|
331
|
+
with torch.no_grad():
|
|
332
|
+
result = _iterate(
|
|
333
|
+
system,
|
|
334
|
+
x0,
|
|
335
|
+
preconditioner=settings.preconditioner,
|
|
336
|
+
max_iter=settings.max_iter,
|
|
337
|
+
rtol=settings.rtol,
|
|
338
|
+
atol=settings.atol,
|
|
339
|
+
batch_dim=settings.batch_dim,
|
|
340
|
+
)
|
|
341
|
+
record.update(
|
|
342
|
+
iterations=result.iterations,
|
|
343
|
+
residual_norm=result.residual_norm,
|
|
344
|
+
converged=result.converged,
|
|
345
|
+
definite=result.definite,
|
|
346
|
+
)
|
|
347
|
+
ctx.settings = settings
|
|
348
|
+
ctx.save_for_backward(result.solution, *parameters)
|
|
349
|
+
return result.solution
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
@once_differentiable
|
|
353
|
+
def backward(ctx: Any, grad_solution: torch.Tensor) -> tuple[Any, ...]:
|
|
354
|
+
solution, *parameters = ctx.saved_tensors
|
|
355
|
+
settings: _Settings = ctx.settings
|
|
356
|
+
system = _System(
|
|
357
|
+
settings.normal, settings.tikhonov, settings.terms, grad_solution
|
|
358
|
+
)
|
|
359
|
+
with torch.no_grad():
|
|
360
|
+
adjoint = _iterate(
|
|
361
|
+
system,
|
|
362
|
+
None,
|
|
363
|
+
preconditioner=settings.preconditioner,
|
|
364
|
+
max_iter=settings.backward_max_iter,
|
|
365
|
+
rtol=settings.backward_rtol,
|
|
366
|
+
atol=settings.backward_atol,
|
|
367
|
+
batch_dim=settings.batch_dim,
|
|
368
|
+
).solution
|
|
369
|
+
|
|
370
|
+
gradients: list[torch.Tensor | None] = [None] * len(parameters)
|
|
371
|
+
wanted = [
|
|
372
|
+
(index, parameter)
|
|
373
|
+
for index, parameter in enumerate(parameters)
|
|
374
|
+
if parameter.requires_grad
|
|
375
|
+
]
|
|
376
|
+
if wanted:
|
|
377
|
+
# d/dp of (M(p) x - b) = 0 gives -adjoint^H (dM/dp) x, which is the
|
|
378
|
+
# gradient of this scalar with respect to whatever M closed over.
|
|
379
|
+
with torch.enable_grad():
|
|
380
|
+
applied = system.normal(solution.detach())
|
|
381
|
+
for term in system.terms:
|
|
382
|
+
applied = applied.add(
|
|
383
|
+
term.normal(solution.detach()), alpha=term.weight
|
|
384
|
+
)
|
|
385
|
+
pseudo = -_inner(adjoint.detach(), applied, None)
|
|
386
|
+
if pseudo.requires_grad:
|
|
387
|
+
found = torch.autograd.grad(
|
|
388
|
+
pseudo,
|
|
389
|
+
[parameter for _, parameter in wanted],
|
|
390
|
+
allow_unused=True,
|
|
391
|
+
)
|
|
392
|
+
for (index, _), gradient in zip(wanted, found, strict=True):
|
|
393
|
+
gradients[index] = gradient
|
|
394
|
+
return adjoint, None, None, None, *gradients
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@dataclass(frozen=True)
|
|
398
|
+
class _Settings:
|
|
399
|
+
"""Everything the backward pass has to reconstruct the system from."""
|
|
400
|
+
|
|
401
|
+
normal: Operator
|
|
402
|
+
tikhonov: float
|
|
403
|
+
terms: Sequence[Regularizer]
|
|
404
|
+
preconditioner: Operator | None
|
|
405
|
+
max_iter: int
|
|
406
|
+
rtol: float
|
|
407
|
+
atol: float
|
|
408
|
+
backward_max_iter: int
|
|
409
|
+
backward_rtol: float
|
|
410
|
+
backward_atol: float
|
|
411
|
+
batch_dim: int | None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def conjugate_gradient(
|
|
415
|
+
normal: Operator,
|
|
416
|
+
rhs: torch.Tensor,
|
|
417
|
+
*,
|
|
418
|
+
x0: torch.Tensor | None = None,
|
|
419
|
+
regularizers: Iterable[Regularizer] = (),
|
|
420
|
+
preconditioner: Operator | None = None,
|
|
421
|
+
max_iter: int = 10,
|
|
422
|
+
rtol: float = 0.0,
|
|
423
|
+
atol: float = 0.0,
|
|
424
|
+
batch_dim: int | None = None,
|
|
425
|
+
parameters: Iterable[torch.Tensor] = (),
|
|
426
|
+
backward_max_iter: int | None = None,
|
|
427
|
+
backward_rtol: float | None = None,
|
|
428
|
+
backward_atol: float | None = None,
|
|
429
|
+
warn_indefinite: bool = True,
|
|
430
|
+
) -> CGResult:
|
|
431
|
+
r"""Solve the regularised normal equations by conjugate gradient.
|
|
432
|
+
|
|
433
|
+
Parameters
|
|
434
|
+
----------
|
|
435
|
+
normal
|
|
436
|
+
:math:`A^H A`, as a callable taking and returning one tensor.
|
|
437
|
+
rhs
|
|
438
|
+
:math:`A^H y`.
|
|
439
|
+
x0
|
|
440
|
+
Starting iterate. ``None`` starts from zero, which also saves the first
|
|
441
|
+
operator application, because the residual is then the right-hand side.
|
|
442
|
+
regularizers
|
|
443
|
+
The :class:`Regularizer` terms. Identity terms are summed into one
|
|
444
|
+
scalar and applied without an operator call.
|
|
445
|
+
preconditioner
|
|
446
|
+
:math:`M^{-1}`, applied to the residual each iteration. This is what
|
|
447
|
+
replaces density compensation for a non-Cartesian acquisition: the
|
|
448
|
+
weighting belongs in the solver, where it changes only the path taken,
|
|
449
|
+
rather than in the data, where it changes the answer.
|
|
450
|
+
max_iter
|
|
451
|
+
Iteration cap.
|
|
452
|
+
rtol, atol
|
|
453
|
+
Stop once ``||r|| <= atol + rtol * ||b||``. Both zero, the default,
|
|
454
|
+
runs the full iteration count and never reads a value back to the host,
|
|
455
|
+
so the loop does not synchronise.
|
|
456
|
+
batch_dim
|
|
457
|
+
Axis along which the problem is a batch of independent systems, each
|
|
458
|
+
with its own step size. ``None`` treats the whole tensor as one system.
|
|
459
|
+
parameters
|
|
460
|
+
Tensors inside ``normal`` or the regularizers that gradients are wanted
|
|
461
|
+
for. A closure cannot be inspected, so they are named here.
|
|
462
|
+
backward_max_iter, backward_rtol, backward_atol
|
|
463
|
+
Settings for the solve the backward pass runs. Each defaults to its
|
|
464
|
+
forward counterpart.
|
|
465
|
+
warn_indefinite
|
|
466
|
+
Whether to warn when the recurrence meets negative curvature.
|
|
467
|
+
|
|
468
|
+
Returns
|
|
469
|
+
-------
|
|
470
|
+
CGResult
|
|
471
|
+
The iterate, and what the iteration met on the way.
|
|
472
|
+
|
|
473
|
+
Notes
|
|
474
|
+
-----
|
|
475
|
+
Differentiable in ``rhs`` and in ``parameters``, by implicit
|
|
476
|
+
differentiation rather than by unrolling: memory is flat in ``max_iter``
|
|
477
|
+
and the backward pass is one more solve.
|
|
478
|
+
|
|
479
|
+
Examples
|
|
480
|
+
--------
|
|
481
|
+
>>> import torch
|
|
482
|
+
>>> from torchsolve import Regularizer, conjugate_gradient
|
|
483
|
+
>>> matrix = torch.tensor([[4.0, 1.0], [1.0, 3.0]])
|
|
484
|
+
>>> truth = torch.tensor([1.0, 2.0])
|
|
485
|
+
>>> result = conjugate_gradient(lambda v: matrix @ v, matrix @ truth, max_iter=8)
|
|
486
|
+
>>> bool(torch.allclose(result.solution, truth, atol=1e-5))
|
|
487
|
+
True
|
|
488
|
+
|
|
489
|
+
Regularising towards zero shrinks the answer:
|
|
490
|
+
|
|
491
|
+
>>> pulled = conjugate_gradient(
|
|
492
|
+
... lambda v: matrix @ v,
|
|
493
|
+
... matrix @ truth,
|
|
494
|
+
... regularizers=[Regularizer(100.0)],
|
|
495
|
+
... max_iter=8,
|
|
496
|
+
... )
|
|
497
|
+
>>> bool(pulled.solution.norm() < result.solution.norm())
|
|
498
|
+
True
|
|
499
|
+
|
|
500
|
+
A gradient reaches the right-hand side through the solve:
|
|
501
|
+
|
|
502
|
+
>>> data = (matrix @ truth).requires_grad_(True)
|
|
503
|
+
>>> conjugate_gradient(lambda v: matrix @ v, data, max_iter=8).solution.sum().backward()
|
|
504
|
+
>>> bool(data.grad.abs().sum() > 0)
|
|
505
|
+
True
|
|
506
|
+
"""
|
|
507
|
+
if max_iter < 1:
|
|
508
|
+
raise ValueError(f"max_iter must be at least 1, got {max_iter}")
|
|
509
|
+
if rtol < 0 or atol < 0:
|
|
510
|
+
raise ValueError("tolerances must not be negative")
|
|
511
|
+
|
|
512
|
+
system = _assemble(normal, rhs, regularizers)
|
|
513
|
+
held = tuple(parameters)
|
|
514
|
+
differentiating = torch.is_grad_enabled() and (
|
|
515
|
+
system.rhs.requires_grad or any(one.requires_grad for one in held)
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if not differentiating:
|
|
519
|
+
result = _iterate(
|
|
520
|
+
system,
|
|
521
|
+
x0,
|
|
522
|
+
preconditioner=preconditioner,
|
|
523
|
+
max_iter=max_iter,
|
|
524
|
+
rtol=rtol,
|
|
525
|
+
atol=atol,
|
|
526
|
+
batch_dim=batch_dim,
|
|
527
|
+
)
|
|
528
|
+
else:
|
|
529
|
+
settings = _Settings(
|
|
530
|
+
normal=system.normal,
|
|
531
|
+
tikhonov=system.tikhonov,
|
|
532
|
+
terms=system.terms,
|
|
533
|
+
preconditioner=preconditioner,
|
|
534
|
+
max_iter=max_iter,
|
|
535
|
+
rtol=rtol,
|
|
536
|
+
atol=atol,
|
|
537
|
+
backward_max_iter=max_iter
|
|
538
|
+
if backward_max_iter is None
|
|
539
|
+
else backward_max_iter,
|
|
540
|
+
backward_rtol=rtol if backward_rtol is None else backward_rtol,
|
|
541
|
+
backward_atol=atol if backward_atol is None else backward_atol,
|
|
542
|
+
batch_dim=batch_dim,
|
|
543
|
+
)
|
|
544
|
+
record: dict[str, Any] = {}
|
|
545
|
+
solution = _ImplicitSolve.apply(system.rhs, x0, settings, record, *held)
|
|
546
|
+
result = CGResult(solution=solution, **record)
|
|
547
|
+
|
|
548
|
+
if not result.definite and warn_indefinite:
|
|
549
|
+
warnings.warn(
|
|
550
|
+
"conjugate gradient met negative curvature: the operator is not "
|
|
551
|
+
"positive definite, so the answer is not a minimiser. It is still "
|
|
552
|
+
"the best iterate reached. Raise the regularisation, stop earlier, "
|
|
553
|
+
"or keep the whole transfer if it is a compressed one.",
|
|
554
|
+
stacklevel=2,
|
|
555
|
+
)
|
|
556
|
+
return result
|