From fc440368545e6b3c4db81bdfb2774930a13714a0 Mon Sep 17 00:00:00 2001 From: GuySten Date: Fri, 28 Aug 2026 11:25:59 +0300 Subject: [PATCH 1/3] fix dg filter scoring and add a test --- src/tallies/tally_scoring.cpp | 12 +- tests/unit_tests/test_filter_delayedgroup.py | 115 +++++++++++++++++++ 2 files changed, 125 insertions(+), 2 deletions(-) mode change 100644 => 100755 src/tallies/tally_scoring.cpp create mode 100755 tests/unit_tests/test_filter_delayedgroup.py diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp old mode 100644 new mode 100755 index 241cbf0084c..ea2c5aad038 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -968,8 +968,16 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, const DelayedGroupFilter& filt { *dynamic_cast( model::tally_filters[i_dg_filt].get())}; - score_fission_delayed_dg(i_tally, delayed_groups[0] - 1, - score, score_index, p.filter_matches()); + // The bin index is the group's position in the filter's + // list of groups, which equals group - 1 only when the + // filter selects all groups in ascending order. + for (int d_bin = 0; d_bin < filt.n_bins(); ++d_bin) { + if (filt.groups()[d_bin] == delayed_groups[0]) { + score_fission_delayed_dg( + i_tally, d_bin, score, score_index, p.filter_matches()); + break; + } + } continue; } } diff --git a/tests/unit_tests/test_filter_delayedgroup.py b/tests/unit_tests/test_filter_delayedgroup.py new file mode 100755 index 00000000000..fc540ae0e7f --- /dev/null +++ b/tests/unit_tests/test_filter_delayedgroup.py @@ -0,0 +1,115 @@ +"""Test that delayed group filter bins follow the groups they name rather than +their position in the filter's list of bins.""" + +import pytest +import openmc + + +GROUPS_ASCENDING = [1, 2, 3, 4, 5, 6] +GROUPS_SHUFFLED = [4, 1, 6, 3, 5, 2] +GROUPS_SUBSET = [2, 4] + +SCORES = ["delayed-nu-fission", "ifp-beta-numerator"] + + +@pytest.fixture(scope="module") +def geometry(): + openmc.reset_auto_ids() + material = openmc.Material() + material.add_nuclide("U235", 1.0) + material.set_density("g/cm3", 16.0) + sphere = openmc.Sphere(r=10.0, boundary_type="vacuum") + cell = openmc.Cell(region=-sphere, fill=material) + return openmc.Geometry([cell]) + + +def build_model(geometry, score): + """Model tallying one score four ways: three delayed group filters that + differ only in how the bins are listed, plus an unfiltered reference.""" + model = openmc.Model(geometry=geometry) + model.settings.particles = 5000 + model.settings.batches = 20 + model.settings.inactive = 5 + model.settings.ifp_n_generation = 5 + + for name, groups in [ + ("ascending", GROUPS_ASCENDING), + ("shuffled", GROUPS_SHUFFLED), + ("subset", GROUPS_SUBSET), + ]: + tally = openmc.Tally(name=name) + tally.scores = [score] + tally.filters = [openmc.DelayedGroupFilter(groups)] + model.tallies.append(tally) + + tally = openmc.Tally(name="unfiltered") + tally.scores = [score] + model.tallies.append(tally) + + return model + + +def means_by_group(statepoint, name): + """Map delayed group number to tallied mean. + + The group order is read back from the filter stored in the statepoint, so + a mismatch between the reported bin labels and the values fails here + instead of being hidden by assuming an order. + """ + tally = statepoint.get_tally(name=name) + delayed_group_filter = tally.find_filter(openmc.DelayedGroupFilter) + means = tally.mean.ravel() + assert len(means) == len(delayed_group_filter.bins) + return dict(zip(delayed_group_filter.bins, means)) + + +@pytest.mark.parametrize("score", SCORES) +def test_bin_order(run_in_tmpdir, geometry, score): + """Listing the groups out of order must not move the results. + + All tallies come from a single run, so they see the same histories and the + same scoring events. Corresponding groups must therefore agree exactly, + not merely within statistics. + """ + model = build_model(geometry, score) + sp_file = model.run() + with openmc.StatePoint(sp_file) as sp: + ascending = means_by_group(sp, "ascending") + shuffled = means_by_group(sp, "shuffled") + + # Guard against a vacuous pass if the run were too short to populate bins + assert all(value > 0.0 for value in ascending.values()) + + assert sorted(shuffled) == sorted(ascending) + for group in GROUPS_ASCENDING: + assert shuffled[group] == pytest.approx(ascending[group], rel=1e-6) + + +@pytest.mark.parametrize("score", SCORES) +def test_bin_subset(run_in_tmpdir, geometry, score): + """A partial selection must reproduce those groups from the full list. + + A bin index computed as ``group - 1`` also writes out of bounds here, + since group 4 is bin 3 of a two bin filter. + """ + model = build_model(geometry, score) + sp_file = model.run() + with openmc.StatePoint(sp_file) as sp: + ascending = means_by_group(sp, "ascending") + subset = means_by_group(sp, "subset") + + assert sorted(subset) == sorted(GROUPS_SUBSET) + for group in GROUPS_SUBSET: + assert subset[group] == pytest.approx(ascending[group], rel=1e-6) + + +@pytest.mark.parametrize("score", SCORES) +def test_bins_sum_to_unfiltered(run_in_tmpdir, geometry, score): + """Every delayed neutron lands in exactly one bin and none are lost.""" + model = build_model(geometry, score) + sp_file = model.run() + with openmc.StatePoint(sp_file) as sp: + ascending = means_by_group(sp, "ascending") + unfiltered = sp.get_tally(name="unfiltered").mean.ravel()[0] + + assert sum(ascending.values()) == pytest.approx(unfiltered, rel=1e-6) From 92b3b39a516a8f3c248c5685ba097a39bdebd9f1 Mon Sep 17 00:00:00 2001 From: GuySten Date: Fri, 28 Aug 2026 11:31:18 +0300 Subject: [PATCH 2/3] remove unneeded comment --- src/tallies/tally_scoring.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index ea2c5aad038..37d450ecdbb 100755 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -968,9 +968,6 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, const DelayedGroupFilter& filt { *dynamic_cast( model::tally_filters[i_dg_filt].get())}; - // The bin index is the group's position in the filter's - // list of groups, which equals group - 1 only when the - // filter selects all groups in ascending order. for (int d_bin = 0; d_bin < filt.n_bins(); ++d_bin) { if (filt.groups()[d_bin] == delayed_groups[0]) { score_fission_delayed_dg( From 2cc45f190615c7707300b316541302c403992d4f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 29 Aug 2026 14:31:13 -0500 Subject: [PATCH 3/3] Consolidate tests --- src/tallies/tally_scoring.cpp | 0 tests/unit_tests/test_filter_delayedgroup.py | 40 ++++++-------------- 2 files changed, 11 insertions(+), 29 deletions(-) mode change 100755 => 100644 src/tallies/tally_scoring.cpp mode change 100755 => 100644 tests/unit_tests/test_filter_delayedgroup.py diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp old mode 100755 new mode 100644 diff --git a/tests/unit_tests/test_filter_delayedgroup.py b/tests/unit_tests/test_filter_delayedgroup.py old mode 100755 new mode 100644 index fc540ae0e7f..d471e42df78 --- a/tests/unit_tests/test_filter_delayedgroup.py +++ b/tests/unit_tests/test_filter_delayedgroup.py @@ -64,52 +64,34 @@ def means_by_group(statepoint, name): @pytest.mark.parametrize("score", SCORES) -def test_bin_order(run_in_tmpdir, geometry, score): - """Listing the groups out of order must not move the results. +def test_delayed_group_bins(run_in_tmpdir, geometry, score): + """Delayed group labels determine values independently of bin order. All tallies come from a single run, so they see the same histories and the - same scoring events. Corresponding groups must therefore agree exactly, - not merely within statistics. + same scoring events. Corresponding groups must therefore agree within + statistics, and all selected groups must sum to the unfiltered result. """ model = build_model(geometry, score) sp_file = model.run() with openmc.StatePoint(sp_file) as sp: ascending = means_by_group(sp, "ascending") shuffled = means_by_group(sp, "shuffled") + subset = means_by_group(sp, "subset") + unfiltered = sp.get_tally(name="unfiltered").mean.ravel()[0] - # Guard against a vacuous pass if the run were too short to populate bins + # Guard against a vacuous pass if the run were too short to populate bins. assert all(value > 0.0 for value in ascending.values()) + # Listing groups out of order must not move the corresponding results. assert sorted(shuffled) == sorted(ascending) for group in GROUPS_ASCENDING: assert shuffled[group] == pytest.approx(ascending[group], rel=1e-6) - -@pytest.mark.parametrize("score", SCORES) -def test_bin_subset(run_in_tmpdir, geometry, score): - """A partial selection must reproduce those groups from the full list. - - A bin index computed as ``group - 1`` also writes out of bounds here, - since group 4 is bin 3 of a two bin filter. - """ - model = build_model(geometry, score) - sp_file = model.run() - with openmc.StatePoint(sp_file) as sp: - ascending = means_by_group(sp, "ascending") - subset = means_by_group(sp, "subset") - + # A partial selection must reproduce those groups from the full list. A + # group-to-bin index assumption would write out of bounds for group 4. assert sorted(subset) == sorted(GROUPS_SUBSET) for group in GROUPS_SUBSET: assert subset[group] == pytest.approx(ascending[group], rel=1e-6) - -@pytest.mark.parametrize("score", SCORES) -def test_bins_sum_to_unfiltered(run_in_tmpdir, geometry, score): - """Every delayed neutron lands in exactly one bin and none are lost.""" - model = build_model(geometry, score) - sp_file = model.run() - with openmc.StatePoint(sp_file) as sp: - ascending = means_by_group(sp, "ascending") - unfiltered = sp.get_tally(name="unfiltered").mean.ravel()[0] - + # Every delayed neutron lands in exactly one bin and none are lost. assert sum(ascending.values()) == pytest.approx(unfiltered, rel=1e-6)