Skip to content

Commit

Permalink
Static variables (#73)
Browse files Browse the repository at this point in the history
* add static=False parameter to variable()

* maybe raise when updating value of static variable

Raise when attempt is made during simulation runtime.
Also update existing tests.

* fix error msg

* update tests

* update doc

* update release notes
  • Loading branch information
benbovy committed Dec 12, 2019
1 parent 7b7cdc4 commit 057c3a3
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 42 deletions.
15 changes: 8 additions & 7 deletions doc/create_model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,24 @@ be given as a list, like variable ``v`` which represents a velocity
field that can be either constant (scalar) or variable (array) in
space.

.. note::

All variable objects also implicitly allow a time dimension.
See section :doc:`run_model`.

Additionally, it is also possible to add a short ``description``
and/or custom metadata like units with the ``attrs`` argument.

Another important argument is ``intent``, which specifies how the
process deals with the value of the variable. By default,
``intent='in'`` means that the process just needs the value of the
``intent='in'`` means that the process needs a value set for the
variable for its computation ; this value should either be computed
elsewhere by another process or be provided by the user as model
input. By contrast, variables ``x`` and ``u`` have ``intent='out'``,
which means that the process ``AdvectionLax1D`` itself initializes and
computes a value for these two variables.

Note also ``static=True`` set for ``spacing``, ``length``, ``loc`` and
``scale``. This is to prevent providing time varying values as model inputs for
those parameters. By default, it is possible to change the value of a variable
during a simulation (external forcing), see section :ref:`time_varying_inputs`
for an example. This is not always desirable, though.

Process "runtime" methods
~~~~~~~~~~~~~~~~~~~~~~~~~

Expand All @@ -108,7 +109,7 @@ simulation, e.g., for some clean-up.
Each of these methods can be decorated with :func:`~xsimlab.runtime`
to pass some useful information during simulation runtime (e.g.,
current time step number, current time or time step duration), which
may be need for the computation. Without this decorator, runtime
may be needed for the computation. Without this decorator, runtime
methods must have no other parameter than ``self``.

Getting / setting variable values
Expand Down
7 changes: 5 additions & 2 deletions doc/run_model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,14 @@ flat initial profile for :math:`u`) instead of ``model2`` :
@savefig run_model4.png width=100%
plot_u(out_ds4);
.. _time_varying_inputs:

Time-varying input values
-------------------------

All model inputs accept arrays which have a dimension that corresponds
to the master clock.
Except for static variables, all model inputs accept arrays which have a
dimension that corresponds to the master clock. This is useful for adding
external forcing.

The example below is based on the last example above, but instead of
being fixed, the flux of :math:`u` at the source point decreases over
Expand Down
16 changes: 8 additions & 8 deletions doc/scripts/advection_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
class AdvectionLax1D:
"""Wrap 1-dimensional advection in a single Process."""

spacing = xs.variable(description='grid spacing')
length = xs.variable(description='grid total length')
spacing = xs.variable(description='grid spacing', static=True)
length = xs.variable(description='grid total length', static=True)
x = xs.variable(dims='x', intent='out')

v = xs.variable(dims=[(), 'x'], description='velocity')

loc = xs.variable(description='location of initial profile')
scale = xs.variable(description='scale of initial profile')
loc = xs.variable(description='location of initial profile', static=True)
scale = xs.variable(description='scale of initial profile', static=True)
u = xs.variable(dims='x', intent='out', description='quantity u',
attrs={'units': 'm'})

Expand All @@ -40,8 +40,8 @@ def finalize_step(self):
class UniformGrid1D:
"""Create a 1-dimensional, equally spaced grid."""

spacing = xs.variable(description='uniform spacing')
length = xs.variable(description='total length')
spacing = xs.variable(description='uniform spacing', static=True)
length = xs.variable(description='total length', static=True)
x = xs.variable(dims='x', intent='out')

def initialize(self):
Expand Down Expand Up @@ -89,8 +89,8 @@ def run_step(self, dt):
class InitUGauss:
"""Initialize `u` profile using a Gaussian pulse."""

