Skip to content

Commit

Permalink
Expand BMI tests, fix BMI bugs.
Browse files Browse the repository at this point in the history
  • Loading branch information
BSchilperoort committed Jan 26, 2024
1 parent f8e38b4 commit a803209
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 38 deletions.
8 changes: 4 additions & 4 deletions PyStemmusScope/bmi/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ def get_time_step(self) -> float:
"""Return the current time step of the model."""
if self.state is None:
raise ValueError(NO_STATE_MSG)
return float(self.state["KT"][0])
return float(self.state["TimeStep"][0][0])

### GETTERS AND SETTERS ###
def get_value(self, name: str, dest: np.ndarray) -> np.ndarray:
Expand Down Expand Up @@ -542,8 +542,8 @@ def get_grid_shape(self, grid: int, shape: np.ndarray) -> np.ndarray:
msg = f"Unknown grid identifier '{grid}'"
raise ValueError(msg)

self.get_grid_x(grid, shape[-1]) # Last element is x
self.get_grid_y(grid, shape[-2]) # Semi-last element is y
shape[-1] = 1 # Last element is x
shape[-2] = 1 # Semi-last element is y
if grid == 1:
self.get_grid_z(grid, shape[-3]) # First element is z
shape[-3] = self.get_grid_size(grid) # First element is z
return shape
8 changes: 6 additions & 2 deletions PyStemmusScope/bmi/local_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from PyStemmusScope.config_io import read_config


def alive_process(process: Union[subprocess.Popen, None]) -> subprocess.Popen: # pragma: no cover
def alive_process(
process: Union[subprocess.Popen, None]
) -> subprocess.Popen: # pragma: no cover
"""Return process if the process is alive, raise an exception if it is not."""
if process is None:
msg = "Model process does not seem to be open."
Expand Down Expand Up @@ -40,7 +42,9 @@ def _model_is_ready(process: subprocess.Popen) -> None: # pragma: no cover
return _wait_for_model(PROCESS_READY, process)


def _wait_for_model(phrase: bytes, process: subprocess.Popen) -> None: # pragma: no cover
def _wait_for_model(
phrase: bytes, process: subprocess.Popen
) -> None: # pragma: no cover
"""Wait for model to be ready for interaction."""
output = b""

Expand Down
194 changes: 162 additions & 32 deletions tests/test_bmi_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@
)


# fmt: off
SOIL_GRID = np.array([
-5. , -4.8 , -4.6 , -4.4 , -4.2 , -4. , -3.8 , -3.6 ,
-3.4 , -3.2 , -3. , -2.8 , -2.6 , -2.45 , -2.3 , -2.2 ,
-2.1 , -2. , -1.9 , -1.8 , -1.7 , -1.6 , -1.5 , -1.4 ,
-1.3 , -1.2 , -1.1 , -1. , -0.9 , -0.8 , -0.7 , -0.6 ,
-0.55 , -0.5 , -0.45 , -0.4 , -0.35 , -0.325, -0.3 , -0.275,
-0.25 , -0.23 , -0.21 , -0.19 , -0.17 , -0.15 , -0.13 , -0.11 ,
-0.09 , -0.07 , -0.05 , -0.03 , -0.02 , -0.01 , -0.,
])

INVALID_METHODS = (
"get_grid_spacing", "get_grid_origin", "get_var_location", "get_grid_node_count",
"get_grid_edge_count", "get_grid_face_count", "get_grid_edge_nodes",
"get_grid_face_edges", "get_grid_face_nodes", "get_grid_nodes_per_face"
)
# fmt: on


def docker_available():
try:
docker.APIClient()
Expand Down Expand Up @@ -98,49 +117,160 @@ def prepare_data_config(tmpdir_factory, prep_input_data) -> Path:
return config_dir


@pytest.mark.skipif(not docker_available(), reason="Docker not available")
def test_initialize(prepare_data_config):
@pytest.fixture(scope="class")
def uninitialized_model():
model = StemmusScopeBmi()
yield model
try:
model.finalize()
except: # noqa
pass

assert model.get_component_name() == "STEMMUS_SCOPE"
with pytest.raises(ValueError, match="STEMMUS_SCOPE process is not running"):
model.update()

@pytest.fixture(scope="class")
def initialized_model(uninitialized_model, prepare_data_config):
model: StemmusScopeBmi = uninitialized_model
model.initialize(str(prepare_data_config))
yield model
model.finalize()

assert isinstance(model.get_input_item_count(), int)
assert isinstance(model.get_output_item_count(), int)
assert "soil_temperature" in model.get_input_var_names()
assert "respiration" in model.get_output_var_names()

assert model.get_var_grid("respiration") == 0
assert model.get_var_grid("soil_temperature") == 1
@pytest.fixture(scope="class")
def updated_model(uninitialized_model, prepare_data_config):
model: StemmusScopeBmi = uninitialized_model
model.initialize(str(prepare_data_config))
model.update()
yield model
model.finalize()

assert model.get_var_type("soil_temperature") == "float64"

# model.get_grid_size needs to have .update() run.
model.update()
@pytest.mark.skipif(not docker_available(), reason="Docker not available")
class TestUninitialized:
def test_component_name(self, uninitialized_model):
assert uninitialized_model.get_component_name() == "STEMMUS_SCOPE"

