Source code for expertsystem.state.conservation_rules

"""Collection of quantum number conservation rules for particle reactions.

Contains:
- Functors for quantum number condition checks.
"""

import logging
from abc import ABC, abstractmethod
from copy import deepcopy
from functools import reduce

from numpy import arange

from .particle import (
    InteractionQuantumNumberNames,
    ParticleDecayPropertyNames,
    ParticlePropertyNames,
    QNNameClassMapping,
    QuantumNumberClasses,
    Spin,
    StateQuantumNumberNames,
    is_boson,
)


[docs]class AbstractConditionFunctor(ABC): """Abstract interface of a condition functor."""
[docs] @abstractmethod def check(self, qn_names, in_edges, out_edges, int_node): pass
[docs]class DefinedForAllEdges(AbstractConditionFunctor): """Check if a graph has all edges defined."""
[docs] def check(self, qn_names, in_edges, out_edges, int_node): for qn_name in qn_names: for edge in in_edges + out_edges: if qn_name not in edge: return False return True
[docs]class DefinedForAllOutgoingEdges(AbstractConditionFunctor): """Check if all outgoing edges are defined."""
[docs] def check(self, qn_names, in_edges, out_edges, int_node): for qn_name in qn_names: for edge in out_edges: if qn_name not in edge: return False return True
[docs]class DefinedForInteractionNode(AbstractConditionFunctor): """Check if all interaction nodes are defined."""
[docs] def check(self, qn_names, in_edges, out_edges, int_node): for qn_name in qn_names: if qn_name not in int_node: return False return True
[docs]class DefinedIfOtherQnNotDefinedInOutSeparate(AbstractConditionFunctor): """Implements logic for...""" def __init__(self, other_qn_names): self.other_qn_names = other_qn_names
[docs] def check(self, qn_names, in_edges, out_edges, int_node): return self.check_edge_set( qn_names, in_edges, int_node ) and self.check_edge_set(qn_names, out_edges, int_node)
[docs] def check_edge_set(self, qn_names, edges, int_node): found_for_all = True for qn_name in self.other_qn_names: if isinstance( qn_name, (StateQuantumNumberNames, ParticlePropertyNames) ): found_for_all = True for edge_props in edges: if not self.find_in_dict(qn_name, edge_props): found_for_all = False break if not found_for_all: break else: if not self.find_in_dict(qn_name, int_node): found_for_all = False break if not found_for_all: for qn_name in qn_names: if isinstance( qn_name, (StateQuantumNumberNames, ParticlePropertyNames) ): for edge_props in edges: if not self.find_in_dict(qn_name, edge_props): return False else: if not self.find_in_dict(qn_name, int_node): return False return True
[docs] @staticmethod def find_in_dict(name, props): found = False for key, val in props.items(): if name == key and val is not None: found = True break return found
[docs]def is_particle_antiparticle_pair(pid1, pid2): # we just check if the pid is opposite in sign # this is a requirement of the pid numbers of course return pid1 == -pid2
[docs]class AbstractRule(ABC): """Abstract interface for a conservation rule.""" def __init__(self, rule_name): self.rule_name = str(rule_name) self.required_qn_names = [] self.qn_conditions = [] self.specify_required_qns() def __repr__(self): return str(self.rule_name) def __str__(self): return str(self.rule_name)
[docs] def get_qn_conditions(self): return self.qn_conditions
[docs] @abstractmethod def specify_required_qns(self): pass
[docs] def add_required_qn(self, qn_name, qn_condition_functions=None): if not ( isinstance( qn_name, ( StateQuantumNumberNames, InteractionQuantumNumberNames, ParticlePropertyNames, ParticleDecayPropertyNames, ), ) ): raise TypeError( "qn_name has to be of type " + "ParticleQuantumNumberNames or " + "InteractionQuantumNumberNames or " + "ParticlePropertyNames" ) self.required_qn_names.append(qn_name) if qn_condition_functions: for condition in qn_condition_functions: self.qn_conditions.append(([qn_name], condition))
[docs] def get_required_qn_names(self): return self.required_qn_names
[docs] @abstractmethod def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): pass
[docs] def check_requirements(self, in_edges, out_edges, int_node): for (qn_name_list, condition_functor) in self.get_qn_conditions(): # part_props = [x for x in qn_name_list if isinstance( # x, ParticlePropertyNames)] # if part_props: # return False if not condition_functor.check( qn_name_list, in_edges, out_edges, int_node ): logging.debug( "condition %s for quantum numbers %s for rule %s not satisfied", condition_functor.__class__, qn_name_list, self.__class__, ) return False return True
[docs]class AdditiveQuantumNumberConservation(AbstractRule): r"""Check for conservation of an additive quantum numbers. :math:`\sum q_{in} = \sum q_{out}` Additive quantum numbers are, for example: - electric charge - baryon number - lepton number """ def __init__(self, qn_name): self.qn_name = qn_name super().__init__(qn_name.name + "Conservation")
[docs] def specify_required_qns(self): self.add_required_qn(self.qn_name, [DefinedForAllEdges()])
[docs] def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): in_qn_sum = sum( [ part[self.qn_name] for part in ingoing_part_qns if self.qn_name in part ] ) out_qn_sum = sum( [ part[self.qn_name] for part in outgoing_part_qns if (self.qn_name in part) ] ) return in_qn_sum == out_qn_sum
[docs]class ParityConservation(AbstractRule): """Check parity conservation.""" def __init__(self): super().__init__("ParityConservation")
[docs] def specify_required_qns(self): self.add_required_qn( StateQuantumNumberNames.Parity, [DefinedForAllEdges()] ) self.add_required_qn( InteractionQuantumNumberNames.L, [DefinedForInteractionNode()] )
[docs] def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): r"""Implement :math:`P_{in} = P_{out} \cdot (-1)^L`.""" # is this valid for two outgoing particles only? parity_label = StateQuantumNumberNames.Parity parity_in = reduce( lambda x, y: x * y[parity_label], ingoing_part_qns, 1 ) parity_out = reduce( lambda x, y: x * y[parity_label], outgoing_part_qns, 1 ) ang_mom = interaction_qns[InteractionQuantumNumberNames.L].magnitude() if parity_in == (parity_out * (-1) ** ang_mom): return True return False
[docs]class ParityConservationHelicity(AbstractRule): """Check parity conservation for the helicity formalism.""" def __init__(self): super().__init__("ParityConservationHelicity")
[docs] def specify_required_qns(self): self.add_required_qn( StateQuantumNumberNames.Parity, [DefinedForAllEdges()] ) self.add_required_qn( StateQuantumNumberNames.Spin, [DefinedForAllEdges()] ) self.add_required_qn( InteractionQuantumNumberNames.ParityPrefactor, [DefinedForInteractionNode()], )
[docs] def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): r"""Implements the check parity conservation check. Check the following: .. math:: A_{-\lambda_1-\lambda_2} = P_1 P_2 P_3 (-1)^{S_2+S_3-S_1} A_{\lambda_1\lambda_2} Notice that only the special case :math:`\lambda_1=\lambda_2=0` may return False. """ if len(ingoing_part_qns) == 1 and len(outgoing_part_qns) == 2: spin_label = StateQuantumNumberNames.Spin parity_label = StateQuantumNumberNames.Parity spins = [ x[spin_label].magnitude() for x in ingoing_part_qns + outgoing_part_qns ] parity_product = reduce( lambda x, y: x * y[parity_label], ingoing_part_qns + outgoing_part_qns, 1, ) prefactor = parity_product * (-1.0) ** ( spins[1] + spins[2] - spins[0] ) daughter_hel = [ 0 for x in outgoing_part_qns if x[spin_label].projection() == 0.0 ] if len(daughter_hel) == 2: if prefactor == -1: return False pf_label = InteractionQuantumNumberNames.ParityPrefactor return prefactor == interaction_qns[pf_label] return True
[docs]class CParityConservation(AbstractRule): """Check for :math:`C`-parity conservation.""" def __init__(self): super().__init__("CParityConservation")
[docs] def specify_required_qns(self): self.add_required_qn(StateQuantumNumberNames.CParity) # the spin quantum number is required to check if the daughter # particles are fermions or bosons self.add_required_qn( StateQuantumNumberNames.Spin, [ DefinedIfOtherQnNotDefinedInOutSeparate( [StateQuantumNumberNames.CParity] ) ], ) self.add_required_qn( InteractionQuantumNumberNames.L, [ DefinedIfOtherQnNotDefinedInOutSeparate( [StateQuantumNumberNames.CParity] ) ], ) self.add_required_qn( InteractionQuantumNumberNames.S, [ DefinedIfOtherQnNotDefinedInOutSeparate( [StateQuantumNumberNames.CParity] ) ], ) self.add_required_qn( ParticlePropertyNames.Pid, [ DefinedIfOtherQnNotDefinedInOutSeparate( [StateQuantumNumberNames.CParity] ) ], )
[docs] def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): """Check for :math:`C_{in} = C_{out}`.""" c_parity_in = self.get_c_parity_multiparticle( ingoing_part_qns, interaction_qns ) if c_parity_in is None: return True c_parity_out = self.get_c_parity_multiparticle( outgoing_part_qns, interaction_qns ) if c_parity_out is None: return True return c_parity_in == c_parity_out
[docs] @staticmethod def get_c_parity_multiparticle(part_qns, interaction_qns): c_parity_label = StateQuantumNumberNames.CParity pid_label = ParticlePropertyNames.Pid ang_mom_label = InteractionQuantumNumberNames.L int_spin_label = InteractionQuantumNumberNames.S no_c_parity_part = [ part_qns.index(x) for x in part_qns if c_parity_label not in x or x[c_parity_label] is None ] # if all states have C parity defined, then just multiply them if not no_c_parity_part: return reduce(lambda x, y: x * y[c_parity_label], part_qns, 1) # two particle case if len(part_qns) == 2: if is_particle_antiparticle_pair( part_qns[0][pid_label], part_qns[1][pid_label] ): ang_mom = interaction_qns[ang_mom_label].magnitude() # if boson if is_boson(part_qns[0]): return (-1) ** ang_mom coupled_spin = interaction_qns[int_spin_label].magnitude() return (-1) ** (ang_mom + coupled_spin) return None
[docs]class GParityConservation(AbstractRule): """Check for :math:`G`-parity conservation.""" def __init__(self): super().__init__("GParityConservation")
[docs] def specify_required_qns(self): self.add_required_qn(StateQuantumNumberNames.GParity) # the spin quantum number is required to check if the daughter # particles are fermions or bosons self.add_required_qn( StateQuantumNumberNames.Spin, [ DefinedIfOtherQnNotDefinedInOutSeparate( [StateQuantumNumberNames.GParity] ) ], ) self.add_required_qn( InteractionQuantumNumberNames.L, [ DefinedIfOtherQnNotDefinedInOutSeparate( [StateQuantumNumberNames.GParity] ) ], ) self.add_required_qn( InteractionQuantumNumberNames.S, [ DefinedIfOtherQnNotDefinedInOutSeparate( [StateQuantumNumberNames.GParity] ) ], ) self.add_required_qn( StateQuantumNumberNames.IsoSpin, [ DefinedIfOtherQnNotDefinedInOutSeparate( [StateQuantumNumberNames.GParity] ) ], ) self.add_required_qn( ParticlePropertyNames.Pid, [ DefinedIfOtherQnNotDefinedInOutSeparate( [StateQuantumNumberNames.GParity] ) ], )
[docs] def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): """Check for :math:`G_{in} = G_{out}`.""" g_parity_label = StateQuantumNumberNames.GParity no_g_parity_in_part = [ ingoing_part_qns.index(x) for x in ingoing_part_qns if g_parity_label not in x or x[g_parity_label] is None ] no_g_parity_out_part = [ outgoing_part_qns.index(x) for x in outgoing_part_qns if g_parity_label not in x or x[g_parity_label] is None ] # if all states have G parity defined, then just multiply them if not no_g_parity_in_part + no_g_parity_out_part: in_g_parity = reduce( lambda x, y: x * y[g_parity_label], ingoing_part_qns, 1 ) out_g_parity = reduce( lambda x, y: x * y[g_parity_label], outgoing_part_qns, 1 ) return in_g_parity == out_g_parity # two particle case particle_counts = (len(ingoing_part_qns), len(outgoing_part_qns)) if particle_counts == (1, 2): if g_parity_label in ingoing_part_qns[0]: out_g_parity = self.check_multistate_g_parity( ingoing_part_qns, outgoing_part_qns, interaction_qns ) in_g_parity = ingoing_part_qns[0][g_parity_label] if out_g_parity is not None and in_g_parity is not None: return out_g_parity == in_g_parity if particle_counts == (2, 1): if g_parity_label in outgoing_part_qns[0]: in_g_parity = self.check_multistate_g_parity( outgoing_part_qns, ingoing_part_qns, interaction_qns ) out_g_parity = outgoing_part_qns[0][g_parity_label] if out_g_parity is not None and in_g_parity is not None: return out_g_parity == in_g_parity return True
[docs] @staticmethod def check_multistate_g_parity( single_state_qns, double_state_qns, interaction_qns ): isospin_label = StateQuantumNumberNames.IsoSpin pid_label = ParticlePropertyNames.Pid ang_mom_label = InteractionQuantumNumberNames.L int_spin_label = InteractionQuantumNumberNames.S if is_particle_antiparticle_pair( double_state_qns[0][pid_label], double_state_qns[1][pid_label] ): ang_mom = interaction_qns[ang_mom_label].magnitude() isospin = single_state_qns[0][isospin_label].magnitude() # if boson if is_boson(double_state_qns[0]): return (-1) ** (ang_mom + isospin) coupled_spin = interaction_qns[int_spin_label].magnitude() return (-1) ** (ang_mom + coupled_spin + isospin) return None
[docs]class IdenticalParticleSymmetrization(AbstractRule): """Implementation of particle symmetrization.""" def __init__(self): super().__init__("IdenticalParticleSymmetrization")
[docs] def specify_required_qns(self): self.add_required_qn(StateQuantumNumberNames.Parity) self.add_required_qn( ParticlePropertyNames.Pid, [DefinedForAllOutgoingEdges()] ) self.add_required_qn( StateQuantumNumberNames.Spin, [DefinedForAllOutgoingEdges()] )
[docs] def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): if self.check_particles_identical(outgoing_part_qns): parity_label = StateQuantumNumberNames.Parity if is_boson(outgoing_part_qns[0]): # we have a boson, check if parity of mother is even parity = ingoing_part_qns[0][parity_label] if parity == -1: # if its odd then return False return False else: # its fermion parity = ingoing_part_qns[0][parity_label] if parity == 1: return False return True
[docs] @staticmethod def check_particles_identical(particles): """Check if pids and spins match.""" pid_label = ParticlePropertyNames.Pid spin_label = StateQuantumNumberNames.Spin reference_pid = particles[0][pid_label] reference_spin = particles[0][spin_label] for particle in particles[1:]: if particle[pid_label] != reference_pid: return False if particle[spin_label] != reference_spin: return False return True
[docs]class SpinConservation(AbstractRule): """Implementation of conservation of a spin-like quantum number. That is, for a two body decay (coupling of two particle states). See :meth:`~.SpinConservation.check` for details. """ def __init__(self, spinlike_qn, use_projection=True): if not isinstance(spinlike_qn, StateQuantumNumberNames): raise TypeError( "Expecting Enum of the type " "ParticleQuantumNumberNames for spinlike_qn" ) if spinlike_qn not in QNNameClassMapping: raise ValueError("spinlike_qn is not associated with a QN class") if QNNameClassMapping[spinlike_qn] is not QuantumNumberClasses.Spin: raise ValueError("spinlike_qn is not of class Spin") self.spinlike_qn = spinlike_qn self.use_projection = use_projection super().__init__(spinlike_qn.name + "Conservation")
[docs] def specify_required_qns(self): self.add_required_qn(self.spinlike_qn, [DefinedForAllEdges()]) # for actual spins we include the angular momentum if self.spinlike_qn is StateQuantumNumberNames.Spin: self.add_required_qn( InteractionQuantumNumberNames.L, [DefinedForInteractionNode()] ) self.add_required_qn( InteractionQuantumNumberNames.S, [DefinedForInteractionNode()] )
[docs] def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): r"""Check for spin conservation. Implements .. math:: |S_1 - S_2| \leq S \leq |S_1 + S_2| and optionally .. math:: |L - S| \leq J \leq |L + S| Also checks :math:`M_1 + M_2 = M` and if Clebsch-Gordan coefficients are all 0. """ spin_label = self.spinlike_qn in_spins = [x[spin_label] for x in ingoing_part_qns] out_spins = [x[spin_label] for x in outgoing_part_qns] if self.use_projection and not self.check_projections( in_spins, out_spins ): return False return self.check_magnitude(in_spins, out_spins, interaction_qns)
[docs] @staticmethod def check_projections(in_part, out_part): in_proj = [x.projection() for x in in_part] out_proj = [x.projection() for x in out_part] return sum(in_proj) == sum(out_proj)
[docs] def check_magnitude(self, in_part, out_part, interaction_qns): in_tot_spins = self.calculate_total_spins(in_part, interaction_qns) out_tot_spins = self.calculate_total_spins(out_part, interaction_qns) matching_spins = in_tot_spins.intersection(out_tot_spins) if len(matching_spins) > 0: return True return False
[docs] def calculate_total_spins(self, part_list, interaction_qns): # pylint: disable=too-many-branches total_spins = set() if len(part_list) == 1: if self.use_projection: total_spins.add(part_list[0]) else: total_spins.add(Spin(part_list[0].magnitude(), 0)) else: # first couple all spins together spins_daughters_coupled = set() spin_list = deepcopy(part_list) while spin_list: if spins_daughters_coupled: temp_coupled_spins = set() tempspin = spin_list.pop() for spin in spins_daughters_coupled: coupled_spins = self.spin_couplings(spin, tempspin) temp_coupled_spins.update(coupled_spins) spins_daughters_coupled = temp_coupled_spins else: spins_daughters_coupled.add(spin_list.pop()) if InteractionQuantumNumberNames.L in interaction_qns: ang_mom = interaction_qns[InteractionQuantumNumberNames.L] spin = interaction_qns[InteractionQuantumNumberNames.S] if self.use_projection: if spin in spins_daughters_coupled: total_spins.update(self.spin_couplings(spin, ang_mom)) else: if spin.magnitude() in [ x.magnitude() for x in spins_daughters_coupled ]: total_spins.update(self.spin_couplings(spin, ang_mom)) else: if self.use_projection: total_spins = spins_daughters_coupled else: total_spins = [ Spin(x.magnitude(), 0.0) for x in spins_daughters_coupled ] return total_spins
[docs] def spin_couplings(self, spin1, spin2): r"""Implement the coupling of two spins. :math:`|S_1 - S_2| \leq S \leq |S_1 + S_2|` and :math:`M_1 + M_2 = M` """ j_1 = spin1.magnitude() j_2 = spin2.magnitude() if self.use_projection: sum_proj = spin1.projection() + spin2.projection() possible_spins = [ Spin(x, sum_proj) for x in arange(abs(j_1 - j_2), j_1 + j_2 + 1, 1).tolist() if x >= abs(sum_proj) ] return [ x for x in possible_spins if not is_clebsch_gordan_coefficient_zero(spin1, spin2, x) ] return [ Spin(x, 0) for x in arange(abs(j_1 - j_2), j_1 + j_2 + 1, 1).tolist() ]
[docs]def is_clebsch_gordan_coefficient_zero(spin1, spin2, spin_coupled): m_1 = spin1.projection() j_1 = spin1.magnitude() m_2 = spin2.projection() j_2 = spin2.magnitude() proj = spin_coupled.projection() mag = spin_coupled.magnitude() is_zero = False if (j_1 == j_2 and m_1 == m_2) or (m_1 == 0.0 and m_2 == 0.0): if abs(mag - j_1 - j_2) % 2 == 1: is_zero = True elif j_1 == mag and m_1 == -proj: if abs(j_2 - j_1 - mag) % 2 == 1: is_zero = True elif j_2 == mag and m_2 == -proj: if abs(j_1 - j_2 - mag) % 2 == 1: is_zero = True return is_zero
[docs]class ClebschGordanCheckHelicityToCanonical(AbstractRule): """Implement Clebsch-Gordan checks. For :math:`S_1, S_2` to :math:`S` and the :math:`L,S` to :math:`J` coupling based on the conversion of helicity to canonical amplitude sums. """ def __init__(self): super().__init__("ClebschGordanCheckHelicityToCanonical")
[docs] def specify_required_qns(self): self.add_required_qn( StateQuantumNumberNames.Spin, [DefinedForAllEdges()] ) self.add_required_qn( InteractionQuantumNumberNames.L, [DefinedForInteractionNode()] ) self.add_required_qn( InteractionQuantumNumberNames.S, [DefinedForInteractionNode()] )
[docs] def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): if len(ingoing_part_qns) == 1 and len(outgoing_part_qns) == 2: spin_label = StateQuantumNumberNames.Spin in_spins = [x[spin_label] for x in ingoing_part_qns] out_spins = [x[spin_label] for x in outgoing_part_qns] out_spins[1] = Spin( out_spins[1].magnitude(), -out_spins[1].projection() ) helicity_diff = sum([x.projection() for x in out_spins]) ang_mom = interaction_qns[InteractionQuantumNumberNames.L] spin = interaction_qns[InteractionQuantumNumberNames.S] if spin.magnitude() < abs(helicity_diff) or in_spins[ 0 ].magnitude() < abs(helicity_diff): return False spin = Spin(spin.magnitude(), helicity_diff) if is_clebsch_gordan_coefficient_zero( out_spins[0], out_spins[1], spin ): return False in_spins[0] = Spin(in_spins[0].magnitude(), helicity_diff) return not is_clebsch_gordan_coefficient_zero( ang_mom, spin, in_spins[0] ) return False
[docs]class HelicityConservation(AbstractRule): """Implementation of helicity conservation.""" def __init__(self): super().__init__("HelicityConservation")
[docs] def specify_required_qns(self): self.add_required_qn( StateQuantumNumberNames.Spin, [DefinedForAllEdges()] )
[docs] def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): r"""Check for :math:`|\lambda_2-\lambda_3| \leq S_1`.""" if len(ingoing_part_qns) == 1 and len(outgoing_part_qns) == 2: spin_label = StateQuantumNumberNames.Spin mother_spin = ingoing_part_qns[0][spin_label].magnitude() daughter_hel = [ x[spin_label].projection() for x in outgoing_part_qns ] if mother_spin >= abs(daughter_hel[0] - daughter_hel[1]): return True return False
[docs]class GellMannNishijimaRule(AbstractRule): """Conservation rule for Gell-Mann-Nishijima.""" def __init__(self): super().__init__("GellMannNishijimaRule")
[docs] def specify_required_qns(self): self.add_required_qn( StateQuantumNumberNames.Charge, [DefinedForAllEdges()] ) self.add_required_qn( StateQuantumNumberNames.IsoSpin, [DefinedForAllEdges()] ) self.add_required_qn(StateQuantumNumberNames.Strangeness) self.add_required_qn(StateQuantumNumberNames.Charm) self.add_required_qn(StateQuantumNumberNames.Bottomness) self.add_required_qn(StateQuantumNumberNames.Topness) self.add_required_qn(StateQuantumNumberNames.BaryonNumber) self.add_required_qn(StateQuantumNumberNames.ElectronLN) self.add_required_qn(StateQuantumNumberNames.MuonLN) self.add_required_qn(StateQuantumNumberNames.TauLN)
[docs] def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): r"""Check the Gell-Mann–Nishijima formula. :math:`Q=I_3+\frac{Y}{2}` for each particle. """ charge_label = StateQuantumNumberNames.Charge isospin_label = StateQuantumNumberNames.IsoSpin eln = StateQuantumNumberNames.ElectronLN mln = StateQuantumNumberNames.MuonLN tln = StateQuantumNumberNames.TauLN for particle in ingoing_part_qns + outgoing_part_qns: if ( sum( [ abs(particle[x]) for x in [eln, mln, tln] if x in particle ] ) > 0.0 ): # if particle is a lepton then skip the check continue isospin_3 = 0 if isospin_label in particle: isospin_3 = particle[isospin_label].projection() if float(particle[charge_label]) != ( isospin_3 + 0.5 * self.calculate_hypercharge(particle) ): return False return True
[docs] @staticmethod def calculate_hypercharge(particle): """Calculate the hypercharge :math:`Y=S+C+B+T+B`.""" qn_labels = [ StateQuantumNumberNames.Strangeness, StateQuantumNumberNames.Charm, StateQuantumNumberNames.Bottomness, StateQuantumNumberNames.Topness, StateQuantumNumberNames.BaryonNumber, ] qn_values = [particle[x] for x in qn_labels if x in particle] return sum(qn_values)
[docs]class MassConservation(AbstractRule): """Mass conservation rule.""" def __init__(self, width_factor=3): self.width_factor = width_factor super().__init__("MassConservation")
[docs] def specify_required_qns(self): self.add_required_qn( ParticlePropertyNames.Mass, [DefinedForAllEdges()] ) self.add_required_qn(ParticleDecayPropertyNames.Width)
[docs] def check(self, ingoing_part_qns, outgoing_part_qns, interaction_qns): r"""Implement the mass check. :math:`M_{out} - N \cdot W_{out} < M_{in} + N \cdot W_{in}` It makes sure that the net mass outgoing state :math:`M_{out}` is smaller than the net mass of the ingoing state :math:`M_{in}`. Also the width :math:`W` of the states is taken into account. """ mass_label = ParticlePropertyNames.Mass width_label = ParticleDecayPropertyNames.Width mass_in = sum([x[mass_label] for x in ingoing_part_qns]) width_in = sum( [x[width_label] for x in ingoing_part_qns if width_label in x] ) mass_out = sum([x[mass_label] for x in outgoing_part_qns]) width_out = sum( [x[width_label] for x in outgoing_part_qns if width_label in x] ) return (mass_out - self.width_factor * width_out) < ( mass_in + self.width_factor * width_in )