Photon transport effect on neutron flux and MGXS results

Hi all,
I’ve been trying to use to produce multigroup cross sections using the MGXS module, and compare them to cross-sections obtained from tallying the multigroup flux and running GROUPR.

As a minimal example, I am considering a sphere of NaClPuCl3 with a radius of r=35cm and a spherical reflector of SiC with a radius of r=200cm. I am trying to compute the multigroup cross-section of the (n,p) reaction of Cl35 using the ECCO-1968, ECCO-33 and 1G energy meshes. I am using JEFF-3.3 cross-sections as provided on the OpenMC website.

Here is the complete script for running the case:

import openmc
import numpy as np
import pandas as pd
import argparse

openmc.config["cross_sections"] = "/data/nuclear_data/endfb8/HDF5/cross_sections.xml"

ecco33 = np.array(
    [
        1.000010e-05,
        1.000000e-01,
        5.400000e-01,
        4.000000e00,
        8.315287e00,
        1.370959e01,
        2.260329e01,
        4.016900e01,
        6.790405e01,
        9.166088e01,
        1.486254e02,
        3.043248e02,
        4.539993e02,
        7.485183e02,
        1.234098e03,
        2.034684e03,
        3.354626e03,
        5.530844e03,
        9.118820e03,
        1.503439e04,
        2.478752e04,
        4.086771e04,
        6.737947e04,
        1.110900e05,
        1.831564e05,
        3.019738e05,
        4.978707e05,
        8.208500e05,
        1.353353e06,
        2.231302e06,
        3.678794e06,
        6.065307e06,
        1.000000e07,
        1.964033e07,
    ]
)
GROUP_STRUCTURES = { "ECCO-1968": openmc.mgxs.GROUP_STRUCTURES["ECCO-1968"],
                    "ECCO-33": ecco33,
                    "1G": np.array([1e-5, 2e7])
                    }


def build_model():
    pu = openmc.Material(name="Pu")
    pu.add_components(
        {"Pu238": 1, "Pu239": 50, "Pu240": 35, "Pu241": 7, "Pu242": 7},
        percent_type="wo",
    )
    cl = openmc.Material(name="Cl")
    cl.add_element("Cl", percent=1)
    na = openmc.Material(name="Na")
    na.add_element("Na", percent=1)

    fuel = openmc.Material.mix_materials(
        [pu, cl, na], [1 / 6, 4 / 6, 1/6], percent_type="ao")
    fuel.set_density("g/cm3", 3)

    sic = openmc.Material()
    sic.add_elements_from_formula("SiC")

    boron = openmc.Material()
    boron.add_element("B", percent=1)

    ppm = 1e-6
    refl = openmc.Material.mix_materials([sic, boron], [1 - 1500*ppm, 1500*ppm])
    refl.set_density("g/cm3", 3)
    
    water = openmc.Material()
    water.add_elements_from_formula("H2O")
    water.set_density("g/cm3", 1)

    materials = openmc.Materials([fuel, refl])

    sphere = openmc.Sphere(r=35, boundary_type="transmission")
    reflector = openmc.Sphere(r=200, boundary_type="vacuum")

    cells = [
        openmc.Cell(name="Fuel", region=-sphere, fill=fuel),
        openmc.Cell(name="Reflector", region=-reflector & +sphere, fill=refl),
    ]
    root = openmc.Universe(cells=cells)
    geometry = openmc.Geometry(root=root)

    source = openmc.IndependentSource(space=openmc.stats.Point((25, 0, 0)))
    settings = openmc.Settings()
    settings.source = source
    settings.batches = 100
    settings.inactive = 10
    settings.particles = 1_000_000
    settings.ptables = False
    settings.output = {'tallies': False}
    settings.temperature = {"method": "interpolation"}
    settings.photon_transport = True

    plot = openmc.Plot()
    plot.basis = "yz"
    plot.pixels = (1000, 1000)
    plot.origin = (0., 0., 0.)
    plot.width = (100, 100)
    plot.color_by = "material"
    plots = openmc.Plots(plots=[plot])

    tallies = openmc.Tallies()
    for gname, g in GROUP_STRUCTURES.items():
        tally = openmc.Tally(name=f"Spectrum {gname}")
        tally.scores = ["flux"]
        tally.filters = [
            openmc.MaterialFilter([fuel]),
            openmc.EnergyFilter(GROUP_STRUCTURES[gname]),
        ]
        tallies.append(tally)

    xs = {
        "ECCO-1968": {},
        "ECCO-33": {},
        "1G": {}
    }
    for gname, g in GROUP_STRUCTURES.items():
        groups = openmc.mgxs.EnergyGroups(group_edges=g)
        for r in ["(n,gamma)", "(n,p)"]:
            xs[gname][r] = openmc.mgxs.ArbitraryXS(
                r,
                energy_groups=groups,
                domain=fuel,
                by_nuclide=True,
            )

        xs[gname]["fission"] = openmc.mgxs.FissionXS(
            energy_groups=groups,
            domain=fuel,
            by_nuclide=True,
        )

    for g in xs:
        for r in xs[g]:
            tallies+= xs[g][r].tallies.values()

    model = openmc.Model(
        materials=materials, geometry=geometry, settings=settings, tallies=tallies, plots=plots
    )
    return model, xs

