Skip to content

Search universes with many cells with a bounding box tree - #4168

Draft
GuySten wants to merge 13 commits into
openmc-dev:developfrom
GuySten:claude/cell-box-tree
Draft

GuySten wants to merge 13 commits into
openmc-dev:developfrom
GuySten:claude/cell-box-tree

Conversation

@GuySten

@GuySten GuySten commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Description

This PR is stacked on the region tree PR (#4167) and on #4163. Only the last two commits are new.

Finding the cell containing a point checks the cells of a universe in order. With #4163, cells whose bounding boxes do not contain the point are skipped. In a universe with many cells, such as the layers of many TRISO particles placed directly in a matrix, testing every bounding box is itself the cost. Neighbor lists don't help either: the matrix's list grows to hold most of the cells.

With this PR, a universe whose cells have finite bounding boxes that rarely overlap is searched with a bounding volume hierarchy over the boxes. It uses the same tree, and the same rule, as the region tree PR: at least 32 boxes, and a point in at most an eighth of them on average.

  • Same cell found: of the cells whose boxes contain the point, the first in the universe that contains the point is returned, as in a search of all of the cells.
  • Neighbor lists: in such a universe they hold at most 8 cells. A cell with few neighbors is still found from its neighbor list. A cell next to many others, such as the matrix, fills its list, and searches from it then go to the tree directly.

The rule for using a tree moves to box_tree so that regions and universes share it, and the geometry methods documentation describes the search.

Results

Transport time, single thread, median of interleaved runs (3 to 6 per build), compared with the region tree PR (#4167). Results are bit-identical on every model.

Model Region tree PR This PR Ratio
10 cm slice of a flat TRISO compact 295 s 7.0 s 0.024
Flat TRISO 88.5 s 13.3 s 0.15
ATR (5,968 cells), volume calculation 3.92 s 0.98 s 0.25
ATR, photon transport 12.5 s 7.1 s 0.57
ATR 7.27 s 5.75 s 0.79
17 other models, including a ring of 360 sectors whose boxes overlap (kept linear by the rule) 0.92–1.03

Flat models no longer need lattices to be fast. On the same machine:

Model Flat With a TRISO lattice
Flat TRISO on develop 359 s 28.3 s
Flat TRISO with this PR 13.3 s 18.2 s
10 cm compact slice with this PR 7.0 s 9.2 s

With delta tracking (#3971 merged with this PR):

Model #3971 #3971 + this PR
10 cm slice of a flat TRISO compact 87.6 s 1.47 s
Same slice built with a lattice 1.76 s 1.60 s
ATR 33.9 s 6.9 s

Adversarial model checked against develop:

  • Flat 10 × 10 grid of box cells with long histories: the neighbor lists of develop find each new cell in one or two checks. 0.90 times the time of develop. Before neighbor lists were kept in tree universes, this model was 1.2 times slower.

Testing

  • test_cell_search.py (from Skip cells whose bounding boxes do not contain the point in cell searches #4163) now runs through the tree: cells found for random points agree with Python's evaluation of the regions.
  • A new test puts three overlapping cells, one of them with an infinite box, in a tree universe in four orders. In each order, the first of them in the universe is found.
  • The full test suite passes locally. The only errors are tests that need ENDF data or dlopen, which fail the same way on develop.

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

claude added 12 commits October 2, 2026 23:12
Regions were stored as infix token streams including parenthesis and
operator tokens. Evaluating a complex region scanned the tokens while
tracking parenthesis depth, precedence was enforced by inserting
parentheses, and bounding boxes required converting the expression to
postfix on every call.

Region specifications are now parsed with a recursive descent parser
into an expression tree of intersections and unions with half-spaces as
leaves. Nested operators of the same type are merged, so the depth of
the tree is the number of alternations between intersection and union.
The tree is stored in pre-order with the index of the end of each
subtree and of the parent of each node, so contains_complex evaluates it
in a single loop without recursion, skipping the rest of a subtree as
soon as its value is known. The half-spaces of a region are also stored
as a contiguous list, which is all a simple region needs and is used for
distance calculations.

Complements are removed while parsing by applying De Morgan's laws to
the tree. Previously complement operators were removed by flipping the
operators in the token stream before precedence was enforced, so the
complement of an expression mixing intersection and union without inner
parentheses, such as ~(1 2 | 3), was interpreted as -1 | (-2 -3) instead
of (-1 | -2) -3. Expressions written by the Python API are fully
parenthesized and were not affected. A test comparing cell lookup
against the Python region parser for random expressions is added.

Region strings written to the summary file are generated from the tree.
They are equivalent to the previous strings, but redundant parentheses
are no longer kept. n_surfaces now returns the number of half-spaces
rather than the number of tokens including operators.

Results are identical for the tested models. Transport time is
unchanged for lattice and simple geometries, and reduced by 16% for a
water tank containing 400 rods, where the water region is the tank minus
the rods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RN7JroEkrbkLKVHbMDap9
CSGCell::find_left_parenthesis searched token streams and had no
callers, the cell ID argument of Region::bounding_box was only used for
error messages when converting the expression to postfix, and the
<set> and <sstream> includes are no longer used in cell.cpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RN7JroEkrbkLKVHbMDap9
Region::str() is now generated from the expression tree, in which nested
operators of the same type are merged and redundant parentheses are not
kept, so the expected string for the issue openmc-dev#3685 case changes from
" ( ( -1 2 ( -3 4 ) ) | ( -5 6 ) )" to the equivalent
" ( -1 2 -3 4 ) | ( -5 6 )". Add cases checking that complements of
mixed expressions are applied after grouping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RN7JroEkrbkLKVHbMDap9
…ches

A bounding box is computed for each CSG cell at initialization, enlarged
slightly for roundoff, and the searches over the cells of a universe and
over neighbor lists skip cells whose boxes do not contain the point before
evaluating whether the cell contains it. Cells are checked in the same order
as before, so the same cell is found, while expensive containment checks are
replaced by a box test for cells that cannot contain the point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RN7JroEkrbkLKVHbMDap9
A simple region needs only its half-spaces, so the expression tree of a
complex region is moved behind a pointer. This keeps Region, and the cells
holding it, small for the common case of simple cells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RN7JroEkrbkLKVHbMDap9
The distance to the boundary of a complex region is found by stepping
from one surface crossing to the next along the ray until crossing a
surface changes whether the ray is in the region. Every step recomputed
the distance to and the sense with respect to every half-space of the
region, so a call with k virtual crossings cost (k + 1) N distance and up
to (k + 2) N sense evaluations for N half-spaces, counting surfaces that
appear several times in the expression once per appearance.

The half-spaces in the expression tree of a complex region now refer to
a list of the distinct surfaces of the region. Distances and senses are
evaluated once per distinct surface per call and updated as the ray
moves: distances to the other surfaces decrease by the distance moved,
and only the surfaces at the new position are evaluated again. The
updated distances are only used to select the next candidate surface;
the distance to the candidate is recomputed from the current position,
so the result is the same as before apart from the choice among surfaces
at exactly the same distance. Membership is evaluated from the cached
senses with the same tree evaluation as contains_complex.

The cached distances and senses are kept in working space owned by each
particle, next to its boundary information, and sized once for the region
with the most surfaces, so that no memory is allocated during transport.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RN7JroEkrbkLKVHbMDap9
Each particle keeps working space for finding boundaries in complex
regions, sized for the region with the most surfaces. In event-based mode
up to max_particles_in_flight particles exist at once, so a region with
thousands of surfaces could need gigabytes. The total size of the working
space of the particles in flight is now limited to 256 MB, and regions
with more surfaces than it then holds are searched as before, without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RN7JroEkrbkLKVHbMDap9
The working space for finding boundaries in complex regions was sized in
the constructor of every geometry state, including the temporary ones used
only to locate a point, such as when checking whether a sampled source site
is in the source's domain. With a complex region of thousands of surfaces,
each of these allocated and zeroed tens of kilobytes; a fixed source
constrained to a small cell ran 7 times slower than before. The working
space is now sized when a particle is constructed. Other geometry states
have none, and search complex regions without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RN7JroEkrbkLKVHbMDap9
A region that is the intersection of many children, such as the water
around rods or the matrix around TRISO particles, was evaluated by testing
every child, both to determine whether it contains a point and to find the
distance to its boundary. When enough of the children are false only
within finite boxes, the children with boxes are now put in a bounding
volume hierarchy, and only the children whose boxes contain the point, or
whose boxes the ray enters before the nearest boundary found so far, are
evaluated. A region that is an intersection of half-spaces, such as the
space outside of many spheres, is searched the same way.

The tree is used when at least 32 children have finite boxes and a point
is in at most an eighth of the boxes on average, as estimated from their
volumes. Children without finite boxes that are single half-spaces, such
as the walls of a vessel made of many planes, are searched together in
one pass over their surfaces, as in a simple region. Other regions are
evaluated as before. Results are unchanged apart from roundoff in the
distance to a boundary reached after virtual crossings.

The surfaces of each child are stored consecutively, so that a child is
searched with working space for its own surfaces only. In event-based
mode, where the working space of each particle may be too small for all
of the surfaces of the region, the children are still searched with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RN7JroEkrbkLKVHbMDap9
Finding the cell containing a point checks the cells of a universe in
order, skipping cells whose bounding boxes do not contain the point. In a
universe with many cells, such as the layers of many TRISO particles
placed directly in a matrix, testing every bounding box is itself the
cost, and neighbor lists grow to hold most of the cells.

A universe whose cells have finite bounding boxes that rarely overlap is
now searched with a bounding volume hierarchy over the boxes, using the
same tree and the same rule as regions outside of many objects: at least
32 boxes, and a point in at most an eighth of them on average. Of the
cells whose boxes contain the point, the first in the universe that
contains it is found, as in a search of all of the cells, so the cell
found is unchanged. Neighbor lists in such a universe hold at most eight
cells, so that a cell next to many others, such as a matrix around many
particles, has its neighbors found with the tree, while a cell with few
neighbors is still found from its neighbor list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RN7JroEkrbkLKVHbMDap9
In a universe searched with a tree, neighbor lists hold at most eight
cells. A cell next to many others, such as the water around many rods,
fills its list, and every search from it then checked the eight cells in
the list before searching the tree. Such a list is now marked as full,
and searches from its cell go to the tree directly. Cells with few
neighbors still find them from their lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RN7JroEkrbkLKVHbMDap9
@coderabbitai

coderabbitai Bot commented Oct 15, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants