OpenMC Guide
Example: Fuel Assembly
Building an Assembly
This example creates a 17×17 PWR fuel assembly using a hierarchical approach: define pin universes, arrange them in a lattice, then place the lattice in an assembly cell. This pattern scales to full core models.
Materials Definition
python
import openmc
import numpy as np
# =============================================================================
# MATERIALS
# =============================================================================
# UO2 fuel - 4.5% enriched
fuel = openmc.Material(name='UO2 Fuel')
fuel.set_density('g/cm3', 10.4)
fuel.add_nuclide('U235', 0.045)
fuel.add_nuclide('U238', 0.955)
fuel.add_element('O', 2.0)
# Zircaloy-4 cladding
clad = openmc.Material(name='Zircaloy-4')
clad.set_density('g/cm3', 6.55)
clad.add_element('Zr', 0.982)
clad.add_element('Sn', 0.015)
clad.add_element('Fe', 0.002)
clad.add_element('Cr', 0.001)
# Light water moderator
water = openmc.Material(name='Light Water')
water.set_density('g/cm3', 1.0)
water.add_nuclide('H1', 2.0)
water.add_element('O', 1.0)
water.add_s_alpha_beta('c_H_in_H2O')
# Stainless steel for guide tubes
steel = openmc.Material(name='Stainless Steel')
steel.set_density('g/cm3', 8.0)
steel.add_element('Fe', 0.68)
steel.add_element('Cr', 0.20)
steel.add_element('Ni', 0.12)
materials = openmc.Materials([fuel, clad, water, steel])Section of the same example script in examples repo
Pin Universe Definitions
python
# =============================================================================
# PIN UNIVERSES
# =============================================================================
def create_fuel_pin():
"""Standard fuel pin with cladding"""
fuel_radius = 0.4096
clad_inner = 0.4178
clad_outer = 0.4750
# Surfaces
fuel_surf = openmc.ZCylinder(r=fuel_radius)
clad_inner_surf = openmc.ZCylinder(r=clad_inner)
clad_outer_surf = openmc.ZCylinder(r=clad_outer)
# Regions
fuel_region = -fuel_surf
gap_region = +fuel_surf & -clad_inner_surf
clad_region = +clad_inner_surf & -clad_outer_surf
water_region = +clad_outer_surf
# Cells
fuel_cell = openmc.Cell(fill=fuel, region=fuel_region)
gap_cell = openmc.Cell(fill=None, region=gap_region)
clad_cell = openmc.Cell(fill=clad, region=clad_region)
water_cell = openmc.Cell(fill=water, region=water_region)
return openmc.Universe(cells=[fuel_cell, gap_cell, clad_cell, water_cell])
def create_guide_tube():
"""Guide tube for control rods"""
guide_inner = 0.5610
guide_outer = 0.6020
# Surfaces
guide_inner_surf = openmc.ZCylinder(r=guide_inner)
guide_outer_surf = openmc.ZCylinder(r=guide_outer)
# Regions
water_inner = -guide_inner_surf
tube_region = +guide_inner_surf & -guide_outer_surf
water_outer = +guide_outer_surf
# Cells
water_inner_cell = openmc.Cell(fill=water, region=water_inner)
tube_cell = openmc.Cell(fill=steel, region=tube_region)
water_outer_cell = openmc.Cell(fill=water, region=water_outer)
return openmc.Universe(cells=[water_inner_cell, tube_cell, water_outer_cell])
def create_instrumentation_tube():
"""Instrumentation tube (water-filled)"""
tube_inner = 0.5590
tube_outer = 0.6050
# Surfaces
tube_inner_surf = openmc.ZCylinder(r=tube_inner)
tube_outer_surf = openmc.ZCylinder(r=tube_outer)
# Regions
water_inner = -tube_inner_surf
tube_region = +tube_inner_surf & -tube_outer_surf
water_outer = +tube_outer_surf
# Cells
water_inner_cell = openmc.Cell(fill=water, region=water_inner)
tube_cell = openmc.Cell(fill=steel, region=tube_region)
water_outer_cell = openmc.Cell(fill=water, region=water_outer)
return openmc.Universe(cells=[water_inner_cell, tube_cell, water_outer_cell])
# Create the pin universes
fuel_pin = create_fuel_pin()
guide_tube = create_guide_tube()
instr_tube = create_instrumentation_tube()Section of the same example script in examples repo
Assembly Lattice
python
# =============================================================================
# ASSEMBLY LATTICE
# =============================================================================
# Create 17x17 lattice
lattice = openmc.RectLattice(name='Assembly Lattice')
lattice.lower_left = [-10.71, -10.71] # Half of 17 * 1.26 cm pitch
lattice.pitch = [1.26, 1.26]
# Define assembly pattern
# Standard Westinghouse 17x17 assembly layout
# F = fuel pin, G = guide tube, I = instrumentation tube
assembly_pattern = [
['F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F'],
['F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F'],
['F','F','F','F','F','G','F','F','G','F','F','G','F','F','F','F','F'],
['F','F','F','G','F','F','F','F','F','F','F','F','F','G','F','F','F'],
['F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F'],
['F','F','G','F','F','G','F','F','G','F','F','G','F','F','G','F','F'],
['F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F'],
['F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F'],
['F','F','G','F','F','G','F','F','I','F','F','G','F','F','G','F','F'],
['F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F'],
['F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F'],
['F','F','G','F','F','G','F','F','G','F','F','G','F','F','G','F','F'],
['F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F'],
['F','F','F','G','F','F','F','F','F','F','F','F','F','G','F','F','F'],
['F','F','F','F','F','G','F','F','G','F','F','G','F','F','F','F','F'],
['F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F'],
['F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F','F']
]
# Convert pattern to universe array
universe_map = {'F': fuel_pin, 'G': guide_tube, 'I': instr_tube}
lattice_universes = []
for row in assembly_pattern:
universe_row = [universe_map[symbol] for symbol in row]
lattice_universes.append(universe_row)
lattice.universes = lattice_universesSection of the same example script in examples repo
Assembly Cell and Geometry
python
# =============================================================================
# ASSEMBLY GEOMETRY
# =============================================================================
# Assembly boundary (21.42 cm square)
pitch = 1.26
assembly_size = 17 * pitch
# Create assembly boundary
assembly_left = openmc.XPlane(-assembly_size/2, boundary_type='reflective')
assembly_right = openmc.XPlane(assembly_size/2, boundary_type='reflective')
assembly_bottom = openmc.YPlane(-assembly_size/2, boundary_type='reflective')
assembly_top = openmc.YPlane(assembly_size/2, boundary_type='reflective')
assembly_lower = openmc.ZPlane(-200, boundary_type='vacuum')
assembly_upper = openmc.ZPlane(200, boundary_type='vacuum')
# Assembly region
assembly_region = (+assembly_left & -assembly_right &
+assembly_bottom & -assembly_top &
+assembly_lower & -assembly_upper)
# Assembly cell
assembly_cell = openmc.Cell(fill=lattice, region=assembly_region)
# Root universe and geometry
root_universe = openmc.Universe(cells=[assembly_cell])
geometry = openmc.Geometry(root_universe)Section of the same example script in examples repo
Simulation Settings
python
# =============================================================================
# SIMULATION SETTINGS
# =============================================================================
settings = openmc.Settings()
settings.particles = 10000
settings.batches = 120
settings.inactive = 20
# Source distribution across the assembly
source_box = openmc.stats.Box(
[-assembly_size/2, -assembly_size/2, -100],
[assembly_size/2, assembly_size/2, 100]
)
settings.source = openmc.IndependentSource(space=source_box)
# Set temperature on cells (temperature is a Cell property, not Material)
# In the fuel pin function above, you would set:
# fuel_cell.temperature = 900 # K
# gap_cell.temperature = 600 # K
# clad_cell.temperature = 600 # K
# water_cell.temperature = 574 # K
# Here we apply it after geometry is built:
for cell in geometry.get_all_cells().values():
if cell.fill == fuel:
cell.temperature = 900 # K
elif cell.fill == clad or cell.fill == steel:
cell.temperature = 600 # K
elif cell.fill == water:
cell.temperature = 574 # KSection of the same example script in examples repo
Tallies
python
# =============================================================================
# TALLIES
# =============================================================================
tallies = openmc.Tallies()
# Assembly-averaged flux and reaction rates
assembly_tally = openmc.Tally(name='assembly_avg')
assembly_tally.filters = [openmc.CellFilter(assembly_cell)]
assembly_tally.scores = ['flux', 'fission', 'absorption', 'nu-fission']
tallies.append(assembly_tally)
# Pin-by-pin power distribution
mesh = openmc.RegularMesh()
mesh.lower_left = [-assembly_size/2, -assembly_size/2]
mesh.upper_right = [assembly_size/2, assembly_size/2]
mesh.dimension = [17, 17] # One bin per pin
mesh_tally = openmc.Tally(name='pin_powers')
mesh_tally.filters = [openmc.MeshFilter(mesh)]
mesh_tally.scores = ['kappa-fission'] # Energy released per fission
tallies.append(mesh_tally)
# Energy spectrum in fuel regions
energy_bins = np.logspace(-3, 7, 50) # 1 meV to 10 MeV
energy_filter = openmc.EnergyFilter(energy_bins)
spectrum_tally = openmc.Tally(name='fuel_spectrum')
spectrum_tally.filters = [openmc.MaterialFilter(fuel), energy_filter]
spectrum_tally.scores = ['flux']
tallies.append(spectrum_tally)Section of the same example script in examples repo
Running the Model
python
# =============================================================================
# RUN SIMULATION
# =============================================================================
# Create complete model
model = openmc.Model(geometry, materials, settings, tallies)
# Run simulation (model.run() returns path to statepoint)
statepoint_path = model.run()
sp = openmc.StatePoint(statepoint_path)
keff = sp.keff
print(f"k-effective: {keff.nominal_value:.5f}")
print(f"Standard deviation: {keff.std_dev:.5f}")
# =============================================================================
# BASIC ANALYSIS
# =============================================================================
# Load results
sp = openmc.StatePoint('statepoint.120.h5')
# Get pin power distribution
pin_powers = sp.get_tally(name='pin_powers')
power_array = pin_powers.mean.reshape((17, 17))
# Simple power peaking analysis
max_power = np.max(power_array)
avg_power = np.mean(power_array)
peaking_factor = max_power / avg_power
print(f"Peak-to-average power ratio: {peaking_factor:.3f}")
# Find location of peak power
peak_location = np.unravel_index(np.argmax(power_array), power_array.shape)
print(f"Peak power at pin ({peak_location[0]+1}, {peak_location[1]+1})")Section of the same example script in examples repo
The same hierarchical pattern extends to full core models, where assemblies become the building blocks.