From 9796d2301c210bc3da46e4c8e3a198a28fb115e9 Mon Sep 17 00:00:00 2001 From: Ilham Variansyah Date: Sun, 23 Aug 2026 15:38:19 +0700 Subject: [PATCH 1/3] add release preparation --- .gitignore | 3 ++- README.md | 11 +++++++++ prepare_release.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 prepare_release.py diff --git a/.gitignore b/.gitignore index a3529f8..96b7eaf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,9 @@ # Python caches __pycache__/ -# Verification results +# Generated results /results/ +/release/ # Simulation output output* diff --git a/README.md b/README.md index f13c07f..9895c28 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,12 @@ Workflow orchestration is performed using [Maestro](https://github.com/llnl/maes configs/ Shared platform, user, and launch configurations verification/ Verification suites and their cases results/ Processed results organized by suite +release/ Flattened figures prepared as release assets launch.py Launch all enabled suites process.py Process suites and collect their results cleanup.py Remove generated outputs and processed results +prepare_release.py Prepare figures for a GitHub release ``` MC/DC-VVP uses **suite**, **case**, and **task** as standard terms for its organizational hierarchy: @@ -90,6 +92,15 @@ An existing suite `results/` directory can still be collected when no Maestro ru Within each suite, `convergence/` contains study-wide convergence figures and `comparison/` contains plots or animations from the largest-statistics result. Collecting a suite replaces that suite's existing top-level results. +Prepare the collected PNG and GIF figures for upload as GitHub release assets: + +```bash +python prepare_release.py +``` + +The script recreates `release/`, copies every figure from `results/`, and replaces each directory boundary in its relative path with `--` to form a unique flat asset name. +The structured files in `results/` are not modified. + Remove generated case outputs and processed results from every registered suite: ```bash diff --git a/prepare_release.py b/prepare_release.py new file mode 100644 index 0000000..b380a74 --- /dev/null +++ b/prepare_release.py @@ -0,0 +1,61 @@ +"""Prepare flat GitHub release assets from collected VVP results.""" + +import shutil +from pathlib import Path + +# ====================================================================================== +# Paths +# ====================================================================================== + +REPO_DIR = Path(__file__).resolve().parent +RESULTS_DIR = REPO_DIR / "results" +RELEASE_DIR = REPO_DIR / "release" + + +# ====================================================================================== +# Discover result figures +# ====================================================================================== + +if not RESULTS_DIR.is_dir(): + raise FileNotFoundError(f"Results directory not found: {RESULTS_DIR}") + +figures = sorted( + path + for path in RESULTS_DIR.rglob("*") + if path.is_file() and path.suffix.lower() in {".png", ".gif"} +) + +if not figures: + raise FileNotFoundError(f"No PNG or GIF results found in {RESULTS_DIR}") + + +# ====================================================================================== +# Prepare flat release assets +# ====================================================================================== + +if RELEASE_DIR.is_dir(): + shutil.rmtree(RELEASE_DIR) +RELEASE_DIR.mkdir() + +total_size = 0 +for source in figures: + relative_path = source.relative_to(RESULTS_DIR) + asset_name = "--".join(relative_path.parts) + destination = RELEASE_DIR / asset_name + + if destination.exists(): + raise FileExistsError(f"Flattened asset name is not unique: {destination.name}") + + shutil.copy2(source, destination) + total_size += destination.stat().st_size + print(f"{relative_path} -> {destination.name}") + + +# ====================================================================================== +# Summary +# ====================================================================================== + +print() +print(f"Assets: {len(figures)}") +print(f"Size : {total_size / 1024**2:.1f} MiB") +print(f"Release directory: {RELEASE_DIR}") From a21ec15c472d6c3f22800b7e0d36b7964ec68e48 Mon Sep 17 00:00:00 2001 From: Ilham Variansyah Date: Mon, 24 Aug 2026 17:59:51 +0700 Subject: [PATCH 2/3] add reference plot to code-to-code --- verification/code_to_code/neutron/README.md | 3 + .../neutron/cases/c5g7-4phase/process.py | 17 ++++ .../neutron/cases/kobayashi/process.py | 19 +++++ verification/code_to_code/neutron/process.py | 26 +++++- verification/code_to_code/neutron/util.py | 85 +++++++++++++++++++ 5 files changed, 146 insertions(+), 4 deletions(-) diff --git a/verification/code_to_code/neutron/README.md b/verification/code_to_code/neutron/README.md index 8808a54..6ef0c93 100644 --- a/verification/code_to_code/neutron/README.md +++ b/verification/code_to_code/neutron/README.md @@ -21,6 +21,7 @@ results/ launch_config.yaml Effective suite launch configuration task.yaml Task-generation configuration used by the launch convergence/ Statistical-convergence figures + reference/ Largest-sample fixed-reference figures and animations comparison/ Largest-sample comparison and difference animations task.yaml Configure task generation for each case @@ -97,7 +98,9 @@ python process.py maestro_run_ ``` Convergence figures are written to `results/convergence/`. +Fixed-reference figures and animations are written to `results/reference/`. Animated spatial comparisons and relative-difference evolution at the largest shared sample size are written to `results/comparison/`. +Each case may produce `reference_*.png` figures or `reference_*.gif` animations from the arithmetic mean of the largest-sample participating-code estimates. Each case produces `comparison.gif` for the participating-code solutions and `difference.gif` for their relative differences. The top-level `process.py` collects these figures under the repository's `results/` directory. diff --git a/verification/code_to_code/neutron/cases/c5g7-4phase/process.py b/verification/code_to_code/neutron/cases/c5g7-4phase/process.py index 6c813d9..0099146 100644 --- a/verification/code_to_code/neutron/cases/c5g7-4phase/process.py +++ b/verification/code_to_code/neutron/cases/c5g7-4phase/process.py @@ -12,6 +12,7 @@ sys.path.insert(0, str(SUITE_DIR)) from util import ( + animate_spatial_reference, comparison_reference, load_openmc_tally, particle_counts, @@ -61,6 +62,22 @@ def main(): ) openmc_reference = load_openmc_fission(reference_files[-1]) reference = comparison_reference(mcdc_reference, openmc_reference) + + # Present the fixed largest-sample reference used by the convergence study. + largest_output = case_dir / f"output_{int(N_particle[-1])}.h5" + with h5py.File(largest_output, "r") as f: + time = f["tallies/tracklength_tally_0/grid/time"][:] + x = f["tallies/tracklength_tally_0/grid/x"][:] + y = f["tallies/tracklength_tally_0/grid/y"][:] + z = f["tallies/tracklength_tally_0/grid/z"][:] + animate_spatial_reference( + "fission", + 0.5 * (time[:-1] + time[1:]), + (x, y, z), + reference, + filename="reference_fission.gif", + ) + del mcdc_reference, openmc_reference for index, (count, reference_file) in enumerate(zip(N_particle, reference_files)): diff --git a/verification/code_to_code/neutron/cases/kobayashi/process.py b/verification/code_to_code/neutron/cases/kobayashi/process.py index cca6bb6..dc27e6e 100644 --- a/verification/code_to_code/neutron/cases/kobayashi/process.py +++ b/verification/code_to_code/neutron/cases/kobayashi/process.py @@ -12,6 +12,7 @@ sys.path.insert(0, str(SUITE_DIR)) from util import ( + animate_spatial_reference, comparison_reference, load_openmc_tally, particle_counts, @@ -72,6 +73,24 @@ def main(): mcdc_density_reference, openmc_density_reference, ) + + # Present the fixed largest-sample reference used by the convergence study. + largest_output = case_dir / f"output_{int(N_particle[-1])}.h5" + with h5py.File(largest_output, "r") as f: + time = f["tallies/tracklength_tally_0/grid/time"][:] + x = f["tallies/tracklength_tally_0/grid/x"][:] + y = f["tallies/tracklength_tally_0/grid/y"][:] + z = f["tallies/tracklength_tally_0/grid/z"][:] + animate_spatial_reference( + "flux", + 0.5 * (time[:-1] + time[1:]), + (x, y, z), + flux_reference, + history=density_reference, + history_label="Neutron density", + filename="reference_flux.gif", + ) + del ( mcdc_flux_reference, mcdc_density_reference, diff --git a/verification/code_to_code/neutron/process.py b/verification/code_to_code/neutron/process.py index 6600a9b..46a1df2 100644 --- a/verification/code_to_code/neutron/process.py +++ b/verification/code_to_code/neutron/process.py @@ -38,9 +38,9 @@ def clear_case_figures(case_dir): figure.unlink() -def collect_case_figures(case_dir, destination, case_name): +def collect_case_figures(case_dir, destination, case_name, patterns): """Move generated case figures into a named suite results directory.""" - for pattern in ("*.png", "*.gif"): + for pattern in patterns: for figure in case_dir.glob(pattern): figure.replace(destination / f"{case_name}_{figure.name}") @@ -90,6 +90,7 @@ def collect_case_figures(case_dir, destination, case_name): results_dir = suite_dir / "results" convergence_dir = results_dir / "convergence" comparison_dir = results_dir / "comparison" +reference_dir = results_dir / "reference" # Start with an empty suite results hierarchy on every processing run. if results_dir.is_dir(): @@ -97,6 +98,7 @@ def collect_case_figures(case_dir, destination, case_name): convergence_dir.mkdir(parents=True, exist_ok=True) comparison_dir.mkdir(parents=True, exist_ok=True) +reference_dir.mkdir(parents=True, exist_ok=True) # Keep the effective launch and task definitions beside the processed figures. shutil.copy2(launch_config_file, results_dir / "launch_config.yaml") @@ -130,7 +132,18 @@ def collect_case_figures(case_dir, destination, case_name): cwd=case_dir, check=True, ) - collect_case_figures(case_dir, convergence_dir, case_name) + collect_case_figures( + case_dir, + convergence_dir, + case_name, + ("convergence_*.png",), + ) + collect_case_figures( + case_dir, + reference_dir, + case_name, + ("reference_*.png", "reference_*.gif"), + ) # Compare the participating codes at the largest shared sample size. counts = particle_counts(task["logN_min"], task["logN_max"], task["N_task"]) @@ -149,7 +162,12 @@ def collect_case_figures(case_dir, destination, case_name): cwd=case_dir, check=True, ) - collect_case_figures(case_dir, comparison_dir, case_name) + collect_case_figures( + case_dir, + comparison_dir, + case_name, + ("comparison.gif", "difference.gif"), + ) # ====================================================================================== diff --git a/verification/code_to_code/neutron/util.py b/verification/code_to_code/neutron/util.py index 77baabb..6b573fc 100644 --- a/verification/code_to_code/neutron/util.py +++ b/verification/code_to_code/neutron/util.py @@ -221,6 +221,91 @@ def update(frame): plt.close(fig) +def animate_spatial_reference( + score, + time, + spatial_edges, + reference, + history=None, + history_label=None, + filename="reference.gif", +): + """Animate a fixed comparison reference and its integral history.""" + time = np.asarray(time) + reference = np.asarray(reference) + x, y, z = (np.asarray(edges) for edges in spatial_edges) + projections = _spatial_projections(reference) + + if history is None: + history = np.sum(reference, axis=(1, 2, 3)) + history = np.asarray(history) + if history.shape != time.shape: + raise ValueError( + "The reference history and time grid must have the same shape." + ) + if history_label is None: + history_label = f"Total {score}" + + projection_data = ( + ("XY", x, y, projections[0]), + ("XZ", x, z, projections[1]), + ("YZ", y, z, projections[2]), + ) + norm = _positive_norm(*projections) + + fig, axes = plt.subplots(2, 2, figsize=(12, 9), constrained_layout=True) + + # Show the transient's integral response beside its spatial projections. + history_axis = axes[0, 0] + positive_history = np.where(history > 0.0, history, np.nan) + history_axis.plot(time, positive_history, "b") + marker = history_axis.plot([], [], "ro", fillstyle="none")[0] + if np.any(history > 0.0): + history_axis.set_yscale("log") + history_axis.set_xlabel("Time") + history_axis.set_ylabel(history_label) + history_axis.grid() + + images = [] + for axis, (plane, horizontal, vertical, projection) in zip( + (axes[0, 1], axes[1, 0], axes[1, 1]), projection_data + ): + image = axis.imshow( + projection[0].T, + extent=(horizontal[0], horizontal[-1], vertical[0], vertical[-1]), + origin="lower", + aspect="auto", + cmap="viridis", + norm=norm, + ) + axis.set_title(f"{score.capitalize()}-{plane}") + axis.set_xlabel(plane[0].lower()) + axis.set_ylabel(plane[1].lower()) + images.append((image, projection)) + + fig.colorbar( + images[0][0], + ax=(axes[0, 1], axes[1, 0], axes[1, 1]), + label=score.capitalize(), + ) + title = fig.suptitle(f"{score.capitalize()} reference, t = {time[0]:.3g}") + + def update(frame): + marker.set_data([time[frame]], [history[frame]]) + for image, projection in images: + image.set_data(projection[frame].T) + title.set_text(f"{score.capitalize()} reference, t = {time[frame]:.3g}") + return [title, marker, *(image for image, _ in images)] + + simulation = animation.FuncAnimation(fig, update, frames=len(time)) + simulation.save( + filename, + writer=animation.PillowWriter(fps=max(2, len(time) // 10)), + dpi=120, + ) + plt.close(fig) + + def animate_spatial_difference( score, time, From f193440d6a1b54f8f0df351be58ec3eaac2b4e4a Mon Sep 17 00:00:00 2001 From: Ilham Variansyah Date: Mon, 24 Aug 2026 18:44:10 +0700 Subject: [PATCH 3/3] update CHANGELOG --- CHANGELOG.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b69549e..315e0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,30 +11,38 @@ Target release: 0.16.0, the first versioned MC/DC-VVP release. ### Added -- Add Kornreich k-eigenvalue slabs problem -- Add the analytical neutron $k$-eigenvalue MMS one-group slab, from [@ilhamv] -- Add the analytical neutron fixed-source MMS two-group slab, from [@ilhamv] -- Add the analytical neutron $k$-eigenvalue suite with subcritical and supercritical SHEM-361 cases, analytical matrix-eigenvalue references, active-cycle convergence studies, and uncertainty plots, from [@ilhamv] +- Add the analytical neutron $k$-eigenvalue suite with a semi-analytical homogeneous one-group slab, the Kornreich-Parsons heterogeneous slab benchmark, subcritical and supercritical infinite-medium SHEM-361 cases, active-cycle convergence studies, and uncertainty plots, from [@ilhamv] +- Add the analytical neutron fixed-source manufactured two-group slab, from [@ilhamv] - Add energy-dependent weight-window and time-census variants of the infinite homogeneous SHEM-361 problem, from [@ilhamv] - Add AZURV1 variants for basic variance-reduction techniques, analytical spatial weight windows, time censuses, and census-based tallies, from [@ilhamv] +- Add a neutron code-to-code verification suite for the C5G7 four-phase and Kobayashi dog-leg transients, including archived participating-code data, fixed largest-sample references, convergence metrics, and animated reference, comparison, and difference results, from [@ilhamv] +- Add preliminary SINBAD model scaffolds for OKTAVIAN Si-60, FNG SiC, FNG/TUD SiC, and RFNC photon-compound experiments based on their public benchmark entries, from [@ilhamv] - Add top-level and suite-level READMEs describing layouts, configuration, launching, processing, cases, and references, from [@ilhamv] - Add shared platform, user, and launch configuration for local and HPC campaigns, from [@ilhamv] +- Add top-level cleanup, result collection, and flat GitHub release-asset preparation workflows, from [@ilhamv] +- Add Black formatting checks through pre-commit and GitHub Actions, from [@ilhamv] ### Changed - Distribute the analytical fixed-source slab cases across the $x$, $y$, and $z$ axes to exercise every Cartesian slab orientation, from [@ilhamv] - Migration to Maestro-based launch, from [@ilhamv] - Update analytical fixed-source cases for the simulation-owned MC/DC interface and unified material model, from [@ilhamv] -- Standardize **suite** and **case** as the VVP repository's organizational terminology, from [@ilhamv] +- Standardize **suite**, **case**, and **task** as the VVP repository's organizational terminology, from [@ilhamv] - Organize fixed-source cases around consistent input, reference, processing, and optional plotting scripts, from [@ilhamv] +- Reorganize legacy neutron benchmarks as code-to-code verification cases without separate continuous-energy and multigroup directory levels, from [@ilhamv] +- Use `N_node` for HPC resource selection, apply case-specific `walltime_factor` values to suite base walltimes, and skip completed tasks while retaining partial results, from [@ilhamv] +- Organize processed results into convergence, reference, and comparison outputs and collect them through the top-level processing workflow, from [@ilhamv] +- Consolidate the canonical SHEM-361 multigroup dataset under the analytical fixed-source suite for reuse by all SHEM-361 cases, from [@ilhamv] ### Deprecated ### Removed +- Remove the legacy continuous-energy pulsed pin-cell cases, from [@ilhamv] + ### Fixed ### Security -[Unreleased]: https://github.com/ilhamv/mcdc-vvp/tree/master +[Unreleased]: https://github.com/mcdc-project/mcdc-vvp/tree/dev [@ilhamv]: https://github.com/ilhamv