loc = xs.variable(description='location of initial pulse')
scale = xs.variable(description='scale of initial pulse')
loc = xs.variable(description='location of initial pulse', static=True)
scale = xs.variable(description='scale of initial pulse', static=True)
x = xs.foreign(UniformGrid1D, 'x')
u = xs.foreign(ProfileU, 'u', intent='out')

Expand Down
2 changes: 2 additions & 0 deletions doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Enhancements
:func:`xarray.Dataset.xsimlab.reset_vars` allows to (re)populate an input
Dataset with variables and their default values. :func:`~xsimlab.create_setup`
has also a new ``fill_default`` parameter.
- Added static variables, i.e., variables that don't accept time-varying input
values (:issue:`73`).

Bug fixes
~~~~~~~~~
Expand Down
54 changes: 42 additions & 12 deletions xsimlab/drivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,29 @@ def _bind_store_to_model(self):
for p_obj in self.model.values():
p_obj.__xsimlab_store__ = self.store

def update_store(self, input_vars):
"""Update the simulation active data store with input variable
values.
def _set_in_store(self, input_vars, check_static=True):
for key in self.model.input_vars:
value = input_vars.get(key)

if value is None:
continue

p_name, var_name = key
var = variables_dict(self.model[p_name].__class__)[var_name]

if check_static and var.metadata.get('static', False):
raise RuntimeError("Cannot set value in store for "
"static variable {!r} defined "
"in process {!r}"
.format(var_name, p_name))

self.store[key] = copy.copy(value)

def initialize_store(self, input_vars):
"""Pre-populate the simulation active data store with input
variable values.
This should be called before the simulation starts.
``input_vars`` is a dictionary where keys are store keys, i.e.,
``(process_name, var_name)`` tuples, and values are the input
Expand All @@ -82,11 +102,17 @@ def update_store(self, input_vars):
inputs are silently ignored.
"""
for key in self.model.input_vars:
value = input_vars.get(key)
self._set_in_store(input_vars, check_static=False)

if value is not None:
self.store[key] = copy.copy(value)
def update_store(self, input_vars):
"""Update the simulation active data store with input variable
values.
Like ``initialize_store``, but here meant to be called during
simulation runtime.
"""
self._set_in_store(input_vars, check_static=True)

def update_output_store(self, output_var_keys):
"""Update the simulation output store (i.e., append new values to the
Expand Down Expand Up @@ -188,19 +214,23 @@ def _get_output_save_steps(self):

return save_steps

def _set_input_vars(self, dataset):
def _get_input_vars(self, dataset):
input_vars = {}

for p_name, var_name in self.model.input_vars:
xr_var_name = p_name + '__' + var_name
xr_var = dataset.get(xr_var_name)

if xr_var is not None:
data = xr_var.data.copy()
data = xr_var.data

if data.ndim == 0:
# convert array to scalar
data = data.item()

self.store[(p_name, var_name)] = data
input_vars[(p_name, var_name)] = data

return input_vars

def _maybe_save_output_vars(self, istep):
# TODO: optimize this for performance
Expand Down Expand Up @@ -303,7 +333,7 @@ def run_model(self):
sim_end=ds_init['_sim_end'].values
)

self._set_input_vars(ds_init)
self.initialize_store(self._get_input_vars(ds_init))

self.model.execute('initialize', runtime_context)

Expand All @@ -314,7 +344,7 @@ def run_model(self):
step_end=ds_step['_clock_end'].values,
step_delta=ds_step['_clock_diff'].values)

self._set_input_vars(ds_step)
self.update_store(self._get_input_vars(ds_step))

self.model.execute('run_step', runtime_context)
self._maybe_save_output_vars(step)
Expand Down
3 changes: 2 additions & 1 deletion xsimlab/tests/fixture_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def _get_u_opposite(self):

@xs.process
class InitProfile:
n_points = xs.variable(description='nb. of profile points')
n_points = xs.variable(description='nb. of profile points',
static=True)
u = xs.foreign(Profile, 'u', intent='out')

