Skip to content

Commit 6d076f4

Browse files
authored
Merge pull request #752 from gaoflow/feature/cg-damp
Add damp support to CG solver (closes #406)
2 parents df1c0f2 + 6828c57 commit 6d076f4

3 files changed

Lines changed: 63 additions & 3 deletions

File tree

pylops/optimization/basic.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def cg(
2222
y: NDArray,
2323
x0: NDArray | None = None,
2424
niter: int = 10,
25+
damp: float = 0.0,
2526
tol: float = 1e-4,
2627
rtol: float = 0.0,
2728
rtol1: float = 0.0,
@@ -45,6 +46,8 @@ def cg(
4546
Initial guess
4647
niter : :obj:`int`, optional
4748
Number of iterations
49+
damp : :obj:`float`, optional
50+
Damping coefficient
4851
tol : :obj:`float`, optional
4952
Absolute tolerance on residual norm. Stops the solver when the
5053
residual norm is below this value.
@@ -101,6 +104,7 @@ def cg(
101104
x0=x0,
102105
tol=tol,
103106
niter=niter,
107+
damp=damp,
104108
show=show,
105109
itershow=itershow,
106110
preallocate=preallocate,

pylops/optimization/cls_basic.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ class CG(Solver):
6565
6666
Notes
6767
-----
68-
Solve the :math:`\mathbf{y} = \mathbf{Op}\,\mathbf{x}` problem using conjugate gradient
69-
iterations [1]_.
68+
Solve the :math:`\mathbf{y} = (\mathbf{Op} + \epsilon\mathbf{I})\,\mathbf{x}` problem
69+
using conjugate gradient iterations [1]_, where :math:`\epsilon` is the damping
70+
coefficient.
7071
7172
.. [1] Hestenes, M R., Stiefel, E., “Methods of Conjugate Gradients for Solving
7273
Linear Systems”, Journal of Research of the National Bureau of Standards.
@@ -134,6 +135,7 @@ def setup(
134135
y: NDArray,
135136
x0: NDArray | None = None,
136137
niter: int | None = None,
138+
damp: float = 0.0,
137139
tol: float = 1e-4,
138140
preallocate: bool = False,
139141
show: bool = False,
@@ -150,6 +152,8 @@ def setup(
150152
niter : :obj:`int`, optional
151153
Number of iterations (default to ``None`` in case a user wants to
152154
manually step over the solver)
155+
damp : :obj:`float`, optional
156+
Damping coefficient
153157
tol : :obj:`float`, optional
154158
Absolute tolerance on residual norm. Stops the solver when the
155159
residual norm is below this value.
@@ -170,6 +174,7 @@ def setup(
170174
"""
171175
self.y = y
172176
self.niter = niter
177+
self.damp = damp
173178
self.tol = tol
174179

175180
self.ncp = get_array_module(y)
@@ -187,6 +192,12 @@ def setup(
187192
else:
188193
self.r = self.ncp.empty_like(self.y)
189194
self.ncp.subtract(self.y, self.Op.matvec(x), out=self.r)
195+
# account for the damping term in the initial residual
196+
if self.damp != 0.0:
197+
if not self.preallocate:
198+
self.r = self.r - self.damp * x
199+
else:
200+
self.ncp.subtract(self.r, self.damp * x, out=self.r)
190201
self.c = self.r.copy()
191202
self.kold = self.ncp.abs(self.r.dot(self.r.conj()))
192203

@@ -221,6 +232,9 @@ def step(self, x: NDArray, show: bool = False) -> NDArray:
221232
222233
"""
223234
Opc = self.Op.matvec(self.c)
235+
# add damping contribution
236+
if self.damp != 0.0:
237+
Opc = Opc + self.damp * self.c
224238
cOpc = self.ncp.abs(self.c.dot(Opc.conj()))
225239
a = self.kold / cOpc
226240
if not self.preallocate:
@@ -317,6 +331,7 @@ def solve(
317331
y: NDArray,
318332
x0: NDArray | None = None,
319333
niter: int = 10,
334+
damp: float = 0.0,
320335
tol: float = 1e-4,
321336
preallocate: bool = False,
322337
show: bool = False,
@@ -333,6 +348,8 @@ def solve(
333348
internally as zero vector
334349
niter : :obj:`int`, optional
335350
Number of iterations
351+
damp : :obj:`float`, optional
352+
Damping coefficient
336353
tol : :obj:`float`, optional
337354
Absolute tolerance on residual norm. Stops the solver when the
338355
residual norm is below this value.
@@ -360,7 +377,13 @@ def solve(
360377
361378
"""
362379
x = self.setup(
363-
y=y, x0=x0, niter=niter, tol=tol, preallocate=preallocate, show=show
380+
y=y,
381+
x0=x0,
382+
niter=niter,
383+
damp=damp,
384+
tol=tol,
385+
preallocate=preallocate,
386+
show=show,
364387
)
365388
x = self.run(x, niter, show=show, itershow=itershow)
366389
self.finalize(show)

pytests/test_solver.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,39 @@ def test_cg(par):
102102
assert_array_almost_equal(x, xinv, decimal=4)
103103

104104

105+
@pytest.mark.parametrize("par", [(par1), (par2)])
106+
def test_cg_damp(par):
107+
"""CG with damping solves the damped system (Op + damp*I) x = y.
108+
109+
Verify equivalence between internal damping, adding ``damp*I`` to the
110+
operator (with ``damp=0`` in the solver), and the dense solution; and that
111+
``damp=0`` reproduces the undamped result.
112+
"""
113+
np.random.seed(10)
114+
115+
n = par["nx"]
116+
A = np.random.normal(0, 1, (n, n)) + par["imag"] * np.random.normal(0, 1, (n, n))
117+
A = np.conj(A).T @ A + np.eye(n) # symmetric positive-definite
118+
Aop = MatrixMult(A, dtype=par["dtype"])
119+
x = np.ones(n) + par["imag"] * np.ones(n)
120+
y = Aop * x
121+
damp = 0.8
122+
123+
x_dense = np.linalg.solve(A + damp * np.eye(n), y)
124+
# adding damp*I to the operator and using damp=0 must match internal damping
125+
Aop_damped = MatrixMult(A + damp * np.eye(n), dtype=par["dtype"])
126+
127+
for preallocate in [False, True]:
128+
x_int = cg(Aop, y, niter=4 * n, tol=1e-10, damp=damp, preallocate=preallocate)[0]
129+
x_ext = cg(Aop_damped, y, niter=4 * n, tol=1e-10, preallocate=preallocate)[0]
130+
assert_array_almost_equal(x_int, x_dense, decimal=5)
131+
assert_array_almost_equal(x_int, x_ext, decimal=5)
132+
133+
# damp=0 reproduces the undamped solution
134+
x_undamped = cg(Aop, y, niter=4 * n, tol=1e-10, damp=0.0)[0]
135+
assert_array_almost_equal(x_undamped, np.linalg.solve(A, y), decimal=5)
136+
137+
105138
@pytest.mark.parametrize(
106139
"par", [(par1), (par2), (par3), (par4), (par1j), (par2j), (par3j), (par3j)]
107140
)

0 commit comments

Comments
 (0)