Repository: openmc-dev/openmc

Issue:
Tallies ignored or not output in depletion calculations
<!--
If you are a user of OpenMC and are running into trouble with the code or are
seeking general user support, we highly recommend posting on the OpenMC
discourse forum first. GitHub issues should be used specifically for bug reports
and feature requests.

https://openmc.discourse.group/

-->

## Bug Description
The expected behavior of OpenMC when run via the `CoupledOperator` for depletion is to output the tallies from the beginning of each time step in files named `openmc_simulation_n{i}.h5` where i is the time step. However, NO tallies are ever present.


## Steps to Reproduce
<!--Steps to reproduce the behavior (input file, or modifications to an existing input file, etc.)-->
Go to the classic pincell_depletion problem, and modify the steps leading to the time integration to read as:

```
tallies = openmc.Tallies()
abs_235 = openmc.Tally()
abs_235.name = 'U235 absorption'
abs_235.nuclides = ['U235']
abs_235.scores = ['absorption']
tallies.append(abs_235)

model = openmc.Model(geometry=geometry, settings=settings, tallies=tallies)

```

You're in for a big disappointment if you want to check the absorption fraction into U235 at timestep 2 though, because:

```
In [1]: import openmc

In [2]: sp = openmc.StatePoint('./openmc_simulation_n2.h5')

In [3]: sp.tallies
Out[3]: {}
```

😢 

## Environment
<!--Operating system, OpenMC version, how OpenMC was installed, nuclear data being used, etc.-->
Arch linux, latest commit.


Discussion:

Resolving patch:
diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py
--- a/openmc/deplete/coupled_operator.py
+++ b/openmc/deplete/coupled_operator.py
@@ -178,10 +178,6 @@ class CoupledOperator(OpenMCOperator):
     ----------
     model : openmc.model.Model
         OpenMC model object
-    geometry : openmc.Geometry
-        OpenMC geometry object
-    settings : openmc.Settings
-        OpenMC settings object
     output_dir : pathlib.Path
         Path to output directory to save results.
     round_number : bool
@@ -242,8 +238,6 @@ def __init__(self, model, chain_file=None, prev_results=None,
                 warn("Fission Q dictionary will not be used")
                 fission_q = None
         self.model = model
-        self.settings = model.settings
-        self.geometry = model.geometry
 
         # determine set of materials in the model
         if not model.materials:
@@ -358,7 +352,7 @@ def _get_helper_classes(self, helper_kwargs):
         if normalization_mode == "fission-q":
             self._normalization_helper = ChainFissionHelper()
         elif normalization_mode == "energy-deposition":
-            score = "heating" if self.settings.photon_transport else "heating-local"
+            score = "heating" if self.model.settings.photon_transport else "heating-local"
             self._normalization_helper = EnergyScoreHelper(score)
         else:
             self._normalization_helper = SourceRateHelper()
@@ -380,8 +374,12 @@ def initial_condition(self):
 
         # Create XML files
         if comm.rank == 0:
-            self.geometry.export_to_xml()
-            self.settings.export_to_xml()
+            self.model.geometry.export_to_xml()
+            self.model.settings.export_to_xml()
+            if self.model.plots:
+                self.model.plots.export_to_xml()
+            if self.model.tallies:
+                self.model.tallies.export_to_xml()
             self._generate_materials_xml()
 
         # Initialize OpenMC library
@@ -528,6 +526,36 @@ def finalize(self):
         if self.cleanup_when_done:
             openmc.lib.finalize()
 
+    # The next few class variables and methods should be removed after one
+    # release cycle or so. For now, we will provide compatibility to
+    # accessing CoupledOperator.settings and CoupledOperator.geometry. In
+    # the future these should stay on the Model class.
+
+    var_warning_msg = "The CoupledOperator.{0} variable should be \
+accessed through CoupledOperator.model.{0}."
+    geometry_warning_msg = var_warning_msg.format("geometry")
+    settings_warning_msg = var_warning_msg.format("settings")
+
+    @property
+    def settings(self):
+        warn(self.settings_warning_msg, FutureWarning)
+        return self.model.settings
+
+    @settings.setter
+    def settings(self, new_settings):
+        warn(self.settings_warning_msg, FutureWarning)
+        self.model.settings = new_settings
+
+    @property
+    def geometry(self):
+        warn(self.geometry_warning_msg, FutureWarning)
+        return self.model.geometry
+
+    @geometry.setter
+    def geometry(self, new_geometry):
+        warn(self.geometry_warning_msg, FutureWarning)
+        self.model.geometry = new_geometry
+
 
 # Retain deprecated name for the time being
 def Operator(*args, **kwargs):


Test patch:
diff --git a/tests/unit_tests/test_deplete_coupled_operator.py b/tests/unit_tests/test_deplete_coupled_operator.py
--- a/tests/unit_tests/test_deplete_coupled_operator.py
+++ b/tests/unit_tests/test_deplete_coupled_operator.py
@@ -38,7 +38,8 @@ def model():
 
     pin_surfaces = [openmc.ZCylinder(r=r) for r in radii]
     pin_univ = openmc.model.pin(pin_surfaces, materials)
-    bound_box = openmc.model.RectangularPrism(1.24, 1.24, boundary_type="reflective")
+    bound_box = openmc.model.RectangularPrism(
+        1.24, 1.24, boundary_type="reflective")
     root_cell = openmc.Cell(fill=pin_univ, region=-bound_box)
     geometry = openmc.Geometry([root_cell])
 
@@ -95,7 +96,7 @@ def test_diff_volume_method_match_cell(model_with_volumes):
         chain_file=CHAIN_PATH
     )
 