def initialize(self):
Expand Down
1 change: 1 addition & 0 deletions xsimlab/tests/fixture_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def in_var_details():
- intent : in
- dims : (('x',), ('x', 'y'))
- groups : ()
- static : False
- attrs : {}
""")

Expand Down
36 changes: 25 additions & 11 deletions xsimlab/tests/test_drivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,32 @@ def test_bind_store(self, base_driver):
base_driver.store[('init_profile', 'n_points')] = 10
assert base_driver.model.init_profile.n_points == 10

def test_update_store(self, base_driver):
n = [10, 100, 1000]
def test_set_store_ignore(self, base_driver):
input_vars = {('not-a-model', 'input'): 0}
base_driver.initialize_store(input_vars)

assert ('not-a-model', 'input') not in base_driver.store

def test_set_store_copy(self, base_driver):
n = np.array(10)
input_vars = {('init_profile', 'n_points'): n}
base_driver.update_store(input_vars)
base_driver.initialize_store(input_vars)

assert base_driver.store[('init_profile', 'n_points')] == n
assert base_driver.store[('init_profile', 'n_points')] is not n

def test_initialize_store(self, base_driver):
input_vars = {('init_profile', 'n_points'): 10}
base_driver.initialize_store(input_vars)

assert base_driver.store[('init_profile', 'n_points')] == 10

def test_update_store(self, base_driver):
input_vars = {('init_profile', 'n_points'): 10}

with pytest.raises(RuntimeError, match=r".* static variable .*"):
base_driver.update_store(input_vars)

def test_update_output_store(self, base_driver):
base_driver.store[('init_profile', 'n_points')] = 5
base_driver.model.init_profile.initialize()
Expand All @@ -53,7 +71,7 @@ def test_run_model(self, base_driver):
base_driver.run_model()


@pytest.mark.parametrize('array,clock,expected' , [
@pytest.mark.parametrize('array,clock,expected', [
(np.zeros((2, 2)), None, ('x', 'y')),
(np.zeros((2, 2)), 'clock', ('x',)),
(np.array(0), None, tuple())
Expand Down Expand Up @@ -91,11 +109,11 @@ def test_output_save_steps(self, xarray_driver):
(('init_profile', 'n_points'), True),
(('add', 'offset'), False)
])
def test_set_input_vars(self, in_dataset, xarray_driver,
def test_get_input_vars(self, in_dataset, xarray_driver,
var_key, is_scalar):
xarray_driver._set_input_vars(in_dataset)
in_vars = xarray_driver._get_input_vars(in_dataset)

actual = xarray_driver.store[var_key]
actual = in_vars[var_key]
expected = in_dataset['__'.join(var_key)].data

if is_scalar:
Expand All @@ -106,10 +124,6 @@ def test_set_input_vars(self, in_dataset, xarray_driver,
assert_array_equal(actual, expected)
assert not np.isscalar(actual)

# test copy
actual[0] = -9999
assert not np.array_equal(actual, expected)

def test_get_output_dataset(self, in_dataset, xarray_driver):
# regression test: make sure a copy of input dataset is used
out_ds = xarray_driver.run_model()
Expand Down
11 changes: 10 additions & 1 deletion xsimlab/variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ def _as_group_tuple(groups, group):


def variable(dims=(), intent='in', group=None, groups=None,
default=attr.NOTHING, validator=None, description='', attrs=None):
default=attr.NOTHING, validator=None, static=False,
description='', attrs=None):
"""Create a variable.
Variables store useful metadata such as dimension labels, a short
Expand Down Expand Up @@ -144,6 +145,13 @@ def variable(dims=(), intent='in', group=None, groups=None,
If a ``list`` is passed, its items are treated as validators and must
all pass.
The validator can also be set using decorator notation.
static : bool, optional
If True, the value of the (input) variable must be set once
before the simulation starts and cannot be further updated
externally (default: False). Note that it doesn't prevent updating
the value internally, i.e., from within the process class in which
the variable is declared if ``intent`` is set to 'out' or 'inout',
or from another process class (foreign variable).
description : str, optional
Short description of the variable.
attrs : dict, optional
Expand All @@ -155,6 +163,7 @@ def variable(dims=(), intent='in', group=None, groups=None,
'dims': _as_dim_tuple(dims),
'intent': VarIntent(intent),
'groups': _as_group_tuple(groups, group),
'static': static,
'attrs': attrs or {},
'description': description}

Expand Down

0 comments on commit 057c3a3

Please sign in to comment.