diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index c28db988b98..78101ab668d 100644 --- a/docs/source/io_formats/tallies.rst +++ b/docs/source/io_formats/tallies.rst @@ -100,6 +100,18 @@ The ```` 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. diff --git a/include/openmc/tallies/trigger.h b/include/openmc/tallies/trigger.h index 9fe159b9d9b..7feed5e8ad2 100644 --- a/include/openmc/tallies/trigger.h +++ b/include/openmc/tallies/trigger.h @@ -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 }; diff --git a/openmc/trigger.py b/openmc/trigger.py index c7d9e9240bb..79f5ea5af89 100644 --- a/openmc/trigger.py +++ b/openmc/trigger.py @@ -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 @@ def threshold(self, threshold): 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 @@ def to_xml_element(self): 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 @@ def from_xml_element(cls, elem: ET.Element): # 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") diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 2e77bcad25f..674987b8f13 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -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 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}); } } } diff --git a/src/tallies/trigger.cpp b/src/tallies/trigger.cpp index 87f298baa10..f1f83e2982c 100644 --- a/src/tallies/trigger.cpp +++ b/src/tallies/trigger.cpp @@ -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_; diff --git a/tests/unit_tests/test_triggers.py b/tests/unit_tests/test_triggers.py index 4fe6e044ab7..6b9e54eeb72 100644 --- a/tests/unit_tests/test_triggers.py +++ b/tests/unit_tests/test_triggers.py @@ -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 +