Skip to content

Commit ecb0a33

Browse files
ilhamvgridleypaulromano
authored
Combing for fission site sampling, and delayed neutron emission time (#2992)
Co-authored-by: Gavin Ridley <[email protected]> Co-authored-by: Paul Romano <[email protected]>
1 parent 007ac81 commit ecb0a33

File tree

241 files changed

+9795
-9675
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

241 files changed

+9795
-9675
lines changed

docs/source/methods/neutron_physics.rst

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,10 @@ create and store fission sites for the following generation. First, the average
290290
number of prompt and delayed neutrons must be determined to decide whether the
291291
secondary neutrons will be prompt or delayed. This is important because delayed
292292
neutrons have a markedly different spectrum from prompt neutrons, one that has a
293-
lower average energy of emission. The total number of neutrons emitted
293+
lower average energy of emission. Furthermore, in simulations where tracking
294+
time of neutrons is important, we need to consider the emission time delay of
295+
the secondary neutrons, which is dependent on the decay constant of the
296+
delayed neutron precursor. The total number of neutrons emitted
294297
:math:`\nu_t` is given as a function of incident energy in the ENDF format. Two
295298
representations exist for :math:`\nu_t`. The first is a polynomial of order
296299
:math:`N` with coefficients :math:`c_0,c_1,\dots,c_N`. If :math:`\nu_t` has this
@@ -306,8 +309,8 @@ interpolation law. The number of prompt neutrons released per fission event
306309
:math:`\nu_p` is also given as a function of incident energy and can be
307310
specified in a polynomial or tabular format. The number of delayed neutrons
308311
released per fission event :math:`\nu_d` can only be specified in a tabular
309-
format. In practice, we only need to determine :math:`nu_t` and
310-
:math:`nu_d`. Once these have been determined, we can calculated the delayed
312+
format. In practice, we only need to determine :math:`\nu_t` and
313+
:math:`\nu_d`. Once these have been determined, we can calculate the delayed
311314
neutron fraction
312315

313316
.. math::
@@ -335,8 +338,14 @@ neutrons. Otherwise, we produce :math:`\lfloor \nu \rfloor + 1` neutrons. Then,
335338
for each fission site produced, we sample the outgoing angle and energy
336339
according to the algorithms given in :ref:`sample-angle` and
337340
:ref:`sample-energy` respectively. If the neutron is to be born delayed, then
338-
there is an extra step of sampling a delayed neutron precursor group since they
339-
each have an associated secondary energy distribution.
341+
there is an extra step of sampling a delayed neutron precursor group to get the
342+
associated secondary energy distribution and the decay constant
343+
:math:`\lambda`, which is needed to sample the emission delay time :math:`t_d`:
344+
345+
.. math::
346+
:label: sample-delay-time
347+
348+
t_d = -\frac{\ln \xi}{\lambda}.
340349
341350
The sampled outgoing angle and energy of fission neutrons along with the
342351
position of the collision site are stored in an array called the fission
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import matplotlib.pyplot as plt
2+
import numpy as np
3+
import openmc
4+
5+
###############################################################################
6+
# Create materials for the problem
7+
8+
uo2 = openmc.Material(name="UO2 fuel at 2.4% wt enrichment")
9+
uo2.set_density("g/cm3", 10.29769)
10+
uo2.add_element("U", 1.0, enrichment=2.4)
11+
uo2.add_element("O", 2.0)
12+
13+
helium = openmc.Material(name="Helium for gap")
14+
helium.set_density("g/cm3", 0.001598)
15+
helium.add_element("He", 2.4044e-4)
16+
17+
zircaloy = openmc.Material(name="Zircaloy 4")
18+
zircaloy.set_density("g/cm3", 6.55)
19+
zircaloy.add_element("Sn", 0.014, "wo")
20+
zircaloy.add_element("Fe", 0.00165, "wo")
21+
zircaloy.add_element("Cr", 0.001, "wo")
22+
zircaloy.add_element("Zr", 0.98335, "wo")
23+
24+
borated_water = openmc.Material(name="Borated water")
25+
borated_water.set_density("g/cm3", 0.740582)
26+
borated_water.add_element("B", 2.0e-4) # 3x the original pincell
27+
borated_water.add_element("H", 5.0e-2)
28+
borated_water.add_element("O", 2.4e-2)
29+
borated_water.add_s_alpha_beta("c_H_in_H2O")
30+
31+
###############################################################################
32+
# Define problem geometry
33+
34+
# Create cylindrical surfaces
35+
fuel_or = openmc.ZCylinder(r=0.39218, name="Fuel OR")
36+
clad_ir = openmc.ZCylinder(r=0.40005, name="Clad IR")
37+
clad_or = openmc.ZCylinder(r=0.45720, name="Clad OR")
38+
39+
# Create a region represented as the inside of a rectangular prism
40+
pitch = 1.25984
41+
box = openmc.model.RectangularPrism(pitch, pitch, boundary_type="reflective")
42+
43+
# Create cells, mapping materials to regions
44+
fuel = openmc.Cell(fill=uo2, region=-fuel_or)
45+
gap = openmc.Cell(fill=helium, region=+fuel_or & -clad_ir)
46+
clad = openmc.Cell(fill=zircaloy, region=+clad_ir & -clad_or)
47+
water = openmc.Cell(fill=borated_water, region=+clad_or & -box)
48+
49+
# Create a model and assign geometry
50+
model = openmc.Model()
51+
model.geometry = openmc.Geometry([fuel, gap, clad, water])
52+
53+
###############################################################################
54+
# Define problem settings
55+
56+
# Set the mode
57+
model.settings.run_mode = "fixed source"
58+
59+
# Indicate how many batches and particles to run
60+
model.settings.batches = 10
61+
model.settings.particles = 10000
62+
63+
# Set time cutoff (we only care about t < 100 seconds, see tally below)
64+
model.settings.cutoff = {"time_neutron": 100}
65+
66+
# Create the neutron pulse source (by default, isotropic direction, t=0)
67+
space = openmc.stats.Point() # At the origin (0, 0, 0)
68+
energy = openmc.stats.delta_function(14.1e6) # At 14.1 MeV
69+
model.settings.source = openmc.IndependentSource(space=space, energy=energy)
70+
71+
###############################################################################
72+
# Define tallies
73+
74+
# Create time filter
75+
t_grid = np.insert(np.logspace(-6, 2, 100), 0, 0.0)
76+
time_filter = openmc.TimeFilter(t_grid)
77+
78+
# Tally for total neutron density in time
79+
density_tally = openmc.Tally(name="Density")
80+
density_tally.filters = [time_filter]
81+
density_tally.scores = ["inverse-velocity"]
82+
83+
# Add tallies to model
84+
model.tallies = openmc.Tallies([density_tally])
85+
86+
87+
# Run the model
88+
model.run(apply_tally_results=True)
89+
90+
# Bin-averaged result
91+
density_mean = density_tally.mean.ravel() / np.diff(t_grid)
92+
93+
# Plot particle density versus time
94+
fig, ax = plt.subplots()
95+
ax.stairs(density_mean, t_grid)
96+
ax.set_xscale("log")
97+
ax.set_yscale("log")
98+
ax.set_xlabel("Time [s]")
99+
ax.set_ylabel("Total density")
100+
ax.grid()
101+
plt.show()

include/openmc/settings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ extern double source_rejection_fraction; //!< Minimum fraction of source sites
150150

151151
extern int
152152
max_history_splits; //!< maximum number of particle splits for weight windows
153+
extern int max_secondaries; //!< maximum number of secondaries in the bank
153154
extern int64_t ssw_max_particles; //!< maximum number of particles to be
154155
//!< banked on surfaces per process
155156
extern int64_t ssw_max_files; //!< maximum number of surface source files

openmc/settings.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ class Settings:
128128
Maximum number of times a particle can split during a history
129129
130130
.. versionadded:: 0.13
131+
max_secondaries : int
132+
Maximum secondary bank size
133+
134+
.. versionadded:: 0.15.3
131135
max_tracks : int
132136
Maximum number of tracks written to a track file (per MPI process).
133137
@@ -431,6 +435,7 @@ def __init__(self, **kwargs):
431435
self._weight_window_checkpoints = {}
432436
self._max_history_splits = None
433437
self._max_tracks = None
438+
self._max_secondaries = None
434439
self._use_decay_photons = None
435440

436441
self._random_ray = {}
@@ -1137,6 +1142,16 @@ def max_history_splits(self, value: int):
11371142
cv.check_greater_than('max particle splits', value, 0)
11381143
self._max_history_splits = value
11391144

1145+
@property
1146+
def max_secondaries(self) -> int:
1147+
return self._max_secondaries
1148+
1149+
@max_secondaries.setter
1150+
def max_secondaries(self, value: int):
1151+
cv.check_type('maximum secondary bank size', value, Integral)
1152+
cv.check_greater_than('max secondary bank size', value, 0)
1153+
self._max_secondaries = value
1154+
11401155
@property
11411156
def max_tracks(self) -> int:
11421157
return self._max_tracks
@@ -1673,6 +1688,11 @@ def _create_max_history_splits_subelement(self, root):
16731688
elem = ET.SubElement(root, "max_history_splits")
16741689
elem.text = str(self._max_history_splits)
16751690

1691+
def _create_max_secondaries_subelement(self, root):
1692+
if self._max_secondaries is not None:
1693+
elem = ET.SubElement(root, "max_secondaries")
1694+
elem.text = str(self._max_secondaries)
1695+
16761696
def _create_max_tracks_subelement(self, root):
16771697
if self._max_tracks is not None:
16781698
elem = ET.SubElement(root, "max_tracks")
@@ -2073,6 +2093,11 @@ def _max_history_splits_from_xml_element(self, root):
20732093
if text is not None:
20742094
self.max_history_splits = int(text)
20752095

2096+
def _max_secondaries_from_xml_element(self, root):
2097+
text = get_text(root, 'max_secondaries')
2098+
if text is not None:
2099+
self.max_secondaries = int(text)
2100+
20762101
def _max_tracks_from_xml_element(self, root):
20772102
text = get_text(root, 'max_tracks')
20782103
if text is not None:
@@ -2194,6 +2219,7 @@ def to_xml_element(self, mesh_memo=None):
21942219
self._create_weight_window_checkpoints_subelement(element)
21952220
self._create_max_history_splits_subelement(element)
21962221
self._create_max_tracks_subelement(element)
2222+
self._create_max_secondaries_subelement(element)
21972223
self._create_random_ray_subelement(element, mesh_memo)
21982224
self._create_use_decay_photons_subelement(element)
21992225
self._create_source_rejection_fraction_subelement(element)
@@ -2302,6 +2328,7 @@ def from_xml_element(cls, elem, meshes=None):
23022328
settings._weight_window_checkpoints_from_xml_element(elem)
23032329
settings._max_history_splits_from_xml_element(elem)
23042330
settings._max_tracks_from_xml_element(elem)
2331+
settings._max_secondaries_from_xml_element(elem)
23052332
settings._random_ray_from_xml_element(elem)
23062333
settings._use_decay_photons_from_xml_element(elem)
23072334
settings._source_rejection_fraction_from_xml_element(elem)

src/eigenvalue.cpp

Lines changed: 29 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -127,30 +127,8 @@ void synchronize_bank()
127127
"No fission sites banked on MPI rank " + std::to_string(mpi::rank));
128128
}
129129

