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.

python
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.

python
# 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_universes

Hexagonal Lattices

Hexagonal lattices work similarly but arrange universes in a triangular grid, commonly used for CANDU, HTGR, and some fast reactor designs.

python
# 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.

python
# 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_universes

Partial Lattices

Sometimes you need only part of a lattice pattern. OpenMC handles this with slicing or by defining appropriate boundaries.

python
# 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:

python
# 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_lattice

The 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.