Skip to content

Commit f571be8

Browse files
authored
Add user setting for source rejection fraction (#3433)
1 parent f796fa0 commit f571be8

10 files changed

Lines changed: 118 additions & 50 deletions

File tree

‎docs/source/io_formats/settings.rst‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ found in the :ref:`random ray user guide <random_ray>`.
487487

488488
:type:
489489
The type of the domain. Can be ``material``, ``cell``, or ``universe``.
490-
490+
491491
:diagonal_stabilization_rho:
492492
The rho factor for use with diagonal stabilization. This technique is
493493
applied when negative diagonal (in-group) elements are detected in
@@ -917,6 +917,16 @@ variable and whose sub-elements/attributes are as follows:
917917
:dist:
918918
This sub-element of a ``pair`` element provides information on the corresponding univariate distribution.
919919

920+
---------------------------------------
921+
``<source_rejection_fraction>`` Element
922+
---------------------------------------
923+
924+
The ``<source_rejection_fraction>`` element specifies the minimum fraction of
925+
external source sites that must be accepted when applying rejection sampling
926+
based on constraints.
927+
928+
*Default*: 0.05
929+
920930
-------------------------
921931
``<state_point>`` Element
922932
-------------------------

‎include/openmc/settings.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ extern std::unordered_set<int>
145145
statepoint_batch; //!< Batches when state should be written
146146
extern std::unordered_set<int>
147147
source_write_surf_id; //!< Surface ids where sources will be written
148+
extern double source_rejection_fraction; //!< Minimum fraction of source sites
149+
//!< that must be accepted
148150

149151
extern int
150152
max_history_splits; //!< maximum number of particle splits for weight windows

‎include/openmc/source.h‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ namespace openmc {
2121
// Constants
2222
//==============================================================================
2323

24-
// Maximum number of external source spatial resamples to encounter before an
25-
// error is thrown.
24+
// Minimum number of external source sites rejected before checking againts the
25+
// source_rejection_fraction
2626
constexpr int EXTSRC_REJECT_THRESHOLD {10000};
27-
constexpr double EXTSRC_REJECT_FRACTION {0.05};
2827

2928
//==============================================================================
3029
// Global variables

‎openmc/settings.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,10 @@ class Settings:
222222
Number of random numbers allocated for each source particle history
223223
source : Iterable of openmc.SourceBase
224224
Distribution of source sites in space, angle, and energy
225+
source_rejection_fraction : float
226+
Minimum fraction of source sites that must be accepted when applying
227+
rejection sampling based on constraints. If not specified, the default
228+
value is 0.05.
225229
sourcepoint : dict
226230
Options for writing source points. Acceptable keys are:
227231
@@ -357,6 +361,7 @@ def __init__(self, **kwargs):
357361

358362
# Source subelement
359363
self._source = cv.CheckedList(SourceBase, 'source distributions')
364+
self._source_rejection_fraction = None
360365

361366
self._confidence_intervals = None
362367
self._electron_treatment = None
@@ -1224,6 +1229,17 @@ def use_decay_photons(self, value):
12241229
cv.check_type('use decay photons', value, bool)
12251230
self._use_decay_photons = value
12261231

1232+
@property
1233+
def source_rejection_fraction(self) -> float:
1234+
return self._source_rejection_fraction
1235+
1236+
@source_rejection_fraction.setter
1237+
def source_rejection_fraction(self, source_rejection_fraction: float):
1238+
cv.check_type('source_rejection_fraction', source_rejection_fraction, Real)
1239+
cv.check_greater_than('source_rejection_fraction', source_rejection_fraction, 0)
1240+
cv.check_less_than('source_rejection_fraction', source_rejection_fraction, 1)
1241+
self._source_rejection_fraction = source_rejection_fraction
1242+
12271243
def _create_run_mode_subelement(self, root):
12281244
elem = ET.SubElement(root, "run_mode")
12291245
elem.text = self._run_mode.value
@@ -1685,6 +1701,11 @@ def _create_random_ray_subelement(self, root, mesh_memo=None):
16851701
subelement = ET.SubElement(element, key)
16861702
subelement.text = str(value)
16871703

1704+
def _create_source_rejection_fraction_subelement(self, root):
1705+
if self._source_rejection_fraction is not None:
1706+
element = ET.SubElement(root, "source_rejection_fraction")
1707+
element.text = str(self._source_rejection_fraction)
1708+
16881709
def _eigenvalue_from_xml_element(self, root):
16891710
elem = root.find('eigenvalue')
16901711
if elem is not None:
@@ -2104,6 +2125,11 @@ def _use_decay_photons_from_xml_element(self, root):
21042125
if text is not None:
21052126
self.use_decay_photons = text in ('true', '1')
21062127

2128+
def _source_rejection_fraction_from_xml_element(self, root):
2129+
text = get_text(root, 'source_rejection_fraction')
2130+
if text is not None:
2131+
self.source_rejection_fraction = float(text)
2132+
21072133
def to_xml_element(self, mesh_memo=None):
21082134
"""Create a 'settings' element to be written to an XML file.
21092135
@@ -2171,6 +2197,7 @@ def to_xml_element(self, mesh_memo=None):
21712197
self._create_max_tracks_subelement(element)
21722198
self._create_random_ray_subelement(element, mesh_memo)
21732199
self._create_use_decay_photons_subelement(element)
2200+
self._create_source_rejection_fraction_subelement(element)
21742201

21752202
# Clean the indentation in the file to be user-readable
21762203
clean_indentation(element)
@@ -2279,6 +2306,7 @@ def from_xml_element(cls, elem, meshes=None):
22792306
settings._max_tracks_from_xml_element(elem)
22802307
settings._random_ray_from_xml_element(elem)
22812308
settings._use_decay_photons_from_xml_element(elem)
2309+
settings._source_rejection_fraction_from_xml_element(elem)
22822310

22832311
return settings
22842312

‎src/error.cpp‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -110,23 +110,26 @@ void write_message(const std::string& message, int level)
110110

111111
void fatal_error(const std::string& message, int err)
112112
{
113+
#pragma omp critical(FatalError)
114+
{
113115
#ifdef _POSIX_VERSION
114-
// Make output red if user is in a terminal
115-
if (isatty(STDERR_FILENO)) {
116-
std::cerr << "\033[0;31m";
117-
}
116+
// Make output red if user is in a terminal
117+
if (isatty(STDERR_FILENO)) {
118+
std::cerr << "\033[0;31m";
119+
}
118120
#endif
119121

120-
// Write error message
121-
std::cerr << " ERROR: ";
122-
output(message, std::cerr, 8);
122+
// Write error message
123+
std::cerr << " ERROR: ";
124+
output(message, std::cerr, 8);
123125

124126
#ifdef _POSIX_VERSION
125-
// Reset color for terminal
126-
if (isatty(STDERR_FILENO)) {
127-
std::cerr << "\033[0m";
128-
}
127+
// Reset color for terminal
128+
if (isatty(STDERR_FILENO)) {
129+
std::cerr << "\033[0m";
130+
}
129131
#endif
132+
}
130133

131134
#ifdef OPENMC_MPI
132135
MPI_Abort(mpi::intracomm, err);

‎src/finalize.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ int openmc_finalize()
119119
settings::run_CE = true;
120120
settings::run_mode = RunMode::UNSET;
121121
settings::source_latest = false;
122+
settings::source_rejection_fraction = 0.05;
122123
settings::source_separate = false;
123124
settings::source_write = true;
124125
settings::ssw_cell_id = C_NONE;

‎src/settings.cpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ RunMode run_mode {RunMode::UNSET};
124124
SolverType solver_type {SolverType::MONTE_CARLO};
125125
std::unordered_set<int> sourcepoint_batch;
126126
std::unordered_set<int> statepoint_batch;
127+
double source_rejection_fraction {0.05};
127128
std::unordered_set<int> source_write_surf_id;
128129
int64_t ssw_max_particles;
129130
int64_t ssw_max_files;
@@ -644,6 +645,12 @@ void read_settings_xml(pugi::xml_node root)
644645
write_initial_source = get_node_value_bool(root, "write_initial_source");
645646
}
646647

648+
// Get relative number of lost particles
649+
if (check_for_node(root, "source_rejection_fraction")) {
650+
source_rejection_fraction =
651+
std::stod(get_node_value(root, "source_rejection_fraction"));
652+
}
653+
647654
// Survival biasing
648655
if (check_for_node(root, "survival_biasing")) {
649656
survival_biasing = get_node_value_bool(root, "survival_biasing");

‎src/source.cpp‎

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,28 @@ void Source::read_constraints(pugi::xml_node node)
157157
}
158158
}
159159

160+
void check_rejection_fraction(int64_t n_reject, int64_t n_accept)
161+
{
162+
// Don't check unless we've hit a minimum number of total sites rejected
163+
if (n_reject < EXTSRC_REJECT_THRESHOLD)
164+
return;
165+
166+
// Compute fraction of accepted sites and compare against minimum
167+
double fraction = static_cast<double>(n_accept) / n_reject;
168+
if (fraction <= settings::source_rejection_fraction) {
169+
fatal_error(fmt::format(
170+
"Too few source sites satisfied the constraints (minimum source "
171+
"rejection fraction = {}). Please check your source definition or "
172+
"set a lower value of Settings.source_rejection_fraction.",
173+
settings::source_rejection_fraction));
174+
}
175+
}
176+
160177
SourceSite Source::sample_with_constraints(uint64_t* seed) const
161178
{
162179
bool accepted = false;
163-
static int n_reject = 0;
164-
static int n_accept = 0;
180+
static int64_t n_reject = 0;
181+
static int64_t n_accept = 0;
165182
SourceSite site;
166183

167184
while (!accepted) {
@@ -176,13 +193,9 @@ SourceSite Source::sample_with_constraints(uint64_t* seed) const
176193
satisfies_energy_constraints(site.E) &&
177194
satisfies_time_constraints(site.time);
178195
if (!accepted) {
196+
// Increment number of rejections and check against minimum fraction
179197
++n_reject;
180-
if (n_reject >= EXTSRC_REJECT_THRESHOLD &&
181-
static_cast<double>(n_accept) / n_reject <=
182-
EXTSRC_REJECT_FRACTION) {
183-
fatal_error("More than 95% of external source sites sampled were "
184-
"rejected. Please check your source definition.");
185-
}
198+
check_rejection_fraction(n_reject, n_accept);
186199

187200
// For the "kill" strategy, accept particle but set weight to 0 so that
188201
// it is terminated immediately
@@ -337,8 +350,8 @@ SourceSite IndependentSource::sample(uint64_t* seed) const
337350

338351
// Repeat sampling source location until a good site has been accepted
339352
bool accepted = false;
340-
static int n_reject = 0;
341-
static int n_accept = 0;
353+
static int64_t n_reject = 0;
354+
static int64_t n_accept = 0;
342355

343356
while (!accepted) {
344357

@@ -351,12 +364,7 @@ SourceSite IndependentSource::sample(uint64_t* seed) const
351364
// Check for rejection
352365
if (!accepted) {
353366
++n_reject;
354-
if (n_reject >= EXTSRC_REJECT_THRESHOLD &&
355-
static_cast<double>(n_accept) / n_reject <= EXTSRC_REJECT_FRACTION) {
356-
fatal_error("More than 95% of external source sites sampled were "
357-
"rejected. Please check your external source's spatial "
358-
"definition.");
359-
}
367+
check_rejection_fraction(n_reject, n_accept);
360368
}
361369
}
362370

@@ -381,18 +389,12 @@ SourceSite IndependentSource::sample(uint64_t* seed) const
381389
site.E = energy_->sample(seed);
382390

383391
// Resample if energy falls above maximum particle energy
384-
if (site.E < data::energy_max[p] and
392+
if (site.E < data::energy_max[p] &&
385393
(satisfies_energy_constraints(site.E)))
386394
break;
387395

388396
n_reject++;
389-
if (n_reject >= EXTSRC_REJECT_THRESHOLD &&
390-
static_cast<double>(n_accept) / n_reject <= EXTSRC_REJECT_FRACTION) {
391-
fatal_error(
392-
"More than 95% of external source sites sampled were "
393-
"rejected. Please check your external source energy spectrum "
394-
"definition.");
395-
}
397+
check_rejection_fraction(n_reject, n_accept);
396398
}
397399

398400
// Sample particle creation time

‎tests/unit_tests/test_settings.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ def test_export_to_xml(run_in_tmpdir):
6666
space=openmc.stats.Box((-1., -1., -1.), (1., 1., 1.))
6767
)
6868
}
69-
7069
s.max_particle_events = 100
70+
s.source_rejection_fraction = 0.01
7171

7272
# Make sure exporting XML works
7373
s.export_to_xml()
@@ -130,7 +130,7 @@ def test_export_to_xml(run_in_tmpdir):
130130
assert s.log_grid_bins == 2000
131131
assert not s.photon_transport
132132
assert s.electron_treatment == 'led'
133-
assert s.write_initial_source == True
133+
assert s.write_initial_source
134134
assert len(s.volume_calculations) == 1
135135
vol = s.volume_calculations[0]
136136
assert vol.domain_type == 'cell'
@@ -144,3 +144,4 @@ def test_export_to_xml(run_in_tmpdir):
144144
assert s.random_ray['distance_active'] == 100.0
145145
assert s.random_ray['ray_source'].space.lower_left == [-1., -1., -1.]
146146
assert s.random_ray['ray_source'].space.upper_right == [1., 1., 1.]
147+
assert s.source_rejection_fraction == 0.01

‎tests/unit_tests/test_source.py‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -264,21 +264,36 @@ def test_constraints_file(sphere_box_model, run_in_tmpdir):
264264

265265

266266
@pytest.mark.skipif(config['mpi'], reason='Not compatible with MPI')
267-
def test_rejection_limit(sphere_box_model, run_in_tmpdir):
268-
model, cell1 = sphere_box_model[:2]
267+
def test_rejection_fraction(run_in_tmpdir):
268+
mat = openmc.Material()
269+
mat.add_nuclide('H1', 1.0)
270+
w = 0.25
271+
rpp1 = openmc.model.RectangularParallelepiped(
272+
-w/2, w/2, -w/2, w/2, -w/2, w/2)
273+
rpp2 = openmc.model.RectangularParallelepiped(
274+
-0.5, 0.5, -0.5, 0.5, -0.5, 0.5, boundary_type='vacuum')
275+
cell1 = openmc.Cell(fill=mat, region=-rpp1)
276+
cell2 = openmc.Cell(region=+rpp1 & -rpp2)
277+
model = openmc.Model()
278+
model.geometry = openmc.Geometry([cell1, cell2])
269279

270-
# Define a point source that will get rejected 100% of the time
280+
# Create a box source over a 1 cm³ volume that is constrained to the source
281+
# cell of volume (0.25 cm)³ = 0.0125 cm³, which means the default rejection
282+
# fraction of 0.05 won't work
283+
model.settings.particles = 1000
284+
model.settings.batches = 1
285+
model.settings.run_mode = 'fixed source'
271286
model.settings.source = openmc.IndependentSource(
272-
space=openmc.stats.Point((-3., 0., 0.)),
287+
space=openmc.stats.Box(*(-rpp2).bounding_box),
273288
constraints={'domains': [cell1]}
274289
)
275-
276-
# Confirm that OpenMC doesn't run in an infinite loop. Note that this may
277-
# work when running with MPI since it won't necessarily capture the error
278-
# message correctly
279-
with pytest.raises(RuntimeError, match="rejected"):
290+
with pytest.raises(RuntimeError, match='Too few source sites'):
280291
model.run(openmc_exec=config['exe'])
281292

293+
# With a source rejection fraction below 0.0125, the simulation should run
294+
model.settings.source_rejection_fraction = 0.005
295+
model.run(openmc_exec=config['exe'])
296+
282297

283298
def test_exceptions():
284299

0 commit comments

Comments
 (0)