Concepts
This document explains the domain concepts that aleathor works with. If you know MCNP, most of this will be familiar, but there are a few places where aleathor's Python representation differs from the underlying C library or from what you might expect.
Surfaces
A surface is a mathematical object that divides all of 3D space into two halves. A plane divides space into "above" and "below." A sphere divides space into "inside" and "outside."
In aleathor, a surface is always one of these types:
| Class | Equation | MCNP equivalent |
|---|---|---|
Plane |
ax + by + cz = d | P, PX, PY, PZ |
Sphere |
(x-x0)^2 + (y-y0)^2 + (z-z0)^2 = r^2 | S, SO, SX, SY, SZ |
XCylinder |
(y-y0)^2 + (z-z0)^2 = r^2 | CX, C/X |
YCylinder |
(x-x0)^2 + (z-z0)^2 = r^2 | CY, C/Y |
ZCylinder |
(x-x0)^2 + (y-y0)^2 = r^2 | CZ, C/Z |
XCone/Y/Z |
... = t^2 * (axis)^2 | KX, KY, KZ |
XTorus/Y/Z |
fourth-degree surface of revolution | TX, TY, TZ |
RPP |
axis-aligned box | RPP |
Quadric |
Ax^2 + By^2 + ... + K = 0 | GQ, SQ |
Macrobodies (RPP, RCC, TRC, ELL, REC, WED, RHP, Box) are also surface types. They represent composite shapes (a cylinder-with-caps, a wedge, etc.) as single objects.
Surface IDs are auto-assigned when you create a surface. If you load from MCNP, the original surface IDs are preserved.
s = ath.Sphere(0, 0, 0, radius=5.0)
print(s.id) # Auto-assigned integer
Sense and Halfspaces
Every surface divides space into two sides. In aleathor (and MCNP), these sides are called positive and negative sense.
- Negative sense (inside): for a sphere, points closer to the center than the radius. For a plane, points on the side opposite to the normal vector. Written as
-Sin MCNP. - Positive sense (outside): for a sphere, points farther from the center. Written as
+Sor justSin MCNP.
In Python, you get halfspaces using the - and + operators on surfaces:
sphere = ath.Sphere(0, 0, 0, radius=5.0)
inside = -sphere # Negative halfspace: points where r < 5
outside = +sphere # Positive halfspace: points where r > 5
These return Halfspace objects — a type of Region. You can also use the .interior() and .exterior() methods:
inside = sphere.interior() # Same as -sphere
outside = sphere.exterior() # Same as +sphere
Regions and Boolean Operations
A Region defines a volume of space. The simplest region is a Halfspace (one side of a surface). Complex regions are built by combining halfspaces with boolean operations.
aleathor uses Python operators for boolean combinations:
| Operation | Python | MCNP | Meaning |
|---|---|---|---|
| Intersection | a & b |
-1 -2 (juxtaposition) |
Inside both a and b |
| Union | a \| b |
-1:-2 (colon) |
Inside a or b or both |
| Difference | a - b |
-1 2 (sense flip) |
Inside a but not b |
| Complement | ~a |
#(...) |
Everything not inside a |
sphere = ath.Sphere(0, 0, 0, radius=5.0)
box = ath.RPP(-10, 10, -10, 10, -10, 10)
# Hemisphere: inside sphere AND above z=0
plane = ath.ZPlane(0)
hemisphere = -sphere & +plane
# Shell: inside box minus inside sphere
shell = -box & +sphere # Same as: -box - (-sphere)
# Union of two spheres
s1 = ath.Sphere(-3, 0, 0, radius=2.0)
s2 = ath.Sphere(3, 0, 0, radius=2.0)
dumbbell = -s1 | -s2
Region types
| Class | Description |
|---|---|
Halfspace |
One side of a surface |
Intersection |
AND of multiple regions (flattened automatically) |
Union |
OR of multiple regions (flattened automatically) |
Complement |
NOT of a region (double negation eliminated) |
Nested operations of the same type are flattened: (a & b) & c becomes Intersection([a, b, c]), not a tree of two intersections.
Region methods
Every Region supports:
point in region # Containment test: True if (x, y, z) is inside
region.get_surfaces() # Set of all surfaces referenced by the region
Cells
A cell is a region of space with a specific material assignment. It corresponds to an MCNP cell card.
Every cell has:
- Cell ID: the MCNP cell number (unique identifier, positive integer)
- Region: a
Regiondefining the shape - Material: material number (0 for void)
- Density: positive magnitude on the Python side;
density_unitcontrols whether it is exported as MCNP mass density ("g/cm3", negative syntax) or atom density ("atoms/b-cm", positive syntax) - Universe: which universe the cell belongs to (default 0)
- Fill: optionally, a universe to fill this cell with
- Importance: particle importance for transport (default 1.0)
A cell can have a material, a fill, or neither. In normal transport-style models, a filled cell is usually a container: it defines a region of space and says "look in universe N to find what's actually here." For clarity, avoid assigning both a material and a fill unless you are deliberately modeling an imported format that uses that combination.
model = ath.Model()
sphere = ath.Sphere(0, 0, 0, radius=5.0)
cell = model.add_cell(
region=-sphere,
material=1,
density=10.5,
name="fuel"
)
Cell Views
Cell is the public API type for inspecting and mutating cells:
| Type | Purpose | Source |
|---|---|---|
Cell |
Mutable view backed by the C system | model.add_cell(), model.get_cell(), model.cell_at(), iteration |
When you build geometry in Python, cells are pushed to the C backend immediately and Python keeps metadata such as names, regions, and importances in side dictionaries. When you query geometry, the C system returns cell indices that are wrapped as Cell objects.
Cell is the public API type. It provides access to all cell properties, with mutation support for key fields:
cell = model.get_cell(10) # Returns Cell
cell.id # Cell ID
cell.material # Material number (read-write)
cell.density # Density (read-write)
cell.universe # Universe ID
cell.fill # Fill universe or None (read-write)
cell.bounds # Bounding box tuple
cell.is_void # True if material == 0
cell.is_filled # True if fill is set
cell.is_lattice # True if this cell defines a lattice
cell.contains(x, y, z) # Point containment
To modify cell properties, assign directly or use model.update_cell():
cell.material = 2 # Direct mutation
cell.density = 5.0
model.update_cell(10, material=2, density=5.0) # Also works
Cell property setters update the C backend directly. Python-side metadata such as name and importance is stored on the Model.
Universes
A universe is a collection of cells that share the same coordinate system. Every cell belongs to exactly one universe. By default, everything is in universe 0.
Universes become important when you have repeated geometry. Instead of defining the same fuel pin 1000 times, you define it once in universe 5, and then fill 1000 container cells with universe 5.
Fills
A cell with fill=N is a container. When aleathor evaluates a point query, it:
- Finds the container cell in the current universe
- Sees it has a fill
- Applies the inverse of the fill transform (if any)
- Searches for the point among the cells of universe N
- Repeats if it hits another fill
This is recursive. aleathor handles arbitrary nesting depth.
# Define pin geometry in universe 1
model.add_cell(-fuel_surf, material=1, density=10.0, universe=1)
model.add_cell(-clad_surf & +fuel_surf, material=2, density=6.5, universe=1)
# Container in universe 0, filled with universe 1
model.add_cell(-box, fill=1)
Lattices
A lattice cell is a cell that tiles space with a repeating pattern of universes. MCNP supports two lattice types:
| Type | Value | Description |
|---|---|---|
| Rectangular | 1 | Elements arranged on a Cartesian grid |
| Hexagonal | 2 | Elements arranged on a hexagonal grid |
A lattice cell defines a pitch (element spacing in each dimension), a fill array (which universe goes in each element), and dimensions (the range of lattice indices).
for cell in model.cells:
if cell.is_lattice:
print(f"Cell {cell.id}: type={cell.lattice_type}")
print(f" Pitch: {cell.lattice_pitch}") # (dx, dy, dz)
print(f" Dims: {cell.lattice_dims}") # (imin,imax,jmin,jmax,kmin,kmax)
print(f" Fill: {cell.lattice_fill[:5]}...") # Universe IDs
print(f" Lower-left: {cell.lattice_lower_left}")
When aleathor evaluates a point inside a lattice cell, it determines which lattice element the point falls in (based on pitch and lower-left corner), looks up the universe for that element in the fill array, and searches for the point among that universe's cells.
Fill Mutation
You can change a cell's fill after creation:
cell = model[10]
cell.fill = 5
This operates on the C system directly and takes effect immediately. Use
cell.fill_with(universe, transform=...) when a fill transform is needed.
Materials
A material is a composition of nuclides or elements. aleathor stores:
- Material ID: the MCNP material number
- Name: optional human-readable name
- Density: an optional standard material density; cell density is still stored on each cell
- Composition: optional nuclide and element fractions
Materials are preserved through loading and export. aleathor does limited material bookkeeping for composition and export; geometry queries return material IDs but do not perform transport physics.
model.add_material(1, name="UO2", density=10.5)
model.add_material(2, name="Steel")
# Create a mixture
mix_id = model.create_mixture([1, 2], [0.7, 0.3], name="fuel-steel-mix")
Model Lifecycle
The Model class manages C-backed geometry plus Python-side metadata:
- C system: the compiled geometry in
model._sys, used for all queries - Python metadata: names, importances, Python-created surfaces, and inspectable region wrappers
Query preparation
When you query the geometry, aleathor prepares the needed acceleration structures automatically:
model.add_cell(...) # Pushes the cell to the C backend
model.cell_at(0, 0, 0) # Ensures query caches are ready, then queries C
The query acceleration caches are built automatically and transparently. You never need to call a preparation method before querying.
Loaded models
When you load a model from an MCNP file, the C system is populated directly by the parser. Cells use _ImportedRegion objects instead of Python-created Region trees — these are lightweight wrappers that reference the C system's CSG nodes and can reconstruct a Python region tree lazily for inspection.
This means loaded models are efficient: they do not eagerly duplicate the geometry in Python objects.
Macrobodies
Macrobodies are composite surfaces that MCNP defines as single surface cards but that are really combinations of simpler surfaces:
| Macrobody | Description | Decomposition |
|---|---|---|
RCC |
Right circular cylinder | Cylinder + 2 planes |
RPP |
Axis-aligned box | 6 planes (or single primitive) |
TRC |
Truncated right cone | Cone + 2 planes |
ELL |
Ellipsoid | Quadric surface |
REC |
Right elliptical cylinder | Quadric + 2 planes |
WED |
Wedge | 5 planes |
RHP / HEX |
Hexagonal prism | 8 planes |
Box |
Oriented box | 6 planes |
You can expand macrobodies to their primitive surfaces:
n = model.repair.expand_macrobodies()
print(f"Expanded {n} macrobodies")
Boundary Conditions
Surfaces can have boundary conditions that affect particle behavior in transport:
| Type | Meaning |
|---|---|
'transmissive' |
Particles cross normally (default) |
'reflective' |
Particles reflect at the surface |
'vacuum' |
Particles are killed |
Boundary conditions are preserved through loading and export. They don't affect point queries or ray tracing (those are purely geometric operations).
s = ath.Sphere(0, 0, 0, radius=100.0, boundary='reflective')
Configuration
System behavior is controlled through the config property:
cfg = model.backend.config # Returns dict
model.backend.config = { # Update specific keys
'abs_tol': 1e-8,
'log_level': 3,
}
Common configuration fields:
| Key | Default | Meaning |
|---|---|---|
abs_tol |
implementation default | Absolute tolerance for geometric comparisons |
rel_tol |
implementation default | Relative tolerance |
log_level |
implementation default | 0=none, 1=error, 2=warn, 3=info, 4=debug, 5=trace |
dedup |
true | Enable surface deduplication |
export_materials |
true | Include material cards in export |
Error Handling
aleathor functions raise standard Python exceptions:
| Exception | When |
|---|---|
KeyError |
Cell or material not found |
ValueError |
Invalid arguments (negative radius, missing bounds, etc.) |
RuntimeError |
C extension not available, internal error |
FileNotFoundError |
Input file not found |
KeyboardInterrupt |
Ctrl+C during long operations (ray tracing, grid queries) |
The C library supports cooperative interruption: long-running operations check a flag periodically. When you press Ctrl+C during a grid query or ray trace, the operation terminates cleanly and Python raises KeyboardInterrupt.
Plotting Constants
For working with raw grid data:
| Constant | Value | Meaning |
|---|---|---|
CELL_VOID |
-1 | Grid pixel is in void |
CELL_UNDEFINED |
-2 | No cell claims this pixel |
CELL_OVERLAP |
-3 | Multiple cells claim this pixel |
GRID_ERROR_OK |
0 | No error at this pixel |
GRID_ERROR_OVERLAP |
1 | Overlap detected |
GRID_ERROR_UNDEFINED |
2 | Undefined region |