Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimal carbon pool model #134

Merged
merged 51 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
53b361d
Added skelton SoilCarbon class
jacobcook1995 Dec 13, 2022
893c098
Add basic pools init
jacobcook1995 Dec 13, 2022
3ae2dba
Created draft function for pool update function
jacobcook1995 Dec 13, 2022
363d0f3
Started to flesh out mineral_association function
jacobcook1995 Dec 13, 2022
fbd56a5
Wrote function for soil moisture scalar
jacobcook1995 Dec 14, 2022
02333af
Wrote function for soil temperature scalar
jacobcook1995 Dec 14, 2022
df6aab4
Added unit annotations
jacobcook1995 Dec 14, 2022
86e1929
Defined a time step for the function update
jacobcook1995 Dec 14, 2022
1a82ba1
Added error handling for bad soil carbon pool initialisation
jacobcook1995 Dec 15, 2022
ae2efd7
Merge branch 'develop' into feature/dummy_carbon
jacobcook1995 Dec 15, 2022
6cc5f3c
Added tests for scalar calculating functions
jacobcook1995 Dec 15, 2022
07b94fd
Added test for mineral association function
jacobcook1995 Dec 15, 2022
713f0af
Added test for update pools function
jacobcook1995 Dec 15, 2022
088e735
Changed qa python version to see if that fixes the 'Expected — Waitin…
jacobcook1995 Dec 15, 2022
3e0625a
Moved plant and soil module folders into new models folder
jacobcook1995 Dec 16, 2022
32fdf62
Updated test paths to match new models/soil/ directory structure
jacobcook1995 Dec 16, 2022
e0c1a2c
Switch to using np.exp and np.pi
jacobcook1995 Dec 16, 2022
7fcbbc1
Converted comments to docstrings
jacobcook1995 Dec 16, 2022
a1a0f57
Switched to returning single flux
jacobcook1995 Dec 16, 2022
5704337
Improved naming of scalar generating functions
jacobcook1995 Dec 16, 2022
9907355
Switched to using float for pool update time step
jacobcook1995 Dec 16, 2022
c2ec463
Started using np.allclose
jacobcook1995 Jan 3, 2023
ceedb05
Removed repeated dictionary accesses for temperature scalar
jacobcook1995 Jan 3, 2023
988b325
Switched to using np.any
jacobcook1995 Jan 3, 2023
ae8af43
Switched to defining attributes as docstrings rather than init docstring
jacobcook1995 Jan 9, 2023
bf18cc4
Added multiple tests for soil moisture and temp scalars
jacobcook1995 Jan 16, 2023
b0c048b
Made smaller functions for mineral association calculations
jacobcook1995 Jan 17, 2023
0cbd846
Nested binding affinity calculation
jacobcook1995 Jan 17, 2023
a8df83e
Added test for Langmuir binding coef function
jacobcook1995 Jan 17, 2023
a049f08
Added test function for Max soprtion capacity
jacobcook1995 Jan 17, 2023
820745b
Added test for equilibrium maom function
jacobcook1995 Jan 17, 2023
f84159c
Improved unit test for mineral association
jacobcook1995 Jan 17, 2023
d9e2a17
Paramaterised update_pools test
jacobcook1995 Jan 18, 2023
6b0621a
Adjusted api tree to reflect new models dir structure
jacobcook1995 Jan 18, 2023
f8bc6b0
Added details of units
jacobcook1995 Jan 18, 2023
a103e88
Added sanity checks for obviously bad but hard to spot input
jacobcook1995 Jan 18, 2023
01fdd56
Fixed minor issue with using any rather than np.any
jacobcook1995 Jan 18, 2023
9cf48f2
Switched to new api docs structure
jacobcook1995 Jan 18, 2023
e934c0a
Merge branch 'develop' into feature/dummy_carbon
jacobcook1995 Jan 24, 2023
21d5e0d
Caught pinned python version I previously missed
jacobcook1995 Jan 24, 2023
a848ff0
Merging changes from develop in and updating docs index to match new …
jacobcook1995 Jan 30, 2023
5026a22
Improved docstring style for soil.carbon module
jacobcook1995 Feb 2, 2023
1e1b759
Removed deprecated log_and_raise function from the soil.carbon module
jacobcook1995 Feb 2, 2023
4a82512
Removed unnecessary mocking from test_calculate_equilibrium_maom
jacobcook1995 Feb 2, 2023
040a1f7
Removed unneeded mocking from test_mineral_association
jacobcook1995 Feb 2, 2023
056dbdd
Removed unnecessary mocking from test_update_pools
jacobcook1995 Feb 3, 2023
7009b60
Merged in changes from develop
jacobcook1995 Feb 3, 2023
8d985d2
Fixed various problems with the docs
jacobcook1995 Feb 3, 2023
8c4d2d8
Converted dictonaries of fitting parameters to dataclasses
jacobcook1995 Feb 3, 2023
27f7498
Merge branch 'develop' into feature/dummy_carbon
jacobcook1995 Feb 3, 2023
4867277
Moved data classes to seperate constants.py script
jacobcook1995 Feb 6, 2023
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.16"
python-version: "3.9"
- uses: pre-commit/[email protected]

test:
Expand Down
17 changes: 8 additions & 9 deletions docs/source/api/soil.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ kernelspec:

# API reference for `soil` modules

This page contains the detailed documentation of the functions and classes in the
`virtual_rainforest.soil` modules.
The {mod}`~virtual_rainforest.models.soil` module is one of the component models of the
Virtual Rainforest. It is comprised of a number of submodules.

## The `virtual_rainforest.soil.model` module
Each of the soil sub-modules has its own API reference page:

```{eval-rst}
.. automodule:: virtual_rainforest.soil.model
:autosummary:
:members:
:special-members: __init__
```
* The {mod}`~virtual_rainforest.models.soil.model` submodule instantiates the SoilModel
class which consolidates the functionality of the soil module into a single class,
which the high level functions of the Virtual Rainforest can then make use of.
* The {mod}`~virtual_rainforest.models.soil.carbon` provides a model for the soil carbon
cycle.
24 changes: 24 additions & 0 deletions docs/source/api/soil/carbon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
jupytext:
cell_metadata_filter: -all
formats: md:myst
main_language: python
text_representation:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.13.8
kernelspec:
display_name: vr_python3
language: python
name: vr_python3
---

<!-- markdownlint-disable MD041 -->

```{eval-rst}
.. automodule:: virtual_rainforest.models.soil.carbon
:autosummary:
:members:
:special-members: __init__
```
24 changes: 24 additions & 0 deletions docs/source/api/soil/model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
jupytext:
cell_metadata_filter: -all
formats: md:myst
main_language: python
text_representation:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.13.8
kernelspec:
display_name: vr_python3
language: python
name: vr_python3
---

<!-- markdownlint-disable MD041 -->

```{eval-rst}
.. automodule:: virtual_rainforest.models.soil.model
:autosummary:
:members:
:special-members: __init__
```
4 changes: 3 additions & 1 deletion docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ team.
File readers <api/core/readers.md>
Core axes <api/core/axes.md>
Base Model <api/core/model.md>
Soil <api/soil.md>
Soil Overview <api/soil.md>
Soil Model <api/soil/model.md>
Soil Carbon <api/soil/carbon.md>
```

```{eval-rst}
Expand Down
256 changes: 206 additions & 50 deletions tests/test_soil_carbon.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,12 @@

from contextlib import nullcontext as does_not_raise
from logging import CRITICAL
from math import isclose

import numpy as np
import pytest

from virtual_rainforest.core.model import InitialisationError
from virtual_rainforest.models.soil.carbon import (
SoilCarbonPools,
convert_moisture_to_scalar,
convert_temperature_to_scalar,
)
from virtual_rainforest.models.soil.carbon import SoilCarbonPools

from .conftest import log_check

Expand Down Expand Up @@ -66,72 +61,233 @@ def test_soil_carbon_class(caplog, maom, lmwc, raises, expected_log_entries):
log_check(caplog, expected_log_entries)


def test_update_pools():
@pytest.mark.parametrize(
"maom,lmwc,lmwc_to_maom,dt,end_maom,end_lmwc",
[
(
[2.5, 1.7],
[0.05, 0.02],
[0.000397665, 1.178336e-5],
[2.0 / 24.0, 1.0 / 24.0],
[2.500033, 1.70000049],
[0.0499668, 0.0199995],
),
([4.5], [0.1], [0.0001434178], [0.5], [4.500071], [0.0999282]),
([0.5], [0.005], [2.80359e-7], [1.0 / 30.0], [0.500000], [0.00499999]),
],
)
def test_update_pools(mocker, maom, lmwc, lmwc_to_maom, dt, end_maom, end_lmwc):
"""Test that update_pools runs and generates the correct values."""

# Initialise soil carbon class
maom = np.array([23.0, 23.0], dtype=np.float32)
lmwc = np.array([98.0, 55.0], dtype=np.float32)
soil_carbon = SoilCarbonPools(maom, lmwc)

# Define all the required variables to run function
pH = np.array([7.0, 7.0], dtype=np.float32)
bulk_density = np.array([1350, 1350], dtype=np.float32)
percent_clay = np.array([50.0, 50.0], dtype=np.float32)
soil_moisture = np.array([0.5, 0.5], dtype=np.float32)
soil_temp = np.array([35.0, 35.0], dtype=np.float32)
dt = 2.0 / 24.0

soil_carbon.update_pools(
pH, bulk_density, soil_moisture, soil_temp, percent_clay, dt
soil_carbon = SoilCarbonPools(
maom=np.array(maom, dtype=np.float32), lmwc=np.array(lmwc, dtype=np.float32)
)

# Mock required values
mock_l_to_m = mocker.patch(
"virtual_rainforest.models.soil.carbon.SoilCarbonPools.mineral_association"
)
mock_l_to_m.return_value = np.array(lmwc_to_maom, dtype=np.float32)

soil_carbon.update_pools([], [], [], [], [], dt)

# Check that pools are correctly incremented
assert isclose(soil_carbon.maom[0].item(), 28.82632255)
assert isclose(soil_carbon.lmwc[0].item(), 92.17367553)
assert isclose(soil_carbon.maom[1].item(), 25.73223495)
assert isclose(soil_carbon.lmwc[1].item(), 52.26776504)
assert np.allclose(soil_carbon.maom, np.array(end_maom))
assert np.allclose(soil_carbon.lmwc, np.array(end_lmwc))


def test_mineral_association():
@pytest.mark.parametrize(
"maom,lmwc,temp_scalar,moist_scalar,equib_maom,Q_max,output_l_to_m",
[
(
[2.5, 1.7],
[0.05, 0.02],
[1.27113, 1.27196],
[0.750035, 0.947787],
[19900.19, 969.4813],
[2.385207e6, 1.980259e6],
[0.000397665, 1.178336e-5],
),
([4.5], [0.1], [1.27263], [0.880671], [832.6088], [647142.61], [0.0001434178]),
([0.5], [0.005], [1.26344], [0.167814], [742.4128], [2.805371e6], [2.80359e-7]),
],
)
def test_mineral_association(
mocker, maom, lmwc, temp_scalar, moist_scalar, equib_maom, Q_max, output_l_to_m
):
"""Test that mineral_association runs and generates the correct values."""

# Initialise soil carbon class
maom = np.array([23.0, 23.0], dtype=np.float32)
lmwc = np.array([98.0, 55.0], dtype=np.float32)
soil_carbon = SoilCarbonPools(maom, lmwc)

# Define all the required variables to run function
pH = np.array([7.0, 7.0], dtype=np.float32)
bulk_density = np.array([1350, 1350], dtype=np.float32)
percent_clay = np.array([50.0, 50.0], dtype=np.float32)
soil_moisture = np.array([0.5, 0.5], dtype=np.float32)
soil_temp = np.array([35.0, 35.0], dtype=np.float32)

lmwc_to_maom = soil_carbon.mineral_association(
pH, bulk_density, soil_moisture, soil_temp, percent_clay
soil_carbon = SoilCarbonPools(
maom=np.array(maom, dtype=np.float32), lmwc=np.array(lmwc, dtype=np.float32)
)

# Mock required values
mock_t_scalar = mocker.patch(
"virtual_rainforest.models.soil.carbon.convert_temperature_to_scalar"
)
mock_t_scalar.return_value = np.array(temp_scalar, dtype=np.float32)
jacobcook1995 marked this conversation as resolved.
Show resolved Hide resolved
mock_m_scalar = mocker.patch(
"virtual_rainforest.models.soil.carbon.convert_moisture_to_scalar"
)
mock_m_scalar.return_value = np.array(moist_scalar, dtype=np.float32)
mock_equib_maom = mocker.patch(
"virtual_rainforest.models.soil.carbon.calculate_equilibrium_maom"
)
mock_equib_maom.return_value = np.array(equib_maom, dtype=np.float32)
mock_Q_max = mocker.patch(
"virtual_rainforest.models.soil.carbon.calculate_max_sorption_capacity"
)
mock_Q_max.return_value = np.array(Q_max, dtype=np.float32)

# Then calculate mineral association rate
lmwc_to_maom = soil_carbon.mineral_association([], [], [], [], [])

# Check that expected values are generated
assert isclose(lmwc_to_maom[0].item(), 69.9158630)
assert isclose(lmwc_to_maom[1].item(), 32.78682708)
assert np.allclose(lmwc_to_maom, output_l_to_m)


@pytest.mark.parametrize(
"binding_coefficients,Q_max,lmwc,output_eqb_maoms",
[
(
[0.16826738, 0.02449064],
[2.385207e6, 1.980259e6],
[0.05, 0.02],
[19900.19, 969.4813],
),
([0.0128825], [647142.61], [0.1], [832.6088]),
([0.05294197], [2.805371e6], [0.005], [742.4128]),
],
)
def test_calculate_equilibrium_maom(
mocker, binding_coefficients, Q_max, lmwc, output_eqb_maoms
):
"""Test that equilibrium maom calculation works as expected."""
from virtual_rainforest.models.soil.carbon import calculate_equilibrium_maom

# Configure the mock to return specific binding coefficients
mock_binding = mocker.patch(
"virtual_rainforest.models.soil.carbon.calculate_binding_coefficient"
)
mock_binding.return_value = np.array(binding_coefficients, dtype=np.float32)

soil_Q_max = np.array(Q_max, dtype=np.float32)
soil_lmwc = np.array(lmwc, dtype=np.float32)

equib_maoms = calculate_equilibrium_maom([], soil_Q_max, soil_lmwc)
assert np.allclose(equib_maoms, np.array(output_eqb_maoms))


@pytest.mark.parametrize(
"bulk_density,percent_clay,output_capacities,raises,expected_log_entries",
[
(
[1350.0, 1800.0],
[80.0, 30.0],
[2.385207e6, 1.980259e6],
does_not_raise(),
(),
),
([1000.0], [10.0], [647142.61], does_not_raise(), ()),
([1500.0], [90.0], [2.805371e6], does_not_raise(), ()),
(
[1500.0],
[156.0],
[],
pytest.raises(ValueError),
((CRITICAL, "Relative clay content must be expressed as a percentage!"),),
),
(
[1500.0],
[-9.0],
[],
pytest.raises(ValueError),
((CRITICAL, "Relative clay content must be expressed as a percentage!"),),
),
],
)
def test_calculate_max_sorption_capacity(
caplog, bulk_density, percent_clay, output_capacities, raises, expected_log_entries
):
"""Test that max sorption capacity calculation works as expected."""
from virtual_rainforest.models.soil.carbon import calculate_max_sorption_capacity

# Check that initialisation fails (or doesn't) as expected
with raises:
soil_BD = np.array(bulk_density, dtype=np.float32)
soil_clay = np.array(percent_clay, dtype=np.float32)
max_capacities = calculate_max_sorption_capacity(soil_BD, soil_clay)

assert np.allclose(max_capacities, np.array(output_capacities))

log_check(caplog, expected_log_entries)


@pytest.mark.parametrize(
"pH,output_coefs",
[
([3.0, 7.5], [0.16826738, 0.02449064]),
([9.0], [0.0128825]),
([5.7], [0.05294197]),
],
)
def test_calculate_binding_coefficient(pH, output_coefs):
"""Test that Langmuir binding coefficient calculation works as expected."""
from virtual_rainforest.models.soil.carbon import calculate_binding_coefficient

soil_pH = np.array(pH, dtype=np.float32)
binding_coefs = calculate_binding_coefficient(soil_pH)

assert np.allclose(binding_coefs, np.array(output_coefs))


def test_convert_temperature_to_scalar():
@pytest.mark.parametrize(
"temperatures,output_scalars",
[([35.0, 37.5], [1.27113, 1.27196]), ([40.0], [1.27263]), ([25.0], [1.26344])],
)
def test_convert_temperature_to_scalar(temperatures, output_scalars):
"""Test that scalar_temperature runs and generates the correct value."""
from virtual_rainforest.models.soil.carbon import convert_temperature_to_scalar

soil_temperature = np.array([35.0, 37.5], dtype=np.float32)
soil_temperature = np.array(temperatures, dtype=np.float32)
temp_scalar = convert_temperature_to_scalar(soil_temperature)

assert isclose(temp_scalar[0].item(), 1.271131634)
assert isclose(temp_scalar[1].item(), 1.271966338)
assert np.allclose(temp_scalar, np.array(output_scalars))


def test_convert_moisture_to_scalar():
@pytest.mark.parametrize(
"moistures,output_scalars,raises,expected_log_entries",
[
([0.5, 0.7], [0.750035, 0.947787], does_not_raise(), ()),
([0.6], [0.880671], does_not_raise(), ()),
([0.2], [0.167814], does_not_raise(), ()),
(
[-0.2],
[],
pytest.raises(ValueError),
((CRITICAL, "Relative water content cannot go below zero or above one!"),),
),
(
[2.7],
[],
pytest.raises(ValueError),
((CRITICAL, "Relative water content cannot go below zero or above one!"),),
),
],
)
def test_convert_moisture_to_scalar(
caplog, moistures, output_scalars, raises, expected_log_entries
):
"""Test that scalar_moisture runs and generates the correct value."""
from virtual_rainforest.models.soil.carbon import convert_moisture_to_scalar

soil_moisture = np.array([0.5, 0.7], dtype=np.float32)
moist_scalar = convert_moisture_to_scalar(soil_moisture)
# Check that initialisation fails (or doesn't) as expected
with raises:
soil_moisture = np.array(moistures, dtype=np.float32)
moist_scalar = convert_moisture_to_scalar(soil_moisture)

assert isclose(moist_scalar[0].item(), 0.750035703)
assert isclose(moist_scalar[1].item(), 0.947787225)
assert np.allclose(moist_scalar, np.array(output_scalars))

log_check(caplog, expected_log_entries)
Loading