diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 784023f26ff..19221109bc7 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -515,6 +515,18 @@ class Integrator(ABC): :attr:`solver`. .. versionadded:: 0.12 + continue_timesteps : bool, optional + Whether or not to treat the current solve as a continuation of a + previous simulation. Defaults to `False`. If `True`, the timesteps + provided to the `Integrator` must match exactly those that exist + in the `prev_results` passed to the `Operator`. The `power`, + `power_density`, or `source_rates` must match as well. It + is the user's responsibility to make sure that the continue + solve uses the same method of specifying `power`, `power_density`, + or `source_rates`. + + .. versionadded:: 0.15.1 + Attributes ---------- operator : openmc.deplete.abc.TransportOperator @@ -556,7 +568,8 @@ def __init__( power_density: Optional[Union[float, Sequence[float]]] = None, source_rates: Optional[Sequence[float]] = None, timestep_units: str = 's', - solver: str = "cram48" + solver: str = "cram48", + continue_timesteps: bool = False, ): # Check number of stages previously used if operator.prev_res is not None: @@ -568,6 +581,9 @@ def __init__( "this uses {}".format( self.__class__.__name__, res.data.shape[0], self._num_stages)) + else: + # if the user specifies a continue run without a previous results, set the flag to False for them + continue_timesteps = False self.operator = operator self.chain = operator.chain @@ -629,6 +645,29 @@ def __init__( else: raise ValueError(f"Invalid timestep unit '{unit}'") + # validate existing depletion steps are consistent with those passed to operator + if continue_timesteps: + completed_times = operator.prev_res.get_times(time_units=timestep_units) + completed_timesteps = completed_times[1:] - completed_times[:-1] # convert absolute t to dt + completed_source_rates = operator.prev_res.get_source_rates() + num_previous_steps_run = len(completed_timesteps) + if (np.array_equal(completed_timesteps, timesteps[:num_previous_steps_run])): + seconds = seconds[num_previous_steps_run:] + else: + raise ValueError( + "You are attempting to continue a run in which the previous timesteps " + "do not have the same initial timesteps as those provided to the " + "Integrator. Please make sure you are using the correct timesteps." + ) + if(np.array_equal(completed_source_rates, np.asarray(source_rates)[:num_previous_steps_run] )): + source_rates = source_rates[num_previous_steps_run:] + else: + raise ValueError( + "You are attempting to continue a run in which the previous results " + "do not have the same initial source rates, powers, or power densities " + "as those provided to the Integrator. Please make sure you are using " + "the correct powers, power densities, or source rates and previous results file." + ) self.timesteps = np.asarray(seconds) self.source_rates = np.asarray(source_rates) @@ -919,6 +958,17 @@ class SIIntegrator(Integrator): :attr:`solver`. .. versionadded:: 0.12 + continue_timesteps : bool, optional + Whether or not to treat the current solve as a continuation of a + previous simulation. Defaults to `False`. If `True`, the timesteps + provided to the `Integrator` must match exactly those that exist + in the `prev_results` passed to the `Opereator`. The `power`, + `power_density`, or `source_rates` must match as well. It + is the user's responsibility to make sure that the continue + solve uses the same method of specifying `power`, `power_density`, + or `source_rates`. + + .. versionadded:: 0.15.1 Attributes ---------- @@ -960,13 +1010,14 @@ def __init__( source_rates: Optional[Sequence[float]] = None, timestep_units: str = 's', n_steps: int = 10, - solver: str = "cram48" + solver: str = "cram48", + continue_timesteps: bool = False, ): check_type("n_steps", n_steps, Integral) check_greater_than("n_steps", n_steps, 0) super().__init__( operator, timesteps, power, power_density, source_rates, - timestep_units=timestep_units, solver=solver) + timestep_units=timestep_units, solver=solver, continue_timesteps=continue_timesteps) self.n_steps = n_steps def _get_bos_data_from_operator(self, step_index, step_power, n_bos): diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index f897a88422c..b7ec8fb8c57 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -17,6 +17,10 @@ __all__ = ["Results", "ResultsList"] +_SECONDS_PER_MINUTE = 60 +_SECONDS_PER_HOUR = 60*60 +_SECONDS_PER_DAY = 24*60*60 +_SECONDS_PER_JULIAN_YEAR = 365.25*24*60*60 def _get_time_as(seconds: float, units: str) -> float: """Converts the time in seconds to time in different units @@ -31,13 +35,13 @@ def _get_time_as(seconds: float, units: str) -> float: """ if units == "a": - return seconds / (60 * 60 * 24 * 365.25) # 365.25 due to the leap year + return seconds / _SECONDS_PER_JULIAN_YEAR if units == "d": - return seconds / (60 * 60 * 24) + return seconds / _SECONDS_PER_DAY elif units == "h": - return seconds / (60 * 60) + return seconds / _SECONDS_PER_HOUR elif units == "min": - return seconds / 60 + return seconds / _SECONDS_PER_MINUTE else: return seconds @@ -71,7 +75,6 @@ def __init__(self, filename='depletion_results.h5'): data.append(StepResult.from_hdf5(fh, i)) super().__init__(data) - @classmethod def from_hdf5(cls, filename: PathLike): """Load in depletion results from a previous file @@ -460,6 +463,25 @@ def get_times(self, time_units: str = "d") -> np.ndarray: return _get_time_as(times, time_units) + def get_source_rates(self) -> np.ndarray: + """ + .. versionadded:: 0.15.1 + + Returns + ------- + numpy.ndarray + 1-D vector of source rates at each point in the depletion simulation + with the units originally defined by the user. + + """ + source_rates = np.fromiter( + (r.source_rate for r in self), + dtype=self[0].source_rate.dtype, + count=len(self)-1, + ) # Results duplicates the final source rate at the final simulation time + + return source_rates + def get_step_where( self, time, time_units: str = "d", atol: float = 1e-6, rtol: float = 1e-3 ) -> int: diff --git a/tests/unit_tests/test_deplete_continue.py b/tests/unit_tests/test_deplete_continue.py new file mode 100644 index 00000000000..b1f857a1c52 --- /dev/null +++ b/tests/unit_tests/test_deplete_continue.py @@ -0,0 +1,99 @@ +"""Unit tests for openmc.deplete continue run capability. + +These tests run in two steps: first a normal run and then a continue run based on the prev_results +""" + +import pytest + +import openmc.deplete + +from tests import dummy_operator + +# test that the continue timesteps works when the second integrate call contains all previous timesteps +@pytest.mark.parametrize("scheme", dummy_operator.SCHEMES) +def test_continue(run_in_tmpdir, scheme): + """Test to ensure that a properly defined continue run works""" + + # set up the problem + + bundle = dummy_operator.SCHEMES[scheme] + + operator = dummy_operator.DummyOperator() + + # initial depletion + bundle.solver(operator, [1.0, 2.0], [1.0, 2.0]).integrate() + + # set up continue run + prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + operator = dummy_operator.DummyOperator(prev_res) + + # if continue run happens, test passes + bundle.solver(operator, [1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0], continue_timesteps=True).integrate() + +@pytest.mark.parametrize("scheme", dummy_operator.SCHEMES) +def test_continue_for_null_previous(run_in_tmpdir, scheme): + """Test to ensure that a continue run works even if there are no previous results""" + # set up the problem + + bundle = dummy_operator.SCHEMES[scheme] + + operator = dummy_operator.DummyOperator() + + # initial depletion + bundle.solver(operator, [1.0, 2.0], [1.0, 2.0], continue_timesteps=True).integrate() + +@pytest.mark.parametrize("scheme", dummy_operator.SCHEMES) +def test_mismatched_initial_times(run_in_tmpdir, scheme): + """Test to ensure that a continue run with different initial steps is properly caught""" + + # set up the problem + + bundle = dummy_operator.SCHEMES[scheme] + + operator = dummy_operator.DummyOperator() + + # take first step + bundle.solver(operator, [0.75, 0.75], [1.0, 1.0]).integrate() + + # restart + prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + operator = dummy_operator.DummyOperator(prev_res) + + with pytest.raises( + ValueError, + match="You are attempting to continue a run in which the previous timesteps " + "do not have the same initial timesteps as those provided to the " + "Integrator. Please make sure you are using the correct timesteps.", + ): + bundle.solver( + operator, [0.75, 0.5, 0.75], [1.0, 1.0, 1.0], continue_timesteps=True + ).integrate() + + +@pytest.mark.parametrize("scheme", dummy_operator.SCHEMES) +def test_mismatched_initial_source_rates(run_in_tmpdir, scheme): + """Test to ensure that a continue run with different initial steps is properly caught""" + + # set up the problem + + bundle = dummy_operator.SCHEMES[scheme] + + operator = dummy_operator.DummyOperator() + + # take first step + bundle.solver(operator, [0.75, 0.75], [1.0, 1.0]).integrate() + + # restart + prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + operator = dummy_operator.DummyOperator(prev_res) + + with pytest.raises( + ValueError, + match="You are attempting to continue a run in which the previous results " + "do not have the same initial source rates, powers, or power densities " + "as those provided to the Integrator. Please make sure you are using " + "the correct powers, power densities, or source rates and previous results file.", + ): + bundle.solver( + operator, [0.75, 0.75, 0.75], [1.0, 2.0, 1.0], continue_timesteps=True + ).integrate()