From 873f64956c78babf8987865bce7d7a38bf689404 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 2 Jun 2026 13:26:19 -0500 Subject: [PATCH 01/11] Widen Tally::n_filter_bins_ and strides_ to int64_t A combined mesh+energy tally can exceed 2^31 filter bins (e.g. a 97.8M-cell mesh times 70 energy groups = 6.85e9 bins). With int32_t storage the bin count overflowed to a negative value that init_results() then cast to size_t, requesting a ~1.8e19-element vector and throwing std::length_error before transport even started. Widen n_filter_bins_, the per-filter strides_, and their accessors to int64_t. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/openmc/tallies/tally.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 66e6ff764f3..ae604b732de 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -97,9 +97,9 @@ class Tally { //! Given already-set filters, set the stride lengths void set_strides(); - int32_t strides(int i) const { return strides_[i]; } + int64_t strides(int i) const { return strides_[i]; } - int32_t n_filter_bins() const { return n_filter_bins_; } + int64_t n_filter_bins() const { return n_filter_bins_; } bool multiply_density() const { return multiply_density_; } @@ -184,9 +184,9 @@ class Tally { vector filters_; //!< Filter indices in global filters array //! Index strides assigned to each filter to support 1D indexing. - vector strides_; + vector strides_; - int32_t n_filter_bins_ {0}; + int64_t n_filter_bins_ {0}; //! Whether to multiply by atom density for reaction rates bool multiply_density_ {true}; From f0bb4dd9ecd55b13eed2606cb5eceb6c43ea1766 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 2 Jun 2026 13:26:48 -0500 Subject: [PATCH 02/11] Accumulate filter-bin stride product in int64_t in set_strides() set_strides() multiplied the per-filter bin counts into a plain int accumulator, so the running product overflowed before being stored into the now-int64_t n_filter_bins_. Promote the accumulator to int64_t so the product is formed in 64-bit. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/tallies/tally.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index e4ea56e5976..1a4a95f0342 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -512,7 +512,7 @@ void Tally::set_strides() // longest stride. auto n = filters_.size(); strides_.resize(n, 0); - int stride = 1; + int64_t stride = 1; for (int i = n - 1; i >= 0; --i) { strides_[i] = stride; stride *= model::tally_filters[filters_[i]]->n_bins(); From 59faa9af852cdde6fba62006735abfbd033011ae Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 2 Jun 2026 13:27:05 -0500 Subject: [PATCH 03/11] Return int64_t from SourceRegionContainer::index() index(sr, g) computes sr * negroups_ + g in 64-bit (sr is int64_t) but truncated the result back to int on return. With more than ~30.7M source regions at 70 groups the flattened element index exceeds 2^31, yielding a wrapped/negative offset and an out-of-bounds access into the correctly-sized flux arrays during the solve. Return int64_t. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/openmc/random_ray/source_region.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 65f2e6bc41c..86d80884ad3 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -690,7 +690,7 @@ class SourceRegionContainer { // Private Methods // Helper function for indexing - inline int index(int64_t sr, int g) const { return sr * negroups_ + g; } + inline int64_t index(int64_t sr, int g) const { return sr * negroups_ + g; } }; } // namespace openmc From c399a86b86aac59b42014f2e420b1891b5ccde07 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 2 Jun 2026 13:27:56 -0500 Subject: [PATCH 04/11] Widen FilterBinIter::index_ to int64_t FilterBinIter accumulates the flat results_ index across the tally filters in index_. For tallies with more than 2^31 filter bins this overflowed. Widen to int64_t so the random ray tally-task setup, which copies filter_iter.index_ into each TallyTask, records correct indices. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/openmc/tallies/tally_scoring.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/openmc/tallies/tally_scoring.h b/include/openmc/tallies/tally_scoring.h index d1aed28318c..4303ddb7a90 100644 --- a/include/openmc/tallies/tally_scoring.h +++ b/include/openmc/tallies/tally_scoring.h @@ -41,7 +41,7 @@ class FilterBinIter { FilterBinIter& operator++(); - int index_ {1}; + int64_t index_ {1}; double weight_ {1.}; vector& filter_matches_; From a42e8782dec86bd7b72922fa92946174c359a9b2 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 2 Jun 2026 13:29:24 -0500 Subject: [PATCH 05/11] Widen local filter_index accumulators in tally scoring to int64_t Three scoring helpers accumulated filter_index in a 32-bit int before indexing results_, which overflows for tallies exceeding 2^31 filter bins. Widen the locals to int64_t. Note: the continuous-energy and multigroup score_general_ce/score_general_mg paths still take filter_index as an int parameter; those are not exercised by the random ray solver and are left for a separate change. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/tallies/tally_scoring.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index b7947afabe9..0336c9bf79d 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -158,7 +158,7 @@ void score_fission_delayed_dg(int i_tally, int d_bin, double score, dg_match.bins_[i_bin] = d_bin; // Determine the filter scoring index - auto filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.; for (auto i = 0; i < tally.filters().size(); ++i) { auto i_filt = tally.filters(i); @@ -449,7 +449,7 @@ void score_fission_eout(Particle& p, int i_tally, int i_score, int score_bin) (score_bin == SCORE_PROMPT_NU_FISSION && g == 0)) { // Find the filter scoring index for this filter combination - int filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.0; for (auto j = 0; j < tally.filters().size(); ++j) { auto i_filt = tally.filters(j); @@ -497,7 +497,7 @@ void score_fission_eout(Particle& p, int i_tally, int i_score, int score_bin) } else { // Find the filter index and weight for this filter combination - int filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.; for (auto j = 0; j < tally.filters().size(); ++j) { auto i_filt = tally.filters(j); From 7a172c968fa185f9a106ea41fa1efe79fbf6f463 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 2 Jun 2026 13:29:51 -0500 Subject: [PATCH 06/11] Widen TallyTask::filter_idx to int64_t TallyTask stores the flat tally results_ index for each source-region/energy-group element. With more than 2^31 filter bins the int member truncated the now-64-bit filter index taken from FilterBinIter::index_, sending random ray scores to the wrong (or out-of-bounds) results_ bins. Widen the member and the constructor parameter to int64_t. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/openmc/random_ray/source_region.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 86d80884ad3..1d2bbe1e8dc 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -51,10 +51,10 @@ inline void hash_combine(size_t& seed, const size_t v) // every iteration. struct TallyTask { int tally_idx; - int filter_idx; + int64_t filter_idx; int score_idx; int score_type; - TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) + TallyTask(int tally_idx, int64_t filter_idx, int score_idx, int score_type) : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), score_type(score_type) {} From 3db88e4ade0c1cb2db1525a911ca740c4425ea1c Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 2 Jun 2026 13:32:13 -0500 Subject: [PATCH 07/11] Use int64_t loop counter when normalizing flux tally bins The flux-tally volume-normalization loop in random_ray_tally iterated filter bins with an int counter compared against n_filter_bins(), which is now int64_t. For tallies with more than 2^31 bins the int counter cannot reach the end (it overflows and the comparison wraps), so the upper bins would never be volume-normalized. Use an int64_t loop variable. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/random_ray/flat_source_domain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 06c6ef14d71..8e7f31290ff 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -724,7 +724,7 @@ void FlatSourceDomain::random_ray_tally() for (int i = 0; i < model::tallies.size(); i++) { Tally& tally {*model::tallies[i]}; #pragma omp parallel for - for (int bin = 0; bin < tally.n_filter_bins(); bin++) { + for (int64_t bin = 0; bin < tally.n_filter_bins(); bin++) { for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) { auto score_type = tally.scores_[score_idx]; if (score_type == SCORE_FLUX) { From 32bd43979f879ea52684932207ef9e2b3de449f7 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 2 Jun 2026 15:31:06 -0500 Subject: [PATCH 08/11] Widen filter_index parameter to int64_t in CE/MG tally scoring score_general_ce_nonanalog, score_general_ce_analog, and score_general_mg took the flat results_ filter index as an int parameter. Their callers compute it from FilterBinIter::index_ (now int64_t), so the value was narrowed back to 32 bits at the call boundary and could index the wrong or out-of-bounds bin for a regular Monte Carlo tally exceeding 2^31 filter bins. This path is not used by the random ray solver; widen the parameter so large continuous-energy and multigroup MC tallies are correct too. Each function uses filter_index only for its final tally.results_(filter_index, ...) write, so widening the parameter is sufficient. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/tallies/tally_scoring.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 0336c9bf79d..9f93df37ec1 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -578,7 +578,7 @@ double get_nuclide_xs(const Particle& p, int i_nuclide, int score_bin) //! collision estimator. void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, + int64_t filter_index, double filter_weight, int i_nuclide, double atom_density, double flux) { Tally& tally {*model::tallies[i_tally]}; @@ -1112,7 +1112,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, //! is not used for analog tallies. void score_general_ce_analog(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, + int64_t filter_index, double filter_weight, int i_nuclide, double atom_density, double flux) { Tally& tally {*model::tallies[i_tally]}; @@ -1615,7 +1615,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, //! argument is really just used for filter weights. void score_general_mg(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, + int64_t filter_index, double filter_weight, int i_nuclide, double atom_density, double flux) { auto& tally {*model::tallies[i_tally]}; From 00bab99435bbd0a20388efcc7bb1a7f377014e59 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 4 Jun 2026 11:52:53 -0500 Subject: [PATCH 09/11] Add unit test for tally filter-bin count 64-bit overflow Adds a Catch2 case to the existing tests/cpp_unit_tests/test_tally.cpp (already in TEST_NAMES, so no CMakeLists change). It builds a tally with two ~50,000-bin energy filters whose bin-count product (2.5e9) exceeds INT32_MAX, calls set_strides(), and asserts n_filter_bins() equals the exact 64-bit product and is positive. This pins the arithmetic fixed on this branch; it runs in milliseconds on ~1 MB and does not allocate the results tensor. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/cpp_unit_tests/test_tally.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/cpp_unit_tests/test_tally.cpp b/tests/cpp_unit_tests/test_tally.cpp index 964d30cc42a..d917bf81868 100644 --- a/tests/cpp_unit_tests/test_tally.cpp +++ b/tests/cpp_unit_tests/test_tally.cpp @@ -1,3 +1,4 @@ +#include "openmc/tallies/filter_energy.h" #include "openmc/tallies/tally.h" #include @@ -52,4 +53,28 @@ TEST_CASE("Test add/set_filter") REQUIRE(tally->filters().size() == 1); REQUIRE(model::filter_map[cell_filter->id()] == tally->filters(0)); +} + +// Regression test for 64-bit tally filter-bin counts (mesh x groups > 2^31). +TEST_CASE("Tally filter-bin count does not overflow 32 bits") +{ + // Two energy filters whose bin counts multiply to 2.5e9, above INT32_MAX. + constexpr int64_t bins_per_filter = 50000; + + // Only the bin count matters here, so the edge values are an arbitrary ramp. + std::vector edges(bins_per_filter + 1); + for (int64_t i = 0; i < bins_per_filter + 1; ++i) + edges[i] = static_cast(i); + + Tally* tally = Tally::create(); + for (int i = 0; i < 2; ++i) { + Filter* filter = Filter::create("energy"); + dynamic_cast(filter)->set_bins(edges); + tally->add_filter(filter); + } + tally->set_strides(); + + // set_strides() previously accumulated this product in a 32-bit int. + REQUIRE(tally->n_filter_bins() == bins_per_filter * bins_per_filter); + REQUIRE(tally->n_filter_bins() > 2147483647); } \ No newline at end of file From a667da4c54339f808fb3471c5138affd7fc7767d Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 4 Jun 2026 12:49:55 -0500 Subject: [PATCH 10/11] Fail loudly instead of overflowing for >2^31 mesh bins StructuredMesh::n_bins() and n_surface_bins() formed their bin counts in 32-bit int and silently overflowed for very large meshes (n_bins near ~2.1B cells; n_surface_bins ~12x sooner, ~179M cells in 3D). The resulting negative count surfaced downstream as the same cryptic std::length_error this branch fixes, but originating from the mesh layer. Compute both counts in int64_t and raise a clear fatal_error naming the mesh and count when they exceed the 2^31 tally-indexing limit. This does not lift the limit (the per-event bin index and Filter::n_bins_ are still 32-bit) -- it converts a silent overflow into an actionable message. The unstructured (MOAB/LibMesh) counts are left as-is, since a >2^31-element unstructured mesh is not realistic. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/mesh.cpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index 5ab7ac3988b..709e97b8b87 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -3,6 +3,7 @@ #include #include // for uint64_t #include // for memcpy +#include #define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include // for ceil #include // for size_t @@ -1080,13 +1081,26 @@ int StructuredMesh::get_bin(Position r) const int StructuredMesh::n_bins() const { - return std::accumulate( - shape_.begin(), shape_.begin() + n_dimension_, 1, std::multiplies<>()); + // Bin indices are stored as 32-bit ints in the tally system. + int64_t n = 1; + for (int i = 0; i < n_dimension_; ++i) + n *= shape_[i]; + if (n > std::numeric_limits::max()) { + fatal_error(fmt::format( + "Mesh {} has too many bins ({}) for 32-bit tally indexing", id_, n)); + } + return static_cast(n); } int StructuredMesh::n_surface_bins() const { - return 4 * n_dimension_ * n_bins(); + // Surface bin indices are stored as 32-bit ints in the tally system. + int64_t n = static_cast(n_bins()) * 4 * n_dimension_; + if (n > std::numeric_limits::max()) { + fatal_error(fmt::format( + "Mesh {} has too many surface bins ({}) for tally indexing", id_, n)); + } + return static_cast(n); } tensor::Tensor StructuredMesh::count_sites( From 053e5098b072e895b6ae45158f6649d7470863a3 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 4 Jun 2026 15:37:22 -0500 Subject: [PATCH 11/11] ran clang-format --- src/tallies/tally_scoring.cpp | 12 ++++++------ tests/cpp_unit_tests/test_tally.cpp | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 9f93df37ec1..7bce6ec137a 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -578,8 +578,8 @@ double get_nuclide_xs(const Particle& p, int i_nuclide, int score_bin) //! collision estimator. void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, - int64_t filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { Tally& tally {*model::tallies[i_tally]}; @@ -1112,8 +1112,8 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, //! is not used for analog tallies. void score_general_ce_analog(Particle& p, int i_tally, int start_index, - int64_t filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { Tally& tally {*model::tallies[i_tally]}; @@ -1615,8 +1615,8 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, //! argument is really just used for filter weights. void score_general_mg(Particle& p, int i_tally, int start_index, - int64_t filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { auto& tally {*model::tallies[i_tally]}; diff --git a/tests/cpp_unit_tests/test_tally.cpp b/tests/cpp_unit_tests/test_tally.cpp index d917bf81868..7af53c55517 100644 --- a/tests/cpp_unit_tests/test_tally.cpp +++ b/tests/cpp_unit_tests/test_tally.cpp @@ -52,7 +52,6 @@ TEST_CASE("Test add/set_filter") tally->set_filters(filters); REQUIRE(tally->filters().size() == 1); REQUIRE(model::filter_map[cell_filter->id()] == tally->filters(0)); - } // Regression test for 64-bit tally filter-bin counts (mesh x groups > 2^31).