mbd_rpa.F90 Source File


Source Code

! This Source Code Form is subject to the terms of the Mozilla Public
! License, v. 2.0. If a copy of the MPL was not distributed with this
! file, You can obtain one at http://mozilla.org/MPL/2.0/.
#ifndef DO_COMPLEX_TYPE
module mbd_rpa

use mbd_constants
use mbd_damping, only: damping_t
use mbd_dipole, only: dipole_matrix
use mbd_formulas, only: sigma_selfint, rpa_rescale_eigval
use mbd_geom, only: geom_t
use mbd_gradients, only: grad_t, grad_matrix_re_t, grad_matrix_cplx_t, &
    grad_request_t
use mbd_matrix, only: matrix_re_t, matrix_cplx_t
use mbd_utils, only: result_t, tostr

implicit none

private
public :: get_mbd_rpa_energy

interface get_mbd_rpa_energy
    !! Get the MBD energy by the RPA method.
    !!
    !! The energy is obtained from the eigenvalues \(\mu_k(\mathrm iu)\) of the
    !! symmetrized dipole matrix \(\mathbf M(\mathrm iu)=\mathbf A(\mathrm
    !! iu)^\frac12\mathbf T\mathbf A(\mathrm iu)^\frac12\), with \(A_{ij}(\mathrm
    !! iu)=\alpha_i(\mathrm iu)\delta_{ij}\),
    !!
    !! $$
    !! E=\frac1{2\pi}\int_0^\infty\mathrm du\sum_k\log(1+\mu_k(\mathrm iu))
    !! =\frac1{2\pi}\int_0^\infty\mathrm du\,\log\det(\mathbf 1+\mathbf M(\mathrm iu))
    !! $$
    !!
    !! The derivatives use \(\partial\mu_k=\mathbf c_k^\dagger(\partial\mathbf
    !! M)\mathbf c_k\), which upon summation over the eigenvalues gives the
    !! weighted resolvent \(\mathbf B(\mathrm iu)=\mathbf C\operatorname{diag}(
    !! g'(\mu_k))\mathbf C^\dagger\), where \(g(\mu)=\log(1+\mu)\) is the
    !! per-eigenvalue energy contribution,
    !!
    !! $$
    !! \partial E=\frac1{2\pi}\int_0^\infty\mathrm du
    !! \operatorname{Tr}\big(\mathbf B(\mathrm iu)\partial\mathbf M(\mathrm iu)\big)
    !! $$
    !!
    !! Both the explicit derivatives (coordinates, lattice vectors, van der
    !! Waals radii, \(\mathbf q\)) and the implicit derivatives with respect to
    !! the dynamic polarizabilities are returned. The latter propagate both
    !! through the \(\sqrt{\alpha_i\alpha_j}\) prefactor and, when the damping
    !! depends on it, through \(\sigma_{ij}(\alpha)\). With eigenvalue rescaling
    !! ([[mbd_calc_params_t:rpa_rescale_eigs]]) the contribution becomes
    !! \(g(\mu)=\log(1+\lambda(\mu))-\lambda(\mu)\), with
    !! \(\lambda=-\operatorname{erf}(\tfrac{\sqrt\pi}2\mu^4)^\frac14\) for
    !! \(\mu<0\), and the same weighted resolvent is used.
    !!
    !! The real-typed version is equivalent to \(\mathbf q=0\).
    module procedure get_mbd_rpa_energy_real
    module procedure get_mbd_rpa_energy_complex
end interface

contains

#endif

#ifndef DO_COMPLEX_TYPE
type(result_t) function get_mbd_rpa_energy_real( &
        geom, alpha, damp, grad) result(res)
#else
type(result_t) function get_mbd_rpa_energy_complex( &
        geom, alpha, damp, q, grad) result(res)
#endif
    type(geom_t), intent(inout) :: geom
    real(dp), intent(in) :: alpha(:, 0:)
    type(damping_t), intent(in) :: damp
#ifdef DO_COMPLEX_TYPE
    real(dp), intent(in) :: q(3)
#endif
    type(grad_request_t), intent(in), optional :: grad

#ifndef DO_COMPLEX_TYPE
    type(matrix_re_t) :: relay, AT, Mmat, modes, B, dQ
    type(grad_matrix_re_t) :: dT
#else
    type(matrix_cplx_t) :: relay, AT, Mmat, modes, B, dQ
    type(grad_matrix_cplx_t) :: dT
#endif
    real(dp), allocatable :: eigs(:), log_eigs(:), sqrt_alpha(:), &
        g_prime(:), contr(:), dxr(:)
    integer :: i_freq, my_i_atom, n_order, n_negative_eigs, my_j_atom, &
        n_atoms, i_xyz, i_latt
    real(dp) :: freq_w, sigma_ij
    type(damping_t) :: damp_alpha
    type(grad_request_t) :: grad_dip
    logical :: do_grad

    do_grad = .false.
    if (present(grad)) do_grad = grad%any()
    n_atoms = geom%siz()
    if (do_grad) then
        allocate (g_prime(3 * n_atoms))
        if (geom%param%rpa_rescale_eigs) allocate (dxr(3 * n_atoms))
        grad_dip%dcoords = grad%dcoords
        grad_dip%dlattice = grad%dlattice
        grad_dip%dr_vdw = grad%dr_vdw
        ! the dipole matrix depends on the dynamic polarizability through the
        ! self-consistent-screening width sigma only for the gg dampings
        grad_dip%dsigma = grad%dalpha_dyn .and. index(damp%version, 'gg') > 0
#ifdef DO_COMPLEX_TYPE
        grad_dip%dq = grad%dq
#endif
        if (grad%dcoords) allocate (res%dE%dcoords(n_atoms, 3), source=0d0)
        if (grad%dr_vdw) allocate (res%dE%dr_vdw(n_atoms), source=0d0)
        if (grad%dalpha_dyn) &
            allocate (res%dE%dalpha_dyn(n_atoms, 0:ubound(alpha, 2)), source=0d0)
#ifndef DO_COMPLEX_TYPE
        if (grad%dlattice) allocate (res%dE%dlattice(3, 3), source=0d0)
#else
        if (grad%dlattice) allocate (res%dE%dlattice(3, 3), source=0d0)
        if (grad%dq) allocate (res%dE%dq(3), source=0d0)
#endif
    end if

    res%energy = 0d0
    damp_alpha = damp
    ! implicit allocation doesn't work here in gfortran 4.9
    allocate (eigs(3 * geom%siz()), log_eigs(3 * geom%siz()))
    if (geom%get_rpa_orders) allocate (res%rpa_orders(geom%param%rpa_order_max), source=0d0)
    do i_freq = 0, ubound(geom%freq, 1)
        damp_alpha%sigma = sigma_selfint(alpha(:, i_freq))
        sqrt_alpha = sqrt(alpha(:, i_freq))
        if (do_grad) then
#ifndef DO_COMPLEX_TYPE
            relay = dipole_matrix(geom, damp_alpha, dT, grad_dip)
#else
            relay = dipole_matrix(geom, damp_alpha, dT, grad_dip, q=q)
#endif
        else
#ifndef DO_COMPLEX_TYPE
            relay = dipole_matrix(geom, damp_alpha)
#else
            relay = dipole_matrix(geom, damp_alpha, q=q)
#endif
        end if
        if (geom%has_exc()) return
        if (do_grad) then
            ! keep relay = T, build M = A^1/2 T A^1/2 separately
            call Mmat%copy_from(relay)
            call Mmat%mult_cross(sqrt_alpha)
            call modes%alloc_from(Mmat)
            call geom%clock(23)
            call modes%eigh(eigs, geom%exc, src=Mmat, clock=geom%timer)
            call geom%clock(-23)
        else
            call relay%mult_cross(sqrt_alpha)
            call AT%move_from(relay)
            call geom%clock(23)
            eigs = AT%eigvalsh(geom%exc, destroy=.true.)
            call geom%clock(-23)
        end if
        if (geom%has_exc()) return
        if (geom%param%rpa_rescale_eigs) then
            eigs(:) = rpa_rescale_eigval(eigs, dxr)
        end if
        n_negative_eigs = count(eigs(:) <= -1)
        if (n_negative_eigs > 0) then
            geom%exc%code = MBD_EXC_NEG_EIGVALS
            geom%exc%msg = "1+AT matrix has "// &
                trim(tostr(n_negative_eigs))//" negative eigenvalues"
            return
        end if
        log_eigs = log(1 + eigs)
        if (geom%param%rpa_rescale_eigs) then
            log_eigs = log_eigs - eigs
        end if
        res%energy = res%energy + &
            1d0 / (2 * pi) * sum(log_eigs) * geom%freq(i_freq)%weight
        if (geom%get_rpa_orders) then
            do n_order = 2, geom%param%rpa_order_max
                res%rpa_orders(n_order) = res%rpa_orders(n_order) &
                    + (-1d0 / (2 * pi) * (-1)**n_order &
                    * sum(eigs**n_order) / n_order) &
                    * geom%freq(i_freq)%weight
            end do
        end if
        if (.not. do_grad) cycle
        freq_w = geom%freq(i_freq)%weight
        ! The per-eigenvalue energy contribution is g(mu_k), whose derivative
        ! with respect to a parameter is g'(mu_k) times the derivative of the
        ! raw eigenvalue mu_k of M. Summed, this gives the weighted resolvent
        ! B = C diag(g'(mu_k)) C^dagger, contracted below with dM.
        if (.not. geom%param%rpa_rescale_eigs) then
            ! g(mu) = log(1 + mu)
            g_prime = 1d0 / (1d0 + eigs)
        else
            ! g(mu) = log(1 + lambda) - lambda with lambda = eigs (rescaled) and
            ! dlambda/dmu = dxr; chain rule g'(mu) = (1/(1 + lambda) - 1) dlambda/dmu
            g_prime = (1d0 / (1d0 + eigs) - 1d0) * dxr
        end if
        call B%copy_from(modes)
        call B%mult_cols_3n(g_prime)
        B = B%mmul(modes, transB='C')
#ifdef DO_COMPLEX_TYPE
        B%val = conjg(B%val)
#endif
        call dQ%init_from(relay)
        if (grad%dcoords) then
            do i_xyz = 1, 3
                dQ%val = dT%dr(:, :, i_xyz)
                call dQ%mult_cross(sqrt_alpha)
                dQ%val = B%val * dQ%val
                contr = freq_w / pi * dble(dQ%contract_n33_rows())
                res%dE%dcoords(:, i_xyz) = res%dE%dcoords(:, i_xyz) + contr
            end do
        end if
        if (grad%dlattice) then
            do i_latt = 1, 3
                do i_xyz = 1, 3
                    dQ%val = dT%dlattice(:, :, i_latt, i_xyz)
                    call dQ%mult_cross(sqrt_alpha)
                    dQ%val = B%val * dQ%val
                    res%dE%dlattice(i_latt, i_xyz) = res%dE%dlattice(i_latt, i_xyz) + &
                        freq_w / (2 * pi) * dble(dQ%sum_all())
                end do
            end do
        end if
        if (grad%dr_vdw) then
            dQ%val = dT%dvdw
            call dQ%mult_cross(sqrt_alpha)
            dQ%val = B%val * dQ%val
            contr = freq_w / pi * dble(dQ%contract_n33_rows())
            res%dE%dr_vdw = res%dE%dr_vdw + contr
        end if
#ifdef DO_COMPLEX_TYPE
        if (grad%dq) then
            do i_latt = 1, 3
                dQ%val = dT%dq(:, :, i_latt)
                call dQ%mult_cross(sqrt_alpha)
                dQ%val = B%val * dQ%val
                res%dE%dq(i_latt) = res%dE%dq(i_latt) + &
                    freq_w / (2 * pi) * dble(dQ%sum_all())
            end do
        end if
#endif
        if (grad%dalpha_dyn) then
            ! channel through the sqrt(alpha_i alpha_j) prefactor
            dQ%val = relay%val
            call dQ%mult_cross(sqrt_alpha)
            call dQ%mult_rows(1d0 / (2 * alpha(:, i_freq)))
            dQ%val = B%val * dQ%val
            contr = freq_w / pi * dble(dQ%contract_n33_rows())
            res%dE%dalpha_dyn(:, i_freq) = res%dE%dalpha_dyn(:, i_freq) + contr
            ! channel through sigma_ij(alpha), if the damping uses it
            if (grad_dip%dsigma) then
                dQ%val = dT%dsigma
                call dQ%mult_cross(sqrt_alpha)
                dQ%val = B%val * dQ%val
                ! scale each block (i, j) by 1 / sigma_ij
                do my_i_atom = 1, size(dQ%idx%i_atom)
                    do my_j_atom = 1, size(dQ%idx%j_atom)
                        associate ( &
                                i_atom => dQ%idx%i_atom(my_i_atom), &
                                j_atom => dQ%idx%j_atom(my_j_atom), &
                                dQ_sub => dQ%val( &
                                    3 * (my_i_atom - 1) + 1:, &
                                    3 * (my_j_atom - 1) + 1: &
                                ) &
                        )
                            sigma_ij = damp%mayer_scaling * sqrt(sum( &
                                damp_alpha%sigma([i_atom, j_atom])**2))
                            dQ_sub(:3, :3) = dQ_sub(:3, :3) / sigma_ij
                        end associate
                    end do
                end do
                contr = freq_w / pi * damp%mayer_scaling**2 &
                    * damp_alpha%sigma**2 / (3 * alpha(:, i_freq)) &
                    * dble(dQ%contract_n33_rows())
                res%dE%dalpha_dyn(:, i_freq) = res%dE%dalpha_dyn(:, i_freq) + contr
            end if
        end if
    end do
end function

#ifndef DO_COMPLEX_TYPE
#   define DO_COMPLEX_TYPE
#   include "mbd_rpa.F90"

end module

#endif