"""
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.
Parameters
----------
name : str
name of the nucleus (e.g. "c12")
dummy : bool
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
Attributes
----------
Z : float
atomic number
N : float
neutron number
A : float
atomic mass
short_spec_name : str
nucleus abbreviation (e.g. "he4")
caps_name : str
capitalized short species name (e.g. "He4")
el : str
element name (e.g. "he")
pretty : str
LaTeX formatted version of the nucleus name
dm : float
mass excess (MeV)
nucbind : float
nuclear binding energy (MeV / nucleon)
A_nuc : float
nuclear mass (amu)
mass : float
nuclear mass (MeV)
tau : float
half life (s)
spin_states : int
the ground state spin
partition_function : PartitionFunction
the `PartitionFunction` object for this nucleus, which
allows for the evaluation of the temperature-dependent
partition function.
dummy : bool
is this a dummy nucleus
nse : bool
an NSE proton has the same properties
as a proton but compares as being distinct
"""
_cache = {}
def __init__(self, name, dummy=False):
name = name.lower()
self.raw = name
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:
# Note: for Nubase 2020, we need to use the CODATA 18 constants
mass_H = _mass_table.get_mass_diff(a=1, z=1) + constants.m_u_MeV_C18
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_C18
self.mass = self.A * constants.m_u_MeV_C18 + self.dm
B = (self.Z * mass_H + self.N * constants.m_n_MeV_C18) - 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):
"""Check if we've already created this nucleus, and if so,
return a reference to it from the cache.
Parameters
----------
name : str
name of the nucleus (e.g. "c12")
dummy : bool
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
Returns
-------
Nucleus
"""
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):
"""Create a nucleus given Z and A
Parameters
----------
Z : int
atomic number
A : int
atomic weight
dummy : bool
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
Returns
-------
Nucleus
"""
# 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):
"""Create a Nucleus from a string
Parameters
----------
obj : str or Nucleus
the object to cast. If it is a `Nucleus`, then we simply return
it.
Returns
-------
Nucleus
"""
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):
"""Convert a list of objects into a list of Nucleus objects
Parameters
----------
lst : list
a list of `str` or `Nucleus`
allow_None : bool
allow lst = None and simply return None
allow_single : bool
allow lst to be a single `str` or `Nucleus` instead
of a `list`
Returns
-------
list
"""
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
Parameters
----------
zmin : int
minimum atomic number
zmax : int
maximum atomic number
amin : int
minimum atomic weight
amax : int
maximum atomic weight
Returns
-------
list
"""
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 with every Nucleus that has a known mass
Returns
-------
list
"""
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