Source code for pdg.data

"""
Container base classes for PDG data.

All PDG data classes use lazy (i.e. only when needed) loading of data from the database
as implemented in the PdgData base class. In most cases, the data is read only once and
cached for subsequent use.

PdgProperty is a subclass of PdgData and adds the retrieval of summary values, measurement
data, etc., that is shared by all classes supporting the retrieval of different kinds of
particle physics properties such as branching fractions or particle masses.
"""

import pprint
from sqlalchemy import select, bindparam, func
from pdg.utils import parse_id, make_id
from pdg.units import UNIT_CONVERSION_FACTORS, convert
from pdg.errors import PdgApiError, PdgInvalidPdgIdError, PdgAmbiguousValueError, PdgNoDataError


[docs] class PdgSummaryValue(dict): """Container for a single value from the Summary Tables.""" def __str__(self): indicator = self.value_type if not indicator: indicator = '[key = %s]' % self.value_type_key return '%-20s %-20s %s' % (self.display_value_text, indicator, self.comment if self.comment else '')
[docs] def pprint(self): """Print all data in this PdgSummaryValue object in a nice format (for debugging).""" pprint.pprint(self)
[docs] def get_value(self, units=None): """Return value after conversion into units specified by parameter units (string). If units are not specified, the value is returned without conversion in the default units for this quantity. Check properties is_limit, is_lower_limit and is_upper_limit to determine if value is a central value or limit. """ if units is None: return self['value'] else: try: return convert(self['value'], self['unit_text'], units) except TypeError: return None
[docs] def get_error_positive(self, units=None): """Return positive error after conversion into units specified by parameter units (string). If units are not specified, the positive error is returned without conversion in the default units for this quantity. """ if units is None: return self['error_positive'] else: return convert(self['error_positive'], self['unit_text'], units)
[docs] def get_error_negative(self, units=None): """Return negative error after conversion into units specified by parameter units (string). If units are not specified, the negative error is returned without conversion in the default units for this quantity. """ if units is None: return self['error_negative'] else: return convert(self['error_negative'], self['unit_text'], units)
[docs] def get_error(self, units=None): """Symmetric error or None, in units specified by parameter units (string). Returns symmetric error as average of positive and negative errors if they differ by less than 10% of their average. Otherwise, returns None. Also returns None if the quantity is a limit.""" if self.is_limit: return None try: err_avg = (self.error_positive + self.error_negative) / 2.0 if abs(self.error_positive - self.error_negative) < 0.1 * err_avg: return convert(err_avg, self['unit_text'], units) else: return None except TypeError: return None
@property def pdgid(self): """PDG Identifier of quantity for which value is given.""" return self['pdgid'] @property def description(self): """Description of quantity for which value is given""" return self['description'] @property def value_type_key(self): """Type of value, given by its key. See PdgApi.doc_value_type_keys() for the meaning of the different value type keys.""" return self['value_type'] @property def value_type(self): """Type of value, given as the PDG indicator string.""" if self.value_type_key in ('AC', 'D', 'E'): return 'OUR AVERAGE' elif self.value_type_key in ('L',): return 'BEST LIMIT' elif self.value_type_key in ('OL',): return 'OUR LIMIT' elif self.value_type_key in ('FC', 'DR'): return 'OUR FIT' elif self.value_type_key in ('V', 'DV'): return 'OUR EVALUATION' else: return '' @property def in_summary_table(self): """True if value is included in Summary Table.""" return self['in_summary_table'] @property def confidence_level(self): """Confidence level for limits, None otherwise.""" return self['confidence_level'] @property def is_limit(self): """True if value is a limit.""" return self['confidence_level'] is not None or self['limit_type'] is not None @property def is_upper_limit(self): """True if value is an upper limit.""" return self['limit_type'] == 'U' @property def is_lower_limit(self): """True if value is an upper limit.""" return self['limit_type'] == 'L' @property def comment(self): """Details for or comments on this value.""" return self['comment'] @property def value(self): """Numerical value in units given by property units. Check properties is_limit, is_lower_limit and is_upper_limit to determine if value is a central value or limit. """ return self['value'] @property def error_positive(self): """Numerical value of positive error in units given by property units.""" return self['error_positive'] @property def error_negative(self): """Numerical value of negative error in units given by property units.""" return self['error_negative'] @property def error(self): """Symmetric error or None. Returns symmetric error as average of positive and negative errors if they differ by less than 10% of their average. Otherwise, returns None. Also returns None if the quantity is a limit.""" return self.get_error() @property def scale_factor(self): """PDG error scale factor that was applied to error_positive and error_negative.""" return self['scale_factor'] or 1.0 @property def units(self): """Units (as a string) used by value, error_positive, error_negative, and display_value_text.""" return self['unit_text'] @property def display_value_text(self): """Value and uncertainty in plain text format in units given by property units.""" return self['display_value_text'] @property def display_power_of_ten(self): """Unit multiplier (as power of ten) as used for display in Summary Tables.""" return self['display_power_of_ten'] @property def display_in_percent(self): """True if value is rendered in percent for display in Summary Tables.""" return self['display_in_percent']
[docs] class PdgConvertedValue(PdgSummaryValue): """A PdgSummaryValue class for storing summary values after unit conversion.""" def __init__(self, value, to_units): """Instantiate a copy of PdgSummaryValue value with new units to_unit.""" super(PdgConvertedValue, self).__init__(value) self.original_units = value.units try: old_factor = UNIT_CONVERSION_FACTORS[self.original_units] except KeyError: raise PdgApiError('Cannot convert from %s' % self.original_units) try: new_factor = UNIT_CONVERSION_FACTORS[to_units] except KeyError: raise PdgApiError('Cannot convert to %s' % to_units) if old_factor[1] != new_factor[1]: raise PdgApiError('Illegal unit conversion from %s to %s', old_factor[1], new_factor[1]) conversion_factor = old_factor[0]/new_factor[0] for k in ('value', 'error_positive', 'error_negative', ): if self[k] is not None: self[k] *= conversion_factor for k in ('value_text', 'display_power_of_ten'): self[k] = None self['display_in_percent'] = False self['unit_text'] = to_units
[docs] class PdgData(object): """Base class for PDG data containers. This class implements the lazy data retrieval from the database and is the base class for all PDG data container classes. """ def __init__(self, api, pdgid, edition=None): """Instantiate a PdgData object for the given PDG Identifier pdgid. When a PdgData object is instantiated, the edition of the Review of Particle Physics from which data will be retrieved is determined by the first edition information found from the following list: 1. An edition specified as part of the PDG Identifier 2. An edition specified by parameter edition 3. The default edition specified by the database to which the API is connected The chosen edition can be queried by calling edition() and changed at any by calling set_edition(). """ self.api = api self.baseid, self._edition = parse_id(pdgid) if self._edition is None: self._edition = edition if self._edition is None: self._edition = self.api.edition self.pdgid = make_id(self.baseid, self._edition) self.cache = dict() def __str__(self): return 'Data for PDG Identifier %s: %s' % (self.pdgid, self.description) def __repr__(self): extra = self._repr_extra() if extra: extra = ', ' + extra return "%s('%s'%s)" % (self.__class__.__name__, make_id(self.baseid, self.edition), extra) def _repr_extra(self): """A method that subclasses can override in order to add info to the result of __repr__. """ return '' def _get_pdgid(self): """Get PDG Identifier information.""" if 'pdgid' not in self.cache: pdgid_table = self.api.db.tables['pdgid'] query = select(pdgid_table).where(pdgid_table.c.pdgid == bindparam('pdgid')) with self.api.engine.connect() as conn: try: self.cache['pdgid'] = conn.execute(query, {'pdgid': self.baseid}).fetchone()._mapping except AttributeError: raise PdgInvalidPdgIdError('PDG Identifier %s not found' % self.pdgid) return self.cache['pdgid'] def _get_summary_values(self): """Get all summary data values.""" if 'summary' not in self.cache: pdgid_table = self.api.db.tables['pdgid'] pdgdata_table = self.api.db.tables['pdgdata'] query = select(pdgdata_table, pdgid_table.c.description).join(pdgid_table) query = query.where(pdgid_table.c.pdgid == bindparam('pdgid')) query = query.where(pdgdata_table.c.edition == bindparam('edition')) query = query.order_by(pdgdata_table.c.sort) self.cache['summary'] = [] with self.api.engine.connect() as conn: for entry in conn.execute(query, {'pdgid': self.baseid, 'edition': self.edition}): self.cache['summary'].append(PdgSummaryValue(entry._mapping)) return self.cache['summary'] def _count_data_entries(self, pdgid, edition): """Count number of data entries for a given PDG identifier and edition.""" pdgdata_table = self.api.db.tables['pdgdata'] query = select(func.count("*")).select_from(pdgdata_table) query = query.where(pdgdata_table.c.pdgid == bindparam('pdgid')) query = query.where(pdgdata_table.c.edition == bindparam('edition')) with self.api.engine.connect() as conn: return conn.execute(query, {'pdgid': pdgid.upper(), 'edition': edition}).scalar()
[docs] def get_parent_pdgid(self, include_edition=True): """Return PDG Identifiers of parent quantity.""" if include_edition: return make_id(self._get_pdgid()['parent_pdgid'], self.edition) else: return self._get_pdgid()['parent_pdgid']
[docs] def get_particles(self): """Return PdgParticleList for this property's particle.""" p = self while p.baseid != p.get_parent_pdgid(False) and p.get_parent_pdgid(False): p = self.api.get(p.get_parent_pdgid()) if p.data_type != 'PART': err = 'Identifier %s does not have a parent particle' raise PdgNoDataError(err) return p
[docs] def get_particle(self): """Returns PdgParticle for this property's particle. Raises PdgAmbiguousValueError when there are multiple matches.""" ps = self.get_particles() assert len(ps) > 0 if len(ps) > 1: err = "More than one PdgParticle found. Consider using get_particles() instead." raise PdgAmbiguousValueError(err) return ps[0]
[docs] def get_children(self, recurse=False): pdgid_table = self.api.db.tables['pdgid'] ## NOTE: Querying on IDs doesn't work because the `parent_id` seems off # query = select(pdgid_table.c.pdgid) \ # .where(pdgid_table.c.parent_id == bindparam('parent_id')) # params = {'parent_id': self._get_pdgid()['id']} query = select(pdgid_table.c.pdgid) \ .where(pdgid_table.c.parent_pdgid == bindparam('parent_pdgid')) params = {'parent_pdgid': self.baseid} with self.api.engine.connect() as conn: child_pdgids = [row.pdgid for row in conn.execute(query, params)] for child_pdgid in child_pdgids: child = self.api.get(child_pdgid) yield child if recurse: for c in child.get_children(recurse=True): yield c
@property def edition(self): """Year of edition for which data is requested.""" return self._edition @edition.setter def edition(self, edition): """Set year of edition used for retrieving data (invalidates cache).""" self._edition = edition self.pdgid = make_id(self.baseid, self._edition) self.cache = dict() @property def description(self): """Description of data.""" return self._get_pdgid()['description'] @property def data_type(self): """Type of data.""" return self._get_pdgid()['data_type'] @property def data_flags(self): """Flags augmenting data type information.""" return self._get_pdgid()['flags']
[docs] class PdgProperty(PdgData): """Base class for containers for data containers for particle properties."""
[docs] def summary_values(self, summary_table_only=False): """Return list of summary values for this quantity. By default, all summary values are included, even if they are only shown in the Particle Listings and not listed in the Summary Tables. Set summary_table_only to True to get only summary values listed in the Summary Tables. """ if summary_table_only: return [v for v in self._get_summary_values() if v.in_summary_table] else: return self._get_summary_values()
[docs] def n_summary_table_values(self): """Return number of summary values in Summary Table for this quantity.""" return len(self.summary_values(summary_table_only=True))
[docs] def best_summary(self, summary_table_only=False): """Return the PDG "best" summary value for this quantity. If there is either a single summary value in Particle Listings and Summary Tables, or there are multiple summary values but only one is included in the Summary Tables, then this value is returned as the PDG best value. If there are multiple summary values (e.g. based on assuming or not assuming CPT in the evaluation) and PdgApi.pedantic is False, the first value shown in Summary Tables or Particle Listings will be returned. If PdgApi.pedantic is True, a PdgAmbiguousValue Exception will be raised. If there are no summary values, None is returned. If summary_table_only is True, then the best value must be included in the Summary Table and cannot be shown only in the Particle Listings. """ if not summary_table_only: summaries = self.summary_values(summary_table_only=False) if len(summaries) == 1: return summaries[0] else: return self.best_summary(summary_table_only=True) else: summaries = self.summary_values(summary_table_only=True) if len(summaries) == 1: return summaries[0] elif len(summaries) == 0: return None else: if self.api.pedantic: raise PdgAmbiguousValueError('%s (%s) has multiple summary values' % (self.pdgid, self.description)) else: return summaries[0]
[docs] def has_best_summary(self, summary_table_only=False): """Return True if there is a single PDG "best" value (see best_value() for definition).""" try: return self.best_summary(summary_table_only) is not None except PdgAmbiguousValueError: return False
@property def confidence_level(self): """Shortcut for best_summary().confidence_level.""" return self.best_summary().confidence_level @property def is_limit(self): """Shortcut for best_summary().is_limit.""" return self.best_summary().is_limit @property def value(self): """Shortcut for best_summary().value.""" return self.best_summary().value @property def error(self): """Shortcut for best_summary().error.""" return self.best_summary().error @property def error_positive(self): """Shortcut for best_summary().error.""" return self.best_summary().error_positive @property def error_negative(self): """Shortcut for best_summary().error.""" return self.best_summary().error_negative @property def scale_factor(self): """Shortcut for best_summary().scale_factor.""" return self.best_summary().scale_factor @property def units(self): """Shortcut for best_summary().units.""" return self.best_summary().units @property def comment(self): """Shortcut for best_summary().comment.""" return self.best_summary().comment @property def display_value_text(self): """Shortcut for best_summary().display_value_text.""" return self.best_summary().display_value_text
[docs] class PdgMass(PdgProperty): pass
[docs] class PdgWidth(PdgProperty): pass
[docs] class PdgLifetime(PdgProperty): pass