Source code for decaylanguage.particle.particle

from __future__ import absolute_import, division, print_function

# Python standard library
import operator
import re
import os

from copy import copy
from functools import reduce, total_ordering
from fractions import Fraction

# Backport needed if Python 2 is used
from enum import IntEnum

## External dependencies
import attr
import pandas as pd

# The path of this file (used to load data files)
dir_path = os.path.dirname(os.path.realpath(__file__))

# Default files to load
FILE_LATEX = os.path.join(dir_path, 'pdgID_to_latex.txt')
FILE_MASSES = os.path.join(dir_path, 'mass_width.csv')
FILE_EXTENDED = os.path.join(dir_path, 'MintDalitzSpecialParticles.csv')


[docs]def programatic_name(name): 'Return a name safe to use as a variable name' return (name.replace('(','').replace(')','') .replace('*','').replace('::', '_') .replace('-', 'm').replace('+', 'p')
.replace('~', 'bar')) getname = re.compile(r''' ^ # Beginning of string (?P<name> \w+? ) # One or more characters, non-greedy (?:\( (?P<state> \d+ ) \) # Optional state in () (?= \*? \( ) )? # - lookahead for mass (?P<star> \* )? # Optional star (?:\( (?P<mass> \d+ ) \) )? # Optional mass in () (?P<bar> bar )? # Optional bar (?P<charge> [0\+\-][+-]?) # Required 0, -, --, or +, ++ $ # End of string ''', re.VERBOSE)
[docs]class SpinType(IntEnum): Scalar = 1 # (0, 1) PseudoScalar = -1 # (0,-1) Vector = 2 # (1,-1) Axial = -2 # (1, 1) Tensor = 3 # (2, 1) PseudoTensor = -3 # (2,-1)
Unknown = 0 # (0, 0)
[docs]class Par(IntEnum): 'Parity or charge' pp = 2 p = 1 o = 0 m = -1 mm = -2
u = 5 Charge = Par
[docs]class Inv(IntEnum): 'Definition of what happens when particle is inverted' Same = 0 Full = 1
Barless = 2
[docs]class Status(IntEnum): 'The status of the particle' Common = 0 Rare = 1 Unsure = 2 Further = 3
Nonexistant = 4 # Mappings that allow the above classes to be produced from text mappings Par_mapping = {'+':Par.p, '0':Par.o, '+2/3':Par.u, '++':Par.pp, '-':Par.m, '-1/3':Par.u, '?':Par.u, '':Par.o} Inv_mapping = {'':Inv.Same, 'F':Inv.Full, 'B':Inv.Barless} Status_mapping = {'R':Status.Common, 'D':Status.Rare, 'S':Status.Unsure, 'F':Status.Further} # Mappings that allow the above classes to be turned into text mappings Par_undo = {Par.pp:'++', Par.p:'+', Par.o:'0', Par.m:'-', Par.mm:'--', Par.u:'?'} Par_prog = {Par.pp:'pp', Par.p:'p', Par.o:'0', Par.m:'m', Par.mm:'mm', Par.u:'u'}
[docs]def get_from_latex(filename): 'Produce a pandas series from a file with latex mappings in it' latex_table = pd.read_csv(filename, delim_whitespace=True, names='id a b'.split(), index_col=0) series_real = latex_table.a series_anti = latex_table.b series_anti.index = -series_anti.index
return pd.concat([series_real, series_anti])
[docs]def get_from_PDG(filename, latexes=(FILE_LATEX,)): 'Read a file, plus a list of latex files, to produce a pandas DataFrame with particle information' def unmap(mapping): return lambda x: mapping[x.strip()] # Convert each column from text to appropriate data type PDG_converters = dict( Charge=unmap(Par_mapping), G=unmap(Par_mapping), P=unmap(Par_mapping), C=unmap(Par_mapping), A=unmap(Inv_mapping), Rank=lambda x: int(x.strip()) if x.strip() else 0, ID=lambda x: int(x.strip()) if x.strip() else -1, Status=unmap(Status_mapping), Name=lambda x: x.strip(), I=lambda x: x.strip(), J=lambda x: x.strip(), Quarks=lambda x: x.strip() ) # Read in the table, apply the converters, add names, ignore comments pdg_table = pd.read_csv(filename, comment='*', names= 'Mass,MassUpper,MassLower,Width,WidthUpper,WidthLower,I,G,J,P,C,A,' 'ID,Charge,Rank,Status,Name,Quarks'.split(','), converters=PDG_converters ) # Filtering out non-particles (quarks, negative IDs) pdg_table = pdg_table[pdg_table.Charge != Par.u] pdg_table = pdg_table[pdg_table.ID >= 0] # PDG's ID should be the key to table pdg_table.set_index('ID', inplace=True) # Some post processing to produce inverted particles pdg_table_inv = pdg_table[(pdg_table.A == Inv.Full) | ((pdg_table.A == Inv.Barless) # Maybe add? & (pdg_table.Charge != Par.u) & (pdg_table.Charge != Par.o))].copy() pdg_table_inv.index = -pdg_table_inv.index pdg_table_inv.loc[(pdg_table_inv.A != Inv.Same) & (pdg_table_inv.Charge != Par.u), 'Charge'] *= -1 pdg_table_inv.Quarks = (pdg_table_inv.Quarks.str.swapcase() .str.replace('SQRT','sqrt') .str.replace('P','p').str.replace('Q','q') .str.replace('mAYBE NON', 'Maybe non') .str.replace('X','x').str.replace('Y','y')) # Make a combined table with + and - ID numbers full = pd.concat([pdg_table, pdg_table_inv]) # Add the latex latex_series = pd.concat([get_from_latex(latex) for latex in latexes]) full = full.assign(Latex=latex_series) # Return the table, making sure NaNs are just empty strings
return full.fillna('')
[docs]def mkul(upper, lower): 'Utility to print out an uncertainty with different or identical upper/lower bounds' if upper==lower: if upper==0: return '' else: return f{upper!s}' else:
return f'+ {upper!s} - {lower!s}'
[docs]@total_ordering @attr.s(slots=True, cmp=False) class Particle(object): 'The Particle object class. Hold a series of properties for a particle.' val = attr.ib() name = attr.ib() mass = attr.ib() width = attr.ib() charge = attr.ib() A = attr.ib() # Info about particle name for anti-particles rank = attr.ib(0) I = attr.ib(None) # Isospin J = attr.ib(None) # Total angular momentum G = attr.ib(Par.u) # Parity: '', +, -, or ? P = attr.ib(Par.u) # Space parity C = attr.ib(Par.u) # Charge conjugation parity # (B (just charge), F (add bar) , and '' (No change)) quarks = attr.ib('') status = attr.ib(Status.Nonexistant) latex = attr.ib('') mass_upper = attr.ib(0.0) mass_lower = attr.ib(0.0) width_upper = attr.ib(0.0) width_lower = attr.ib(0.0) # Make a class level property that holds the PDG table. Loads on first access (via method) _pdg_table = None
[docs] @classmethod def load_pdg_table(cls, files=(FILE_MASSES, FILE_EXTENDED), latexes=(FILE_LATEX,)): 'Load a PDG table. Will be called on first access to the PDG table' tables = [get_from_PDG(f, latexes) for f in files]
cls._pdg_table = pd.concat(tables)
[docs] @classmethod def pdg_table(cls): 'Get the PDG table. Loads on first access.' if cls._pdg_table is None: cls.load_pdg_table()
return cls._pdg_table # The following needed for total ordering (sort, etc) def __le__(self, other): return abs(self.val) < (abs(other.val)-.5) def __eq__(self, other): return self.val == other.val def __hash__(self): return hash(self.val) @property def radius(self): 'Particle radius, hard coded' if abs(self.val) in [411, 421, 431]: return 5 else: return 1.5 @property def bar(self): 'Check to see if particle is inverted' return self.val < 0 and self.A == Inv.Full @property def spintype(self) -> SpinType: 'Access the SpinType enum' if self.J in [0, 1, 2]: J = int(self.J) if self.P == Par.p: return (SpinType.Scalar, SpinType.Axial, SpinType.Tensor)[J] elif self.P == Par.m: return (SpinType.PseudoScalar, SpinType.Vector, SpinType.PseudoTensor)[J] return SpinType.Unknown
[docs] def invert(self): "Get the antiparticle" other = copy(self) if self.A == Inv.Full or (self.A == Inv.Barless and self.charge != Par.o): other.val = -self.val if self.charge != Par.u: other.charge = -self.charge try: other.quarks = (self.quarks.swapcase() .replace('SQRT','sqrt') .replace('P','p').replace('Q','q') .replace('mAYBE NON', 'Maybe non') .replace('X','x').replace('Y','y')) except AttributeError: pass
return other # Pretty descriptions def __str__(self): return self.name + ('~' if self.A==Inv.Full and self.val < 0 else '') + Par_undo[self.charge] def _repr_latex_(self): name = self.latex if self.bar: name = re.sub(r'^(\\mathrm{|)([\w\\]\w*)', r'\1\\bar{\2}', name) return ("$" + name + '$') if self.latex else '?'
[docs] def describe(self): 'Make a nice high-density string for a particle\'s properties.' if self.val == 0: return "Name: Unknown" val = f"""Name: {self.name:<10} ID: {self.val:<12} Fullname: {self!s:<14} Latex: {self._repr_latex_()} Mass = {self.mass!s:<10} {mkul(self.mass_upper, self.mass_lower)} GeV Width = {self.width!s:<10} {mkul(self.width_upper, self.width_lower)} GeV I (isospin) = {self.I!s:<6} G (parity) = {Par_undo[self.G]:<5} Q (charge) = {Par_undo[self.charge]} J (total angular) = {self.J!s:<6} C (charge parity) = {Par_undo[self.C]:<5} P (space parity) = {Par_undo[self.P]} """ if self.spintype != SpinType.Unknown: val += f" SpinType: {self.spintype!s}\n" if self.quarks: val += f" Quarks: {self.quarks}\n" val += f" Antiparticle status: {self.A.name}\n" val += f" Radius: {self.radius} GeV"
return val @property def programatic_name(self): 'This name could be used for a variable name' name = self.name name += '_' + Par_prog[self.charge] return programatic_name(name) @property def html_name(self): 'This is the name using HTML instead of LaTeX' name = self.latex name = re.sub(r'\^\{(.*?)\}', r'<SUP>\1</SUP>', name) name = re.sub(r'\_\{(.*?)\}', r'<SUB>\1</SUB>', name) name = re.sub(r'\\mathrm\{(.*?)\}', r'\1', name) name = re.sub(r'\\left\[(.*?)\\right\]', r'[\1] ', name) name = name.replace(r'\pi','π').replace(r'\rho','ρ').replace(r'\omega','ω') if self.bar: name += '~' return name
[docs] @classmethod def empty(cls): 'Get a new empty particle'
return cls(0, 'Unknown', 0., 0., 0, Inv.Same)
[docs] @classmethod def from_pdg(cls, val): 'Get a particle from a PDG number' if val == 0: return cls.empty() else: col = cls.pdg_table().loc[val] J = Fraction(col.J) if col.J not in {'2or4', '?'} else col.J I = Fraction(col.I) if col.I not in {'', '<2', '?'} else col.I name = col.Name if abs(val) == 313: name += '(892)' return cls(val, name, col.Mass/1000, col.Width/1000, Par(col.Charge), Inv(col.A), col.Rank, I, J, Par(col.G), Par(col.P), Par(col.C), col.Quarks, Status(col.Status), latex=col.Latex, mass_upper=col.MassUpper/1000, mass_lower=col.MassLower/1000, width_upper=col.WidthUpper/1000,
width_lower=col.WidthLower/1000,)
[docs] @classmethod def from_search_list(cls, name=None, latex=None, *, name_re=None, latex_re=None, particle=None, **search_terms): 'Search for a particle, returning a list of candidates' for term in list(search_terms): if search_terms[term] is None: del search_terms[term] # If J or I is passed, make sure it is a string if not isinstance(search_terms.get('J', ''), str): search_terms['J'] = str(search_terms['J']) if not isinstance(search_terms.get('J', ''), str): search_terms['I'] = str(search_terms['I']) bools = [cls.pdg_table()[term]==match for term, match in search_terms.items()] if name is not None: bools.append(cls.pdg_table().Name.str.contains(str(name), regex=False)) if name_re is not None: bools.append(cls.pdg_table().Name.str.contains(name_re, regex=True)) if latex is not None: bools.append(cls.pdg_table().Latex.str.contains(str(latex), regex=False)) if latex_re is not None: bools.append(cls.pdg_table().Latex.str.contains(latex_re, regex=True)) if particle is not None: bools.append(cls.pdg_table().index > 0 if particle else cls.pdg_table().index < 0) results = cls.pdg_table()[reduce(operator.and_, bools)]
return [cls.from_pdg(r) for r in results.index] raise RuntimeError("Found too many particles")
[docs] @classmethod def from_AmpGen(cls, name): 'Get a particle from an AmpGen style name' mat = getname.match(name) mat = mat.groupdict() Par_mapping = {'++':2, '+':1, '0':0, '-':-1, '--':2} particle = False if mat['bar'] is not None else (True if mat['charge'] == '0' else None) fullname = mat['name'] if mat['state']: fullname += f'({mat["state"]})' if mat['mass']: maxname = fullname + f'({mat["mass"]})' else: maxname = fullname vals = cls.from_search_list(Name = maxname, Charge = Par_mapping[mat['charge']], particle = particle, J = mat['state']) if not vals: vals = cls.from_search_list(Name = fullname, Charge = Par_mapping[mat['charge']], particle = particle, J = mat['state']) if len(vals) > 1 and mat['mass'] is not None: vals = [val for val in vals if mat['mass'] in val.latex] if len(vals) > 1: vals = sorted(vals)
return vals[0]