Source code for pyepics.readers.epdl

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

"""
EPDL (Evaluated Photon Data Library) reader

Parses ENDF-format EPDL files and returns a strongly-typed
:class:`~pyepics.models.records.EPDLDataset` instance.

Supported ENDF sections
-----------------------
* **MF=23** — Photon cross sections (total, coherent, incoherent,
  pair production, photoelectric, subshell photoelectric).
* **MF=27** — Form factors and scattering functions (coherent form
  factor, incoherent scattering function, anomalous scattering factors).

File Format Assumptions
-----------------------
* Standard ENDF-6 fixed-width format (80 chars/line).
* Parsed entirely via the ``endf`` Python package.

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

from __future__ import annotations

import logging
from pathlib import Path

import numpy as np

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

from pyepics.exceptions import FileFormatError
from pyepics.models.records import (
    CrossSectionRecord,
    EPDLDataset,
    FormFactorRecord,
)
from pyepics.readers.base import BaseReader
from pyepics.utils.constants import (
    PERIODIC_TABLE,
    PHOTON_SECTIONS_ABBREVS,
)
from pyepics.utils.parsing import extract_atomic_number_from_path
from pyepics.utils.validation import (
    validate_atomic_number,
    validate_cross_section,
)

logger = logging.getLogger(__name__)


[docs] class EPDLReader(BaseReader): """Reader for EPDL (Evaluated Photon Data Library) ENDF files Extracts photon interaction cross sections (MF=23) and form factors (MF=27) from a single-element ENDF file generated by the LLNL EPICS 2025 pipeline. Notes ----- All parsing is done via ``endf.Material``. The ``sigma`` attribute of each section's ``Tabulated1D`` object is read for both MF=23 and MF=27 data. Examples -------- >>> reader = EPDLReader() >>> dataset = reader.read("epdl/EPDL.ZA026000.endf") >>> dataset.Z 26 >>> "xs_tot" in dataset.cross_sections True """
[docs] def read( self, path: Path | str, *, validate: bool = True, ) -> EPDLDataset: """Parse an EPDL ENDF file and return a typed dataset model Parameters ---------- path : Path | str Path to the EPDL ENDF file. The filename must contain ``ZA{ZZZ}000``. validate : bool, optional Run post-parse validation. Default ``True``. Returns ------- EPDLDataset Fully populated photon dataset model. Raises ------ FileFormatError If the file is missing or cannot be parsed. ParseError If any ENDF section is malformed. ValidationError If validation is enabled and fails. """ filepath = Path(path) logger.debug("Opening EPDL file: %s", filepath) if not filepath.is_file(): raise FileFormatError(f"EPDL 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 cross_sections: dict[str, CrossSectionRecord] = {} form_factors: dict[str, FormFactorRecord] = {} for (mf, mt), abbrev in PHOTON_SECTIONS_ABBREVS.items(): if (mf, mt) not in mat.section_data: continue sec = mat.section_data[(mf, mt)] sigma = sec.get("sigma") if sigma is None: continue x_arr = np.asarray(sigma.x, dtype="f8") y_arr = np.asarray(sigma.y, dtype="f8") bps = ( np.asarray(sigma.breakpoints, dtype="f8") if sigma.breakpoints is not None else None ) interp = ( np.asarray(sigma.interpolation, dtype="f8") if sigma.interpolation is not None else None ) if mf == 23: if validate: validate_cross_section(x_arr, y_arr, label=abbrev) cross_sections[abbrev] = CrossSectionRecord( label=abbrev, energy=x_arr, cross_section=y_arr, breakpoints=bps, interpolation=interp, ) logger.debug(" MF=23/MT=%d (%s): %d points", mt, abbrev, x_arr.size) elif mf == 27: form_factors[abbrev] = FormFactorRecord( label=abbrev, x=x_arr, y=y_arr, breakpoints=bps, interpolation=interp, ) logger.debug(" MF=27/MT=%d (%s): %d points", mt, abbrev, x_arr.size) dataset = EPDLDataset( Z=Z, symbol=symbol, atomic_weight_ratio=awr, ZA=za, cross_sections=cross_sections, form_factors=form_factors, ) logger.debug( "EPDL parse complete for Z=%d: %d xs, %d ff", Z, len(cross_sections), len(form_factors), ) return dataset