#!/usr/bin/env python3
"""
XRF support functions.
@author: Nicola VIGANĂ’, ESRF - The European Synchrotron, Grenoble, France,
and CEA-IRIG, Grenoble, France
"""
try:
from . import xraylib_helper # noqa: F401, F402
xraylib = xraylib_helper.xraylib
except ImportError:
print("WARNING: Physics support is only available when xraylib is installed!")
raise
from collections.abc import Sequence
from dataclasses import dataclass
from typing import overload, Literal, Union
import numpy as np
from numpy.typing import NDArray
[docs]
@dataclass
class FluoLine:
"""Fluorescence line description class."""
name: str
indx: int
[docs]
class LinesSiegbahn:
"""Siegbahn fluorescence lines collection class."""
lines = [
FluoLine(name="KA1", indx=xraylib.KA1_LINE),
FluoLine(name="KA2", indx=xraylib.KA2_LINE),
FluoLine(name="KA3", indx=xraylib.KA3_LINE),
FluoLine(name="KB1", indx=xraylib.KB1_LINE),
FluoLine(name="KB2", indx=xraylib.KB2_LINE),
FluoLine(name="KB3", indx=xraylib.KB3_LINE),
FluoLine(name="KB4", indx=xraylib.KB4_LINE),
FluoLine(name="KB5", indx=xraylib.KB5_LINE),
FluoLine(name="LA1", indx=xraylib.LA1_LINE),
FluoLine(name="LA2", indx=xraylib.LA2_LINE),
FluoLine(name="LB1", indx=xraylib.LB1_LINE),
FluoLine(name="LB2", indx=xraylib.LB2_LINE),
FluoLine(name="LB3", indx=xraylib.LB3_LINE),
FluoLine(name="LB4", indx=xraylib.LB4_LINE),
FluoLine(name="LB5", indx=xraylib.LB5_LINE),
FluoLine(name="LB6", indx=xraylib.LB6_LINE),
FluoLine(name="LB7", indx=xraylib.LB7_LINE),
FluoLine(name="LB9", indx=xraylib.LB9_LINE),
FluoLine(name="LB10", indx=xraylib.LB10_LINE),
FluoLine(name="LB15", indx=xraylib.LB15_LINE),
FluoLine(name="LB17", indx=xraylib.LB17_LINE),
FluoLine(name="LG1", indx=xraylib.LG1_LINE),
FluoLine(name="LG2", indx=xraylib.LG2_LINE),
FluoLine(name="LG3", indx=xraylib.LG3_LINE),
FluoLine(name="LG4", indx=xraylib.LG4_LINE),
FluoLine(name="LG5", indx=xraylib.LG5_LINE),
FluoLine(name="LG6", indx=xraylib.LG6_LINE),
FluoLine(name="LG8", indx=xraylib.LG8_LINE),
FluoLine(name="LE", indx=xraylib.LE_LINE),
FluoLine(name="LH", indx=xraylib.LH_LINE),
FluoLine(name="LL", indx=xraylib.LL_LINE),
FluoLine(name="LS", indx=xraylib.LS_LINE),
FluoLine(name="LT", indx=xraylib.LT_LINE),
FluoLine(name="LU", indx=xraylib.LU_LINE),
FluoLine(name="LV", indx=xraylib.LV_LINE),
FluoLine(name="MA1", indx=xraylib.MA1_LINE),
FluoLine(name="MA2", indx=xraylib.MA2_LINE),
FluoLine(name="MB", indx=xraylib.MB_LINE),
FluoLine(name="MG", indx=xraylib.MG_LINE),
]
[docs]
@staticmethod
def get_lines(line: str) -> Sequence[FluoLine]:
"""
Return the list of xraylib line macro definitions for the requested family.
Parameters
----------
line : str
The requested line. It can be a whole shell (transition to that shell),
or sub-shells.
Returns
-------
Sequence
List of corresponding lines.
"""
return [f for f in LinesSiegbahn.lines if f.name[: len(line)] == line.upper()]
def _get_lines_list(lines) -> Sequence[FluoLine]:
if isinstance(lines, FluoLine):
return [lines]
elif isinstance(lines, str):
return LinesSiegbahn.get_lines(lines)
elif len(lines) == 0:
raise ValueError(f"No line was passed! lines={lines}")
else:
return lines
[docs]
def get_radiation_rate(
element: Union[str, int],
lines: Union[str, FluoLine, Sequence[FluoLine]],
verbose: bool = False,
) -> NDArray:
"""Return the radiation rates of the requested lines for the given element.
Parameters
----------
element : Union[str, int]
The requested element
lines : Union[str, FluoLine, Sequence[FluoLine]]
The requested line. It can be a whole shell (transition to that shell),
or sub-shells.
verbose : bool, optional
Whether to produce verbose output in case of errors, by default False
Returns
-------
NDArray
The list of radiation rates
"""
el_sym, el_num = xraylib_helper.get_element_number_and_symbol(element)
lines_list = _get_lines_list(lines)
rates = np.empty(len(lines_list), dtype=np.float32)
for ii, line in enumerate(lines_list):
try:
rates[ii] = xraylib.RadRate(el_num, line.indx)
except ValueError as exc:
if verbose:
print(f"INFO - RadRate - {exc}: el_num={el_num} ({el_sym}) line={line}")
rates[ii] = 0
return rates
@overload
def get_energy(
element: Union[str, int],
lines: Union[str, FluoLine, Sequence[FluoLine]],
*,
compute_average: Literal[False] = False,
verbose: bool = False,
) -> NDArray: ...
@overload
def get_energy(
element: Union[str, int],
lines: Union[str, FluoLine, Sequence[FluoLine]],
*,
compute_average: Literal[True] = True,
verbose: bool = False,
) -> float: ...
[docs]
def get_energy(
element: Union[str, int],
lines: Union[str, FluoLine, Sequence[FluoLine]],
*,
compute_average: bool = False,
verbose: bool = False,
) -> Union[float, NDArray]:
"""
Return the energy(ies) of the requested line for the given element.
Parameters
----------
element : Union[str, int]
The requested element.
line : str
The requested line. It can be a whole shell (transition to that shell),
or sub-shells.
compute_average : bool, optional
Weighted averaging the lines, using the radiation rate. The default is False.
Returns
-------
energy_keV : Union[float, NDArray]
Either the average energy or the list of different energies.
"""
el_sym, el_num = xraylib_helper.get_element_number_and_symbol(element)
lines_list = _get_lines_list(lines)
energy_keV = np.empty(len(lines_list), dtype=np.float32)
for ii, line in enumerate(lines_list):
try:
energy_keV[ii] = xraylib.LineEnergy(el_num, line.indx)
except ValueError as exc:
if verbose:
print(f"INFO - Energy - {exc}: el_num={el_num} ({el_sym}) line={line}")
energy_keV[ii] = 0
if compute_average:
rates = get_radiation_rate(element, lines_list)
energy_keV = float(np.sum(energy_keV * rates / np.sum(rates)))
if verbose:
print(f"{el_sym}-{lines} emission energy (keV):", energy_keV, "\n")
return energy_keV
[docs]
@dataclass
class DetectorXRF:
"""Simple XRF detector model."""
surface_mm2: float
distance_mm: Union[float, NDArray]
angle_rad: float = np.pi / 2
@property
def solid_angle_sr(self) -> Union[float, NDArray]:
"""Compute the solid angle covered by the detector.
Returns
-------
float | NDArray
The computed solid angle of the detector geometry.
"""
return self.surface_mm2 / (4 * np.pi * self.distance_mm**2)