Skip to content

Commit

Permalink
Merge branch '451-add-dynamic-filled-canopy-index-to-layerstructure' …
Browse files Browse the repository at this point in the history
…into 458-adopt-extended-layerstructure-functionality
  • Loading branch information
davidorme committed Jun 27, 2024
2 parents c60a201 + 022cb50 commit a2d19e9
Show file tree
Hide file tree
Showing 4 changed files with 501 additions and 172 deletions.
48 changes: 31 additions & 17 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,23 +253,37 @@ def dummy_carbon_data(fixture_core_components):

# The layer dependant data has to be handled separately - at present all of these
# are defined only for the topsoil layer
data["soil_moisture"] = fixture_core_components.layer_structure.from_template()
data["soil_moisture"].loc[
{"layers": fixture_core_components.layer_structure.role_indices["topsoil"]}
] = [232.61550125, 196.88733175, 126.065797, 75.63195175]

data["matric_potential"] = fixture_core_components.layer_structure.from_template()
data["matric_potential"].loc[
{"layers": fixture_core_components.layer_structure.role_indices["topsoil"]}
] = [-3.0, -10.0, -250.0, -10000.0]

data["soil_temperature"] = fixture_core_components.layer_structure.from_template()
data["soil_temperature"].loc[
{"layers": fixture_core_components.layer_structure.role_indices["topsoil"]}
] = [35.0, 37.5, 40.0, 25.0]
data["soil_temperature"].loc[
{"layers": fixture_core_components.layer_structure.role_indices["subsoil"]}
] = [22.5, 22.5, 22.5, 22.5]
ls = fixture_core_components.layer_structure

data["soil_moisture"] = ls.from_template()
data["soil_moisture"].loc[{"layers": ls.role_indices["topsoil"]}] = [
232.61550125,
196.88733175,
126.065797,
75.63195175,
]

data["matric_potential"] = ls.from_template()
data["matric_potential"].loc[{"layers": ls.role_indices["topsoil"]}] = [
-3.0,
-10.0,
-250.0,
-10000.0,
]

data["soil_temperature"] = ls.from_template()
data["soil_temperature"].loc[{"layers": ls.role_indices["topsoil"]}] = [
35.0,
37.5,
40.0,
25.0,
]
data["soil_temperature"].loc[{"layers": ls.role_indices["subsoil"]}] = [
22.5,
22.5,
22.5,
22.5,
]

return data

Expand Down
29 changes: 22 additions & 7 deletions tests/core/test_base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@


@pytest.fixture(scope="module")
def data_instance():
"""Creates a simple data instance for use in testing."""
def fixture_data_instance_for_model_validation():
"""Data instance with badly dimensioned data.
Creates a simple data instance for use in testing whether models correctly apply
validation of required variables.
"""
from xarray import DataArray

