Repository navigation
Delayed fission sites rejected by the time cutoff break analog fission tallies #4179
Description
Bug Description
With a neutron time cutoff (Settings.cutoff['time_neutron']), the analog fission tallies count the fission neutrons of a collision inconsistently, and some of them read past the end of a vector.
create_fission_sites() rejects a delayed fission site whose emission time is past the cutoff. Both copies of the function do this: src/physics.cpp (continuous energy) and src/physics_mg.cpp (multigroup). The continuous-energy version, abridged:
for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
...
// Reject site if it exceeds time cutoff
if (site.delayed_group > 0) {
double t_cutoff = settings::time_cutoff[site.particle.transport_index()];
if (site.time > t_cutoff) {
continue;
}
}
...
// Increment the number of neutrons born delayed
if (site.delayed_group > 0) {
nu_d[site.delayed_group - 1]++;
}
// Write fission particles to nuBank
NuBank& nu_bank_entry = p.nu_bank().emplace_back();
...
}
// If shared fission bank was full, and no fissions could be added,
// set the particle fission flag to false.
if (n_sites_stored == 0) {
p.fission() = false;
return;
}
...
nu = n_sites_stored;
// Store the total weight banked for analog fission tallies
p.n_bank() = nu;
p.wgt_bank() = nu / weight;The continue still runs the loop increment. A rejected site is therefore counted in n_bank and wgt_bank, but it gets no nu_bank entry and no n_delayed_bank count. After a collision with r rejected sites, n_bank == nu_bank.size() + r, and n_sites_stored == 0 never holds when a site was rejected, so fission() stays true even if every site was rejected. The analog scores in src/tallies/tally_scoring.cpp then go wrong in four ways:
- Out-of-bounds reads.
score_fission_eout()is used for analognu-fission,prompt-nu-fissionanddelayed-nu-fissionwith anEnergyoutFilter. It and the analogdecay-ratescore loop overp.nu_bank(i)fori < p.n_bank(), andnu_bank(i)is unchecked. They read past the end of the vector. Within its capacity they get a stale entry from an earlier collision, and the result is silently wrong. Beyond the capacity they read other memory, and the run can crash. - Rejected delayed neutrons are scored as prompt. Analog
prompt-nu-fissionuses1 - n_delayed / n_bank. - They are missing from
delayed-nu-fission. Analogdelayed-nu-fission(with or without aDelayedGroupFilter) counts only the delayed neutrons emitted before the cutoff. The track-length and collision estimates of the same score usenu(E) * fission, so they count all of them. fission()stays true with an emptynu_bank. This happens when every site of a collision is rejected, and the loops of item 1 then read from an empty vector. In the runs counted below, nearly every collision that produced fission neutrons produced one, and every collision that lost a site lost all of its sites.
How often it happens was counted on develop with a temporary instrumentation patch, which is not part of any PR. The cutoff was 1e-2 s in every run. Collisions with a rejected site, out of all collisions that produced fission neutrons:
- 300 of 1034 in the first generation of a 2-group MG slab eigenvalue run with delayed fractions 0.1 and 0.2;
- 55 of 9027 in a CE U-235 sphere eigenvalue run;
- 4974 of 4975 in the fixed-source script below.
The rejection came with the delayed neutron emission time (#2898, merged as part of #2992), so it was first released in v0.15.3 and is also in v0.16.0. Before that, a delayed site carried its parent's time and could not be past the cutoff. Without a time cutoff (the default is infinite) no site is rejected, and none of the above happens.
Steps to Reproduce
The script below is a multigroup model that needs no external data. Every fission neutron is delayed (the prompt nu-fission is zero). It runs in fixed-source mode with a time cutoff of 1e-2 s:
import numpy as np
import openmc
from openmc.examples import slab_mg
# Two energy groups and two delayed groups. Every fission neutron is
# delayed: the prompt nu-fission cross section is zero.
groups = openmc.mgxs.EnergyGroups([0.0, 0.625, 20.0e6])
fission = np.array([0.002817, 0.097])
nu_fission = 2.5 * fission
xsdata = openmc.XSdata('mat_1', groups, num_delayed_groups=2)
xsdata.order = 1
xsdata.set_total([0.33588, 0.54628])
xsdata.set_absorption(np.array([0.008708, 0.02518]) + fission)
xsdata.set_scatter_matrix(np.array(
[[[0.31980, 0.06694], [0.004555, -0.0003972]],
[[0.00000, 0.00000], [0.424100, 0.05439000]]]))
xsdata.set_fission(fission)
xsdata.set_prompt_nu_fission(np.zeros(2))
xsdata.set_delayed_nu_fission(np.outer([1/3, 2/3], nu_fission))
xsdata.set_chi_prompt([1.0, 0.0])
xsdata.set_chi_delayed([[0.8, 0.2], [0.6, 0.4]])
xsdata.set_decay_rate([0.0127, 0.0317])
xsdata.set_inverse_velocity([1.0e-9, 4.5e-6])
library = openmc.MGXSLibrary(groups, num_delayed_groups=2)
library.add_xsdata(xsdata)
library.export_to_hdf5('2g.h5')
model = slab_mg(mgxslib_name='2g.h5')
model.settings.run_mode = 'fixed source'
model.settings.batches = 5
model.settings.particles = 1000
model.settings.seed = 1
model.settings.cutoff = {'time_neutron': 1.0e-2}
tally = openmc.Tally(name='analog')
tally.scores = ['nu-fission', 'prompt-nu-fission', 'delayed-nu-fission']
tally.estimator = 'analog'
model.tallies = [tally]
sp_path = model.run()
with openmc.StatePoint(sp_path) as sp:
t = sp.get_tally(name='analog')
for score in t.scores:
print(f'{score:20s} {t.get_values(scores=[score]).sum():.4f}')Output on develop (a5bc348, 2 OpenMP threads):
nu-fission 0.9950
prompt-nu-fission 0.9948
delayed-nu-fission 0.0002
The material has no prompt fission neutrons, so prompt-nu-fission should be 0. Of the 4975 fission neutrons, 4974 are delayed neutrons that would be emitted after the cutoff. They are counted in nu-fission, scored as prompt, and missing from delayed-nu-fission.
To see the out-of-bounds read, add an analog tally with an EnergyoutFilter, i.e. replace model.tallies = [tally] with:
eout = openmc.Tally(name='energyout')
eout.filters = [openmc.EnergyoutFilter([0.0, 0.625, 20.0e6])]
eout.scores = ['nu-fission']
eout.estimator = 'analog'
model.tallies = [tally, eout]On a RelWithDebInfo build of develop, this run crashed with SIGSEGV in score_fission_eout() (tally_scoring.cpp:405, the first read of the entry). A build configured with -DCMAKE_CXX_FLAGS=-D_GLIBCXX_ASSERTIONS, which checks std::vector::operator[], aborts with:
/usr/include/c++/13/bits/stl_vector.h:1128: std::vector<_Tp, _Alloc>::reference std::vector<_Tp, _Alloc>::operator[](size_type) [with _Tp = openmc::NuBank; _Alloc = std::allocator<openmc::NuBank>; reference = openmc::NuBank&; size_type = long unsigned int]: Assertion '__n < this->size()' failed.
#8 openmc::ParticleData::nu_bank (i=0, ...) at include/openmc/particle_data.h:735
#9 openmc::score_fission_eout (...) at src/tallies/tally_scoring.cpp:402
It is called from score_general_mg(), score_analog_tally_mg() and Particle::event_collide(). Continuous-energy mode aborts the same way, with score_general_ce_analog() as the caller. One such case is the first generation of an eigenvalue run of a bare U-235 sphere with the cutoff and analog fission tallies with an EnergyoutFilter.
A second check needs no assumption about the expected values. In the first generation of an eigenvalue run, no neutron history lasts 1e-2 s, so the transport is the same with and without the cutoff: k and the flux agree to round-off. Every analog fission tally should then agree too. On develop they do not:
- 2-group MG slab (
slab_mgwith the library above, but with delayed fractions 0.1 and 0.2 and prompt fission), 1000 particles, cutoff versus no cutoff:prompt-nu-fissionis 1.034 versus 0.734;delayed-nu-fissionis 0 versus 0.3;nu-fissionis 1.034 in both runs.
- CE U-235 sphere (16 g/cm3, r = 10 cm), 10000 particles:
prompt-nu-fissionis 0.9039 versus 0.8984;delayed-nu-fissionis 0.0001 versus 0.0056;nu-fissionis 0.904 in both runs.
Environment
- OpenMC develop at a5bc348 (
openmc --version: 0.16.1-dev57), built from source with CMake (RelWithDebInfo,OPENMC_ENABLE_STRICT_FP=ON); for the assertion, the same with-DCMAKE_CXX_FLAGS=-D_GLIBCXX_ASSERTIONS. - Ubuntu 24.04.5, GCC 13.3.0, 2 OpenMP threads. History-based and event-based transport are both affected.
- Data: the multigroup library is made by the script. Continuous energy uses the NNDC HDF5 library (
nndc_hdf5). - Reading the code shows the same rejection in v0.15.3 and v0.16.0; nothing was run on those versions.
Activity