130-
// Make sure all processors start at the same point for random sampling. Then
131-
// skip ahead in the sequence using the starting index in the 'global'
132-
// fission bank for each processor.
133-
134-
int64_t id = simulation::total_gen + overall_generation();
135-
uint64_t seed = init_seed(id, STREAM_TRACKING);
136-
advance_prn_seed(start, &seed);
137-
138-
// Determine how many fission sites we need to sample from the source bank
139-
// and the probability for selecting a site.
140-
141-
int64_t sites_needed;
142-
if (total < settings::n_particles) {
143-
sites_needed = settings::n_particles % total;
144-
} else {
145-
sites_needed = settings::n_particles;
146-
}
147-
double p_sample = static_cast<double>(sites_needed) / total;
148-
149130
simulation::time_bank_sample.start();
150131

151-
// ==========================================================================
152-
// SAMPLE N_PARTICLES FROM FISSION BANK AND PLACE IN TEMP_SITES
153-
154132
// Allocate temporary source bank -- we don't really know how many fission
155133
// sites were created, so overallocate by a factor of 3
156134
int64_t index_temp = 0;
@@ -165,33 +143,38 @@ void synchronize_bank()
165143
temp_delayed_groups, temp_lifetimes, 3 * simulation::work_per_rank);
166144
}
167145

