Surface Types in OpenMC

What you'll learn

After the first pin · 10 / 1112 min read
  • Pick the right surface class for a shape, and know why axis-aligned ones are faster.
  • Apply each boundary type, including the transmission default.
  • Fall back to a general Plane or Quadric when no axis-aligned class fits.
  • Build a finite cylinder by intersecting a cylinder with two planes.

Before you start

Introduction to Surfaces

Come back to this page once the pin cell from Example: Pin Cell needs a shape that a cylinder and two planes cannot express — a cone, a torus, a hemispherical core cap, or a general quadric.

Each surface is defined by a mathematical equation f(x,y,z) = 0 that divides space into positive and negative half-spaces. In cell definitions, the + and - operators select which side of a surface the cell occupies.

Basic Surface Concepts

Surface Equations and Half-Spaces

Every surface is an equation set equal to zero, and the sign of the left-hand side at a point tells you which side of the surface that point is on. Where f(x,y,z) is negative you are in the negative half-space, which OpenMC writes as -surface; where it is positive you are in the positive half-space, written +surface. For a sphere written as x² + y² + z² − r² = 0 this works out intuitively — inside is negative — and the same holds for cylinders. It is worth checking rather than assuming for planes and cones, where which side is "inside" is a matter of how the equation happens to be written.

Surface ID and Name

Each surface can have an optional ID (integer) and name (string):

python
# Creating a surface with ID and name
sphere = openmc.Sphere(r=10.0, surface_id=1, name="fuel_outer_boundary")

# IDs and names can be accessed or modified
print(sphere.id)
sphere.name = "new_name"

Tutorial snippet — no separate file in examples repo

Boundary Conditions

A surface's boundary_type is what turns geometry into a physical model, because it decides what a particle does on arrival. The default is transmission: the particle crosses and carries on, which is what every interior surface should be. The two you will set deliberately are vacuum, which kills the particle and so represents everything outside the problem, and reflective, which reflects it specularly — the mirror condition that makes a single pin cell stand in for an infinite lattice.

Two more are situational. periodic translates the particle to a matching surface instead of reflecting it, which is what a genuinely repeating lattice with an asymmetric unit cell requires. white re-emits the particle in a direction sampled from a cosine distribution, discarding the memory of how it arrived.

The choice between reflective and white is where models go wrong, and it matters most on curved surfaces. Specular reflection off a cylinder conserves the impact parameter, so a neutron launched tangentially circles the annulus forever without ever crossing back into the fuel. A Wigner–Seitz equivalent cylinder therefore needs a white boundary, while a flat symmetry plane cut through a square lattice needs a reflective one.

python
# Setting boundary conditions
outer_surface = openmc.Sphere(r=50.0, boundary_type='vacuum')
reflective_surface = openmc.XPlane(x0=0.0, boundary_type='reflective')
white_surface = openmc.YPlane(y0=0.0, boundary_type='white')

Tutorial snippet — no separate file in examples repo

Planar Surfaces

Planar surfaces are defined by equations of the form Ax + By + Cz = D, where (A,B,C) is the normal vector to the plane.

Planes Perpendicular to Axes

OpenMC provides dedicated classes for planes perpendicular to the coordinate axes:

python
# X-plane: x = x0
xplane = openmc.XPlane(x0=5.0)  # Equation: x - 5.0 = 0

# Y-plane: y = y0
yplane = openmc.YPlane(y0=-2.0)  # Equation: y + 2.0 = 0

# Z-plane: z = z0
zplane = openmc.ZPlane(z0=10.0)  # Equation: z - 10.0 = 0

Tutorial snippet — no separate file in examples repo

General Planes

For planes with arbitrary orientation, you can use the general Plane class:

python
# Plane defined by coefficients A, B, C, D in equation Ax + By + Cz = D
plane1 = openmc.Plane(a=1.0, b=1.0, c=1.0, d=5.0)  # x + y + z = 5

# To define a plane from three points, compute the coefficients manually
import numpy as np
p1 = np.array([0.0, 0.0, 5.0])
p2 = np.array([1.0, 0.0, 5.0])
p3 = np.array([0.0, 1.0, 5.0])
normal = np.cross(p2 - p1, p3 - p1)  # normal vector (A, B, C)
A, B, C = normal
D = np.dot(normal, p1)
plane2 = openmc.Plane(a=A, b=B, c=C, d=D)