-    all_cells = list(operator.geometry.get_all_cells().values())
+    all_cells = list(operator.model.geometry.get_all_cells().values())
     assert all_cells[0].fill.volume == 4.19
     assert all_cells[1].fill.volume == 33.51
     # mat2 is not depletable
@@ -112,9 +113,8 @@ def test_diff_volume_method_divide_equally(model_with_volumes):
         chain_file=CHAIN_PATH
     )
 
-    all_cells = list(operator.geometry.get_all_cells().values())
+    all_cells = list(operator.model.geometry.get_all_cells().values())
     assert all_cells[0].fill[0].volume == 51
     assert all_cells[1].fill[0].volume == 51
     # mat2 is not depletable
     assert all_cells[2].fill.volume is None
-
diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py
--- a/tests/unit_tests/test_model.py
+++ b/tests/unit_tests/test_model.py
@@ -457,6 +457,20 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm):
     assert after_xe + after_u == pytest.approx(initial_u, abs=1e-15)
     assert test_model.is_initialized is False
 
+    # check the tally output
+    def check_tally_output():
+        with openmc.StatePoint('openmc_simulation_n0.h5') as sp:
+            flux = sp.get_tally(id=1).get_values(scores=['flux'])[0, 0, 0]
+            fission = sp.get_tally(id=1).get_values(
+                scores=['fission'])[0, 0, 0]
+
+            # we're mainly just checking that the result was produced,
+            # so a rough numerical comparison doesn't hurt to have.
+            assert flux == pytest.approx(13.1, abs=0.2)
+            assert fission == pytest.approx(0.47, abs=0.2)
+
+    check_tally_output()
+
     # Reset the initial material densities
     mats[0].nuclides.clear()
     densities = initial_mat.get_nuclide_atom_densities()
@@ -481,6 +495,8 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm):
     assert after_xe == pytest.approx(after_lib_xe, abs=1e-15)
     assert after_u == pytest.approx(after_lib_u, abs=1e-15)
 
+    check_tally_output()
+
     test_model.finalize_lib()
 
 
@@ -531,6 +547,7 @@ def test_calc_volumes(run_in_tmpdir, pin_model_attributes, mpi_intracomm):
 
     test_model.finalize_lib()
 
+
 def test_model_xml(run_in_tmpdir):
 
     # load a model from examples
@@ -549,6 +566,7 @@ def test_model_xml(run_in_tmpdir):
     # XML files
     new_model.export_to_xml()
 
+
 def test_single_xml_exec(run_in_tmpdir):
 
     pincell_model = openmc.examples.pwr_pin_cell()