Skip to content

Commit e3bc615

Browse files
MatteoZammataromatteozammataronewcleopaulromano
authored
Fix numerical cancellation in RectLattice::distance for large pitch values (#3853)
Co-authored-by: matteo.zammataro <matteo.zammataro@newcleo.com> Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
1 parent 66359e5 commit e3bc615

8 files changed

Lines changed: 256 additions & 12 deletions

File tree

include/openmc/math_functions.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,5 +233,17 @@ void get_energy_index(
233233

234234
double standard_normal_cdf(double z);
235235

236+
//==============================================================================
237+
//! Return true if two floating-point values are approximately equal within a
238+
//! combined relative and absolute tolerance.
239+
//!
240+
//! \param a first floating point value
241+
//! \param b second floating point value
242+
//! \param rel_tol relative tolerance
243+
//! \param abs_tol absolute tolerance
244+
//! \return true if a and b are approximately equal, false otherwise
245+
//==============================================================================
246+
bool isclose(double a, double b, double rel_tol, double abs_tol);
247+
236248
} // namespace openmc
237249
#endif // OPENMC_MATH_FUNCTIONS_H

src/lattice.cpp

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "openmc/geometry.h"
1111
#include "openmc/geometry_aux.h"
1212
#include "openmc/hdf5_interface.h"
13+
#include "openmc/math_functions.h"
1314
#include "openmc/string_utils.h"
1415
#include "openmc/vector.h"
1516
#include "openmc/xml_interface.h"
@@ -260,25 +261,33 @@ std::pair<double, array<int, 3>> RectLattice::distance(
260261
// Determine the oncoming edge.
261262
double x0 {copysign(0.5 * pitch_[0], u.x)};
262263
double y0 {copysign(0.5 * pitch_[1], u.y)};
263-
double z0;
264264

265-
double d = std::min(
266-
u.x != 0.0 ? (x0 - x) / u.x : INFTY, u.y != 0.0 ? (y0 - y) / u.y : INFTY);
265+
// Evaluate the distance to each oncoming edge independently. Comparing these
266+
// distances directly (rather than reconstructing the crossing position)
267+
// avoids the floating-point cancellation that occurs for large pitches.
268+
double dx = u.x != 0.0 ? (x0 - x) / u.x : INFTY;
269+
double dy = u.y != 0.0 ? (y0 - y) / u.y : INFTY;
270+
double dz = INFTY;
267271
if (is_3d_) {
268-
z0 = copysign(0.5 * pitch_[2], u.z);
269-
d = std::min(d, u.z != 0.0 ? (z0 - z) / u.z : INFTY);
272+
double z0 {copysign(0.5 * pitch_[2], u.z)};
273+
dz = u.z != 0.0 ? (z0 - z) / u.z : INFTY;
270274
}
271275

272-
// Determine which lattice boundaries are being crossed
276+
// The distance to the nearest lattice boundary is the smallest axial
277+
// distance.
278+
double d = std::min({dx, dy, dz});
279+
280+
// Determine which lattice boundaries are being crossed. The axis attaining
281+
// the minimum is exactly equal to d, so at least one translation is always
282+
// set for a finite crossing; a near-equal second axis indicates a corner
283+
// crossing.
273284
array<int, 3> lattice_trans = {0, 0, 0};
274-
if (u.x != 0.0 && std::abs(x + u.x * d - x0) < FP_PRECISION)
285+
if (isclose(d, dx, FP_COINCIDENT, FP_PRECISION))
275286
lattice_trans[0] = copysign(1, u.x);
276-
if (u.y != 0.0 && std::abs(y + u.y * d - y0) < FP_PRECISION)
287+
if (isclose(d, dy, FP_COINCIDENT, FP_PRECISION))
277288
lattice_trans[1] = copysign(1, u.y);
278-
if (is_3d_) {
279-
if (u.z != 0.0 && std::abs(z + u.z * d - z0) < FP_PRECISION)
280-
lattice_trans[2] = copysign(1, u.z);
281-
}
289+
if (is_3d_ && isclose(d, dz, FP_COINCIDENT, FP_PRECISION))
290+
lattice_trans[2] = copysign(1, u.z);
282291

283292
return {d, lattice_trans};
284293
}

src/math_functions.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,4 +959,12 @@ void get_energy_index(
959959
}
960960
}
961961

