Skip to content

Commit 9a15c77

Browse files
jalveszAlexanderGSCjvdp1Copilot
authored
feat (linalg_iterative) : add gmres solver to the iterative methods library (#1186)
* start gmres draft * add comments * remove kdim from test * use dot_product * gmres: add Hilbert matrix test * linalg: add MGS reorthogonalization to GMRES * fix: rename loop variable to avoid fypp collision * fix: hilbert sp tolerance test * fix: hilbert sp tolerance test * fix: hilbert dp tolerance test * fix: hilbert dp tolerance test for Intel 2024 1 cmake * Update test/linalg/test_linalg_solve_iterative.fypp Co-authored-by: Jeremie Vandenplas <jeremie.vandenplas@gmail.com> * Update test/linalg/test_linalg_solve_iterative.fypp Co-authored-by: Jeremie Vandenplas <jeremie.vandenplas@gmail.com> * Update test/linalg/test_linalg_solve_iterative.fypp Co-authored-by: Jeremie Vandenplas <jeremie.vandenplas@gmail.com> * Update test/linalg/test_linalg_solve_iterative.fypp Co-authored-by: Jeremie Vandenplas <jeremie.vandenplas@gmail.com> * Update test/linalg/test_linalg_solve_iterative.fypp Co-authored-by: Jeremie Vandenplas <jeremie.vandenplas@gmail.com> * fix declarations * make gmres implementation flexible by allowing switching between memory efficiency or runtime speed * add example * convert static enum to integer helper function to determine the number of colums of the gmres workspace * simplify iterations logic * replace manual given rotations and the triangular solve by lapack kernels * try removing the hilbert test for sp as it is meaningless for the selected size * Refactor coverage report generation in workflow Test proposal by copilot to fix coverage report issues. Testing in an actual PR exhibiting coverage issues because of the examples. * Restrict fpm-deployment coverage upload to the filtered `src/` report (#13) * Initial plan * Disable Codecov auto-search in coverage job --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * add review corrections * parametrize system size --------- Co-authored-by: AlexanderGSC <alexander.gsc@gmail.com> Co-authored-by: Jeremie Vandenplas <jeremie.vandenplas@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent a2ef8aa commit 9a15c77

8 files changed

Lines changed: 609 additions & 8 deletions

File tree

.github/workflows/fpm-deployment.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,20 +114,25 @@ jobs:
114114
115115
- name: Create coverage report
116116
run: |
117+
set -euo pipefail
117118
mkdir -p ${{ env.COV_DIR }}
118119
mv ./build/gfortran_*/*/* ${{ env.COV_DIR }}
119120
lcov --capture --initial --base-directory . --directory ${{ env.COV_DIR }} --output-file ${{ env.COV_DIR }}/coverage.base --ignore-errors mismatch
120121
lcov --capture --base-directory . --directory ${{ env.COV_DIR }} --output-file ${{ env.COV_DIR }}/coverage.capture --ignore-errors mismatch
121-
lcov --add-tracefile ${{ env.COV_DIR }}/coverage.base --add-tracefile ${{ env.COV_DIR }}/coverage.capture --output-file ${{ env.COV_DIR }}/coverage.info --ignore-errors mismatch
122-
# Exclude example files from coverage: use both absolute-path pattern (*/example/*)
123-
# and relative-path pattern (example/**) because lcov 2.x requires ** for recursive matching
124-
# and the fpm build stores paths relative to the project root.
125-
lcov --remove ${{ env.COV_DIR }}/coverage.info '*/example/*' 'example/**' --output-file ${{ env.COV_DIR }}/coverage.info --ignore-errors unused,mismatch,inconsistent
122+
lcov --add-tracefile ${{ env.COV_DIR }}/coverage.base --add-tracefile ${{ env.COV_DIR }}/coverage.capture --output-file ${{ env.COV_DIR }}/coverage.full.info --ignore-errors mismatch
123+
# Keep only src files in final coverage report to avoid example/ and test/ sources affecting the metric.
124+
lcov --extract ${{ env.COV_DIR }}/coverage.full.info '*/src/*' 'src/**' --output-file ${{ env.COV_DIR }}/coverage.info --ignore-errors unused,mismatch,inconsistent
125+
echo "Files included in final coverage report:"
126+
grep '^SF:' ${{ env.COV_DIR }}/coverage.info | sed 's#^SF:##' | sort -u
126127
env:
127128
COV_DIR: build/coverage
128129

129130
- name: Upload coverage report
130131
uses: codecov/codecov-action@v4
131132
with:
132133
token: ${{ secrets.CODECOV_TOKEN }}
134+
# Upload only the lcov report rooted at src/; otherwise Codecov also
135+
# discovers generated *.gcov files under example/ and treats them as
136+
# primary analyzed sources.
137+
disable_search: true
133138
files: build/coverage/coverage.info

doc/specs/stdlib_linalg_iterative_solvers.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,4 +348,102 @@ The method uses 8 auxiliary vectors internally, requiring more memory than simpl
348348
#### Example 2
349349
```fortran
350350
{!example/linalg/example_solve_bicgstab_wilkinson.f90!}
351+
```
352+
353+
<!-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->
354+
### `stdlib_solve_gmres_kernel` subroutine
355+
356+
#### Description
357+
358+
Implements the restarted Generalized Minimal Residual (GMRES) method for solving the linear system \( Ax = b \), where \( A \) is a general linear operator defined via the `stdlib_linop` type. This is the core implementation, allowing custom matrix types, custom preconditioners, and user-managed workspace.
359+
360+
#### Syntax
361+
362+
`call ` [[stdlib_linalg_iterative_solvers(module):stdlib_solve_gmres_kernel(interface)]] ` (A, M, b, x, rtol, atol, maxiter, kdim, workspace, compact)`
363+
364+
#### Status
365+
366+
Experimental
367+
368+
#### Class
369+
370+
Subroutine
371+
372+
#### Argument(s)
373+
374+
`A`: `class(stdlib_linop_<kind>_type)` defining the linear operator. This argument is `intent(in)`.
375+
376+
`M`: `class(stdlib_linop_<kind>_type)` defining the preconditioner linear operator. This argument is `intent(in)`.
377+
378+
`b`: 1-D array of `real(<kind>)` defining the loading conditions of the linear system. This argument is `intent(in)`.
379+
380+
`x`: 1-D array of `real(<kind>)` which serves as the input initial guess and the output solution. This argument is `intent(inout)`.
381+
382+
`rtol` and `atol`: scalars of type `real(<kind>)` specifying the convergence test. For convergence, the following criterion is used \( || b - Ax ||^2 <= max(rtol^2 * || b ||^2 , atol^2 ) \). These arguments are `intent(in)`.
383+
384+
`maxiter`: scalar of type `integer` defining the maximum allowed number of iterations. This argument is `intent(in)`.
385+
386+
`kdim`: scalar of type `integer` defining the Krylov subspace size before restart. This argument is `intent(in)`.
387+
388+
`workspace`: scalar derived type of `type(stdlib_solver_workspace_<kind>_type)` holding the work array for the solver. This argument is `intent(inout)`.
389+
390+
`compact`: scalar of type `logical`. If true, GMRES stores only one preconditioned basis vector at a time and rebuilds the cycle-end correction through the preconditioner, reducing memory usage. If false, GMRES caches the full preconditioned basis for a faster cycle-end update at the cost of additional memory. Default is `.true.`. This argument is `intent(in)`.
391+
392+
#### Note
393+
394+
GMRES trades increasing memory and orthogonalization cost for robust convergence on general non-symmetric systems. The `compact` option allows choosing between a lower-memory layout and a slightly faster restart update.
395+
396+
<!-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->
397+
### `stdlib_solve_gmres` subroutine
398+
399+
#### Description
400+
401+
Provides a user-friendly interface to the restarted GMRES method for solving \( Ax = b \), supporting `dense` and `CSR_<kind>_type` matrices. GMRES is suitable for general non-symmetric systems and supports optional preconditioning, workspace reuse, and a storage-mode choice for balancing memory use against cycle-end update speed.
402+
403+
#### Syntax
404+
405+
`call ` [[stdlib_linalg_iterative_solvers(module):stdlib_solve_gmres(interface)]] ` (A, b, x [, di, rtol, atol, maxiter, restart, kdim, precond, M, workspace, compact])`
406+
407+
#### Status
408+
409+
Experimental
410+
411+
#### Class
412+
413+
Subroutine
414+
415+
#### Argument(s)
416+
417+
`A`: `dense` or `CSR_<kind>_type` matrix defining the linear system. This argument is `intent(in)`.
418+
419+
`b`: 1-D array of `real(<kind>)` defining the loading conditions of the linear system. This argument is `intent(in)`.
420+
421+
`x`: 1-D array of `real(<kind>)` which serves as the input initial guess and the output solution. This argument is `intent(inout)`.
422+
423+
`di` (optional): 1-D mask array of type `logical(int8)` defining the degrees of freedom subject to dirichlet boundary conditions. The actual boundary condition values should be stored in the `b` load array. This argument is `intent(in)`.
424+
425+
`rtol` and `atol` (optional): scalars of type `real(<kind>)` specifying the convergence test. For convergence, the following criterion is used \( || b - Ax ||^2 <= max(rtol^2 * || b ||^2 , atol^2 ) \). Default values are `rtol=1.e-5` and `atol=epsilon(1._<kind>)`. These arguments are `intent(in)`.
426+
427+
`maxiter` (optional): scalar of type `integer` defining the maximum allowed number of iterations. If no value is given, a default of `N` is set, where `N = size(b)`. This argument is `intent(in)`.
428+
429+
`restart` (optional): scalar of type `logical` indicating whether to restart the iteration with zero initial guess. Default is `.true.`. This argument is `intent(in)`.
430+
431+
`kdim` (optional): scalar of type `integer` defining the Krylov subspace size before restart. If no value is given, a default of `min(30, N)` is used. This argument is `intent(in)`.
432+
433+
`precond` (optional): scalar of type `integer` enabling to switch among the default preconditioners available with the following enum (`pc_none`, `pc_jacobi`). If no value is given, no preconditioning will be applied. This argument is `intent(in)`.
434+
435+
`M` (optional): scalar derived type of `class(stdlib_linop_<kind>_type)` defining a custom preconditioner linear operator. If given, `precond` will have no effect, and a pointer is set to this custom preconditioner. This argument is `intent(in)`.
436+
437+
`workspace` (optional): scalar derived type of `type(stdlib_solver_workspace_<kind>_type)` holding the work temporal array for the solver. If the user passes its own `workspace`, then internally a pointer is set to it, otherwise, memory will be internally allocated and deallocated before exiting the procedure. This argument is `intent(inout)`.
438+
439+
`compact` (optional): scalar of type `logical`. If true, GMRES stores only one preconditioned basis vector per iteration cycle and minimizes memory use. If false, GMRES caches the full preconditioned basis to speed up the cycle-end update. Default is `.true.`. This argument is `intent(in)`.
440+
441+
#### Note
442+
443+
GMRES is especially useful for general non-symmetric systems where CG and PCG do not apply. Its main cost is the Krylov basis storage and orthogonalization work within each restart cycle. With `compact=.true.`, the solver reduces workspace requirements significantly. With `compact=.false.`, it retains the previous cached-basis update strategy.
444+
445+
#### Example
446+
447+
```fortran
448+
{!example/linalg/example_solve_gmres.f90!}
351449
```

example/linalg/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ if (STDLIB_LINALG_ITERATIVE)
4848
ADD_EXAMPLE(solve_bicgstab)
4949
ADD_EXAMPLE(solve_bicgstab_wilkinson)
5050
ADD_EXAMPLE(solve_cg)
51+
ADD_EXAMPLE(solve_gmres)
5152
ADD_EXAMPLE(solve_pcg)
5253
ADD_EXAMPLE(solve_custom)
5354
endif()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
program example_solve_gmres
2+
use stdlib_kinds, only: dp
3+
use stdlib_linalg_iterative_solvers, only: stdlib_solve_gmres, pc_jacobi
4+
implicit none
5+
integer, parameter :: n = 4
6+
real(dp), parameter :: A(n,n) = reshape([5.0_dp, 1.0_dp, 2.0_dp, 0.0_dp, &
7+
0.0_dp, 4.0_dp, -1.0_dp, 1.0_dp, &
8+
1.0_dp, 0.0_dp, 3.0_dp, 2.0_dp, &
9+
2.0_dp, -1.0_dp, 0.0_dp, 6.0_dp], [n,n])
10+
real(dp), parameter :: x0(n) = [0.2_dp, -0.1_dp, 0.3_dp, 0.0_dp]
11+
real(dp), parameter :: xref(n) = [1.0_dp, 2.0_dp, -1.0_dp, 0.5_dp]
12+
real(dp) :: rhs(n), x(n)
13+
14+
rhs = matmul(A, xref)
15+
16+
! GMRES lets the user tune the restart size and the storage mode.
17+
x = x0
18+
call stdlib_solve_gmres(A, rhs, x, restart=.false., kdim=2, maxiter=12, precond=pc_jacobi)
19+
print *, 'compact:', x
20+
21+
x = x0
22+
call stdlib_solve_gmres(A, rhs, x, restart=.false., kdim=2, maxiter=12, precond=pc_jacobi, compact=.false.)
23+
print *, 'cached :', x
24+
end program example_solve_gmres

src/linalg_iterative/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
set(linalg_iterative_fppFiles
22
stdlib_linalg_iterative_solvers_bicgstab.fypp
33
stdlib_linalg_iterative_solvers_cg.fypp
4+
stdlib_linalg_iterative_solvers_gmres.fypp
45
stdlib_linalg_iterative_solvers.fypp
56
stdlib_linalg_iterative_solvers_pcg.fypp
67
)
@@ -13,4 +14,8 @@ set(linalg_iterative_f90Files
1314

1415
configure_stdlib_target(${PROJECT_NAME}_linalg_iterative linalg_iterative_f90Files linalg_iterative_fppFiles linalg_iterative_cppFiles)
1516

16-
target_link_libraries(${PROJECT_NAME}_linalg_iterative PUBLIC ${PROJECT_NAME}_linalg ${PROJECT_NAME}_sparse)
17+
target_link_libraries(${PROJECT_NAME}_linalg_iterative
18+
PUBLIC
19+
${PROJECT_NAME}_blas
20+
${PROJECT_NAME}_linalg
21+
${PROJECT_NAME}_sparse)

src/linalg_iterative/stdlib_linalg_iterative_solvers.fypp

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ module stdlib_linalg_iterative_solvers
1717
enumerator :: stdlib_size_wksp_pcg = 4
1818
enumerator :: stdlib_size_wksp_bicgstab = 8
1919
end enum
20-
public :: stdlib_size_wksp_cg, stdlib_size_wksp_pcg, stdlib_size_wksp_bicgstab
20+
public :: stdlib_size_wksp_cg, stdlib_size_wksp_pcg, stdlib_size_wksp_bicgstab, stdlib_size_wksp_gmres
2121

2222
enum, bind(c)
2323
enumerator :: pc_none = 0
@@ -161,6 +161,28 @@ module stdlib_linalg_iterative_solvers
161161
end interface
162162
public :: stdlib_solve_bicgstab_kernel
163163

164+
!! version: experimental
165+
!!
166+
!! stdlib_solve_gmres_kernel interface for the restarted generalized minimal residual method.
167+
!! [Specifications](../page/specs/stdlib_linalg_iterative_solvers.html#stdlib_solve_gmres_kernel)
168+
interface stdlib_solve_gmres_kernel
169+
#:for k, t, s in R_KINDS_TYPES
170+
module subroutine stdlib_solve_gmres_kernel_${s}$(A,M,b,x,rtol,atol,maxiter,kdim,workspace,compact)
171+
class(stdlib_linop_${s}$_type), intent(in) :: A !! linear operator
172+
class(stdlib_linop_${s}$_type), intent(in) :: M !! preconditioner linear operator
173+
${t}$, intent(in) :: b(:) !! right-hand side vector
174+
${t}$, intent(inout) :: x(:) !! solution vector and initial guess
175+
${t}$, intent(in) :: rtol !! relative tolerance for convergence
176+
${t}$, intent(in) :: atol !! absolute tolerance for convergence
177+
integer, intent(in) :: maxiter !! maximum number of iterations
178+
integer, intent(in) :: kdim !! Krylov subspace size before restart
179+
type(stdlib_solver_workspace_${s}$_type), intent(inout) :: workspace !! workspace for the solver
180+
logical, intent(in) :: compact !! keep only one preconditioned basis vector and rebuild the cycle update at restart
181+
end subroutine
182+
#:endfor
183+
end interface
184+
public :: stdlib_solve_gmres_kernel
185+
164186
!! version: experimental
165187
!!
166188
!! [Specifications](../page/specs/stdlib_linalg_iterative_solvers.html#stdlib_solve_pcg)
@@ -219,7 +241,47 @@ module stdlib_linalg_iterative_solvers
219241
end interface
220242
public :: stdlib_solve_bicgstab
221243

244+
!! version: experimental
245+
!!
246+
!! [Specifications](../page/specs/stdlib_linalg_iterative_solvers.html#stdlib_solve_gmres)
247+
interface stdlib_solve_gmres
248+
#:for matrix in MATRIX_TYPES
249+
#:for k, t, s in R_KINDS_TYPES
250+
module subroutine stdlib_solve_gmres_${matrix}$_${s}$(A,b,x,di,rtol,atol,maxiter,restart,kdim,precond,M,workspace,compact)
251+
!! linear operator matrix
252+
#:if matrix == "dense"
253+
${t}$, intent(in) :: A(:,:)
254+
#:else
255+
type(${matrix}$_${s}$_type), intent(in) :: A
256+
#:endif
257+
${t}$, intent(in) :: b(:) !! right-hand side vector
258+
${t}$, intent(inout) :: x(:) !! solution vector and initial guess
259+
${t}$, intent(in), optional :: rtol !! relative tolerance for convergence
260+
${t}$, intent(in), optional :: atol !! absolute tolerance for convergence
261+
logical(int8), intent(in), optional, target :: di(:) !! dirichlet conditions mask
262+
integer, intent(in), optional :: maxiter !! maximum number of iterations
263+
logical, intent(in), optional :: restart !! restart flag
264+
integer, intent(in), optional :: kdim !! Krylov subspace size before restart
265+
integer, intent(in), optional :: precond !! preconditioner method enumerator
266+
class(stdlib_linop_${s}$_type), optional, intent(in), target :: M !! preconditioner linear operator
267+
type(stdlib_solver_workspace_${s}$_type), optional, intent(inout), target :: workspace !! workspace for the solver
268+
logical, intent(in), optional :: compact !! if true, favor lower memory use over cycle-end update speed
269+
end subroutine
270+
#:endfor
271+
#:endfor
272+
end interface
273+
public :: stdlib_solve_gmres
274+
222275
contains
276+
277+
!------------------------------------------------------------------
278+
! helpers
279+
!------------------------------------------------------------------
280+
elemental integer(int32) function stdlib_size_wksp_gmres(kdim,compact)
281+
integer(int32), intent(in) :: kdim
282+
logical, intent(in) :: compact
283+
stdlib_size_wksp_gmres = 3 + kdim + merge(1, kdim, compact)
284+
end function
223285

224286
!------------------------------------------------------------------
225287
! defaults

0 commit comments

Comments
 (0)