OpenMC Guide
Lattices & Universes
The Universe Concept
Universes are reusable containers that hold collections of cells. Think of them as templates - define a fuel pin universe once, then use it hundreds of times in different locations. This approach makes complex models manageable and modifications straightforward.
The hierarchy works like this: surfaces define boundaries, cells fill regions between surfaces, universes group related cells together, and lattices arrange universes in regular patterns.
import openmc
# Create a simple pin universe
def create_fuel_pin():
# Surfaces and regions (from geometry basics)
fuel_surf = openmc.ZCylinder(r=0.4096)
clad_surf = openmc.ZCylinder(r=0.4750)
fuel_region = -fuel_surf
clad_region = +fuel_surf & -clad_surf
# Cells
fuel_cell = openmc.Cell(fill=fuel_material, region=fuel_region)
clad_cell = openmc.Cell(fill=clad_material, region=clad_region)
# Universe groups the cells
pin_universe = openmc.Universe(cells=[fuel_cell, clad_cell])
return pin_universe
# Now this universe can be reused anywhere
fuel_pin = create_fuel_pin()Rectangular Lattices
Rectangular lattices arrange universes in a regular grid pattern, perfect for modeling square-pitch fuel assemblies and reactor cores.
# Create different pin types
fuel_pin = create_fuel_pin()
guide_tube = create_guide_tube() # Function creating guide tube universe
burnable_pin = create_burnable_pin() # Function creating burnable absorber
# Create 17x17 PWR assembly pattern
lattice = openmc.RectLattice()
lattice.lower_left = [-10.71, -10.71] # Position of corner
lattice.pitch = [1.26, 1.26] # Spacing between pins
# Define the pattern (17x17 grid)
# 'F' = fuel pin, 'G' = guide tube, 'B' = burnable absorber
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'],
# ... (continue pattern)
]
# Map letters to universes
universe_map = {'F': fuel_pin, 'G': guide_tube, 'B': burnable_pin}
# Convert pattern to universe array
lattice_universes = []
for row in pattern:
universe_row = [universe_map[symbol] for symbol in row]
lattice_universes.append(universe_row)
lattice.universes = lattice_universesHexagonal Lattices
Hexagonal lattices work similarly but arrange universes in a triangular grid, commonly used for CANDU, HTGR, and some fast reactor designs.
# Hexagonal lattice for CANDU-style geometry
hex_lattice = openmc.HexLattice()
hex_lattice.center = [0.0, 0.0]
hex_lattice.pitch = [1.26] # Single pitch value for hex
hex_lattice.orientation = 'y' # Flat-to-flat along y-axis
# Define rings of universes (center outward)
# Ring 0: center pin
# Ring 1: 6 pins around center
# Ring 2: 12 pins around ring 1
ring_0 = [fuel_pin]
ring_1 = [fuel_pin] * 6
ring_2 = [fuel_pin] * 12
hex_lattice.universes = [ring_0, ring_1, ring_2]
# Create assembly cell using hexagonal boundary
hex_boundary = openmc.model.hexagonal_prism(
edge_length=5.0, # Half-width of hexagon
orientation='y'
)
assembly_cell = openmc.Cell(fill=hex_lattice, region=hex_boundary)Nested Lattices
For reactor cores, you often need lattices within lattices - pin lattices form assemblies, and assembly lattices form cores.
# Step 1: Create pin lattice (assembly)
pin_lattice = create_assembly_lattice() # 17x17 pins
# Step 2: Put lattice in assembly cell
assembly_boundary = openmc.rectangular_prism(21.42, 21.42)
assembly_cell = openmc.Cell(fill=pin_lattice, region=assembly_boundary)
assembly_universe = openmc.Universe(cells=[assembly_cell])
# Step 3: Create core lattice using assemblies
core_lattice = openmc.RectLattice()
core_lattice.lower_left = [-193.86, -193.86] # 18 assemblies × 21.42 cm
core_lattice.pitch = [21.42, 21.42]
# Core loading pattern (18x18 assemblies)
# Different assembly types for different burnups/enrichments
fresh_assembly = create_fresh_assembly()
once_burned = create_once_burned_assembly()
twice_burned = create_twice_burned_assembly()
core_pattern = [
['F','F','O','O','T','T','O','O','F','F'], # Simplified pattern
['F','O','T','T','O','O','T','T','O','F'],
# ... (full core pattern)
]
pattern_map = {'F': fresh_assembly, 'O': once_burned, 'T': twice_burned}
core_universes = [[pattern_map[symbol] for symbol in row] for row in core_pattern]
core_lattice.universes = core_universesPartial Lattices
Sometimes you need only part of a lattice pattern. OpenMC handles this with slicing or by defining appropriate boundaries.
# Quarter-assembly model (common for PWR pin-cell studies)
full_lattice = create_17x17_lattice()
# Use only quarter of the lattice
quarter_boundary = openmc.rectangular_prism(10.71, 10.71)
quarter_boundary = quarter_boundary & +openmc.XPlane(0) & +openmc.YPlane(0)
quarter_cell = openmc.Cell(fill=full_lattice, region=quarter_boundary)
# Alternative: Create partial lattice directly
quarter_lattice = openmc.RectLattice()
quarter_lattice.lower_left = [0.0, 0.0]
quarter_lattice.pitch = [1.26, 1.26]
# Only define the quadrant you need
quarter_pattern = [
['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'],
# ... (9x9 pattern)
]
quarter_lattice.universes = [[fuel_pin for _ in row] for row in quarter_pattern]Common Patterns
Here are typical lattice patterns you'll encounter in reactor modeling:
# PWR 17x17 assembly with water gaps
def create_pwr_assembly():
lattice = openmc.RectLattice()
lattice.lower_left = [-10.71, -10.71]
lattice.pitch = [1.26, 1.26]
# Standard PWR pattern: 264 fuel pins, 25 guide tubes
pattern = []
for i in range(17):
row = []
for j in range(17):
if (i, j) in guide_tube_positions:
row.append(guide_tube)
else:
row.append(fuel_pin)
pattern.append(row)
lattice.universes = pattern
return lattice
# CANDU 37-element bundle
def create_candu_bundle():
hex_lattice = openmc.HexLattice()
hex_lattice.center = [0.0, 0.0]
hex_lattice.pitch = [1.66]
hex_lattice.orientation = 'y'
# Center + 2 rings = 37 elements total
rings = [
[fuel_pin], # Center
[fuel_pin] * 6, # First ring
[fuel_pin] * 12, # Second ring
[fuel_pin] * 18 # Third ring
]
hex_lattice.universes = rings
return hex_latticeThe key insight is that lattices make complex models manageable. Start with the smallest repeating unit (usually a fuel pin), build that into an assembly, then use assemblies to build a core.