Skip to content

Commit a6494d0

Browse files
feefladderbenbovy
andauthored
add time clock to runtime (#170)
* added a main_clock with dimension 'mclock' to initialize context, this returns a xr.DataArray. However, I cannot set xs.variables in the initialize step: 'AttributeError: cannot set attribute' * fixed black * changed 'master' to 'main' everywhere except in non-breaking testcases. * fixed space and undid changes of access-clock * added placeholder * fixed black (again) * added master_clock_dim + tests, master_clock_coords no tests * Update xsimlab/xr_accessor.py Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de> * Update xsimlab/xr_accessor.py Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de> * Update xsimlab/xr_accessor.py Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de> * added master/main_clock_coord access tests, updated whats-new * removed vscode settings * created duck-typed singleton class for main clock and managed access * stuck at 'store_output_vars' has unmatching dimensions * Apply suggestions from code review Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de> * removed raise checks from test_update_clocks_master_clock_warning * removed raises in test_update_clocks_master_warning * removed redundancy in test_update_master_clock * properly implemented singleton, should work now * added tests * added check for double dimensions. * stuff, deleted prints * Update xsimlab/utils.py Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de> * Update xsimlab/variable.py Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de> * better tests * black... * stupid test solved * Update xsimlab/xr_accessor.py Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de> * Update xsimlab/tests/test_model.py Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de> * did some stuff * should be good * docs * updated what's new Co-authored-by: Benoit Bovy <bbovy@gfz-potsdam.de> Co-authored-by: Benoit Bovy <benbovy@gmail.com>
1 parent 201ba8e commit a6494d0

10 files changed

Lines changed: 134 additions & 12 deletions

File tree

doc/api.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ Process introspection and variables
141141
variable_info
142142
filter_variables
143143

144+
145+
144146
Process runtime methods
145147
-----------------------
146148

@@ -156,6 +158,7 @@ Variable
156158
:toctree: _api_generated/
157159

158160
variable
161+
MAIN_CLOCK
159162
index
160163
any_object
161164
foreign

doc/whats_new.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ v0.6.0 (Unreleased)
99
``main_clock``, ``main_clock_dim`` and ``main_clock_coords`` and all
1010
occurences of ``master`` to ``main`` in the rest of the codebase. all
1111
``master...`` API hooks are still working, but raise a Futurewarning
12+
- Added access to main clock in initialize step as ``main_clock_values``
13+
and as a ``xr.DataArray``: ``main_clock_array``. for refering to the main
14+
clock as a dimension label, the placeholder ``xs.MAIN_CLOCK`` can be used.
15+
This will be set to the main clock when storing the dataset.
1216
- Changed default ``fill_value`` in the zarr stores to maximum dtype value
1317
for integer dtypes and ``np.nan`` for floating-point variables.
1418

15-
16-
1719
v0.5.0 (26 January 2021)
1820
------------------------
1921

xsimlab/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
group,
2525
group_dict,
2626
)
27+
from .utils import MAIN_CLOCK
2728
from .xr_accessor import SimlabAccessor, create_setup
2829
from . import monitoring
2930

xsimlab/drivers.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .hook import flatten_hooks, group_hooks, RuntimeHook
88
from .process import RuntimeSignal
99
from .stores import ZarrSimulationStore
10-
from .utils import get_batch_size
10+
from .utils import get_batch_size, MAIN_CLOCK
1111

1212

1313
class ValidateOption(Enum):
@@ -28,6 +28,8 @@ class RuntimeContext(Mapping[str, Any]):
2828
"batch",
2929
"sim_start",
3030
"sim_end",
31+
"main_clock_values",
32+
"main_clock_dataarray",
3133
"step",
3234
"nsteps",
3335
"step_start",
@@ -161,6 +163,8 @@ def _generate_runtime_datasets(dataset):
161163
init_data_vars = {
162164
"_sim_start": mclock_coord[0],
163165
"_nsteps": dataset.xsimlab.nsteps,
166+
# since we pass a dataset, we need to set the coords
167+
"_main_clock_values": dataset.coords[mclock_dim].data,
164168
"_sim_end": mclock_coord[-1],
165169
}
166170

@@ -327,6 +331,8 @@ def _run(
327331
sim_start=ds_init["_sim_start"].values,
328332
nsteps=ds_init["_nsteps"].values,
329333
sim_end=ds_init["_sim_end"].values,
334+
main_clock_values=ds_init["_main_clock_values"].values,
335+
main_clock_dataarray=dataset.xsimlab.main_clock_coord,
330336
)
331337

