Source code for ampform.kinematics

# cspell:ignore einsum
# pylint: disable=arguments-differ,no-member,protected-access,too-many-lines
# pylint: disable=unused-argument
"""Classes and functions for relativistic four-momentum kinematics."""

import itertools
import sys
from collections import abc
from functools import singledispatch
from typing import (
    TYPE_CHECKING,
    Any,
    Dict,
    FrozenSet,
    Iterable,
    List,
    Optional,
    Sequence,
    Set,
    Union,
)

import attrs
import sympy as sp
from qrules.topology import Topology
from qrules.transition import ReactionInfo, StateTransition
from sympy.printing.latex import LatexPrinter
from sympy.printing.numpy import NumPyPrinter

from ampform.helicity.decay import (
    assert_isobar_topology,
    determine_attached_final_state,
    get_parent_id,
    get_sibling_state_id,
    is_opposite_helicity_state,
    list_decay_chain_ids,
)
from ampform.helicity.naming import (
    get_helicity_angle_label,
    get_helicity_suffix,
)
from ampform.sympy import (
    NumPyPrintable,
    UnevaluatedExpression,
    _implement_latex_subscript,
    create_expression,
    implement_doit_method,
    make_commutative,
)
from ampform.sympy._array_expressions import (
    ArrayAxisSum,
    ArrayMultiplication,
    ArraySlice,
    ArraySum,
    ArraySymbol,
    MatrixMultiplication,
)
from ampform.sympy.math import ComplexSqrt

if TYPE_CHECKING:  # pragma: no cover
    if sys.version_info < (3, 10):
        from typing_extensions import TypeAlias
    else:
        from typing import TypeAlias