168-
for (int64_t i = 0; i < simulation::fission_bank.size(); i++) {
169-
const auto& site = simulation::fission_bank[i];
146+
// ==========================================================================
147+
// SAMPLE N_PARTICLES FROM FISSION BANK AND PLACE IN TEMP_SITES
170148

171-
// If there are less than n_particles particles banked, automatically add
172-
// int(n_particles/total) sites to temp_sites. For example, if you need
173-
// 1000 and 300 were banked, this would add 3 source sites per banked site
174-
// and the remaining 100 would be randomly sampled.
175-
if (total < settings::n_particles) {
176-
for (int64_t j = 1; j <= settings::n_particles / total; ++j) {
177-
temp_sites[index_temp] = site;
178-
if (settings::ifp_on) {
179-
copy_ifp_data_from_fission_banks(
180-
i, temp_delayed_groups[index_temp], temp_lifetimes[index_temp]);
181-
}
182-
++index_temp;
183-
}
184-
}
149+
// We use Uniform Combing method to exactly get the targeted particle size
150+
// [https://doi.org/10.1080/00295639.2022.2091906]
185151

186-
// Randomly sample sites needed
187-
if (prn(&seed) < p_sample) {
188-
temp_sites[index_temp] = site;
189-
if (settings::ifp_on) {
190-
copy_ifp_data_from_fission_banks(
191-
i, temp_delayed_groups[index_temp], temp_lifetimes[index_temp]);
192-
}
193-
++index_temp;
152+
// Make sure all processors use the same random number seed.
153+
int64_t id = simulation::total_gen + overall_generation();
154+
uint64_t seed = init_seed(id, STREAM_TRACKING);
155+
156+
// Comb specification
157+
double teeth_distance = static_cast<double>(total) / settings::n_particles;
158+
double teeth_offset = prn(&seed) * teeth_distance;
159+
160+
// First and last hitting tooth
161+
int64_t end = start + simulation::fission_bank.size();
162+
int64_t tooth_start = std::ceil((start - teeth_offset) / teeth_distance);
163+
int64_t tooth_end = std::floor((end - teeth_offset) / teeth_distance) + 1;
164+
165+
// Locally comb particles in fission_bank
166+
double tooth = tooth_start * teeth_distance + teeth_offset;
167+
for (int64_t i = tooth_start; i < tooth_end; i++) {
168+
int64_t idx = std::floor(tooth) - start;
169+
temp_sites[index_temp] = simulation::fission_bank[idx];
170+
if (settings::ifp_on) {
171+
copy_ifp_data_from_fission_banks(
172+
idx, temp_delayed_groups[index_temp], temp_lifetimes[index_temp]);
194173
}
174+
++index_temp;
175+
176+
// Next tooth
177+
tooth += teeth_distance;
195178
}
196179

197180
// At this point, the sampling of source sites is done and now we need to
@@ -217,37 +200,6 @@ void synchronize_bank()
217200
finish = index_temp;
218201
#endif
219202

220-
// Now that the sampling is complete, we need to ensure that we have exactly
221-
// n_particles source sites. The way this is done in a reproducible manner is
222-
// to adjust only the source sites on the last processor.
223-
224-
if (mpi::rank == mpi::n_procs - 1) {
225-
if (finish > settings::n_particles) {
226-
// If we have extra sites sampled, we will simply discard the extra
227-
// ones on the last processor
228-
index_temp = settings::n_particles - start;
229-
230-
} else if (finish < settings::n_particles) {
231-
// If we have too few sites, repeat sites from the very end of the
232-
// fission bank
233-
sites_needed = settings::n_particles - finish;
234-
// TODO: sites_needed > simulation::fission_bank.size() or other test to
235-
// make sure we don't need info from other proc
236-
for (int i = 0; i < sites_needed; ++i) {
237-
int i_bank = simulation::fission_bank.size() - sites_needed + i;
238-
temp_sites[index_temp] = simulation::fission_bank[i_bank];
239-
if (settings::ifp_on) {
240-
copy_ifp_data_from_fission_banks(i_bank,
241-
temp_delayed_groups[index_temp], temp_lifetimes[index_temp]);
242-
}
243-
++index_temp;
244-
}
245-
}
246-
247-
// the last processor should not be sending sites to right
248-
finish = simulation::work_index[mpi::rank + 1];
249-
}
250-
251203
simulation::time_bank_sample.stop();
252204
simulation::time_bank_sendrecv.start();
253205

src/finalize.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ int openmc_finalize()
9292
settings::max_lost_particles = 10;
9393
settings::max_order = 0;
9494
settings::max_particles_in_flight = 100000;
95+
settings::max_secondaries = 10000;
9596
settings::max_particle_events = 1'000'000;
9697
settings::max_history_splits = 10'000'000;
9798
settings::max_tracks = 1000;

0 commit comments

Comments
 (0)