Repository navigation
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 4 |
| Duplication | 0 |
🟢 Coverage 97.37% diff coverage · +0.01% coverage variation
Metric Results Coverage variation ✅ +0.01% coverage variation Diff coverage ✅ 97.37% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (f2da0cd) 51844 33084 63.81% Head commit (778f084) 103790 (+51946) 66242 (+33158) 63.82% (+0.01%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#604) 76 74 97.37% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
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.
7cae052 to
caf0703
Compare
`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`.
caf0703 to
dd703ea
Compare
campospinto
left a comment
There was a problem hiding this comment.
thanks for this PR which seems good for me.
I have mostly one question:
the PR descriptions states that it fixes #603 but also says that a few issues listed in #603 are not addressed in this PR (eg MINRES reports success=True for an unsolvable system), so there should probably be two different issues, one fixed and one not yet?
jowezarek
left a comment
There was a problem hiding this comment.
I added a few comments related to the new tests (and a remark related to define_data). My main concern is that LSMR returning success in case of an infeasible system (0 matrix) is misleading. I did not check the solver details as I am not familiar with the theory.
| s = V.starts[0] | ||
|
|
||
| # Build banded matrix with 2p+1 diagonals: must be symmetric and positive definite | ||
| # Here we assign value 2*p on main diagonal and -1 on other diagonals |
There was a problem hiding this comment.
I suppose this function would benefit from updated comments / docstrings.
It's no longer true that the main diagonal entries are 2p and the off-diagonal entries are -1. Now, one can pass matrix_data (length >= 3), but only the first 3 values are used to fill the matrix. Also, the resulting matrix could be not symmetric and not positive definit.
|
|
||
| r = b - A @ x | ||
| assert np.isclose(info['res_norm'], np.sqrt(r.inner(r).real), rtol=1e-8, atol=1e-12) | ||
| assert info['success'] == (maxiter == n) |
There was a problem hiding this comment.
I understand the previous assert (test whether the true residual is reported in info['res_norm'], but what is the purpose of this assertion? A and b are chosen in such a way that the solver requires >= 12 iterations (1+12=13 niter) to converge? But why is this checked?
|
|
||
| #=============================================================================== | ||
| # With maxiter=1, CG only evaluates the residual of the initial guess | ||
| @pytest.mark.parametrize('use_jacobi_pc', [False, True]) |
There was a problem hiding this comment.
Regarding Behavior notes of the PR description: GMRES also reports niter=maxiter+1 in case of convergence in case of convergence with the last iteration (see test above). Question: Why not change it such that niter=1 actually does exactly 1 iteration (from initial guess to improved guess)?
| expected = xe if rhs else V.zeros() | ||
| assert np.array_equal(x.toarray(), expected.toarray()) | ||
| assert info['niter'] == 0 | ||
| assert info['success'] |
There was a problem hiding this comment.
Now LSMR reports success in case of a zero matrix and a non-zero rhs. Is that really intended?
e-moral-sanchez
left a comment
There was a problem hiding this comment.
I compared the changes against PETSc implementations of BiCGStab and GMRES and found some discrepancies.
|
|
||
| x = inverse(A, solver, tol=1e-10) @ b | ||
|
|
||
| assert np.array_equal(x.toarray(), np.full(6, expected)) |
There was a problem hiding this comment.
Why would the expected solution value depend on the solver if it converges? I don't think this test brings anything.
|
|
||
| r = b - A @ x | ||
| assert np.isclose(info['res_norm'], np.sqrt(r.inner(r).real), rtol=1e-8, atol=1e-12) | ||
| assert info['success'] == (maxiter == n) |
There was a problem hiding this comment.
This is only true with exact precision. Also here we set tol=1e-10. I don't think this test brings anything.
|
|
||
| assert np.array_equal(x.toarray(), V.zeros().toarray()) | ||
| assert info['niter'] == 1 | ||
| assert not info['success'] |
There was a problem hiding this comment.
Do we need to test this?
| assert np.array_equal(x.toarray(), expected.toarray()) | ||
| assert info['niter'] == 0 | ||
| assert info['success'] | ||
|
|
There was a problem hiding this comment.
I also would not add this test, it brings nothing.
| A.dot(pp, out=v) | ||
| pc.dot(v, out=vp) | ||
| alphap = rhop / vp.inner(rp0) | ||
| alphap = rhop / rp0.inner(vp) |
There was a problem hiding this comment.
I agree with this change, but I think it's worth commenting it. In PSYDAC inner takes the conjugate of the first argument (
) . In PETSc, they take it in the second argument (https://petsc.org/release/manualpages/Vec/VecDot/) and in their implementation they do "VecDot(V, RP, &d1))" see line 78 in https://petsc.org/release/src/ksp/ksp/impls/bcgs/bcgs.c.html#KSPBCGS| test1 = normr / normb | ||
| if (normA * normr) != 0:test2 = normar / (normA * normr) | ||
| else:test2 = np.infty | ||
| else:test2 = inf |
There was a problem hiding this comment.
Is this change necessary?
| # update the residual vector | ||
| beta.append(- sn[k] * beta[k]) | ||
| beta[k] *= cn[k] | ||
| beta[k] *= cn[k].conjugate() |
There was a problem hiding this comment.
I agree with this change, and here also I think it's good to comment that it is consistent with the PETSc implementation: see line 377 of https://petsc.org/release/src/ksp/ksp/impls/gmres/gmres.c.html#KSPGMRES
| h[k] *= cn[k] | ||
| h[k] += sn[k] * h[k+1] | ||
| h[k] *= cn[k].conjugate() | ||
| h[k] += sn[k].conjugate() * h[k+1] |
There was a problem hiding this comment.
This one is not conjugated in PETSc GMRES: see line 378 in https://petsc.org/release/src/ksp/ksp/impls/gmres/gmres.c.html#KSPGMRES
| h[i] *= cn[i] | ||
| h[i] += sn[i] * h[i+1] | ||
| h[i] *= cn[i].conjugate() | ||
| h[i] += sn[i].conjugate() * h[i+1] |
There was a problem hiding this comment.
This one is not conjugated in PETSc GMRES: see line 378 in https://petsc.org/release/src/ksp/ksp/impls/gmres/gmres.c.html#KSPGMRES
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
inffrommath, which MINRES used without importing. Replacenp.infty, removed in NumPy 2.0, in LSMR.k+1Arnoldi vectors instead ofk. The returnedxnow matches the reported residual.Q[i]^H pand unitary Givens rotations.rp0in the inner products, as in the unpreconditioned version. This fixes complex operators.maxiter=1no longer raisesUnboundLocalError.b = 0(returnx = 0) andA^H (b - A x0) = 0(returnx0), which previously caused aZeroDivisionError.Behavior notes
niteris unchanged for CG and GMRES. Both still count the initial residual as iteration 1. Because of this, GMRES reportsniter = maxiter + 1when it does not converge.niter = 0) whenb = 0or whenx0is 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 with2*Itest_GMRES_solve: compares the reported residual with the true residual, for real and complex operatorstest_ConjugateGradient_solve_maxiter_1: CG with and without preconditionertest_LSMR_solve_early_exitThe 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:
success=Truefor an unsolvable system.niterdiffers between solvers.