#!/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