Source code for dispersionrelations.integrals

import warnings

import numpy as np
from dispersionrelations.utils import logC

z_gl_100, w_gl_100 = np.polynomial.legendre.leggauss(100)
z_gl_500, w_gl_500 = np.polynomial.legendre.leggauss(500)
z_gl_1000, w_gl_1000 = np.polynomial.legendre.leggauss(1000)
z_gl_2000, w_gl_2000 = np.polynomial.legendre.leggauss(2000)

gl_storage = {
    100: (z_gl_100, w_gl_100),
    500: (z_gl_500, w_gl_500),
    1000: (z_gl_1000, w_gl_1000),
    2000: (z_gl_2000, w_gl_2000),
}


[docs] def integrate_gl(function : callable, a : float, b : float, order : int = 100, split_n : int = 3): r""" Gauss–Legendre integration. Parameters ---------- function : callable A complex function of a single variable. a : float, complex Lower boundary of the integral. b : float, complex Upper boundary of the integral. order : int Quadrature order. split_n : int Number of subintervals of the same length :math:`[a,x], [x,y], ... [z,b]`. Returns ------- I : float, complex Integral :math:`I = \int_a^b f(x) \mathrm{d}x`. """ if order in gl_storage.keys(): (gl_roots, gl_weights) = gl_storage[order] else: (gl_roots, gl_weights) = np.polynomial.legendre.leggauss(order) jacobian = (b - a) / 2 / split_n s_prime = np.array( [[(x + 1 + 2 * i) * jacobian + a for x in gl_roots] for i in range(split_n)] ) return np.sum(function(s_prime) * gl_weights * jacobian)
[docs] def residue(function : callable, z0 : complex, radius : float = 1e-2, order : int = 100, split_n : int = 3): r""" Residue of a complex function. Parameters ---------- function : callable Complex function of a single variable. z0 : complex Complex point to compute the residue at. radius : float Radius of the circular integral around :math:`z_0`. order : int Quadrature order. split_n : int Number of sections of the circle. Returns ------- r : complex Residue, computed via :math:`r = \frac{1}{2\pi i}\oint_{\mathcal{C}_{z_0}} f(z) \mathrm{d}z`. Examples -------- >>> from dispersionrelations import integrals >>> print(integrals.residue(lambda z: 13/(z-2-2j), z0=2+2j)) np.complex128(13.000000000000007+4.3193350746692004e-15j) """ function_θ = ( lambda θ: 1 / (2 * np.pi) * function(z0 + radius * np.exp(1j * θ)) * radius * np.exp(1j * θ) ) return integrate_gl(function_θ, 0, 2*np.pi, order, split_n)
[docs] class SimplePoleMonomial: r""" Cauchy integral of a shifted monomial Parameters ---------- n : float Power of the shifted monomial (can be an integer or a half-integer). Raises ------ NotImplementedError Any input :code:`n` other than an integer or a half-integer triggers an error. """ def __init__(self, n : float): self.n = n if n % 1 == 0: if n == 0: self.indefinite_integral = self.__indefinite_integral_00__ elif n > 0: self.indefinite_integral = self.__indefinite_integral_ZP__ elif n < 0: self.indefinite_integral = self.__indefinite_integral_ZM__ else: raise NotImplementedError( f"Integral assignment failed for n={self.n}. Only integers and half-integers are supported." ) elif n % 1 == 1 / 2: if n > 0: self.indefinite_integral = self.__indefinite_integral_ZP_HALF__ elif n < 0: self.indefinite_integral = self.__indefinite_integral_ZM_HALF__ else: raise NotImplementedError( f"Integral assignment failed for n={self.n}. Only integers and half-integers are supported." ) else: raise NotImplementedError( f"Integral assignment failed for n={self.n}. Only integers and half-integers are supported." ) def __definite_integral__(self, s, s0, a, b): return self.indefinite_integral(s, s0, b) - self.indefinite_integral(s, s0, a)
[docs] def __call__(self, s, s0, a, b): r""" Definite integral .. math:: \mathcal{I}_{n}(s,s_0;a,b) = \int_a^b \frac{(x-s_0)^n \, \mathrm{d}x}{x-s}. Parameters ---------- s : float Location of the Cauchy singularity. s0 : float Shift of the monomial in the integrand. a : float Lower integration boundary. b : float Upper integration boundary. Notes ----- Several special cases are defined separately, Case 1. :math:`n = 0`, .. math:: \mathcal{I}_{0}(s, s_0; a, b) = \log(x-s) \bigg|_a^b \, . Case 2. :math:`n` is a positive integer, .. math:: \mathcal{I}_{n \in \mathbb{Z}^+}(s, s_0; a, b) = (s-s_0)^n \left( \mathcal{I}_{0}(s, s_0; a, b) + \sum_{k=1}^{n} \frac{1}{k}\frac{(x-s_0)^{k}}{(s-s_0)^{k}} \bigg|_a^b \right) \, . Case 3. :math:`n = -\bar{n}` is a negative integer, .. math:: \mathcal{I}_{-\bar{n}}(s, s_0; a, b) = (s-s_0)^{-\bar{n}} \left( \log\left(\frac{x-s}{x-s_0}\right) + \sum_{k=1}^{\bar{n}-1} \frac{1}{k} \frac{(s-s_0)^{k}}{(x-s_0)^{k}} \right)\bigg|_{a}^{b} \, . Case 4. :math:`n=\frac{m}{2}` is a positive half-integer, .. math:: \mathcal{I}_{m/2}(s, s_0; a, b) = \sqrt{s-s_0}^{m} \left( \log\left(\frac{\sqrt{s-s_0}-\sqrt{x-s_0}}{\sqrt{s-s_0}+\sqrt{x-s_0}}\right) + \sum_{k=1}^{(m+1)/2} \frac{2}{2k-1} \frac{\sqrt{x-s_0}^{2k-1}}{\sqrt{s-s_0}^{2k-1}} \right)\bigg|_a^b \, . Case 5. :math:`n=-\frac{\bar{m}}{2}` is a negative half-integer, .. math:: \mathcal{I}_{-\bar{m}/2}(s, s_0; a, b) \frac{1}{\sqrt{s-s_0}^{\bar{m}}} \left( \log\left(\frac{\sqrt{s-s_0}-\sqrt{x-s_0}}{\sqrt{s-s_0}+\sqrt{x-s_0}}\right) + \sum_{k=1}^{(\bar{m}-1)/2} \frac{2}{2k-1} \frac{\sqrt{s-s_0}^{2k-1}}{\sqrt{x-s_0}^{2k-1}} \right)\bigg|_a^b \, . """ return self.__definite_integral__(s, s0, a, b)
def __indefinite_integral_00__(self, s, s0, x): """Special case: n=0.""" return logC(x - s) def __indefinite_integral_ZP__(self, s, s0, x): """n is a positive integer.""" s_shifted = s - s0 x_shifted = x - s0 w = s_shifted / x_shifted result = np.log(x - s) for k in range(1, self.n + 1): result += 1 / k * (1 / w) ** k return s_shifted**self.n * result def __indefinite_integral_ZM__(self, s, s0, x): """n is a negative integer.""" s_shifted = s - s0 x_shifted = x - s0 w = s_shifted / x_shifted # Special case: if self.n == -1: return 1 / s_shifted * logC(1 - w) # The rest: n_abs = -self.n result = logC(1 - w) for k in range(1, n_abs): result += 1 / k * w**k return s_shifted**self.n * result def __indefinite_integral_ZP_HALF__(self, s, s0, x): """n is a positive half-integer.""" s_shifted = s - s0 x_shifted = x - s0 w = s_shifted / x_shifted w_sqrt = np.sqrt(w + 0j) m = 2 * self.n result = logC((w_sqrt - 1) / (w_sqrt + 1)) for k in range(1, (m + 1) // 2 + 1): result += (2 / (2 * k - 1)) * (1 / w_sqrt) ** (2 * k - 1) return s_shifted**self.n * result def __indefinite_integral_ZM_HALF__(self, s, s0, x): """n is a negative half-integer.""" s_shifted = s - s0 x_shifted = x - s0 w = s_shifted / x_shifted w_sqrt = np.sqrt(w + 0j) m = 2 * self.n result = logC((w_sqrt - 1) / (w_sqrt + 1)) for k in range(1, (m - 1) // 2 + 1): result += (2 / (2 * k - 1)) * w_sqrt ** (2 * k - 1) return s_shifted**self.n * result
[docs] class DispersionIntegralRHC: r""" Dispersion integral along the right-hand cut. Parameters ---------- integrand : callable Integrand along the RHC. threshold : float Lower boundary of the integral. infinity : float, optional Upper boundary of the integral in the units of :code:`threshold`. integration_split_points : array_like, optional Integration split points in the units of :code:`threshold`. integration_order : int, optional Gauss–Legendre quadrature order. subtraction_point : float, optional Point :math:`s_0` of subtraction. Needs to be below the threshold. subtraction_constants : array_like, optional Array of :math:`f_i` subtraction constants. Defines subtraction level :math:`n`. Notes ----- The integral is defined as .. math:: F(s) = \sum_{i=0}^{n-1}f_i (s-s_0)^i + \frac{(s-s_0)^n}{\pi}\int_{s_{thr}}^{\infty}\frac{f(x) \, dx}{(x-s_0)^n(x-s-i\epsilon)}, where :math:`s_0` is the subtraction point, :math:`f_i` are the subtraction constants, :math:`n` is the subtraction level, and :math:`f` is the `integrand`. """ def __init__( self, integrand : callable, threshold : float, infinity : float = 1e6, integration_split_points : list[float] = [2, 5, 10, 50, 100], integration_order : int = 100, subtraction_point : float = 0, subtraction_constants : list[float] = [0], ): self.integrand = integrand self.threshold = threshold self.infinity = infinity self.interval_split = threshold * np.array( [1, *integration_split_points, infinity] ) self.order = integration_order self.s0 = subtraction_point if self.s0 >= self.threshold: warnings.warn(f"Subtraction point s0={self.s0} needs to be below the threshold={self.threshold}.") self.f0_arr = subtraction_constants self.n_subtr = len(subtraction_constants) self.analytic_remainder = SimplePoleMonomial(-self.n_subtr) self.__precompute__() # self.compute = np.vectorize(self.__compute__) def __precompute__(self): (self.gl_roots, self.gl_weights) = np.polynomial.legendre.leggauss(self.order) intervals = [] scalings = [] s_prime = [] for i in range(len(self.interval_split) - 1): a = self.interval_split[i] b = self.interval_split[i + 1] intervals.append([a, b]) scalings.append((b - a) / 2) s_prime.append( np.array([((x + 1) / 2) * (b - a) + a for x in self.gl_roots]) ) self.intervals = np.array(intervals) self.scalings = np.array(scalings) self.scaled_weights = self.gl_weights[:, None] * self.scalings[None, :] self.s_prime = np.array(s_prime).T self.f_at_s_prime = self.integrand(self.s_prime) def __compute_real__(self, s): s = np.array(s) dim_s = len(s.shape) s_axes = tuple(np.arange(dim_s)) gl_axes = tuple(dim_s + np.arange(2)) scaled_weights = np.expand_dims(self.scaled_weights, s_axes) __s__ = np.expand_dims(s, gl_axes) __s_prime__ = np.expand_dims(self.s_prime, s_axes) __f_at_s_prime__ = np.expand_dims(self.f_at_s_prime, s_axes) f_at_s = self.integrand(s) __f_at_s__ = np.expand_dims(f_at_s, gl_axes) integrand = ( (__s__ - self.s0) ** self.n_subtr * (__f_at_s_prime__ - __f_at_s__) / ((__s_prime__ - self.s0) ** self.n_subtr * (__s_prime__ - __s__)) ) numerical_integral = np.zeros(s.shape, dtype=np.complex128) analytic_integral = np.zeros(s.shape, dtype=np.complex128) numerical_integral += np.sum(integrand * scaled_weights, gl_axes) analytic_integral += ( f_at_s * (s - self.s0) ** self.n_subtr * np.where( np.imag(s) == 0, self.analytic_remainder(s, self.s0, self.threshold, np.inf), 0, ) ) polynomial_head = 0.0 for i in range(self.n_subtr): polynomial_head += self.f0_arr[i] * (s - self.s0) ** i return polynomial_head + (1 / np.pi) * (numerical_integral + analytic_integral) def __compute_complex__(self, s): s = np.array(s) dim_s = len(s.shape) s_axes = tuple(np.arange(dim_s)) gl_axes = tuple(dim_s + np.arange(2)) scaled_weights = np.expand_dims(self.scaled_weights, s_axes) __s__ = np.expand_dims(s, gl_axes) __s_prime__ = np.expand_dims(self.s_prime, s_axes) __f_at_s_prime__ = np.expand_dims(self.f_at_s_prime, s_axes) integrand = ( (__s__ - self.s0) ** self.n_subtr * (__f_at_s_prime__) / ((__s_prime__ - self.s0) ** self.n_subtr * (__s_prime__ - __s__)) ) numerical_integral = np.zeros(s.shape, dtype=np.complex128) numerical_integral += np.sum(integrand * scaled_weights, gl_axes) polynomial_head = 0.0 for i in range(self.n_subtr): polynomial_head += self.f0_arr[i] * (s - self.s0) ** i return polynomial_head + (1 / np.pi) * numerical_integral
[docs] def __call__(self, s): r""" Parameters ---------- s : array_like Four-momentum squared of the propagating particle. Returns ------- F : array_like The same shape as input `s`. """ s = np.array(s) is_complex = np.imag(s) != 0 result = np.zeros(s.shape, dtype=np.complex128) if np.sum(is_complex) > 0: result[is_complex] = self.__compute_complex__(s[is_complex]) if np.sum(~is_complex) > 0: result[~is_complex] = self.__compute_real__(np.real(s[~is_complex])) return result
[docs] class OmnesFunction(DispersionIntegralRHC): r""" Omnès–Muskhelishvili solution :cite:`Omnes:1958hv`. Parameters ---------- integrand : callable Integrand along the RHC. threshold : float Lower boundary of the integral. infinity : float, optional Upper boundary of the integral in the units of :code:`threshold`. integration_split_points : array_like, optional Integration split points in the units of :code:`threshold`. integration_order : int, optional Gauss–Legendre quadrature order. subtraction_point : float, optional Point :math:`s_0` of subtraction. subtraction_constants : array_like, optional Array of :math:`f_i` subtraction constants. Defines subtraction level :math:`n`. Notes ----- The function is defined as .. math:: \Omega(s) = \exp\left(\sum_{i=0}^{n-1}f_i (s-s_0)^i + \frac{(s-s_0)^n}{\pi}\int_{s_{thr}}^{\infty}\frac{\delta(x) \, dx}{(x-s_0)^n(x-s-i\epsilon)}\right), where :math:`s_0` is the subtraction point, :math:`f_i` are the subtraction constants for :math:`\log\Omega`, :math:`n` is the subtraction level, and :math:`\delta` is the input phase, passed as `integrand`. See Also -------- dispersionrelations.integrals.DispersionIntegralRHC """
[docs] def __call__(self, s): r""" Parameters ---------- s : array_like Four-momentum squared of the propagating particle. Returns ------- Ω : array_like The same shape as input `s`. """ return np.exp(super().__call__(s))