Skip to content

Commit

Permalink
Merge pull request #373 from ImperialCollegeLondon/frozen_models
Browse files Browse the repository at this point in the history
Adds support for static models
  • Loading branch information
TaranRallings authored Dec 13, 2024
2 parents 48ec0ff + 732cea3 commit 1a92bef
Show file tree
Hide file tree
Showing 26 changed files with 986 additions and 444 deletions.
19 changes: 19 additions & 0 deletions docs/source/using_the_ve/configuration/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,22 @@ formatted the configuration process will critically fail.

In addition to saving the configuration as an output file, it is also returned so that
downstream functions can make use of it. This is as a simple nested dictionary.

## Static models

All models (except `core`) accept a boolean configuration option, `static`, that
indicates if such a model should be updated every iteration (`static=false`, the
default behaviour) or not.

If `static=true`, there are several possibilities depending on the data
variables available in the [`Data` object](../data/data.md):

- For the initialisation, if the variables that are populated are:
- **All present**: The model initialisation process is bypassed.
- **None present**: The model initialisation process runs normally.
- For the update, if the variables that are populated are:
- **All present**: Then the update process is bypassed.
- **None present**: The update *is run just once*, keeping the same values for those
variables throughout the simulation.

Providing some but not all of the variables will result in an error in all cases.
15 changes: 15 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Collection of fixtures to assist the testing scripts."""

from logging import DEBUG
from unittest.mock import patch

import numpy as np
import pytest
Expand Down Expand Up @@ -567,3 +568,17 @@ def dummy_climate_data_varying_canopy(fixture_core_components, dummy_climate_dat
]

return dummy_climate_data


def patch_run_update(model: str):
"""Patch the run_update_check udring the init of the model."""
klass = "".join([w.capitalize() for w in model.split("_")] + ["Model"])
object_to_patch = f"virtual_ecosystem.models.{model}.{model}_model.{klass}"
return patch(f"{object_to_patch}._run_update_due_to_static_configuration")


def patch_bypass_setup(model: str):
"""Patch the run_update_check udring the init of the model."""
klass = "".join([w.capitalize() for w in model.split("_")] + ["Model"])
object_to_patch = f"virtual_ecosystem.models.{model}.{model}_model.{klass}"
return patch(f"{object_to_patch}._bypass_setup_due_to_static_configuration")
257 changes: 255 additions & 2 deletions tests/core/test_base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,13 +522,13 @@ class TimingTestModel(
vars_populated_by_init=tuple(),
vars_populated_by_first_update=tuple(),
):
def setup(self) -> None:
def _setup(self) -> None:
pass

def spinup(self) -> None:
pass

def update(self, time_index: int, **kwargs: Any) -> None:
def _update(self, time_index: int, **kwargs: Any) -> None:
pass

def cleanup(self) -> None:
Expand Down Expand Up @@ -557,3 +557,256 @@ def from_config(
)

log_check(caplog, expected_log)


@pytest.mark.parametrize(
"static, vars_populated_by_init, data_vars, expected_result, expected_exception, "
"expected_message",
[
# Test case where model is static and all variables are present
pytest.param(
True,
("var1", "var2"),
{"var1": 1, "var2": 2},
True,
does_not_raise(),
None,
id="static_all_vars_present",
),
# Test case where model is static and no variables are present
pytest.param(
True,
("var1", "var2"),
{},
True,
does_not_raise(),
None,
id="static_no_vars_present",
),
# Test case where model is static and some variables are present
pytest.param(
True,
("var1", "var2"),
{"var1": 1},
None,
pytest.raises(ConfigurationError),
"Static model test_model requires to either all variables in "
"vars_populated_by_init to be present in the data object or all to be "
"absent. 1 out of 2 found: var1.",
id="static_some_vars_present",
),
# Test case where model is not static and no variables are present
pytest.param(
False,
("var1", "var2"),
{},
False,
does_not_raise(),
None,
id="non_static_no_vars_present",
),
# Test case where model is not static and some variables are present
pytest.param(
False,
("var1", "var2"),
{"var1": 1},
None,
pytest.raises(ConfigurationError),
"Non-static model test_model requires none of the variables in "
"vars_populated_by_init to be present in the data object. "
"Present variables: var1",
id="non_static_some_vars_present",
),
],
)
def test_bypass_setup_due_to_static_configuration(
static,
vars_populated_by_init,
data_vars,
expected_result,
expected_exception,
expected_message,
fixture_data,
fixture_config,
):
"""Test the _bypass_setup_due_to_static_configuration method."""
from virtual_ecosystem.core.base_model import BaseModel
from virtual_ecosystem.core.config import Config
from virtual_ecosystem.core.core_components import (
CoreComponents,
)
from virtual_ecosystem.core.data import Data

class TestModel(
BaseModel,
model_name="test_model",
model_update_bounds=("1 day", "1 month"),
vars_required_for_init=(),
vars_updated=(),
vars_required_for_update=(),
vars_populated_by_init=vars_populated_by_init,
vars_populated_by_first_update=(),
):
def _setup(self, *args: Any, **kwargs: Any) -> None:
pass

def spinup(self) -> None:
pass

def _update(self, time_index: int, **kwargs: Any) -> None:
pass

def cleanup(self) -> None:
pass

@classmethod
def from_config(
cls, data: Data, core_components: CoreComponents, config: Config
) -> BaseModel:
return super().from_config(
data=data, core_components=core_components, config=config
)

for var in data_vars.keys():
fixture_data[var] = fixture_data["existing_var"].copy()

core_components = CoreComponents(config=fixture_config)

with expected_exception as exc:
model = TestModel(
data=fixture_data, core_components=core_components, static=static
)
result = model._bypass_setup_due_to_static_configuration()
assert result == expected_result

if expected_message:
assert str(exc.value) == expected_message


@pytest.mark.parametrize(
"static, vars_populated_by_first_update, vars_updated, data_vars, expected_result,"
" expected_exception, expected_message",
[
# Test case where model is static and all variables are present
pytest.param(
True,
("var1", "var2"),
("var3",),
{"var1": 1, "var2": 2, "var3": 3},
False,
does_not_raise(),
None,
id="static_all_vars_present",
),
# Test case where model is static and no variables are present
pytest.param(
True,
("var1", "var2"),
("var3",),
{},
True,
does_not_raise(),
None,
id="static_no_vars_present",
),
# Test case where model is static and some variables are present
pytest.param(
True,
("var1", "var2"),
("var3",),
{"var1": 1},
None,
pytest.raises(ConfigurationError),
"Static model test_model requires to either all variables in "
"vars_populated_by_first_update and vars_updated to be present "
"in the data object or all to be absent. 1 out of 3 found: var1.",
id="static_some_vars_present",
),
# Test case where model is not static and no variables are present
pytest.param(
False,
("var1", "var2"),
("var3",),
{},
True,
does_not_raise(),
None,
id="non_static_no_vars_present",
),
# Test case where model is not static and some variables are present
pytest.param(
False,
("var1", "var2"),
("var3",),
{"var1": 1},
None,
pytest.raises(ConfigurationError),
"Non-static model test_model requires none of the variables in "
"vars_populated_by_first_update or vars_updated to be present in the "
"data object. Present variables: var1",
id="non_static_some_vars_present",
),
],
)
def test_run_update_due_to_static_configuration(
static,
vars_populated_by_first_update,
vars_updated,
data_vars,
expected_result,
expected_exception,
expected_message,
fixture_data,
fixture_config,
):
"""Test the _run_update_due_to_static_configuration method."""

from virtual_ecosystem.core.base_model import BaseModel
from virtual_ecosystem.core.config import Config
from virtual_ecosystem.core.core_components import CoreComponents
from virtual_ecosystem.core.data import Data

class TestModel(
BaseModel,
model_name="test_model",
model_update_bounds=("1 day", "1 month"),
vars_required_for_init=(),
vars_updated=vars_updated,
vars_required_for_update=(),
vars_populated_by_init=(),
vars_populated_by_first_update=vars_populated_by_first_update,
):
def _setup(self, *args: Any, **kwargs: Any) -> None:
pass

def spinup(self) -> None:
pass

def _update(self, time_index: int, **kwargs: Any) -> None:
pass

def cleanup(self) -> None:
pass

@classmethod
def from_config(
cls, data: Data, core_components: CoreComponents, config: Config
) -> BaseModel:
return super().from_config(
data=data, core_components=core_components, config=config
)

for var in data_vars.keys():
fixture_data[var] = fixture_data["existing_var"].copy()

core_components = CoreComponents(config=fixture_config)

with expected_exception as exc:
model = TestModel(
data=fixture_data, core_components=core_components, static=static
)
result = model._run_update_due_to_static_configuration()
assert result == expected_result

if expected_message:
assert str(exc.value) == expected_message
Loading

0 comments on commit 1a92bef

Please sign in to comment.