Sensitivity tutorial: Serpent files

This notebook reads the Serpent multi-group sensitivity files available for the Jezebel and Godiva benchmarks. It is also written as a lightweight smoke test: the code cells contain assertions on the reader, energy-grid ordering, extraction, merging, and collapsing operations.

[2]:
import os
import sys
import tempfile
from pathlib import Path

os.environ.setdefault("MPLCONFIGDIR", str(Path(tempfile.gettempdir()) / "matplotlib"))

def find_project_root(start: Path | None = None) -> Path:
    """Find the pyNDUS repository root."""
    start = (start or Path.cwd()).resolve()

    for directory in (start, *start.parents):
        if (
            (directory / "setup.py").is_file()
            and (directory / "src" / "pyNDUS").is_dir()
            and (directory / "docs" / "tutorials").is_dir()
        ):
            return directory

    raise FileNotFoundError(
        f"Could not find the pyNDUS repository root starting from {start}"
    )


PROJECT_ROOT = find_project_root()
DATA_ROOT = PROJECT_ROOT / "docs" / "tutorials"
assert DATA_ROOT.exists(), f"Tutorial data directory not found: {DATA_ROOT}"

SENS_ROOT = DATA_ROOT / "MG_sensitivities"
COV_ROOT = DATA_ROOT / "MG_covariances"
assert SENS_ROOT.exists(), SENS_ROOT
assert COV_ROOT.exists(), COV_ROOT
[3]:
import copy

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from pyNDUS import Sensitivity

Read Jezebel and Godiva

Serpent sensitivity files are read directly with Sensitivity. The current pyNDUS convention stores profiles in ascending energy order.

[4]:
serpent_files = {
    "jezebel": SENS_ROOT / "serpent" / "jezebel_sens0.m",
    "godiva": SENS_ROOT / "serpent" / "godiva_sens0.m",
}

sens = {name: Sensitivity(path) for name, path in serpent_files.items()}

for name, obj in sens.items():
    assert obj.reader == "serpent"
    assert obj.energy_unit == "MeV"
    assert obj.n_groups == 33
    assert np.all(np.diff(obj.group_structure) > 0.0)
    assert "keff" in obj.responses

summary = pd.DataFrame({
    name: {
        "responses": ", ".join(obj.responses),
        "materials": ", ".join(obj.materials.keys()),
        "nuclides": ", ".join(obj.zais.keys()),
        "n_MTs": obj.n_MTs,
        "n_groups": obj.n_groups,
    }
    for name, obj in sens.items()
}).T
summary
[4]:
responses materials nuclides n_MTs n_groups
jezebel keff, beff, leff total Pu-239, Pu-240, Pu-241, Ga-69, Ga-71 55 33
godiva keff, beff, leff total U-235, U-238, U-234 55 33

Extract profiles

The default group order is ascending. The optional descending view is still available and should be the reverse of the default extraction.

[5]:
jezebel = sens["jezebel"]
godiva = sens["godiva"]

pu239_fission_avg, pu239_fission_rsd = jezebel.get(resp="keff", mat="total", za="Pu-239", MT=18)
pu239_desc_avg, _ = jezebel.get(resp="keff", mat="total", za="Pu-239", MT=18,
                                group_order="descending",
                                )

u235_capture_avg, u235_capture_rsd = godiva.get(resp="keff", mat="total", za="U-235", MT=102)

assert pu239_fission_avg.shape[-1] == jezebel.n_groups
assert u235_capture_avg.shape[-1] == godiva.n_groups
np.testing.assert_allclose(pu239_desc_avg.reshape(-1), pu239_fission_avg.reshape(-1)[::-1])