332338
in_vars = _get_input_vars(ds_init, model)

xsimlab/stores.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import zarr
88

99
from . import Model
10-
from .utils import get_batch_size, normalize_encoding
10+
from .utils import get_batch_size, normalize_encoding, MAIN_CLOCK
1111
from .variable import VarType
1212

1313

@@ -252,6 +252,14 @@ def _create_zarr_dataset(
252252
f"its accepted dimension(s): {var_info['metadata']['dims']}"
253253
)
254254

255+
# set MAIN_CLOCK placeholder to main_clock dimension
256+
if self.mclock_dim in dim_labels and MAIN_CLOCK in dim_labels:
257+
raise ValueError(
258+
f"Main clock: '{self.mclock_dim}' has a duplicate in {dim_labels}."
259+
"Please change the name of 'main_clock' in `create_setup`"
260+
)
261+
dim_labels = [self.mclock_dim if d is MAIN_CLOCK else d for d in dim_labels]
262+
255263
if clock is not None:
256264
dim_labels.insert(0, clock)
257265
if add_batch_dim:
@@ -331,7 +339,6 @@ def write_output_vars(self, batch: int, step: int, model: Optional[Model] = None
331339

332340
else:
333341
idx_dims = [clock_inc] + [slice(0, n) for n in np.shape(value)]
334-
335342
if batch != -1:
336343
idx_dims.insert(0, batch)
337344

xsimlab/tests/test_model.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,3 +452,68 @@ def initialize(self):
452452
model.execute("initialize", {})
453453

454454
assert model.state[("baz", "actual")] == Frozen({("foo", "a"): 1, ("bar", "b"): 2})
455+
456+
457+
def test_main_clock_access():
458+
@xs.process
459+
class Foo:
460+
a = xs.variable(intent="out", dims=xs.MAIN_CLOCK)
461+
b = xs.variable(intent="out", dims=xs.MAIN_CLOCK)
462+
463+
@xs.runtime(args=["main_clock_values", "main_clock_dataarray"])
464+
def initialize(self, clock_values, clock_array):
465+
self.a = clock_values * 2
466+
np.testing.assert_equal(self.a, [0, 2, 4, 6])
467+
self.b = clock_array * 2
468+
assert clock_array.dims[0] == "clock"
469+
assert all(clock_array[clock_array.dims[0]].data == [0, 1, 2, 3])
470+
471+
@xs.runtime(args=["step_delta", "step"])
472+
def run_step(self, dt, n):
473+
assert self.a[n] == 2 * n
474+
self.a[n] += 1
475+
476+
model = xs.Model({"foo": Foo})
477+
ds_in = xs.create_setup(
478+
model=model,
479+
clocks={"clock": range(4)},
480+
input_vars={},
481+
output_vars={"foo__a": None},
482+
)
483+
ds_out = ds_in.xsimlab.run(model=model)
484+
assert all(ds_out.foo__a.data == [1, 3, 5, 6])
485+
486+
# test for error when another dim has the same name as xs.MAIN_CLOCK
487+
@xs.process
488+
class DoubleMainClockDim:
489+
a = xs.variable(intent="out", dims=("clock", xs.MAIN_CLOCK))
490+
491+
def initialize(self):
492+
self.a = [[1, 2, 3], [3, 4, 5]]
493+
494+
def run_step(self):
495+
self.a += self.a
496+
497+
model = xs.Model({"foo": DoubleMainClockDim})
498+
with pytest.raises(ValueError, match=r"Main clock:*"):
499+
xs.create_setup(
500+
model=model,
501+
clocks={"clock": [0, 1, 2, 3]},
502+
input_vars={},
503+
output_vars={"foo__a": None},
504+
).xsimlab.run(model)
505+
506+
# test for error when trying to put xs.MAIN_CLOCK as a dim in an input var
507+
with pytest.raises(
508+
ValueError, match="Do not pass xs.MAIN_CLOCK into input vars dimensions"
509+
):
510+
a = xs.variable(intent="in", dims=xs.MAIN_CLOCK)
511+
512+
with pytest.raises(
513+
ValueError, match="Do not pass xs.MAIN_CLOCK into input vars dimensions"
514+
):
515+
b = xs.variable(intent="in", dims=(xs.MAIN_CLOCK,))
516+
with pytest.raises(
517+
ValueError, match="Do not pass xs.MAIN_CLOCK into input vars dimensions"
518+
):
519+
c = xs.variable(intent="in", dims=["a", ("a", xs.MAIN_CLOCK)])

xsimlab/tests/test_xr_accessor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,9 @@ def test_master_clock_coords_warning(self):
158158
)
159159
with pytest.warns(
160160
FutureWarning,
161-
match="master_clock is to be deprecated in favour of main_clock",
161+
match="master_clock_coord is to be deprecated in favour of main_clock",
162162
):
163-
xr.testing.assert_equal(ds.xsimlab.master_clock_coord, ds.mclock)
163+
ds.xsimlab.master_clock_coord
164164

