"""XRayResult dataclass for X-ray optical property calculations."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
import warnings
import numpy as np
if TYPE_CHECKING:
from xraylabtool.typing_extensions import (
EnergyArray,
OpticalConstantArray,
WavelengthArray,
)
[docs]
@dataclass
class XRayResult:
"""
Dataclass containing complete X-ray optical property calculations for a material.
This comprehensive data structure holds all computed X-ray properties including
fundamental scattering factors, optical constants, and derived quantities like
critical angles and attenuation lengths. All fields use descriptive snake_case
names with clear units for maximum clarity.
The dataclass is optimized for scientific workflows, supporting both single-energy
calculations and energy-dependent analysis. All array fields are automatically
converted to numpy arrays for efficient numerical operations.
**Legacy Compatibility:**
Deprecated CamelCase property aliases are available for backward compatibility
but emit DeprecationWarning when accessed. Use the new snake_case field names
for all new code.
Attributes:
Material Properties:
formula (str): Chemical formula string exactly as provided
molecular_weight_g_mol (float): Molecular weight in g/mol
total_electrons (float): Total electrons per molecule (sum over all atoms)
density_g_cm3 (float): Mass density in g/cm³
electron_density_per_ang3 (float): Electron density in electrons/ų
X-ray Energy and Wavelength:
energy_kev (np.ndarray): X-ray photon energies in keV
wavelength_angstrom (np.ndarray): Corresponding X-ray wavelengths in Å
Fundamental X-ray Properties:
dispersion_delta (np.ndarray): Dispersion coefficient δ (real part of
refractive index decrement: n = 1 - δ - iβ)
absorption_beta (np.ndarray): Absorption coefficient β (imaginary part of
refractive index decrement)
scattering_factor_f1 (np.ndarray): Real part of atomic scattering factor
scattering_factor_f2 (np.ndarray): Imaginary part of atomic scattering factor
Derived Optical Properties:
critical_angle_degrees (np.ndarray): Critical angles for total external
reflection in degrees (θc = √(2δ))
attenuation_length_cm (np.ndarray): 1/e penetration depths in cm
real_sld_per_ang2 (np.ndarray): Real part of scattering length density in Å⁻²
imaginary_sld_per_ang2 (np.ndarray): Imaginary part of scattering length
density in Å⁻²
Physical Relationships:
- Refractive Index: n = 1 - δ - iβ where δ and β are wavelength-dependent
- Critical Angle: θc = √(2δ) for grazing incidence geometry
- Attenuation Length: μ^-1 = (4πβ/λ)^-1 for exponential decay
- Dispersion/Absorption: Related to f1, f2 via classical electron radius
Examples:
Basic Property Access:
>>> import xraylabtool as xlt
>>> result = xlt.calculate_single_material_properties("SiO2", 10.0, 2.2)
>>> print(f"Material: {result.formula}")
Material: SiO2
>>> print(f"MW: {result.molecular_weight_g_mol:.2f} g/mol")
MW: 60.08 g/mol
>>> print(result.critical_angle_degrees[0] > 0.1) # Reasonable critical angle
True
Array Properties for Energy Scans:
>>> import numpy as np
>>> energies = np.linspace(8, 12, 5)
>>> result = xlt.calculate_single_material_properties("Si", energies, 2.33)
>>> print(f"Energies: {result.energy_kev}")
Energies: [ 8. 9. 10. 11. 12.]
>>> print(len(result.wavelength_angstrom))
5
Optical Constants Analysis:
>>> print(result.dispersion_delta.min() > 0) # δ should be positive
True
>>> print(result.absorption_beta.min() >= 0) # β should be non-negative
True
Derived Quantities:
>>> print(len(result.critical_angle_degrees))
5
>>> print(len(result.attenuation_length_cm))
5
Note:
All numpy arrays have the same length as the input energy array. For scalar
energy inputs, arrays will have length 1. Use standard numpy operations
for analysis (e.g., np.min(), np.max(), np.argmin(), indexing).
See Also:
calculate_single_material_properties : Primary function returning this class
calculate_xray_properties : Function returning Dict[str, XRayResult]
"""
# Material properties with enhanced type annotations
formula: str # Chemical formula string
molecular_weight_g_mol: float # Molecular weight (g/mol)
total_electrons: float # Total electrons per molecule
density_g_cm3: float # Mass density (g/cm³)
electron_density_per_ang3: float # Electron density (electrons/ų)
# X-ray energy and wavelength arrays (performance-optimized dtypes)
energy_kev: EnergyArray = field() # X-ray energies in keV
wavelength_angstrom: WavelengthArray = field() # X-ray wavelengths in Å
# Fundamental optical constants (performance-critical arrays)
dispersion_delta: OpticalConstantArray = field() # Dispersion coefficient δ
absorption_beta: OpticalConstantArray = field() # Absorption coefficient β
# Atomic scattering factors (complex arrays for scientific accuracy)
scattering_factor_f1: OpticalConstantArray = (
field()
) # Real part of scattering factor
scattering_factor_f2: OpticalConstantArray = (
field()
) # Imaginary part of scattering factor
# Derived optical properties (performance-optimized arrays)
critical_angle_degrees: OpticalConstantArray = field() # Critical angles (degrees)
attenuation_length_cm: OpticalConstantArray = field() # Attenuation lengths (cm)
real_sld_per_ang2: OpticalConstantArray = field() # Real SLD (Å⁻²)
imaginary_sld_per_ang2: OpticalConstantArray = field() # Imaginary SLD (Å⁻²)
[docs]
def __post_init__(self) -> None:
"""Post-initialization to handle any setup after object creation."""
# Only convert if not already a numpy array (e.g. when constructed from
# raw Python lists or scalars, not from the internal calculation path
# which already produces float64 contiguous arrays).
if not isinstance(self.energy_kev, np.ndarray):
self.energy_kev = np.asarray(self.energy_kev, dtype=np.float64)
if not isinstance(self.wavelength_angstrom, np.ndarray):
self.wavelength_angstrom = np.asarray(
self.wavelength_angstrom, dtype=np.float64
)
if not isinstance(self.dispersion_delta, np.ndarray):
self.dispersion_delta = np.asarray(self.dispersion_delta, dtype=np.float64)
if not isinstance(self.absorption_beta, np.ndarray):
self.absorption_beta = np.asarray(self.absorption_beta, dtype=np.float64)
if not isinstance(self.scattering_factor_f1, np.ndarray):
self.scattering_factor_f1 = np.asarray(
self.scattering_factor_f1, dtype=np.float64
)
if not isinstance(self.scattering_factor_f2, np.ndarray):
self.scattering_factor_f2 = np.asarray(
self.scattering_factor_f2, dtype=np.float64
)
if not isinstance(self.critical_angle_degrees, np.ndarray):
self.critical_angle_degrees = np.asarray(
self.critical_angle_degrees, dtype=np.float64
)
if not isinstance(self.attenuation_length_cm, np.ndarray):
self.attenuation_length_cm = np.asarray(
self.attenuation_length_cm, dtype=np.float64
)
if not isinstance(self.real_sld_per_ang2, np.ndarray):
self.real_sld_per_ang2 = np.asarray(
self.real_sld_per_ang2, dtype=np.float64
)
if not isinstance(self.imaginary_sld_per_ang2, np.ndarray):
self.imaginary_sld_per_ang2 = np.asarray(
self.imaginary_sld_per_ang2, dtype=np.float64
)
# Convenience properties used in docs/notebooks
@property
def energy_ev(self): # type: ignore[no-untyped-def]
return self.energy_kev * 1000.0
@property
def delta(self): # type: ignore[no-untyped-def]
return self.dispersion_delta
@property
def beta(self): # type: ignore[no-untyped-def]
return self.absorption_beta
@property
def critical_angle_mrad(self): # type: ignore[no-untyped-def]
return self.critical_angle_degrees * np.pi / 180.0 * 1000.0
@property
def linear_absorption_coefficient(self): # type: ignore[no-untyped-def]
# μ = 1 / attenuation length
arr = np.where(
self.attenuation_length_cm != 0, 1.0 / self.attenuation_length_cm, 0.0
)
return np.asarray(arr)
# Legacy property aliases (deprecated) - emit warnings when accessed
@property
def Formula(self) -> str:
"""Deprecated: Use 'formula' instead."""
warnings.warn(
"Formula is deprecated, use 'formula' instead",
DeprecationWarning,
stacklevel=2,
)
return self.formula
@property
def MW(self) -> float:
"""Deprecated: Use 'molecular_weight_g_mol' instead."""
warnings.warn(
"MW is deprecated, use 'molecular_weight_g_mol' instead",
DeprecationWarning,
stacklevel=2,
)
return self.molecular_weight_g_mol
@property
def Number_Of_Electrons(self) -> float:
"""Deprecated: Use 'total_electrons' instead."""
warnings.warn(
"Number_Of_Electrons is deprecated, use 'total_electrons' instead",
DeprecationWarning,
stacklevel=2,
)
return self.total_electrons
@property
def Density(self) -> float:
"""Deprecated: Use 'density_g_cm3' instead."""
warnings.warn(
"Density is deprecated, use 'density_g_cm3' instead",
DeprecationWarning,
stacklevel=2,
)
return self.density_g_cm3
@property
def Electron_Density(self) -> float:
"""Deprecated: Use 'electron_density_per_ang3' instead."""
warnings.warn(
"Electron_Density is deprecated, use 'electron_density_per_ang3' instead",
DeprecationWarning,
stacklevel=2,
)
return self.electron_density_per_ang3
@property
def Energy(self) -> np.ndarray:
"""Deprecated: Use 'energy_kev' instead."""
warnings.warn(
"Energy is deprecated, use 'energy_kev' instead",
DeprecationWarning,
stacklevel=2,
)
return self.energy_kev
@property
def Wavelength(self) -> np.ndarray:
"""Deprecated: Use 'wavelength_angstrom' instead."""
warnings.warn(
"Wavelength is deprecated, use 'wavelength_angstrom' instead",
DeprecationWarning,
stacklevel=2,
)
return self.wavelength_angstrom
@property
def Dispersion(self) -> np.ndarray:
"""Deprecated: Use 'dispersion_delta' instead."""
warnings.warn(
"Dispersion is deprecated, use 'dispersion_delta' instead",
DeprecationWarning,
stacklevel=2,
)
return self.dispersion_delta
@property
def Absorption(self) -> np.ndarray:
"""Deprecated: Use 'absorption_beta' instead."""
warnings.warn(
"Absorption is deprecated, use 'absorption_beta' instead",
DeprecationWarning,
stacklevel=2,
)
return self.absorption_beta
@property
def f1(self) -> np.ndarray:
"""Deprecated: Use 'scattering_factor_f1' instead."""
warnings.warn(
"f1 is deprecated, use 'scattering_factor_f1' instead",
DeprecationWarning,
stacklevel=2,
)
return self.scattering_factor_f1
@property
def f2(self) -> np.ndarray:
"""Deprecated: Use 'scattering_factor_f2' instead."""
warnings.warn(
"f2 is deprecated, use 'scattering_factor_f2' instead",
DeprecationWarning,
stacklevel=2,
)
return self.scattering_factor_f2
@property
def Critical_Angle(self) -> np.ndarray:
"""Deprecated: Use 'critical_angle_degrees' instead."""
warnings.warn(
"Critical_Angle is deprecated, use 'critical_angle_degrees' instead",
DeprecationWarning,
stacklevel=2,
)
return self.critical_angle_degrees
@property
def Attenuation_Length(self) -> np.ndarray:
"""Deprecated: Use 'attenuation_length_cm' instead."""
warnings.warn(
"Attenuation_Length is deprecated, use 'attenuation_length_cm' instead",
DeprecationWarning,
stacklevel=2,
)
return self.attenuation_length_cm
@property
def reSLD(self) -> np.ndarray:
"""Deprecated: Use 'real_sld_per_ang2' instead."""
warnings.warn(
"reSLD is deprecated, use 'real_sld_per_ang2' instead",
DeprecationWarning,
stacklevel=2,
)
return self.real_sld_per_ang2
@property
def imSLD(self) -> np.ndarray:
"""Deprecated: Use 'imaginary_sld_per_ang2' instead."""
warnings.warn(
"imSLD is deprecated, use 'imaginary_sld_per_ang2' instead",
DeprecationWarning,
stacklevel=2,
)
return self.imaginary_sld_per_ang2
[docs]
@classmethod
def from_legacy(
cls,
formula: str | None = None,
mw: float | None = None,
number_of_electrons: float | None = None,
density: float | None = None,
electron_density: float | None = None,
energy: np.ndarray | None = None,
wavelength: np.ndarray | None = None,
dispersion: np.ndarray | None = None,
absorption: np.ndarray | None = None,
f1: np.ndarray | None = None,
f2: np.ndarray | None = None,
critical_angle: np.ndarray | None = None,
attenuation_length: np.ndarray | None = None,
re_sld: np.ndarray | None = None,
im_sld: np.ndarray | None = None,
**kwargs: Any,
) -> XRayResult:
"""Create XRayResult from legacy field names (for internal use)."""
return cls(
formula=formula or kwargs.get("formula", ""),
molecular_weight_g_mol=mw or kwargs.get("molecular_weight_g_mol", 0.0),
total_electrons=number_of_electrons or kwargs.get("total_electrons", 0.0),
density_g_cm3=density or kwargs.get("density_g_cm3", 0.0),
electron_density_per_ang3=(
electron_density or kwargs.get("electron_density_per_ang3", 0.0)
),
energy_kev=(
energy if energy is not None else kwargs.get("energy_kev", np.array([]))
),
wavelength_angstrom=(
wavelength
if wavelength is not None
else kwargs.get("wavelength_angstrom", np.array([]))
),
dispersion_delta=(
dispersion
if dispersion is not None
else kwargs.get("dispersion_delta", np.array([]))
),
absorption_beta=(
absorption
if absorption is not None
else kwargs.get("absorption_beta", np.array([]))
),
scattering_factor_f1=(
f1
if f1 is not None
else kwargs.get("scattering_factor_f1", np.array([]))
),
scattering_factor_f2=(
f2
if f2 is not None
else kwargs.get("scattering_factor_f2", np.array([]))
),
critical_angle_degrees=(
critical_angle
if critical_angle is not None
else kwargs.get("critical_angle_degrees", np.array([]))
),
attenuation_length_cm=(
attenuation_length
if attenuation_length is not None
else kwargs.get("attenuation_length_cm", np.array([]))
),
real_sld_per_ang2=(
re_sld
if re_sld is not None
else kwargs.get("real_sld_per_ang2", np.array([]))
),
imaginary_sld_per_ang2=(
im_sld
if im_sld is not None
else kwargs.get("imaginary_sld_per_ang2", np.array([]))
),
)