Source code for dgamore.bubble_gen

# SPDX-FileCopyrightText: 2025-2026 Julian Peil <julian.peil@tuwien.ac.at>
# SPDX-License-Identifier: MIT
#
# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) &
#           Eliashberg Equation Solver for Strongly Correlated Electron Systems
r"""
Generalized bare susceptibilities (the "bubbles"). :class:`BubbleGenerator` builds the products of two Green's
functions :math:`\chi_{0;abcd} = -\beta\, G_{ad}\, G_{cb}` in the particle-hole and particle-particle channels,
both local and momentum-dependent. The non-local versions are evaluated either by an FFT over the BZ or by a
direct momentum-shift einsum, distributed over MPI ranks and optionally accelerated on the GPU (CuPy).
"""

import numpy as np

from dgamore.brillouin_zone import KGrid
from dgamore.dga_logger import DgaLogger
from dgamore.four_point import FourPoint
from dgamore.greens_function import GreensFunction
from dgamore.local_four_point import LocalFourPoint
from dgamore.matsubara_frequencies import MFHelper
from dgamore.mpi_utils import MpiDistributor
from dgamore.n_point_base import SpinChannel, FrequencyNotation


[docs] class BubbleGenerator: """ Collection of static factory methods that build the generalized bare susceptibilities (bubbles) from a Green's function in the particle-hole and particle-particle channels, both local and momentum-dependent. """
[docs] @staticmethod def create_generalized_chi0(g_dmft: GreensFunction, niw: int, niv: int, beta: float) -> LocalFourPoint: r""" Returns the local generalized bare susceptibility :math:`\chi_{0;abcd}^{\omega\nu} = -\beta\, G_{ad}^{\nu}\, G_{cb}^{\nu-\omega}`. :param g_dmft: The local (DMFT) :class:`GreensFunction`. :param niw: Number of positive bosonic frequencies. :param niv: Number of positive fermionic frequencies. :param beta: Inverse temperature :math:`\beta`. :return: The local bubble as a :class:`LocalFourPoint` with one bosonic and one fermionic frequency axis. """ wn = MFHelper.wn(niw) niv_range = np.arange(-niv, niv) g_left_mat = g_dmft.mat[:, None, None, :, None, g_dmft.niv - niv : g_dmft.niv + niv] g_right_mat = g_dmft.transpose_orbitals().mat[None, :, :, None, g_dmft.niv + niv_range[None, :] - wn[:, None]] return LocalFourPoint(-beta * g_left_mat * g_right_mat, SpinChannel.NONE, 1, 1).filter_small_values()
[docs] @staticmethod def create_generalized_chi0_q_fft( mpi_dist_irrk: MpiDistributor, giwk: GreensFunction, niw: int, niv: int, k_grid: KGrid, beta: float, use_gpu: bool = False, ) -> FourPoint: r""" Returns the momentum-dependent generalized bare susceptibility :math:`\chi_{0;abcd}^{q\nu} = -\beta \sum_k G^{k}_{ad}\, G^{k-q}_{cb}`, evaluated via an FFT over the BZ with preallocated buffers. The result is computed on rank 0 over the irreducible BZ and scattered across ranks. :param mpi_dist_irrk: MPI distributor over the irreducible BZ q-points (see :class:`MpiDistributor`). :param giwk: The momentum-dependent :class:`GreensFunction`. :param niw: Number of positive bosonic frequencies. :param niv: Number of positive fermionic frequencies. :param k_grid: The :class:`KGrid` over which the BZ sum/FFT is performed. :param beta: Inverse temperature :math:`\beta`. :param use_gpu: If True, compute with CuPy on the GPU; otherwise with NumPy on the CPU. :return: The bubble as a :class:`FourPoint` over the irreducible BZ (compressed momentum, half niw range). """ if use_gpu: import cupy as xp else: import numpy as xp order = "F" if use_gpu else "C" gchi0_q_mat = None if mpi_dist_irrk.my_rank == 0: nb = giwk.n_bands wn = MFHelper.wn(niw, return_only_positive=True) g_k = giwk.cut_niv(niv + niw) g_r = g_k.fft().decompress_q_dimension() g_r_rev = g_r.flip_momentum_axis(copy=True).transpose_orbitals() gchi0_q_mat = xp.zeros( (len(k_grid.irrk_ind), nb, nb, nb, nb, len(wn), 2 * niv), dtype=g_r.mat.dtype, order=order ) chi_r_v_buffer = xp.empty((*k_grid.nk, nb, nb, nb, nb, 2 * niv), dtype=g_r.mat.dtype, order=order) giwk_niv = g_r.current_shape[-1] // 2 start = giwk_niv - niv end = giwk_niv + niv g_r = xp.asarray(g_r.mat[..., start:end], order=order) g_r_rev = xp.asarray(g_r_rev.mat, order=order) for iw, wn_i in enumerate(wn): g_vw = g_r_rev[..., start - wn_i : end - wn_i] xp.multiply(g_r[:, :, :, :, None, None, :, :], g_vw[:, :, :, None, :, :, None, :], out=chi_r_v_buffer) gchi0_q_mat[..., iw, :] = xp.fft.ifftn(chi_r_v_buffer, axes=(0, 1, 2)).reshape( (k_grid.nk_tot, nb, nb, nb, nb, 2 * niv) )[k_grid.irrk_ind] gchi0_q_mat *= -beta / k_grid.nk_tot if use_gpu: gchi0_q_mat = xp.asnumpy(gchi0_q_mat) gchi0_q_mat = mpi_dist_irrk.scatter(gchi0_q_mat) return FourPoint( gchi0_q_mat, SpinChannel.NONE, k_grid.nk, 1, 1, full_niw_range=False, has_compressed_q_dimension=True ).filter_small_values()
[docs] @staticmethod def create_generalized_chi0_q_fft_auto( mpi_dist_irrk: MpiDistributor, giwk: GreensFunction, niw: int, niv: int, k_grid: KGrid, beta: float, logger: DgaLogger, ): r""" Dispatches :meth:`create_generalized_chi0_q_fft` to the GPU when CuPy and a usable CUDA device are available (assigning one GPU per MPI rank round-robin), otherwise falls back to the CPU. :param mpi_dist_irrk: MPI distributor over the irreducible BZ q-points (see :class:`MpiDistributor`). :param giwk: The momentum-dependent :class:`GreensFunction`. :param niw: Number of positive bosonic frequencies. :param niv: Number of positive fermionic frequencies. :param k_grid: The :class:`KGrid` over which the BZ sum/FFT is performed. :param beta: Inverse temperature :math:`\beta`. :param logger: Logger used to report whether GPU acceleration is used. :return: The bubble as a :class:`FourPoint` over the irreducible BZ. """ cp = None try: import cupy as cp except ImportError: pass # CuPy not installed -> CPU n_gpus = 0 if cp is not None: try: n_gpus = cp.cuda.runtime.getDeviceCount() except cp.cuda.runtime.CUDARuntimeError: n_gpus = 0 # no usable CUDA driver/device -> CPU if n_gpus > 0 and cp.cuda.is_available(): logger.info(f"CuPy detected {n_gpus} GPU(s). Using GPU acceleration for gchi0_q calculation.") gpu_id = mpi_dist_irrk.my_rank % n_gpus cp.cuda.Device(gpu_id).use() return BubbleGenerator.create_generalized_chi0_q_fft( mpi_dist_irrk, giwk, niw, niv, k_grid, beta, use_gpu=True ) return BubbleGenerator.create_generalized_chi0_q_fft(mpi_dist_irrk, giwk, niw, niv, k_grid, beta, use_gpu=False)
[docs] @staticmethod def create_generalized_chi0_q( giwk: GreensFunction, niw: int, niv: int, q_list: np.ndarray, q_grid: KGrid, beta: float, use_gpu: bool = False, ) -> FourPoint: r""" Returns the momentum-dependent generalized bare susceptibility :math:`\chi_{0;abcd}^{q\nu} = -\beta \sum_k G^{k}_{ad}\, G^{k-q}_{cb}`, evaluated by a direct momentum-shift and a fused einsum over the explicit list of q-points (preallocated buffers). :param giwk: The momentum-dependent :class:`GreensFunction`. :param niw: Number of positive bosonic frequencies. :param niv: Number of positive fermionic frequencies. :param q_list: Array of integer q-point index triplets to compute. :param q_grid: The :class:`KGrid` providing the momentum normalization (``nk_tot``). :param beta: Inverse temperature :math:`\beta`. :param use_gpu: If True, compute with CuPy on the GPU; otherwise with NumPy on the CPU. :return: The bubble as a :class:`FourPoint` over the given q-points (compressed momentum, half niw range). """ if use_gpu: import cupy as xp else: import numpy as xp order = "F" if use_gpu else "C" wn = MFHelper.wn(niw, return_only_positive=True) nb = giwk.n_bands nq = len(q_list) gchi0_q = xp.zeros((nq, nb, nb, nb, nb, len(wn), 2 * niv), dtype=giwk.mat.dtype, order=order) # g_left (the central niv window) and g_right (the full range, momentum-shifted per q) are both read-only, # so they can share one backing array instead of holding a full-size duplicate. g_full = xp.asarray(giwk.cut_niv(niv + niw).mat, order=order) giwk_niv = g_full.shape[-1] // 2 g_r_buf = xp.empty_like(g_full) g_right = g_full g_left = g_full[..., giwk_niv - niv : giwk_niv + niv] # the precomputed contraction path is only used on the CPU; cupy.einsum optimizes internally path = True if use_gpu else xp.einsum_path("xyzadv,xyzcbv->abcdv", g_left, g_left, optimize="optimal")[0] kxs, kys, kzs = xp.arange(g_right.shape[0]), xp.arange(g_right.shape[1]), xp.arange(g_right.shape[2]) for iq, q in enumerate(q_list): g_r_buf[...] = xp.take(g_right, (kxs - q[0]) % g_right.shape[0], axis=0) g_r_buf[...] = xp.take(g_r_buf, (kys - q[1]) % g_right.shape[1], axis=1) g_r_buf[...] = xp.take(g_r_buf, (kzs - q[2]) % g_right.shape[2], axis=2) for iw, wn_i in enumerate(wn): s = giwk_niv - niv - wn_i e = giwk_niv + niv - wn_i gchi0_q[iq, ..., iw, :] = xp.einsum("xyzadv,xyzcbv->abcdv", g_left, g_r_buf[..., s:e], optimize=path) gchi0_q *= -beta / q_grid.nk_tot if use_gpu: gchi0_q = xp.asnumpy(gchi0_q) return FourPoint( gchi0_q, SpinChannel.NONE, q_grid.nk, 1, 1, full_niw_range=False, has_compressed_q_dimension=True ).filter_small_values()
[docs] @staticmethod def create_generalized_chi0_q_auto( mpi_distributor: MpiDistributor, giwk: GreensFunction, niw: int, niv: int, q_list: np.ndarray, q_grid: KGrid, beta: float, logger: DgaLogger, ): r""" Dispatches :meth:`create_generalized_chi0_q` to the GPU when CuPy and a usable CUDA device are available (assigning one GPU per MPI rank round-robin), otherwise falls back to the CPU. :param mpi_distributor: MPI distributor over the q-points (see :class:`MpiDistributor`). :param giwk: The momentum-dependent :class:`GreensFunction`. :param niw: Number of positive bosonic frequencies. :param niv: Number of positive fermionic frequencies. :param q_list: Array of integer q-point index triplets to compute. :param q_grid: The :class:`KGrid` providing the momentum normalization. :param beta: Inverse temperature :math:`\beta`. :param logger: Logger used to report whether GPU acceleration is used. :return: The bubble as a :class:`FourPoint` over the given q-points. """ cp = None try: import cupy as cp except ImportError: pass # CuPy not installed -> CPU n_gpus = 0 if cp is not None: try: n_gpus = cp.cuda.runtime.getDeviceCount() except cp.cuda.runtime.CUDARuntimeError: n_gpus = 0 # no usable CUDA driver/device -> CPU if n_gpus > 0 and cp.cuda.is_available(): logger.info(f"CuPy detected {n_gpus} GPU(s). Using GPU acceleration for gchi0_q calculation.") gpu_id = mpi_distributor.my_rank % n_gpus cp.cuda.Device(gpu_id).use() return BubbleGenerator.create_generalized_chi0_q(giwk, niw, niv, q_list, q_grid, beta, use_gpu=True) return BubbleGenerator.create_generalized_chi0_q(giwk, niw, niv, q_list, q_grid, beta, use_gpu=False)
[docs] @staticmethod def create_generalized_chi0_pp_w0(g_dmft: GreensFunction, niv_pp: int, beta: float) -> LocalFourPoint: r""" Returns the local particle-particle bare bubble at :math:`\omega = 0`, :math:`\chi_{0;abcd}^{\nu} = -\beta\, G_{ad}^{\nu}\, G_{cb}^{-\nu}`. :param g_dmft: The local (DMFT) :class:`GreensFunction`. :param niv_pp: Number of positive fermionic frequencies of the pp bubble. :param beta: Inverse temperature :math:`\beta`. :return: The local pp bubble as a :class:`LocalFourPoint` in pp notation at :math:`\omega = 0`. """ g = g_dmft.cut_niv(niv_pp) # transpose_orbitals() returns a fresh private copy, so flip it in place (copy=False) instead of deepcopying # that throwaway again. gchi0_pp_w0 = ( g.mat[:, None, None, :, :] * g.transpose_orbitals().flip_frequency_axis(-1, copy=False).mat[None, :, :, None, :] ) return LocalFourPoint( -beta * gchi0_pp_w0[..., None, :], SpinChannel.NONE, 1, 1, frequency_notation=FrequencyNotation.PP ).filter_small_values()
[docs] @staticmethod def create_generalized_chi0_q_pp_w0(giwk: GreensFunction, niv_pp: int, q_grid: KGrid) -> FourPoint: r""" Returns the momentum-dependent particle-particle bare bubble at :math:`\omega = 0`, :math:`\chi_{0;abcd}^{\vec{k}(\omega=0)\nu} = G_{ad}^{k}\, G_{bc}^{-k}` with :math:`G_{bc}^{-k} = G_{cb}^{*k}`. Note that no factor of :math:`-\beta` is included here. :param giwk: The momentum-dependent :class:`GreensFunction`. :param niv_pp: Number of positive fermionic frequencies of the pp bubble. :param q_grid: The :class:`KGrid` defining the momentum grid. :return: The momentum-dependent pp bubble as a :class:`FourPoint` (no bosonic axis, pp notation, compressed q). """ g = giwk.cut_niv(niv_pp).compress_q_dimension() # transpose_orbitals() returns a fresh private copy, so conjugate it in place to reuse its buffer. g_t = g.transpose_orbitals() np.conj(g_t.mat, out=g_t.mat) gchi0_q_pp_w0 = g.mat[:, :, None, None, :, :] * g_t.mat[:, None, :, :, None, :] return FourPoint( gchi0_q_pp_w0, SpinChannel.NONE, q_grid.nk, 0, 1, True, True, True, FrequencyNotation.PP ).filter_small_values()