Allow zero bins in tally triggers (#2928)

Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
Travis L 2024-05-02 10:00:22 -04:00 committed by GitHub
parent d1366c0545
commit c976653abf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 90 additions and 7 deletions

View file

@ -100,6 +100,18 @@ The ``<tally>`` element accepts the following sub-elements:
*Default*: None
:ignore_zeros:
Whether to allow zero tally bins to be ignored when assessing the
convergece of the precision trigger. If True, only nonzero tally scores
will be compared to the trigger's threshold.
.. note:: The ``ignore_zeros`` option can cause the tally trigger to fire
prematurely if there are no hits in any bins at the first
evalulation. It is the user's responsibility to specify enough
particles per batch to get a nonzero score in at least one bin.
*Default*: False
:scores:
The score(s) in this tally to which the trigger should be applied.

View file

@ -23,6 +23,7 @@ enum class TriggerMetric {
struct Trigger {
TriggerMetric metric; //!< The type of uncertainty (e.g. std dev) measured
double threshold; //!< Uncertainty value below which trigger is satisfied
bool ignore_zeros; //!< Whether to allow zero tally bins to be ignored
int score_index; //!< Index of the relevant score in the tally's arrays
};

View file

@ -17,6 +17,12 @@ class Trigger(EqualityMixin):
relative error of scores.
threshold : float
The threshold for the trigger type.
ignore_zeros : bool
Whether to allow zero tally bins to be ignored. Note that this option
can cause the trigger to fire prematurely if there are zero scores in
any bin at the first evaluation.
.. versionadded:: 0.14.1
Attributes
----------
@ -27,18 +33,22 @@ class Trigger(EqualityMixin):
The threshold for the trigger type.
scores : list of str
Scores which should be checked against the trigger
ignore_zeros : bool
Whether to allow zero tally bins to be ignored.
"""
def __init__(self, trigger_type: str, threshold: float):
def __init__(self, trigger_type: str, threshold: float, ignore_zeros: bool = False):
self.trigger_type = trigger_type
self.threshold = threshold
self.ignore_zeros = ignore_zeros
self._scores = []
def __repr__(self):
string = 'Trigger\n'
string += '{: <16}=\t{}\n'.format('\tType', self._trigger_type)
string += '{: <16}=\t{}\n'.format('\tThreshold', self._threshold)
string += '{: <16}=\t{}\n'.format('\tIgnore Zeros', self._ignore_zeros)
string += '{: <16}=\t{}\n'.format('\tScores', self._scores)
return string
@ -61,6 +71,15 @@ class Trigger(EqualityMixin):
cv.check_type('tally trigger threshold', threshold, Real)
self._threshold = threshold
@property
def ignore_zeros(self):
return self._ignore_zeros
@ignore_zeros.setter
def ignore_zeros(self, ignore_zeros):
cv.check_type('tally trigger ignores zeros', ignore_zeros, bool)
self._ignore_zeros = ignore_zeros
@property
def scores(self):
return self._scores
@ -88,6 +107,8 @@ class Trigger(EqualityMixin):
element = ET.Element("trigger")
element.set("type", self._trigger_type)
element.set("threshold", str(self._threshold))
if self._ignore_zeros:
element.set("ignore_zeros", "true")
if len(self._scores) != 0:
element.set("scores", ' '.join(self._scores))
return element
@ -110,7 +131,10 @@ class Trigger(EqualityMixin):
# Generate trigger object
trigger_type = elem.get("type")
threshold = float(elem.get("threshold"))
trigger = cls(trigger_type, threshold)
ignore_zeros = str(elem.get("ignore_zeros", "false")).lower()
# Try to convert to bool. Let Trigger error out on instantiation.
ignore_zeros = ignore_zeros in ('true', '1')
trigger = cls(trigger_type, threshold, ignore_zeros)
# Add scores if present
scores = elem.get("scores")

View file

@ -690,6 +690,12 @@ void Tally::init_triggers(pugi::xml_node node)
"Must specify trigger threshold for tally {} in tally XML file", id_));
}
// Read whether to allow zero-tally bins to be ignored.
bool ignore_zeros = false;
if (check_for_node(trigger_node, "ignore_zeros")) {
ignore_zeros = get_node_value_bool(trigger_node, "ignore_zeros");
}
// Read the trigger scores.
vector<std::string> trigger_scores;
if (check_for_node(trigger_node, "scores")) {
@ -703,7 +709,7 @@ void Tally::init_triggers(pugi::xml_node node)
if (score_str == "all") {
triggers_.reserve(triggers_.size() + this->scores_.size());
for (auto i_score = 0; i_score < this->scores_.size(); ++i_score) {
triggers_.push_back({metric, threshold, i_score});
triggers_.push_back({metric, threshold, ignore_zeros, i_score});
}
} else {
int i_score = 0;
@ -717,7 +723,7 @@ void Tally::init_triggers(pugi::xml_node node)
"{} but it was listed in a trigger on that tally",
score_str, id_));
}
triggers_.push_back({metric, threshold, i_score});
triggers_.push_back({metric, threshold, ignore_zeros, i_score});
}
}
}

View file

@ -77,9 +77,9 @@ void check_tally_triggers(double& ratio, int& tally_id, int& score)
auto uncert_pair =
get_tally_uncertainty(i_tally, trigger.score_index, filter_index);
// if there is a score without contributions, set ratio to inf and
// exit early
if (uncert_pair.first == -1) {
// If there is a score without contributions, set ratio to inf and
// exit early, unless zero scores are ignored for this trigger.
if (uncert_pair.first == -1 && !trigger.ignore_zeros) {
ratio = INFINITY;
score = t.scores_[trigger.score_index];
tally_id = t.id_;

View file

@ -73,3 +73,43 @@ def test_tally_trigger_null_score(run_in_tmpdir):
total_batches = sp.n_realizations + sp.n_inactive
assert total_batches == pincell.settings.trigger_max_batches
def test_tally_trigger_zero_ignored(run_in_tmpdir):
pincell = openmc.examples.pwr_pin_cell()
# create an energy filter below and around the O-16(n,p) threshold (1.02e7 eV)
e_filter = openmc.EnergyFilter([0.0, 1e7, 2e7])
# create a tally with triggers applied
tally = openmc.Tally()
tally.filters = [e_filter]
tally.scores = ['(n,p)']
tally.nuclides = ["O16"]
# 100% relative error: should be immediately satisfied in nonzero bin
trigger = openmc.Trigger('rel_err', 1.0)
trigger.scores = ['(n,p)']
trigger.ignore_zeros = True
tally.triggers = [trigger]
pincell.tallies = [tally]
pincell.settings.particles = 1000 # we need a few more particles for this
pincell.settings.trigger_active = True
pincell.settings.trigger_max_batches = 50
pincell.settings.trigger_batch_interval = 20
sp_file = pincell.run()
with openmc.StatePoint(sp_file) as sp:
# verify that the first bin is zero and the second is nonzero
tally_out = sp.get_tally(id=tally.id)
below, above = tally_out.mean.squeeze()
assert below == 0.0, "Tally events observed below expected threshold"
assert above > 0, "No tally events observed. Test with more particles."
# we expect that the trigger fires before max batches are hit
total_batches = sp.n_realizations + sp.n_inactive
assert total_batches < pincell.settings.trigger_max_batches