#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# Copyright (c) 2026 Melek Derman
#
# SPDX-License-Identifier: BSD-3-Clause
# -----------------------------------------------------------------------------
"""
EPICS dataset downloader
Downloads EEDL, EPDL, and EADL ENDF files from the LLNL Nuclear Data
website (EPICS 2025).
Data Sources
------------
* EEDL: ``https://nuclear.llnl.gov/EPICS/ENDF2025/EEDL.ELEMENTS/``
* EPDL: ``https://nuclear.llnl.gov/EPICS/ENDF2025/EPDL.ELEMENTS/``
* EADL: ``https://nuclear.llnl.gov/EPICS/ENDF2025/EADL.ELEMENTS/``
Examples
--------
>>> from pyepics.io.download import download_library, download_all
>>> download_library("eedl") # downloads to ./data/endf/eedl/
>>> download_all(out_dir="data/endf") # downloads all three
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Literal
from urllib.parse import urljoin
from pyepics.exceptions import DownloadError
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Library metadata
# ---------------------------------------------------------------------------
LIBRARY_URLS: dict[str, dict[str, str]] = {
"eedl": {
"url": "https://nuclear.llnl.gov/EPICS/ENDF2025/EEDL.ELEMENTS/getza.htm",
"prefix": "EEDL",
"description": "Evaluated Electron Data Library",
},
"epdl": {
"url": "https://nuclear.llnl.gov/EPICS/ENDF2025/EPDL.ELEMENTS/getza.htm",
"prefix": "EPDL",
"description": "Evaluated Photon Data Library",
},
"eadl": {
"url": "https://nuclear.llnl.gov/EPICS/ENDF2025/EADL.ELEMENTS/getza.htm",
"prefix": "EADL",
"description": "Evaluated Atomic Data Library",
},
}
"""Download URLs and metadata for each EPICS library."""
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
[docs]
def download_library(
library_name: Literal["eedl", "epdl", "eadl"],
out_dir: Path | str | None = None,
) -> Path:
"""Download a specific EPICS library from the LLNL website
Fetches the LLNL index page for the requested library, discovers all
element ENDF files, and downloads each one to *out_dir*.
Parameters
----------
library_name : ``"eedl"`` | ``"epdl"`` | ``"eadl"``
Which library to download.
out_dir : Path | str | None, optional
Output directory. Defaults to ``"./{library_name}"``.
Returns
-------
Path
Path to the directory containing the downloaded files.
Raises
------
DownloadError
If network requests fail or the HTML cannot be parsed.
ValueError
If *library_name* is not one of the supported values.
"""
if library_name not in LIBRARY_URLS:
raise ValueError(
f"Unknown library: {library_name!r}. "
f"Choose from: {sorted(LIBRARY_URLS.keys())}"
)
try:
import requests
from bs4 import BeautifulSoup
except ImportError as exc:
raise DownloadError(
"Download requires 'requests' and 'beautifulsoup4'. "
"Install with: pip install requests beautifulsoup4"
) from exc
config = LIBRARY_URLS[library_name]
base_url = config["url"]
prefix = config["prefix"]
if out_dir is None:
out_dir = Path(library_name)
else:
out_dir = Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
logger.info(
"Downloading %s (%s) from %s",
config["description"], prefix, base_url,
)
print(f"\n{'=' * 60}")
print(f" Downloading {config['description']} ({prefix})")
print(f"{'=' * 60}")
try:
resp = requests.get(base_url, timeout=30)
resp.raise_for_status()
except requests.RequestException as exc:
raise DownloadError(f"Failed to fetch index page: {exc}") from exc
soup = BeautifulSoup(resp.text, "html.parser")
links = soup.find_all("a", href=True)
if not links:
raise DownloadError(
f"No download links found on {base_url}. "
"The LLNL page format may have changed."
)
n_downloaded = 0
for a in links:
href = a["href"]
url = urljoin(base_url, href)
fname = f"{prefix}.{Path(href).name}.endf"
dst = out_dir / fname
logger.debug("Downloading %s -> %s", url, dst)
print(f" {fname}", end=" ... ", flush=True)
try:
with requests.get(url, stream=True, timeout=60) as r:
r.raise_for_status()
with open(dst, "wb") as f:
for chunk in r.iter_content(8192):
f.write(chunk)
print("OK")
n_downloaded += 1
except requests.RequestException as exc:
print(f"FAILED: {exc}")
logger.warning("Failed to download %s: %s", url, exc)
print(f"\n Downloaded {n_downloaded} files to {out_dir}/")
logger.info("Downloaded %d files to %s", n_downloaded, out_dir)
return out_dir
[docs]
def download_all(out_dir: Path | str | None = None) -> dict[str, Path]:
"""Download all three EPICS libraries (EEDL, EPDL, EADL)
Parameters
----------
out_dir : Path | str | None, optional
Parent output directory. Each library will be placed in a
sub-folder (``data/endf/eedl/``, etc.). Defaults to
the current working directory.
Returns
-------
dict[str, Path]
Mapping from library name to download directory.
"""
parent = Path(out_dir) if out_dir else Path.cwd()
results: dict[str, Path] = {}
for name in LIBRARY_URLS:
results[name] = download_library(name, parent / name)
return results