# cspell:ignore cond cpar cparity inpart gpar gparity
"""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):
[docs] @abstractmethod
def check(self, qn_names, in_edges, out_edges, int_node):
pass
[docs]class DefinedForAllEdges(AbstractConditionFunctor):
[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):
[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):
[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] def find_in_dict(self, 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):
"""Check if PID is opposite sign.
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):
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=[]):
if not (
isinstance(qn_name, StateQuantumNumberNames)
or isinstance(qn_name, InteractionQuantumNumberNames)
or isinstance(qn_name, ParticlePropertyNames)
or isinstance(qn_name, ParticleDecayPropertyNames)
):
raise TypeError(
"qn_name has to be of type "
+ "ParticleQuantumNumberNames or "
+ "InteractionQuantumNumberNames or "
+ "ParticlePropertyNames"
)
self.required_qn_names.append(qn_name)
for cond in qn_condition_functions:
self.qn_conditions.append(([qn_name], cond))
[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, cond_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 cond_functor.check(
qn_name_list, in_edges, out_edges, int_node
):
logging.debug(
"condition "
+ str(cond_functor.__class__)
+ " for quantum numbers "
+ str(qn_name_list)
+ " for rule "
+ str(self.__class__)
+ " not satisfied"
)
return False
return True
[docs]class AdditiveQuantumNumberConservation(AbstractRule):
"""checks for the conservation of an additive quantum number such as
electric charge, baryon number, lepton number.
:math:`\\sum q_{in} = \\sum q_{out}`
"""
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):
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):
"""Implements :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):
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):
"""Implements the check.
: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):
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):
"""Implements :math:`C_{in} = C_{out}`."""
cparity_in = self.get_cparity_multiparticle(
ingoing_part_qns, interaction_qns
)
if cparity_in is None:
return True
cparity_out = self.get_cparity_multiparticle(
outgoing_part_qns, interaction_qns
)
if cparity_out is None:
return True
return cparity_in == cparity_out
[docs] def get_cparity_multiparticle(self, part_qns, interaction_qns):
cparity_label = StateQuantumNumberNames.Cparity
pid_label = ParticlePropertyNames.Pid
ang_mom_label = InteractionQuantumNumberNames.L
int_spin_label = InteractionQuantumNumberNames.S
no_cpar_part = [
part_qns.index(x)
for x in part_qns
if cparity_label not in x or x[cparity_label] is None
]
# if all states have c parity defined, then just multiply them
if not no_cpar_part:
return reduce(lambda x, y: x * y[cparity_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
else:
coupled_spin = interaction_qns[int_spin_label].magnitude()
return (-1) ** (ang_mom + coupled_spin)
"""elif len(no_cpar_part) > 0 and len(no_cpar_part) % 2 == 0:
# does this also work for more than 2 particles?
# try to find pairs of particle antiparticle
pids = [x[pid_label] for x in part_qns]
while pids:
found = False
ref_pid = pids.pop()
for x in pids:
if is_particle_antiparticle_pair(ref_pid, x):
found = True
break
if not found:
break"""
return None
[docs]class GParityConservation(AbstractRule):
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):
"""Implements :math:`G_{in} = G_{out}`."""
gparity_label = StateQuantumNumberNames.Gparity
no_gpar_inpart = [
ingoing_part_qns.index(x)
for x in ingoing_part_qns
if gparity_label not in x or x[gparity_label] is None
]
no_gpar_outpart = [
outgoing_part_qns.index(x)
for x in outgoing_part_qns
if gparity_label not in x or x[gparity_label] is None
]
# if all states have g parity defined, then just multiply them
if not no_gpar_inpart + no_gpar_outpart:
in_gpar = reduce(
lambda x, y: x * y[gparity_label], ingoing_part_qns, 1
)
out_gpar = reduce(
lambda x, y: x * y[gparity_label], outgoing_part_qns, 1
)
return in_gpar == out_gpar
# two particle case
particle_counts = (len(ingoing_part_qns), len(outgoing_part_qns))
if particle_counts == (1, 2):
if gparity_label in ingoing_part_qns[0]:
out_gpar = self.check_multistate_gparity(
ingoing_part_qns, outgoing_part_qns, interaction_qns
)
in_gpar = ingoing_part_qns[0][gparity_label]
if out_gpar is not None and in_gpar is not None:
return out_gpar == in_gpar
if particle_counts == (2, 1):
if gparity_label in outgoing_part_qns[0]:
in_gpar = self.check_multistate_gparity(
outgoing_part_qns, ingoing_part_qns, interaction_qns
)
out_gpar = outgoing_part_qns[0][gparity_label]
if out_gpar is not None and in_gpar is not None:
return out_gpar == in_gpar
return True
[docs] def check_multistate_gparity(
self, 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)
else:
coupled_spin = interaction_qns[int_spin_label].magnitude()
return (-1) ** (ang_mom + coupled_spin + isospin)
return None
[docs]class IdenticalParticleSymmetrization(AbstractRule):
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] def check_particles_identical(self, 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 p in particles[1:]:
if p[pid_label] != reference_pid:
return False
if p[spin_label] != reference_spin:
return False
return True
[docs]class SpinConservation(AbstractRule):
"""Implements conservation of a spin-like quantum number for a two body
decay (coupling of two particle states).
See :meth:`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):
"""
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 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] def check_projections(self, 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):
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 s in spins_daughters_coupled:
coupled_spins = self.spin_couplings(s, 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:
L = interaction_qns[InteractionQuantumNumberNames.L]
S = interaction_qns[InteractionQuantumNumberNames.S]
if self.use_projection:
if S in spins_daughters_coupled:
total_spins.update(self.spin_couplings(S, L))
else:
if S.magnitude() in [
x.magnitude() for x in spins_daughters_coupled
]:
total_spins.update(self.spin_couplings(S, L))
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):
"""implements the coupling of two spins.
:math:`|S_1 - S_2| \\leq S \\leq |S_1 + S_2|` and :math:`M_1 + M_2 = M`
"""
j1 = spin1.magnitude()
j2 = spin2.magnitude()
if self.use_projection:
m = spin1.projection() + spin2.projection()
possible_spins = [
Spin(x, m)
for x in arange(abs(j1 - j2), j1 + j2 + 1, 1).tolist()
if x >= abs(m)
]
return [
x
for x in possible_spins
if not is_clebsch_gordan_coefficient_zero(spin1, spin2, x)
]
else:
return [
Spin(x, 0)
for x in arange(abs(j1 - j2), j1 + j2 + 1, 1).tolist()
]
[docs]def is_clebsch_gordan_coefficient_zero(spin1, spin2, spin_coupled):
m1 = spin1.projection()
j1 = spin1.magnitude()
m2 = spin2.projection()
j2 = spin2.magnitude()
m = spin_coupled.projection()
j = spin_coupled.magnitude()
is_zero = False
if (j1 == j2 and m1 == m2) or (m1 == 0.0 and m2 == 0.0):
if abs(j - j1 - j2) % 2 == 1:
is_zero = True
elif j1 == j and m1 == -m:
if abs(j2 - j1 - j) % 2 == 1:
is_zero = True
elif j2 == j and m2 == -m:
if abs(j1 - j2 - j) % 2 == 1:
is_zero = True
return is_zero
[docs]class ClebschGordanCheckHelicityToCanonical(AbstractRule):
"""
implements 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])
L = interaction_qns[InteractionQuantumNumberNames.L]
S = interaction_qns[InteractionQuantumNumberNames.S]
if S.magnitude() < abs(helicity_diff) or in_spins[
0
].magnitude() < abs(helicity_diff):
return False
S = Spin(S.magnitude(), helicity_diff)
if is_clebsch_gordan_coefficient_zero(
out_spins[0], out_spins[1], S
):
return False
in_spins[0] = Spin(in_spins[0].magnitude(), helicity_diff)
return not is_clebsch_gordan_coefficient_zero(L, S, in_spins[0])
return False
[docs]class HelicityConservation(AbstractRule):
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):
"""implements :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):
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):
"""checks 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] def calculate_hypercharge(self, particle):
"""calculates 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):
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):
"""implements 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
)