Skip to content

Commit 84cabf6

Browse files
authored
Runtime signals (#161)
* wip implementation * full implementation Process runtime methods may also return signals. * fix/update some tests * black * unrelated doc change * add api docs * fix handling CONTINUE signal * add tests * black * update release notes * update doc (general concepts) and docstrings * doc: add user guide subsection * typo
1 parent 12e5952 commit 84cabf6

File tree

13 files changed

+395
-70
lines changed

13 files changed

+395
-70
lines changed

doc/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,4 @@ Model runtime monitoring
189189
monitoring.ProgressBar
190190
runtime_hook
191191
RuntimeHook
192+
RuntimeSignal

doc/framework.rst

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -224,18 +224,26 @@ A model run is divided into four successive stages:
224224
3. finalize step
225225
4. finalization
226226

227-
During a simulation, stages 1 and 4 are run only once while stages 2
228-
and 3 are repeated for a given number of (time) steps. Stage 4 is run even if
229-
an exception is raised during stage 1, 2 or 3.
230-
231-
Each process-ified class may provide its own computation instructions
232-
by implementing specific methods named ``.initialize()``,
233-
``.run_step()``, ``.finalize_step()`` and ``.finalize()`` for each
234-
stage above, respectively. Note that this is entirely optional. For
235-
example, time-independent processes (e.g., for setting model grids)
236-
usually implement stage 1 only. In a few cases, the role of a process
237-
may even consist of just declaring some variables that are used
238-
elsewhere.
227+
During a simulation, stages 1 and 4 are run only once while stages 2 and 3 are
228+
repeated for a given number of (time) steps. Stage 4 is always run even when an
229+
exception is raised during stage 1, 2 or 3.
230+
231+
Each :func:`~xsimlab.process` decorated class may provide its own computation
232+
instructions by implementing specific "runtime" methods named ``.initialize()``,
233+
``.run_step()``, ``.finalize_step()`` and ``.finalize()`` for each stage above,
234+
respectively. Note that this is entirely optional. For example, time-independent
235+
processes (e.g., for setting model grids) usually implement stage 1 only. In a
236+
few cases, the role of a process may even consist of just declaring some
237+
variables that are used elsewhere.
238+
239+
Runtime methods may be decorated by :func:`~xsimlab.runtime`. This is useful if
240+
one needs to access the value of some runtime-specific variables like the
241+
current step, time step duration, etc. from within those methods. Runtime
242+
methods may also return a :func:`~xsimlab.RuntimeSignal` to control the
243+
workflow, e.g., break the execution of the current stage.
244+
245+
It is also possible to monitor and/or control simulations independently of any
246+
model, using runtime hooks. See Section :ref:`monitor`.
239247

240248
Get / set variable values inside a process
241249
------------------------------------------

doc/monitor.rst

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,24 @@ it exemplifies how to create your own custom monitoring.
1616
sys.path.append('scripts')
1717
from advection_model import advect_model, advect_model_src
1818
19-
The following imports are necessary for the examples below.
19+
Let's use the following setup for the examples below. It is based on the
20+
``advect_model`` created in Section :ref:`create_model`.
2021

2122
.. ipython:: python
2223
2324
import xsimlab as xs
2425
25-
.. ipython:: python
26-
:suppress:
27-
2826
in_ds = xs.create_setup(
2927
model=advect_model,
3028
clocks={
31-
'time': np.linspace(0., 1., 5),
29+
'time': np.linspace(0., 1., 6),
3230
},
3331
input_vars={
3432
'grid': {'length': 1.5, 'spacing': 0.01},
3533
'init': {'loc': 0.3, 'scale': 0.1},
3634
'advect__v': 1.
3735
},
36+
output_vars={'profile__u': 'time'}
3837
)
3938
4039
@@ -172,3 +171,41 @@ methods that may share some state:
172171
173172
with PrintStepTime():
174173
in_ds.xsimlab.run(model=advect_model)
174+
175+
176+
Control simulation runtime
177+
--------------------------
178+
179+
Runtime hook functions may return a :class:`~xsimlab.RuntimeSignal` so that you
180+
can control the simulation workflow (e.g., skip the current stage or process,
181+
break the simulation time steps) based on some condition or some computed value.
182+
183+
In the example below, the simulation stops as soon as the gaussian pulse (peak
184+
value) has been advected past ``x = 0.4``.
185+
186+
.. ipython::
187+
188+
In [2]: @xs.runtime_hook("run_step", "model", "post")
189+
...: def maybe_stop(model, context, state):
190+
...: peak_idx = np.argmax(state[('profile', 'u')])
191+
...: peak_x = state[('grid', 'x')][peak_idx]
192+
...:
193+
...: if peak_x > 0.4:
194+
...: print("Peak crossed x=0.4, stop simulation!")
195+
...: return xs.RuntimeSignal.BREAK
196+
...:
197+
198+
In [3]: out_ds = in_ds.xsimlab.run(
199+
...: model=advect_model,
200+
...: hooks=[print_step_start, maybe_stop]
201+
...: )
202+
203+
Even when a simulation stops early like in the example above, the resulting
204+
xarray Dataset still contains all time steps defined in the input Dataset.
205+
Output variables have fill (masked) values for the time steps that were not run,
206+
as shown below with the ``nan`` values for ``profile__u`` (fill values are not
207+
stored physically in the Zarr output store).
208+
209+
.. ipython:: python
210+
211+
out_ds

doc/run_model.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,20 +106,21 @@ IPython (Jupyter) magic commands
106106

107107
Writing a new setup from scratch may be tedious, especially for big models with
108108
a lot of input variables. If you are using IPython (Jupyter), xarray-simlab
109-
provides convenient commands that can be activated with:
109+
provides helper commands that are available after loading the
110+
``xsimlab.ipython`` extension, i.e.,
110111

111112
.. ipython:: python
112113
113114
%load_ext xsimlab.ipython
114115
115116
The ``%create_setup`` magic command auto-generates the
116-
:func:`~xsimlab.create_setup` code cell above from a given model:
117+
:func:`~xsimlab.create_setup` code cell above from a given model, e.g.,
117118

118119
.. ipython:: python
119120
120-
%create_setup advect_model --default --comment
121+
%create_setup advect_model --default --verbose
121122
122-
The ``--default`` and ``--comment`` options respectively add default values found
123+
The ``--default`` and ``--verbose`` options respectively add default values found
123124
for input variables in the model and input variable description as line comments.
124125

125126
Full command help:

doc/whats_new.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Enhancements
3030
- Added :func:`~xsimlab.group_dict` variable (:issue:`159`).
3131
- Added :func:`~xsimlab.global_ref` variable for model-wise implicit linking of
3232
variables in separate processes, based on global names (:issue:`160`).
33+
- Added :class:`~xsimlab.RuntimeSignal` for controlling simulation workflow from
34+
process runtime methods and/or runtime hook functions (:issue:`161`).
3335

3436
Bug fixes
3537
~~~~~~~~~

xsimlab/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
process,
1212
process_info,
1313
runtime,
14+
RuntimeSignal,
1415
variable_info,
1516
)
1617
from .variable import (

xsimlab/drivers.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pandas as pd
66

77
from .hook import flatten_hooks, group_hooks, RuntimeHook
8+
from .process import RuntimeSignal
89
from .stores import ZarrSimulationStore
910
from .utils import get_batch_size
1011

@@ -345,16 +346,27 @@ def _run(
345346

346347
in_vars = _get_input_vars(ds_step, model)
347348
model.update_state(in_vars, validate=validate_inputs, ignore_static=False)
348-
model.execute("run_step", rt_context, **execute_kwargs)
349+
signal = model.execute("run_step", rt_context, **execute_kwargs)
350+
351+
if signal == RuntimeSignal.BREAK:
352+
break
349353

350354
store.write_output_vars(batch, step, model=model)
351355

352-
model.execute("finalize_step", rt_context, **execute_kwargs)
356+
# after writing output variables so that index positions
357+
# are properly updated in store.
358+
if signal == RuntimeSignal.CONTINUE:
359+
continue
360+
361+
signal = model.execute("finalize_step", rt_context, **execute_kwargs)
362+
363+
if signal == RuntimeSignal.BREAK:
364+
break
353365

354366
store.write_output_vars(batch, -1, model=model)
355367
store.write_index_vars(model=model)
356-
except Exception as error:
357-
raise error
368+
except Exception:
369+
raise
358370
finally:
359371
model.execute("finalize", rt_context, **execute_kwargs)
360372

xsimlab/hook.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import inspect
2+
from enum import Enum
23
from typing import Callable, Dict, Iterable, List, Union
34

45
from .process import SimulationStage
56

67

7-
__all__ = ("runtime_hook", "RuntimeHook")
8-
9-
108
def runtime_hook(stage, level="model", trigger="post"):
119
"""Decorator that allows to call a function or a method
1210
at one or more specific times during a simulation.
1311
1412
The decorated function / method must have the following signature:
15-
``func(model, context, state)`` or ``meth(self, model, context, state)``.
13+
``func(model, context, state)`` or ``meth(self, model, context, state)``. It
14+
may return a :class:`RuntimeSignal` (optional).
1615
1716
Parameters
1817
----------

0 commit comments

Comments
 (0)