Skip to content

Represent cell regions as expression trees - #4160

Open
GuySten wants to merge 4 commits into
openmc-dev:developfrom
GuySten:claude/region-expression-tree
Open

GuySten wants to merge 4 commits into
openmc-dev:developfrom
GuySten:claude/region-expression-tree

Conversation

@GuySten

@GuySten GuySten commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Description

Cell regions were stored as infix token streams, including parenthesis and operator tokens. Evaluating a complex region scanned the tokens while tracking parenthesis depth. Operator precedence was enforced by inserting parentheses into the token stream, and computing a bounding box converted the expression to postfix on every call.

This PR parses region specifications with a recursive descent parser (intersection binds tighter than union; complement applies to the following half-space or parenthesized group) into an expression tree of intersections and unions with half-spaces as leaves:

  • Nested operators of the same type are merged and single-child operators collapsed, so the tree depth is the number of alternations between intersection and union, not the number of parentheses.
  • The tree is stored in pre-order in a single vector of small nodes, each with the index of the end of its subtree and of its parent. contains_complex evaluates it in one loop without recursion, skipping the rest of a subtree as soon as its value is known.
  • The half-spaces of a region are also kept as a contiguous list. A simple region is just this list (no tree), so the simple-cell code paths are unchanged, and the list is used for distance calculations.
  • The data needed only by complex regions (the tree) is kept out of line behind a pointer that is null for simple regions, so Region, and the cells holding it, stay small in the common case of simple cells.
  • str() and bounding_box() work directly on the tree. enforce_precedence, add_parentheses, remove_complement_ops, apply_demorgan, generate_postfix, the two bounding box helpers, the unused CSGCell::find_left_parenthesis, and the no longer needed cell_id argument of Region::bounding_box are removed (about −190 lines in cell.cpp/cell.h).

Bug fix: complements of unparenthesized expressions

Complements were previously removed by flipping the operators in the token stream before precedence was enforced. The complement of an expression mixing intersection and union without inner parentheses was therefore wrong. For example, ~(1 2 | 3) was interpreted as -1 | (-2 -3) instead of (-1 | -2) -3, which produces overlapping cells (reproduced with openmc -g). Complements are now applied to the tree with De Morgan's laws after grouping. Expressions written by the Python API are fully parenthesized and were not affected; hand-written XML was.

A new test, tests/unit_tests/test_region_parsing.py, builds a cell from a random expression with minimal parentheses and complements, and its complement as a second cell. It then checks openmc.lib.find_cell against Python's independent Region.from_expression at random points. On develop, 7 of the 20 cases fail; with this PR all pass.

Other behavior changes

  • Region strings written to summary.h5 are generated from the tree. They are equivalent to the previous strings but no longer keep redundant parentheses (simple regions print exactly as before). openmc.Summary reads them back unchanged.
  • Region::n_surfaces() now returns the number of half-spaces, where it previously counted operator and parenthesis tokens too. It is used as a ray-tracing cost estimate for random ray domain-decomposition load balancing, where the half-space count is the better proxy. Random ray maintainers may want to confirm.

Performance

Single thread, transport time, median of 3 runs in shuffled order, develop / this PR. Results (k-eff and all tallies) are bit-for-bit identical in every case. Run-to-run spread is about ±2.5–4%.

Model develop this PR Ratio
17×17 PWR assembly (lattice, simple cells) 16.82 s 16.84 s 0.999
TRISO compact (particles as cells, matrix = compact minus particles) 19.37 s 19.69 s 0.983 (within spread)
FNG benchmark (many complex regions) 23.66 s 22.65 s 1.045
Water tank with 400 finite rods, water = tank minus rods 7.55 s 5.62 s 1.34

The full unit and regression test suite gives the same results as 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 2 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
@GuySten
GuySten requested a review from paulromano October 3, 2026 00:44
@GuySten
GuySten marked this pull request as ready for review October 3, 2026 00:44
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

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