from virtual_ecosystem.core.data import Data
Expand Down Expand Up @@ -292,7 +296,9 @@ class InitVarModel(
pass

with pytest.raises(TypeError) as err:
_ = InitVarModel(data=data_instance, core_components=fixture_core_components)
_ = InitVarModel(
data=dummy_climate_data, core_components=fixture_core_components
)

# Note python version specific exception messages:
# - Can't instantiate abstract class InitVarModel with abstract methods cleanup,
Expand Down Expand Up @@ -351,7 +357,7 @@ class InitVarModel(
)
def test_check_required_init_vars(
caplog,
data_instance,
fixture_data_instance_for_model_validation,
fixture_core_components,
req_init_vars,
raises,
Expand Down Expand Up @@ -408,7 +414,7 @@ def from_config(
# Create an instance to check the handling
with raises as err:
inst = TestCaseModel(
data=data_instance,
data=fixture_data_instance_for_model_validation,
core_components=fixture_core_components,
)

Expand Down Expand Up @@ -476,7 +482,13 @@ def from_config(
),
],
)
def test_check_update_speed(caplog, config_string, raises, expected_log):
def test_check_update_speed(
caplog,
fixture_data_instance_for_model_validation,
config_string,
raises,
expected_log,
):
"""Tests check on update speed."""

from virtual_ecosystem.core.base_model import BaseModel
Expand Down Expand Up @@ -520,6 +532,9 @@ def from_config(
caplog.clear()

with raises:
_ = TimingTestModel(data=data_instance, core_components=core_components)
_ = TimingTestModel(
data=fixture_data_instance_for_model_validation,
core_components=core_components,
)

log_check(caplog, expected_log)
136 changes: 104 additions & 32 deletions tests/core/test_core_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
pytest.param(
"[core]",
{
"canopy_layers": 10,
"soil_layers": np.array([-0.25, -1.0]),
"n_canopy_layers": 10,
"soil_layer_depth": np.array([-0.25, -1.0]),
"above_canopy_height_offset": 2.0,
"surface_layer_height": 0.1,
"n_layers": 14,
Expand Down Expand Up @@ -83,8 +83,8 @@
max_depth_of_microbial_activity = 0.8
""",
{
"canopy_layers": 3,
"soil_layers": np.array([-0.1, -0.5, -0.9]),
"n_canopy_layers": 3,
"soil_layer_depth": np.array([-0.1, -0.5, -0.9]),
"above_canopy_height_offset": 1.5,
"surface_layer_height": 0.2,
"n_layers": 8,
Expand Down Expand Up @@ -139,8 +139,8 @@ def test_CoreComponents(config, expected_layers, expected_timing, expected_const
0.25,
does_not_raise(),
dict(
canopy_layers=10,
soil_layers=np.array([-0.25, -1.0]),
n_canopy_layers=10,
soil_layer_depth=np.array([-0.25, -1.0]),
offset_height=2.0,
surface_height=0.1,
layer_roles=DEFAULT_CANOPY,
Expand All @@ -153,6 +153,9 @@ def test_CoreComponents(config, expected_layers, expected_timing, expected_const
"all_soil": np.array([12, 13]),
"active_soil": np.array([12]),
"atmosphere": np.arange(0, 12),
"filled_canopy": np.array([], dtype=np.int_),
"filled_atmosphere": np.array([0, 11]),
"flux_layers": np.array([12]),
},
soil_thickness=np.array([0.25, 0.75]),
soil_active=np.array([0.25, 0]),
Expand All @@ -170,8 +173,8 @@ def test_CoreComponents(config, expected_layers, expected_timing, expected_const
0.25,
does_not_raise(),
dict(
canopy_layers=3,
soil_layers=np.array([-0.1, -0.5, -0.9]),
n_canopy_layers=3,
soil_layer_depth=np.array([-0.1, -0.5, -0.9]),
offset_height=1.5,
surface_height=0.2,
layer_roles=ALTERNATE_CANOPY,
Expand All @@ -184,6 +187,9 @@ def test_CoreComponents(config, expected_layers, expected_timing, expected_const
"all_soil": np.array([5, 6, 7]),
"active_soil": np.array([5, 6]),
"atmosphere": np.arange(0, 5),
"filled_canopy": np.array([], dtype=np.int_),
"filled_atmosphere": np.array([0, 4]),
"flux_layers": np.array([5]),
},
soil_thickness=np.array([0.1, 0.4, 0.4]),
soil_active=np.array([0.1, 0.15, 0]),
Expand All @@ -201,8 +207,8 @@ def test_CoreComponents(config, expected_layers, expected_timing, expected_const
0.45,
does_not_raise(),
dict(
canopy_layers=3,
soil_layers=np.array(
n_canopy_layers=3,
soil_layer_depth=np.array(
[-0.1, -0.2, -0.3, -0.4, -0.5, -0.6, -0.7, -0.8, -0.9]
),
offset_height=1.5,
Expand All @@ -217,6 +223,9 @@ def test_CoreComponents(config, expected_layers, expected_timing, expected_const
"all_soil": np.arange(5, 14),
"active_soil": np.array([5, 6, 7, 8, 9]),
"atmosphere": np.arange(0, 5),
"filled_canopy": np.array([], dtype=np.int_),
"filled_atmosphere": np.array([0, 4]),
"flux_layers": np.array([5]),
},
soil_thickness=np.repeat(0.1, 9),
soil_active=np.array([0.1, 0.1, 0.1, 0.1, 0.05, 0, 0, 0, 0]),
Expand Down Expand Up @@ -258,7 +267,7 @@ def test_CoreComponents(config, expected_layers, expected_timing, expected_const
),
],
)
def test_LayerStructure(
def test_LayerStructure_init(
caplog, config_string, max_active_depth, raises, expected_values, expected_log
):
"""Test the creation and error handling of LayerStructure."""
Expand All @@ -275,10 +284,12 @@ def test_LayerStructure(
log_check(caplog=caplog, expected_log=expected_log, subset=slice(-1, None, None))

if isinstance(raises, does_not_raise):
# Check the main properties
assert layer_structure.canopy_layers == expected_values["canopy_layers"]
# Check the simple properties
assert layer_structure.n_canopy_layers == expected_values["n_canopy_layers"]
assert np.all(
np.equal(layer_structure.soil_layers, expected_values["soil_layers"])
np.equal(
layer_structure.soil_layer_depth, expected_values["soil_layer_depth"]
)
)
assert (
layer_structure.above_canopy_height_offset
Expand All @@ -288,36 +299,44 @@ def test_LayerStructure(
assert np.all(
np.equal(layer_structure.layer_roles, expected_values["layer_roles"])
)
assert np.allclose(
layer_structure.soil_layer_thickness, expected_values["soil_thickness"]
)
assert np.allclose(
layer_structure.soil_layer_active_thickness, expected_values["soil_active"]
)
assert np.all(
np.equal(np.isnan(layer_structure.lowest_canopy_filled), np.repeat(True, 9))
)

# Check the index dictionaries
assert (
layer_structure.role_indices.keys()
layer_structure._role_indices_int.keys()
== expected_values["layer_indices"].keys()
)
for ky in layer_structure.role_indices.keys():
for ky in layer_structure._role_indices_int.keys():
exp_int_index = expected_values["layer_indices"][ky]
# Do the integer indices match
assert np.all(
np.equal(
layer_structure.role_indices[ky],
expected_values["layer_indices"][ky],
)
np.equal(layer_structure._role_indices_int[ky], exp_int_index)
)
# Do the boolean indices match

bool_indices = np.repeat(False, layer_structure.n_layers)
bool_indices[exp_int_index] = True
assert np.all(
np.equal(
np.where(layer_structure.role_indices_bool[ky]),
expected_values["layer_indices"][ky],
)
np.equal(layer_structure._role_indices_bool[ky], bool_indices)
)

# Does the attribute/property API return the same as the boolean index
assert np.all(
np.equal(getattr(layer_structure, f"index_{ky}"), bool_indices)
)
assert np.allclose(
layer_structure.soil_layer_thickness, expected_values["soil_thickness"]
)
assert np.allclose(
layer_structure.soil_layer_active_thickness, expected_values["soil_active"]
)

# Check the from_template data array
template = layer_structure.from_template("a_variable")
assert isinstance(template, DataArray)
assert template.shape == (layer_structure.n_layers, layer_structure.n_cells)
assert template.shape == (layer_structure.n_layers, layer_structure._n_cells)
assert template.dims == ("layers", "cell_id")
assert template.name == "a_variable"
assert np.all(
Expand All @@ -327,10 +346,63 @@ def test_LayerStructure(
np.equal(template["layer_roles"].to_numpy(), layer_structure.layer_roles)
)
assert np.all(
np.equal(template["cell_id"].to_numpy(), np.arange(layer_structure.n_cells))
np.equal(
template["cell_id"].to_numpy(), np.arange(layer_structure._n_cells)
)
)


def test_LayerStructure_set_filled_canopy():
"""Test the set_filled_canopy_method.
This test:
* Calls the `set_filled_canopy` method with a simple canopy structure with a simple
triangle of filled canopy layers across the 9 grid cells, so that the lowest
canopy layer is never filled and the ninth cell has no filled.canopy.
* Checks that the filled canopy layers and lowest filled canopy attributes are then
as expected
* Checks that the aggregate role index has been updated with the new canopy state.
"""

from virtual_ecosystem.core.config import Config
from virtual_ecosystem.core.core_components import LayerStructure

cfg = Config(cfg_strings="[core]")
layer_structure = LayerStructure(
cfg, n_cells=9, max_depth_of_microbial_activity=0.25
)

# Run the set_filled_canopy method to populate the filled layers and update cached
# indices.
canopy_heights = np.full(
(layer_structure.n_canopy_layers, layer_structure._n_cells), np.nan
)
canopy_heights[0:8, 0:8] = np.where(np.flipud(np.tri(8)), 1, np.nan)

layer_structure.set_filled_canopy(canopy_heights=canopy_heights)

# Check the attributes have been set correctly.
assert np.allclose(
layer_structure.lowest_canopy_filled,
np.concatenate([np.arange(8, 0, -1), [np.nan]]),
equal_nan=True,
)

# Index attributes that are defined using filled_canopy
exp_filled_canopy = np.repeat(False, layer_structure.n_layers)
exp_filled_canopy[np.arange(1, 9)] = True
assert np.allclose(layer_structure.index_filled_canopy, exp_filled_canopy)

exp_filled_atmosphere = np.repeat(False, layer_structure.n_layers)
exp_filled_atmosphere[np.concatenate([[0], np.arange(1, 9), [11]])] = True
assert np.allclose(layer_structure.index_filled_atmosphere, exp_filled_atmosphere)

exp_flux_layers = np.repeat(False, layer_structure.n_layers)
exp_flux_layers[np.concatenate([np.arange(1, 9), [12]])] = True
assert np.allclose(layer_structure.index_flux_layers, exp_flux_layers)


@pytest.mark.parametrize(
"config,output,raises,expected_log_entries",
[
Expand Down
Loading

0 comments on commit a2d19e9

Please sign in to comment.