Tutorial snippet — no separate file in examples repo

Cylindrical Surfaces

Cylinders Parallel to Axes

python
# Cylinder parallel to the z-axis
zcyl = openmc.ZCylinder(r=0.5, x0=0.0, y0=0.0)  # (x-x0)² + (y-y0)² = r²

# Cylinder parallel to the y-axis
ycyl = openmc.YCylinder(r=1.0, x0=0.0, z0=0.0)  # (x-x0)² + (z-z0)² = r²

# Cylinder parallel to the x-axis
xcyl = openmc.XCylinder(r=0.8, y0=0.0, z0=0.0)  # (y-y0)² + (z-z0)² = r²

Tutorial snippet — no separate file in examples repo

Arbitrarily Oriented Cylinders

OpenMC has no general Cylinder surface class. For a cylinder with arbitrary orientation, either define an axis-aligned cylinder inside a universe and rotate the cell that contains it, or express the cylinder as a general Quadric surface:

python
# Option 1 (preferred): axis-aligned cylinder in a rotated, universe-filled cell
zcyl = openmc.ZCylinder(r=0.5)
inner_cell = openmc.Cell(fill=some_material, region=-zcyl)
tilted_univ = openmc.Universe(cells=[inner_cell])
container = openmc.Cell(fill=tilted_univ, region=container_region)
container.rotation = (0, 30, 0)  # tilt 30 degrees about the y-axis

# Option 2: general Quadric surface (coefficients derived by hand)
# Ax² + By² + Cz² + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0

Tutorial snippet — no separate file in examples repo

Note: For most typical reactor geometries, the axis-aligned cylinders (XCylinder, YCylinder, ZCylinder) are more efficient and easier to use.

Spherical and Other Curved Surfaces

Spheres

Spherical surfaces are defined by the equation (x-x0)² + (y-y0)² + (z-z0)² = r², where (x0,y0,z0) is the center and r is the radius:

python
# Sphere centered at the origin with radius 10.0
sphere1 = openmc.Sphere(r=10.0)

# Sphere with custom center
sphere2 = openmc.Sphere(r=5.0, x0=1.0, y0=2.0, z0=3.0)

Tutorial snippet — no separate file in examples repo

Cones

Conical surfaces are available for coordinate-aligned cones:

python
# Cone parallel to the z-axis
zcone = openmc.ZCone(r2=1.0, x0=0.0, y0=0.0, z0=0.0)  # (x-x0)² + (y-y0)² = r2*(z-z0)²

# Cone parallel to the x-axis
xcone = openmc.XCone(r2=2.0, x0=0.0, y0=0.0, z0=0.0)  # (y-y0)² + (z-z0)² = r2*(x-x0)²

# Cone parallel to the y-axis
ycone = openmc.YCone(r2=0.5, x0=0.0, y0=0.0, z0=0.0)  # (x-x0)² + (z-z0)² = r2*(y-y0)²

Tutorial snippet — no separate file in examples repo

The r2 parameter represents the square of the tangent of the cone's half-angle.

Tori (Toruses)

Toroidal surfaces can be used for more complex geometries:

python
# Torus parallel to the z-axis
ztorus = openmc.ZTorus(a=5.0, b=1.0, c=1.0)

# where:
# a = major radius (distance from center of torus to center of tube)
# b = minor radius of the tube in the x-y plane
# c = minor radius of the tube in the z direction
# (√((x-x0)² + (y-y0)²) - a)²/b² + (z-z0)²/c² = 1

Tutorial snippet — no separate file in examples repo

Quadric and Other Advanced Surfaces

General Quadric Surface

python
# General quadric surface: Ax² + By² + Cz² + Dxy + Eyz + Fxz + Gx + Hy + Jz + K = 0
quadric = openmc.Quadric(a=1.0, b=1.0, c=1.0, k=-1.0)  # x² + y² + z² - 1 = 0 (a sphere)

# Ellipsoid with semi-axes 2, 3, and 4
ellipsoid = openmc.Quadric(a=0.25, b=1/9, c=1/16, k=-1.0)  # x²/4 + y²/9 + z²/16 = 1