962+
// Return true if two floating-point values are approximately equal within a
963+
// combined relative and absolute tolerance.
964+
bool isclose(double a, double b, double rel_tol, double abs_tol)
965+
{
966+
return std::abs(a - b) <=
967+
std::max(rel_tol * std::max(std::abs(a), std::abs(b)), abs_tol);
968+
}
969+
962970
} // namespace openmc

tests/cpp_unit_tests/test_math.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,24 @@ TEST_CASE("Test broaden_wmp_polynomials")
355355
REQUIRE_THAT(ref_val, Catch::Matchers::Approx(test_val));
356356
}
357357
}
358+
359+
TEST_CASE("Test isclose")
360+
{
361+
using openmc::isclose;
362+
363+
// Identical values are always close, regardless of tolerances.
364+
REQUIRE(isclose(1.0, 1.0, 0.0, 0.0));
365+
REQUIRE(isclose(0.0, 0.0, 0.0, 0.0));
366+
367+
// Absolute tolerance governs comparisons near zero.
368+
REQUIRE(isclose(0.0, 1e-15, 0.0, 1e-14));
369+
REQUIRE_FALSE(isclose(0.0, 1e-13, 0.0, 1e-14));
370+
371+
// Relative tolerance scales with the magnitude of the operands.
372+
REQUIRE(isclose(1.0e12, 1.0e12 + 1.0, 1e-12, 0.0));
373+
REQUIRE_FALSE(isclose(1.0e12, 1.0e12 + 10.0, 1e-12, 0.0));
374+
375+
// The looser of the two tolerances wins.
376+
REQUIRE(isclose(1.0, 1.0 + 1e-13, 0.0, 1e-12));
377+
REQUIRE(isclose(1.0e6, 1.0e6 + 1e-4, 1e-9, 0.0));
378+
}

tests/regression_tests/lattice_large_pitch/__init__.py

