Source code for dispersionrelations.dynamics

from abc import ABC, abstractmethod
import warnings
from typing import Union, Optional  # for python<=3.9

import numpy as np
from scipy.interpolate import interp1d

from dispersionrelations.constants import GeV, alpha_fs, E_almost_zero
from dispersionrelations.kinematics import phase_space_twobody
from dispersionrelations.integrals import DispersionIntegralRHC, integrate_gl


[docs] def sR(M_R, G_R): r""" Resonance pole location calculated from its mass and width. Parameters ---------- M_R : float Mass of the particle. G_R : float Width of the particle. Returns ------- sR : complex The complex pole location. Notes ----- The pole location is calculated using :math:`s_R(M_R, \Gamma_R) = (M_R - i \Gamma_R / 2)^2`. """ return (M_R - 1j * G_R / 2) ** 2
[docs] def vertex_VPP__2(s, mP, mP_2=None): r"""A vector :math:`\to` pseudoscalar–pseudoscalar vertex squared. Parameters ---------- s : array_like Kinematic variable (can be complex). mP : float Mass of the pseudoscalar with momentum :math:`p_1`. mP_2 : float Mass of the pseudoscalar with momentum :math:`p_2` (if not passed, a case with equal masses will be returned). Returns ------- b : array_like The same shape as input `s`. Notes ----- The vertex :math:`V(q)\to P(p_1)P(p_2)` is given by .. math:: \hat{\beta}_{VPP}^{\mu}(q,p_1,p_2) \propto (p_1 - p_2)^{\mu}. Spin-averaged vertex function .. math:: \beta_{VPP}^2(q^2, p_1^2, p_2^2) = \frac{\lambda(q^2, p_1^2, p_2^2)}{q^2} . The function returns :math:`\beta_{VPP}^2(s, m_{P1}^2, m_{P2}^2)/3`, where :math:`3=(2l+1)` corresponds to the P-wave :math:`(l=1)`. """ if mP_2 is None: return 1 / 3 * (s - 4 * mP**2) return 1 / 3 * (s - (mP + mP_2) ** 2) * (s - (mP - mP_2) ** 2) / s
[docs] def vertex_VPP(s, mP, mP_2=None): r"""A vector :math:`\to` pseudoscalar–pseudoscalar vertex. Parameters ---------- s : array_like Kinematic variable (can be complex). mP : float Mass of the pseudoscalar with momentum :math:`p_1`. mP_2 : float Mass of the pseudoscalar with momentum :math:`p_2` (if not passed, a case with equal masses will be returned). Returns ------- b : array_like The same shape as input `s`. Notes ----- This returns the square root of :func:`vertex_VPP__2`, i.e. :math:`\frac{1}{\sqrt{3}}\beta_{VPP}(s, m_{P1}^2, m_{P2}^2)`. For most applications we need the squared version. """ return np.sqrt(vertex_VPP__2(s, mP, mP_2) + 0j)
[docs] def vertex_VVP__2(s, mV, mP): r"""A vector :math:`\to` vector–pseudoscalar vertex squared. Parameters ---------- s : array_like Kinematic variable (can be complex). mV : float Mass of the vector with momentum :math:`k`. mP : float Mass of the pseudoscalar with momentum :math:`p`. Returns ------- b : array_like The same shape as input `s`. Notes ----- The vertex :math:`V(q)\to V(k)P(p)` is given by .. math:: \hat{\beta}_{VVP}^{(\lambda)\mu}(q, k, p) = \epsilon^{\mu\nu\alpha\beta} n^{(\lambda)}_\nu p_\alpha q_\beta . Spin-averaged vertex function .. math:: \beta_{VVP}^2(q^2, k^2, p^2) = \frac{\lambda(q^2, k^2, p^2)}{2} . The function returns :math:`\beta_{VVP}^2(s, m_V^2, m_P^2)/3`, where :math:`3=(2l+1)` corresponds to the P-wave :math:`(l=1)`. """ return 1 / 6 * (s - (mV - mP) ** 2) * (s - (mV + mP) ** 2)
[docs] def vertex_VVP(s, mV, mP): r"""A vector :math:`\to` vector–pseudoscalar vertex. Parameters ---------- s : array_like Kinematic variable (can be complex). mV : float Mass of the vector with momentum :math:`k`. mP : float Mass of the pseudoscalar with momentum :math:`p`. Returns ------- b : array_like The same shape as input `s`. Notes ----- This returns the square root of :func:`vertex_VVP__2`, i.e. :math:`\frac{1}{\sqrt{3}}\beta_{VVP}(s, m_V^2, m_P^2)`. For most applications we need the squared version. """ return np.sqrt(vertex_VVP__2(s, mV, mP) + 0j)
[docs] def vertex_AVP__2(s, mV, mP): r"""An axial vector :math:`\to` vector–pseudoscalar vertex squared. Parameters ---------- s : array_like Kinematic variable (can be complex). mV : float Mass of the vector with momentum :math:`k`. mP : float Mass of the pseudoscalar with momentum :math:`p`. Returns ------- b : array_like The same shape as input `s`. Notes ----- The vertex :math:`A(q)\to V(k)P(p)` is given by .. math:: \hat{\beta}_{AVP}^{(\lambda)\mu}(q, k, p) = (q \cdot k) \, n^{(\lambda)\mu} - (n^{(\lambda)} \cdot q) \, k^\mu . Spin-averaged vertex function .. math:: \beta_{AVP}^2(q^2, k^2, p^2) = 3q^2k^2 + \frac{\lambda(q^2, k^2, p^2)}{2} . The function returns :math:`\beta_{AVP}^2(s, m_V^2, m_P^2)/3`. """ return 1 / 6 * (2 * s * mV**2 + (s + mV**2 - mP**2) ** 2)
[docs] def vertex_AVP(s, mV, mP): r"""An axial vector :math:`\to` vector–pseudoscalar vertex. Parameters ---------- s : array_like Kinematic variable (can be complex). mV : float Mass of the vector with momentum :math:`k`. mP : float Mass of the pseudoscalar with momentum :math:`p`. Returns ------- b : array_like The same shape as input `s`. Notes ----- This returns the square root of :func:`vertex_AVP__2`, i.e. :math:`\frac{1}{\sqrt{3}}\beta_{AVP}(s, m_V^2, m_P^2)`. For most applications we need the squared version. """ return np.sqrt(vertex_AVP__2(s, mV, mP) + 0j)
[docs] def vertex_VAP__2(s, mA, mP): r"""A vector :math:`\to` axial vector–pseudoscalar vertex squared. Parameters ---------- s : array_like Kinematic variable (can be complex). mV : float Mass of the axial vector with momentum :math:`k`. mP : float Mass of the pseudoscalar with momentum :math:`p`. Returns ------- b : array_like The same shape as input `s`. Notes ----- The vertex :math:`V(q)\to A(k)P(p)` is given by .. math:: \hat{\beta}_{VAP}^{(\lambda)\mu}(q, k, p) = (q \cdot k) \, n^{(\lambda)\mu} - (n^{(\lambda)} \cdot q) \, k^\mu . Spin-averaged vertex function .. math:: \beta_{VAP}^2(q^2, k^2, p^2) = 3q^2k^2 + \frac{\lambda(q^2, k^2, p^2)}{2} . The function returns :math:`\beta_{VAP}^2(s, m_A^2, m_P^2)/3`. """ return 1 / 6 * (2 * s * mA**2 + (s + mA**2 - mP**2) ** 2)
[docs] def vertex_VAP(s, mA, mP): r"""A vector :math:`\to` axial vector–pseudoscalar vertex. Parameters ---------- s : array_like Kinematic variable (can be complex). mV : float Mass of the axial vector with momentum :math:`k`. mP : float Mass of the pseudoscalar with momentum :math:`p`. Returns ------- b : array_like The same shape as input `s`. Notes ----- This returns the square root of :func:`vertex_VAP__2`, i.e. :math:`\frac{1}{\sqrt{3}}\beta_{VAP}(s, m_A^2, m_P^2)`. For most applications we need the squared version. """ return np.sqrt(vertex_VAP__2(s, mA, mP) + 0j)
[docs] def vertex_VAP_T1__2(s, mA, mP): r"""A vector :math:`\to` axial vector–pseudoscalar vertex squared. Parameters ---------- s : array_like Kinematic variable (can be complex). mV : float Mass of the axial vector with momentum :math:`k`. mP : float Mass of the pseudoscalar with momentum :math:`p`. Returns ------- b : array_like The same shape as input `s`. """ return s / 3
[docs] def vertex_VAP_T2__2(s, mA, mP): r"""A vector :math:`\to` axial vector–pseudoscalar vertex squared. Parameters ---------- s : array_like Kinematic variable (can be complex). mV : float Mass of the axial vector with momentum :math:`k`. mP : float Mass of the pseudoscalar with momentum :math:`p`. Returns ------- b : array_like The same shape as input `s`. """ return 1 / 6 + s * 0
[docs] def taming_BlattWeisskopf( s, sthr: float, sB: float, l: int = 1, normalize_at_sB: bool = False ): r""" Blatt–Weisskopf taming factors. Parameters ---------- s : array_like Kinematic variable (can be complex). sthr : float Threshold of the relevant channel. sB : float Range parameter. l : int Partial wave. normalize_at_sB : boolean If set to `True`, :math:`B(s_B) = 1`. Returns ------- B : array_like The same shape as input `s`. Raises ------ NotImplementedError The function is only implemented for the partial waves :math:`l\in\{0,1,2,3,4\}`. Any other input triggers an error. Notes ----- The first few Blatt–Weisskopf taming factors are given by :cite:`Chung:1995dx` .. math:: \begin{align} B_0(s) &= 1, \\ B_1(s) &= 1 / (x + 1), \\ B_2(s) &= 1 / ((x-3)^2 + 9x), \\ \end{align} where :math:`x=(s-s_{thr}) / (s_B - s_{thr})`. """ if l == 0: return 1 elif l == 1: x = (s - sthr) / (sB - sthr) numerator = 2 * x if normalize_at_sB else 1 return numerator / (1 + x) elif l == 2: x = (s - sthr) / (sB - sthr) numerator = 13 * x**2 if normalize_at_sB else 1 return numerator / ((x - 3) ** 2 + 9 * x) elif l == 3: x = (s - sthr) / (sB - sthr) numerator = 277 * x**3 if normalize_at_sB else 1 return numerator / (x * (x - 15) ** 2 + 9 * (2 * x - 5)) elif l == 4: x = (s - sthr) / (sB - sthr) numerator = 12746 * x**4 if normalize_at_sB else 1 return numerator / ((x**2 - 45 * x + 105) ** 2 + 25 * x * (2 * x - 21) ** 2) else: raise NotImplementedError( f"Blatt-Weisskopf factor for l={l} has not been implemented." )
[docs] def taming_spacelike_pole(s, sB: float = (2 * GeV) ** 2, n: int = 1): r""" A simple decaying function, used as a taming factor. Parameters ---------- s : array_like Kinematic variable (can be complex). sB : float Range parameter. n : int Degree of decay. Returns ------- B : array_like The same shape as input `s`. Notes ----- The function is defined as .. math:: B_n(s) = \left( \frac{s_B}{s_B + s} \right)^n . This creates a space-like pole (of degree `n`) at :math:`s = -s_B`. """ return (sB / (sB + s)) ** n
[docs] def BreitWigner(s, mass: float, width: float): r""" Breit–Wigner distribution. Parameters ---------- s : array_like Four-momentum squared of the propagating particle. mass : float Mass of the propagating particle. width : float Width of the propagating particle. Returns ------- G : array_like The same shape as input `s`. Notes ----- The distribution is defined as :cite:`Breit:1936zzb` .. math:: G(s, M, \Gamma) = \frac{1}{s - M^2 + i M \Gamma}, where :math:`M` and :math:`\Gamma` are the mass and the width of the propagating particle, correspondingly. """ return 1 / (s - mass**2 + 1j * mass * width)
[docs] def BreitWignerED(s, mass: float, width_s: callable, width_0: float): r""" Breit–Wigner distribution. Parameters ---------- s : array_like Four-momentum squared of the propagating particle. mass : float Mass of the propagating particle. width_s : callable Energy-dependent width of the propagating particle, must be a function of `s`, `mass`, and `width_0`. width_0 : float A parameter that enters the definition of the energy-dependent width. Returns ------- G : array_like The same shape as input `s`. Notes ----- The distribution is defined as .. math:: G(s, M, \Gamma_s, \Gamma_0) = \frac{1}{s - M^2 + i M \Gamma_s(s, M, \Gamma_0)}, where :math:`M` and :math:`\Gamma_s` are the mass and the width of the propagating particle, correspondingly. The index :math:`s` indicates that the width need not be constant and can depend on energy. See Also -------- dispersionrelations.kinematics.BreitWigner """ return 1 / (s - mass**2 + 1j * mass * width_s(s, mass, width_0))
[docs] def radiative_width_to_normalisation_squared(width, mV, mP): r"""Computes the vector–pseudoscalar form factor normalisation squared from the radiative width of the vector particle. Parameters ---------- width : float Radiative width of the vector particle. mV : float Mass of the vector particle. mP : float Mass of the pseudoscalar particle. Returns ------- c_squared : float Normalisation squared :math:`|f(0)|^2`. Notes ----- The radiative width :math:`\Gamma` is connected to the form factor normalisation :math:`|f(0)|` via .. math:: \Gamma_{V \rightarrow P\gamma} = \frac{\alpha(M_V^2 - M_P^2)^3}{24 M_V^3} |f_{VP}(0)|^2. """ return width / ((alpha_fs / 24) * ((mV**2 - mP**2) / mV) ** 3)
[docs] def radiative_width_to_normalisation_squared_error( width, mV, mP, width_std, mV_std, mP_std ): r"""Computes the error of the vector–pseudoscalar form factor normalisation squared from the radiative width of the vector particle. Parameters ---------- width, width_std : float Radiative width of the vector particle (and the corresponding error). mV, mV_std : float Mass of the vector particle (and the corresponding error). mP, mP_std : float Mass of the pseudoscalar particle (and the corresponding error). Returns ------- c_squared_std : float Error of the normalisation squared :math:`|f(0)|^2`. Notes ----- The radiative width :math:`\Gamma` is connected to the form factor normalisation :math:`|f(0)|` via .. math:: \Gamma_{V \rightarrow P\gamma} = \frac{\alpha(M_V^2 - M_P^2)^3}{24 M_V^3} |f_{VP}(0)|^2. """ jacobian_width = 1 / ((alpha_fs / 24) * ((mV**2 - mP**2) / mV) ** 3) jacobian_mV = ( 3 * width * mV**2 * (mP**2 + mV**2) / (alpha_fs * (mV**2 - mP**2) ** 4) ) jacobian_mP = 6 * width * mP * mV**3 / (alpha_fs * (mV**2 - mP**2) ** 4) return np.sqrt( (jacobian_width * width_std) ** 2 + (jacobian_mV * mV_std) ** 2 + (jacobian_mP * mP_std) ** 2 )
[docs] class Channel(DispersionIntegralRHC, ABC): r""" Dispersive representation of a channel. Parameters ---------- loop_integrand : callable Imaginary part of the self-energy. 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 integral is defined as .. math:: \Pi(s) = \sum_{i=0}^{n-1}f_i (s-s_0)^i + \frac{(s-s_0)^n}{\pi}\int_{s_{thr}}^{\infty}\frac{\mathrm{Im}(\Pi(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:`\mathrm{Im}(\Pi(x))` is the `integrand`. See also -------- dispersionrelations.dynamics.StableTwoBodyChannel """ def __init__( self, loop_integrand: callable, threshold: float, infinity: float = 1e6, integration_split_points: list[float] = [ 2, 3, 5, 8, 10, 50, 1e2, 1e3, 1e4, 1e5, ], integration_order: int = 100, subtraction_point: float = 0, subtraction_constants: list[float] = [0], ): super().__init__( loop_integrand, threshold, infinity, integration_split_points, integration_order, subtraction_point, subtraction_constants, )
[docs] def loop_discontinuity(self, s): r""" Discontinuity of the two-body channel self-energy. Parameters ---------- s : array_like Four-momentum squared of the two-body state. Returns ------- DiscΠ : array_like The same shape as input `s`. Notes ----- We assume the Schwarz reflection principle and therefore, .. math:: \mathrm{Disc}(\Pi(s)) = 2i \, \mathrm{Im}(\Pi(s)) \, . """ result_up = np.where(np.imag(s) >= 0, 2j * self.integrand(s), 0) result_down = np.where( np.imag(s) < 0, np.conj(2j * self.integrand(np.conj(s))), 0 ) return result_up + result_down
[docs] def loop(self, s, sheet=1): r""" Invokes the :code:`__call__` method of the parent class, with an added functionality of switching to the second sheet. Computed the self-energy function of the two-body channel. Parameters ---------- s : array_like Four-momentum squared of the two-body state. sheet : int, optional Riemann sheet number (can only be 1 or 2, other inputs return the first sheet). Returns ------- Π : array_like The same shape as input `s`. """ result = self.__call__(s) if sheet == 2: result -= self.loop_discontinuity(s) return result
[docs] @abstractmethod def phase_space_function(self, s): return None
[docs] @abstractmethod def vertex_function_squared(self, s): return None
[docs] @abstractmethod def taming_factor_squared(self, s): return None
[docs] def vertex_function(self, s): return np.sqrt(self.vertex_function_squared(s) + 0j)
[docs] def taming_factor(self, s): return np.sqrt(self.taming_factor_squared(s) + 0j)
[docs] class PrecomputedChannel(Channel): def __init__(self, s_values, integrand_values, threshold, **kwargs): self.last_s = s_values[-1] self.last_integrand = integrand_values[-1] self.interpolated_integrand = interp1d(s_values, integrand_values) warnings.warn( "You are loading this channel from precomputed values. Full discontinuity will be absorbed in the phase space function." ) super().__init__(self.extrapolated_integrand, threshold, **kwargs)
[docs] def extrapolated_integrand(self, s): s = np.asarray(s) res = np.zeros_like(s) mask1 = s < self.threshold mask2 = s > self.last_s mask3 = ~(mask1 | mask2) res[mask1] = 0 res[mask2] = self.last_integrand res[mask3] = self.interpolated_integrand(s[mask3]) return res
[docs] def phase_space_function(self, s): return self.integrand(s)
[docs] def vertex_function_squared(self, s): return 0 * s + 1
[docs] def taming_factor_squared(self, s): return 0 * s + 1
[docs] class StableTwoBodyChannel(Channel): r""" Constructs a two-body channel with stable particles with masses :code:`m1` and :code:`m2`. Parameters ---------- m1 : float Mass of the first particle. m2 : float Mass of the second particle. vertex_function_squared : callable Vertex function squared. Depends on the interaction of the particles. taming_factor : callable Taming factor, to make sure that the integrand does not grow indefinitely. **kwargs Other keyword arguments, to be passed to the constructor of the parent class. Notes ----- The integrand of the loop is constructed via .. math:: \mathrm{Disc}(\Pi(s)) = 2i \, \rho(s, m_1^2, m_2^2) \beta^2(s, m_1^2, m_2^2) B^2(s) \, , where :math:`\rho` is the phase space, :math:`\beta` is the vertex function, and :math:`B` is the taming factor. """ def __init__( self, m1, m2, vertex_function_squared=vertex_VPP__2, taming_factor_squared=lambda s: taming_spacelike_pole(s) ** 2, **kwargs, ): # Store parameters: self.m1 = m1 self.m2 = m2 self.threshold = (m1 + m2) ** 2 # Implement abstract methods: self.__psf__ = np.vectorize(lambda s: phase_space_twobody(s, m1, m2)) self.__vfs__ = np.vectorize(lambda s: vertex_function_squared(s, m1, m2)) self.__tfs__ = np.vectorize(lambda s: taming_factor_squared(s)) # Construct the integrand and call the parent constructor: loop_integrand = ( lambda s: self.phase_space_function(s) * self.vertex_function_squared(s) * self.taming_factor_squared(s) ) super().__init__(loop_integrand, self.threshold, **kwargs)
[docs] def phase_space_function(self, s): return self.__psf__(s)
[docs] def vertex_function_squared(self, s): return self.__vfs__(s)
[docs] def taming_factor_squared(self, s): return self.__tfs__(s)
[docs] class UnstableParticle: r""" Unstable particle. Parameters ---------- decay_channel : TwoBodyChannel A channel that the particle decays into. The channel does not need to be two-body, as long as it has a :code:`threshold` and functions :code:`integrand` and :code:`loop`. pole_location : complex Location of the resonance pole (see :func:`sR`). pole_sheet : int Riemann sheet of the decay channel, on which the resonance lives. For above-threshold resonances, the typical sheet is `2`. mass_and_coupling : (float, float) Bare mass and the coupling constant, parameters of the propagator. If passed, :code:`pole_location` is ignored. interpolation_points : list Intervals to use for interpolating the spectral function. interpolation_density : int Number of points per interpolation interval. """ def __init__( self, decay_channel: Channel, pole_location: complex, pole_sheet: int = 2, mass_and_coupling: Optional[tuple] = None, interpolation_points=[1, 2, 5, 1e1, 5e1, 1e2, 1e3, 1e5, 1e8], interpolation_density=1000, ): self.decay_channel = decay_channel self.sp = pole_location self.pole_sheet = pole_sheet self.mass_and_coupling = mass_and_coupling self.__calculate_m_g__() self.interpolation_points = interpolation_points self.interpolation_density = interpolation_density self.__interpolate_spectral_function__() def __calculate_m_g__(self): if self.mass_and_coupling is not None: self.m, self.g = self.mass_and_coupling return # The code below is the only part where self.decay_channel.loop should be known for complex numbers. # If mass_and_coupling is provided, real-valued loop is sufficient. loop_at_the_pole = self.decay_channel.loop(self.sp, sheet=self.pole_sheet) self.g = np.sqrt(-np.imag(self.sp) / np.imag(loop_at_the_pole)) self.m = np.sqrt(np.real(self.sp) + self.g**2 * np.real(loop_at_the_pole))
[docs] def propagator(self, s, sheet=1): r""" Propagator of the unstable particle. Parameters ---------- s : array_like Four-momentum squared of the propagating particle. sheet : int, optional Riemann sheet number of the decay channel self-energy (can only be 1 or 2, other inputs return the first sheet). Returns ------- G : array_like The same shape as input `s`. Notes ----- The propagator is defined as .. math:: G(s) = \frac{1}{s - m^2 + g^2 \Pi(s)} \, , where :math:`\Pi(s)` is the self-energy of the decay channel. """ return 1 / (s - self.m**2 + self.g**2 * self.decay_channel.loop(s, sheet))
[docs] def spectral_function_CP(self, s): r""" Spectral function of the unstable particle for :math:`s\in\mathbb{C}`. Parameters ---------- s : array_like Four-momentum squared of the propagating particle. Returns ------- σ : array_like The same shape as input `s`. Notes ----- The spectral function is defined as .. math:: \sigma(s) = \frac{1}{\pi} G^{I}(s) G^{II}(s) \, g^2 \mathrm{Im}(\Pi(s)) \, , which, for real :math:`s` reduces to .. math:: \sigma(s\in\mathbb{R}) = -\frac{1}{\pi} \mathrm{Im}(G(s)) \, . """ return ( 1 / np.pi * self.propagator(s, sheet=1) * self.propagator(s, sheet=2) * self.g**2 * self.decay_channel.integrand(s) )
def __interpolate_spectral_function__(self): s_interpolation = np.concatenate( [ self.decay_channel.threshold * np.linspace( 1.0001 * self.interpolation_points[i], self.interpolation_points[i + 1], self.interpolation_density, ) for i in range(len(self.interpolation_points) - 1) ] ) sf_interpolation = np.real(self.spectral_function_CP(s_interpolation)) self.__spectral_function_interpolated__ = interp1d( [self.decay_channel.threshold, *s_interpolation], [0, *sf_interpolation] )
[docs] def spectral_function_RE(self, s): r""" Spectral function of the unstable particle for :math:`s\in\mathbb{R}`. Parameters ---------- s : array_like Four-momentum squared of the propagating particle. Returns ------- σ : array_like The same shape as input `s`. Notes ----- The spectral function is defined as .. math:: \sigma(s) = -\frac{1}{\pi} \mathrm{Im}(G(s)) \, . An interpolation function is used when possible. """ below_threshold = np.real(s) < self.decay_channel.threshold above_interpolation_maximum = ( np.real(s) > self.interpolation_points[-1] * self.decay_channel.threshold ) interpolatable = ~below_threshold & ~above_interpolation_maximum result = np.zeros_like(s, dtype=float) if np.sum(interpolatable) > 0: result[interpolatable] = self.__spectral_function_interpolated__( s[interpolatable] ) return result
[docs] class SemiStableTwoBodyCut: r""" Two-body channel with one unstable particle and one spectator. Parameters ---------- unstable_particle : UnstableParticle Unstable particle. spectator_mass : float Mass of the spectator particle. vertex_function : callable Vertex function. Depends on the interaction of the particles. phase_space : callable Phase space function. taming_factor : callable Taming factor, to make sure that the integrand does not grow indefinitely. gl_order : int Gauss–Legendre quadrature order. split_n : int Number of intervals per numerical integration. interpolation_points : list Intervals to use for interpolating the spectral function. interpolation_density: int Number of points per interpolation interval. """ def __init__( self, unstable_particle: UnstableParticle, spectator_mass: float, vertex_function_squared: callable, gl_order: int = 100, split_n: int = 3, integration_split_points: list[float] = [2, 5, 10, 50, 100], interpolation_points: list = [ 1, 2, 3, 4, 5, 1e1, 3e1, 5e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, ], interpolation_density: int = 1000, ): self.unstable_particle = unstable_particle self.spectator_mass = spectator_mass self.phase_space_factor = phase_space_twobody self.vertex_function_squared = vertex_function_squared self.gl_order = gl_order self.split_n = split_n self.integration_split_points = ( self.unstable_particle.decay_channel.threshold * np.array(integration_split_points) ) self.interpolation_points = interpolation_points self.interpolation_density = interpolation_density self.__interpolate_ImPI_integral__()
[docs] def ImPI_integrand_RE(self, s, x): r""" Integrand for the :math:`\mathrm{Im}(\Pi(s))` spectral integral for :math:`x\in\mathbb{R}`. Parameters ---------- s : complex Four-momentum squared of the two-body system. x : array_like Four-momentum squared of the unstable particle. Returns ------- f : array_like The same shape as input `x`. Notes ----- The integrand is defined via .. math:: \mathrm{Im}(\Pi(s)) = \int_{s_\text{thr}}^{(\sqrt{s}-m)^2} \sigma(x) \rho(s, x, m^2) \beta^2(s, x, m^2) B^2(s) \, , where :math:`\sigma` is the spectral function of the unstable particle and :math:`s_\text{thr}` is the threshold of the corresponding decay channel. """ return ( self.unstable_particle.spectral_function_RE(x) * self.phase_space_factor(s, np.sqrt(x + 0j), self.spectator_mass) * self.vertex_function_squared(s, np.sqrt(x + 0j), self.spectator_mass) )
[docs] def ImPI_integrand_CP(self, s, x): r""" Integrand for the :math:`\mathrm{Im}(\Pi(s))` spectral integral for :math:`x\in\mathbb{C}`. Parameters ---------- s : complex Four-momentum squared of the two-body system. x : array_like Four-momentum squared of the unstable particle. Returns ------- f : array_like The same shape as input `x`. Notes ----- The integrand is defined via .. math:: \mathrm{Im}(\Pi(s)) = \int_{s_\text{thr}}^{(\sqrt{s}-m)^2} \sigma(x) \rho(s, x, m^2) \beta^2(s, x, m^2) B^2(s) \, , where :math:`\sigma` is the spectral function of the unstable particle and :math:`s_\text{thr}` is the threshold of the corresponding decay channel. """ return ( self.unstable_particle.spectral_function_CP(x) * self.phase_space_factor(s, np.sqrt(x + 0j), self.spectator_mass) * self.vertex_function_squared(s, np.sqrt(x + 0j), self.spectator_mass) )
[docs] def ImPI_integral(self, s): r""" The :math:`\mathrm{Im}(\Pi(s))` spectral integral. Parameters ---------- s : complex Four-momentum squared of the two-body system. Returns ------- ImΠ : complex A single complex number. Notes ----- The integral is defined via .. math:: \mathrm{Im}(\Pi(s)) = \int_{s_\text{thr}}^{(\sqrt{s}-m)^2} \sigma(x) \rho(s, x, m^2) \beta^2(s, x, m^2) B^2(s) \, , where :math:`\sigma` is the spectral function of the unstable particle and :math:`s_\text{thr}` is the threshold of the corresponding decay channel. For complex values of :math:`s`, the integral contour is taken to be rectangular (see e.g. :cite:`JPAC:2018zwp` for more details). """ lower_bound = self.unstable_particle.decay_channel.threshold upper_bound = (np.sqrt(s) - self.spectator_mass) ** 2 middle_bound = np.real(upper_bound) integrand = lambda x: self.ImPI_integrand_RE(s, x) if np.imag(upper_bound) != 0: integrand = lambda x: self.ImPI_integrand_CP(s, x) integration_points_1 = np.concatenate( ( [lower_bound], self.integration_split_points[ self.integration_split_points < middle_bound ], [middle_bound], ) ) integral_1 = 0 * s + 0j for i in range(len(integration_points_1) - 1): integral_1 += integrate_gl( integrand, integration_points_1[i], integration_points_1[i + 1], order=self.gl_order, split_n=self.split_n, ) # integral_1 = integrate_gl( # integrand, # lower_bound, # middle_bound, # order=self.gl_order, # split_n=self.split_n, # ) if np.imag(upper_bound) == 0: return integral_1 integral_2 = integrate_gl( integrand, lower_bound, middle_bound, order=self.gl_order, split_n=self.split_n, ) return integral_1 + integral_2
@property def ImPI_integral_vectorized(self): r"""Vectorized version of :func:`dispersionrelations.dynamics.SemiStableTwoBodyCut.ImPI_integral`""" return np.vectorize(self.ImPI_integral) def __interpolate_ImPI_integral__(self): s_interpolation = np.concatenate( [ self.unstable_particle.decay_channel.threshold * np.linspace( 1.0001 * self.interpolation_points[i], self.interpolation_points[i + 1], self.interpolation_density, ) for i in range(len(self.interpolation_points) - 1) ] ) ImPI_interpolation = np.array( [np.real(self.ImPI_integral(s)) for s in s_interpolation] ) self.__ImPI_integral_interpolated__ = interp1d( [self.unstable_particle.decay_channel.threshold, *s_interpolation], [0, *ImPI_interpolation], )
[docs] def ImPI_integral_RE(self, s): r""" The :math:`\mathrm{Im}(\Pi(s))` spectral integral for :math:`s\in\mathbb{R}`. Parameters ---------- s : array_like Four-momentum squared of the two-body system. Returns ------- ImΠ : array_like The same shape as input `s`. Notes ----- An interpolation function is used when possible. """ below_threshold = np.real(s) < self.unstable_particle.decay_channel.threshold above_interpolation_maximum = ( np.real(s) > self.interpolation_points[-1] * self.unstable_particle.decay_channel.threshold ) interpolatable = ~below_threshold & ~above_interpolation_maximum result = np.zeros_like(s, dtype=float) if np.sum(interpolatable) > 0: result[interpolatable] = self.__ImPI_integral_interpolated__( s[interpolatable] ) if np.sum(above_interpolation_maximum) > 0: result[above_interpolation_maximum] = self.ImPI_integral_vectorized( s[above_interpolation_maximum] ) return result
[docs] def ImPI_integral_CP(self, s): r""" The :math:`\mathrm{Im}(\Pi(s))` spectral integral for :math:`s\in\mathbb{C}`. Parameters ---------- s : array_like Four-momentum squared of the two-body system. Returns ------- ImΠ : array_like The same shape as input `s`. """ return self.ImPI_integral_vectorized(s)
[docs] def __call__(self, s): r""" The :math:`\mathrm{Im}(\Pi(s))` spectral integral for a generic :math:`s`. Parameters ---------- s : array_like Four-momentum squared of the two-body system. Returns ------- ImΠ : array_like The same shape as input `s`. Notes ----- An interpolation function is used when possible for real values of :math:`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.ImPI_integral_CP(s[is_complex]) if np.sum(~is_complex) > 0: result[~is_complex] = self.ImPI_integral_RE(np.real(s[~is_complex])) return result
[docs] class SemiStableTwoBodyChannel(Channel): def __init__( self, twobodycut: SemiStableTwoBodyCut, taming_factor_squared=lambda s: taming_spacelike_pole(s) ** 2, ): # Store parameters: threshold = ( np.sqrt(twobodycut.unstable_particle.decay_channel.threshold) + twobodycut.spectator_mass ) ** 2 # Implement abstract methods: self.__psf__ = twobodycut.__call__ self.__vfs__ = np.vectorize(lambda s: 1) self.__tfs__ = np.vectorize(taming_factor_squared) # Construct the integrand and call the parent constructor: loop_integrand = ( lambda s: self.phase_space_function(s) * self.vertex_function_squared(s) * self.taming_factor_squared(s) ) super().__init__(loop_integrand, threshold)
[docs] def phase_space_function(self, s): return self.__psf__(s)
[docs] def vertex_function_squared(self, s): return self.__vfs__(s)
[docs] def taming_factor_squared(self, s): return self.__tfs__(s)
[docs] class TwoPotentialModel: def __init__( self, channels: dict[str, Channel], bg_par: dict, res_par: dict, channel_sheets: dict = {}, s_array: Union[np.ndarray, list] = np.linspace(E_almost_zero, 3 * GeV, 200) ** 2, ): # Initialize channels self.channels = channels self.channel_sheets = {key: 1 for key in channels.keys()} for key in channel_sheets.keys(): if key in channels.keys(): self.channel_sheets[key] = channel_sheets[key] else: warnings.warn( f"Channel '{key}' not found in the provided channels dictionary. Ignoring the sheet specification." ) # Initialize parameters self.bg_par = bg_par self.res_par = res_par # Initialize auxilary variables self.nC = len(channels) self.nR = len(res_par["m"]) self.IdC = np.identity(self.nC) self.IdR = np.identity(self.nR) # Initialize kinematic variables self.__set_s_array__(s_array) # Default parameters, in case none are provided self.bg_par = { "f0": np.zeros((self.nC, self.nC)), "fR": np.array([0.0]), } self.res_par = { "g": np.zeros((self.nR, self.nC)), "m": np.arange(self.nR) + 1 * GeV, "c": np.ones(self.nC), "a": np.ones(self.nR), } # Store model parameters from passed dictionaries self.store_parameters(bg_par, res_par) # Computation steps self.precompute() self.compute_bg() self.compute_res() def __set_s_array__(self, s_array): # Index _rs stands for "reshaped" self.s_array = np.array(s_array) self.s_rs = self.s_array[..., np.newaxis, np.newaxis] self.E_array = np.sign(s_array) * np.sqrt(np.absolute(self.s_array)) self.E_rs = self.E_array[..., np.newaxis, np.newaxis]
[docs] def store_parameters(self, new_bg_par=None, new_res_par=None): if new_bg_par is not None: for key in new_bg_par: if key in self.bg_par.keys(): self.bg_par[key] = new_bg_par[key] else: warnings.warn( f"Background parameter '{key}' not recognized. Ignoring." ) self.f0 = np.array(self.bg_par["f0"]) self.fR = np.array(self.bg_par["fR"]) if new_res_par is not None: for key in new_res_par: if key in self.res_par.keys(): self.res_par[key] = new_res_par[key] else: warnings.warn( f"Resonance parameter '{key}' not recognized. Ignoring." ) self.g = np.array(self.res_par["g"]) self.m = np.array(self.res_par["m"]) self.c = np.array(self.res_par["c"]) self.a = np.array(self.res_par["a"])
def __diag_in_channel_space__(self, arr): arr = np.asarray(arr) if arr.shape != (*self.s_array.shape, self.nC): raise ValueError( f"Input array must have shape {(*self.s_array.shape, self.nC)}" ) channel_indexing = np.arange(self.nC) result = np.zeros((*self.s_array.shape, self.nC, self.nC)) + 0j result[..., channel_indexing, channel_indexing] = arr return result def __diag_in_resonance_space__(self, arr): arr = np.asarray(arr) if arr.shape != (*self.s_array.shape, self.nR): raise ValueError( f"Input array must have shape {(*self.s_array.shape, self.nR)}" ) resonance_indexing = np.arange(self.nR) result = np.zeros((*self.s_array.shape, self.nR, self.nR)) + 0j result[..., resonance_indexing, resonance_indexing] = arr return result
[docs] def precompute(self): loops_arr = np.array( [ self.channels[key].loop(self.s_array, sheet=self.channel_sheets[key]) for key in self.channels.keys() ] ) self.loops = self.__diag_in_channel_space__(np.moveaxis(loops_arr, 0, -1))
[docs] def compute_bg(self, new_bg_par=None): if new_bg_par is not None: self.store_parameters(new_bg_par=new_bg_par) self.f_s = ( np.ones((*self.s_array.shape, self.nC, self.nC)) + self.fR * self.s_rs ) self.VB = self.f0 / self.f_s self.TB = np.linalg.solve(self.IdC - self.VB @ self.loops, self.VB) self.vertex = self.IdC + self.TB @ self.loops self.loops_with_bg = self.loops @ self.vertex
[docs] def compute_res(self, new_res_par=None): if new_res_par is not None: self.store_parameters(new_res_par=new_res_par) self.GR_inv = self.__diag_in_resonance_space__( self.s_array[..., np.newaxis] - self.m[np.newaxis, ...] ** 2 ) self.GR_renormalized_inv = self.GR_inv + self.g @ self.loops_with_bg @ self.g.T self.tR = -self.g.T @ np.linalg.solve(self.GR_renormalized_inv, self.g) # TODO: add a warning for failed inversion self.TR = self.vertex @ self.tR @ (np.swapaxes(self.vertex, -2, -1)) self.F = np.einsum( "...ij,...j->...i", -self.vertex @ self.g.T, np.linalg.solve(self.GR_renormalized_inv, self.a), # TODO: add a warning for failed inversion )
[docs] def compute_everything(self, bg_par=None, res_par=None): self.compute_bg(bg_par) self.compute_res(res_par)
[docs] def new_s_array(self, s_array): self.__set_s_array__(s_array) self.precompute() self.compute_bg() self.compute_res()
def __amplitude__(self, amp): vertex_factors = self.__diag_in_channel_space__( np.moveaxis( np.array( [ channel.vertex_function(self.s_array) * channel.taming_factor(self.s_array) for channel in self.channels.values() ] ), 0, -1, ) ) return vertex_factors @ amp @ vertex_factors
[docs] def T_matrix_bg(self): return self.__amplitude__(self.TB)
[docs] def T_matrix_res(self): return self.__amplitude__(self.TR)
[docs] def T_matrix(self): return self.__amplitude__(self.TB + self.TR)
[docs] def S_matrix(self): phase_space_factors = self.__diag_in_channel_space__( np.moveaxis( np.array( [ channel.phase_space_function(self.s_array) for channel in self.channels.values() ] ), 0, -1, ) ) return self.IdC + 2j * phase_space_factors * self.T_matrix()
[docs] def form_factor(self): return self.F