Tutorial snippet — no separate file in examples repo

Creating Half-Spaces

python
# Create a sphere
sphere = openmc.Sphere(r=10.0)

# Create the region inside the sphere (negative half-space)
inside_sphere = -sphere

# Create the region outside the sphere (positive half-space)
outside_sphere = +sphere

Tutorial snippet — no separate file in examples repo

Geometry Transformations

Geometric transformations (translation and rotation) are applied at the cell level, not the surface level. Define surfaces in a local coordinate system, then translate or rotate the cell.

python
# Define surfaces in a local coordinate system
sphere = openmc.Sphere(r=10.0)
cell = openmc.Cell(region=-sphere)

# Translate the cell so the sphere is centered at (10, 0, 0)
cell.translation = (10.0, 0.0, 0.0)

# Rotate the cell using Euler angles (phi, theta, psi) in degrees
cell.rotation = (0.0, 0.0, 45.0)

Tutorial snippet — no separate file in examples repo

Tip: For repeated structures such as fuel pin lattices, it is often more convenient to use universes and lattices to position and orient geometry rather than applying individual cell transformations.

Practical Examples

Creating a Simple Fuel Pin

Here's how to create a simple fuel pin model using surfaces. Unlike the Path A pin, which starts cladding right at the pellet surface, this example carries an explicit helium gap between fuel and clad.

python
# Define the surfaces for a fuel pin
fuel_radius = 0.4096
clad_inner_radius = 0.4178
clad_outer_radius = 0.4750

# Create cylindrical surfaces
fuel_surface = openmc.ZCylinder(r=fuel_radius)
clad_inner_surface = openmc.ZCylinder(r=clad_inner_radius)
clad_outer_surface = openmc.ZCylinder(r=clad_outer_radius)

# Create top and bottom planes for finite height
top_plane = openmc.ZPlane(z0=10.0)
bottom_plane = openmc.ZPlane(z0=-10.0)

# Define regions
fuel_region = -fuel_surface & -top_plane & +bottom_plane
gap_region = +fuel_surface & -clad_inner_surface & -top_plane & +bottom_plane
clad_region = +clad_inner_surface & -clad_outer_surface & -top_plane & +bottom_plane

Tutorial snippet — no separate file in examples repo

Creating a Reactor Core Boundary

Defining a cylindrical reactor core with a hemispherical bottom:

python
# Define the dimensions
core_radius = 200.0
core_height = 400.0

# Create the cylindrical side
cylinder = openmc.ZCylinder(r=core_radius)

# Create the top plane
top_plane = openmc.ZPlane(z0=core_height)

# Create the hemispherical bottom
sphere = openmc.Sphere(r=core_radius, z0=0.0)
bottom_plane = openmc.ZPlane(z0=0.0)

# Define the core region
cylindrical_part = -cylinder & -top_plane & +bottom_plane
hemispherical_part = -sphere & -bottom_plane
core_region = cylindrical_part | hemispherical_part

Tutorial snippet — no separate file in examples repo

Choosing surfaces well

Prefer the specialized axis-aligned classes over the general ones wherever the geometry allows it. ZCylinder and Quadric can describe the same cylinder, but the first reduces the distance-to-surface calculation to a two-dimensional problem with a closed-form answer, while the second solves the general quadratic. Since that calculation happens for every surface a particle might reach, at every flight, the difference is measurable in a way that few other modeling choices are.

Surface count matters for the same reason, and it is the argument for building repeated structures out of universes and lattices rather than out of explicit surfaces. Ten thousand fuel pins written as ten thousand cylinders is a model that runs; the same core written as one pin universe placed in a lattice is a model that runs quickly, because OpenMC only ever considers the surfaces of the current lattice element.

Beyond performance, two habits save debugging time. Name surfaces that mark something physically meaningful, since a warning about surface 47 is far less useful than one about clad_outer. And when a region behaves strangely, suspect a sign before suspecting anything else — a mistaken + for a - produces a valid model of a different object, and the resulting geometry error, if there is one at all, usually appears somewhere other than where the mistake is.

Check yourself

  • Pick the right surface class for a shape, and say why axis-aligned ones are faster?
  • Apply each boundary type, including the transmission default?
  • Build a finite cylinder by intersecting a cylinder with two planes?