Source code for pynucastro.nucdata.nucleus

"""
Classes and methods to interface with files storing rate data.
"""

import re
from pathlib import Path

from pynucastro.constants import constants
from pynucastro.nucdata.elements import PeriodicTable, UnidentifiedElement
from pynucastro.nucdata.halflife_table import HalfLifeTable
from pynucastro.nucdata.mass_table import MassTable
from pynucastro.nucdata.partition_function import PartitionFunctionCollection
from pynucastro.nucdata.spin_table import SpinTable

_pynucastro_dir = Path(__file__).parents[1]
_pynucastro_rates_dir = _pynucastro_dir/'library'
_pynucastro_tabular_dir = _pynucastro_rates_dir/'tabular'

# read the various tables with nuclear properties at the module-level
_mass_table = MassTable()
_halflife_table = HalfLifeTable()
_spin_table = SpinTable(reliable=True)

# read the partition function table once and store it at the module-level
_pcollection = PartitionFunctionCollection(use_high_temperatures=True, use_set='frdm')


[docs] class UnsupportedNucleus(Exception): pass
[docs] class Nucleus: """ a nucleus that participates in a reaction -- we store it in a class to hold its properties, define a sorting, and give it a pretty printing string. :var Z: atomic number :var N: neutron number :var A: atomic mass :var nucbind: nuclear binding energy (MeV / nucleon) :var short_spec_name: nucleus abbreviation (e.g. "he4") :var caps_name: capitalized short species name (e.g. "He4") :var el: element name (e.g. "he") :var pretty: LaTeX formatted version of the nucleus name :var dm: mass excess (MeV) :var A_nuc: nuclear mass (amu) :var mass: nuclear mass (MeV) :var tau: half life (s) """ _cache = {} def __init__(self, name, dummy=False): name = name.lower() self.raw = name # a dummy nucleus is one that we can use where a nucleus is needed # but it is not considered to be part of the network self.dummy = dummy self.nse = False # element symbol and atomic weight if name == "p": self.el = "h" self.A = 1 self.short_spec_name = "h1" self.caps_name = "p" elif name == "d": self.el = "h" self.A = 2 self.short_spec_name = "h2" self.caps_name = "H2" elif name == "t": self.el = "h" self.A = 3 self.short_spec_name = "h3" self.caps_name = "H3" elif name == "a": #this is a convenience, enabling the use of a commonly-used alias: # He4 --> \alpha --> "a" , e.g. c12(a,g)o16 self.el = "he" self.A = 4 self.short_spec_name = "he4" self.raw = "he4" self.caps_name = "He4" elif name == "n": self.el = "n" self.A = 1 self.Z = 0 self.N = 1 self.short_spec_name = "n" self.spec_name = "neutron" self.pretty = fr"\mathrm{{{self.el}}}" self.caps_name = "n" elif name == "p_nse": # this is a proton with a different name # it is meant to be used in iron-group rates # in NSE self.el = "h" self.A = 1 self.Z = 1 self.N = 0 self.short_spec_name = "p_nse" self.spec_name = "proton-nse" self.pretty = r"\mathrm{p}_\mathrm{NSE}" self.caps_name = "p_NSE" self.nse = True elif name.strip() in ("al-6", "al*6"): raise UnsupportedNucleus() else: e = re.match(r"([a-zA-Z]*)(\d*)", name) self.el = e.group(1).title() # chemical symbol assert self.el self.A = int(e.group(2)) assert self.A >= 0 self.short_spec_name = name self.caps_name = name.capitalize() # use lowercase element abbreviation regardless the case of the input self.el = self.el.lower() # atomic number comes from periodic table if name not in ["n", "p_nse"]: i = PeriodicTable.lookup_abbreviation(self.el) self.Z = i.Z assert isinstance(self.Z, int) assert self.Z >= 0 self.N = self.A - self.Z assert isinstance(self.N, int) assert self.N >= 0 # long name self.spec_name = f'{i.name}-{self.A}' # latex formatted style self.pretty = fr"{{}}^{{{self.A}}}\mathrm{{{self.el.capitalize()}}}" # set the number of spin states try: self.spin_states = _spin_table.get_spin_states(a=self.A, z=self.Z) except NotImplementedError: self.spin_states = None # set a partition function object to every nucleus try: self.partition_function = _pcollection.get_partition_function(self.short_spec_name) except ValueError: self.partition_function = None # nuclear mass try: mass_H = _mass_table.get_mass_diff(a=1, z=1) + constants.m_u_MeV self.dm = _mass_table.get_mass_diff(a=self.A, z=self.Z) self.A_nuc = float(self.A) + self.dm / constants.m_u_MeV self.mass = self.A * constants.m_u_MeV + self.dm B = (self.Z * mass_H + self.N * constants.m_n_MeV) - self.mass self.nucbind = B / self.A except NotImplementedError: self.dm = None self.A_nuc = None self.mass = None self.nucbind = None # halflife try: self.tau = _halflife_table.get_halflife(a=self.A, z=self.Z) except NotImplementedError: self.tau = None
[docs] @classmethod def from_cache(cls, name, dummy=False): key = (name.lower(), dummy) if key not in cls._cache: cls._cache[key] = cls(name, dummy) return cls._cache[key]
[docs] @classmethod def from_Z_A(cls, Z, A, dummy=False): """creates a nucleus given Z and A""" # checks if Z and A are valid inputs if not (isinstance(Z, int) and isinstance(A, int)): raise TypeError("Nucleus Z and A must be integers") if not (Z >= 0 and A >= 0): raise ValueError("Nucleus Z and A must be non-negative") if Z > A: raise ValueError("Nucleus Z can't be bigger than A") # checks if neutron if (Z, A) == (0, 1): return cls.from_cache("n", dummy) # otherwise, finds element Z on the periodic table i = PeriodicTable.lookup_Z(Z) if i is None: raise UnidentifiedElement(f"Element {Z} could not be found") name = i.abbreviation + str(A) return cls.from_cache(name, dummy)
def __repr__(self): if self.raw not in ("p", "p_nse", "d", "t", "n"): return self.raw.capitalize() return self.raw def __hash__(self): return hash((self.Z, self.A))
[docs] def c(self): """return the capitalized-style name""" return self.caps_name
[docs] def cindex(self): """return the name for C++ indexing""" return self.short_spec_name.capitalize()
def __eq__(self, other): if isinstance(other, Nucleus): return (self.el == other.el and self.Z == other.Z and self.A == other.A and self.nse == other.nse) if isinstance(other, tuple): return (self.Z, self.A) == other return NotImplemented def __lt__(self, other): if not self.Z == other.Z: return self.Z < other.Z return self.A < other.A def __add__(self, other): Z = self.Z + other.Z A = self.A + other.A dummy = self.dummy and other.dummy return Nucleus.from_Z_A(Z, A, dummy) def __sub__(self, other): Z = self.Z - other.Z A = self.A - other.A dummy = self.dummy and other.dummy return Nucleus.from_Z_A(Z, A, dummy)
[docs] @classmethod def cast(cls, obj): if isinstance(obj, cls): return obj if isinstance(obj, str): return cls.from_cache(obj) raise TypeError("Invalid type passed to Nucleus.cast() (expected str or Nucleus)")
[docs] @classmethod def cast_list(cls, lst, *, allow_None=False, allow_single=False): if allow_None and lst is None: return lst if isinstance(lst, (str, cls)): if allow_single: return [cls.cast(lst)] raise ValueError("Single object passed to Nucleus.cast_list() instead of list") return [cls.cast(obj) for obj in lst]
[docs] def get_nuclei_in_range(zmin, zmax, amin, amax): """given a range of Z = [zmin, zmax], and A = [amin, amax], return a list of Nucleus objects for all nuclei in this range""" nuc_list = [] assert zmax >= zmin, "zmax must be >= zmin" assert amax >= amin, "amax must be >= amin" for z in range(zmin, zmax+1): element = PeriodicTable.lookup_Z(z) for a in range(amin, amax+1): name = f"{element.abbreviation}{a}" nuc_list.append(Nucleus(name)) return nuc_list
[docs] def get_all_nuclei(): """Return a list will every Nucleus that has a known mass""" nuc_list = [] for (A, Z) in _mass_table.mass_diff: if Z == 0 and A == 1: nuc = "n" else: el = PeriodicTable.lookup_Z(Z) nuc = f"{el.abbreviation}{A}" nuc_list.append(Nucleus(nuc)) return nuc_list