3.1.2. The One-Dimensional Reed Problem

Reed’s problem is a one-group slab-transport benchmark containing strong absorbers, a void, and scattering regions with and without sources. These abrupt material changes produce a scalar-flux profile that is useful for exercising spatial and angular discretizations. OpenSn represents a one-dimensional slab along the \(z\) axis, so the benchmark coordinate commonly written as \(x\) is denoted by \(z\) here.

3.1.2.1. Import the OpenSn objects

[ ]:
if "opensn_console" not in globals():
    from mpi4py import MPI
    from pyopensn.aquad import GLProductQuadrature1DSlab
    from pyopensn.context import Finalize, UseColor
    from pyopensn.fieldfunc import FieldFunctionInterpolationVolume
    from pyopensn.logvol import RPPLogicalVolume
    from pyopensn.mesh import OrthogonalMeshGenerator
    from pyopensn.solver import DiscreteOrdinatesProblem, SteadyStateSourceSolver
    from pyopensn.source import VolumetricSource
    from pyopensn.xs import MultiGroupXS

    rank = MPI.COMM_WORLD.rank
    UseColor(False)

3.1.2.2. Create the slab mesh

The five regions meet at 2, 3, 5, and 6 cm. Placing mesh nodes exactly at these interfaces prevents any cell from spanning two materials. Each region contains 200 cells, matching OpenSn’s Reed regression problem.

[ ]:
region_edges = [0.0, 2.0, 3.0, 5.0, 6.0, 8.0]
cells_per_region = 200
nodes = [region_edges[0]]

for left, right in zip(region_edges[:-1], region_edges[1:]):
    dz = (right - left) / cells_per_region
    nodes.extend(left + i * dz for i in range(1, cells_per_region + 1))

mesh = OrthogonalMeshGenerator(node_sets=[nodes]).Execute()
mesh.SetOrthogonalBoundaries()

3.1.2.3. Define the material and source regions

The total cross section \(\sigma_t\), scattering cross section \(\sigma_s\), and isotropic source strength \(Q\) are piecewise constant:

Reed problem

Block ID

Interval (cm)

Region

\(\sigma_t\) (cm\(^{-1}\))

\(\sigma_s\) (cm\(^{-1}\))

\(Q\)

0

\([0,2]\)

absorber and source

50

0

50

1

\([2,3]\)

absorber

5

0

0

2

\([3,5]\)

void

0

0

0

3

\([5,6]\)

scatterer and source

1

0.9

1

4

\([6,8]\)

scatterer

1

0.9

0

CreateSimpleOneGroup accepts the scattering ratio \(c=\sigma_s/\sigma_t\). The void uses \(c=0\) because both cross sections vanish.

[ ]:
sigma_t = [50.0, 5.0, 0.0, 1.0, 1.0]
scattering_ratio = [0.0, 0.0, 0.0, 0.9, 0.9]
xs_map = []

for block_id, (left, right, total, ratio) in enumerate(
    zip(region_edges[:-1], region_edges[1:], sigma_t, scattering_ratio)
):
    region = RPPLogicalVolume(
        infx=True, infy=True, zmin=left, zmax=right
    )
    mesh.SetBlockIDFromLogicalVolume(region, block_id, True)

    xs = MultiGroupXS()
    xs.CreateSimpleOneGroup(sigma_t=total, c=ratio)
    xs_map.append({"block_ids": [block_id], "xs": xs})

absorber_source = VolumetricSource(
    block_ids=[0], group_strength=[50.0]
)
scattering_source = VolumetricSource(
    block_ids=[3], group_strength=[1.0]
)

3.1.2.4. Assemble and solve the transport problem

A 128-direction Gauss–Legendre quadrature resolves the angular dependence in slab geometry. Vacuum boundary conditions at both ends allow outgoing particles to leave the domain without returning.

[ ]:
quadrature = GLProductQuadrature1DSlab(
    n_polar=128, scattering_order=0
)

problem = DiscreteOrdinatesProblem(
    mesh=mesh,
    num_groups=1,
    groupsets=[
        {
            "groups_from_to": (0, 0),
            "angular_quadrature": quadrature,
            "inner_linear_method": "petsc_gmres",
            "l_abs_tol": 1.0e-9,
            "l_max_its": 300,
            "gmres_restart_interval": 30,
        }
    ],
    xs_map=xs_map,
    volumetric_sources=[absorber_source, scattering_source],
    boundary_conditions=[
        {"name": "zmin", "type": "vacuum"},
        {"name": "zmax", "type": "vacuum"},
    ],
)

solver = SteadyStateSourceSolver(problem=problem)
solver.Initialize()
solver.Execute()

3.1.2.5. Verify the scalar flux

The scalar flux reaches its maximum in the source-bearing scattering region. Evaluating that maximum supplies a compact, deterministic regression check for the complete calculation.

[ ]:
scalar_flux = problem.GetScalarFluxFieldFunction()[0]
whole_domain = RPPLogicalVolume(
    infx=True, infy=True, zmin=region_edges[0], zmax=region_edges[-1]
)
maximum = FieldFunctionInterpolationVolume()
maximum.SetOperationType("max")
maximum.SetLogicalVolume(whole_domain)
maximum.AddFieldFunction(scalar_flux)
maximum.Execute()
maximum_flux = maximum.GetValue()

if rank == 0:
    print(f"REED_MAX_FLUX={maximum_flux:.8e}")

3.1.2.6. Plot the one-dimensional flux

FieldFunctionInterpolationLine samples the scalar flux along the slab, and ExportToCSV writes the sampled coordinates and values. The following optional plotting code is kept non-executable so regression tests do not create output files. Run it in a new code cell to regenerate the figure.

import csv
from glob import glob
import matplotlib.pyplot as plt

from pyopensn.fieldfunc import FieldFunctionInterpolationLine
from pyopensn.math import Vector3

line = FieldFunctionInterpolationLine()
line.SetInitialPoint(Vector3(0.0, 0.0, region_edges[0]))
line.SetFinalPoint(Vector3(0.0, 0.0, region_edges[-1]))
line.SetNumberOfPoints(801)
line.AddFieldFunction(scalar_flux)
line.Execute()
line.ExportToCSV("reed_flux")

if rank == 0:
    csv_filename = glob("reed_flux_*.csv")[0]
    with open(csv_filename, newline="") as csv_file:
        rows = list(csv.DictReader(csv_file))

    z = [float(row["z"]) for row in rows]
    flux = [float(row["phi_g000_m00"]) for row in rows]

    plt.plot(z, flux, color="#7b2cbf", linewidth=2)
    for interface in region_edges[1:-1]:
        plt.axvline(interface, color="0.75", linewidth=0.8)
    plt.xlabel("Position, $z$ (cm)")
    plt.ylabel(r"Scalar flux, $\phi(z)$")
    plt.grid(alpha=0.25)
    plt.tight_layout()
    plt.show()

Scalar flux for the one-dimensional Reed problem

3.1.2.7. Next steps

Compare the flux after changing the number of cells or polar directions. The absorber–void interfaces make this problem particularly useful for studying spatial refinement, while the scattering regions expose changes in angular resolution.

[ ]:
if "opensn_console" not in globals():
    from IPython import get_ipython

    if get_ipython() is not None:
        Finalize()
        MPI.Finalize()