3.1.5. A Nested-Square Fixed-Source Problem
This two-dimensional problem demonstrates transport through three nested material regions. A unit isotropic source fills the lower-left square, a highly scattering shell surrounds it, and a more absorbing background fills the remainder of the domain. Particles are removed by absorption or escape through the vacuum exterior boundaries.
3.1.5.1. Import the OpenSn objects
[ ]:
if "opensn_console" not in globals():
from mpi4py import MPI
from pyopensn.aquad import GLCProductQuadrature2DXY
from pyopensn.context import Finalize, UseColor
from pyopensn.logvol import RPPLogicalVolume
from pyopensn.mesh import OrthogonalMeshGenerator
from pyopensn.post import VolumePostprocessor
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.5.2. Create the mesh
The domain is the square \([0,10]\times[0,10]\) cm. Ten cells per centimeter place mesh nodes on the material interfaces at 2 and 4 cm. As with the other two-dimensional tutorials, the calculation represents a unit thickness in the \(z\) direction.
[ ]:
domain_length = 10.0
cells_per_cm = 10
num_cells = int(domain_length * cells_per_cm)
nodes = [i / cells_per_cm for i in range(num_cells + 1)]
mesh = OrthogonalMeshGenerator(node_sets=[nodes, nodes]).Execute()
mesh.SetOrthogonalBoundaries()
3.1.5.3. Assign the nested material regions
Region I occupies \([0,2]\times[0,2]\) cm. Region II is the part of \([0,4]\times[0,4]\) cm outside Region I, and Region III fills the rest of the domain.

The mesh is initialized as Region III. Region II is then assigned over the 4 cm square, followed by Region I over the 2 cm square. The final assignment overwrites the cells in the source region.
[ ]:
REGION_I = 0
REGION_II = 1
REGION_III = 2
mesh.SetUniformBlockID(REGION_III)
region_ii_extent = RPPLogicalVolume(
xmin=0.0, xmax=4.0,
ymin=0.0, ymax=4.0,
infz=True,
)
mesh.SetBlockIDFromLogicalVolume(region_ii_extent, REGION_II, True)
region_i_extent = RPPLogicalVolume(
xmin=0.0, xmax=2.0,
ymin=0.0, ymax=2.0,
infz=True,
)
mesh.SetBlockIDFromLogicalVolume(region_i_extent, REGION_I, True)
3.1.5.4. Define the materials and source
For each material, CreateSimpleOneGroup receives the total cross section \(\sigma_t\) and scattering ratio \(c=\sigma_s/\sigma_t\).
Region |
\(\sigma_t\) (cm\(^{-1}\)) |
\(c\) |
\(\sigma_s\) (cm\(^{-1}\)) |
Source (cm\(^{-3}\) s\(^{-1}\)) |
|---|---|---|---|---|
I |
1.0 |
0.90 |
0.90 |
1.0 |
II |
1.5 |
0.96 |
1.44 |
0.0 |
III |
1.0 |
0.30 |
0.30 |
0.0 |
Only Region I is selected by the volumetric source.
[ ]:
material_data = [
(REGION_I, 1.0, 0.90),
(REGION_II, 1.5, 0.96),
(REGION_III, 1.0, 0.30),
]
xs_map = []
for block_id, sigma_t, scattering_ratio in material_data:
xs = MultiGroupXS()
xs.CreateSimpleOneGroup(sigma_t=sigma_t, c=scattering_ratio)
xs_map.append({"block_ids": [block_id], "xs": xs})
source = VolumetricSource(
block_ids=[REGION_I], group_strength=[1.0]
)
3.1.5.5. Assemble and solve the transport problem
A Gauss–Legendre–Chebyshev quadrature represents particle directions in the \(xy\) plane. All four exterior faces are vacuum boundaries.
[ ]:
quadrature = GLCProductQuadrature2DXY(
n_polar=2, n_azimuthal=32, 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=[source],
boundary_conditions=[
{"name": "xmin", "type": "vacuum"},
{"name": "xmax", "type": "vacuum"},
{"name": "ymin", "type": "vacuum"},
{"name": "ymax", "type": "vacuum"},
],
)
solver = SteadyStateSourceSolver(problem=problem, compute_balance=True)
solver.Initialize()
solver.Execute()
3.1.5.6. Check the source and reaction-rate balance
The source occupies a \(2\times2\) cm square and has unit strength, so its integrated rate is 4 particles per second per unit depth. The region-wise absorption rates are
Their sum and the outward leakage should reproduce the source rate. The source-region average flux provides a compact regression quantity for the spatial solution.
[ ]:
absorption_rates = {}
for block_id, _, _ in material_data:
postprocessor = VolumePostprocessor(
problem=problem,
value_type="integral",
block_ids=[block_id],
xs_multiplier="sigma_a",
)
postprocessor.Execute()
absorption_rates[block_id] = postprocessor.GetValue()[0][0]
source_flux_postprocessor = VolumePostprocessor(
problem=problem, value_type="avg", block_ids=[REGION_I]
)
source_flux_postprocessor.Execute()
source_region_average_flux = source_flux_postprocessor.GetValue()[0][0]
balance = solver.ComputeBalanceTable()
source_rate = balance["production_rate"] + balance["inflow_rate"]
loss_rate = balance["absorption_rate"] + balance["outflow_rate"]
relative_balance_error = abs(source_rate - loss_rate) / source_rate
total_absorption_rate = sum(absorption_rates.values())
if rank == 0:
print(f"NESTED_SQUARE_SOURCE_RATE={source_rate:.8e}")
print(f"NESTED_SQUARE_SOURCE_AVG_FLUX={source_region_average_flux:.8e}")
print(f"NESTED_SQUARE_ABSORPTION_RATE={total_absorption_rate:.8e}")
print(f"NESTED_SQUARE_BALANCE_ERROR={relative_balance_error:.8e}")
assert abs(source_rate - 4.0) < 1.0e-8
assert abs(total_absorption_rate - balance["absorption_rate"]) < 1.0e-10
assert relative_balance_error < 1.0e-8
3.1.5.7. Export and visualize the scalar flux
After running the generated Python input, use the following code to export the scalar flux for visualization. It is kept in Markdown so the regression test does not create output files.
from pyopensn.fieldfunc import FieldFunctionGridBased
scalar_flux = problem.GetScalarFluxFieldFunction()[0]
FieldFunctionGridBased.ExportMultipleToPVTU(
[scalar_flux], "Flux/NestedSquare_Phi"
)

3.1.5.8. Next steps
Compare the absorption rate in each material, refine the mesh near the reentrant corners, or increase the azimuthal resolution to examine angular discretization effects.
[ ]:
if "opensn_console" not in globals():
from IPython import get_ipython
if get_ipython() is not None:
Finalize()
MPI.Finalize()