165165
def test_clock_sizes(self):
166166
ds = xr.Dataset(

xsimlab/utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,34 @@
1515
V = TypeVar("V")
1616

1717

18+
class _MainClockDim:
19+
"""Singleton class to be used as a placeholder of the main clock
20+
dimension.
21+
22+
It will be replaced by the actual dimension label set during simulation setup
23+
(i.e., ``main_clock`` argument).
24+
25+
"""
26+
27+
_singleton = None
28+
29+
def __new__(cls):
30+
if _MainClockDim._singleton is None:
31+
# if there is no instance of it yet, create a class instance
32+
_MainClockDim._singleton = super(_MainClockDim, cls).__new__(cls)
33+
return _MainClockDim._singleton
34+
35+
def __repr__(self):
36+
return "MAIN_CLOCK (undefined)"
37+
38+
39+
MAIN_CLOCK = _MainClockDim()
40+
"""
41+
Sentinel to indicate simulation's main clock dimension, to be
42+
replaced by the actual dimension label set in input/output datasets.
43+
"""
44+
45+
1846
def variables_dict(process_cls):
1947
"""Get all xsimlab variables declared in a process.
2048

xsimlab/variable.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import attr
66
from attr._make import _CountingAttr
77

8-
from .utils import normalize_encoding
8+
from .utils import normalize_encoding, MAIN_CLOCK
99

1010

1111
class VarType(Enum):
@@ -57,12 +57,18 @@ def _as_dim_tuple(dims):
5757
ambiguous and thus not allowed.
5858
5959
"""
60-
if not len(dims):
60+
# MAIN_CLOCK is sentinel and does not have length (or zero), so check explicitly
61+
if dims is MAIN_CLOCK:
62+
dims = [(dims,)]
63+
elif not len(dims):
6164
dims = [()]
6265
elif isinstance(dims, str):
6366
dims = [(dims,)]
6467
elif isinstance(dims, list):
65-
dims = [tuple([d]) if isinstance(d, str) else tuple(d) for d in dims]
68+
dims = [
69+
tuple([d]) if (isinstance(d, str) or d is MAIN_CLOCK) else tuple(d)
70+
for d in dims
71+
]
6672
else:
6773
dims = [dims]
6874

@@ -221,6 +227,9 @@ def variable(
221227
else:
222228
_init = True
223229
_repr = True
230+
# also check if MAIN_CLOCK is there
231+
if any([MAIN_CLOCK in d for d in metadata["dims"]]):
232+
raise ValueError("Do not pass xs.MAIN_CLOCK into input vars dimensions")
224233

225234
return attr.attrib(
226235
metadata=metadata,

xsimlab/xr_accessor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def master_clock_coord(self):
209209
Returns None if no main clock is defined in the dataset.
210210
"""
211211
warnings.warn(
212-
"master_clock is to be deprecated in favour of main_clock",
212+
"master_clock_coord is to be deprecated in favour of main_clock",
213213
FutureWarning,
214214
)
215215
return self.main_clock_coord
@@ -224,7 +224,7 @@ def main_clock_coord(self):
224224

225225
@property
226226
def nsteps(self):
227-
"""Number of simulation steps, computed from the master
227+
"""Number of simulation steps, computed from the main
228228
clock coordinate.
229229
230230
Returns 0 if no main clock is defined in the dataset.
@@ -319,6 +319,7 @@ def _uniformize_clock_coords(self, dim=None, units=None, calendar=None):
319319
)
320320

321321
def _set_input_vars(self, model, input_vars):
322+
322323
invalid_inputs = set(input_vars) - set(model.input_vars)
323324

324325
if invalid_inputs:

0 commit comments

Comments
 (0)