Skip to content

Fix several bugs in iterative linear solvers - #604

Draft
yguclu wants to merge 11 commits into
yguclu-improve-Geometryfrom
yguclu-fix-linear-solvers
Draft

yguclu wants to merge 11 commits into
yguclu-improve-Geometryfrom
yguclu-fix-linear-solvers

Conversation

@yguclu

@yguclu yguclu commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Fix several bugs in the iterative solvers of psydac.linalg.solvers, as reported in #603. Each bug is fixed in a separate commit, together with a regression test that fails before the fix.

Fixes #603

Changes

  • MINRES and LSMR: import inf from math, which MINRES used without importing. Replace np.infty, removed in NumPy 2.0, in LSMR.
  • GMRES: build the solution from all k+1 Arnoldi vectors instead of k. The returned x now matches the reported residual.
  • GMRES: support complex operators by using the Gram-Schmidt coefficients Q[i]^H p and unitary Givens rotations.
  • Preconditioned BiCGSTAB: conjugate the shadow residual rp0 in the inner products, as in the unpreconditioned version. This fixes complex operators.
  • CG: initialize the loop counter before the loop, so that maxiter=1 no longer raises UnboundLocalError.
  • LSMR: restore SciPy's early exits for b = 0 (return x = 0) and A^H (b - A x0) = 0 (return x0), which previously caused a ZeroDivisionError.

Behavior notes

  • niter is unchanged for CG and GMRES. Both still count the initial residual as iteration 1. Because of this, GMRES reports niter = maxiter + 1 when it does not converge.
  • LSMR can now return without iterating (niter = 0) when b = 0 or when x0 is already a least-squares solution.

Tests

New tests in psydac/linalg/tests/test_solvers.py:

  • test_solver_diagonal: MINRES with a zero operator, and LSMR with 2*I
  • test_GMRES_solve: compares the reported residual with the true residual, for real and complex operators
  • test_ConjugateGradient_solve_maxiter_1: CG with and without preconditioner
  • test_LSMR_solve_early_exit

The skip of complex operators for preconditioned BiCGSTAB was removed from test_solver_tridiagonal.

Not addressed

These are listed in #603 and left for follow-up work:

Notes

PR can be merged only after #527!

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 4 complexity · 0 duplication

Metric Results
Complexity 4
Duplication 0

View in Codacy

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.

@yguclu yguclu changed the title Fix several bugs in linear solvers Fix several bugs in iterative linear solvers Oct 5, 2026
@yguclu yguclu linked an issue Oct 6, 2026 that may be closed by this pull request
yguclu added 11 commits October 6, 2026 10:27
`MinimumResidual.solve` used `inf` without importing it, raising a
NameError when the operator norm or the solution norm vanishes.
`LSMR.solve` used `np.infty`, which was removed in NumPy 2.0, raising
an AttributeError when the residual vanishes exactly. Both now use
`math.inf`.

Add `test_solver_diagonal`, which reaches these branches with diagonal
operators (zero operator for MINRES, 2*I for LSMR).
`GMRES.solve` built the solution from the first k Arnoldi vectors only,
dropping the last one. The returned x was therefore always one iteration
behind the reported residual, and `success` could be True for an
unconverged solution. Check convergence at the end of each iteration and
use all k+1 Arnoldi vectors. The `niter` convention is unchanged.

Add `test_GMRES_solve`, which compares the reported residual with the
true residual b - A x.
The Arnoldi process computed the Gram-Schmidt coefficients as
`p.inner(Q[i])`, i.e. conj(p) . Q[i], instead of `Q[i].inner(p)`, so the
Krylov basis was not orthogonal for complex operators. The Givens
rotations were also written for real numbers only. Use the coefficients
Q[i]^H p and the unitary rotation [[conj(c), conj(s)], [-s, c]].

Complex systems now converge in at most n iterations, as in the real
case. Extend `test_GMRES_solve` to complex operators.
`BiConjugateGradientStabilized.solve_with_pc` computed the inner products
with the shadow residual rp0 as `vp.inner(rp0)` and `rp.inner(rp0)`,
conjugating the wrong vector. This conjugated alpha and beta, so the
solver failed for complex operators. Use `rp0.inner(vp)` and
`rp0.inner(rp)`, as in the unpreconditioned version.

Remove the corresponding skip from `test_solver_tridiagonal`.
In `ConjugateGradient.solve_without_pc` and `solve_with_pc` the loop
counter starts at 2, because iteration 1 is the initial residual. With
maxiter=1 the loop body never runs, and the counter was used unassigned
to fill `niter`, raising an UnboundLocalError. Initialize the counter to
1 before the loop; the `niter` convention is unchanged.

Add `test_ConjugateGradient_solve_maxiter_1`.
The SciPy port of `LSMR.solve` dropped the early exits taken before the
main loop. With b = 0 the stopping test divided by norm(b) = 0, and when
A^H (b - A x0) = 0 the plane rotations divided by zero. Now return x = 0
if b = 0, and return x0 if it is already a least-squares solution.

Add `test_LSMR_solve_early_exit`.
@yguclu
yguclu force-pushed the yguclu-fix-linear-solvers branch from 7cae052 to caf0703 Compare October 6, 2026 08:31

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Several bugs in iterative solvers

1 participant