Source code for dgamore.interaction

# 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"""
Interaction tensors. :class:`LocalInteraction` wraps the momentum-independent (Hubbard/Kanamori) interaction
:math:`U_{abcd}`; :class:`Interaction` adds a momentum dimension for the non-local interaction :math:`V_{abcd}^q`.
Both provide the channel projections (density/magnetic/singlet/triplet) and the algebra used in the ladder
equations.
"""

from copy import deepcopy

import numpy as np

from dgamore.n_point_base import IHaveMat, IHaveChannel, IAmNonLocal, SpinChannel


[docs] class LocalInteraction(IHaveMat, IHaveChannel): r""" Class for the local (momentum-independent) interaction :math:`U_{abcd}`. """ def __init__(self, mat: np.ndarray, channel: SpinChannel = SpinChannel.NONE): r""" Initializes the local interaction tensor in the given spin channel. :param mat: Interaction tensor :math:`U_{abcd}` with four orbital axes ``[o1, o2, o3, o4]``. :param channel: Spin channel the tensor is expressed in (see :class:`SpinChannel`); ``NONE`` for the bare, not-yet-projected interaction. """ IHaveMat.__init__(self, mat) IHaveChannel.__init__(self, channel) @property def n_bands(self) -> int: """ Returns the number of bands (orbitals). :return: Number of bands, i.e. the size of the first orbital axis. """ return self.original_shape[0]
[docs] def permute_orbitals(self, permutation: str = "abcd->abcd") -> "LocalInteraction": """ Permutes the four orbital axes according to an einsum-style permutation string. :param permutation: Einsum permutation of the four orbital labels, e.g. ``"abcd->adcb"``. :return: A new :class:`LocalInteraction` with permuted orbitals (a deep copy for the identity permutation). :raises ValueError: If the permutation string is not a valid four-orbital permutation. """ split = permutation.split("->") if len(split) != 2 or len(split[0]) != 4 or len(split[1]) != 4: raise ValueError("Invalid permutation.") if split[0] == split[1]: return deepcopy(self) return LocalInteraction(np.einsum(permutation, self.mat, optimize=True), self.channel)
[docs] def as_channel(self, channel: SpinChannel) -> "LocalInteraction": r""" Projects the bare interaction onto a given spin channel. With the crossing-permuted tensor :math:`\tilde U_{abcd} = U_{adcb}` the projections are :math:`U_d = 2U - \tilde U`, :math:`U_m = -\tilde U`, :math:`U_s = U + \tilde U`, :math:`U_t = U - \tilde U`. :param channel: Target spin channel (density, magnetic, singlet or triplet). :return: A new :class:`LocalInteraction` in the requested channel. :raises ValueError: If the object is already in a (non-``NONE``) channel, or the target channel is unsupported. """ copy = deepcopy(self) if copy.channel == channel: return copy elif copy.channel != channel.NONE: raise ValueError(f"Cannot transform interaction from channel {copy.channel} to {channel}.") copy._channel = channel perm: str = "abcd->adcb" if channel == SpinChannel.DENS: return 2 * copy - copy.permute_orbitals(perm) elif channel == SpinChannel.MAGN: return -copy.permute_orbitals(perm) elif channel == SpinChannel.SING: return copy + copy.permute_orbitals(perm) elif channel == SpinChannel.TRIP: return copy - copy.permute_orbitals(perm) else: raise ValueError(f"Channel {channel} not supported.")
[docs] def add(self, other) -> "LocalInteraction": """ Adds another interaction or a raw numpy array element-wise; see :meth:`_add`. :param other: A :class:`LocalInteraction` or a numpy array broadcastable to ``mat``. :return: A new :class:`LocalInteraction` holding the sum (inheriting the non-``NONE`` channel of the operands). """ return self._add(other)
def _add(self, other, subtract: bool = False) -> "LocalInteraction": """ Adds (or, if ``subtract`` is True, subtracts) another interaction or a raw numpy array element-wise. :param other: A :class:`LocalInteraction` or a numpy array broadcastable to ``mat``. :param subtract: If True, subtract ``other`` instead of adding it (used by :meth:`sub` to avoid a negated copy). :return: A new :class:`LocalInteraction` holding the sum/difference (the non-``NONE`` channel of the operands). :raises ValueError: If ``other`` is neither a :class:`LocalInteraction` nor a numpy array. """ if not isinstance(other, (LocalInteraction, np.ndarray)): raise ValueError(f"Operation {type(self)} +/- {type(other)} not supported.") op = np.subtract if subtract else np.add if isinstance(other, np.ndarray): return LocalInteraction(op(self.mat, other), self.channel) return LocalInteraction( op(self.mat, other.mat), self.channel if self.channel != SpinChannel.NONE else other.channel )
[docs] def sub(self, other) -> "LocalInteraction": """ Subtracts another interaction or a raw numpy array element-wise; see :meth:`_add`. :param other: A :class:`LocalInteraction` or a numpy array broadcastable to ``mat``. :return: A new :class:`LocalInteraction` holding the difference ``self - other``. """ return self._add(other, subtract=True)
[docs] def pow(self, power) -> "LocalInteraction": r""" Raises the interaction to an integer power via repeated orbital contraction :math:`U^{(n)}_{abef} = U_{abcd} U^{(n-1)}_{dcef}`. :param power: Positive integer exponent (must be greater than zero). :return: A new :class:`LocalInteraction` equal to ``self`` contracted with itself ``power`` times. :raises ValueError: If ``power`` is not a positive integer greater than zero. """ if power <= 0: raise ValueError("Exponentiation of Interaction objects only supports positive powers greater than zero.") result = deepcopy(self) for _ in range(1, power): result = LocalInteraction(result.times("abcd,dcef->abef", self), self.channel) return result
def __add__(self, other) -> "LocalInteraction": """ Operator overload for ``self + other``; see :meth:`add`. :param other: A :class:`LocalInteraction` or a numpy array. :return: The sum as a new :class:`LocalInteraction`. """ return self.add(other) def __radd__(self, other) -> "LocalInteraction": """ Reflected operator overload for ``other + self``; see :meth:`add` (addition commutes). :param other: A :class:`LocalInteraction` or a numpy array. :return: The sum as a new :class:`LocalInteraction`. """ return self.add(other) def __sub__(self, other) -> "LocalInteraction": """ Operator overload for ``self - other``; see :meth:`sub`. :param other: A :class:`LocalInteraction` or a numpy array. :return: The difference ``self - other`` as a new :class:`LocalInteraction`. """ return self.sub(other) def __rsub__(self, other) -> "LocalInteraction": """ Reflected operator overload for ``other - self`` (returns ``-(self - other)``). :param other: A :class:`LocalInteraction` or a numpy array on the left-hand side. :return: The difference ``other - self`` as a new :class:`LocalInteraction`. """ return -self.sub(other) def __pow__(self, power, modulo=None) -> "LocalInteraction": """ Operator overload for ``self ** power``; see :meth:`pow`. :param power: Positive integer exponent. :param modulo: Unused (present for the ``__pow__`` protocol). :return: ``self`` raised to ``power`` as a new :class:`LocalInteraction`. """ return self.pow(power)
[docs] class Interaction(IAmNonLocal, LocalInteraction): r""" Class for the non-local (momentum-dependent) interaction :math:`V_{abcd}^q`. """ def __init__( self, mat: np.ndarray, channel: SpinChannel = SpinChannel.NONE, nq: tuple[int, int, int] = (1, 1, 1), has_compressed_q_dimension: bool = False, ): r""" Initializes the non-local interaction tensor in the given spin channel and momentum layout. :param mat: Interaction tensor :math:`V_{abcd}^q` with one momentum dimension and four orbital axes. :param channel: Spin channel the tensor is expressed in (see :class:`SpinChannel`). :param nq: Number of momenta per spatial direction ``(nqx, nqy, nqz)``. :param has_compressed_q_dimension: Whether the momentum is stored as a single compressed axis ``[q, ...]`` (True) or as three separate axes ``[qx, qy, qz, ...]`` (False). """ LocalInteraction.__init__(self, mat, channel) IAmNonLocal.__init__(self, mat, nq, has_compressed_q_dimension)
[docs] def permute_orbitals(self, permutation: str = "abcd->abcd") -> "Interaction": """ Permutes the four orbital axes (leaving the momentum axis untouched). :param permutation: Einsum permutation of the four orbital labels, e.g. ``"abcd->adcb"``. :return: A new :class:`Interaction` with permuted orbitals (a deep copy for the identity permutation). :raises ValueError: If the permutation string is not a valid four-orbital permutation. """ split = permutation.split("->") if len(split) != 2 or len(split[0]) != 4 or len(split[1]) != 4: raise ValueError("Invalid permutation.") if split[0] == split[1]: return deepcopy(self) permutation = f"...{split[0]}->...{split[1]}" return Interaction( np.einsum(permutation, self.mat, optimize=True), self.channel, self.nq, self.has_compressed_q_dimension )
[docs] def as_channel(self, channel: SpinChannel) -> "Interaction": r""" Projects the bare non-local interaction onto a given spin channel. Only the non-local particle-hole contribution enters the ladder DGA equations and the ph-bar contribution to the spin channels vanishes, so the projections reduce to :math:`V_d = 2V`, :math:`V_m = 0`, :math:`V_s = V_t = V`. :param channel: Target spin channel (density, magnetic, singlet or triplet). :return: A new :class:`Interaction` in the requested channel. :raises ValueError: If the object is already in a (non-``NONE``) channel, or the target channel is unsupported. """ copy = deepcopy(self) if copy.channel == channel: return copy elif copy.channel != channel.NONE: raise ValueError(f"Cannot transform interaction from channel {copy.channel} to {channel}.") copy._channel = channel if channel == SpinChannel.DENS: return 2 * copy elif channel == SpinChannel.MAGN: return 0 * copy elif channel == SpinChannel.SING: return copy elif channel == SpinChannel.TRIP: return copy else: raise ValueError(f"Channel {channel} not supported.")
[docs] def add(self, other) -> "Interaction": """ Adds another (non-)local interaction or a raw numpy array (a local operand is broadcast over the momentum axis); see :meth:`_add`. :param other: An :class:`Interaction`, a :class:`LocalInteraction`, or a numpy array. :return: A new :class:`Interaction` holding the sum (inheriting the non-``NONE`` channel of the operands). """ return self._add(other)
def _add(self, other, subtract: bool = False) -> "Interaction": """ Adds (or, if ``subtract`` is True, subtracts) another (non-)local interaction or a raw numpy array. A local operand is broadcast over the momentum axis. :param other: An :class:`Interaction`, a :class:`LocalInteraction`, or a numpy array. :param subtract: If True, subtract ``other`` instead of adding it (used by :meth:`sub` to avoid a negated copy). :return: A new :class:`Interaction` holding the sum/difference (the non-``NONE`` channel of the operands). :raises ValueError: If ``other`` is not one of the supported types. """ if not isinstance(other, (LocalInteraction, Interaction, np.ndarray)): raise ValueError(f"Operation {type(self)} +/- {type(other)} not supported.") op = np.subtract if subtract else np.add if isinstance(other, np.ndarray): return Interaction(op(self.mat, other), self.channel, self.nq, self.has_compressed_q_dimension) if not isinstance(other, Interaction): other_mat = other.mat[None, ...] if self.has_compressed_q_dimension else other.mat[None, None, None, ...] else: other_mat = other.mat return Interaction( op(self.mat, other_mat), self.channel if self.channel != SpinChannel.NONE else other.channel, self.nq, self.has_compressed_q_dimension, )
[docs] def sub(self, other) -> "Interaction": """ Subtracts another (non-)local interaction or a raw numpy array; see :meth:`_add`. :param other: An :class:`Interaction`, a :class:`LocalInteraction`, or a numpy array. :return: A new :class:`Interaction` holding the difference ``self - other``. """ return self._add(other, subtract=True)
[docs] def pow(self, power) -> "Interaction": r""" Raises the interaction to an integer power via repeated momentum-diagonal orbital contraction :math:`V^{(n);q}_{abef} = V^{q}_{abcd} V^{(n-1);q}_{dcef}`. :param power: Positive integer exponent (must be greater than zero). :return: A new :class:`Interaction` in the same momentum-compression state as ``self``. :raises ValueError: If ``power`` is not a positive integer greater than zero. """ if power <= 0: raise ValueError("Exponentiation of Interaction objects only supports positive powers greater than zero.") is_self_compressed = self.has_compressed_q_dimension result = deepcopy(self).compress_q_dimension() for _ in range(1, power): result = Interaction(result.times("qabcd,qdcef->qabef", self), self.channel, self.nq, True) return result if is_self_compressed else result.decompress_q_dimension()
def __radd__(self, other) -> "Interaction": """ Reflected operator overload for ``other + self``; see :meth:`add` (addition commutes). Kept (not redundant) so that a mixed ``LocalInteraction + Interaction`` dispatches here. :param other: An :class:`Interaction`, a :class:`LocalInteraction`, or a numpy array. :return: The sum as a new :class:`Interaction`. """ return self.add(other) def __rsub__(self, other) -> "Interaction": """ Reflected operator overload for ``other - self`` (returns ``-(self - other)``). Kept (not redundant) so that a mixed ``LocalInteraction - Interaction`` dispatches here. :param other: An :class:`Interaction`, a :class:`LocalInteraction`, or a numpy array on the left-hand side. :return: The difference ``other - self`` as a new :class:`Interaction`. """ return -self.sub(other)