Skip to content

Commit 51ea89c

Browse files
paulromanoGuySten
andauthored
Implement depth-awareness when enforcing precedence between union/intersection operators (#3730)
Co-authored-by: GuySten <guyste@post.bgu.ac.il>
1 parent 179048b commit 51ea89c

File tree

4 files changed

+158
-46
lines changed

4 files changed

+158
-46
lines changed

include/openmc/cell.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,11 @@ class Region {
123123
//! BoundingBox if the particle is in a complex cell.
124124
BoundingBox bounding_box_complex(vector<int32_t> postfix) const;
125125

126-
//! Enfource precedence: Parenthases, Complement, Intersection, Union
127-
void add_precedence();
126+
//! Enforce precedence between intersections and unions
127+
void enforce_precedence();
128128

129129
//! Add parenthesis to enforce precedence
130-
int64_t add_parentheses(int64_t start);
130+
void add_parentheses(int64_t start);
131131

132132
//! Remove complement operators from the expression
133133
void remove_complement_ops();

src/cell.cpp

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ Region::Region(std::string region_spec, int32_t cell_id)
660660
if (token == OP_UNION) {
661661
simple_ = false;
662662
// Ensure intersections have precedence over unions
663-
add_precedence();
663+
enforce_precedence();
664664
break;
665665
}
666666
}
@@ -703,7 +703,7 @@ void Region::apply_demorgan(
703703
//! precedence than unions using parentheses.
704704
//==============================================================================
705705

706-
int64_t Region::add_parentheses(int64_t start)
706+
void Region::add_parentheses(int64_t start)
707707
{
708708
int32_t start_token = expression_[start];
709709
// Add left parenthesis and set new position to be after parenthesis
@@ -712,14 +712,6 @@ int64_t Region::add_parentheses(int64_t start)
712712
}
713713
expression_.insert(expression_.begin() + start - 1, OP_LEFT_PAREN);
714714

715-
// Keep track of return iterator distance. If we don't encounter a left
716-
// parenthesis, we return an iterator corresponding to wherever the right
717-
// parenthesis is inserted. If a left parenthesis is encountered, an iterator
718-
// corresponding to the left parenthesis is returned. Also note that we keep
719-
// track of a *distance* instead of an iterator because the underlying memory
720-
// allocation may change.
721-
std::size_t return_it_dist = 0;
722-
723715
// Add right parenthesis
724716
// While the start iterator is within the bounds of infix
725717
while (start + 1 < expression_.size()) {
@@ -733,7 +725,6 @@ int64_t Region::add_parentheses(int64_t start)
733725
// in the region, when the operator is an intersection then include the
734726
// operator and next surface
735727
if (expression_[start] == OP_LEFT_PAREN) {
736-
return_it_dist = start;
737728
int depth = 1;
738729
do {
739730
start++;
@@ -750,54 +741,73 @@ int64_t Region::add_parentheses(int64_t start)
750741
--start;
751742
}
752743
expression_.insert(expression_.begin() + start, OP_RIGHT_PAREN);
753-
if (return_it_dist > 0) {
754-
return return_it_dist;
755-
} else {
756-
return start - 1;
757-
}
744+
return;
758745
}
759746
}
760747
}
761-
// If we get here a right parenthesis hasn't been placed,
762-
// return iterator
748+
// If we get here a right parenthesis hasn't been placed
763749
expression_.push_back(OP_RIGHT_PAREN);
764-
if (return_it_dist > 0) {
765-
return return_it_dist;
766-
} else {
767-
return start - 1;
768-
}
769750
}
770751

752+
//==============================================================================
753+
//! Add parentheses to enforce operator precedence in region expressions
754+
//!
755+
//! This function ensures that intersection operators have higher precedence
756+
//! than union operators by adding parentheses where needed. For example:
757+
//! "1 2 | 3" becomes "(1 2) | 3"
758+
//! "1 | 2 3" becomes "1 | (2 3)"
759+
//!
760+
//! The algorithm uses stacks to track the current operator type and its
761+
//! position at each parenthesis depth level. When it encounters a different
762+
//! operator at the same depth, it adds parentheses to group the
763+
//! higher-precedence operations.
771764
//==============================================================================
772765