parser = argparse.ArgumentParser(prog='SphericalModel')
parser.add_argument("resdir")
parser.add_argument("--photons", "-p", action="store_true")

if __name__ == "__main__":
    args = parser.parse_args()
    model, xs = build_model()
    model.plot_geometry(cwd=args.resdir)
    model.settings.photon_transport = args.photons
    model.run(cwd=args.resdir, 
              mpi_args=["mpirun", "-n", "4"], 
              export_model_xml=False)

    batches = model.settings.batches
    with openmc.StatePoint(f"{args.resdir}/statepoint.{batches}.h5") as sp:
        for gname, energies in GROUP_STRUCTURES.items():
            tally = sp.get_tally(name=f"Spectrum {gname}")
            avg = tally.mean.ravel()
            unc = tally.std_dev.ravel()

            spectrum_data = {
                "Emin": np.array([]),
                "Emax": np.array([]),
                "Flux": np.array([]),
                "Flux std. dev.": np.array([]),
                "Flux/du": np.array([]),
                "Flux/du std. dev.": np.array([]),
            }
            lethargy = -np.log(20e6 / energies)
            spectrum_data["Emin"] = energies[:-1]
            spectrum_data["Emax"] = energies[1:]
            spectrum_data["Flux"] = avg
            spectrum_data["Flux std. dev."] = unc
            spectrum_data["Flux/du"] = avg / np.diff(lethargy)
            spectrum_data["Flux/du std. dev."] = unc / np.diff(lethargy)

            df = pd.DataFrame(data=spectrum_data)
            df.to_csv(f"{args.resdir}/spectrum-{gname}.csv", index=False)

        for groupstruct in GROUP_STRUCTURES:
            for r in ["(n,gamma)", "(n,p)", "fission"]:
                xs[groupstruct][r].load_from_statepoint(sp)
                xs[groupstruct][r].build_hdf5_store(directory=args.resdir, 
                                                    filename=f"{groupstruct}.h5", 
                                                    xs_type="micro")

Here is the script for running NJOY and processing the results in a pdf:

import pandas as pd
import sandy
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
import h5py
import openmc
import openmc.mgxs
from pathlib import Path

name_to_energy = {
    "ECCO-33": sandy.energy_grids.ECCO33,
    "ECCO-1968": openmc.mgxs.GROUP_STRUCTURES["ECCO-1968"],
    "1G": np.array([1e-5, 2e7]),
}
ignnames = {
    2: "CSEWG-239",
    19: "ECCO-33",
    20: "ECCO-1968",
}
MTnames = {
    102: "(n,gamma)",
    103: "(n,p)",
    "(n,gamma)": 102,
    "(n,p)": 103,
}

def gendf(results="results"):
    df = pd.read_csv(f"{results}/spectrum-ECCO-1968.csv")
    df["DE"] = df["Emax"] - df["Emin"]
    tapes = {}
    tape = sandy.get_endf6_file("jeff_33", "xs", 170350)

    spectrum = np.array([df["Emin"], df["Flux"]/df["DE"]]).T.flatten()
    tapes["1G"] = tape.get_gendf(temperature=294, groupr_kws={"spectrum": spectrum, "ek": [1e-5, 2e7]})
    Path(f"{results}/gendf/1G/").mkdir(parents=True, exist_ok=True)
    tapes["1G"].to_file(f"{results}/gendf/1G/Cl35.gendf")

    for ign in [2, 19, 20]:
        tapes[ign] = tape.get_gendf(temperature=294, groupr_kws={"spectrum": spectrum, "ign": ign})
        Path(f"{results}/gendf/{ignnames[ign]}/").mkdir(parents=True, exist_ok=True)
        tapes[ign].to_file(f"{results}/gendf/{ignnames[ign]}/Cl35.gendf")