pd.DataFrame({
    "E_upper_MeV": jezebel.group_structure[1:],
    "Pu239_MT18_keff": pu239_fission_avg.reshape(-1),
    "Pu239_MT18_rsd": pu239_fission_rsd.reshape(-1),
}).head()
[5]:
E_upper_MeV Pu239_MT18_keff Pu239_MT18_rsd
0 1.000000e-07 0.000000e+00 0.00
1 5.400000e-07 0.000000e+00 0.00
2 4.000000e-06 7.354140e-10 1.00
3 8.315290e-06 1.025790e-09 0.73
4 1.370960e-05 8.119800e-09 0.52
[6]:
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(jezebel.group_structure[1:], pu239_fission_avg.reshape(-1), marker="o", label="Jezebel Pu-239 MT18")
ax.plot(godiva.group_structure[1:], u235_capture_avg.reshape(-1), marker="s", label="Godiva U-235 MT102")
ax.set_xscale("log")
ax.set_xlabel("Upper group boundary [MeV]")
ax.set_ylabel("Sensitivity coefficient")
ax.legend()
ax.grid(True, which="both", alpha=0.3)
fig.tight_layout()
../_images/tutorials_sensitivity_serpent_7_0.png

Merge Serpent files

Multiple Serpent files can be merged when their energy grids match. The keep_last policy is convenient for tutorials because it handles duplicate response/material/ZA/MT entries deterministically.

[7]:
merged = Sensitivity(list(serpent_files.values()), duplicate_policy="keep_last")

assert merged.reader == "serpent"
assert merged.n_groups == 33
assert "Pu-239" in merged.zais
assert "U-235" in merged.zais

avg_merged, rsd_merged = merged.get(resp="keff", mat="total", za="U-235", MT=18)
assert avg_merged.shape[-1] == merged.n_groups
print(f"Merged object: {merged.n_zai} nuclides, {merged.n_MTs} MTs, {merged.n_groups} groups")
Merged object: 8 nuclides, 55 MTs, 33 groups

Collapse to a coarse grid

collapse mutates the object, so this example works on a copy.

[8]:
godiva_fewG = copy.deepcopy(godiva)
idx = [0, 5, 11, 15, 22, 25, 33]
few_group = godiva_fewG.group_structure[idx]
godiva_fewG.collapse(fewgrp=few_group, egridname="tutorial-3G")

assert godiva_fewG.n_groups == len(idx) - 1
assert godiva_fewG.egridname == "tutorial-3G"
np.testing.assert_allclose(godiva_fewG.group_structure, few_group)
godiva_fewG.group_structure

u235_capture_fewG_avg, u235_capture_fewG_rsd = godiva_fewG.get(resp="keff", mat="total", za="U-235", MT=102)

Plot multigroup profiles (without normalisation per unit lethargy)

[9]:
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(godiva.group_structure[1:], u235_capture_avg.reshape(-1), marker="s", label="Godiva U-235 MT102")
ax.plot(godiva_fewG.group_structure[1:], u235_capture_fewG_avg.reshape(-1), marker="s", label="Godiva few G U-235 MT102")

ax.set_xscale("log")
ax.set_xlabel("Upper group boundary [MeV]")
ax.set_ylabel("Sensitivity coefficient")
ax.legend()
ax.grid(True, which="both", alpha=0.3)
fig.tight_layout()
../_images/tutorials_sensitivity_serpent_13_0.png

Normalise profiles per unit lethargy

[10]:
sens_fewG = godiva_fewG.NormalizeSensProfile(u235_capture_fewG_avg.reshape(-1), godiva_fewG.group_structure)
sens_33G = godiva.NormalizeSensProfile(u235_capture_avg.reshape(-1), godiva.group_structure)
[11]:
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(godiva.group_structure[1:], sens_33G, marker="s", label="Godiva U-235 MT102")
ax.plot(godiva_fewG.group_structure[1:], sens_fewG, marker="s", label="Godiva few G U-235 MT102")

ax.set_xscale("log")
ax.set_xlabel("Upper group boundary [MeV]")
ax.set_ylabel("Sensitivity coefficient")
ax.legend()
ax.grid(True, which="both", alpha=0.3)
fig.tight_layout()
../_images/tutorials_sensitivity_serpent_16_0.png