OpenMC Guide
Python API Basics
What you'll learn
- Name the object types every OpenMC model is built from, and how they nest.
- Assemble a model with openmc.Model rather than exporting each XML file by hand.
- Read k-effective back out of a completed run.
Before you start
- A working OpenMC install
- Basic Python: variables, lists, and objects
The object graph, and why it replaces a card deck
A card deck describes a model as text: cards reference other cards by number, and the solver parses the whole file before it can check anything. The Python API describes the same model as live objects that reference each other directly, and every object is Pythonic enough to inspect, print, or feed to numpy and matplotlib the moment you create it.
import openmc
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)
print(f"Fuel density: {fuel.density} g/cm³")
print(f"Fuel contains {len(fuel.nuclides)} nuclides")Tutorial snippet — no separate file in examples repo
Every model follows the same chain of object types, each one built from the last: Material objects get referenced by Cell fills; Surface objects become Regions once you take a half-space of them, and a Cell pairs one region with one fill; cells collect into a Universe, a universe becomes a Geometry, and geometry plus materials plus settings plus tallies become one Model. Nothing here is implicit, and nothing cross-checks it either: a cell that references a material you left out of Materials exports without complaint, and fails later when the solver reads the geometry and cannot find that material ID. Leave Model's materials argument unset and OpenMC collects them from the geometry itself, which removes the mistake entirely.
materials = openmc.Materials([fuel, clad, water]) # Material → collection
fuel_outer = openmc.ZCylinder(r=0.4096)
clad_outer = openmc.ZCylinder(r=0.4750)
fuel_region = -fuel_outer # Surface → Region
clad_region = +fuel_outer & -clad_outer
water_region = +clad_outer
fuel_cell = openmc.Cell(fill=fuel, region=fuel_region) # Region + Material → Cell
clad_cell = openmc.Cell(fill=clad, region=clad_region)
water_cell = openmc.Cell(fill=water, region=water_region)
universe = openmc.Universe(cells=[fuel_cell, clad_cell, water_cell]) # Cells → Universe
geometry = openmc.Geometry(universe) # Universe → Geometry
settings = openmc.Settings()
settings.particles = 10000
settings.batches = 100
model = openmc.Model(geometry, materials, settings) # everything → Model
sp = openmc.StatePoint(model.run())
keff = sp.keff
print(f"k-effective: {keff.nominal_value:.5f} ± {keff.std_dev:.5f}")Tutorial snippet — no separate file in examples repo
Two mistakes the object graph does not catch until you export
A material with no density is a perfectly valid Python object — nothing stops you from building one — but exporting it fails, because a density-less material has no cross sections to build. Set it before export, in any order relative to adding nuclides.
fuel = openmc.Material()
fuel.add_nuclide('U235', 0.045)
fuel.add_nuclide('U238', 0.955)
fuel.set_density('g/cm3', 10.4) # order relative to add_nuclide doesn't matter
# openmc.Materials([fuel]).export_to_xml() # fine nowTutorial snippet — no separate file in examples repo
And once you have four separate collections — materials, geometry, settings, tallies — exporting each one by hand is one more place to forget a step. Pass all four into a Model and export that instead; it writes every XML file the run needs in one call, and it is what model.run() does internally anyway.
model = openmc.Model(geometry, materials, settings, tallies)
model.export_to_xml() # writes materials, geometry, settings, and tallies XMLTutorial snippet — no separate file in examples repo
One thing to try
Because every object here is a live Python variable, running the same model many times with one parameter changed is just a loop around the code above — no new files, no re-parsing. Example: Pin Cell's "Exploring Further" section does exactly this, sweeping enrichment, fuel radius, and lattice pitch on the same pin this page just built. Before opening it, predict which of those three should move k-effective the most for a fixed-size step, and check your reasoning against what you see.
Check yourself
- Name the object types every OpenMC model is built from, and how they nest?
- Assemble a model with
openmc.Modelrather than exporting each XML by hand? - Read k-effective back out of a completed run?