LCOV - code coverage report
Current view: top level - src - bse_matvec.F (source / functions) Coverage Total Hit
Test: CP2K Regtests (git:92574dc) Lines: 63.1 % 268 169
Test Date: 2026-09-24 01:27:39 Functions: 63.6 % 11 7

            Line data    Source code
       1              : !--------------------------------------------------------------------------------------------------!
       2              : !   CP2K: A general program to perform molecular dynamics simulations                              !
       3              : !   Copyright 2000-2026 CP2K developers group <https://cp2k.org>                                   !
       4              : !                                                                                                  !
       5              : !   SPDX-License-Identifier: GPL-2.0-or-later                                                      !
       6              : !--------------------------------------------------------------------------------------------------!
       7              : 
       8              : ! **************************************************************************************************
       9              : !> \brief Matrix-free application of the BSE matrices A and B to trial vectors from RI slabs that
      10              : !>        are sliced along the RI index over all MPI ranks
      11              : !> \par History
      12              : !>      09.2026 created [Maximilian Graml]
      13              : ! **************************************************************************************************
      14              : MODULE bse_matvec
      15              : 
      16              :    USE bse_util,                        ONLY: ia_of_occ_virt,&
      17              :                                               occ_of_ia,&
      18              :                                               virt_of_ia
      19              :    USE cp_blacs_env,                    ONLY: cp_blacs_env_create,&
      20              :                                               cp_blacs_env_release,&
      21              :                                               cp_blacs_env_type
      22              :    USE cp_fm_struct,                    ONLY: cp_fm_struct_create,&
      23              :                                               cp_fm_struct_release,&
      24              :                                               cp_fm_struct_type
      25              :    USE cp_fm_types,                     ONLY: cp_fm_create,&
      26              :                                               cp_fm_get_info,&
      27              :                                               cp_fm_get_submatrix,&
      28              :                                               cp_fm_release,&
      29              :                                               cp_fm_set_all,&
      30              :                                               cp_fm_to_fm_submat_general,&
      31              :                                               cp_fm_type
      32              :    USE input_constants,                 ONLY: bse_precond_full_diag
      33              :    USE kinds,                           ONLY: dp
      34              :    USE message_passing,                 ONLY: mp_mem_avail_per_rank_GB,&
      35              :                                               mp_para_env_type
      36              : #include "./base/base_uses.f90"
      37              : 
      38              :    IMPLICIT NONE
      39              : 
      40              :    PRIVATE
      41              : 
      42              :    CHARACTER(len=*), PARAMETER, PRIVATE :: moduleN = 'bse_matvec'
      43              : 
      44              :    ! share of the free memory per rank the iterative solver may use, for the pass width of
      45              :    ! bse_matvec_apply and the default budget of the subspace ceiling: half, since the probe counts
      46              :    ! reclaimable cache and is optimistic
      47              :    REAL(KIND=dp), PARAMETER, PUBLIC :: mem_fraction = 0.5_dp
      48              : 
      49              :    PUBLIC :: bse_matvec_env_type, bse_matvec_create, bse_matvec_release, bse_matvec_apply, &
      50              :              bse_matvec_diagonal, bse_matvec_subblock, bse_matvec_vector_struct, bse_matvec_selfcheck
      51              : 
      52              : ! **************************************************************************************************
      53              : !> \brief RI slabs of one spin channel, every rank holding n_ri_loc whole RI slices
      54              : !> \param alpha prefactor of the exchange term (2 singlet, 0 triplet)
      55              : !> \param w_fac prefactor of the screened term (0 switches it off)
      56              : !> \param eps_diff ε_a - ε_i at the compound index ia = (i-1)*virt + a of bse_util's ia_of_occ_virt
      57              : !> \param B_ia B^P_ia at (a, i, P)
      58              : !> \param B_bar_ia \bar{B}^P_ia at (a, i, P), only for do_abba
      59              : !> \param B_bar_ij \bar{B}^P_ij at (j, i, P)
      60              : !> \param B_ab B^P_ab at (b, a, P)
      61              : !> \param row_count rows of a trial vector owned by each rank, in rank order
      62              : !> \param row_displ first row of each rank minus one, so that row_count and row_displ describe
      63              : !>        the contiguous ia range per rank that allgatherv and sum_scatter need
      64              : !> \param blacs_env npe x 1 process grid of the slabs and of all trial vectors
      65              : !> \param block_cols trial vectors per pass in bse_matvec_apply, -1 to size it from free memory
      66              : ! **************************************************************************************************
      67              :    TYPE bse_matvec_env_type
      68              :       INTEGER                                            :: homo = 0, virt = 0, n_ov = 0, &
      69              :                                                             n_ri_loc = 0, block_cols = -1
      70              :       REAL(KIND=dp)                                      :: alpha = 0.0_dp, w_fac = 0.0_dp
      71              :       LOGICAL                                            :: do_abba = .FALSE.
      72              :       INTEGER, ALLOCATABLE, DIMENSION(:)                 :: row_count, row_displ
      73              :       REAL(KIND=dp), ALLOCATABLE, DIMENSION(:)           :: eps_diff
      74              :       REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :, :)     :: B_ia, B_bar_ia, B_bar_ij, B_ab
      75              :       TYPE(cp_blacs_env_type), POINTER                   :: blacs_env => NULL()
      76              :       TYPE(mp_para_env_type), POINTER                    :: para_env => NULL()
      77              :    END TYPE bse_matvec_env_type
      78              : 
      79              : CONTAINS
      80              : 
      81              : ! **************************************************************************************************
      82              : !> \brief Moves the RI slabs onto an npe x 1 process grid, such that every rank owns whole RI
      83              : !>        slices, and stores them as local 3-index arrays: the contraction over each P in
      84              : !>        bse_matvec_apply is then a local DGEMM and only the final sum over P crosses the ranks
      85              : !> \param mv_env the environment created here
      86              : !> \param fm_S_ia B^P_ia, N_RI x homo*virt
      87              : !> \param fm_S_bar_ij \bar{B}^P_ij, the slab that enters W_ij,ab, N_RI x homo*homo
      88              : !> \param fm_S_ab B^P_ab, N_RI x virt*virt
      89              : !> \param fm_S_bar_ia \bar{B}^P_ia, the slab that enters W_ib,aj, N_RI x homo*virt
      90              : !> \param eps_reduced quasiparticle energies of the homo+virt active levels
      91              : !> \param homo occupied levels of the active window
      92              : !> \param virt virtual levels of the active window
      93              : !> \param alpha prefactor of the exchange term, 2 singlet, 0 triplet
      94              : !> \param w_fac prefactor of the screened term, 0 switches it off
      95              : !> \param do_abba slice \bar{B}^P_ia as well, for the application of B
      96              : !> \param unit_nr output unit, positive on the writing rank only
      97              : !> \param block_cols trial vectors per pass of bse_matvec_apply; absent or -1 sizes it from the free memory
      98              : ! **************************************************************************************************
      99            6 :    SUBROUTINE bse_matvec_create(mv_env, fm_S_ia, fm_S_bar_ij, fm_S_ab, fm_S_bar_ia, eps_reduced, &
     100              :                                 homo, virt, alpha, w_fac, do_abba, unit_nr, block_cols)
     101              : 
     102              :       TYPE(bse_matvec_env_type), INTENT(OUT)             :: mv_env
     103              :       TYPE(cp_fm_type), INTENT(IN)                       :: fm_S_ia, fm_S_bar_ij, fm_S_ab, &
     104              :                                                             fm_S_bar_ia
     105              :       REAL(KIND=dp), DIMENSION(:), INTENT(IN)            :: eps_reduced
     106              :       INTEGER, INTENT(IN)                                :: homo, virt
     107              :       REAL(KIND=dp), INTENT(IN)                          :: alpha, w_fac
     108              :       LOGICAL, INTENT(IN)                                :: do_abba
     109              :       INTEGER, INTENT(IN)                                :: unit_nr
     110              :       INTEGER, INTENT(IN), OPTIONAL                      :: block_cols
     111              : 
     112              :       CHARACTER(LEN=*), PARAMETER                        :: routineN = 'bse_matvec_create'
     113              : 
     114              :       CHARACTER(LEN=12)                                  :: n_idle_str, n_ri_str, npe_str
     115              :       INTEGER                                            :: a, handle, i, n_ri, n_ri_slab, &
     116              :                                                             n_row_block
     117              :       REAL(KIND=dp)                                      :: mem_slabs_GB, n_slab_entries
     118              : 
     119            6 :       CALL timeset(routineN, handle)
     120              : 
     121            6 :       mv_env%homo = homo
     122            6 :       mv_env%virt = virt
     123            6 :       IF (PRESENT(block_cols)) mv_env%block_cols = block_cols
     124            6 :       mv_env%n_ov = homo*virt
     125            6 :       mv_env%alpha = alpha
     126            6 :       mv_env%w_fac = w_fac
     127            6 :       mv_env%do_abba = do_abba
     128            6 :       mv_env%para_env => fm_S_ia%matrix_struct%para_env
     129              : 
     130              :       CALL cp_blacs_env_create(mv_env%blacs_env, mv_env%para_env, &
     131           18 :                                grid_2d=[mv_env%para_env%num_pe, 1])
     132              : 
     133              :       ! one contiguous ia range per rank, so that a trial vector is gathered with allgatherv and
     134              :       ! its image scattered back with sum_scatter, rather than replicated by two allreduces
     135            6 :       n_row_block = contiguous_row_block(mv_env%n_ov, mv_env%para_env%num_pe)
     136           30 :       ALLOCATE (mv_env%row_count(mv_env%para_env%num_pe), mv_env%row_displ(mv_env%para_env%num_pe))
     137           18 :       DO i = 1, mv_env%para_env%num_pe
     138           12 :          mv_env%row_displ(i) = MIN(mv_env%n_ov, (i - 1)*n_row_block)
     139           18 :          mv_env%row_count(i) = MIN(mv_env%n_ov, i*n_row_block) - mv_env%row_displ(i)
     140              :       END DO
     141              : 
     142              :       ! eps_diff_ia = ε_a - ε_i at the compound index ia = (i-1)*virt + a, the row index of every trial vector
     143           18 :       ALLOCATE (mv_env%eps_diff(mv_env%n_ov))
     144           30 :       DO i = 1, homo
     145          318 :          DO a = 1, virt
     146          312 :             mv_env%eps_diff(ia_of_occ_virt(i, a, virt)) = eps_reduced(homo + a) - eps_reduced(i)
     147              :          END DO
     148              :       END DO
     149              : 
     150              :       ! every slab is sliced on the same grid, so every rank owns the same RI indices of each slab
     151            6 :       CALL slice_slab(mv_env, fm_S_ia, virt, homo, mv_env%B_ia, mv_env%n_ri_loc)
     152            6 :       IF (w_fac /= 0.0_dp) THEN
     153            6 :          CALL slice_slab(mv_env, fm_S_bar_ij, homo, homo, mv_env%B_bar_ij, n_ri_slab)
     154            6 :          CPASSERT(n_ri_slab == mv_env%n_ri_loc)
     155            6 :          CALL slice_slab(mv_env, fm_S_ab, virt, virt, mv_env%B_ab, n_ri_slab)
     156            6 :          CPASSERT(n_ri_slab == mv_env%n_ri_loc)
     157            6 :          IF (do_abba) THEN
     158            4 :             CALL slice_slab(mv_env, fm_S_bar_ia, virt, homo, mv_env%B_bar_ia, n_ri_slab)
     159            4 :             CPASSERT(n_ri_slab == mv_env%n_ri_loc)
     160              :          END IF
     161              :       END IF
     162              : 
     163            6 :       CALL cp_fm_get_info(fm_S_ia, nrow_global=n_ri)
     164            6 :       IF (mv_env%para_env%num_pe > n_ri .AND. unit_nr > 0) THEN
     165            0 :          WRITE (npe_str, '(I0)') mv_env%para_env%num_pe
     166            0 :          WRITE (n_ri_str, '(I0)') n_ri
     167            0 :          WRITE (n_idle_str, '(I0)') mv_env%para_env%num_pe - n_ri
     168              :          CALL cp_warn(__LOCATION__, &
     169              :                       "BSE iterative solver: more MPI ranks ("//TRIM(npe_str)// &
     170              :                       ") than RI basis functions ("//TRIM(n_ri_str)//"); "// &
     171            0 :                       TRIM(n_idle_str)//" ranks hold no RI slice and idle in the kernel application.")
     172              :       END IF
     173              : 
     174            6 :       IF (unit_nr > 0) THEN
     175              :          ! pair entries of the sliced slabs per RI index
     176            3 :          n_slab_entries = REAL(homo*virt, dp)
     177            3 :          IF (w_fac /= 0.0_dp) THEN
     178            3 :             n_slab_entries = n_slab_entries + REAL(homo, dp)**2 + REAL(virt, dp)**2
     179            3 :             IF (do_abba) n_slab_entries = n_slab_entries + REAL(homo*virt, dp)
     180              :          END IF
     181            3 :          mem_slabs_GB = n_slab_entries*REAL(mv_env%n_ri_loc, dp)*8.0E-9_dp
     182            3 :          WRITE (unit_nr, '(T2,A4,T7,A,T71,I10)') 'BSE|', &
     183            6 :             'Max. number of RI functions per MPI rank', mv_env%n_ri_loc
     184            3 :          WRITE (unit_nr, '(T2,A4,T7,A,T67,F14.3)') 'BSE|', &
     185            6 :             'Memory of the RI slabs per MPI rank (GB)', mem_slabs_GB
     186              :       END IF
     187              : 
     188            6 :       CALL timestop(handle)
     189              : 
     190           12 :    END SUBROUTINE bse_matvec_create
     191              : 
     192              : ! **************************************************************************************************
     193              : !> \brief Rows of a trial vector per rank, the smallest block that leaves no rank beyond the last
     194              : !> \param n_ov number of transitions, the global row count
     195              : !> \param npe ranks of the npe x 1 grid
     196              : !> \return rows of every full block; the last rank takes what remains, possibly none
     197              : ! **************************************************************************************************
     198           28 :    PURE FUNCTION contiguous_row_block(n_ov, npe) RESULT(n_row_block)
     199              : 
     200              :       INTEGER, INTENT(IN)                                :: n_ov, npe
     201              :       INTEGER                                            :: n_row_block
     202              : 
     203           28 :       n_row_block = (n_ov + npe - 1)/npe
     204              : 
     205           28 :    END FUNCTION contiguous_row_block
     206              : 
     207              : ! **************************************************************************************************
     208              : !> \brief Redistributes one N_RI x n_fast*n_slow slab to whole RI rows per rank and copies the owned
     209              : !>        rows into a contiguous (n_fast, n_slow, n_ri_loc) array, slab_loc(x, y, p) = slab(P_p, (y-1)*n_fast + x)
     210              : !>        for the p-th owned RI index P_p, so that every slice slab_loc(:, :, p) is a n_fast x n_slow DGEMM operand
     211              : !> \param mv_env process grid and communicator of the slices
     212              : !> \param fm_slab the slab on the grid of the caller, N_RI x n_fast*n_slow
     213              : !> \param n_fast fast index of the pair index of the slab
     214              : !> \param n_slow slow index of the pair index of the slab
     215              : !> \param slab_loc the owned slices, allocated here
     216              : !> \param n_ri_loc RI indices owned by this rank, the third extent of slab_loc
     217              : ! **************************************************************************************************
     218           22 :    SUBROUTINE slice_slab(mv_env, fm_slab, n_fast, n_slow, slab_loc, n_ri_loc)
     219              : 
     220              :       TYPE(bse_matvec_env_type), INTENT(IN)              :: mv_env
     221              :       TYPE(cp_fm_type), INTENT(IN)                       :: fm_slab
     222              :       INTEGER, INTENT(IN)                                :: n_fast, n_slow
     223              :       REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :, :), &
     224              :          INTENT(OUT)                                     :: slab_loc
     225              :       INTEGER, INTENT(OUT)                               :: n_ri_loc
     226              : 
     227              :       CHARACTER(LEN=*), PARAMETER                        :: routineN = 'slice_slab'
     228              : 
     229              :       INTEGER                                            :: handle, n_ri, npair, p
     230              :       TYPE(cp_fm_struct_type), POINTER                   :: fm_struct
     231              :       TYPE(cp_fm_type)                                   :: fm_sliced
     232              : 
     233           22 :       CALL timeset(routineN, handle)
     234              : 
     235              :       ! row count from the struct: the slabs may carry fewer rows than the full RI basis
     236           22 :       CALL cp_fm_get_info(fm_slab, nrow_global=n_ri, ncol_global=npair)
     237           22 :       CPASSERT(npair == n_fast*n_slow)
     238              : 
     239              :       ! one-row blocks on the npe x 1 grid spread the RI rows cyclically over the ranks; the copy
     240              :       ! below turns each strided local_data row into a contiguous (n_fast, n_slow) slice
     241           22 :       NULLIFY (fm_struct)
     242              :       CALL cp_fm_struct_create(fm_struct, para_env=mv_env%para_env, context=mv_env%blacs_env, &
     243              :                                nrow_global=n_ri, ncol_global=npair, nrow_block=1, &
     244           22 :                                force_block=.TRUE.)
     245           22 :       CALL cp_fm_create(fm_sliced, fm_struct, name="fm_slab_ri_sliced")
     246           22 :       CALL cp_fm_struct_release(fm_struct)
     247              :       CALL cp_fm_to_fm_submat_general(fm_slab, fm_sliced, n_ri, npair, 1, 1, 1, 1, &
     248           22 :                                       fm_slab%matrix_struct%context)
     249              : 
     250           22 :       CALL cp_fm_get_info(fm_sliced, nrow_local=n_ri_loc)
     251          110 :       ALLOCATE (slab_loc(n_fast, n_slow, n_ri_loc))
     252          935 :       DO p = 1, n_ri_loc
     253         2761 :          slab_loc(:, :, p) = RESHAPE(fm_sliced%local_data(p, 1:npair), [n_fast, n_slow])
     254              :       END DO
     255           22 :       CALL cp_fm_release(fm_sliced)
     256              : 
     257           22 :       CALL timestop(handle)
     258              : 
     259           66 :    END SUBROUTINE slice_slab
     260              : 
     261              : ! **************************************************************************************************
     262              : !> \brief Frees the sliced slabs, the transition energies and the process grid of mv_env
     263              : !> \param mv_env the environment released
     264              : ! **************************************************************************************************
     265            6 :    SUBROUTINE bse_matvec_release(mv_env)
     266              : 
     267              :       TYPE(bse_matvec_env_type), INTENT(INOUT)           :: mv_env
     268              : 
     269            6 :       IF (ALLOCATED(mv_env%row_count)) DEALLOCATE (mv_env%row_count)
     270            6 :       IF (ALLOCATED(mv_env%row_displ)) DEALLOCATE (mv_env%row_displ)
     271            6 :       IF (ALLOCATED(mv_env%eps_diff)) DEALLOCATE (mv_env%eps_diff)
     272            6 :       IF (ALLOCATED(mv_env%B_ia)) DEALLOCATE (mv_env%B_ia)
     273            6 :       IF (ALLOCATED(mv_env%B_bar_ia)) DEALLOCATE (mv_env%B_bar_ia)
     274            6 :       IF (ALLOCATED(mv_env%B_bar_ij)) DEALLOCATE (mv_env%B_bar_ij)
     275            6 :       IF (ALLOCATED(mv_env%B_ab)) DEALLOCATE (mv_env%B_ab)
     276            6 :       IF (ASSOCIATED(mv_env%blacs_env)) CALL cp_blacs_env_release(mv_env%blacs_env)
     277            6 :       NULLIFY (mv_env%para_env)
     278              : 
     279            6 :    END SUBROUTINE bse_matvec_release
     280              : 
     281              : ! **************************************************************************************************
     282              : !> \brief Matrix structure of a block of trial vectors: rows ia distributed, all columns local
     283              : !> \param mv_env process grid and communicator of the trial vectors
     284              : !> \param ncol_global number of trial vectors
     285              : !> \param fm_struct created here, released by the caller
     286              : ! **************************************************************************************************
     287           22 :    SUBROUTINE bse_matvec_vector_struct(mv_env, ncol_global, fm_struct)
     288              : 
     289              :       TYPE(bse_matvec_env_type), INTENT(IN)              :: mv_env
     290              :       INTEGER, INTENT(IN)                                :: ncol_global
     291              :       TYPE(cp_fm_struct_type), POINTER                   :: fm_struct
     292              : 
     293           22 :       NULLIFY (fm_struct)
     294              :       ! the row block matches mv_env%row_count, which bse_matvec_apply's collectives rely on
     295              :       CALL cp_fm_struct_create(fm_struct, para_env=mv_env%para_env, context=mv_env%blacs_env, &
     296              :                                nrow_global=mv_env%n_ov, ncol_global=ncol_global, &
     297              :                                nrow_block=contiguous_row_block(mv_env%n_ov, mv_env%para_env%num_pe), &
     298           22 :                                force_block=.TRUE.)
     299              : 
     300           22 :    END SUBROUTINE bse_matvec_vector_struct
     301              : 
     302              : ! **************************************************************************************************
     303              : !> \brief Applies A (and B) to ncol trial vectors without forming an N_ov x N_ov object,
     304              : !>        (A Z)_ia = (ε_a-ε_i) Z_ia + α sum_P B^P_ia sum_jb B^P_jb Z_jb - w sum_Pjb \bar{B}^P_ij B^P_ab Z_jb
     305              : !>        (B Z)_ia =                  α sum_P B^P_ia sum_jb B^P_jb Z_jb - w sum_Pjb \bar{B}^P_ib B^P_ja Z_jb
     306              : !>        α: exchange prefactor (2 singlet, 0 triplet), w: prefactor of the screened term,
     307              : !>        \bar{B}: slab bound by the caller of bse_matvec_create (B itself for TDHF and ALPHA
     308              : !>        screening, sum_Q [1+Q(0)]^-1_PQ B^Q otherwise); the sums over P run over the local RI
     309              : !>        slices and are completed by a sum over the ranks
     310              : !> \param mv_env slabs, prefactors and transition energies
     311              : !> \param fm_Z trial vectors, columns first_col .. first_col+ncol-1 are read
     312              : !> \param first_col first trial vector read, and first column written
     313              : !> \param ncol number of trial vectors
     314              : !> \param fm_AZ receives A Z in the same columns
     315              : !> \param fm_BZ receives B Z in the same columns, or from first_col_BZ on
     316              : !> \param first_col_BZ first column of fm_BZ written, first_col by default
     317              : ! **************************************************************************************************
     318           82 :    SUBROUTINE bse_matvec_apply(mv_env, fm_Z, first_col, ncol, fm_AZ, fm_BZ, first_col_BZ)
     319              : 
     320              :       TYPE(bse_matvec_env_type), INTENT(IN), TARGET      :: mv_env
     321              :       TYPE(cp_fm_type), INTENT(IN)                       :: fm_Z
     322              :       INTEGER, INTENT(IN)                                :: first_col, ncol
     323              :       TYPE(cp_fm_type), INTENT(IN)                       :: fm_AZ
     324              :       TYPE(cp_fm_type), INTENT(IN), OPTIONAL             :: fm_BZ
     325              :       INTEGER, INTENT(IN), OPTIONAL                      :: first_col_BZ
     326              : 
     327              :       CHARACTER(LEN=*), PARAMETER                        :: routineN = 'bse_matvec_apply'
     328              : 
     329              :       INTEGER                                            :: col, col_shift_B, handle, handle2, ia, &
     330              :                                                             iloc, k, me, n_work_bufs, nb, nb_max, &
     331              :                                                             nrow_local, p
     332           82 :       INTEGER, DIMENSION(:), POINTER                     :: row_indices
     333              :       LOGICAL                                            :: do_B
     334              :       REAL(KIND=dp)                                      :: mem_avail_GB
     335           82 :       REAL(KIND=dp), ALLOCATABLE, DIMENSION(:)           :: R_loc
     336           82 :       REAL(KIND=dp), ALLOCATABLE, DIMENSION(:), TARGET   :: BabZ_buf, RA_buf, RB_buf, Z_buf
     337           82 :       REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :)        :: BjaZ_ab, T_Pk
     338              :       REAL(KIND=dp), CONTIGUOUS, DIMENSION(:, :), &
     339           82 :          POINTER                                         :: B_ia_2d, BabZ_wide, RA, RB, Z, Z_wide
     340              :       REAL(KIND=dp), CONTIGUOUS, DIMENSION(:, :, :), &
     341           82 :          POINTER                                         :: BabZ_3d, RA_3d, RB_3d, Z_3d
     342              : 
     343           82 :       CALL timeset(routineN, handle)
     344              : 
     345           82 :       do_B = PRESENT(fm_BZ)
     346           82 :       IF (do_B) THEN
     347              :          ! B Z needs the \bar{B}^P_ia slab, which is sliced for do_abba only
     348           66 :          CPASSERT(mv_env%do_abba)
     349              :       END IF
     350           82 :       col_shift_B = 0
     351           82 :       IF (PRESENT(first_col_BZ)) col_shift_B = first_col_BZ - first_col
     352              : 
     353           82 :       CALL cp_fm_get_info(fm_Z, nrow_local=nrow_local, row_indices=row_indices)
     354              :       ! the collectives below read the rows of one rank as one range, as bse_matvec_vector_struct
     355              :       ! lays them out; a vector built elsewhere would scatter into the wrong rows
     356           82 :       me = mv_env%para_env%mepos + 1
     357           82 :       CPASSERT(nrow_local == mv_env%row_count(me))
     358           82 :       CPASSERT(nrow_local == 0 .OR. row_indices(1) == mv_env%row_displ(me) + 1)
     359          246 :       ALLOCATE (R_loc(nrow_local))
     360              : 
     361              :       ! columns per pass from the memory of the replicated work arrays Z, B_ab Z, R^A (and R^B)
     362           82 :       IF (do_B) THEN
     363              :          n_work_bufs = 4
     364              :       ELSE
     365           16 :          n_work_bufs = 3
     366              :       END IF
     367           82 :       IF (mv_env%block_cols > 0) THEN
     368              :          ! pinned by input: the replicated work arrays are then a known n_work_bufs*n_ov*nb doubles
     369           82 :          nb_max = MIN(ncol, mv_env%block_cols)
     370              :       ELSE
     371            0 :          CALL mp_mem_avail_per_rank_GB(mv_env%para_env, mem_avail_GB)
     372            0 :          nb_max = ncol
     373            0 :          IF (mem_avail_GB > 0.0_dp) THEN
     374              :             nb_max = INT(MIN(REAL(ncol, dp), &
     375            0 :                              mem_fraction*mem_avail_GB*1.0E9_dp/(8.0_dp*REAL(n_work_bufs, dp)*REAL(mv_env%n_ov, dp))))
     376              :          END IF
     377              :       END IF
     378           82 :       nb_max = MAX(nb_max, 1)
     379              : 
     380          410 :       ALLOCATE (Z_buf(mv_env%n_ov*nb_max), RA_buf(mv_env%n_ov*nb_max), BabZ_buf(mv_env%n_ov*nb_max))
     381           82 :       IF (do_B) THEN
     382          330 :          ALLOCATE (RB_buf(mv_env%n_ov*nb_max), BjaZ_ab(mv_env%virt, mv_env%virt))
     383              :       END IF
     384          328 :       ALLOCATE (T_Pk(mv_env%n_ri_loc, nb_max))
     385           82 :       B_ia_2d(1:mv_env%n_ov, 1:mv_env%n_ri_loc) => mv_env%B_ia
     386              : 
     387          164 :       DO col = first_col, first_col + ncol - 1, nb_max
     388           82 :          nb = MIN(nb_max, first_col + ncol - col)
     389              :          ! three views of one contiguous buffer, bounds-remapping pointer assignment (Fortran 2003):
     390              :          ! Z_ia,k for the exchange DGEMM, Z_3d(a, i, k) per column, Z_wide(a, (i k)) for the wide product
     391           82 :          Z(1:mv_env%n_ov, 1:nb) => Z_buf(1:mv_env%n_ov*nb)
     392           82 :          Z_3d(1:mv_env%virt, 1:mv_env%homo, 1:nb) => Z_buf(1:mv_env%n_ov*nb)
     393           82 :          Z_wide(1:mv_env%virt, 1:mv_env%homo*nb) => Z_buf(1:mv_env%n_ov*nb)
     394           82 :          RA(1:mv_env%n_ov, 1:nb) => RA_buf(1:mv_env%n_ov*nb)
     395           82 :          RA_3d(1:mv_env%virt, 1:mv_env%homo, 1:nb) => RA_buf(1:mv_env%n_ov*nb)
     396           82 :          BabZ_3d(1:mv_env%virt, 1:mv_env%homo, 1:nb) => BabZ_buf(1:mv_env%n_ov*nb)
     397           82 :          BabZ_wide(1:mv_env%virt, 1:mv_env%homo*nb) => BabZ_buf(1:mv_env%n_ov*nb)
     398           82 :          IF (do_B) THEN
     399           66 :             RB(1:mv_env%n_ov, 1:nb) => RB_buf(1:mv_env%n_ov*nb)
     400           66 :             RB_3d(1:mv_env%virt, 1:mv_env%homo, 1:nb) => RB_buf(1:mv_env%n_ov*nb)
     401              :          END IF
     402              : 
     403              :          ! Z_ia,k on every rank: every rank owns a contiguous ia range, so the column arrives sorted
     404           82 :          CALL timeset(routineN//"_gather", handle2)
     405          330 :          DO k = 1, nb
     406              :             CALL mv_env%para_env%allgatherv(fm_Z%local_data(1:nrow_local, col + k - 1), Z(:, k), &
     407          330 :                                             mv_env%row_count, mv_env%row_displ)
     408              :          END DO
     409           82 :          CALL timestop(handle2)
     410              : 
     411              :          ! exchange, identical in A Z and B Z: R_ia,k = α sum_P B^P_ia t_Pk, t_Pk = sum_jb B^P_jb Z_jb,k
     412           82 :          CALL timeset(routineN//"_exchange", handle2)
     413           82 :          RA(:, :) = 0.0_dp
     414           82 :          IF (mv_env%alpha /= 0.0_dp .AND. mv_env%n_ri_loc > 0) THEN
     415              :             CALL DGEMM('T', 'N', mv_env%n_ri_loc, nb, mv_env%n_ov, 1.0_dp, B_ia_2d, mv_env%n_ov, Z, mv_env%n_ov, &
     416           82 :                        0.0_dp, T_Pk, mv_env%n_ri_loc)
     417              :             CALL DGEMM('N', 'N', mv_env%n_ov, nb, mv_env%n_ri_loc, mv_env%alpha, B_ia_2d, mv_env%n_ov, T_Pk, mv_env%n_ri_loc, &
     418           82 :                        0.0_dp, RA, mv_env%n_ov)
     419              :          END IF
     420        20336 :          IF (do_B) RB(:, :) = RA(:, :)
     421           82 :          CALL timestop(handle2)
     422              : 
     423           82 :          CALL timeset(routineN//"_W", handle2)
     424           82 :          IF (mv_env%w_fac /= 0.0_dp) THEN
     425         3485 :             DO p = 1, mv_env%n_ri_loc
     426              :                ! A: R_ia,k -= w sum_jb \bar{B}^P_ij B^P_ab Z_jb,k, BabZ_ib,k = sum_b B^P_ab Z_jb,k first
     427              :                CALL DGEMM('T', 'N', mv_env%virt, mv_env%homo*nb, mv_env%virt, 1.0_dp, mv_env%B_ab(:, :, p), mv_env%virt, &
     428         3403 :                           Z_wide, mv_env%virt, 0.0_dp, BabZ_wide, mv_env%virt)
     429        13695 :                DO k = 1, nb
     430              :                   CALL DGEMM('N', 'N', mv_env%virt, mv_env%homo, mv_env%homo, -mv_env%w_fac, BabZ_3d(:, :, k), mv_env%virt, &
     431        13695 :                              mv_env%B_bar_ij(:, :, p), mv_env%homo, 1.0_dp, RA_3d(:, :, k), mv_env%virt)
     432              :                END DO
     433              :                ! B: R_ia,k -= w sum_jb \bar{B}^P_ib B^P_ja Z_jb,k, BjaZ_ab = sum_j B^P_ja Z_jb,k first
     434         3485 :                IF (do_B) THEN
     435        11288 :                   DO k = 1, nb
     436              :                      CALL DGEMM('N', 'T', mv_env%virt, mv_env%virt, mv_env%homo, 1.0_dp, mv_env%B_ia(:, :, p), mv_env%virt, &
     437         8549 :                                 Z_3d(:, :, k), mv_env%virt, 0.0_dp, BjaZ_ab, mv_env%virt)
     438              :                      CALL DGEMM('N', 'N', mv_env%virt, mv_env%homo, mv_env%virt, -mv_env%w_fac, BjaZ_ab, mv_env%virt, &
     439        11288 :                                 mv_env%B_bar_ia(:, :, p), mv_env%virt, 1.0_dp, RB_3d(:, :, k), mv_env%virt)
     440              :                   END DO
     441              :                END IF
     442              :             END DO
     443              :          END IF
     444           82 :          CALL timestop(handle2)
     445              : 
     446              :          ! the sum over the ranks lands on the owner of each row, never replicated
     447           82 :          CALL timeset(routineN//"_reduce", handle2)
     448              :          ! (A Z)_ia,k = (ε_a-ε_i) Z_ia,k + R^A_ia,k and (B Z)_ia,k = R^B_ia,k on the local rows
     449          330 :          DO k = 1, nb
     450          248 :             CALL mv_env%para_env%sum_scatter(RA(:, k:k), R_loc, mv_env%row_count)
     451         6200 :             DO iloc = 1, nrow_local
     452         5952 :                ia = row_indices(iloc)
     453         6200 :                fm_AZ%local_data(iloc, col + k - 1) = R_loc(iloc) + mv_env%eps_diff(ia)*Z(ia, k)
     454              :             END DO
     455          330 :             IF (do_B) THEN
     456          206 :                CALL mv_env%para_env%sum_scatter(RB(:, k:k), R_loc, mv_env%row_count)
     457         5150 :                fm_BZ%local_data(1:nrow_local, col + col_shift_B + k - 1) = R_loc(1:nrow_local)
     458              :             END IF
     459              :          END DO
     460          492 :          CALL timestop(handle2)
     461              :       END DO
     462              : 
     463           82 :       DEALLOCATE (Z_buf, RA_buf, BabZ_buf, T_Pk)
     464           82 :       IF (do_B) THEN
     465           66 :          DEALLOCATE (RB_buf, BjaZ_ab)
     466              :       END IF
     467              : 
     468           82 :       CALL timestop(handle)
     469              : 
     470          246 :    END SUBROUTINE bse_matvec_apply
     471              : 
     472              : ! **************************************************************************************************
     473              : !> \brief Diagonal used by the Davidson correction, either ε_a-ε_i or the full diagonal
     474              : !>        A_ia,ia = ε_a-ε_i + α sum_P (B^P_ia)^2 - w sum_P \bar{B}^P_ii B^P_aa
     475              : !>        with α, w and \bar{B} as in bse_matvec_apply
     476              : !> \param mv_env slabs, prefactors and transition energies
     477              : !> \param precond_kind bse_precond_full_diag or bse_precond_qp_diff
     478              : !> \param diag replicated, size N_ov
     479              : ! **************************************************************************************************
     480            6 :    SUBROUTINE bse_matvec_diagonal(mv_env, precond_kind, diag)
     481              : 
     482              :       TYPE(bse_matvec_env_type), INTENT(IN)              :: mv_env
     483              :       INTEGER, INTENT(IN)                                :: precond_kind
     484              :       REAL(KIND=dp), DIMENSION(:), INTENT(OUT)           :: diag
     485              : 
     486              :       CHARACTER(LEN=*), PARAMETER :: routineN = 'bse_matvec_diagonal'
     487              : 
     488              :       INTEGER                                            :: a, handle, i, ia, p
     489              : 
     490            6 :       CALL timeset(routineN, handle)
     491              : 
     492          294 :       diag(:) = 0.0_dp
     493            6 :       IF (precond_kind == bse_precond_full_diag) THEN
     494          255 :          DO p = 1, mv_env%n_ri_loc
     495         1251 :             DO i = 1, mv_env%homo
     496        13197 :                DO a = 1, mv_env%virt
     497        11952 :                   ia = ia_of_occ_virt(i, a, mv_env%virt)
     498        11952 :                   diag(ia) = diag(ia) + mv_env%alpha*mv_env%B_ia(a, i, p)**2
     499        12948 :                   IF (mv_env%w_fac /= 0.0_dp) THEN
     500        11952 :                      diag(ia) = diag(ia) - mv_env%w_fac*mv_env%B_bar_ij(i, i, p)*mv_env%B_ab(a, a, p)
     501              :                   END IF
     502              :                END DO
     503              :             END DO
     504              :          END DO
     505          582 :          CALL mv_env%para_env%sum(diag)
     506              :       END IF
     507          294 :       diag(:) = diag(:) + mv_env%eps_diff(:)
     508              : 
     509            6 :       CALL timestop(handle)
     510              : 
     511            6 :    END SUBROUTINE bse_matvec_diagonal
     512              : 
     513              : ! **************************************************************************************************
     514              : !> \brief Exact A (and B) on a list of transitions, replicated on every rank,
     515              : !>        A_kl = δ_kl (ε_a-ε_i) + α sum_P B^P_ia B^P_jb - w sum_P \bar{B}^P_ij B^P_ab
     516              : !>        B_kl =                  α sum_P B^P_ia B^P_jb - w sum_P \bar{B}^P_ib B^P_ja
     517              : !>        with k = (i,a), l = (j,b) and α, w, \bar{B} as in bse_matvec_apply
     518              : !> \param mv_env slabs, prefactors and transition energies
     519              : !> \param ia_list global transition indices ia = (i-1)*virt + a of the block
     520              : !> \param A_sub SIZE(ia_list) x SIZE(ia_list)
     521              : !> \param B_sub same, only formed when present
     522              : ! **************************************************************************************************
     523            0 :    SUBROUTINE bse_matvec_subblock(mv_env, ia_list, A_sub, B_sub)
     524              : 
     525              :       TYPE(bse_matvec_env_type), INTENT(IN)              :: mv_env
     526              :       INTEGER, DIMENSION(:), INTENT(IN)                  :: ia_list
     527              :       REAL(KIND=dp), DIMENSION(:, :), INTENT(OUT)        :: A_sub
     528              :       REAL(KIND=dp), DIMENSION(:, :), INTENT(OUT), &
     529              :          OPTIONAL                                        :: B_sub
     530              : 
     531              :       CHARACTER(LEN=*), PARAMETER :: routineN = 'bse_matvec_subblock'
     532              : 
     533              :       INTEGER                                            :: handle, k, l, n_sub, p
     534            0 :       INTEGER, ALLOCATABLE, DIMENSION(:)                 :: a_virt, i_occ
     535            0 :       REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :)        :: B_Pk
     536              : 
     537            0 :       CALL timeset(routineN, handle)
     538              : 
     539            0 :       n_sub = SIZE(ia_list)
     540            0 :       ALLOCATE (i_occ(n_sub), a_virt(n_sub), B_Pk(MAX(mv_env%n_ri_loc, 1), n_sub))
     541            0 :       DO k = 1, n_sub
     542            0 :          i_occ(k) = occ_of_ia(ia_list(k), mv_env%virt)
     543            0 :          a_virt(k) = virt_of_ia(ia_list(k), mv_env%virt)
     544              :       END DO
     545              : 
     546              :       ! exchange, identical in A and B: α sum_P B_Pk B_Pl with B_Pk = B^P_ia at ia = ia_list(k)
     547            0 :       A_sub(:, :) = 0.0_dp
     548            0 :       IF (mv_env%alpha /= 0.0_dp .AND. mv_env%n_ri_loc > 0) THEN
     549              :          ! P outermost: the gathered entries of one slice lie within one contiguous (a, i) block
     550            0 :          DO p = 1, mv_env%n_ri_loc
     551            0 :             DO k = 1, n_sub
     552            0 :                B_Pk(p, k) = mv_env%B_ia(a_virt(k), i_occ(k), p)
     553              :             END DO
     554              :          END DO
     555              :          CALL DGEMM('T', 'N', n_sub, n_sub, mv_env%n_ri_loc, mv_env%alpha, B_Pk, SIZE(B_Pk, 1), B_Pk, SIZE(B_Pk, 1), &
     556            0 :                     0.0_dp, A_sub, n_sub)
     557              :       END IF
     558            0 :       IF (PRESENT(B_sub)) B_sub(:, :) = A_sub(:, :)
     559              : 
     560            0 :       IF (mv_env%w_fac /= 0.0_dp) THEN
     561            0 :          DO p = 1, mv_env%n_ri_loc
     562            0 :             DO l = 1, n_sub
     563            0 :                DO k = 1, n_sub
     564            0 :                 A_sub(k, l) = A_sub(k, l) - mv_env%w_fac*mv_env%B_bar_ij(i_occ(l), i_occ(k), p)*mv_env%B_ab(a_virt(l), a_virt(k), p)
     565              :                END DO
     566              :             END DO
     567            0 :             IF (PRESENT(B_sub)) THEN
     568            0 :                DO l = 1, n_sub
     569            0 :                   DO k = 1, n_sub
     570            0 :                 B_sub(k, l) = B_sub(k, l) - mv_env%w_fac*mv_env%B_ia(a_virt(k), i_occ(l), p)*mv_env%B_bar_ia(a_virt(l), i_occ(k), p)
     571              :                   END DO
     572              :                END DO
     573              :             END IF
     574              :          END DO
     575              :       END IF
     576              : 
     577            0 :       CALL mv_env%para_env%sum(A_sub)
     578            0 :       IF (PRESENT(B_sub)) CALL mv_env%para_env%sum(B_sub)
     579            0 :       DO k = 1, n_sub
     580            0 :          A_sub(k, k) = A_sub(k, k) + mv_env%eps_diff(ia_list(k))
     581              :       END DO
     582              : 
     583            0 :       DEALLOCATE (i_occ, a_virt, B_Pk)
     584              : 
     585            0 :       CALL timestop(handle)
     586              : 
     587            0 :    END SUBROUTINE bse_matvec_subblock
     588              : 
     589              : ! **************************************************************************************************
     590              : !> \brief Debug check of the matrix-free application against the explicit matrices A (and B),
     591              : !>        dev = max_ia,k |(A Z)_ia,k - sum_jb A_ia,jb Z_jb,k| over up to eight unit vectors Z_ia,k = δ_ia,k
     592              : !>        and one dense vector Z_ia = sin(ia), the same for B, and max_ia |d_ia - A_ia,ia| for the diagonal
     593              : !> \param mv_env the environment under test
     594              : !> \param fm_A_explicit A from create_A_and_B, N_ov x N_ov on the grid of the caller
     595              : !> \param unit_nr output unit, positive on the writing rank only
     596              : !> \param fm_B_explicit B, present for an ABBA run
     597              : ! **************************************************************************************************
     598            0 :    SUBROUTINE bse_matvec_selfcheck(mv_env, fm_A_explicit, unit_nr, fm_B_explicit)
     599              : 
     600              :       TYPE(bse_matvec_env_type), INTENT(IN)              :: mv_env
     601              :       TYPE(cp_fm_type), INTENT(IN)                       :: fm_A_explicit
     602              :       INTEGER, INTENT(IN)                                :: unit_nr
     603              :       TYPE(cp_fm_type), INTENT(IN), OPTIONAL             :: fm_B_explicit
     604              : 
     605              :       CHARACTER(LEN=*), PARAMETER :: routineN = 'bse_matvec_selfcheck'
     606              : 
     607              :       INTEGER                                            :: handle, ia, iloc, k, n_ov, n_unit, ncol, &
     608              :                                                             nrow_local
     609            0 :       INTEGER, DIMENSION(:), POINTER                     :: row_indices
     610              :       REAL(KIND=dp)                                      :: dev_A, dev_B, dev_diag
     611            0 :       REAL(KIND=dp), ALLOCATABLE, DIMENSION(:)           :: diag
     612            0 :       REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :)        :: mv_result, ref_matrix, ref_result, Z
     613              :       TYPE(cp_fm_struct_type), POINTER                   :: fm_struct
     614              :       TYPE(cp_fm_type)                                   :: fm_AZ, fm_BZ, fm_Z
     615              : 
     616            0 :       CALL timeset(routineN, handle)
     617              : 
     618            0 :       n_ov = mv_env%n_ov
     619              :       ! eight unit vectors read eight columns of the explicit matrix exactly; the dense column covers the whole contraction
     620            0 :       n_unit = MIN(n_ov, 8)
     621            0 :       dev_B = 0.0_dp
     622            0 :       ncol = n_unit + 1
     623              : 
     624              :       ! trial vectors: e_k for k <= n_unit, then one dense column Z_ia = sin(ia)
     625            0 :       ALLOCATE (Z(n_ov, ncol))
     626            0 :       Z(:, :) = 0.0_dp
     627            0 :       DO k = 1, n_unit
     628            0 :          Z(k, k) = 1.0_dp
     629              :       END DO
     630            0 :       DO ia = 1, n_ov
     631            0 :          Z(ia, ncol) = SIN(REAL(ia, dp))
     632              :       END DO
     633              : 
     634            0 :       CALL bse_matvec_vector_struct(mv_env, ncol, fm_struct)
     635            0 :       CALL cp_fm_create(fm_Z, fm_struct, name="fm_Z_selfcheck")
     636            0 :       CALL cp_fm_create(fm_AZ, fm_struct, name="fm_AZ_selfcheck")
     637            0 :       CALL cp_fm_set_all(fm_AZ, 0.0_dp)
     638            0 :       IF (PRESENT(fm_B_explicit)) THEN
     639            0 :          CALL cp_fm_create(fm_BZ, fm_struct, name="fm_BZ_selfcheck")
     640            0 :          CALL cp_fm_set_all(fm_BZ, 0.0_dp)
     641              :       END IF
     642            0 :       CALL cp_fm_struct_release(fm_struct)
     643              : 
     644            0 :       CALL cp_fm_get_info(fm_Z, nrow_local=nrow_local, row_indices=row_indices)
     645            0 :       DO k = 1, ncol
     646            0 :          DO iloc = 1, nrow_local
     647            0 :             fm_Z%local_data(iloc, k) = Z(row_indices(iloc), k)
     648              :          END DO
     649              :       END DO
     650              : 
     651            0 :       IF (PRESENT(fm_B_explicit)) THEN
     652            0 :          CALL bse_matvec_apply(mv_env, fm_Z, 1, ncol, fm_AZ, fm_BZ)
     653              :       ELSE
     654            0 :          CALL bse_matvec_apply(mv_env, fm_Z, 1, ncol, fm_AZ)
     655              :       END IF
     656              : 
     657            0 :       ALLOCATE (ref_matrix(n_ov, n_ov), mv_result(n_ov, ncol), ref_result(n_ov, ncol), diag(n_ov))
     658              : 
     659            0 :       CALL cp_fm_get_submatrix(fm_A_explicit, ref_matrix)
     660            0 :       CALL cp_fm_get_submatrix(fm_AZ, mv_result)
     661            0 :       ref_result(:, :) = MATMUL(ref_matrix, Z)
     662            0 :       dev_A = MAXVAL(ABS(mv_result - ref_result))
     663            0 :       CALL bse_matvec_diagonal(mv_env, bse_precond_full_diag, diag)
     664            0 :       dev_diag = 0.0_dp
     665            0 :       DO ia = 1, n_ov
     666            0 :          dev_diag = MAX(dev_diag, ABS(diag(ia) - ref_matrix(ia, ia)))
     667              :       END DO
     668              : 
     669            0 :       IF (PRESENT(fm_B_explicit)) THEN
     670            0 :          CALL cp_fm_get_submatrix(fm_B_explicit, ref_matrix)
     671            0 :          CALL cp_fm_get_submatrix(fm_BZ, mv_result)
     672            0 :          ref_result(:, :) = MATMUL(ref_matrix, Z)
     673            0 :          dev_B = MAXVAL(ABS(mv_result - ref_result))
     674              :       END IF
     675              : 
     676            0 :       IF (unit_nr > 0) THEN
     677            0 :          WRITE (unit_nr, '(T2,A10,T13,A,T59,ES22.6)') 'BSE|DEBUG|', &
     678            0 :             'Max deviation matvec vs explicit A', dev_A
     679            0 :          IF (PRESENT(fm_B_explicit)) THEN
     680            0 :             WRITE (unit_nr, '(T2,A10,T13,A,T59,ES22.6)') 'BSE|DEBUG|', &
     681            0 :                'Max deviation matvec vs explicit B', dev_B
     682              :          END IF
     683            0 :          WRITE (unit_nr, '(T2,A10,T13,A,T59,ES22.6)') 'BSE|DEBUG|', &
     684            0 :             'Max deviation diagonal vs explicit A', dev_diag
     685              :       END IF
     686              : 
     687            0 :       DEALLOCATE (Z, ref_matrix, mv_result, ref_result, diag)
     688            0 :       CALL cp_fm_release(fm_Z)
     689            0 :       CALL cp_fm_release(fm_AZ)
     690            0 :       IF (PRESENT(fm_B_explicit)) CALL cp_fm_release(fm_BZ)
     691              : 
     692            0 :       CALL timestop(handle)
     693              : 
     694            0 :    END SUBROUTINE bse_matvec_selfcheck
     695              : 
     696            0 : END MODULE bse_matvec
        

Generated by: LCOV version 2.0-1