dest = np.zeros(model.get_grid_size(0))
np.testing.assert_almost_equal(model.get_grid_x(0, x=dest), np.array([-107.80752563]))
np.testing.assert_almost_equal(model.get_grid_y(0, y=dest), np.array([37.93380356]))
def test_invalid_update(self, uninitialized_model):
with pytest.raises(ValueError, match="STEMMUS_SCOPE process is not running"):
uninitialized_model.update()

with pytest.raises(ValueError, match="has no dimension `z`"):
model.get_grid_z(0, z=dest)
def test_get_ptr(self, uninitialized_model):
with pytest.raises(NotImplementedError):
uninitialized_model.get_value_ptr("soil_temperature")

model.update()
@pytest.mark.parametrize("method_name", INVALID_METHODS)
def test_not_implemented(self, uninitialized_model, method_name):
method = getattr(uninitialized_model, method_name)
nargs = method.__code__.co_argcount - 1 # remove "self"
with pytest.raises(NotImplementedError):
method(*(nargs * [0]))

dest = np.zeros(1)
model.get_value("respiration", dest)
assert dest[0] != 0.
def test_initialize(self, uninitialized_model, prepare_data_config):
uninitialized_model.initialize(str(prepare_data_config))

dest = np.zeros(1)
model.set_value_at_indices(
"soil_temperature",
inds=np.array([0]),
src=np.array([0.]),
)
model.get_value_at_indices("soil_temperature", dest, inds=np.array([0]))
assert dest[0] == 0.

model.finalize()
@pytest.mark.skipif(not docker_available(), reason="Docker not available")
class TestInitializedModel:
def test_input_item(self, initialized_model):
assert isinstance(initialized_model.get_input_item_count(), int)

def test_output_item(self, initialized_model):
assert isinstance(initialized_model.get_output_item_count(), int)

def test_input_var(self, initialized_model):
assert "soil_temperature" in initialized_model.get_input_var_names()

def test_output_var(self, initialized_model):
assert "respiration" in initialized_model.get_output_var_names()

def test_var_grid(self, initialized_model):
assert initialized_model.get_var_grid("respiration") == 0
assert initialized_model.get_var_grid("soil_temperature") == 1

def test_var_type(self, initialized_model):
assert initialized_model.get_var_type("soil_temperature") == "float64"

def test_grid_type(self, initialized_model):
assert initialized_model.get_grid_type(0) == "rectilinear"
assert initialized_model.get_grid_type(1) == "rectilinear"

def test_var_units(self, initialized_model):
assert initialized_model.get_var_units("soil_temperature") == "degC"

def test_grid_rank(self, initialized_model):
grid_resp = initialized_model.get_var_grid("respiration")
grid_t = initialized_model.get_var_grid("soil_temperature")
assert initialized_model.get_grid_rank(grid_resp) == 2
assert initialized_model.get_grid_rank(grid_t) == 3

def test_get_time_units(self, initialized_model):
assert (
initialized_model.get_time_units()
== "seconds since 1970-01-01 00:00:00.0 +0000"
)

def test_get_start_time(self, initialized_model):
assert initialized_model.get_start_time() == 820454400.0 # 1996-01-01 00:00:00

def test_get_end_time(self, initialized_model):
assert initialized_model.get_end_time() == 820461600.0 # 1996-01-01 02:00:00

def test_model_update(self, initialized_model):
initialized_model.update()


class TestUpdatedModel:
# Many of these should be available after init
def test_get_current_time(self, updated_model):
assert updated_model.get_current_time() == (
updated_model.get_start_time() + updated_model.get_time_step()
)

def test_get_time_step(self, updated_model):
assert updated_model.get_time_step() == 1800

def test_grid_coords(self, updated_model):
dest = np.zeros(updated_model.get_grid_size(0))
np.testing.assert_almost_equal(
updated_model.get_grid_x(0, x=dest), np.array([-107.80752563])
)
np.testing.assert_almost_equal(
updated_model.get_grid_y(0, y=dest), np.array([37.93380356])
)

def test_invalid_dimension(self, updated_model):
dest = np.zeros(updated_model.get_grid_size(0))
with pytest.raises(ValueError, match="has no dimension `z`"):
updated_model.get_grid_z(0, z=dest)

def test_grid_z(self, updated_model):
grid = updated_model.get_var_grid("soil_temperature")
dest = np.zeros(updated_model.get_grid_size(grid))
updated_model.get_grid_z(grid, z=dest)
np.testing.assert_array_equal(dest, SOIL_GRID)

def test_grid_shape(self, updated_model):
grid = updated_model.get_var_grid("soil_temperature")
shape = np.zeros(updated_model.get_grid_rank(grid), dtype=int)
updated_model.get_grid_shape(grid, shape)
np.testing.assert_array_equal(shape, np.array([55, 1, 1], dtype=int))

def test_get_value(self, updated_model):
dest = np.zeros(1)
updated_model.get_value("respiration", dest)
assert dest[0] != 0.0

def test_set_value_inds(self, updated_model):
dest = np.zeros(1)
updated_model.set_value_at_indices(
"soil_temperature",
inds=np.array([0]),
src=np.array([0.0]),
)
updated_model.get_value_at_indices("soil_temperature", dest, inds=np.array([0]))
assert dest[0] == 0.0

def test_itemsize(self, updated_model):
assert updated_model.get_var_itemsize("soil_temperature") == 8 # ==64 bits

def test_get_var_nbytes(self, updated_model):
assert updated_model.get_var_nbytes("soil_temperature") == 8 * 55

0 comments on commit a803209

Please sign in to comment.