Source code for ampform.helicity.naming

"""Generate descriptions used in the `~ampform.helicity` formalism."""
from __future__ import annotations

import re
from abc import ABC, abstractmethod
from functools import lru_cache
from typing import Iterable

import sympy as sp
from qrules.topology import Topology
from qrules.transition import ReactionInfo, State, StateTransition

from .decay import (
    assert_isobar_topology,
    determine_attached_final_state,
    get_helicity_info,
    get_sorted_states,
)


[docs]class NameGenerator(ABC): """Name generator for amplitudes and coefficients in a `.HelicityModel`. .. seealso:: :ref:`usage/helicity/formalism:Coefficient names` """
[docs] @abstractmethod def generate_amplitude_name( self, transition: StateTransition, node_id: int | None = None ) -> str: """Generates a unique name for the amplitude corresponding. That is, corresponging to the given `~qrules.transition.StateTransition`. If ``node_id`` is given, it generates a unique name for the partial amplitude corresponding to the interaction node of the given `~qrules.transition.StateTransition`. """
[docs] @abstractmethod def generate_sequential_amplitude_suffix( self, transition: StateTransition ) -> str: """Generate unique suffix for a sequential amplitude transition."""
[docs] @abstractmethod def generate_two_body_decay_suffix( self, transition: StateTransition, node_id: int ) -> str: """Generate partial amplitude coefficient name suffix."""
@property @abstractmethod def parity_partner_coefficient_mapping(self) -> dict[str, str]: ...
[docs]class HelicityAmplitudeNameGenerator(NameGenerator): def __init__( self, transitions: ReactionInfo | Iterable[StateTransition], insert_parent_helicities: bool = False, insert_child_helicities: bool = True, ) -> None: if isinstance(transitions, ReactionInfo): transitions = transitions.transitions self.__transitions = transitions self.__insert_parent_helicities = insert_parent_helicities self.__insert_child_helicities = insert_child_helicities self._register_amplitude_coefficients() @property def parity_partner_coefficient_mapping(self) -> dict[str, str]: return self.__parity_partner_coefficient_mapping @property def insert_parent_helicities(self) -> bool: """Insert helicities of each parent state in the coefficient names.""" return self.__insert_parent_helicities @insert_parent_helicities.setter def insert_parent_helicities(self, value: bool) -> None: self.__insert_parent_helicities = value self._register_amplitude_coefficients() @property def insert_child_helicities(self) -> bool: """Embed the helicity of each decay product in the coefficients.""" return self.__insert_child_helicities @insert_child_helicities.setter def insert_child_helicities(self, value: bool) -> None: self.__insert_child_helicities = value self._register_amplitude_coefficients() def _register_amplitude_coefficients(self) -> None: self.__parity_partner_coefficient_mapping: dict[str, str] = {} for transition in self.__transitions: self.__register_amplitude_coefficient_name(transition) def __register_amplitude_coefficient_name( self, transition: StateTransition ) -> None: for node_id in transition.topology.nodes: ( coefficient_suffix, parity_partner_coefficient_suffix, priority_partner_coefficient_suffix, ) = self.__generate_amplitude_coefficient_couple( transition, node_id ) if transition.interactions[node_id].parity_prefactor is None: continue if ( coefficient_suffix not in self.__parity_partner_coefficient_mapping ): if ( parity_partner_coefficient_suffix in self.__parity_partner_coefficient_mapping ): if ( parity_partner_coefficient_suffix == priority_partner_coefficient_suffix ): self.__parity_partner_coefficient_mapping[ coefficient_suffix ] = parity_partner_coefficient_suffix else: self.__parity_partner_coefficient_mapping[ parity_partner_coefficient_suffix ] = coefficient_suffix self.__parity_partner_coefficient_mapping[ coefficient_suffix ] = coefficient_suffix else: # if neither this coefficient nor its partner are registered just add it self.__parity_partner_coefficient_mapping[ coefficient_suffix ] = coefficient_suffix def __generate_amplitude_coefficient_couple( self, transition: StateTransition, node_id: int ) -> tuple[str, str, str]: incoming_state, outgoing_states = get_helicity_info( transition, node_id ) par_name_suffix = self.generate_two_body_decay_suffix( transition, node_id ) pp_par_name_suffix = ( _state_to_str(incoming_state, use_helicity=False) + R" \to " + " ".join( _state_to_str(s, make_parity_partner=True) for s in outgoing_states ) ) priority_name_suffix = par_name_suffix if outgoing_states[0].spin_projection < 0 or ( outgoing_states[0].spin_projection == 0 and outgoing_states[1].spin_projection < 0 ): priority_name_suffix = pp_par_name_suffix return (par_name_suffix, pp_par_name_suffix, priority_name_suffix)
[docs] def generate_amplitude_name( # pylint: disable=no-self-use self, transition: StateTransition, node_id: int | None = None, ) -> str: name = "" if node_id is None: node_ids = transition.topology.nodes else: node_ids = frozenset({node_id}) names: list[str] = [] for i in node_ids: incoming_state, outgoing_states = get_helicity_info(transition, i) name = ( _state_to_str(incoming_state) + R" \to " + " ".join(_state_to_str(s) for s in outgoing_states) ) names.append(name) return "; ".join(names)
[docs] def generate_two_body_decay_suffix( self, transition: StateTransition, node_id: int ) -> str: components = self._get_coefficient_components(transition, node_id) return "".join(components)
def _get_coefficient_components( self, transition: StateTransition, node_id: int ) -> tuple[str, str, str]: in_hel_info, out_hel_info = get_helicity_info(transition, node_id) return ( _state_to_str( in_hel_info, use_helicity=self.insert_parent_helicities, ), R" \to ", " ".join( _state_to_str( state, use_helicity=self.insert_child_helicities, ) for state in out_hel_info ), )
[docs] def generate_sequential_amplitude_suffix( self, transition: StateTransition ) -> str: coefficient_names: list[str] = [] for node_id in transition.topology.nodes: suffix = self.generate_two_body_decay_suffix(transition, node_id) if suffix in self.parity_partner_coefficient_mapping: suffix = self.parity_partner_coefficient_mapping[suffix] coefficient_names.append(suffix) return "; ".join(coefficient_names)
[docs]class CanonicalAmplitudeNameGenerator(HelicityAmplitudeNameGenerator): def __init__( self, transitions: ReactionInfo | Iterable[StateTransition], insert_parent_helicities: bool = False, insert_child_helicities: bool = False, insert_ls_combinations: bool = True, ) -> None: self.__insert_ls_combinations = insert_ls_combinations super().__init__( transitions, insert_parent_helicities=insert_parent_helicities, insert_child_helicities=insert_child_helicities, ) @property def insert_ls_combinations(self) -> bool: """Embed each :math:`LS`-combination in the coefficient names.""" return self.__insert_ls_combinations @insert_ls_combinations.setter def insert_ls_combinations(self, value: bool) -> None: self.__insert_ls_combinations = value self._register_amplitude_coefficients()
[docs] def generate_amplitude_name( self, transition: StateTransition, node_id: int | None = None, ) -> str: if isinstance(node_id, int): node_ids = frozenset({node_id}) else: node_ids = transition.topology.nodes names: list[str] = [] for node in node_ids: helicity_name = super().generate_amplitude_name(transition, node) canonical_name = helicity_name.replace( R" \to ", self.__generate_ls_arrow(transition, node), ) names.append(canonical_name) return "; ".join(names)
def _get_coefficient_components( self, transition: StateTransition, node_id: int ) -> tuple[str, str, str]: components = super()._get_coefficient_components(transition, node_id) if not self.insert_ls_combinations: return components return ( components[0], self.__generate_ls_arrow(transition, node_id), components[2], ) @staticmethod def __generate_ls_arrow(transition: StateTransition, node_id: int) -> str: interaction = transition.interactions[node_id] angular_momentum = sp.Rational(interaction.l_magnitude) coupled_spin = sp.Rational(interaction.s_magnitude) return Rf" \xrightarrow[S={coupled_spin}]{{L={angular_momentum}}} "
[docs]def generate_transition_label(transition: StateTransition) -> str: r"""Generate a label for a coherent intensity, including spin projection. >>> import qrules >>> reaction = qrules.generate_transitions( ... initial_state="J/psi(1S)", ... final_state=["gamma", "pi0", "pi0"], ... allowed_intermediate_particles=["f(0)(980)"], ... ) >>> print(generate_transition_label(reaction.transitions[0])) J/\psi(1S)_{-1} \to \gamma_{-1} \pi^{0}_{0} \pi^{0}_{0} >>> print(generate_transition_label(reaction.transitions[-1])) J/\psi(1S)_{+1} \to \gamma_{+1} \pi^{0}_{0} \pi^{0}_{0} """ initial_state_ids = transition.topology.incoming_edge_ids final_state_ids = transition.topology.outgoing_edge_ids initial_states = get_sorted_states(transition, initial_state_ids) final_states = get_sorted_states(transition, final_state_ids) return ( _state_to_str(initial_states[0]) + R" \to " + " ".join(_state_to_str(s) for s in final_states) )
[docs]def get_helicity_angle_label( topology: Topology, state_id: int ) -> tuple[str, str]: r"""Generate a nested helicity angle label for :math:`\phi,\theta`. See :func:`get_boost_chain_suffix` for the meaning of the suffix. """ suffix = get_boost_chain_suffix(topology, state_id) return f"phi{suffix}", f"theta{suffix}"
[docs]@lru_cache(maxsize=None) def get_boost_chain_suffix(topology: Topology, state_id: int) -> str: """Generate a subscript-superscript to identify a chain of Lorentz boosts. The generated subscripts describe the decay sequence from the right to the left, separated by commas. Resonance edge IDs are expressed as a sum of the final state IDs that lie below them (see :func:`.determine_attached_final_state`). The generated label does not state the top-most edge (the initial state). Example ------- The following two allowed isobar topologies for a **1-to-5-body** decay illustrates how the naming scheme results in a unique label for each of the **eight edges** in the decay topology. Note that label only uses final state IDs, but still reflects the internal decay topology. >>> from qrules.topology import create_isobar_topologies >>> topologies = create_isobar_topologies(5) >>> topology = topologies[0] >>> for i in topology.intermediate_edge_ids | topology.outgoing_edge_ids: ... suffix = get_boost_chain_suffix(topology, i) ... print(f"{i}: 'phi{suffix}'") 0: 'phi_0^034' 1: 'phi_1^12' 2: 'phi_2^12' 3: 'phi_3^34,034' 4: 'phi_4^34,034' 5: 'phi_034' 6: 'phi_12' 7: 'phi_34^034' >>> topology = topologies[1] >>> for i in topology.intermediate_edge_ids | topology.outgoing_edge_ids: ... suffix = get_boost_chain_suffix(topology, i) ... print(f"{i}: 'phi{suffix}'") 0: 'phi_0^01' 1: 'phi_1^01' 2: 'phi_2^234' 3: 'phi_3^34,234' 4: 'phi_4^34,234' 5: 'phi_01' 6: 'phi_234' 7: 'phi_34^234' Some labels explained: - :code:`phi_12`: **edge 6** on the *left* topology, because for this topology, we have :math:`p_6=p_1+p_2`. - :code:`phi_234`: **edge 6** *right*, because for this topology, :math:`p_6=p_2+p_3+p_4`. - :code:`phi_1^12`: **edge 1** *left*, because 1 decays from :math:`p_6=p_1+p_2`. - :code:`phi_1^01`: **edge 1** *right*, because it decays from :math:`p_5=p_0+p_1`. - :code:`phi_4^34,234`: **edge 4** *right*, because it decays from edge 7 (:math:`p_7=p_3+p_4`), which comes from edge 6 (:math:`p_7=p_2+p_3+p_4`). As noted, the top-most parent (initial state) is not listed in the label. """ assert_isobar_topology(topology) def recursive_label(topology: Topology, state_id: int) -> str: edge = topology.edges[state_id] if edge.ending_node_id is None: label = f"{state_id}" else: attached_final_state_ids = determine_attached_final_state( topology, state_id ) label = "".join(map(str, attached_final_state_ids)) if edge.originating_node_id is not None: incoming_state_ids = topology.get_edge_ids_ingoing_to_node( edge.originating_node_id ) state_id = next(iter(incoming_state_ids)) if state_id not in topology.incoming_edge_ids: label += f",{recursive_label(topology, state_id)}" return label label = recursive_label(topology, state_id) index_groups = label.split(",") subscript = index_groups[0] suffix = f"_{subscript}" if len(index_groups) > 1: superscript = ",".join(index_groups[1:]) suffix += f"^{superscript}" return suffix
[docs]def get_helicity_suffix(topology: Topology, state_id: int) -> str: """Create an identifier suffix for a topology. Used in :doc:`/usage/helicity/spin-alignment`. Comparable to :func:`get_boost_chain_suffix`. """ superscript = get_topology_identifier(topology) return f"_{state_id}^{superscript}"
[docs]def get_topology_identifier(topology: Topology) -> str: """Create an identifier `str` for a `~qrules.topology.Topology`.""" resonance_names = [ "".join(__get_resonance_identifier(topology, i)) for i in topology.intermediate_edge_ids ] return ",".join(sorted(resonance_names, key=natural_sorting))
def __get_resonance_identifier(topology: Topology, state_id: int) -> str: attached_final_state_ids = determine_attached_final_state( topology, state_id ) return "".join(map(str, attached_final_state_ids))
[docs]def natural_sorting(text: str) -> list[float | str]: """Function that can be used for natural sort order in :func:`sorted`. See `natural sort order <https://en.wikipedia.org/wiki/Natural_sort_order>`_. >>> sorted(["z11", "z2"], key=natural_sorting) ['z2', 'z11'] """ # https://stackoverflow.com/a/5967539/13219025 return [ __attempt_number_cast(c) for c in re.split(r"[+-]?([0-9]+(?:[.][0-9]*)?|[.][0-9]+)", text) ]
def __attempt_number_cast(text: str) -> float | str: try: return float(text) except ValueError: return text def _state_to_str( state: State, use_helicity: bool = True, make_parity_partner: bool = False, ) -> str: if state.particle.latex is not None: output_string = state.particle.latex else: output_string = state.particle.name if use_helicity: if make_parity_partner: helicity = -1 * state.spin_projection else: helicity = state.spin_projection helicity_str = _render_float(helicity) if "_" in output_string: output_string = f"{{{output_string}}}" output_string += f"_{{{helicity_str}}}" return output_string def _render_float(value: float) -> str: """Render a `float` nicely as a string. >>> _render_float(-0.5) '-1/2' >>> _render_float(1) '+1' """ rational = sp.Rational(value) if value > 0: return f"+{rational}" return str(rational)