Repository: openmc-dev/openmc

Issue:
Feature: Decay depletion steps
In discussing #1611, some additional items arose related to how `openmc.deplete` could support decay depletion steps, e.g. a step with zero reactor power. This is different enough from changes required for activation calculations (subject of #1611) that I felt it merited it's own issue for tracking / discussion. Summarizing comments from that PR

https://github.com/openmc-dev/openmc/issues/1611#issuecomment-659399904

> With respect to the decay mode, setting a normalization target of zero will result in the zero-valued reaction rates necessary to build a pure-decay depletion matrix. The downside is a full transport solution would still be performed at BOS to compute those unused reaction rates. Possible implementations for creating / emulating zero reaction rates w/o running a full transport solve
    1. Make a list of zero-valued reaction_rates to be passed to [`Chain.form_matrix`](https://docs.openmc.org/en/latest/pythonapi/generated/openmc.deplete.Chain.html#openmc.deplete.Chain.form_matrix) during the decay solver.
    2. Exploit some duck-typing and make a class that looks like an array of reaction rates but always returns zero upon indexing
    3. Add a dedicated `Chain.build_decay_matrix` method that doesn't accept reaction rates. Since this is only dependent on nuclear data, we could store it for later steps, assuming all materials track the same nuclides which I'm pretty sure they do. Then we don't have to re-build it at every step for all materials.

https://github.com/openmc-dev/openmc/issues/1611#issuecomment-659442438 

> I would add one other note which is that we always perform a transport calculation at the last time point, which is not desirable in an activation calculation.


Discussion:

Resolving patch:
diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py
--- a/openmc/deplete/abc.py
+++ b/openmc/deplete/abc.py
@@ -858,16 +858,29 @@ def _get_start_data(self):
         return (self.operator.prev_res[-1].time[-1],
                 len(self.operator.prev_res) - 1)
 
-    def integrate(self):
-        """Perform the entire depletion process across all steps"""
+    def integrate(self, final_step=True):
+        """Perform the entire depletion process across all steps
+
+        Parameters
+        ----------
+        final_step : bool, optional
+            Indicate whether or not a transport solve should be run at the end
+            of the last timestep.
+
+            .. versionadded:: 0.12.1
+
+        """
         with self.operator as conc:
             t, self._i_res = self._get_start_data()
 
             for i, (dt, p) in enumerate(self):
+                # Solve transport equation (or obtain result from restart)
                 if i > 0 or self.operator.prev_res is None:
                     conc, res = self._get_bos_data_from_operator(i, p, conc)
                 else:
                     conc, res = self._get_bos_data_from_restart(i, p, conc)
+
+                # Solve Bateman equations over time interval
                 proc_time, conc_list, res_list = self(conc, res.rates, dt, p, i)
 
                 # Insert BOS concentration, transport results
@@ -882,8 +895,11 @@ def integrate(self):
 
                 t += dt
 
-            # Final simulation
-            res_list = [self.operator(conc, p)]
+            # Final simulation -- in the case that final_step is False, a zero
+            # source rate is passed to the transport operator (which knows to
+            # just return zero reaction rates without actually doing a transport
+            # solve)
+            res_list = [self.operator(conc, p if final_step else 0.0)]
             Results.save(self.operator, [conc], res_list, [t, t],
                          p, self._i_res + len(self), proc_time)
             self.operator.write_bos_data(len(self) + self._i_res)
diff --git a/openmc/deplete/operator.py b/openmc/deplete/operator.py
--- a/openmc/deplete/operator.py
+++ b/openmc/deplete/operator.py
@@ -276,6 +276,16 @@ def __call__(self, vec, power):
             Eigenvalue and reaction rates resulting from transport operator
 
         """
+        # Reset results in OpenMC
+        openmc.lib.reset()
+
+        # If the source rate is zero, return zero reaction rates without running
+        # a transport solve
+        if power == 0.0:
+            rates = self.reaction_rates.copy()
+            rates.fill(0.0)
+            return OperatorResult(ufloat(0.0, 0.0), rates)
+
         # Prevent OpenMC from complaining about re-creating tallies
         openmc.reset_auto_ids()
 
@@ -290,7 +300,6 @@ def __call__(self, vec, power):
         self._yield_helper.update_tally_nuclides(nuclides)
 
         # Run OpenMC
-        openmc.lib.reset()
         openmc.lib.run()
         openmc.lib.reset_timers()
 


Test patch: