Source code for tables

'''
:mod:`particletools` --- collection of classes dealing with particle properties and indices
==============================================================================================

This tool gives convenient Python access to the particle database from the
`PYTHIA 8 <http://home.thep.lu.se/~torbjorn/pythia81html/Welcome.html>`_ Monte Carlo.

The default particle ID (in all of my codes) follows the Particle Data Group
`PDG <http://pdg.lbl.gov>`_ particle naming convention. Various particle event generators
use, however, proprietary index schemes. The derived classes, such as :class:`SibyllParticleTable`,
provide conversion routines from these proprietary IDs/names into PDG indices. These can be then
used with the class :class:`PYTHIAParticleData` to obtain particle properties such as
mass :func:`PYTHIAParticleData.mass`, life-time :func:`PYTHIAParticleData.ctau`, etc.

Example:
    enter in a Python shell::

      $ from particletools.tables import *
      $ test()

|

'''

from __future__ import print_function

from abc import ABCMeta
from tempfile import TemporaryFile
from collections import namedtuple
import six
from six.moves import cPickle as pickle

# local units in this module: cm, s, GeV

# speed of light in local units of cm/s
c_speed_of_light = 2.99792458e10

__particle_data__ = TemporaryFile()

[docs]class ParticleData(namedtuple("ParticleData", "name mass ctau charge")): __slots__ = () # no dict for this data type; saves memory, faster access
[docs]class ParticleDataDict(object): """Dict-like class to store ParticleData and provide extended lookup""" def __init__(self): self._name2id = {} self._data = {} def __setitem__(self, pid, particledata): # same names are sometimes repeatedly assigned, # use first occurence for name2id mapping if particledata.name not in self._name2id: self._name2id[particledata.name] = pid self._data[pid] = particledata def __getitem__(self, pid_or_name): # accept pdg_id or particle name if isinstance(pid_or_name, six.integer_types): return self._data[pid_or_name] else: i = self._name2id[pid_or_name] return self._data[i] def id2name(self, pid): return self._data[pid].name def name2id(self, name): return self._name2id[name]
#=============================================================================== # PYTHIAParticleData #===============================================================================
[docs]class PYTHIAParticleData(object): """Class wraps around the original ParticleData.xml from PYTHIA 8. Operates on in-memory dictionary that is generated by parsing the XML file. Supply the `cache_file` argument to reduce initialization time. Args: cache_file (str): path to the cache file use_cache (bool): enable cache, or parse XML file every time """ def __init__(self, cache_file=__particle_data__, use_cache=True): if use_cache: try: self._particle_data, self._branchings = pickle.load(cache_file) except (IOError, EOFError): self._load_xml(cache_file) pickle.dump((self._particle_data, self._branchings), cache_file, protocol=-1) else: self._load_xml(cache_file) def _load_xml(self, cache_file): """Reads the xml and pics out particle data only. If no decay length is given, it will calculated from the width.""" import xml.etree.ElementTree as ET import os base = os.path.dirname(os.path.abspath(__file__)) searchpaths = (base + '/ParticleData.xml', 'ParticleData.xml', '../ParticleData.xml', 'ParticleDataTool/ParticleData.xml') xmlname = None for p in searchpaths: if os.path.isfile(p): xmlname = p break if xmlname is None: raise IOError('ParticleDataTool::_load_xml(): ' 'XML file not found.') root = ET.parse(xmlname).getroot() PData = ParticleData self._particle_data = ParticleDataDict() self._branchings = {} GeVfm = 0.19732696312541853 for child in root: if child.tag == 'particle': attr = child.attrib # faster repeated access pdg_id = int(attr['id']) mass = float(attr['m0']) # raw charge is in units of 1/3 e charge = float(attr['chargeType']) / 3.0 if 'tau0' in attr: ctau = 0.1 * float(attr['tau0']) elif 'mWidth' in attr: mWidth = float(attr['mWidth']) ctau = GeVfm / (mWidth) * 1e-15 * 100.0 # in cm # what is this about? (HD) elif mass == 0.0 or pdg_id in (11, 12, 14, 16, 22, 2212): ctau = float('Inf') # elif pdg_id in (4314, 4324, 311, 433): else: ctau = 0.0 # ctau = float("NaN") self._particle_data[pdg_id] = PData(attr['name'], mass, ctau, charge) if 'antiName' in attr: self._particle_data[-pdg_id] = PData( attr['antiName'], mass, ctau, -charge) #Extract branching ratios and decay channels self._branchings[pdg_id] = [] self._branchings[-pdg_id] = [] for channel in child: if channel.attrib['onMode'] == '1': self._branchings[pdg_id].append( (float(channel.attrib['bRatio']), [ int(p) for p in channel.attrib['products'].split(' ') if p != '' ])) self._branchings[-pdg_id].append( (float(channel.attrib['bRatio']), [ -int(p) for p in channel.attrib['products'].split(' ') if p != '' ])) def __iter__(self): """Returns an iterator over PDG IDs""" return six.iterkeys(self._particle_data.data) def __getitem__(self, pid_or_name): return self._particle_data[pid_or_name] def _pdgid_from_pid_or_name(self, pid_or_name): """Return PDG ID if PDG ID or name are suuplied""" return(pid_or_name if isinstance(pid_or_name, six.integer_types) else self._particle_data.name2id(pid_or_name))
[docs] def iteritems(self): """Returns an iterator over PDG IDs and particle data""" return six.iteritems(self._particle_data._data)
[docs] def pdg_id(self, str_id): """Returns PDG particle ID. Args: str_id (str): PYTHIA style name of particle Returns: (int): PDG ID """ return self._particle_data.name2id(str_id)
[docs] def name(self, pdg_id): """Returns PYTHIA particle name. Args: pdg_id (int): particle PDG ID Returns: (str): particle name string """ return self._particle_data.id2name(pdg_id)
[docs] def decay_channels(self, pid_or_name): """Returns decay channels as list of tuples. Warning, this function reflects only the status in PYTHIA and is not a representation of PDG. Be warned! Args: pdg_id (int): particle PDG ID Returns: (list): (BR-ratio,[prod1, prod2, ...]) """ pdg_id = self._pdgid_from_pid_or_name(pid_or_name) return self._branchings[pdg_id]
[docs] def mass(self, pid_or_name): """Returns particle mass in GeV. The mass is calculated from the width if not given in the XML table. Args: pid_or_name: particle PDG ID or string ID Returns: (float): mass in GeV """ return self._particle_data[pid_or_name].mass
[docs] def ctau(self, pid_or_name): """Returns decay length in cm. Args: pid_or_name: particle PDG ID or string ID Returns: (float): decay length :math:`ctau` in cm """ return self._particle_data[pid_or_name].ctau
[docs] def charge(self, pid_or_name): """Returns charge. Args: pid_or_name: particle PDG ID or string ID Returns: (float): charge """ return self._particle_data[pid_or_name].charge
def _force_stable(self, pid_or_name): """Edits the :math:`ctau` value Args: pid_or_name: particle PDG ID or string ID """ d = self._particle_data[pid_or_name] pdg_id = self._pdgid_from_pid_or_name(pid_or_name) self._particle_data[pdg_id] = ParticleData(d.name, d.mass, float('Inf'), d.charge)
[docs] def is_lepton(self, pid_or_name): """Return `True` if particle is a lepton. Note:: A photon is a lepton here for practical reasons. """ pdg_id = self._pdgid_from_pid_or_name(pid_or_name) return (abs(pdg_id) > 10 and abs(pdg_id) < 20) or pdg_id == 22
[docs] def is_hadron(self, pid_or_name): """Return `True` if particle is a hadron.""" pdg_id = self._pdgid_from_pid_or_name(pid_or_name) return not self.is_lepton(pdg_id) and (100 < abs(pdg_id) < 7000)
[docs] def is_nucleus(self, pid_or_name): """Return `True` if particle is a nucleus.""" pdg_id = self._pdgid_from_pid_or_name(pid_or_name) return abs(pdg_id) > 1000000000
[docs]class InteractionModelParticleTable(): """This abstract class provides conversions from interaction model specific particle IDs/names to PDG IDs and vice versa. Interaction model specifics can be added by deriving from this class like it is done in :class:`SibyllParticleTable`, :class:`QGSJetParticleTable` and :class:`DpmJetParticleTable`. """ __metaclass__ = ABCMeta def __init__(self, part_table): # : hand-crafted particle table that maps name, PDG ID and model ID self.part_table = part_table # : converts model ID to PDG ID self.modid2pdg = {} # : converts PDG ID to model ID self.pdg2modid = {} # : converts model specific name to model ID self.modname2modid = {} # : converts model specific name to PDG ID self.modname2pdg = {} # : converts PDG ID to model specific name self.pdg2modname = {} # : converts model ID to model specific name self.modid2modname = {} # : list of allowed model IDs self.mod_ids = [] # : list of allowed PDG IDs self.pdg_ids = [] # : stores the list of meson PDG IDs self.mesons = [] # : stores the list of baryon PDG IDs self.baryons = [] # Unify particle names according to PYTHIA data base _pytab = PYTHIAParticleData() for modname in list(self.part_table): pdgid = self.part_table[modname][1] self.part_table[_pytab.name(pdgid)] = self.part_table.pop(modname) # Fill mapping dictionaries for modname, pids in six.iteritems(part_table): mod_id, pdg_id = pids self.modid2pdg[mod_id] = pdg_id self.pdg2modid[pdg_id] = mod_id self.pdg2modname[pdg_id] = modname self.modid2modname[mod_id] = modname self.modname2modid[modname] = mod_id self.modname2pdg[modname] = pdg_id self.mod_ids.append(mod_id) self.pdg_ids.append(pdg_id) self.mod_ids.sort() self.pdg_ids.sort() # Check for consistency and duplicates assert(len(self.mod_ids) == len(set(self.mod_ids))), \ "InteractionModelParticleTable error 1." assert (len(self.pdg_ids) == len(set(self.pdg_ids)) == len( self.mod_ids)), "InteractionModelParticleTable error 2." assert (len(self.modname2pdg.keys()) == len( set(self.modname2pdg.keys())) == len( self.mod_ids)), "InteractionModelParticleTable error 3." self.leptons = [l for l in self.list_leptons(use_pdg=True)] self.mesons = [ m for m in self.list_mesons(use_pdg=True) if m not in self.leptons ] self.baryons = self.list_baryons(use_pdg=True)
[docs] def list_leptons(self, use_pdg=False): """Returns list of lepton names or PDG IDs. Args: use_pdg (bool, optional): If True, PDG IDs are return otherwise particle names Returns: list: list of lepton names or PDG IDs """ if not use_pdg: return [self.modid2modname[pid] for pid in self._lepton_range] else: return [self.modid2pdg[pid] for pid in self._lepton_range]
[docs] def list_mesons(self, use_pdg=False): """Returns list of meson names or PDG IDs. Args: use_pdg (bool, optional): If True, PDG IDs are return otherwise particle names Returns: list: list of meson names or PDG IDs """ if not use_pdg: return [self.modid2modname[pid] for pid in self._meson_range] else: return [self.modid2pdg[pid] for pid in self._meson_range]
[docs] def list_baryons(self, use_pdg=False): """Returns list of baryon names or PDG IDs. Args: use_pdg (bool, optional): If True, PDG IDs are return otherwise particle names Returns: list: list of baryon names or PDG IDs """ if not use_pdg: return [self.modid2modname[pid] for pid in self._baryon_range] else: return [self.modid2pdg[pid] for pid in self._baryon_range]
[docs]class SibyllParticleTable(InteractionModelParticleTable): """This derived class provides conversions from SIBYLL particle IDs/names to PDG IDs and vice versa. The table part_table is written by hand from the manual of SIBYLL 2.3. """ def __init__(self): # : Internal variable to track indices of mesons, bayons and leptons in model ID self._lepton_range = [] self._meson_range = [] self._baryon_range = [] # : hand-crafted particle table that maps name, PDG ID and model ID part_table = { 'gamma': (1,22), 'e+': (2,-11), 'e-': (3,11), 'mu+': (4,-13), 'mu-': (5,13), 'pi0': (6,111), 'pi+': (7,211), 'pi-': (8,-211), 'K+': (9,321), 'K-': (10,-321), 'K_L0': (11,130), 'K_S0': (12,310), 'p+': (13,2212), 'n0': (14,2112), 'nu_e': (15,12), 'nu_ebar': (16,-12), 'nu_mu': (17,14), 'nu_mubar': (18,-14), # 'pbar-': (19,-2212), # 'nbar0': (20,-2112), 'K0': (21,311), 'Kbar0': (22,-311), 'eta': (23,221), "eta'": (24,331), 'rho+': (25,213), 'rho-': (26,-213), 'rho0': (27,113), 'K*+': (28,323), 'K*-': (29,-323), 'K*0': (30,313), 'K*bar0': (31,-313), 'omega': (32,223), 'phi': (33,333), 'Sigma+': (34,3222), 'Sigma0': (35,3212), 'Sigma-': (36,3112), 'Xi0': (37,3322), 'Xi-': (38,3312), 'Lambda0': (39,3122), 'Delta++': (40,2224), 'Delta+': (41,2214), 'Delta0': (42,2114), 'Delta-': (43,1114), 'Sigma*+': (44,3224), 'Sigma*0': (45,3214), 'Sigma*-': (46,3114), 'Xi*0': (47,3324), 'Xi*-': (48,3314), 'Omega-': (49,3334), 'D+': (59,411), 'D-': (60,-411), 'D0': (71,421), 'Dbar0': (72,-421), 'eta_c': (73,441), 'D_s+': (74,431), 'D_s-': (75,-431), 'D*_s+': (76,433), 'D*_s-': (77,-433), 'D*+': (78,413), 'D*-': (79,-413), 'D*0': (80,423), 'D*bar0': (81,-423), 'J/psi': (83,443), 'Sigma_c++': (84,4222), 'Sigma_c+': (85,4212), 'Sigma_c0': (86,4112), 'Xi_c+': (87,4232), 'Xi_c0': (88,4132), 'Lambda_c+': (89,4122), 'tau+': (90,-15), 'tau-': (91,15), 'nu_taubar': (92,-16), 'nu_tau': (93,16), 'Sigma*_c++': (94,4224), 'Sigma*_c+': (95,4214), 'Sigma*_c0': (96,4114), 'Xi*_c+': (97,4324), 'Xi*_c0': (98,4314), 'Omega_c0': (99,4332) } for _, (modid, pdgid) in six.iteritems(part_table): if (abs(pdgid) > 10 and abs(pdgid) < 20) or pdgid == 22: self._lepton_range.append(modid) self._lepton_range.sort() temp_dict = {} for name, (modid, pdgid) in six.iteritems(part_table): if (abs(pdgid) > 1000) and (abs(pdgid) < 7000): temp_dict[name + '-bar'] = (-modid, -pdgid) self._baryon_range.append(modid) self._baryon_range.append(-modid) self._baryon_range.sort() part_table.update(temp_dict) self._meson_range = [] for name, (modid, pdgid) in six.iteritems(part_table): if (modid not in self._baryon_range and (abs(pdgid) > 100)): self._meson_range.append(modid) self._meson_range.sort() InteractionModelParticleTable.__init__(self, part_table)
[docs]class UrQMDParticleTable(InteractionModelParticleTable): """This derived class provides conversions from UrQMD particle IDs+isospins/names to PDG IDs and vice versa. The table part_table is written by hand from the manual of UrQMD 3.4. Author: Sonia El Hedri (github:soso128) """ def __init__(self): # : Internal variable to track indices of mesons, bayons and leptons in model ID self._lepton_range = [] self._meson_range = [] self._baryon_range = [] # : hand-crafted particle table that maps name, PDG ID and model ID part_table = { 'gamma': ((100, 0), 22), 'pi0': ((101, 0), 111), 'pi+': ((101, 2), 211), 'pi-': ((101, -2), -211), 'K+': ((106, 1), 321), 'K0': ((106, -1), 311), 'K-': ((-106, -1), -321), 'K0-bar': ((-106, 1), -311), 'p': ((1, 1), 2212), 'n': ((1, -1), 2112), 'eta': ((102, 0), 221), 'rho+': ((104, 2), 213), 'rho-': ((104, -2), -213), 'rho0': ((104, 0), 113), 'K*+': ((108, 2), 323), 'K*-': ((108, -2), -323), 'K*0': ((108, 0), 313), 'K*0-bar': ((-108, 0), -313), 'omega': ((103, 0), 223), 'phi': ((109, 0), 333), 'Sigma+': ((40, 2), 3222), 'Sigma0': ((40, 0), 3212), 'Sigma-': ((40, -2), 3112), 'Xi0': ((49, 0), 3322), 'Xi-': ((49, -1), 3312), 'Lambda0': ((27, 0), 3122), 'Delta++': ((17, 4), 2224), 'Delta+': ((17, 2), 2214), 'Delta0': ((17, 0), 2114), 'Delta-': ((17, -2), 1114), 'Sigma*+': ((41, 2), 3224), 'Sigma*0': ((41, 0), 3214), 'Sigma*-': ((41, -2), 3114), 'Xi*0': ((50, 0), 3324), 'Xi*-': ((50, -1), 3314), 'Omega-': ((55, 0), 3334), 'D+': ((133, 2), 411), 'D-': ((133, -2), -411), 'D0': ((133, 0), 421), 'D0-bar': ((-133, 0), -421), 'etaC': ((107, 0), 441), 'Ds+': ((138, 1), 431), 'Ds-': ((138, -1), -431), 'Ds*+': ((139, 1), 433), 'Ds*-': ((139, -1), -433), 'D*+': ((134, 1), 413), 'D*-': ((134, -1), -413), 'D*0': ((134, 0), 10421), 'D*0-bar': ((-134, 0), -10421), 'jpsi': ((135, 0), 443), } for _, (modid, pdgid) in six.iteritems(part_table): if (abs(pdgid) > 10 and abs(pdgid) < 20) or pdgid == 22: self._lepton_range.append(modid) self._lepton_range.sort() temp_dict = {} for name, (modid, pdgid) in six.iteritems(part_table): if (abs(pdgid) > 1000) and (abs(pdgid) < 7000): if type(modid) == int: temp_dict[name + '-bar'] = (-modid, -pdgid) else: temp_dict[name + '-bar'] = ((-modid[0], modid[1]), -pdgid) self._baryon_range.append(modid) self._baryon_range.append(-modid if type(modid) == int else ( -modid[0], modid[1])) self._baryon_range.sort() part_table.update(temp_dict) for name, (modid, pdgid) in six.iteritems(part_table): if modid not in self._baryon_range and abs(pdgid) > 100: self._meson_range.append(modid) self._meson_range.sort() InteractionModelParticleTable.__init__(self, part_table)
[docs]class QGSJetParticleTable(InteractionModelParticleTable): """This derived class provides conversions from QGSJET particle IDs/names to PDG IDs and vice versa. The table part_table is written by hand based on the source code documentation of QGSJET-II-04. This class also converts indices of earlier versions down to QGSJET01c. Due to specifics of the interaction with the QGSJet source code, an additional variable is needed for the particle charge :attr:`charge_tab`. """ # : dictionary provides lookup of particle charge from model IDs charge_tab = {} def __init__(self): # : Internal variable to track indices of mesons, bayons and leptons in model ID self._lepton_range = [] self._meson_range = [] self._baryon_range = [] # : hand-crafted particle table that maps name, PDG ID and model ID part_table = { 'pi0': (0, 111), 'pi+': (1, 211), 'pi-': (-1, -211), 'p': (2, 2212), 'p-bar': (-2, -2212), 'n': (3, 2112), 'n-bar': (-3, -2112), 'K+': (4, 321), 'K-': (-4, -321), 'K0S': (5, 310), 'K0L': (-5, 130), 'Lambda0': (6, 3122), 'Lambda0-bar': (-6, -3122), 'D+': (7, 411), 'D-': (-7, -411), 'D0': (8, 421), 'D0-bar': (-8, -421), 'LambdaC+': (9, 4122), 'LambdaC+-bar': (-9, -4122), 'eta': (10, 221), 'rho0': (-10, 113) } for _, (modid, pdgid) in six.iteritems(part_table): if (abs(pdgid) > 10 and abs(pdgid) < 20) or pdgid == 22: self._lepton_range.append(modid) self._lepton_range.sort() pytab = PYTHIAParticleData() temp_dict = {} for (modid, pdgid) in six.itervalues(part_table): self.charge_tab[modid] = pytab.charge(pdgid) if (abs(pdgid) > 1000) and (abs(pdgid) < 7000): self._baryon_range.append(modid) self._baryon_range.sort() part_table.update(temp_dict) for (modid, pdgid) in six.itervalues(part_table): if modid not in self._baryon_range and abs(pdgid) > 100: self._meson_range.append(modid) self._meson_range.sort() InteractionModelParticleTable.__init__(self, part_table)
#=============================================================================== # QGSJetIIParticleTable #===============================================================================
[docs]class DpmJetParticleTable(SibyllParticleTable): """This derived class provides conversions from DPMJET-III particle IDs/names to PDG IDs and vice versa and derives from :class:`SibyllParticleTable`. In principle DPMJET uses the PDG indices. However, the PDG table provides information about special or hypothetical particles which are not important, yet. The DPMJET table is therefore derived from a reduced list of particles that are known to SIBYLL. """ def __init__(self): SibyllParticleTable.__init__(self) self._lepton_range = [self.modid2pdg[l] for l in self._lepton_range] self._meson_range = [self.modid2pdg[m] for m in self._meson_range] self._baryon_range = [self.modid2pdg[b] for b in self._baryon_range] self.modid2modname = self.pdg2modname self.mod_ids = [self.modid2pdg[sid] for sid in self.mod_ids] self.modid2pdg = {} for mod_id in self.mod_ids: self.modid2pdg[mod_id] = mod_id
[docs]def make_stable_list(min_life_time, pdata=None, full_record=False): """Returns a list of particles PDG IDs with a lifetime longer than specified argument value in s. Stable particles, such as photons, neutrinos, nucleons and electrons are not included. If full_record is set to true, tuples of PDG IDs and particle data are returned.""" if pdata is None: pdata = PYTHIAParticleData() particle_list = [] for pid, pd in pdata.iteritems(): ctau = pd.ctau if ctau >= min_life_time * c_speed_of_light and ctau < 1e30: if full_record: particle_list.append((pid, pd)) else: particle_list.append(pid) return particle_list