Source code for fiasco.levels

"""
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()