Source code for pdg.particle

"""
Definition of top-level particle container class.
"""

from sqlalchemy import select, bindparam, distinct, func
from sqlalchemy import and_, or_
from pdg.errors import PdgApiError, PdgNoDataError, PdgAmbiguousValueError
from pdg.utils import make_id, best
from pdg.data import PdgData
from pdg.units import HBAR_IN_GEV_S


[docs] class PdgItem: """A class to represent an "item" encountered in e.g. a description of a decay's products. An item can correspond directly to one particle, indirectly to one particle (as an alias), to a set of particles (as a generic name), or to an arbitrary string. When possible, a PdgItem can be queried (via the particle/particles properties) for the associated particle(s). """ def __init__(self, api, pdgitem_id, edition=None): """Constructor for a PdgItem. Intended for internal API use.""" self.api = api self.pdgitem_id = pdgitem_id self.cache = {} self.edition = edition def __repr__(self): """Return a human-readable representation of the PdgItem.""" name = self._get_pdgitem()['name'] return 'PdgItem("%s")' % name def _get_pdgitem(self): """Load the PdgItem's data from the database.""" if 'pdgitem' not in self.cache: pdgitem_table = self.api.db.tables['pdgitem'] query = select(pdgitem_table).where(pdgitem_table.c.id == bindparam('pdgitem_id')) with self.api.engine.connect() as conn: result = conn.execute(query, {'pdgitem_id': self.pdgitem_id}).fetchone() if result is None: raise PdgNoDataError('No PDGITEM entry for %s' % self.pdgitem_id) self.cache['pdgitem'] = result._mapping return self.cache['pdgitem'] def _get_targets(self): """Get all PdgItems that this one maps directly to. Does not recurse.""" pdgitem_map_table = self.api.db.tables['pdgitem_map'] query = select(pdgitem_map_table).where(pdgitem_map_table.c.pdgitem_id == bindparam('pdgitem_id')) with self.api.engine.connect() as conn: rows = conn.execute(query, {'pdgitem_id': self.pdgitem_id}).fetchall() for row in rows: yield PdgItem(self.api, row.target_id) @property def has_particle(self): """Whether the PdgItem is associated with exactly one particle. The property has_particles indicates whether it is associated with one or more. """ if 'has_particle' not in self.cache: pdgparticle_table = self.api.db.tables['pdgparticle'] query = select(pdgparticle_table).where(pdgparticle_table.c.pdgitem_id == bindparam('pdgitem_id')) with self.api.engine.connect() as conn: result = conn.execute(query, {'pdgitem_id': self.pdgitem_id}).fetchone() if result: self.cache['pdgparticle'] = result._mapping self.cache['has_particle'] = True else: targets = list(self._get_targets()) if len(targets) == 1 and targets[0].has_particle: self.cache['pdgparticle'] = targets[0].cache['pdgparticle'] self.cache['has_particle'] = True else: self.cache['has_particle'] = False return self.cache['has_particle'] @property def particle(self): """The particle associated with the PdgItem, if there is exactly one such particle. Raises PdgAmbiguousValueError if there are more than one, in which case the particles property can be used instead. """ if not self.has_particle: if self.has_particles: raise PdgAmbiguousValueError('No unique PDGPARTICLE for PDGITEM %s' % self.pdgitem_id) raise PdgNoDataError('No PDGPARTICLE for PDGITEM %s' % self.pdgitem_id) p = self.cache['pdgparticle'] return PdgParticle(self.api, p['pdgid'], edition=self.edition, set_mcid=p['mcid'], set_name=p['name']) @property def particles(self): """The list of all particles associated with the PdgItem.""" if self.has_particle: return [self.particle] else: result = [] for target in self._get_targets(): for p in target.particles: result.append(p) return result @property def has_particles(self): """Whether the PdgItem is associated with at least one particle.""" return len(list(self.particles)) > 0 @property def name(self): """The name of the PdgItem.""" return self._get_pdgitem()['name'] # @property # def name_tex(self): # """The TeX name of the PdgItem.""" # return self._get_pdgitem()['name_tex'] @property def item_type(self): """The type of the PdgItem. A single character with the following meanings: 'P': specific state (e.g. "pi+"), 'A': "also" alias, 'W': "was" alias, 'S': shortcut, 'B': both charges (e.g. "pi+-"), 'C': both charges, conjugate (e.g. "pi-+"), 'G': generic state (e.g. "pi"), 'L': general list (e.g. "leptons"), 'I': inclusive indicator (e.g. "X"), 'T': arbitrary text """ return self._get_pdgitem()['item_type']
[docs] class PdgParticle(PdgData): """Container class for all information about a given particle. In addition to access to basic particle properties such as mass, charge, quantum numbers and PDG MC ID, this class provides methods to iterate over the data on all particle properties listed in Particle Listings and Summary Tables, including branching fractions, masses, life-times, etc. """ def __init__(self, api, pdgid, edition=None, set_mcid=None, set_name=None): """Constructor for a PdgParticle given its PDG Identifier and possibly its MC ID.""" super(PdgParticle, self).__init__(api, pdgid, edition) self.set_mcid = set_mcid self.set_name = set_name def __str__(self): try: return 'Data for PDG Particle %s: %s' % (self.pdgid, self.name) except PdgAmbiguousValueError: return 'Data for PDG Particle %s: multiple particle matches' % self.pdgid def _repr_extra(self): return "name='%s'" % self.name def _get_particle_data(self): """Get particle data.""" if 'pdgparticle' not in self.cache: pdgparticle_table = self.api.db.tables['pdgparticle'] query = select(pdgparticle_table) query = query.where(pdgparticle_table.c.pdgid == bindparam('pdgid')) if self.set_mcid is not None: query = query.where(pdgparticle_table.c.mcid == bindparam('mcid')) if self.set_name is not None: query = query.where(pdgparticle_table.c.name == bindparam('name')) with self.api.engine.connect() as conn: params = {'pdgid': self.baseid, 'mcid': self.set_mcid, 'name': self.set_name} matches = conn.execute(query, params).fetchall() if len(matches) == 1: self.cache['pdgparticle'] = matches[0]._mapping else: # Charge-specific state either not found or ambiguous query = select(pdgparticle_table) query = query.where(pdgparticle_table.c.pdgid == bindparam('pdgid')) if self.set_mcid is not None: query = query.where(pdgparticle_table.c.mcid == bindparam('mcid')) query = query.where(pdgparticle_table.c.cc_type == 'S') query = query.where(pdgparticle_table.c.name.notlike('%bar%')) # Exclude generic "*bar" states with self.api.engine.connect() as conn: params = {'pdgid': self.baseid, 'mcid': self.set_mcid} matches_g = conn.execute(query, params).fetchall() if len(matches_g) == 0: mcid_string = ', MC ID = %s' % self.set_mcid if self.set_mcid else '' raise PdgNoDataError('Particle data for %s%s not found' % (self.pdgid, mcid_string)) elif len(matches_g) == 1: self.cache['pdgparticle'] = matches_g[0]._mapping else: names = [p.name for p in matches_g] mcids = list(set([p.mcid for p in matches])) raise PdgAmbiguousValueError('Multiple particles for %s: MCID %s, names %s' % (self.baseid, mcids, names)) return self.cache['pdgparticle']
[docs] def properties(self, data_type_key=None, require_summary_data=True, in_summary_table=None, omit_branching_ratios=False): """Return iterator over specified particle property data. By default, all properties excluding branching fractions and branching fraction ratios are returned. data_type_key can be set to select specific properties. Possible keys are given by the list printed by PdgApi.doc_data_type_keys. The SQL wildcard character ('%') is allowed, so to select all properties, including branching fractions and ratios, set data_type_key='%'. As another example, to get all mass properties, use data_type_key='M'. require_summary_data can be set False to also include properties, where the selected edition of the Review of Particle Physics has no summary value(s) in Particle Listings or Summary Table. in_summary_table can be set to select properties, where a summary value is (True) or is not (False) included in the Summary Table for the selected edition. Setting in_summary_table to a value other than None implies require_summary_data=True. omit_branching_ratios can be set to True to exclude any branching fraction ratio properties that would be selected otherwise. """ pdgid_table = self.api.db.tables['pdgid'] query = select(distinct(pdgid_table.c.pdgid)) if require_summary_data or in_summary_table is not None: pdgdata_table = self.api.db.tables['pdgdata'] query = query.join(pdgdata_table) query = query.where(pdgdata_table.c.edition == bindparam('edition')) if in_summary_table is not None: query = query.where(pdgdata_table.c.in_summary_table == bindparam('in_summary_table')) query = query.where(pdgid_table.c.parent_pdgid.like(bindparam('parent_id'))) if data_type_key is None: # NOTE: like/notlike SQL operators never match null values query = query.where((pdgid_table.c.data_type.notlike('BF%')) | (pdgid_table.c.data_type.is_(None))) query = query.where((pdgid_table.c.data_type.notlike('BR%')) | (pdgid_table.c.data_type.is_(None))) else: # NOTE: like may or may not be case-sensitive, depending on database, so use it only for BR* and BF* # NOTE: like will not match null values, so data_type='%' must be treated separately if '%' in data_type_key: if data_type_key != '%': query = query.where(pdgid_table.c.data_type.like(bindparam('data_type_key'))) else: query = query.where(pdgid_table.c.data_type == bindparam('data_type_key')) if omit_branching_ratios: query = query.where((pdgid_table.c.data_type.notlike('BR%')) | (pdgid_table.c.data_type.is_(None))) query = query.order_by(pdgid_table.c.sort) with self.api.engine.connect() as conn: for entry in conn.execute(query, {'parent_id': self.baseid+'%', 'edition': self.edition, 'data_type_key': data_type_key, 'in_summary_table': in_summary_table}): prop = self.api.get(make_id(entry.pdgid, self.edition)) # For masses, widths, and lifetimes, we must take care to choose # the appropriate entry according to the particle's charge. # Other types of properties don't require further checks. if prop.data_type not in 'MGT': yield prop # NOTE: Now that 's' properties are sorted last, we can safely # include them without breaking best() etc. # If this property is not charge-specific, yield it. elif not any(flag in prop.data_flags for flag in '012'): yield prop # If this particle isn't a specific charge state, yield # everything. elif self.charge is None: yield prop # Finally check whether the charges match elif str(int(abs(self.charge))) in prop.data_flags: yield prop
[docs] def masses(self, require_summary_data=True): """Return iterator over mass data. For most particles, there is only a single mass property, and so the particle's PDG best mass value in GeV can be obtained e.g. from list(some_particle.masses())[0].best_value_in_GeV() For other particles (e.g. for the top quark) there are different ways to determine the mass and the user needs to decide which mass value is the most appropriate for their use case. """ return self.properties('M', require_summary_data)
[docs] def widths(self, require_summary_data=True): """Return iterator over width data.""" return self.properties('G', require_summary_data)
[docs] def lifetimes(self, require_summary_data=True): """Return iterator over lifetime data.""" return self.properties('T', require_summary_data)
[docs] def branching_fractions(self, data_type_key='BF%', require_summary_data=True): """Return iterator over given type(s) of branching fraction data. With data_type_key='BF%' (default), all branching fractions, including subdecay modes, are returned. require_summary_data can be set False to include branching fractions where the current edition has no summary value(s) in the Particle Listings or Summary Table. """ if data_type_key[0:2] != 'BF': raise PdgApiError('illegal branching fraction data type key %s' % data_type_key) return self.properties(data_type_key, require_summary_data)
[docs] def exclusive_branching_fractions(self, include_subdecays=False, require_summary_data=True): """Return iterator over exclusive branching fraction data. Set include_subdecays to True (default is False) to also include subdecay modes (i.e. modes shown indented in the Summary Tables). require_summary_data can be set False to include branching fractions where the current edition has no summary value(s) in the Particle Listings or Summary Table. """ if include_subdecays: return self.branching_fractions('BFX%', require_summary_data) else: return self.branching_fractions('BFX', require_summary_data)
[docs] def inclusive_branching_fractions(self, include_subdecays=False, require_summary_data=True): """Return iterator over inclusive branching fraction data. Set include_subdecays to True (default is False) to also include subdecay modes (i.e. modes shown indented in the Summary Tables). require_summary_data can be set True to request only branching fractions where the current edition has summary value(s) in the Particle Listings or Summary Table. """ if include_subdecays: return self.branching_fractions('BFI%', require_summary_data) else: return self.branching_fractions('BFI', require_summary_data)
@property def name(self): """Name of particle (ASCII format).""" return self._get_particle_data()['name'] @property def mcid(self): """Monte Carlo ID of particle.""" return self._get_particle_data()['mcid'] @property def charge(self): """Charge of particle in units of e.""" return self._get_particle_data()['charge'] @property def quantum_I(self): """Quantum number I (isospin) of particle.""" return self._get_particle_data()['quantum_i'] @property def quantum_G(self): """Quantum number G (G parity) of particle.""" return self._get_particle_data()['quantum_g'] @property def quantum_J(self): """Quantum number J (spin) of particle.""" return self._get_particle_data()['quantum_j'] @property def quantum_P(self): """Quantum number P (parity) of particle.""" return self._get_particle_data()['quantum_p'] @property def quantum_C(self): """Quantum number C (C parity) of particle.""" return self._get_particle_data()['quantum_c'] @property def is_boson(self): """True if particle is a gauge boson.""" return 'G' in self.data_flags @property def is_quark(self): """True if particle is a quark.""" return 'Q' in self.data_flags @property def is_lepton(self): """True if particle is a lepton.""" return 'L' in self.data_flags @property def is_meson(self): """True if particle is a meson.""" return 'M' in self.data_flags @property def is_baryon(self): """True if particle is a baryon.""" return 'B' in self.data_flags @staticmethod def _if_not_limit(prop, units, error=False): """If a PdgProperty's best summary value is NOT a limit, return it in the specified units. Otherwise return None. If error is True, return the error instead of the value. """ summary = prop.best_summary() if summary.is_limit: return None if error: return summary.get_error(units) return summary.get_value(units) @property def mass(self): """Mass of the particle in GeV.""" best_mass_property = best(self.masses(), self.api.pedantic, '%s mass (%s)' % (self.name, self.pdgid)) return self._if_not_limit(best_mass_property, 'GeV') @property def mass_error(self): """Symmetric error on mass of particle in GeV, or None if mass error are asymmetric or mass is a limit.""" best_mass_property = best(self.masses(), self.api.pedantic, '%s (%s)' % (self.pdgid, self.description)) return self._if_not_limit(best_mass_property, 'GeV', error=True) @property def width(self): """Width of the particle in GeV.""" try: best_width_property = best(self.widths(), self.api.pedantic, '%s width (%s)' % (self.name, self.pdgid)) return self._if_not_limit(best_width_property, 'GeV') except PdgNoDataError: if self.api.pedantic: raise # S063 has a lifetime entry but it's NULL if (not self.has_lifetime_entry) or (self.lifetime is None): return 0. return HBAR_IN_GEV_S / self.lifetime @property def width_error(self): """Symmetric error on width of particle in GeV, or None if width error are asymmetric or width is a limit.""" try: best_width_property = best(self.widths(), self.api.pedantic, '%s (%s)' % (self.pdgid, self.description)) return self._if_not_limit(best_width_property, 'GeV', error=True) except PdgNoDataError: if self.api.pedantic: raise # S063 has a lifetime entry but it's NULL if (not self.has_lifetime_entry) or (self.lifetime is None): return 0. return self.lifetime_error * HBAR_IN_GEV_S / self.lifetime**2 @property def lifetime(self): """Lifetime of the particle in seconds.""" try: best_lifetime_property = best(self.lifetimes(), self.api.pedantic, '%s lifetime (%s)' % (self.name, self.pdgid)) return self._if_not_limit(best_lifetime_property, 's') except PdgNoDataError: if self.api.pedantic: raise if not self.has_width_entry: return float('inf') return HBAR_IN_GEV_S / self.width @property def lifetime_error(self): """Symmetric error on lifetime of particle in seconds, or None if lifetime error are asymmetric or lifetime is a limit.""" try: best_lifetime_property = best(self.lifetimes(), self.api.pedantic, '%s (%s)' % (self.pdgid, self.description)) err = self._if_not_limit(best_lifetime_property, 's', error=True) if err is None: err = 0. return err except PdgNoDataError: if self.api.pedantic: raise if not self.has_width_entry: return 0. return self.width_error * HBAR_IN_GEV_S / self.width**2 @property def has_mass_entry(self): """Whether the particle has at least one defined mass.""" return next(self.masses(), None) is not None @property def has_width_entry(self): """Whether the particle has at least one defined decay width.""" return next(self.widths(), None) is not None @property def has_lifetime_entry(self): """Whether the particle has at least one defined lifetime.""" return next(self.lifetimes(), None) is not None
[docs] class PdgParticleList(PdgData, list): """A PdgData subclass to represent a list of PdgParticles. A PdgParticleList is returned when PdgApi.get is called with the PDGID of a (group of) particles. """ def __init__(self, api, pdgid, edition=None): """Constructor for a PdgParticleList given its PDG Identifier.""" super(PdgParticleList, self).__init__(api, pdgid, edition) pdgparticle_table = self.api.db.tables['pdgparticle'] query = select(pdgparticle_table) query = query.where(func.lower(pdgparticle_table.c.pdgid) == bindparam('pdgid')) with self.api.engine.connect() as conn: result = conn.execute(query, {'pdgid': pdgid.lower()}).fetchall() for row in result: self.append(PdgParticle(api, pdgid, edition=edition, set_mcid=row.mcid, set_name=row.name))