"""
Energy level and transitions classes
"""
import astropy.units as u
import numpy as np
from fractions import Fraction
from fiasco.util import vectorize_where
__all__ = ['Levels', 'Transitions']
_ORBITAL_ANGULAR_MOMENTUM_LABELS = [
'S','P','D','F','G','H','I','K','L','M','N','O','Q','R','T','U','V','W','X','Y'
]
_ORBITAL_ANGULAR_MOMENTUM_LOOKUP = {
l:i for i,l in enumerate(_ORBITAL_ANGULAR_MOMENTUM_LABELS)
}
[docs]
class Levels:
"""
Data associated with all energy levels of an ion.
.. warning:: This is not meant to be instantiated directly,
but rather accessed by indexing a `fiasco.Ion`
object.
Parameters
----------
elvlc: `~fiasco.io.datalayer.DataIndexer`
Pointer to the energy level information for a given ion in
the CHIANTI database.
index: `int`, `slice`, or array-like, optional
Index or slice for which to extract energy level data.
"""
def __init__(self, elvlc, index=None):
self._elvlc = elvlc
self._idx = np.s_[:] if index is None else index
def __len__(self):
try:
return self.level.shape[0]
except IndexError:
return 0
def __getitem__(self, index):
# NOTE: This throws an IndexError to stop iteration
_ = self.level[index]
return type(self)(self._elvlc, index=index)
def __repr__(self):
return f"""Level: {self.level}
Configuration: {self.configuration}
Spin: {self.spin}
Total Angular Momentum: {self.total_angular_momentum}
Orbital Angular Momentum: {self.orbital_angular_momentum_label}
Energy: {self.energy}"""
@property
def level(self):
"Index of each level."
return self._elvlc['level'][self._idx]
@property
def configuration(self):
"Label denoting the electronic configuration."
configuration = self._elvlc['config'][self._idx]
# NOTE: This conditional is necessary because np.char.replace does not
# handle empty arrays.
if configuration.size == 0:
return configuration
return np.char.replace(
configuration,
".",
" "
)
@property
def multiplicity(self):
"Multiplicity, :math:`2S+1`"
return self._elvlc['multiplicity'][self._idx]
@property
@u.quantity_input
def spin(self) -> u.dimensionless_unscaled:
"Spin, :math:`S`, of the electronic configuration."
return (self.multiplicity - 1)/2
@property
def total_angular_momentum(self):
"Total angular momentum number :math:`J`."
return self._elvlc['J'][self._idx]
@property
def label(self):
"""
Label denoting level configuration, multiplicity, angular momentum label,
and total angular momentum.
"""
zipped = zip(np.atleast_1d(self.configuration),
np.atleast_1d(self.multiplicity),
np.atleast_1d(self.orbital_angular_momentum_label),
np.atleast_1d(self.total_angular_momentum),
)
return np.array([f"{i} {j}{k}{Fraction(l.value)}" for i,j,k,l in zipped])
@property
def weight(self):
"Statistical weight, :math:`2J + 1`."
return 2*self.total_angular_momentum + 1
@property
def orbital_angular_momentum_label(self):
"Orbital angular momentum label."
return self._elvlc['L_label'][self._idx]
@property
@u.quantity_input
def orbital_angular_momentum(self) -> u.dimensionless_unscaled:
"Orbital angular momentum number :math:`L`."
return np.array(
[_ORBITAL_ANGULAR_MOMENTUM_LOOKUP[l]
for l in self.orbital_angular_momentum_label]
).squeeze()
@property
def is_observed(self):
"True if the energy of the level is from laboratory measurements."
return self._elvlc['E_obs'][self._idx].to_value('cm-1') != -1
@property
@u.quantity_input
def energy(self) -> u.eV:
"""
Energy of each level. Defaults to observed energy and falls back to
theoretical energy if no measured energy is available.
"""
energy = np.where(self.is_observed,
self._elvlc['E_obs'][self._idx],
self._elvlc['E_th'][self._idx])
return energy.to('eV', equivalencies=u.equivalencies.spectral())
[docs]
class Transitions:
"""
An object for holding atomic transition data from CHIANTI.
.. warning:: This is not meant to be instantiated directly,
but rather accessed through `fiasco.Ion.transitions`.
Parameters
----------
levels: `~fiasco.Levels`
Data structure holding information about all of the energy levels
for a given ion in the CHIANTI database.
wgfa: `~fiasco.io.datalayer.DataIndexer`
Table of transition information for a given ion in
the CHIANTI database.
n_levels: `int`, optional
Maximum number of levels in the CHIANTI atomic model. This is used to
appropriately limit the transition information to the size of the atomic
model. Typically, this is calculated by `fiasco.Ion.n_levels`.
"""
def __init__(self, levels, wgfa, n_levels=None):
self._levels = levels
self._wgfa = wgfa
if n_levels is None:
self._idx = np.s_[:]
else:
# NOTE: For some ions, there may be more rate data available than
# there are levels in the model.
self._idx = np.where(np.logical_and(
self._wgfa['lower_level']<=n_levels,
self._wgfa['upper_level']<=n_levels,
))
def __len__(self):
return self.wavelength.shape[0]
@property
def is_twophoton(self):
"""
True if the transition is a two-photon decay
"""
return np.logical_and(self.wavelength == 0, self.upper_level<=10)
@property
def is_autoionization(self):
"""
True if the transition corresponds to an autoionization.
"""
return np.logical_and(self.wavelength==0, ~self.is_twophoton)
@property
def is_bound_bound(self):
"""
True for bound-bound transitions.
"""
return self._wgfa['wavelength'][self._idx] != 0
@property
def is_observed(self):
"""
True for transitions that connect two observed energy levels
"""
return self._wgfa['wavelength'][self._idx] > 0
@property
@u.quantity_input
def A(self) -> u.s**(-1):
"""
Spontaneous transition probability due to radiative decay
"""
return self._wgfa['A'][self._idx]
@property
@u.quantity_input
def wavelength(self) -> u.angstrom:
"Wavelength of each transition."
return np.fabs(self._wgfa['wavelength'][self._idx])
@property
def upper_level(self):
"Index of the upper level of the transition."
return self._wgfa['upper_level'][self._idx]
@property
def upper_configuration(self):
"Configuration of the upper level of the transition."
idx = vectorize_where(self._levels.level, self.upper_level)
configuration = self._levels.configuration[idx]
return configuration
@property
def upper_label(self):
"Label of the upper level of the transition."
idx = vectorize_where(self._levels.level, self.upper_level)
return self._levels.label[idx]
@property
def lower_level(self):
"Index of the lower level of the transition."
return self._wgfa['lower_level'][self._idx]
@property
def lower_configuration(self):
"Configuration of the lower level of the transition."
idx = vectorize_where(self._levels.level, self.lower_level)
configuration = self._levels.configuration[idx]
return configuration
@property
def lower_label(self):
"Label of the lower level of the transition."
idx = vectorize_where(self._levels.level, self.lower_level)
return self._levels.label[idx]
@property
def label(self):
"Labels of upper and lower energy levels for each transition."
return self.upper_label + ' -- ' + self.lower_label
@property
@u.quantity_input
def delta_energy(self) -> u.eV:
"Energy spacing between the upper and lower level of the transition."
indices = np.vstack([vectorize_where(self._levels.level, self.lower_level),
vectorize_where(self._levels.level, self.upper_level)])
return np.diff(self._levels.energy[indices], axis=0).squeeze()