Whitespace-only changes.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?xml version='1.0' encoding='utf-8'?>
2+
<model>
3+
<materials>
4+
<material id="1">
5+
<density value="0.001" units="g/cm3"/>
6+
<nuclide name="N14" ao="1.0"/>
7+
</material>
8+
<material id="2">
9+
<density value="7.0" units="g/cm3"/>
10+
<nuclide name="Fe56" ao="1.0"/>
11+
</material>
12+
</materials>
13+
<geometry>
14+
<cell id="1" material="2" universe="1"/>
15+
<cell id="2" material="1" universe="2"/>
16+
<cell id="3" fill="3" region="1 -2 3 -4" universe="4"/>
17+
<lattice id="3">
18+
<pitch>100.0 100.0</pitch>
19+
<outer>2</outer>
20+
<dimension>3 3</dimension>
21+
<lower_left>-150.0 -150.0</lower_left>
22+
<universes>
23+
1 2 1
24+
2 1 2
25+
1 2 1 </universes>
26+
</lattice>
27+
<surface id="1" name="minimum x" type="x-plane" boundary="vacuum" coeffs="-150.0"/>
28+
<surface id="2" name="maximum x" type="x-plane" boundary="vacuum" coeffs="150.0"/>
29+
<surface id="3" name="minimum y" type="y-plane" boundary="vacuum" coeffs="-150.0"/>
30+
<surface id="4" name="maximum y" type="y-plane" boundary="vacuum" coeffs="150.0"/>
31+
</geometry>
32+
<settings>
33+
<run_mode>fixed source</run_mode>
34+
<particles>1000</particles>
35+
<batches>10</batches>
36+
</settings>
37+
<tallies>
38+
<mesh id="1">
39+
<dimension>6 6</dimension>
40+
<lower_left>-150.0 -150.0</lower_left>
41+
<upper_right>150.0 150.0</upper_right>
42+
</mesh>
43+
<filter id="1" type="mesh">
44+
<bins>1</bins>
45+
</filter>
46+
<tally id="1">
47+
<filters>1</filters>
48+
<scores>flux</scores>
49+
</tally>
50+
</tallies>
51+
</model>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
tally 1:
2+
1.749265E+01
3+
3.275925E+01
4+
4.159430E+01
5+
1.799568E+02
6+
7.027454E+01
7+
4.967568E+02
8+
6.789716E+01
9+
4.652258E+02
10+
4.272935E+01
11+
1.861129E+02
12+
2.073342E+01
13+
4.374279E+01
14+
4.028297E+01
15+
1.641642E+02
16+
6.734263E+01
17+
4.627171E+02
18+
1.037657E+02
19+
1.078682E+03
20+
1.108394E+02
21+
1.240927E+03
22+
7.236770E+01
23+
5.302034E+02
24+
4.356361E+01
25+
1.921990E+02
26+
6.731536E+01
27+
4.615977E+02
28+
9.850684E+01
29+
9.810272E+02
30+
5.164863E+02
31+
2.670336E+04
32+
5.097770E+02
33+
2.604770E+04
34+
1.064847E+02
35+
1.137536E+03
36+
7.052986E+01
37+
5.003220E+02
38+
7.065046E+01
39+
5.036723E+02
40+
1.044890E+02
41+
1.103250E+03
42+
5.121544E+02
43+
2.632389E+04
44+
5.065331E+02
45+
2.570752E+04
46+
1.044794E+02
47+
1.101249E+03
48+
6.569142E+01
49+
4.361120E+02
50+
4.739812E+01
51+
2.313289E+02
52+
7.402284E+01
53+
5.591689E+02
54+
1.066905E+02
55+
1.149521E+03
56+
1.038665E+02
57+
1.086813E+03
58+
7.160008E+01
59+
5.217744E+02
60+
4.219705E+01
61+
1.816100E+02
62+
2.089838E+01
63+
4.464899E+01
64+
4.154271E+01
65+
1.745866E+02
66+
7.081253E+01
67+
5.049997E+02
68+
6.673273E+01
69+
4.509038E+02
70+
4.184624E+01
71+
1.771546E+02
72+
1.853898E+01
73+
3.538608E+01
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Regression test for rectangular lattices with large pitch values.
2+
3+
Large pitches used to trigger a segmentation fault in ``RectLattice::distance``
4+
because the boundary-crossing check compared a reconstructed crossing position
5+
against an absolute tolerance that did not scale with the geometry (see #3852).
6+
This test transports particles across a lattice with a large pitch to ensure the
7+
crossing logic remains robust.
8+
9+
"""
10+
11+
import openmc
12+
import pytest
13+
14+
from tests.testing_harness import PyAPITestHarness
15+
16+
17+
@pytest.fixture
18+
def model():
19+
model = openmc.Model()
20+
21+
# Large pitch that previously caused floating-point cancellation
22+
pitch = 100.0
23+
n = 3
24+
25+
air = openmc.Material()
26+
air.set_density('g/cm3', 0.001)
27+
air.add_nuclide('N14', 1.0)
28+
metal = openmc.Material()
29+
metal.set_density('g/cm3', 7.0)
30+
metal.add_nuclide('Fe56', 1.0)
31+
32+
metal_cell = openmc.Cell(fill=metal)
33+
metal_uni = openmc.Universe(cells=[metal_cell])
34+
air_cell = openmc.Cell(fill=air)
35+
air_uni = openmc.Universe(cells=[air_cell])
36+
37+
lattice = openmc.RectLattice()
38+
lattice.lower_left = (-pitch*n/2, -pitch*n/2)
39+
lattice.pitch = (pitch, pitch)
40+
lattice.outer = air_uni
41+
lattice.universes = [
42+
[metal_uni, air_uni, metal_uni],
43+
[air_uni, metal_uni, air_uni],
44+
[metal_uni, air_uni, metal_uni],
45+
]
46+
47+
box = openmc.model.RectangularPrism(pitch*n, pitch*n, boundary_type='vacuum')
48+
root_cell = openmc.Cell(region=-box, fill=lattice)
49+
model.geometry = openmc.Geometry([root_cell])
50+
51+
model.settings.run_mode = 'fixed source'
52+
model.settings.batches = 10
53+
model.settings.particles = 1000
54+
55+
mesh = openmc.RegularMesh()
56+
mesh.dimension = (6, 6)
57+
mesh.lower_left = (-pitch*n/2, -pitch*n/2)
58+
mesh.upper_right = (pitch*n/2, pitch*n/2)
59+
mesh_filter = openmc.MeshFilter(mesh)
60+
tally = openmc.Tally(tally_id=1)
61+
tally.filters = [mesh_filter]
62+
tally.scores = ['flux']
63+
model.tallies = [tally]
64+
65+
return model
66+
67+
68+
def test_lattice_large_pitch(model):
69+
harness = PyAPITestHarness('statepoint.10.h5', model)
70+
harness.main()

0 commit comments

Comments
 (0)