Source code for pyepics.readers.eadl

#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# Copyright (c) 2026 Melek Derman
#
# SPDX-License-Identifier: BSD-3-Clause
# -----------------------------------------------------------------------------

"""
EADL (Evaluated Atomic Data Library) reader

Parses ENDF-format EADL files and returns a strongly-typed
:class:`~pyepics.models.records.EADLDataset` instance containing atomic
relaxation data (binding energies, transition probabilities, fluorescence
yields, and Auger yields).

Supported ENDF sections
-----------------------
* **MF=28, MT=533** โ€” Atomic relaxation data (transition arrays per
  subshell, including radiative and non-radiative channels).

File Format Assumptions
-----------------------
* Standard ENDF-6 fixed-width format.
* The ``endf`` Python package handles the MF=28 section, exposing a
  ``subshells`` list of dicts with keys ``SUBI``, ``EBI``, ``ELN``,
  ``NTR``, and ``transitions`` (each with ``SUBJ``, ``SUBK``, ``ETR``,
  ``FTR``).

References
----------
- ENDF-6 Formats Manual (ENDF-102, BNL-90365-2009 Rev. 2), ยง28.
- LLNL Nuclear Data โ€” EPICS 2025, https://nuclear.llnl.gov/EPICS/
"""

from __future__ import annotations

import logging
from pathlib import Path

try:
    import endf
except ImportError as _exc:  # pragma: no cover
    raise ImportError(
        "The 'endf' package is required by EADLReader.  "
        "Install it with: pip install endf"
    ) from _exc

from pyepics.exceptions import FileFormatError
from pyepics.models.records import (
    EADLDataset,
    SubshellRelaxation,
    SubshellTransition,
)
from pyepics.readers.base import BaseReader
from pyepics.utils.constants import (
    PERIODIC_TABLE,
    SUBSHELL_DESIGNATORS,
)
from pyepics.utils.parsing import extract_atomic_number_from_path
from pyepics.utils.validation import validate_atomic_number

logger = logging.getLogger(__name__)


[docs] class EADLReader(BaseReader): """Reader for EADL (Evaluated Atomic Data Library) ENDF files Extracts atomic relaxation data from MF=28 / MT=533. Each subshell entry includes the binding energy, electron count, and a list of transitions (radiative X-ray emission and non-radiative Auger / Coster-Kronig). Notes ----- A radiative transition has ``SUBK == 0`` in the ENDF record; a non-radiative transition has ``SUBK > 0``. This convention is preserved in the :class:`SubshellTransition` model. Examples -------- >>> reader = EADLReader() >>> dataset = reader.read("eadl/EADL.ZA026000.endf") >>> dataset.Z 26 >>> "K" in dataset.subshells True """
[docs] def read( self, path: Path | str, *, validate: bool = True, ) -> EADLDataset: """Parse an EADL ENDF file and return a typed dataset model Parameters ---------- path : Path | str Path to the EADL ENDF file. The filename must contain ``ZA{ZZZ}000``. validate : bool, optional Run post-parse validation on the atomic number. Default ``True``. Returns ------- EADLDataset Fully populated atomic relaxation dataset model. Raises ------ FileFormatError If the file is missing or cannot be parsed. ParseError If the ENDF content is malformed. ValidationError If *validate* is ``True`` and the atomic number is out of range. """ filepath = Path(path) logger.debug("Opening EADL file: %s", filepath) if not filepath.is_file(): raise FileFormatError(f"EADL file not found: {filepath}") Z = extract_atomic_number_from_path(filepath) if validate: validate_atomic_number(Z) entry = PERIODIC_TABLE.get(Z, {}) symbol = entry.get("symbol", f"Z{Z:03d}") try: mat = endf.Material(str(filepath)) except Exception as exc: raise FileFormatError( f"Failed to open {filepath} with endf library: {exc}" ) from exc logger.debug("Loaded ENDF material for Z=%d (%s)", Z, symbol) # Extract AWR / ZA awr = 0.0 za = float(Z * 1000) for sec in mat.section_data.values(): if isinstance(sec, dict) and "AWR" in sec: awr = float(sec["AWR"]) za = float(sec.get("ZA", za)) break # MF=28 / MT=533 โ€” atomic relaxation key = (28, 533) n_subshells = 0 subshells: dict[str, SubshellRelaxation] = {} if key in mat.section_data: sec = mat.section_data[key] n_subshells = int(sec.get("NSS", 0)) for subshell_rec in sec.get("subshells", []): subi = int(subshell_rec.get("SUBI", 0)) shell_name = SUBSHELL_DESIGNATORS.get(subi, f"S{subi}") transitions: list[SubshellTransition] = [] for trans in subshell_rec.get("transitions", []): subj = int(trans.get("SUBJ", 0)) subk = int(trans.get("SUBK", 0)) transitions.append( SubshellTransition( origin_designator=subj, origin_label=SUBSHELL_DESIGNATORS.get(subj, f"S{subj}"), secondary_designator=subk, secondary_label=( "radiative" if subk == 0 else SUBSHELL_DESIGNATORS.get(subk, f"S{subk}") ), energy_eV=float(trans.get("ETR", 0.0)), probability=float(trans.get("FTR", 0.0)), is_radiative=(subk == 0), ) ) subshells[shell_name] = SubshellRelaxation( designator=subi, name=shell_name, binding_energy_eV=float(subshell_rec.get("EBI", 0.0)), n_electrons=float(subshell_rec.get("ELN", 0.0)), transitions=transitions, ) logger.debug( " Subshell %s: BE=%.2f eV, %d transitions", shell_name, subshell_rec.get("EBI", 0.0), len(transitions), ) else: logger.warning("No MF=28/MT=533 section found for Z=%d", Z) dataset = EADLDataset( Z=Z, symbol=symbol, atomic_weight_ratio=awr, ZA=za, n_subshells=n_subshells, subshells=subshells, ) logger.debug( "EADL parse complete for Z=%d: %d subshells", Z, len(subshells), ) return dataset