def fullplot(gname, mt, xlim=(8e-6, 1e8), results="results"):
    mtname = MTnames[mt]
    fig, axes = plt.subplots(2, 1, figsize=(11.69,8.27), height_ratios=[3, 1], sharex=True)
    ax = axes[0]
    with h5py.File("/data/nuclear_data/jeff33/HDF5/Cl35.h5") as f:
        energy = f["Cl35/energy/294K"][...]
        xs = f[f"Cl35/reactions/reaction_{mt}/294K/xs"][...]
        ax.plot(energy, xs, label="Continuous")

    with h5py.File(f"{results}/{gname}.h5") as f:
        xs = f[f"material/4/{mtname}/Cl35/average"][...][::-1]
        std = f[f"material/4/{mtname}/Cl35/std. dev."][...][::-1]
        energy = name_to_energy[gname]
        if len(xs) == 1:
            mgxs_toplot = [xs[0], xs[0]]
            ax.plot(energy, mgxs_toplot, drawstyle="steps-post", label=f"{gname} MGXS")
        else:
            mgxs_toplot = xs
            ax.plot(energy[:-1], xs, drawstyle="steps-post", label=f"{gname} MGXS")
            ax.fill_between(energy[:-1], xs - std, xs + std, step="post", color="tab:orange", alpha=0.3)
    
    gendf = sandy.gendf.Gendf.from_file(f"{results}/gendf/{gname}/Cl35.gendf")
    xs = gendf.get_xs(mt=[mt]).data
    Eleft = np.array([i.left for i in xs.index])
    Eright = np.array([i.right for i in xs.index])
    njoy_toplot = xs[1725, mt]
    if len(njoy_toplot) == 1:
        njoy_toplot = [njoy_toplot.values[0], njoy_toplot.values[0]]
        ax.plot([Eleft, Eright], njoy_toplot, drawstyle="steps-post", label=f"{gname} NJOY")
    else:
        ax.plot(Eleft, njoy_toplot, drawstyle="steps-post", label=f"{gname} NJOY")
    njoy_toplot = np.array(njoy_toplot)

    ax.set_xscale("log")
    ax.set_yscale("log")
    ax.set_xlim(*xlim)
    # ax.set_ylim(1e-5, 1)
    ax.set_ylabel("Cross-section [barn]")
    ax.legend(loc="lower left")

    ax1 = ax.twinx()
    df = pd.read_csv(f"{results}/spectrum-ECCO-1968.csv")
    df["DE"] = df["Emax"] - df["Emin"]
    flux = np.array(df["Flux"]/df["DE"])
    ax1.plot(df["Emin"], flux, ds="steps-post", color="tab:red", label="Spectrum")
    top = (df["Flux"] + 3*df["Flux std. dev."])/df["DE"]
    bot = (df["Flux"] - 3*df["Flux std. dev."])/df["DE"]
    ax1.fill_between(df["Emin"], top, bot, step="post", color="tab:red", alpha=0.3)
    ax1.legend()
    ax1.set_ylim(0.9*np.min(flux), 1.1*np.max(flux))
    ax1.set_yscale("log")
    ax1.set_ylabel(r"Flux [$\mathrm{neutron}\cdot \mathrm{cm} \cdot \mathrm{cm}^{-3} \cdot \mathrm{s}^{-1} \cdot \mathrm{sp}^{-1}$]")
    ax.grid(which="both")
    ax.set_title(f"{mtname} cross-section of Cl35")

    ax = axes[1]
    ax.set_xlabel("Energy [eV]")
    ax.set_ylabel("Relative difference [%]")
    xs = np.array(xs[1725, 103])
    val = abs(njoy_toplot-mgxs_toplot)/mgxs_toplot * 100
    if len(Eleft) == 1:
        ax.plot([Eleft, Eright], val, color="k")
    else:
        ax.plot(Eleft, val, ds="steps-post", color="k")
    ax.set_xscale("log")
    ax.grid()
    fig.tight_layout()
    return fig, axes
    

if __name__=="__main__":
    for results in ["photons", "nophotons"]:
        gendf(results)
        with PdfPages(f"{results}.pdf") as pdf:
            fig, ax = fullplot("ECCO-1968", mt=103, results=results)
            pdf.savefig(fig)

            fig, ax = fullplot("ECCO-1968", mt=103, xlim=(1e2, 1e6), results=results)
            pdf.savefig(fig)

            fig, ax = fullplot("ECCO-1968", mt=103, xlim=(1.5e4, 2e4), results=results)
            pdf.savefig(fig)

            fig, ax = fullplot("ECCO-33", mt=103, results=results)
            pdf.savefig(fig)

            fig, ax = fullplot("1G", mt=103, results=results)
            pdf.savefig(fig)

Without photon transport

When photon transport is disabled, I get the following result:

Beside low energies when the tally uncertainty is too large, and some resonnances which are not correctly meshed by the energy discretization, the cross-sections produced by MGXS and by NJOY are in good agreement. The one group cross-section (in which I am most interested for back-of-the-napkin calculations) show less than a percent of discrepancy:


I find this quite satisfactory!

With photon transport

When enabling coupled photon-neutron transport however I get the following:


For energies > 1e4 eV, the discrepancies between MGXS and NJOY are much larger, and we can also see additionnal “positive” resonnances in the flux. The 1 group calculation results in a relative difference of 70% percent between the two methods making the values somewhat dubious.

I was under the impression that photon transport in OpenMC was mostly aimed at gamma source calculations and accurate heat deposition, and was surprised it had such a large effect on the neutron flux and the MGXS results. Does anyone have an explanation for this?

Best regards,
Nicolas

Update: I fixed the flux issue by setting a ParticleFilter("neutron") to the flux tally, seems like I was tallying both neutron and photon flux accidentaly.

The MGXS results however are still in large disagreement with NJOY and with the neutron-only calculation: