Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 102 additions & 1 deletion tests/models/soil/test_soil_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
(DEBUG, "soil model: required var 'soil_p_pool_secondary' checked"),
(DEBUG, "soil model: required var 'soil_p_pool_labile' checked"),
(DEBUG, "soil model: required var 'pH' checked"),
(DEBUG, "soil model: required var 'bulk_density' checked"),
(DEBUG, "soil model: required var 'clay_fraction' checked"),
)
POST_SETUP_LOG = (
Expand Down Expand Up @@ -511,6 +510,27 @@ def test_integrate_soil_model(
log_check(caplog, expected_log)


def test_integrate_with_nans(caplog, fixture_soil_model):
"""Test that integration fails if NaN values are in the input data."""

# Add Nan value to data and then clean up caplog
fixture_soil_model.data["pH"] = DataArray([3.3, np.nan, 5.6, 7.9], dims=["cell_id"])
caplog.clear()

with pytest.raises(ValueError):
_ = fixture_soil_model.integrate()

expected_log = (
(
ERROR,
"Soil model integration cannot proceed because the following variables "
"have unexpected NaN values: {'pH'}",
),
)

log_check(caplog, expected_log)


def test_order_independance(
dummy_carbon_data,
fixture_soil_model,
Expand Down Expand Up @@ -595,6 +615,87 @@ def test_order_independance(
assert np.allclose(output[pool_name], output_reversed[pool_name])


@pytest.mark.parametrize(
argnames=["unexpected_nans", "variable_name", "input_data"],
argvalues=[
pytest.param(
False,
"pH",
DataArray([3.3, 4.3, 5.6, 7.9], dims=["cell_id"]),
id="no NaNs",
),
pytest.param(
True,
"pH",
DataArray([3.3, np.nan, 5.6, 7.9], dims=["cell_id"]),
id="NaN",
),
],
)
def test_check_for_unexpected_nan_value_flat(
fixture_soil_model, unexpected_nans, variable_name, input_data
):
"""Test unexpected NaN checking values works for variables without layers."""

fixture_soil_model.data[variable_name] = input_data

assert unexpected_nans == fixture_soil_model.check_for_unexpected_nan_values(
var=variable_name
)


@pytest.mark.parametrize(
argnames=["unexpected_nans", "variable_name", "layer_name", "input_data"],
argvalues=[
pytest.param(
False,
"air_temperature",
"index_surface",
np.array([3.3, 4.3, 5.6, 7.9]),
id="surface, good",
),
pytest.param(
True,
"air_temperature",
"index_surface",
np.array([3.3, np.nan, 5.6, 7.9]),
id="surface, bad",
),
pytest.param(
False,
"soil_temperature",
"index_all_soil",
np.array([[3.3, 4.3, 5.6, 7.9], [23.4, 26.1, 24.4, 29.8]]),
id="soil, good",
),
pytest.param(
True,
"soil_temperature",
"index_all_soil",
np.array([[3.3, 4.3, 5.6, 7.9], [np.nan, 26.1, 24.4, 29.8]]),
id="soil, bad",
),
],
)
def test_check_for_unexpected_nan_value_layered(
fixture_soil_model,
fixture_core_components,
unexpected_nans,
variable_name,
layer_name,
input_data,
):
"""Test unexpected NaN checking values works for variables without layers."""

lyr_str = fixture_core_components.layer_structure
fixture_soil_model.data[variable_name] = lyr_str.from_template()
fixture_soil_model.data[variable_name][getattr(lyr_str, layer_name)] = input_data

assert unexpected_nans == fixture_soil_model.check_for_unexpected_nan_values(
var=variable_name
)


def test_convert_fruiting_body_production_to_rate(fixture_soil_model):
"""Test that conversion of fruiting body production to a rate works."""

Expand Down
54 changes: 48 additions & 6 deletions virtual_ecosystem/models/soil/soil_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ class SoilModel(
"soil_p_pool_secondary",
"soil_p_pool_labile",
"pH",
"bulk_density",
"clay_fraction",
),
vars_populated_by_init=(
Expand Down Expand Up @@ -128,9 +127,12 @@ class SoilModel(
"soil_p_pool_primary",
"soil_p_pool_secondary",
"soil_p_pool_labile",
"pH",
"clay_fraction",
"matric_potential",
"vertical_flow",
"soil_temperature",
"air_temperature",
"soil_moisture",
"litter_C_mineralisation_rate",
"litter_N_mineralisation_rate",
Expand Down Expand Up @@ -396,6 +398,7 @@ def integrate(self) -> dict[str, DataArray]:

Raises:
IntegrationError: When the integration cannot be successfully completed.
ValueError: If any of the variables used by the soil model have NaN values.
"""

# Find number of grid cells integration is being performed over
Expand Down Expand Up @@ -431,6 +434,22 @@ def integrate(self) -> dict[str, DataArray]:
**{name: np.array([]) for name in self.refreshed_variables},
}

# Check if any values used by the soil model integration have NaN values (these
# can stall the integration)
unexpected_nans = set()

for var in self.vars_required_for_update:
if self.check_for_unexpected_nan_values(var=var):
unexpected_nans.add(var)

if unexpected_nans:
to_raise_nan = ValueError(
"Soil model integration cannot proceed because the following "
f"variables have unexpected NaN values: {unexpected_nans}"
)
LOGGER.error(to_raise_nan)
raise to_raise_nan

# Carry out simulation
output = solve_ivp(
construct_full_soil_model,
Expand All @@ -453,12 +472,12 @@ def integrate(self) -> dict[str, DataArray]:

# Check if integration failed
if not output.success:
LOGGER.error(
"Integration of soil module failed with following message: {}".format( # noqa: UP032
str(output.message)
)
to_raise = IntegrationError(
"Integration of soil module failed with following message: "
f"{output.message!s}"
)
raise IntegrationError()
LOGGER.error(to_raise)
raise to_raise

# Construct index slices
slices = make_slices(no_cells, round(len(y0) / no_cells))
Expand All @@ -471,6 +490,29 @@ def integrate(self) -> dict[str, DataArray]:

return new_c_pools

def check_for_unexpected_nan_values(self, var: str) -> bool:
"""Check if there are unexpected NaN values in the data for a specific variable.

The soil model needs the air_temperature variable to have non-NaN values at the
soil surface, and the other layer structured variables to be defined for every
soil layer. For these variables, this function takes the appropriate subset.

Args:
var: The name of the variable being checked

Returns:
Whether the data for the variable has any unexpected NaN values.
"""

if var == "air_temperature":
subset = self.data[var].isel(layers=self.layer_structure.index_surface)
elif "layers" in self.data[var].dims:
subset = self.data[var].isel(layers=self.layer_structure.index_all_soil)
else:
subset = self.data[var]

return bool(subset.isnull().any())

def convert_fruiting_body_production_to_rate(
self, total_production: DataArray
) -> dict[str, DataArray]:
Expand Down
Loading