[docs]class HelicityAdapter: r"""Converter for four-momenta to kinematic variable data. The `.create_expressions` method forms the bridge between four-momentum data for the decay you are studying and the kinematic variables that are in the `.HelicityModel`. These are invariant mass (see :func:`.get_invariant_mass_label`) and the :math:`\theta` and :math:`\phi` helicity angles (see :func:`.get_helicity_angle_label`). """ def __init__( self, transitions: Union[ ReactionInfo, Iterable[Union[Topology, StateTransition]] ], ) -> None: self.__topologies = _extract_topologies(transitions) for topology in self.__topologies: assert_isobar_topology(topology)
[docs] def register_transition(self, transition: StateTransition) -> None: topology = _get_topology(transition) self.register_topology(topology)
[docs] def register_topology(self, topology: Topology) -> None: assert_isobar_topology(topology) if self.__topologies: existing = next(iter(self.__topologies)) if topology.incoming_edge_ids != existing.incoming_edge_ids: raise ValueError( "Initial state ID mismatch those of existing topologies" ) if topology.outgoing_edge_ids != existing.outgoing_edge_ids: raise ValueError( "Final state IDs mismatch those of existing topologies" ) self.__topologies.add(topology)
@property def registered_topologies(self) -> FrozenSet[Topology]: return frozenset(self.__topologies)
[docs] def permutate_registered_topologies(self) -> None: """Register outgoing edge permutations of all `registered_topologies`. See :ref:`usage/amplitude:Extend kinematic variables`. """ for topology in set(self.__topologies): final_state_ids = topology.outgoing_edge_ids for permutation in itertools.permutations(final_state_ids): id_mapping = dict(zip(topology.outgoing_edge_ids, permutation)) permuted_topology = attrs.evolve( topology, edges={ id_mapping.get(i, i): edge for i, edge in topology.edges.items() }, ) self.__topologies.add(permuted_topology)
[docs] def create_expressions( self, generate_wigner_angles: bool = False ) -> Dict[str, sp.Expr]: output = {} for topology in self.__topologies: momenta = create_four_momentum_symbols(topology) output.update(compute_helicity_angles(momenta, topology)) output.update(compute_invariant_masses(momenta, topology)) if generate_wigner_angles: wigner_rotation_ids = { i for i in topology.outgoing_edge_ids if get_parent_id(topology, i) != -1 } for state_id in wigner_rotation_ids: angles = compute_wigner_angles(topology, momenta, state_id) output.update(angles) return output
@singledispatch def _extract_topologies( obj: Union[ReactionInfo, Iterable[Union[Topology, StateTransition]]] ) -> Set[Topology]: raise TypeError(f"Cannot extract topologies from a {type(obj).__name__}") @_extract_topologies.register(ReactionInfo) def _(transitions: ReactionInfo) -> Set[Topology]: return _extract_topologies(transitions.transitions) @_extract_topologies.register(abc.Iterable) def _(transitions: abc.Iterable) -> Set[Topology]: return {_get_topology(t) for t in transitions} @singledispatch def _get_topology(obj: Any) -> Topology: raise TypeError( f"Cannot create a {Topology.__name__} from a {type(obj).__name__}" ) @_get_topology.register(Topology) def _(obj: Topology) -> Topology: return obj @_get_topology.register(StateTransition) def _(obj: StateTransition) -> Topology: return obj.topology
[docs]def create_four_momentum_symbols(topology: Topology) -> "FourMomenta": """Create a set of array-symbols for a `~qrules.topology.Topology`. >>> from qrules.topology import create_isobar_topologies >>> topologies = create_isobar_topologies(3) >>> create_four_momentum_symbols(topologies[0]) {0: p0, 1: p1, 2: p2} """ n_final_states = len(topology.outgoing_edge_ids) return {i: FourMomentumSymbol(f"p{i}") for i in range(n_final_states)}
FourMomenta = Dict[int, "FourMomentumSymbol"] """A mapping of state IDs to their corresponding `FourMomentumSymbol`. It's best to create a `dict` of `FourMomenta` with :func:`create_four_momentum_symbols`. """ FourMomentumSymbol: "TypeAlias" = ArraySymbol r"""Array-`~sympy.core.symbol.Symbol` that represents an array of four-momenta. The array is assumed to be of shape :math:`n\times 4` with :math:`n` the number of events. The four-momenta are assumed to be in the order :math:`\left(E,\vec{p}\right)`. See also `Energy`, `FourMomentumX`, `FourMomentumY`, and `FourMomentumZ`. """ # for numpy broadcasting ArraySlice = make_commutative(ArraySlice) # type: ignore[misc]
[docs]@implement_doit_method @make_commutative class Energy(UnevaluatedExpression): """Represents the energy-component of a `FourMomentumSymbol`.""" def __new__(cls, momentum: "FourMomentumSymbol", **hints: Any) -> "Energy": return create_expression(cls, momentum, **hints) @property def _momentum(self) -> "FourMomentumSymbol": return self.args[0] def evaluate(self) -> ArraySlice: return ArraySlice(self._momentum, (slice(None), 0)) def _latex(self, printer: LatexPrinter, *args: Any) -> str: momentum = printer._print(self._momentum) return Rf"E\left({momentum}\right)"
[docs]@_implement_latex_subscript(subscript="x") @implement_doit_method @make_commutative class FourMomentumX(UnevaluatedExpression): """Component :math:`x` of a `FourMomentumSymbol`.""" def __new__( cls, momentum: "FourMomentumSymbol", **hints: Any ) -> "FourMomentumX": return create_expression(cls, momentum, **hints) @property def _momentum(self) -> "FourMomentumSymbol": return self.args[0] def evaluate(self) -> ArraySlice: return ArraySlice(self._momentum, (slice(None), 1))
[docs]@_implement_latex_subscript(subscript="y") @implement_doit_method @make_commutative class FourMomentumY(UnevaluatedExpression): """Component :math:`y` of a `FourMomentumSymbol`.""" def __new__( cls, momentum: "FourMomentumSymbol", **hints: Any ) -> "FourMomentumY": return create_expression(cls, momentum, **hints) @property def _momentum(self) -> "FourMomentumSymbol": return self.args[0] def evaluate(self) -> ArraySlice: return ArraySlice(self._momentum, (slice(None), 2))
[docs]@_implement_latex_subscript(subscript="z") @implement_doit_method @make_commutative class FourMomentumZ(UnevaluatedExpression): """Component :math:`z` of a `FourMomentumSymbol`.""" def __new__( cls, momentum: "FourMomentumSymbol", **hints: Any ) -> "FourMomentumZ": return create_expression(cls, momentum, **hints) @property def _momentum(self) -> "FourMomentumSymbol": return self.args[0] def evaluate(self) -> ArraySlice: return ArraySlice(self._momentum, (slice(None), 3))
[docs]@implement_doit_method @make_commutative class ThreeMomentum(UnevaluatedExpression): """Spatial components of a `FourMomentumSymbol`.""" def __new__( cls, momentum: "FourMomentumSymbol", **hints: Any ) -> "ThreeMomentum": return create_expression(cls, momentum, **hints) @property def _momentum(self) -> "FourMomentumSymbol": return self.args[0] def evaluate(self) -> ArraySlice: three_momentum = ArraySlice( self._momentum, (slice(None), slice(1, None)) ) return three_momentum def _latex(self, printer: LatexPrinter, *args: Any) -> str: momentum = printer._print(self._momentum) return Rf"\vec{{{momentum}}}" def _numpycode(self, printer: NumPyPrinter, *args: Any) -> str: return printer._print(self.evaluate())
[docs]@implement_doit_method @make_commutative class EuclideanNorm(UnevaluatedExpression): """Take the euclidean norm of an array over axis 1.""" def __new__( cls, vector: "FourMomentumSymbol", **hints: Any ) -> "EuclideanNorm": return create_expression(cls, vector, **hints) @property def _vector(self) -> "FourMomentumSymbol": return self.args[0] def evaluate(self) -> ArraySlice: return sp.sqrt(EuclideanNormSquared(self._vector)) def _latex(self, printer: LatexPrinter, *args: Any) -> str: vector = printer._print(self._vector) return Rf"\left|{vector}\right|" def _numpycode(self, printer: NumPyPrinter, *args: Any) -> str: return printer._print(self.evaluate())
[docs]@implement_doit_method @make_commutative class EuclideanNormSquared(UnevaluatedExpression): """Take the squared euclidean norm of an array over axis 1.""" def __new__( cls, vector: "FourMomentumSymbol", **hints: Any ) -> "EuclideanNormSquared": return create_expression(cls, vector, **hints) @property def _vector(self) -> "FourMomentumSymbol": return self.args[0] def evaluate(self) -> ArraySlice: return ArrayAxisSum(self._vector**2, axis=1) def _latex(self, printer: LatexPrinter, *args: Any) -> str: vector = printer._print(self._vector) return Rf"\left|{vector}\right|^{{2}}" def _numpycode(self, printer: NumPyPrinter, *args: Any) -> str: return printer._print(self.evaluate())
[docs]def three_momentum_norm(momentum: FourMomentumSymbol) -> EuclideanNorm: return EuclideanNorm(ThreeMomentum(momentum))
[docs]@implement_doit_method @make_commutative class InvariantMass(UnevaluatedExpression): """Invariant mass of a `FourMomentumSymbol`.""" def __new__(cls, momentum: "FourMomentumSymbol", **hints: Any) -> "Energy": return create_expression(cls, momentum, **hints) @property def _momentum(self) -> "FourMomentumSymbol": return self.args[0] def evaluate(self) -> ArraySlice: p = self._momentum p_xyz = ThreeMomentum(p) return ComplexSqrt(Energy(p) ** 2 - EuclideanNorm(p_xyz) ** 2) def _latex(self, printer: LatexPrinter, *args: Any) -> str: momentum = printer._print(self._momentum) return f"m_{{{momentum}}}"
[docs]@implement_doit_method @make_commutative class Phi(UnevaluatedExpression): r"""Azimuthal angle :math:`\phi` of a `FourMomentumSymbol`.""" def __new__(cls, momentum: "FourMomentumSymbol", **hints: Any) -> "Phi": return create_expression(cls, momentum, **hints) @property def _momentum(self) -> "FourMomentumSymbol": return self.args[0] def evaluate(self) -> sp.Expr: p = self._momentum return sp.atan2(FourMomentumY(p), FourMomentumX(p)) def _latex(self, printer: LatexPrinter, *args: Any) -> str: momentum = printer._print(self._momentum) return Rf"\phi\left({momentum}\right)"
[docs]@implement_doit_method @make_commutative class Theta(UnevaluatedExpression): r"""Polar (elevation) angle :math:`\theta` of a `FourMomentumSymbol`.""" def __new__(cls, momentum: "FourMomentumSymbol", **hints: Any) -> "Theta": return create_expression(cls, momentum, **hints) @property def _momentum(self) -> "FourMomentumSymbol": return self.args[0] def evaluate(self) -> sp.Expr: p = self._momentum return sp.acos(FourMomentumZ(p) / three_momentum_norm(p)) def _latex(self, printer: LatexPrinter, *args: Any) -> str: momentum = printer._print(self._momentum) return Rf"\theta\left({momentum}\right)"
[docs]@implement_doit_method @make_commutative class NegativeMomentum(UnevaluatedExpression): r"""Invert the spatial components of a `FourMomentumSymbol`.""" def __new__(cls, momentum: "FourMomentumSymbol", **hints: Any) -> "Theta": return create_expression(cls, momentum, **hints) @property def _momentum(self) -> "FourMomentumSymbol": return self.args[0] def evaluate(self) -> sp.Expr: p = self._momentum eta = MinkowskiMetric(p) return ArrayMultiplication(eta, p) def _latex(self, printer: LatexPrinter, *args: Any) -> str: momentum = printer._print(self._momentum) return Rf"-\left({momentum}\right)"
[docs]class MinkowskiMetric(NumPyPrintable): # pylint: disable=no-self-use r"""Minkowski metric :math:`\eta = (1, -1, -1, -1)`.""" def __new__( cls, momentum: FourMomentumSymbol, **hints: Any ) -> "MinkowskiMetric": return create_expression(cls, momentum, **hints) @property def _momentum(self) -> "MinkowskiMetric": return self.args[0] def as_explicit(self) -> sp.Expr: return sp.Matrix( [ [1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, -1], ] ) def _latex(self, printer: LatexPrinter, *args: Any) -> str: return R"\boldsymbol{\eta}" def _numpycode(self, printer: NumPyPrinter, *args: Any) -> str: printer.module_imports[printer._module].update( {"array", "ones", "zeros"} ) momentum = printer._print(self._momentum) n_events = f"len({momentum})" zeros = f"zeros({n_events})" ones = f"ones({n_events})" return f"""array( [ [{ones}, {zeros}, {zeros}, {zeros}], [{zeros}, -{ones}, {zeros}, {zeros}], [{zeros}, {zeros}, -{ones}, {zeros}], [{zeros}, {zeros}, {zeros}, -{ones}], ] ).transpose((2, 0, 1))"""
[docs]@implement_doit_method class BoostZMatrix(UnevaluatedExpression): r"""Represents a Lorentz boost matrix in the :math:`z`-direction. Args: beta: Velocity in the :math:`z`-direction, :math:`\beta=p_z/E`. n_events: Number of events :math:`n` for this matrix array of shape :math:`n\times4\times4`. Defaults to the `len` of :code:`beta`. """ def __new__( cls, beta: sp.Expr, n_events: Optional[sp.Symbol] = None, **kwargs: Any ) -> "BoostZMatrix": if n_events is None: n_events = _ArraySize(beta) return create_expression(cls, beta, n_events, **kwargs) def as_explicit(self) -> sp.Expr: beta = self.args[0] gamma = 1 / ComplexSqrt(1 - beta**2) return sp.Matrix( [ [gamma, 0, 0, -gamma * beta], [0, 1, 0, 0], [0, 0, 1, 0], [-gamma * beta, 0, 0, gamma], ] ) def evaluate(self) -> "_BoostZMatrixImplementation": beta = self.args[0] gamma = 1 / sp.sqrt(1 - beta**2) n_events = self.args[1] return _BoostZMatrixImplementation( beta=beta, gamma=gamma, gamma_beta=gamma * beta, ones=_OnesArray(n_events), zeros=_ZerosArray(n_events), ) def _latex(self, printer: LatexPrinter, *args: Any) -> str: return printer._print(self.evaluate(), *args)
class _BoostZMatrixImplementation(NumPyPrintable): def __new__( # pylint: disable=too-many-arguments cls, beta: sp.Expr, gamma: sp.Expr, gamma_beta: sp.Expr, ones: "_OnesArray", zeros: "_ZerosArray", **hints: Any, ) -> "_BoostZMatrixImplementation": return create_expression( cls, beta, gamma, gamma_beta, ones, zeros, **hints ) def _latex(self, printer: LatexPrinter, *args: Any) -> str: beta = printer._print(self.args[0]) return Rf"\boldsymbol{{B_z}}\left({beta}\right)" def _numpycode(self, printer: NumPyPrinter, *args: Any) -> str: printer.module_imports[printer._module].add("array") _, gamma, gamma_beta, ones, zeros = map(printer._print, self.args) return f"""array( [ [{gamma}, {zeros}, {zeros}, -{gamma_beta}], [{zeros}, {ones}, {zeros}, {zeros}], [{zeros}, {zeros}, {ones}, {zeros}], [-{gamma_beta}, {zeros}, {zeros}, {gamma}], ] ).transpose((2, 0, 1))"""
[docs]@implement_doit_method class BoostMatrix(UnevaluatedExpression): r"""Compute a rank-3 Lorentz boost matrix from a `FourMomentumSymbol`.""" def __new__( cls, momentum: FourMomentumSymbol, **kwargs: Any ) -> "BoostMatrix": return create_expression(cls, momentum, **kwargs) def as_explicit(self) -> sp.Expr: momentum = self.args[0] energy = Energy(momentum) beta_sq = EuclideanNormSquared(ThreeMomentum(momentum)) / energy**2 beta_x = FourMomentumX(momentum) / energy beta_y = FourMomentumY(momentum) / energy beta_z = FourMomentumZ(momentum) / energy g = 1 / sp.sqrt(1 - beta_sq) return sp.Matrix( [ [g, -g * beta_x, -g * beta_y, -g * beta_z], [ -g * beta_x, 1 + (g - 1) * beta_x**2 / beta_sq, (g - 1) * beta_y * beta_x / beta_sq, (g - 1) * beta_z * beta_x / beta_sq, ], [ -g * beta_y, (g - 1) * beta_x * beta_y / beta_sq, 1 + (g - 1) * beta_y**2 / beta_sq, (g - 1) * beta_z * beta_y / beta_sq, ], [ -g * beta_z, (g - 1) * beta_x * beta_z / beta_sq, (g - 1) * beta_y * beta_z / beta_sq, 1 + (g - 1) * beta_z**2 / beta_sq, ], ] ) def evaluate(self) -> "_BoostMatrixImplementation": momentum = self.args[0] energy = Energy(momentum) beta_sq = EuclideanNormSquared(ThreeMomentum(momentum)) / energy**2 beta_x = FourMomentumX(momentum) / energy beta_y = FourMomentumY(momentum) / energy beta_z = FourMomentumZ(momentum) / energy gamma = 1 / sp.sqrt(1 - beta_sq) return _BoostMatrixImplementation( momentum, b00=gamma, b01=-gamma * beta_x, b02=-gamma * beta_y, b03=-gamma * beta_z, b11=1 + (gamma - 1) * beta_x**2 / beta_sq, b12=(gamma - 1) * beta_x * beta_y / beta_sq, b13=(gamma - 1) * beta_x * beta_z / beta_sq, b22=1 + (gamma - 1) * beta_y**2 / beta_sq, b23=(gamma - 1) * beta_y * beta_z / beta_sq, b33=1 + (gamma - 1) * beta_z**2 / beta_sq, ) def _latex(self, printer: LatexPrinter, *args: Any) -> str: momentum = printer._print(self.args[0]) return Rf"\boldsymbol{{B}}\left({momentum}\right)"
class _BoostMatrixImplementation(NumPyPrintable): def __new__( # pylint: disable=too-many-arguments,too-many-locals cls, momentum: FourMomentumSymbol, b00: sp.Basic, b01: sp.Basic, b02: sp.Basic, b03: sp.Basic, b11: sp.Basic, b12: sp.Basic, b13: sp.Basic, b22: sp.Basic, b23: sp.Basic, b33: sp.Basic, **kwargs: Any, ) -> "BoostZMatrix": return create_expression( cls, momentum, b00, b01, b02, b03, b11, b12, b13, b22, b23, b33, **kwargs, ) def _latex(self, printer: LatexPrinter, *args: Any) -> str: momentum = printer._print(self.args[0]) return Rf"\boldsymbol{{B}}\left({momentum}\right)" def _numpycode(self, printer: NumPyPrinter, *args: Any) -> str: # pylint: disable=too-many-locals, unbalanced-tuple-unpacking _, b00, b01, b02, b03, b11, b12, b13, b22, b23, b33 = self.args return f"""array( [ [{b00}, {b01}, {b02}, {b03}], [{b01}, {b11}, {b12}, {b13}], [{b02}, {b12}, {b22}, {b23}], [{b03}, {b13}, {b23}, {b33}], ] ).transpose((2, 0, 1))"""
[docs]@implement_doit_method class RotationYMatrix(UnevaluatedExpression): r"""Rotation matrix around the :math:`y`-axis for a `FourMomentumSymbol`. Args: angle: Angle with which to rotate, see e.g. `Phi` and `Theta`. n_events: Number of events :math:`n` for this matrix array of shape :math:`n\times4\times4`. Defaults to the `len` of :code:`angle`. """ def __new__( cls, angle: sp.Expr, n_events: Optional[sp.Symbol] = None, **hints: Any ) -> "RotationYMatrix": if n_events is None: n_events = _ArraySize(angle) return create_expression(cls, angle, n_events, **hints) def as_explicit(self) -> sp.Expr: angle = self.args[0] return sp.Matrix( [ [1, 0, 0, 0], [0, sp.cos(angle), 0, sp.sin(angle)], [0, 0, 1, 0], [0, -sp.sin(angle), 0, sp.cos(angle)], ] ) def evaluate(self) -> "_RotationYMatrixImplementation": angle = self.args[0] n_events = self.args[1] return _RotationYMatrixImplementation( angle=angle, cos_angle=sp.cos(angle), sin_angle=sp.sin(angle), ones=_OnesArray(n_events), zeros=_ZerosArray(n_events), ) def _latex(self, printer: LatexPrinter, *args: Any) -> str: return printer._print(self.evaluate(), *args)
class _RotationYMatrixImplementation(NumPyPrintable): def __new__( # pylint: disable=too-many-arguments cls, angle: sp.Expr, cos_angle: sp.Expr, sin_angle: sp.Expr, ones: "_OnesArray", zeros: "_ZerosArray", **hints: Any, ) -> "_RotationYMatrixImplementation": return create_expression( cls, angle, cos_angle, sin_angle, ones, zeros, **hints ) def _latex(self, printer: LatexPrinter, *args: Any) -> str: angle, *_ = self.args angle = printer._print(angle) return Rf"\boldsymbol{{R_y}}\left({angle}\right)" def _numpycode(self, printer: NumPyPrinter, *args: Any) -> str: printer.module_imports[printer._module].add("array") _, cos_angle, sin_angle, ones, zeros = map(printer._print, self.args) return f"""array( [ [{ones}, {zeros}, {zeros}, {zeros}], [{zeros}, {cos_angle}, {zeros}, {sin_angle}], [{zeros}, {zeros}, {ones}, {zeros}], [{zeros}, -{sin_angle}, {zeros}, {cos_angle}], ] ).transpose((2, 0, 1))"""
[docs]@implement_doit_method class RotationZMatrix(UnevaluatedExpression): r"""Rotation matrix around the :math:`z`-axis for a `FourMomentumSymbol`. Args: angle: Angle with which to rotate, see e.g. `Phi` and `Theta`. n_events: Number of events :math:`n` for this matrix array of shape :math:`n\times4\times4`. Defaults to the `len` of :code:`angle`. """ def __new__( cls, angle: sp.Expr, n_events: Optional[sp.Symbol] = None, **hints: Any ) -> "RotationZMatrix": if n_events is None: n_events = _ArraySize(angle) return create_expression(cls, angle, n_events, **hints) def as_explicit(self) -> sp.Expr: angle = self.args[0] return sp.Matrix( [ [1, 0, 0, 0], [0, sp.cos(angle), -sp.sin(angle), 0], [0, sp.sin(angle), sp.cos(angle), 0], [0, 0, 0, 1], ] ) def evaluate(self) -> "_RotationZMatrixImplementation": angle = self.args[0] n_events = self.args[1] return _RotationZMatrixImplementation( angle=angle, cos_angle=sp.cos(angle), sin_angle=sp.sin(angle), ones=_OnesArray(n_events), zeros=_ZerosArray(n_events), ) def _latex(self, printer: LatexPrinter, *args: Any) -> str: return printer._print(self.evaluate(), *args)
class _RotationZMatrixImplementation(NumPyPrintable): def __new__( # pylint: disable=too-many-arguments cls, angle: sp.Expr, cos_angle: sp.Expr, sin_angle: sp.Expr, ones: "_OnesArray", zeros: "_ZerosArray", **hints: Any, ) -> "_RotationZMatrixImplementation": return create_expression( cls, angle, cos_angle, sin_angle, ones, zeros, **hints ) def _latex(self, printer: LatexPrinter, *args: Any) -> str: angle, *_ = self.args angle = printer._print(angle) return Rf"\boldsymbol{{R_z}}\left({angle}\right)" def _numpycode(self, printer: NumPyPrinter, *args: Any) -> str: printer.module_imports[printer._module].add("array") _, cos_angle, sin_angle, ones, zeros = map(printer._print, self.args) return f"""array( [ [{ones}, {zeros}, {zeros}, {zeros}], [{zeros}, {cos_angle}, -{sin_angle}, {zeros}], [{zeros}, {sin_angle}, {cos_angle}, {zeros}], [{zeros}, {zeros}, {zeros}, {ones}], ] ).transpose((2, 0, 1))""" class _OnesArray(NumPyPrintable): def __new__( cls, shape: Union[int, Sequence[int]], **kwargs: Any ) -> "_OnesArray": return create_expression(cls, shape, **kwargs) def _numpycode(self, printer: NumPyPrinter, *args: Any) -> str: printer.module_imports[printer._module].add("ones") shape = printer._print(self.args[0]) return f"ones({shape})" class _ZerosArray(NumPyPrintable): def __new__( cls, shape: Union[int, Sequence[int]], **kwargs: Any ) -> "_ZerosArray": return create_expression(cls, shape, **kwargs) def _numpycode(self, printer: NumPyPrinter, *args: Any) -> str: printer.module_imports[printer._module].add("zeros") shape = printer._print(self.args[0]) return f"zeros({shape})" class _ArraySize(NumPyPrintable): def __new__(cls, array: sp.Basic, **kwargs: Any) -> "_ArraySize": return create_expression(cls, array, **kwargs) def _numpycode(self, printer: NumPyPrinter, *args: Any) -> str: shape = printer._print(self.args[0]) return f"len({shape})"
[docs]def compute_helicity_angles( four_momenta: "FourMomenta", topology: Topology ) -> Dict[str, sp.Expr]: """Formulate expressions for all helicity angles in a topology. Formulate expressions (`~sympy.core.expr.Expr`) for all helicity angles appearing in a given `~qrules.topology.Topology`. The expressions are given in terms of `FourMomenta` The expressions returned as values in a `dict`, where the keys are defined by :func:`.get_helicity_angle_label`. Example ------- >>> from qrules.topology import create_isobar_topologies >>> topologies = create_isobar_topologies(3) >>> topology = topologies[0] >>> four_momenta = create_four_momentum_symbols(topology) >>> angles = compute_helicity_angles(four_momenta, topology) >>> angles["theta_0"] Theta(p1 + p2) """ if topology.outgoing_edge_ids != set(four_momenta): raise ValueError( f"Momentum IDs {set(four_momenta)} do not match " f"final state edge IDs {set(topology.outgoing_edge_ids)}" ) n_events = _get_number_of_events(four_momenta) def __recursive_helicity_angles( # pylint: disable=too-many-locals four_momenta: "FourMomenta", node_id: int ) -> Dict[str, sp.Expr]: helicity_angles: Dict[str, sp.Expr] = {} child_state_ids = sorted( topology.get_edge_ids_outgoing_from_node(node_id) ) if all( topology.edges[i].ending_node_id is None for i in child_state_ids ): state_id = child_state_ids[0] if is_opposite_helicity_state(topology, state_id): state_id = child_state_ids[1] four_momentum = four_momenta[state_id] phi_label, theta_label = get_helicity_angle_label( topology, state_id ) helicity_angles[phi_label] = Phi(four_momentum) helicity_angles[theta_label] = Theta(four_momentum) for state_id in child_state_ids: edge = topology.edges[state_id] if edge.ending_node_id is not None: # recursively determine all momenta ids in the list sub_momenta_ids = determine_attached_final_state( topology, state_id ) if len(sub_momenta_ids) > 1: # add all of these momenta together -> defines new subsystem four_momentum = ArraySum( *[four_momenta[i] for i in sub_momenta_ids] ) # boost all of those momenta into this new subsystem phi = Phi(four_momentum) theta = Theta(four_momentum) p3_norm = three_momentum_norm(four_momentum) beta = p3_norm / Energy(four_momentum) new_momentum_pool = { k: ArrayMultiplication( BoostZMatrix(beta, n_events), RotationYMatrix(-theta, n_events), RotationZMatrix(-phi, n_events), p, ) for k, p in four_momenta.items() if k in sub_momenta_ids } # register current angle variables if is_opposite_helicity_state(topology, state_id): state_id = get_sibling_state_id(topology, state_id) phi_label, theta_label = get_helicity_angle_label( topology, state_id ) helicity_angles[phi_label] = Phi(four_momentum) helicity_angles[theta_label] = Theta(four_momentum) # call next recursion angles = __recursive_helicity_angles( new_momentum_pool, edge.ending_node_id, ) helicity_angles.update(angles) return helicity_angles initial_state_id = next(iter(topology.incoming_edge_ids)) initial_state_edge = topology.edges[initial_state_id] assert initial_state_edge.ending_node_id is not None return __recursive_helicity_angles( four_momenta, initial_state_edge.ending_node_id )
def _get_number_of_events( four_momenta: "FourMomenta", ) -> "_ArraySize": sorted_momentum_symbols = sorted(four_momenta.values(), key=str) return _ArraySize(sorted_momentum_symbols[0])
[docs]def compute_invariant_masses( four_momenta: "FourMomenta", topology: Topology ) -> Dict[str, sp.Expr]: """Compute the invariant masses for all final state combinations.""" if topology.outgoing_edge_ids != set(four_momenta): raise ValueError( f"Momentum IDs {set(four_momenta)} do not match " f"final state edge IDs {set(topology.outgoing_edge_ids)}" ) invariant_masses = {} for state_id in topology.edges: attached_state_ids = determine_attached_final_state(topology, state_id) total_momentum = ArraySum( *[four_momenta[i] for i in attached_state_ids] ) invariant_mass = InvariantMass(total_momentum) name = get_invariant_mass_label(topology, state_id) invariant_masses[name] = invariant_mass return invariant_masses
[docs]def compute_wigner_angles( topology: Topology, momenta: "FourMomenta", state_id: int ) -> Dict[str, sp.Expr]: """Create an `~sympy.core.expr.Expr` for each angle in a Wigner rotation. Implementation of (B.2-4) in :cite:`marangottoHelicityAmplitudesGeneric2020`, with :math:`x'_z` etc. taken from the result of :func:`compute_wigner_rotation_matrix`. """ wigner_rotation_matrix = compute_wigner_rotation_matrix( topology, momenta, state_id ) x_z = ArraySlice(wigner_rotation_matrix, (slice(None), 1, 3)) y_z = ArraySlice(wigner_rotation_matrix, (slice(None), 2, 3)) z_x = ArraySlice(wigner_rotation_matrix, (slice(None), 3, 1)) z_y = ArraySlice(wigner_rotation_matrix, (slice(None), 3, 2)) z_z = ArraySlice(wigner_rotation_matrix, (slice(None), 3, 3)) alpha = sp.atan2(z_y, z_x) beta = sp.acos(z_z) gamma = sp.atan2(y_z, -x_z) suffix = get_helicity_suffix(topology, state_id) return { f"alpha{suffix}": alpha, f"beta{suffix}": beta, f"gamma{suffix}": gamma, }
[docs]def compute_wigner_rotation_matrix( topology: Topology, momenta: "FourMomenta", state_id: int ) -> MatrixMultiplication: """Compute a Wigner rotation matrix. Implementation of Eq. (36) in :cite:`marangottoHelicityAmplitudesGeneric2020`. """ momentum = momenta[state_id] inverted_direct_boost = BoostMatrix(NegativeMomentum(momentum)) boost_chain = compute_boost_chain(topology, momenta, state_id) return MatrixMultiplication(inverted_direct_boost, *boost_chain)
[docs]def compute_boost_chain( topology: Topology, momenta: "FourMomenta", state_id: int ) -> List[BoostMatrix]: boost_matrices = [] decay_chain_state_ids = __get_boost_chain_ids(topology, state_id) boosted_momenta = { i: get_four_momentum_sum(topology, momenta, i) for i in decay_chain_state_ids } for current_state_id in decay_chain_state_ids: current_momentum = boosted_momenta[current_state_id] boost = BoostMatrix(current_momentum) boosted_momenta = { i: ArrayMultiplication(boost, p) for i, p in boosted_momenta.items() } boost_matrices.append(boost) return boost_matrices
def __get_boost_chain_ids(topology: Topology, state_id: int) -> List[int]: """Get the state IDs from first resonance to this final state. >>> from qrules.topology import create_isobar_topologies >>> topology = create_isobar_topologies(3)[0] >>> __get_boost_chain_ids(topology, state_id=0) [0] >>> __get_boost_chain_ids(topology, state_id=1) [3, 1] >>> __get_boost_chain_ids(topology, state_id=2) [3, 2] """ decay_chain_state_ids = list( reversed(list_decay_chain_ids(topology, state_id)) ) initial_state_id = next(iter(topology.incoming_edge_ids)) decay_chain_state_ids.remove(initial_state_id) return decay_chain_state_ids
[docs]def get_four_momentum_sum( topology: Topology, momenta: "FourMomenta", state_id: int ) -> Union[ArraySum, FourMomentumSymbol]: """Get the `FourMomentumSymbol` or sum of momenta for **any** edge ID. If the edge ID is a final state ID, return its `FourMomentumSymbol`. If it's an intermediate edge ID, return the sum of the momenta of the final states to which it decays. >>> from qrules.topology import create_isobar_topologies >>> topology = create_isobar_topologies(3)[0] >>> momenta = create_four_momentum_symbols(topology) >>> get_four_momentum_sum(topology, momenta, state_id=0) p0 >>> get_four_momentum_sum(topology, momenta, state_id=3) p1 + p2 """ if state_id in topology.outgoing_edge_ids: return momenta[state_id] sub_momenta_ids = determine_attached_final_state(topology, state_id) return ArraySum(*[momenta[i] for i in sub_momenta_ids])
[docs]def get_invariant_mass_label(topology: Topology, state_id: int) -> str: """Generate an invariant mass label for a state (edge on a topology). Example ------- In the case shown in Figure :ref:`one-to-five-topology-0`, the invariant mass of state :math:`5` is :math:`m_{034}`, because :math:`p_5=p_0+p_3+p_4`: >>> from qrules.topology import create_isobar_topologies >>> topologies = create_isobar_topologies(5) >>> get_invariant_mass_label(topologies[0], state_id=5) 'm_034' Naturally, the 'invariant' mass label for a final state is just the mass of the state itself: >>> get_invariant_mass_label(topologies[0], state_id=1) 'm_1' """ final_state_ids = determine_attached_final_state(topology, state_id) return f"m_{''.join(map(str, sorted(final_state_ids)))}"