Skip to content

Commit d2e9186

Browse files
Implement groundwater coupling changes in BMI (#101)
* Implement groundwater coupling changes in BMI * Implement new groundwater coupling variables * Programatically get/set values * Remove outdated test * Pin dependencies to working versions * Fix issues related to variables containing all timesteps * Add notebook demonstrating the groundwater coupling through BMI * Load docker image from test config file instead of hardcoded * Add checks for set_value to ensure arrays are right size * Add tests for new set_value input checks * stemmus_scope:1.6.0 image is now available * Skip notebooks in linting for now
1 parent 8adc6a1 commit d2e9186

10 files changed

Lines changed: 530 additions & 51 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM ghcr.io/ecoextreml/stemmus_scope:1.5.0
1+
FROM ghcr.io/ecoextreml/stemmus_scope:1.6.0
22

33
LABEL maintainer="Bart Schilperoort <b.schilperoort@esciencecenter.nl>"
44
LABEL org.opencontainers.image.source = "https://github.com/EcoExtreML/STEMMUS_SCOPE_Processing"

PyStemmusScope/bmi/docker_process.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class StemmusScopeDocker:
8383
"""Communicate with a STEMMUS_SCOPE Docker container."""
8484

8585
# Default image, can be overridden with config:
86-
compatible_tags = ("1.5.0",)
86+
compatible_tags = ("1.6.0",)
8787

8888
_process_ready_phrase = b"Select BMI mode:"
8989
_process_finalized_phrase = b"Finished clean up."

PyStemmusScope/bmi/implementation.py

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,31 @@
88
import numpy as np
99
from bmipy.bmi import Bmi
1010
from PyStemmusScope.bmi.utils import InapplicableBmiMethods
11+
from PyStemmusScope.bmi.utils import nested_set
12+
from PyStemmusScope.bmi.variable_reference import VARIABLES
13+
from PyStemmusScope.bmi.variable_reference import BmiVariable
1114
from PyStemmusScope.config_io import read_config
1215

1316

14-
MODEL_INPUT_VARNAMES: tuple[str, ...] = ("soil_temperature",)
15-
16-
MODEL_OUTPUT_VARNAMES: tuple[str, ...] = (
17-
"soil_temperature",
18-
"respiration",
17+
MODEL_INPUT_VARNAMES: tuple[str, ...] = tuple(
18+
var.name for var in VARIABLES if var.input
1919
)
2020

21-
MODEL_VARNAMES: tuple[str, ...] = tuple(
22-
set(MODEL_INPUT_VARNAMES + MODEL_OUTPUT_VARNAMES)
21+
MODEL_OUTPUT_VARNAMES: tuple[str, ...] = tuple(
22+
var.name for var in VARIABLES if var.output
2323
)
2424

25-
VARNAME_UNITS: dict[str, str] = {"respiration": "unknown", "soil_temperature": "degC"}
25+
MODEL_VARS: dict[str, BmiVariable] = {var.name: var for var in VARIABLES}
26+
27+
MODEL_VARNAMES: tuple[str, ...] = tuple(var.name for var in VARIABLES)
28+
29+
VARNAME_UNITS: dict[str, str] = {var.name: var.units for var in VARIABLES}
2630

27-
VARNAME_DTYPE: dict[str, str] = {
28-
"respiration": "float64",
29-
"soil_temperature": "float64",
30-
}
31+
VARNAME_DTYPE: dict[str, str] = {var.name: var.dtype for var in VARIABLES}
3132

32-
VARNAME_GRID: dict[str, int] = {
33-
"respiration": 0,
34-
"soil_temperature": 1,
35-
}
33+
VARNAME_GRID: dict[str, int] = {var.name: var.grid for var in VARIABLES}
34+
35+
VARNAME_LOC: dict[str, list[str]] = {var.name: var.keys for var in VARIABLES}
3636

3737
NO_STATE_MSG = (
3838
"The model state is not available. Please run `.update()` before requesting "
@@ -59,23 +59,32 @@ def load_state(config: dict) -> h5py.File:
5959
return h5py.File(matfile, mode="a")
6060

6161

62-
def get_variable(state: h5py.File, varname: str) -> np.ndarray:
62+
def get_variable(
63+
state: h5py.File, varname: str
64+
) -> np.ndarray: # noqa: PLR0911 PLR0912 C901
6365
"""Get a variable from the model state.
6466
6567
Args:
6668
state: STEMMUS_SCOPE model state
6769
varname: Variable name
6870
"""
69-
if varname == "respiration":
70-
return state["fluxes"]["Resp"][0]
71+
if varname not in MODEL_VARNAMES:
72+
msg = "Unknown variable name"
73+
raise ValueError(msg)
74+
75+
# deviating implemetation:
7176
elif varname == "soil_temperature":
7277
return state["TT"][0, :-1]
78+
79+
# default implementation:
80+
_s = state
81+
for _loc in VARNAME_LOC[varname]:
82+
_s = _s.get(_loc)
83+
84+
if MODEL_VARS[varname].all_timesteps:
85+
return _s[0].astype(VARNAME_DTYPE[varname])[[int(state["KT"][0])]]
7386
else:
74-
if varname in MODEL_VARNAMES:
75-
msg = "Varname is missing in get_variable! Contact devs."
76-
else:
77-
msg = "Unknown variable name"
78-
raise ValueError(msg)
87+
return _s[0].astype(VARNAME_DTYPE[varname])
7988

8089

8190
def set_variable(
@@ -101,16 +110,21 @@ def set_variable(
101110
else:
102111
vals = value
103112

113+
if varname in MODEL_OUTPUT_VARNAMES and varname not in MODEL_INPUT_VARNAMES:
114+
msg = "This variable is a model output variable only. You cannot set it."
115+
raise ValueError(msg)
116+
elif varname not in MODEL_INPUT_VARNAMES:
117+
msg = "Uknown variable name"
118+
raise ValueError(msg)
119+
120+
# deviating implementations:
104121
if varname == "soil_temperature":
105122
state["TT"][0, :-1] = vals
123+
elif varname == "groundwater_coupling_enabled":
124+
state["GroundwaterSettings"]["GroundwaterCoupling"][0] = vals.astype("float")
125+
# default:
106126
else:
107-
if varname in MODEL_OUTPUT_VARNAMES and varname not in MODEL_INPUT_VARNAMES:
108-
msg = "This variable is a model output variable only. You cannot set it."
109-
elif varname in MODEL_VARNAMES:
110-
msg = "Varname is missing in set_variable! Contact devs."
111-
else:
112-
msg = "Uknown variable name"
113-
raise ValueError(msg)
127+
nested_set(state, VARNAME_LOC[varname] + [0], vals)
114128
return state
115129

116130

@@ -401,6 +415,9 @@ def set_value(self, name: str, src: np.ndarray) -> None:
401415
"""
402416
if self.state is None:
403417
raise ValueError(NO_STATE_MSG)
418+
if src.size != self.get_grid_size(self.get_var_grid(name)):
419+
msg = f"Size of `src` and variable '{name}' grid size are not equal!"
420+
raise ValueError(msg)
404421
self.state = set_variable(self.state, name, src)
405422

406423
def set_value_at_indices(
@@ -419,6 +436,9 @@ def set_value_at_indices(
419436
"""
420437
if self.state is None:
421438
raise ValueError(NO_STATE_MSG)
439+
if inds.size != src.size:
440+
msg = "Sizes of `inds` and `src` are not equal!"
441+
raise ValueError(msg)
422442
self.state = set_variable(self.state, name, src, inds)
423443

424444
### GRID INFO ###

PyStemmusScope/bmi/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""Utilities for the STEMMUS_SCOPE Basic Model Interface."""
2+
from typing import Any
3+
from typing import Union
24
import numpy as np
35

46

@@ -64,3 +66,18 @@ def get_grid_nodes_per_face(
6466
) -> np.ndarray:
6567
"""Get the number of nodes for each face."""
6668
raise NotImplementedError(INAPPLICABLE_GRID_METHOD_MSG)
69+
70+
71+
def nested_set(dic: dict, keys: Union[list, tuple], value: Any) -> None:
72+
"""Set a value in a nested dictionary programatically.
73+
74+
E.g.: dict[keys[0]][keys[1]] = value
75+
76+
Args:
77+
dic: Dictionary to be modified.
78+
keys: Iterable of keys that are used to find the right value.
79+
value: The new value.
80+
"""
81+
for key in keys[:-1]:
82+
dic = dic.setdefault(key, {})
83+
dic[keys[-1]] = value
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Variable reference to inform the BMI implementation."""
2+
from dataclasses import dataclass
3+
4+
5+
@dataclass
6+
class BmiVariable:
7+
"""Holds all info to inform the BMI implementation."""
8+
9+
name: str
10+
dtype: str
11+
input: bool
12+
output: bool
13+
units: str
14+
grid: int
15+
keys: list[str]
16+
all_timesteps: bool = False
17+
18+
19+
VARIABLES: tuple[BmiVariable, ...] = (
20+
# atmospheric vars:
21+
BmiVariable(
22+
name="respiration",
23+
dtype="float64",
24+
input=False,
25+
output=True,
26+
units="cm s-1",
27+
grid=0,
28+
keys=["fluxes", "Resp"],
29+
),
30+
BmiVariable(
31+
name="evaporation_total",
32+
dtype="float64",
33+
input=False,
34+
output=True,
35+
units="cm s-1",
36+
grid=0,
37+
keys=["EVAP"],
38+
all_timesteps=True,
39+
),
40+
# soil vars:
41+
BmiVariable(
42+
name="soil_temperature",
43+
dtype="float64",
44+
input=True,
45+
output=True,
46+
units="degC",
47+
grid=1,
48+
keys=["TT"],
49+
),
50+
BmiVariable(
51+
name="soil_moisture",
52+
dtype="float64",
53+
input=True,
54+
output=True,
55+
units="m3 m-3",
56+
grid=1,
57+
keys=["SoilVariables", "Theta_U"],
58+
),
59+
BmiVariable(
60+
name="soil_root_water_uptake",
61+
dtype="float64",
62+
input=False,
63+
output=True,
64+
units="cm s-1",
65+
grid=0,
66+
keys=["RWUs"],
67+
),
68+
# surface runoff
69+
BmiVariable(
70+
name="surface_runoff_total",
71+
dtype="float64",
72+
input=False,
73+
output=True,
74+
units="cm s-1",
75+
grid=0,
76+
keys=["RS"],
77+
),
78+
BmiVariable(
79+
name="surface_runoff_hortonian",
80+
dtype="float64",
81+
input=False,
82+
output=True,
83+
units="cm s-1",
84+
grid=0,
85+
keys=["ForcingData", "R_Dunn"],
86+
all_timesteps=True,
87+
),
88+
BmiVariable(
89+
name="surface_runoff_dunnian",
90+
dtype="float64",
91+
input=False,
92+
output=True,
93+
units="cm s-1",
94+
grid=0,
95+
keys=["ForcingData", "R_Hort"],
96+
all_timesteps=True,
97+
),
98+
# groundwater vars (STEMMUS_SCOPE)
99+
BmiVariable(
100+
name="groundwater_root_water_uptake",
101+
dtype="float64",
102+
input=False,
103+
output=True,
104+
units="cm s-1",
105+
grid=0,
106+
keys=["RWUg"],
107+
),
108+
BmiVariable(
109+
name="groundwater_recharge",
110+
dtype="float64",
111+
input=False,
112+
output=True,
113+
units="cm s-1",
114+
grid=0,
115+
keys=["gwfluxes", "recharge"],
116+
),
117+
# groundwater (coupling) vars
118+
BmiVariable(
119+
name="groundwater_coupling_enabled",
120+
dtype="bool",
121+
input=True,
122+
output=False,
123+
units="-",
124+
grid=0,
125+
keys=["GroundwaterSettings", "GroundwaterCoupling"],
126+
),
127+
BmiVariable(
128+
name="groundwater_head_bottom_layer",
129+
dtype="float64",
130+
input=True,
131+
output=False,
132+
units="cm",
133+
grid=0,
134+
keys=["GroundwaterSettings", "headBotmLayer"],
135+
),
136+
BmiVariable(
137+
name="groundwater_temperature",
138+
dtype="float64",
139+
input=True,
140+
output=False,
141+
units="degC",
142+
grid=0,
143+
keys=["GroundwaterSettings", "tempBotm"],
144+
),
145+
BmiVariable(
146+
name="groundwater_elevation_top_aquifer",
147+
dtype="float64",
148+
input=True,
149+
output=False,
150+
units="cm",
151+
grid=0,
152+
keys=["GroundwaterSettings", "topLevel"],
153+
),
154+
)

docs/bmi.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ To use the Docker image, use the `DockerImage` setting in the configuration file
1919
```sh
2020
WorkDir=/home/username/tmp/stemmus_scope
2121
...
22-
DockerImage=ghcr.io/ecoextreml/stemmus_scope:1.5.0
22+
DockerImage=ghcr.io/ecoextreml/stemmus_scope:1.6.0
2323
```
2424

25-
It is best to add the version tag here too (`:1.5.0`), this way the BMI will warn you if the version might be incompatible.
25+
It is best to add the version tag here too (`:1.6.0`), this way the BMI will warn you if the version might be incompatible.
2626

2727
Note that the `docker` package for python is required here. Install this with `pip install PyStemmusScope[docker]`.
2828
Additionally, [Docker](https://docs.docker.com/get-docker/) itself has to be installed.

0 commit comments

Comments
 (0)