From a803209ac410e310360fa77ec9d677aec75d6a2d Mon Sep 17 00:00:00 2001 From: Bart Schilperoort Date: Fri, 26 Jan 2024 10:58:21 +0100 Subject: [PATCH] Expand BMI tests, fix BMI bugs. --- PyStemmusScope/bmi/implementation.py | 8 +- PyStemmusScope/bmi/local_process.py | 8 +- tests/test_bmi_docker.py | 194 ++++++++++++++++++++++----- 3 files changed, 172 insertions(+), 38 deletions(-) diff --git a/PyStemmusScope/bmi/implementation.py b/PyStemmusScope/bmi/implementation.py index d33578ed..edb24876 100644 --- a/PyStemmusScope/bmi/implementation.py +++ b/PyStemmusScope/bmi/implementation.py @@ -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: @@ -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 diff --git a/PyStemmusScope/bmi/local_process.py b/PyStemmusScope/bmi/local_process.py index a8be2e83..5fea5d4d 100644 --- a/PyStemmusScope/bmi/local_process.py +++ b/PyStemmusScope/bmi/local_process.py @@ -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." @@ -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"" diff --git a/tests/test_bmi_docker.py b/tests/test_bmi_docker.py index 9e97c789..d233beab 100644 --- a/tests/test_bmi_docker.py +++ b/tests/test_bmi_docker.py @@ -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() @@ -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