773-
void Region::add_precedence()
766+
void Region::enforce_precedence()
774767
{
775-
int32_t current_op = 0;
776-
std::size_t current_dist = 0;
768+
// Stack tracking the operator type at each depth (0 = no operator seen yet)
769+
vector<int32_t> op_stack = {0};
777770

778-
for (int64_t i = 0; i < expression_.size(); i++) {
771+
// Stack tracking where the operator sequence started at each depth
772+
vector<std::size_t> pos_stack = {0};
773+
774+
for (int64_t i = 0; i < expression_.size(); ++i) {
779775
int32_t token = expression_[i];
780776

777+
if (token == OP_LEFT_PAREN) {
778+
// Entering a new parenthesis level - push new tracking state
779+
op_stack.push_back(0);
780+
pos_stack.push_back(0);
781+
continue;
782+
} else if (token == OP_RIGHT_PAREN) {
783+
// Exiting a parenthesis level - pop tracking state (keep at least one)
784+
if (op_stack.size() > 1) {
785+
op_stack.pop_back();
786+
pos_stack.pop_back();
787+
}
788+
continue;
789+
}
790+
781791
if (token == OP_UNION || token == OP_INTERSECTION) {
782-
if (current_op == 0) {
783-
// Set the current operator if is hasn't been set
784-
current_op = token;
785-
current_dist = i;
786-
} else if (token != current_op) {
787-
// If the current operator doesn't match the token, add parenthesis to
788-
// assert precedence
789-
if (current_op == OP_INTERSECTION) {
790-
i = add_parentheses(current_dist);
792+
if (op_stack.back() == 0) {
793+
// First operator at this depth - record it and its position
794+
op_stack.back() = token;
795+
pos_stack.back() = i;
796+
} else if (token != op_stack.back()) {
797+
// Encountered a different operator at the same depth - need to add
798+
// parentheses to enforce precedence. Intersection has higher
799+
// precedence, so we parenthesize the intersection terms.
800+
if (op_stack.back() == OP_INTERSECTION) {
801+
add_parentheses(pos_stack.back());
791802
} else {
792-
i = add_parentheses(i);
803+
add_parentheses(i);
793804
}
794-
current_op = 0;
795-
current_dist = 0;
805+
806+
// Restart the scan since we modified the expression
807+
i = -1; // Will be incremented to 0 by the for loop
808+
op_stack = {0};
809+
pos_stack = {0};
796810
}
797-
} else if (token > OP_COMPLEMENT) {
798-
// If the token is a parenthesis reset the current operator
799-
current_op = 0;
800-
current_dist = 0;
801811
}
802812
}
803813
}

tests/cpp_unit_tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ set(TEST_NAMES
66
test_math
77
test_mcpl_stat_sum
88
test_mesh
9+
test_region
910
# Add additional unit test files here
1011
)
1112

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
3+
#include "openmc/cell.h"
4+
#include "openmc/surface.h"
5+
6+
#include <pugixml.hpp>
7+
8+
namespace {
9+
10+
// Helper class to set up and tear down test surfaces
11+
class SurfaceFixture {
12+
public:
13+
SurfaceFixture()
14+
{
15+
pugi::xml_document doc;
16+
pugi::xml_node surf_node = doc.append_child("surface");
17+
surf_node.set_name("surface");
18+
surf_node.append_attribute("id") = "0";
19+
surf_node.append_attribute("type") = "x-plane";
20+
surf_node.append_attribute("coeffs") = "1";
21+
22+
for (int i = 1; i < 10; ++i) {
23+
surf_node.attribute("id") = i;
24+
openmc::model::surfaces.push_back(
25+
std::make_unique<openmc::SurfaceXPlane>(surf_node));
26+
openmc::model::surface_map[i] = i - 1;
27+
}
28+
}
29+
30+
~SurfaceFixture()
31+
{
32+
openmc::model::surfaces.clear();
33+
openmc::model::surface_map.clear();
34+
}
35+
};
36+
37+
} // anonymous namespace
38+
39+
TEST_CASE("Test region simplification")
40+
{
41+
SurfaceFixture fixture;
42+
43+
SECTION("Original bug case from issue #3685")
44+
{
45+
// Input: "-1 2 (-3 4) | (-5 6)" was being incorrectly interpreted
46+
auto region = openmc::Region("(-1 2 (-3 4) | (-5 6))", 0);
47+
REQUIRE(region.str() == " ( ( -1 2 ( -3 4 ) ) | ( -5 6 ) )");
48+
}
49+
50+
SECTION("Simple union - no extra parentheses needed")
51+
{
52+
auto region = openmc::Region("1 | 2", 0);
53+
REQUIRE(region.str() == " 1 | 2");
54+
}
55+
56+
SECTION("Intersection then union")
57+
{
58+
// Intersection should have higher precedence, so (1 2) grouped
59+
auto region = openmc::Region("1 2 | 3", 0);
60+
REQUIRE(region.str() == " ( 1 2 ) | 3");
61+
}
62+
63+
SECTION("Union then intersection")
64+
{
65+
// The (2 3) intersection should be grouped
66+
auto region = openmc::Region("1 | 2 3", 0);
67+
REQUIRE(region.str() == " 1 | ( 2 3 )");
68+
}
69+
70+
SECTION("Nested parentheses preserved")
71+
{
72+
// These parentheses are meaningful and should be preserved
73+
auto region = openmc::Region("(1 | 2) (3 | 4)", 0);
74+
REQUIRE(region.str() == " ( 1 | 2 ) ( 3 | 4 )");
75+
}
76+
77+
SECTION("Deep nesting")
78+
{
79+
auto region = openmc::Region("((1 2) | (3 4)) 5", 0);
80+
REQUIRE(region.str() == " ( ( 1 2 ) | ( 3 4 ) ) 5");
81+
}
82+
83+
SECTION("Multiple unions")
84+
{
85+
auto region = openmc::Region("1 | 2 | 3", 0);
86+
REQUIRE(region.str() == " 1 | 2 | 3");
87+
}
88+
89+
SECTION("Multiple intersections")
90+
{
91+
auto region = openmc::Region("1 2 3", 0);
92+
// Simple cell - no operators in output
93+
REQUIRE(region.str() == " 1 2 3");
94+
}
95+
96+
SECTION("Complex mixed expression")
97+
{
98+
auto region = openmc::Region("1 2 | 3 4 | 5 6", 0);
99+
REQUIRE(region.str() == " ( 1 2 ) | ( 3 4 ) | ( 5 6 )");
100+
}
101+
}

0 commit comments

Comments
 (0)