Source code for ebisim.elements

"""
This module most notably implements the Element class, which serves as the main container for
physical data going into the ebisim computations.

Besides that, there are some small helper functions to translate certain element properties,
which may offer convenience to the user.
"""
from __future__ import annotations

import logging
from typing import NamedTuple, Tuple, Union, Optional
import numpy as np

from .utils import load_dr_data, patch_namedtuple_docstrings
from .xs import precompute_rr_quantities, lookup_lotz_factors
from .physconst import MINIMAL_KBT, MINIMAL_N_1D, Q_E, K_B, PI

logger = logging.getLogger(__name__)

logger.debug("Loading element data and shell configurations.")
from .resources import (  # noqa: E402
    ELEMENT_Z as _ELEM_Z,
    ELEMENT_ES as _ELEM_ES,
    ELEMENT_NAME as _ELEM_NAME,
    ELEMENT_A as _ELEM_A,
    ELEMENT_IP as _ELEM_IP,
    SHELL_ORDER as _SHELLORDER,
    SHELL_N as _SHELL_N,
    SHELL_CFG as _SHELL_CFG,
    SHELL_EBIND as _SHELL_EBIND
)

logger.debug("Loading DR data.")
_DR_DATA = load_dr_data()


# ----- Helper functions for translating chemical symbols
[docs]def element_z(element: str) -> int: """ Returns the proton number of the given element. Parameters ---------- element : The full name or the abbreviated symbol of the element. Returns ------- Proton number """ if len(element) < 3: idx = _ELEM_ES.index(element) else: idx = _ELEM_NAME.index(element) return _ELEM_Z[idx]
[docs]def element_symbol(element: Union[str, int]) -> str: """ Returns the abbreviated symbol of the given element. Parameters ---------- element : The full name or the proton number of the element. Returns ------- Element symbol """ if isinstance(element, int): idx = _ELEM_Z.index(element) else: idx = _ELEM_NAME.index(element) return _ELEM_ES[idx]
[docs]def element_name(element: Union[str, int]) -> str: """ Returns the name of the given element. Parameters ---------- element : The abbreviated symbol or the proton number of the element. Returns ------- Element name """ if isinstance(element, int): idx = _ELEM_Z.index(element) else: idx = _ELEM_ES.index(element) return _ELEM_NAME[idx]
[docs]def element_identify(element_id: Union[str, int]) -> Tuple[int, str, str]: """ Returns the proton number, name, and element symbol relating to the supplied element_id. Parameters ---------- element_id : The proton number, name or symbol of a chemical element. Returns ------- (proton number, name, symbol) Raises ------ ValueError If the element_id could not be identified or found in the database. """ try: if isinstance(element_id, int): z = element_id else: z = element_z(element_id) symbol = element_symbol(z) name = element_name(z) except ValueError as e: raise ValueError(f"Unable to interpret element_id = {element_id}, " + "ebisim only supports elements up to Z = 105.") from e return z, name, symbol
[docs]class Element(NamedTuple): """ The Element class is one of the main data structures in ebisim. Virtually any function relies on information provided in this data structure. The leading fields of the underlying tuple contain physcial properties, whereas the `n` `kT` and `cx` fields are optional and only required for advanced simulations. Instead of populating the fields manually, the user should choose one of the factory functions that meets their needs best. For basic simulations and cross sections calculations only the physical / chemical properties of and element are needed. In these cases use the generic `get()` method to create instances of this class. Advanced simulations require additional information about the initial particle densities, temperature and participation in charge exchange. The user will likely want to chose between the `get_ions()` and `get_gas()` methods, which offer a convenient interface for generating this data based on simple parameters. If these functions are not flexible enough, the `get()` method can be used to populate the required fields manually. This class is derived from collections.namedtuple which facilitates use with numba-compiled functions. See Also -------- ebisim.elements.Element.get ebisim.elements.Element.get_ions ebisim.elements.Element.get_gas """ z: int symbol: str name: str a: float ip: float e_cfg: np.ndarray e_bind: np.ndarray rr_z_eff: np.ndarray rr_n_0_eff: np.ndarray dr_cs: np.ndarray dr_e_res: np.ndarray dr_strength: np.ndarray ei_lotz_a: np.ndarray ei_lotz_b: np.ndarray ei_lotz_c: np.ndarray n: Optional[np.ndarray] = None kT: Optional[np.ndarray] = None cx: bool = True
[docs] @classmethod def get(cls, element_id: Union[str, int], a: Optional[float] = None, n: Optional[np.ndarray] = None, kT: Optional[np.ndarray] = None, cx: bool = True) -> Element: """ Factory method to create instances of the Element class. Parameters ---------- element_id : The full name, abbreviated symbol, or proton number of the element of interest. a : If provided sets the mass number of the Element object otherwise a reasonable value is chosen automatically. n : <1/m> Only needed for advanced simulations! Array holding the initial ion line densities of each charge state. If provided, has to be an array of length Z+1, where Z is the nuclear charge. kT : <eV> Only needed for advanced simulations! Array holding the initial ion line densities of each charge state. If provided, has to be an array of length Z+1, where Z is the nuclear charge. cx : Only needed for advanced simulations! Boolean flag determining whether the neutral particles of this element contribute to charge exchange with ions. Returns ------- An instance of Element with the user-supplied and generated data. Raises ------ ValueError If the Element could not be identified or a meaningless mass number is provided. ValueError If the passed arrays for `n` or `kT` have the wrong shape. """ # Basic element info z, name, symbol = element_identify(element_id) # Mass number and ionisation potential idx = _ELEM_Z.index(z) ip = float(_ELEM_IP[idx]) if a is None: a = _ELEM_A[idx] if a <= 0: raise ValueError("Mass number 'a' cannot be smaller than or equal to 0.") # Electron configuration and shell binding energies e_cfg = _SHELL_CFG[z].copy() e_bind = _SHELL_EBIND[z].copy() # Precomputations for radiative recombination rr_z_eff, rr_n_0_eff = precompute_rr_quantities(e_cfg, _SHELL_N) # Data for computations of dielectronic recombination cross sections dr_cs = _DR_DATA[z]["dr_cs"].copy() dr_e_res = _DR_DATA[z]["dr_e_res"].copy() dr_strength = _DR_DATA[z]["dr_strength"].copy() # Precompute the factors for the Lotz formula for EI cross section ei_lotz_a, ei_lotz_b, ei_lotz_c = lookup_lotz_factors(e_cfg, _SHELLORDER) # Validate n and kT if n is not None: if len(n) != z+1: raise ValueError(f"n has the wrong shape {n.shape} for an element with z = {z}.") if np.any(n < MINIMAL_N_1D): n = np.maximum(n, MINIMAL_N_1D) logger.warning( "One or more entries of 'n' were smaller than the minimal internal value.\n" + f"Entries smaller than {MINIMAL_N_1D} were raised to that value." ) if kT is not None: if len(kT) != z+1: raise ValueError(f"kT has the wrong shape {kT.shape} for an element with z = {z}.") if np.any(kT < MINIMAL_KBT): kT = np.maximum(kT, MINIMAL_KBT) logger.warning( "One or more entries of 'kT' were smaller than the minimal internal value.\n" + f"Entries smaller than {MINIMAL_KBT} were raised to that value." ) # Make sure that all arrays are readonly - better safe than sorry arrays = [ e_cfg, e_bind, rr_z_eff, rr_n_0_eff, dr_cs, dr_e_res, dr_strength, ei_lotz_a, ei_lotz_b, ei_lotz_c, n, kT, ] for arr in arrays: if arr is not None: arr.setflags(write=False) return cls( z=z, symbol=symbol, name=name, a=a, ip=ip, e_cfg=e_cfg, e_bind=e_bind, rr_z_eff=rr_z_eff, rr_n_0_eff=rr_n_0_eff, dr_cs=dr_cs, dr_e_res=dr_e_res, dr_strength=dr_strength, ei_lotz_a=ei_lotz_a, ei_lotz_b=ei_lotz_b, ei_lotz_c=ei_lotz_c, n=n, kT=kT, cx=cx, )
[docs] @classmethod def get_gas(cls, element_id: Union[str, int], p: float, r_dt: float, T: float = 300.0, cx: bool = True, a: Optional[float] = None) -> Element: """ Factory method for defining a neutral gas injection target. A gas target is a target with constant density in charge state 0. Parameters ---------- element_id : The full name, abbreviated symbol, or proton number of the element of interest. p : <mbar> Gas pressure. r_dt : <m> Drift tube radius, required to compute linear density from volumetric density. T : <K> Gas temperature, by default 300 K (approx. room temperature) cx : Boolean flag determining whether the neutral particles of this element contribute to charge exchange with ions. a : If provided sets the mass number of the Element object otherwise a reasonable value is chosen automatically. Returns ------- Element instance with automatically populated `n` and `kT` fields. Raises ------ ValueError If the density resulting from the pressure and temperature is smaller than the internal minimal value. """ z, _name, _symbol = element_identify(element_id) _n = np.full(z + 1, MINIMAL_N_1D, dtype=np.float64) _kT = np.full(z + 1, MINIMAL_KBT, dtype=np.float64) _n[0] = (p * 100) / (K_B * T) * PI * r_dt**2 # Convert from mbar to Pa and compute density if _n[0] < MINIMAL_N_1D: raise ValueError("The resulting density is smaller than the internal minimal value.") _kT[0] = K_B * T / Q_E return cls.get(element_id, a=a, n=_n, kT=_kT, cx=cx)
[docs] @classmethod def get_ions(cls, element_id: Union[str, int], nl: float, kT: float = 10.0, q: int = 1, cx: bool = True, a: Optional[float] = None) -> Element: """ Factory method for defining a pulsed ion injection target. An ion target has a given density in the charge state of choice q. Parameters ---------- element_id : The full name, abbreviated symbol, or proton number of the element of interest. nl : <1/m> Linear density of the initial charge state (ions per unit length). kT : <eV> Temperature / kinetic energy of the injected ions. q : Initial charge state. cx : Boolean flag determining whether the neutral particles of this element contribute to charge exchange with ions. a : If provided sets the mass number of the Element object otherwise a reasonable value is chosen automatically. Returns ------- Element instance with automatically populated `n` and `kT` fields. Raises ------ ValueError If the requested density is smaller than the internal minimal value. """ if nl < MINIMAL_N_1D: raise ValueError("The requested density is smaller than the internal minimal value.") z, _name, _symbol = element_identify(element_id) _n = np.full(z + 1, MINIMAL_N_1D, dtype=np.float64) _kT = np.full(z + 1, MINIMAL_KBT, dtype=np.float64) _n[q] = nl _kT[q] = kT return cls.get(element_id, a=a, n=_n, kT=_kT, cx=cx)
[docs] @classmethod def as_element(cls, element: Union[Element, str, int]) -> Element: """ If `element` is already an instance of `Element` it is returned. If `element` is a string or int identyfying an element an appropriate `Element` instance is returned. Parameters ---------- element : An instance of the Element class, or an identifier for the element, i.e. either its name, symbol or proton number. Returns ------- An instance of Element reflecting the input value. """ if isinstance(element, cls): return element elif isinstance(element, (str, int)): return cls.get(element) else: raise TypeError("Could not convert {element} to ebisim.Element")
[docs] def latex_isotope(self) -> str: """ Returns the isotope as a LaTeX formatted string. Returns ------- str LaTeX formatted string describing the isotope. """ return fr"$\mathsf{{^{{{self.a}}}_{{{self.z}}}{self.symbol}}}$"
def __str__(self) -> str: return f"Element: {self.name} ({self.symbol}, Z = {self.z}, A = {self.a})"
_ELEMENT_DOC = dict( z="Atomic number", symbol="Element symbol e.g. H, He, Li", name="Element name", a="Mass number / approx. mass in proton masses", ip="Ionisation potential", e_cfg=f"""\ Numpy array of electron configuration in different charge states. The index of each row corresponds to the charge state. The columns are the subshells sorted as in {_SHELLORDER}.""", e_bind=f"""\ Numpy array of binding energies associated with electron subshells. The index of each row corresponds to the charge state. The columns are the subshells sorted as in {_SHELLORDER}.""", rr_z_eff="Numpy array of effective nuclear charges for RR cross sections.", rr_n_0_eff="Numpy array of effective valence shell numbers for RR cross sections.", dr_cs="Numpy array of charge states for DR cross sections.", dr_e_res="Numpy array of resonance energies for DR cross sections.", dr_strength="Numpy array of transition strengths for DR cross sections.", ei_lotz_a="Numpy array of precomputed Lotz factor 'a' for each entry of 'e_cfg'.", ei_lotz_b="Numpy array of precomputed Lotz factor 'b' for each entry of 'e_cfg'.", ei_lotz_c="Numpy array of precomputed Lotz factor 'c' for each entry of 'e_cfg'.", n="<1/m> Array holding the initial linear density of each charge state.", kT="<eV> Array holding the initial temperature of each charge state.", cx="""\ Boolean flag determining whether neutral particles of this target are considered as charge exchange partners.""", ) patch_namedtuple_docstrings(Element, _ELEMENT_DOC)
[docs]def get_element(element_id: Union[str, int], a: Optional[float] = None) -> Element: """ [LEGACY] Factory function to create instances of the Element class. Parameters ---------- element_id : str or int The full name, abbreviated symbol, or proton number of the element of interest. a : int or None, optional If provided sets the (isotopic) mass number of the Element object otherwise a reasonable value is chosen automatically, by default None. Returns ------- ebisim.elements.Element An instance of Element with the physical data corresponding to the supplied element_id, and optionally mass number. Raises ------ ValueError If the Element could not be identified or a meaningless mass number is provided. """ return Element.get(element_id, a)