Add damp support to CG solver (closes #406)#752
Conversation
The CG solver only solved Op x = y. Re-introduce the damp coefficient (removed in v2) so it can solve the damped system (Op + damp*I) x = y, consistent with cgls/lsqr. Damping is applied by adding damp*c to every operator application Op*c in the iterations (and to the initial residual when x0 is given); damp=0 reproduces the previous behaviour exactly. Thread damp through cg(), CG.solve and CG.setup, and document it. Add test_cg_damp verifying that internal damping matches both adding damp*I to the operator (with damp=0) and the dense (Op + damp*I)^-1 y, and that damp=0 still recovers the undamped solution.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
| Duplication | 6 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
| Solve the :math:`\mathbf{y} = \mathbf{Op}\,\mathbf{x}` problem using conjugate gradient | ||
| iterations [1]_. | ||
|
|
||
| When a non-zero damping coefficient :math:`\epsilon` is provided to ``setup``/``solve``, |
There was a problem hiding this comment.
Follow the same style used in the notes of the other solvers. Simply:
Solve the :math:`\mathbf{y} = (\mathbf{Op} + + \epsilon\mathbf{I})\,\mathbf{x}` problem using conjugate gradient iterations [1]_, where :math:`\epsilon` is the damping coefficient.
There was a problem hiding this comment.
Done in 6828c57 — reduced the CG Notes to the single damped-system sentence matching the other solvers.
| Initial guess | ||
| niter : :obj:`int`, optional | ||
| Number of iterations | ||
| damp : :obj:`float`, optional |
There was a problem hiding this comment.
Just Damping coefficient as in the other solvers
| niter : :obj:`int`, optional | ||
| Number of iterations (default to ``None`` in case a user wants to | ||
| manually step over the solver) | ||
| damp : :obj:`float`, optional |
There was a problem hiding this comment.
Just Damping coefficient as in the other solvers
| self.ncp.subtract(self.y, self.Op.matvec(x), out=self.r) | ||
| # account for the damping term in the initial residual | ||
| if self.damp != 0.0: | ||
| self.r -= self.damp * x |
There was a problem hiding this comment.
Follow the same pattern as self.r = self.y - self.Op.matvec(x) for not preallocate vs self.ncp.subtract(self.y, self.Op.matvec(x), out=self.r) for preallocate
There was a problem hiding this comment.
Done in 6828c57 — setup now follows the non-preallocate self.r = self.r - damp * x vs preallocate self.ncp.subtract(..., out=self.r) pattern.
|
|
||
| """ | ||
| Opc = self.Op.matvec(self.c) | ||
| # solve the damped system (Op + damp*I) x = y by adding the damping |
There was a problem hiding this comment.
too long comment, just # add dampling contribution is enough
There was a problem hiding this comment.
Done in 6828c57 — trimmed to # add damping contribution.
| internally as zero vector | ||
| niter : :obj:`int`, optional | ||
| Number of iterations | ||
| damp : :obj:`float`, optional |
There was a problem hiding this comment.
Done in 6828c57 — trimmed to # add damping contribution.
|
|
||
| @pytest.mark.parametrize("par", [(par1), (par2)]) | ||
| def test_cg_damp(par): | ||
| """CG with damping solves the damped system (Op + damp*I) x = y (issue #406). |
There was a problem hiding this comment.
Done in 6828c57 — dropped the (issue #406). reference.
| # adding damp*I to the operator and using damp=0 must match internal damping | ||
| Aop_damped = MatrixMult(A + damp * np.eye(n), dtype=par["dtype"]) | ||
|
|
||
| for prealloc in [False, True]: |
There was a problem hiding this comment.
for preallocate in [False, True]: to be consistent with other tests naming
There was a problem hiding this comment.
Done in 6828c57 — renamed the loop variable to for preallocate in [False, True]:.
| A = np.random.normal(0, 1, (n, n)) + par["imag"] * np.random.normal(0, 1, (n, n)) | ||
| A = np.conj(A).T @ A + np.eye(n) # symmetric positive-definite | ||
| Aop = MatrixMult(A, dtype=par["dtype"]) | ||
| y = np.random.normal(0, 1, n) + par["imag"] * np.random.normal(0, 1, n) |
There was a problem hiding this comment.
Like in other tests create x
x = np.ones(par["nx"]) + par["imag"] * np.ones(par["nx"])
y = Aop * x
There was a problem hiding this comment.
Done in 6828c57 — now builds y = Aop * x from a known x.
- shorten damp docstrings to 'Damping coefficient' (cls_basic CG.setup/solve, basic.cg) and simplify CG Notes to a single damped-system sentence - follow preallocate vs non-preallocate residual-update pattern for the initial damping correction in setup - shorten the damping comment in step to '# add damping contribution' - test_cg_damp: build y from a known x (y = Aop * x), rename loop var to 'preallocate', drop issue ref from docstring
|
Thanks for the review! Addressed all points in 6828c57:
(The red |
Thanks. Please reply to each comment with link to commit you fixed the issue. |
Motivation
CGcurrently only solvesconsistent with
cgls/lsqr(which both exposedamp). Thedampargument used to exist onCGas an unused stub and was removed in v2; this PR re-introduces it with a working implementation.This matches the issue's definition of done ("Added implementation with$(A + \text{damp},I)x = y$ , equivalent to adding
damp != 0") and the semantics @mrava87 confirmed there: solvedamp*Ito the operator and usingdamp=0.Implementation
Damping is applied by adding$\epsilon\mathbf{c}$ to every application of the operator $\mathbf{Op},\mathbf{c}$ in the iterations (and $\epsilon\mathbf{x}_0$ to the initial residual when $\mathbf{Op} + \epsilon\mathbf{I}$ :
x0is given) — i.e. the operator is implicitly replaced bydampis threaded throughcg(),CG.solveandCG.setup(default0.0). Withdamp=0.0the code path is identical to before, so existing behaviour is unchanged.Opmust remain square and SPD, whichVerification
Added
test_cg_dampwhich checks, for bothpreallocatemodes, the equivalence @mrava87 suggested in the issue:cg(Op, y, damp=d)== addingd*Ito the operatorcg(Op + d*I, y, damp=0)(Op + d*I)^{-1} ydamp=0still recovers the undampedOp^{-1} yFull
pytests/test_solver.pypasses (100 tests),ruff checkclean.