Source code for ampform.dynamics.phasespace

# pylint: disable=abstract-method, arguments-differ, protected-access
# pylint: disable=unbalanced-tuple-unpacking
"""Different parametrizations of phase space factors.

Phase space factors are computed by integrating over the phase space element given by
Equation (49.12) in :pdg-review:`2021; Kinematics; p.2`. See also Equation (50.9) on
:pdg-review:`2021; Resonances; p.6`. This integral is not always easy to solve, which
leads to different parametrizations.

This module provides several parametrizations. They all comply with the
`PhaseSpaceFactorProtocol`, so that they can be used in parametrizations like
`.EnergyDependentWidth`.
"""
from __future__ import annotations

import re
import sys
from typing import Sequence

import sympy as sp
from sympy.printing.conventions import split_super_sub
from sympy.printing.latex import LatexPrinter

from ampform.sympy import (
    UnevaluatedExpression,
    create_expression,
    implement_doit_method,
)
from ampform.sympy.math import ComplexSqrt

if sys.version_info >= (3, 8):
    from typing import Protocol
else:
    from typing_extensions import Protocol  # pragma: no cover


[docs]class PhaseSpaceFactorProtocol(Protocol): """Protocol that is used by `.EnergyDependentWidth`. Use this `~typing.Protocol` when defining other implementations of a phase space factor. Some implementations: - `PhaseSpaceFactor` - `PhaseSpaceFactorAbs` - `PhaseSpaceFactorComplex` - `PhaseSpaceFactorSWave` - `EqualMassPhaseSpaceFactor` Even `BreakupMomentumSquared` and :func:`chew_mandelstam_s_wave` comply with this protocol, but are technically speaking not phase space factors. """
[docs] def __call__(self, s, m_a, m_b) -> sp.Expr: """Expected `~inspect.signature`. Args: s: :ref:`Mandelstam variable <pwa:mandelstam-variables>` :math:`s`. Commonly, this is just :math:`s = m_R^2`, with :math:`m_R` the invariant mass of decaying particle :math:`R`. m_a: Mass of decay product :math:`a`. m_b: Mass of decay product :math:`b`. """
[docs]@implement_doit_method class BreakupMomentumSquared(UnevaluatedExpression): r"""Squared value of the two-body break-up momentum. For a two-body decay :math:`R \to ab`, the *break-up momentum* is the absolute value of the momentum of both :math:`a` and :math:`b` in the rest frame of :math:`R`. See Equation (49.17) on :pdg-review:`2021; Kinematics; p.3`, as well as Equation (50.5) on :pdg-review:`2021; Resonances; p.5`. It's up to the caller in which way to take the square root of this break-up momentum, because :math:`q^2` can have negative values for non-zero :math:`m_a,m_b`. In this case, one may want to use `.ComplexSqrt` instead of the standard :func:`~sympy.functions.elementary.miscellaneous.sqrt`. """ is_commutative = True def __new__(cls, s, m_a, m_b, **hints) -> BreakupMomentumSquared: return create_expression(cls, s, m_a, m_b, **hints) def evaluate(self) -> sp.Expr: s, m_a, m_b = self.args return (s - (m_a + m_b) ** 2) * (s - (m_a - m_b) ** 2) / (4 * s) # type: ignore[operator] def _latex(self, printer: LatexPrinter, *args) -> str: s = self.args[0] s_latex = printer._print(self.args[0]) subscript = _indices_to_subscript(_determine_indices(s)) name = "q^2" + subscript if self._name is None else self._name return Rf"{name}\left({s_latex}\right)"
[docs]@implement_doit_method class PhaseSpaceFactor(UnevaluatedExpression): """Standard phase-space factor, using :func:`BreakupMomentumSquared`. See :pdg-review:`2021; Resonances; p.6`, Equation (50.9). """ is_commutative = True def __new__(cls, s, m_a, m_b, **hints) -> PhaseSpaceFactor: return create_expression(cls, s, m_a, m_b, **hints) def evaluate(self) -> sp.Expr: s, m_a, m_b = self.args q_squared = BreakupMomentumSquared(s, m_a, m_b) denominator = _phase_space_factor_denominator(s) return sp.sqrt(q_squared) / denominator def _latex(self, printer: LatexPrinter, *args) -> str: s_symbol = self.args[0] s_latex = printer._print(s_symbol) subscript = _indices_to_subscript(_determine_indices(s_symbol)) name = R"\rho" + subscript if self._name is None else self._name return Rf"{name}\left({s_latex}\right)"
[docs]@implement_doit_method class PhaseSpaceFactorAbs(UnevaluatedExpression): r"""Phase space factor square root over the absolute value. As opposed to `.PhaseSpaceFactor`, this takes the `~sympy.functions.elementary.complexes.Abs` value of `.BreakupMomentumSquared`, then the :func:`~sympy.functions.elementary.miscellaneous.sqrt`. This version of the phase space factor is often denoted as :math:`\hat{\rho}` and is used in `.EqualMassPhaseSpaceFactor`. """ is_commutative = True def __new__(cls, s, m_a, m_b, **hints) -> PhaseSpaceFactorAbs: return create_expression(cls, s, m_a, m_b, **hints) def evaluate(self) -> sp.Expr: s, m_a, m_b = self.args q_squared = BreakupMomentumSquared(s, m_a, m_b) denominator = _phase_space_factor_denominator(s) return sp.sqrt(sp.Abs(q_squared)) / denominator def _latex(self, printer: LatexPrinter, *args) -> str: s_symbol = self.args[0] s_latex = printer._print(s_symbol) subscript = _indices_to_subscript(_determine_indices(s_symbol)) name = R"\hat{\rho}" + subscript if self._name is None else self._name return Rf"{name}\left({s_latex}\right)"
[docs]@implement_doit_method class PhaseSpaceFactorComplex(UnevaluatedExpression): """Phase-space factor with `.ComplexSqrt`. Same as :func:`PhaseSpaceFactor`, but using a `.ComplexSqrt` that does have defined behavior for defined for negative input values. """ is_commutative = True def __new__(cls, s, m_a, m_b, **hints) -> PhaseSpaceFactorComplex: return create_expression(cls, s, m_a, m_b, **hints) def evaluate(self) -> sp.Expr: s, m_a, m_b = self.args q_squared = BreakupMomentumSquared(s, m_a, m_b) denominator = _phase_space_factor_denominator(s) return ComplexSqrt(q_squared) / denominator def _latex(self, printer: LatexPrinter, *args) -> str: s_symbol = self.args[0] s_latex = printer._print(s_symbol) subscript = _indices_to_subscript(_determine_indices(s_symbol)) name = R"\rho^\mathrm{c}" + subscript if self._name is None else self._name return Rf"{name}\left({s_latex}\right)"
[docs]@implement_doit_method class PhaseSpaceFactorSWave(UnevaluatedExpression): r"""Phase space factor using :func:`chew_mandelstam_s_wave`. This `PhaseSpaceFactor` provides an analytic continuation for decay products with both equal and unequal masses (compare `EqualMassPhaseSpaceFactor`). """ is_commutative = True def __new__(cls, s, m_a, m_b, **hints) -> PhaseSpaceFactorSWave: return create_expression(cls, s, m_a, m_b, **hints) def evaluate(self) -> sp.Expr: s, m_a, m_b = self.args chew_mandelstam = chew_mandelstam_s_wave(s, m_a, m_b) return -sp.I * chew_mandelstam def _latex(self, printer: LatexPrinter, *args) -> str: s_symbol = self.args[0] s_latex = printer._print(s_symbol) subscript = _indices_to_subscript(_determine_indices(s_symbol)) name = R"\rho^\mathrm{CM}" + subscript if self._name is None else self._name return Rf"{name}\left({s_latex}\right)"
[docs]def chew_mandelstam_s_wave(s, m_a, m_b): """Chew-Mandelstam function for :math:`S`-waves (no angular momentum).""" q_squared = BreakupMomentumSquared(s, m_a, m_b) q = ComplexSqrt(q_squared) left_term = sp.Mul( 2 * q / sp.sqrt(s), sp.log((m_a**2 + m_b**2 - s + 2 * sp.sqrt(s) * q) / (2 * m_a * m_b)), evaluate=False, ) right_term = ( (m_a**2 - m_b**2) * (1 / s - 1 / (m_a + m_b) ** 2) * sp.log(m_a / m_b) ) # evaluate=False in order to keep same style as PDG return sp.Mul( 1 / (16 * sp.pi**2), left_term - right_term, evaluate=False, )
[docs]@implement_doit_method class EqualMassPhaseSpaceFactor(UnevaluatedExpression): """Analytic continuation for the :func:`PhaseSpaceFactor`. See :pdg-review:`2018; Resonances; p.9` and :doc:`/usage/dynamics/analytic-continuation`. **Warning**: The PDG specifically derives this formula for a two-body decay *with equal masses*. """ is_commutative = True def __new__(cls, s, m_a, m_b, **hints) -> EqualMassPhaseSpaceFactor: return create_expression(cls, s, m_a, m_b, **hints) def evaluate(self) -> sp.Expr: s, m_a, m_b = self.args rho_hat = PhaseSpaceFactorAbs(s, m_a, m_b) s_threshold = (m_a + m_b) ** 2 # type: ignore[operator] return _analytic_continuation(rho_hat, s, s_threshold) def _latex(self, printer: LatexPrinter, *args) -> str: s_symbol = self.args[0] s_latex = printer._print(s_symbol) subscript = _indices_to_subscript(_determine_indices(s_symbol)) name = R"\rho^\mathrm{eq}" + subscript if self._name is None else self._name return Rf"{name}\left({s_latex}\right)"
def _analytic_continuation(rho, s, s_threshold) -> sp.Piecewise: return sp.Piecewise( ( sp.I * rho / sp.pi * sp.log(sp.Abs((1 + rho) / (1 - rho))), s < 0, ), ( rho + sp.I * rho / sp.pi * sp.log(sp.Abs((1 + rho) / (1 - rho))), s > s_threshold, ), ( 2 * sp.I * rho / sp.pi * sp.atan(1 / rho), True, ), ) def _phase_space_factor_denominator(s) -> sp.Mul: return 8 * sp.pi * sp.sqrt(s) def _indices_to_subscript(indices: Sequence[int]) -> str: """Create a LaTeX subscript from a list of indices. >>> _indices_to_subscript([]) '' >>> _indices_to_subscript([1]) '_{1}' >>> _indices_to_subscript([1, 2]) '_{1,2}' """ if len(indices) == 0: return "" subscript = ",".join(map(str, indices)) return "_{" + subscript + "}" def _determine_indices(symbol) -> list[int]: r"""Extract any indices if available from a `~sympy.core.symbol.Symbol`. >>> _determine_indices(sp.Symbol("m1")) [1] >>> _determine_indices(sp.Symbol("m_a2")) [2] >>> _determine_indices(sp.Symbol(R"\alpha_{i2, 5}")) [2, 5] >>> _determine_indices(sp.Symbol("m")) [] `~sympy.tensor.indexed.Indexed` instances can also be handled: >>> m_a = sp.IndexedBase("m_a") >>> _determine_indices(m_a[0]) [0] """ _, _, subscripts = split_super_sub(sp.latex(symbol)) if not subscripts: return [] subscript: str = subscripts[-1] subscript = re.sub(r"[^0-9^\,]", "", subscript) subscript = f"[{subscript}]" try: indices = eval(subscript) # pylint: disable=eval-used except SyntaxError: return [] return list(indices)