From f2fe4f86067b8f6418007cd8d689335dbaf7dc6f Mon Sep 17 00:00:00 2001 From: shimwell Date: Thu, 23 Jul 2026 22:07:06 +0200 Subject: [PATCH 1/2] Add remove_corrupt_h5 utility to clean up interrupted runs The generate_*/convert_* scripts resume by skipping input files whose output .h5 already exists, checking only for existence and not integrity. A run interrupted mid-write can leave a truncated .h5 that is then treated as "done" and silently kept in the library. This adds src/openmc_data/other/remove_corrupt_h5.py, a small utility that scans a directory of .h5 files, opens each with h5py (an optional --deep pass fully loads neutron data with openmc), and removes any that are unreadable so a rerun regenerates them. A --dry-run flag reports without deleting. Wired up as the remove_corrupt_h5 console entry point. Closes #33 --- pyproject.toml | 1 + src/openmc_data/other/remove_corrupt_h5.py | 93 ++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100755 src/openmc_data/other/remove_corrupt_h5.py diff --git a/pyproject.toml b/pyproject.toml index 000c2d3..3d0a140 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ make_compton = "openmc_data.other.make_compton:main" make_stopping_powers = "openmc_data.other.make_stopping_powers:main" add_branching_ratios = "openmc_data.depletion.add_branching_ratios:main" combine_libraries = "openmc_data.other.combine_libraries:main" +remove_corrupt_h5 = "openmc_data.other.remove_corrupt_h5:main" sample_sandy = "openmc_data.other.sample_sandy:main" reduce_chain = "openmc_data.depletion.reduce_chain:main" diff --git a/src/openmc_data/other/remove_corrupt_h5.py b/src/openmc_data/other/remove_corrupt_h5.py new file mode 100755 index 0000000..c0b05bd --- /dev/null +++ b/src/openmc_data/other/remove_corrupt_h5.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Scan a directory of OpenMC HDF5 data files and remove any that are corrupt. + +Useful after an interrupted ``generate_*`` / ``convert_*`` run (e.g. Ctrl-C). +Those scripts resume by skipping ENDF/ACE files whose output ``.h5`` already +exists, checking only for existence -- not integrity. A file left half-written +when the process was killed is therefore treated as "done" and silently kept in +the library. Running this first deletes any unreadable ``.h5`` files so that a +rerun regenerates them. + +Examples +-------- + # Remove any truncated/corrupt files (fast, opens each with h5py) + remove_corrupt_h5 tendl-2025-hdf5 + + # Also fully load each file with openmc (slower, neutron data only) + remove_corrupt_h5 tendl-2025-hdf5 --deep + + # Report without deleting + remove_corrupt_h5 tendl-2025-hdf5 --dry-run +""" +import argparse +import glob +import os + +import h5py + +try: + import openmc.data +except ModuleNotFoundError: + openmc = None + + +def check_file(path, deep=False): + """Return an error string if ``path`` is corrupt, otherwise ``None``. + + The default check opens the file with h5py, which catches the common + failure mode of a truncated write. ``deep=True`` additionally loads the + file with :class:`openmc.data.IncidentNeutron`. + """ + try: + with h5py.File(path, "r"): + pass + except Exception as e: # noqa: BLE001 - report any read failure + return str(e) + if deep: + if openmc is None: + raise ModuleNotFoundError( + "openmc is required for --deep validation but is not installed." + ) + try: + openmc.data.IncidentNeutron.from_hdf5(path) + except Exception as e: # noqa: BLE001 - report any load failure + return str(e) + return None + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "directory", help="Directory containing the .h5 files to check" + ) + parser.add_argument( + "--deep", + action="store_true", + help="Also fully load each file with openmc (slower, neutron data only)", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Report corrupt files without deleting them", + ) + args = parser.parse_args() + + files = sorted(glob.glob(os.path.join(args.directory, "*.h5"))) + print(f"Scanning {len(files)} .h5 files in {args.directory} ...") + removed = 0 + for f in files: + reason = check_file(f, deep=args.deep) + if reason is not None: + action = "would remove" if args.dry_run else "removing" + print(f"{action} corrupt: {f} -> {reason}") + if not args.dry_run: + os.remove(f) + removed += 1 + verb = "corrupt" if args.dry_run else "removed" + print(f"Done, {removed} {verb} file(s); {len(files) - removed} intact") + + +if __name__ == "__main__": + main() From 8e9bb892a66e15746fb0a23440b336492ea7ca34 Mon Sep 17 00:00:00 2001 From: shimwell Date: Thu, 23 Jul 2026 22:53:05 +0200 Subject: [PATCH 2/2] CI: bump test_urls workflow to Python 3.12 and update actions The develop branch of openmc now requires Python >=3.12, so installing it under the workflow's Python 3.9 failed with "Package 'openmc' requires a different Python". Bump to 3.12 to match the test_package and test_processing workflows, and update the deprecated checkout@v2 / setup-python@v2 actions to v4 / v5 as used by the other workflows. --- .github/workflows/test_urls.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_urls.yml b/.github/workflows/test_urls.yml index 2c5616a..f065462 100644 --- a/.github/workflows/test_urls.yml +++ b/.github/workflows/test_urls.yml @@ -22,12 +22,12 @@ jobs: ping_urls: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 - name: Install testing dependencies run: |