diff --git a/CITATION.cff b/CITATION.cff index f39a3a4..9b8d87e 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,13 +1,41 @@ # YAML 1.2 --- cff-version: "1.2.0" -title: "diffwofost" +title: "diffwofost: A differentiable implementation of the WOFOST crop growth model" +type: software authors: - - family-names: - given-names: - orcid: "" -date-released: 2025-07-11 -version: "0.1.0" + - given-names: Fakhereh (Sarah) + family-names: Alidoost + email: f.alidoost@esciencecenter.nl + affiliation: Netherlands eScience Center + orcid: 'https://orcid.org/0000-0001-8407-6472' + - given-names: Francesco + family-names: Nattino + email: f.nattino@esciencecenter.nl + affiliation: Netherlands eScience Center + orcid: 'https://orcid.org/0000-0003-3286-0139' + - given-names: Michiel + family-names: Kallenberg + email: michiel.kallenberg@wur.nl + affiliation: Wageningen University & Research + orcid: 'https://orcid.org/0000-0002-4661-6674' + - given-names: Ron + family-names: van Bree + email: ron.vanbree@wur.nl + affiliation: Wageningen University & Research + orcid: 'https://orcid.org/0009-0000-6365-6996' + - given-names: Ioannis + family-names: Athanasiadis + email: ioannis.athanasiadis@wur.nl + affiliation: Wageningen University & Research + orcid: 'https://orcid.org/0000-0003-2764-0078' + - given-names: Allard + family-names: de Wit + email: allard.dewit@wur.nl + affiliation: Wageningen University & Research + orcid: 'https://orcid.org/0000-0002-5517-6404' +date-released: 2025-09-23 +version: "0.1.1" repository-code: "https://github.com/WUR-AI/diffwofost" keywords: - "pytorch" diff --git a/LICENSE b/LICENSE index bbafe88..5f2183a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,16 +1,19 @@ -# Licensed under the EUPL, Version 1.1 or – as soon they -# will be approved by the European Commission - subsequent -# versions of the EUPL (the "Licence"); -# You may not use this work except in compliance with the -# Licence. -# You may obtain a copy of the Licence at: +**EUPL, Version 1.1** -# http://ec.europa.eu/idabc/eupl +Licensed under the EUPL, Version 1.1 or as soon they +will be approved by the European Commission - subsequent +versions of the EUPL (the "Licence"). -# Unless required by applicable law or agreed to in -# writing, software distributed under the Licence is -# distributed on an "AS IS" basis, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. -# See the Licence for the specific language governing -# permissions and limitations under the Licence. +You may not use this work except in compliance with the +Licence. +You may obtain a copy of the Licence at: + +https://interoperable-europe.ec.europa.eu/licence/european-union-public-licence-version-11-eupl +Unless required by applicable law or agreed to in +writing, software distributed under the Licence is +distributed on an "AS IS" basis, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +express or implied. + +See the Licence for the specific language governing +permissions and limitations under the Licence. diff --git a/README.md b/README.md index ca94816..c8dbc90 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ + + +[![github repo badge](https://img.shields.io/badge/github-repo-000.svg?logo=github&labelColor=gray&color=blue)](https://github.com/WUR-AI/diffWOFOST) +[![PyPI - Version](https://badge.fury.io/py/diffwofost.svg)](https://img.shields.io/pypi/v/diffwofost) +[![Python package built](https://github.com/WUR-AI/diffWOFOST/actions/workflows/build.yml/badge.svg)](https://github.com/WUR-AI/diffWOFOST/actions/workflows/build.yml) +[![Documentation built](https://github.com/WUR-AI/diffWOFOST/actions/workflows/deploy-documentation.yml/badge.svg)](https://github.com/WUR-AI/diffWOFOST/actions/workflows/deploy-documentation.yml) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=WUR-AI_diffWOFOST&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=WUR-AI_diffWOFOST) + # diffWOFOST +Logo + The python package `diffWOFOST` is a differentiable implementation of WOFOST models using [`torch`](https://pytorch.org/), allowing gradients to flow through the simulations for optimization and data assimilation. @@ -24,20 +34,14 @@ To work with notebooks, you need to install `jupyterlab`: pip install jupyterlab ``` -## Usage - -An example notebooks are provided in the `docs/notebooks` folder. +## Documentation -## PCSE +The documentation for `diffWOFOST` is available at +[https://WUR-AI.github.io/diffWOFOST](https://WUR-AI.github.io/diffWOFOST). -The python implementation of WOFOST is available at -[`PCSE`](https://pcse.readthedocs.io/en/stable/). For more information about the -models, the functional components of PCSE, and documentation have a look at the -following links: +## Acknowledgements -- [Models available in PCSE](https://pcse.readthedocs.io/en/stable/available_models.html#models-available-in-pcse) -- [The Engine](https://pcse.readthedocs.io/en/stable/reference_guide.html#the-engine) -- [PCSE source code](https://github.com/ajwdewit/pcse) -- [PCSE test data](https://github.com/ajwdewit/pcse/tree/master/tests/test_data) -- [PCSE example notebooks](https://github.com/ajwdewit/pcse_notebooks). -- [WOFOST_crop_parameters](https://github.com/ajwdewit/WOFOST_crop_parameters): each branch contains the crop parameters for a specific crop model version +The package `diffWOFOST` is developed in the +[DeltaCrop](https://research-software-directory.org/projects/deltacrop) project, a +collaboration between Wageningen University & Research and Netherlands eScience +Center. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index b4f7281..42f1732 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,6 +1,9 @@ -# Contributing guidelines +# -We welcome any kind of contribution to our software, from simple comment or question to a full fledged [pull request](https://help.github.com/articles/about-pull-requests/). Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md). +We welcome any kind of contribution to our software, from simple comment or +question to a full fledged [pull +request](https://help.github.com/articles/about-pull-requests/). Please read and +follow our [Code of Conduct](CODE_OF_CONDUCT.md). A contribution can be one of the following cases: @@ -13,56 +16,50 @@ The sections below outline the steps in each case. ## You have a question -1. use the search functionality [here](https://github.com/WUR-AI/diffwofost/issues) to see if someone already filed the same issue; +1. use the search functionality [in + issues](https://github.com/WUR-AI/diffwofost/issues) to see if someone + already filed the same issue; 2. if your issue search did not yield any relevant results, make a new issue; 3. apply the "Question" label; apply other labels when relevant. ## You think you may have found a bug -1. use the search functionality [here](https://github.com/WUR-AI/diffwofost/issues) to see if someone already filed the same issue; -1. if your issue search did not yield any relevant results, make a new issue, making sure to provide enough information to the rest of the community to understand the cause and context of the problem. Depending on the issue, you may want to include: - - the [SHA hashcode](https://help.github.com/articles/autolinked-references-and-urls/#commit-shas) of the commit that is causing your problem; - - some identifying information (name and version number) for dependencies you're using; - - information about the operating system; -1. apply relevant labels to the newly created issue. +1. use the search functionality [in + issues](https://github.com/WUR-AI/diffwofost/issues) to see if someone + already filed the same issue; +2. if your issue search did not yield any relevant results, make a new issue, + making sure to provide enough information to the rest of the community to + understand the cause and context of the problem. ## You want to make some kind of change to the code base -1. (**important**) announce your plan to the rest of the community *before you start working*. This announcement should be in the form of a (new) issue; -1. (**important**) wait until some kind of consensus is reached about your idea being a good idea; -1. if needed, fork the repository to your own Github profile and create your own feature branch off of the latest main commit. While working on your feature branch, make sure to stay up to date with the main branch by pulling in changes, possibly from the 'upstream' repository (follow the instructions [here](https://help.github.com/articles/configuring-a-remote-for-a-fork/) and [here](https://help.github.com/articles/syncing-a-fork/)); -1. install dependencies (see the [development documentation](README.dev.md#development_install)); -1. make sure the existing tests still work by running ``pytest``; -1. add your own tests (if necessary); -1. update or expand the documentation; +1. (**important**) announce your plan to the rest of the community *before you + start working*. This announcement should be in the form of a (new) issue; +2. (**important**) wait until some kind of consensus is reached about your idea being a good idea; +3. follow the instruction in [developer_guide.md](developer_guide.md). -1. [push](http://rogerdudler.github.io/git-guide/) your feature branch to (your fork of) the diffwofost repository on GitHub; -1. create the pull request, e.g. following the instructions [here](https://help.github.com/articles/creating-a-pull-request/). - -In case you feel like you've made a valuable contribution, but you don't know how to write or run tests for it, or how to generate the documentation: don't let this discourage you from making the pull request; we can help you! Just go ahead and submit the pull request, but keep in mind that you might be asked to append additional commits to your pull request. +In case you feel like you've made a valuable contribution, but you don't know +how to write or run tests for it, or how to generate the documentation: don't +let this discourage you from making the pull request; we can help you! Just go +ahead and submit an issue and ask your questions. ## You want to make a new release of the code base To create a release you need write permission on the repository. -1. Check the author list in [`CITATION.cff`](CITATION.cff) -1. Bump the version using `bump-my-version bump `. For example, `bump-my-version bump major` will increase major version numbers everywhere it's needed (code, meta, etc.) in the repo. Alternatively the version can be manually changed in diffwofost/__init__.py, pyproject.toml, CITATION.cffand docs/conf.py (and other places it was possibly added). - -1. Go to the [GitHub release page](https://github.com/WUR-AI/diffwofost/releases) -1. Press draft a new release button -1. Fill version, title and description field -1. Press the Publish Release button - - - -Also a Zenodo entry will be made for the release with its own DOI. +1. Check the author list in `CITATION.cff`in + the root of the repository. +2. Bump the version. The version can be manually changed in `pyproject.toml` in + the root of the repository. Follow [Semantic Versioning](https://semver.org/) + principles. Also, update `__version__` variable in `diffwofost/__init__.py` to the + same version. +3. Go to the [GitHub release + page](https://github.com/WUR-AI/diffwofost/releases). Press draft a new + release button. Fill version, title and description field. Press the Publish + Release button. For this package, the zenodo integration is enabled, so a new + DOI will be created automatically. +4. This software automatically publish to PyPI using a release or publish + workflow. Wait until [PyPi publish + workflow](https://github.com/WUR-AI/diffwofost/actions/workflows/python-publish.yml) + has completed and verify new release is on + [PyPi](https://pypi.org/project/matchms/#history) diff --git a/docs/README.dev.md b/docs/README.dev.md deleted file mode 100644 index b365d11..0000000 --- a/docs/README.dev.md +++ /dev/null @@ -1,172 +0,0 @@ -# `diffwofost` developer documentation - -If you're looking for user documentation, go [here](README.md). - -## Development install - -```shell -# Create a virtual environment, e.g. with -python -m venv env - -# activate virtual environment -source env/bin/activate - -# make sure to have a recent version of pip and setuptools -python -m pip install --upgrade pip setuptools - -# (from the project root directory) -# install diffwofost as an editable package -python -m pip install --no-cache-dir --editable . -# install development dependencies -python -m pip install --no-cache-dir --editable .[dev] -# install documentation dependencies only -python -m pip install --no-cache-dir --editable .[docs] -``` - -Afterwards check that the install directory is present in the `PATH` environment variable. - -## Running the tests - -There are two ways to run tests. - -The first way requires an activated virtual environment with the development tools installed: - -```shell -pytest -v -``` - -### Test coverage - -In addition to just running the tests to see if they pass, they can be used for coverage statistics, i.e. to determine how much of the package's code is actually executed during tests. -In an activated virtual environment with the development tools installed, inside the package directory, run: - -```shell -coverage run -``` - -This runs tests and stores the result in a `.coverage` file. -To see the results on the command line, run - -```shell -coverage report -``` - -`coverage` can also generate output in HTML and other formats; see `coverage help` for more information.## Running linters locally - -For linting and sorting imports we will use [ruff](https://beta.ruff.rs/docs/). Running the linters requires an -activated virtual environment with the development tools installed. - -```shell -# linter -ruff check . - -# linter with automatic fixing -ruff check . --fix -``` - -To fix readability of your code style you can use [yapf](https://github.com/google/yapf) - -## Generating documentation page - -- Install the required dependencies as: - - ```bash - cd FOWT-ML - pip install -e .[docs] - ``` - -- Build the documentation as: - - ```bash - mkdocs build - ``` - -- Preview the documentation as: - - ```bash - mkdocs serve - ``` - -Click on the link provided in the terminal to view the documentation page. - -## Versioning - -Bumping the version across all files is done with [bump-my-version](https://github.com/callowayproject/bump-my-version), e.g. - -```shell -bump-my-version bump major # bumps from e.g. 0.3.2 to 1.0.0 -bump-my-version bump minor # bumps from e.g. 0.3.2 to 0.4.0 -bump-my-version bump patch # bumps from e.g. 0.3.2 to 0.3.3 -``` - -## Making a release - -This section describes how to make a release in 3 parts: - -1. preparation -1. making a release on PyPI -1. making a release on GitHub - -### (1/3) Preparation - - -1. Verify that the information in [`CITATION.cff`](CITATION.cff) is correct. -1. Make sure the [version has been updated](#versioning). -1. Run the unit tests with `pytest -v` - -### (2/3) PyPI - -In a new terminal: - -```shell -# OPTIONAL: prepare a new directory with fresh git clone to ensure the release -# has the state of origin/main branch -cd $(mktemp -d diffwofost.XXXXXX) -git clone git@github.com:WUR-AI/diffwofost . - -# make sure to have a recent version of pip and the publishing dependencies -python -m pip install --upgrade pip -python -m pip install .[publishing] - -# create the source distribution and the wheel -python -m build - -# upload to test pypi instance (requires credentials) -python -m twine upload --repository testpypi dist/* -``` - -Visit -[https://test.pypi.org/project/diffwofost](https://test.pypi.org/project/diffwofost) -and verify that your package was uploaded successfully. Keep the terminal open, we'll need it later. - -In a new terminal, without an activated virtual environment or an env directory: - -```shell -cd $(mktemp -d diffwofost-test.XXXXXX) - -# prepare a clean virtual environment and activate it -python -m venv env -source env/bin/activate - -# make sure to have a recent version of pip and setuptools -python -m pip install --upgrade pip - -# install from test pypi instance: -python -m pip -v install --no-cache-dir \ ---index-url https://test.pypi.org/simple/ \ ---extra-index-url https://pypi.org/simple diffwofost -``` - -Check that the package works as it should when installed from pypitest. - -Then upload to pypi.org with: - -```shell -# Back to the first terminal, -# FINAL STEP: upload to PyPI (requires credentials) -python -m twine upload dist/* -``` - -### (3/3) GitHub - -Don't forget to also make a [release on GitHub](https://github.com/WUR-AI/diffwofost/releases/new).GitHub-Zenodo integration will also trigger Zenodo into making a snapshot of your repository and sticking a DOI on it. \ No newline at end of file diff --git a/docs/acknowledgements.md b/docs/acknowledgements.md new file mode 100644 index 0000000..a232d62 --- /dev/null +++ b/docs/acknowledgements.md @@ -0,0 +1 @@ +{% include-markdown "../README.md" start="## Acknowledgements" heading-offset=1%} \ No newline at end of file diff --git a/docs/api_reference.md b/docs/api_reference.md new file mode 100644 index 0000000..ebb4a1b --- /dev/null +++ b/docs/api_reference.md @@ -0,0 +1,20 @@ +--- +hide: +- navigation +--- +# + +## **Crop modules** + +!!! note + At the moment only two modules of `leaf_dynamics` and `root_dynamics` are + differentiable w.r.t two parameters of `SPAN` and `TDWI`. But the package is under + continuous development. So make sure that you install the latest version. + +::: diffwofost.physical_models.crop.leaf_dynamics.WOFOST_Leaf_Dynamics + +::: diffwofost.physical_models.crop.root_dynamics.WOFOST_Root_Dynamics + +## **Utility (under development)** + +::: diffwofost.physical_models.utils.EngineTestHelper diff --git a/docs/citation.md b/docs/citation.md new file mode 100644 index 0000000..610e7f1 --- /dev/null +++ b/docs/citation.md @@ -0,0 +1 @@ +zenodo doi will be added. \ No newline at end of file diff --git a/docs/developer_guide.md b/docs/developer_guide.md new file mode 100644 index 0000000..400da8f --- /dev/null +++ b/docs/developer_guide.md @@ -0,0 +1,100 @@ +# + +If you are a developer wanting to contribute to the diffwofost package, this +guide will help you get started. First check out the contribution guidelines in +[Contributing guide](CONTRIBUTING.md) and the [Project setup](project_setup.md) +to get familiar with the package structure and development practices. + +## Installation in development mode + +To install the package in development mode, you need to clone the source code +and install the package in development mode: + +```bash +git clone https://github.com/WUR-AI/diffWOFOST.git +cd diffWOFOST +pip install -e .[dev] ## install with development dependencies +pip install -e .[docs] ## install with documentation dependencies +``` + +## GitHub collaboration workflow + +We use a GitHub collaboration workflow based on feature branches and pull +requests. When starting adding a new feature or fixing a bug, create a new +branch from `main` branch. When your changes are ready, create a pull request to +merge your changes back into `main` branch. Make sure to ask for at least one +review from another team member before merging your pull request. + +## Running the tests + +- Tests should be put in the `tests` folder. +- The testing framework used is [PyTest](https://pytest.org) +- The project uses [GitHub action workflows](https://docs.github.com/en/actions) + to automatically run tests on GitHub infrastructure against multiple Python + versions. Workflows can be found in `.github/workflows` directory. +- [Relevant section in the + guide](https://guide.esciencecenter.nl/#/best_practices/language_guides/python?id=testing) +- To run the tests locally, you need to make sure that you have installed the +development dependencies as described in the [Installation in development +mode](#installation-in-development-mode) section. +Then, inside the package directory, run: + +```bash +pytest -v +``` + +to run all tests with verbose output. To run an individual test file, run: + +```bash +pytest -v tests/test_my_module.py +``` + +### Linters + +For linting and sorting imports we will use [ruff](https://beta.ruff.rs/docs/). +Running the linters requires an activated virtual environment with the +development tools installed. + +```shell +# linter +ruff check . + +# linter with automatic fixing +ruff check . --fix + +# check formatting only +ruff format --check . --diff +``` + +## Documentation page + +- Documentation should be put in the `docs/` directory. +- We recommend writing the documentation using Google style docstrings. +- The documentation is set up with the [MkDocs](https://www.mkdocs.org/). + - `.mkdocs.yml` is the [MkDocs](https://www.mkdocs.org/) configuration file. When MkDocs is building the documentation this package and its development dependencies are installed so the API reference can be rendered. +- Make sure you have installed the documentation dependencies as described in the +[Installation in development mode](#installation-in-development-mode) section. +Then, inside the package directory, run: + +```bash + +# Build the documentation +mkdocs build + +# Preview the documentation +mkdocs serve + +``` + +Click on the link provided in the terminal to view the documentation page. + +## Coding style conventions and code quality + +- [Relevant section in the NLeSC guide](https://guide.esciencecenter.nl/#/best_practices/language_guides/python?id=coding-style-conventions). + +## Continuous code quality + +[Sonarcloud](https://sonarcloud.io/) is used to perform quality analysis and code coverage report + +- `sonar-project.properties` is the SonarCloud [configuration](https://docs.sonarqube.org/latest/analysis/analysis-parameters/) file +- `.github/workflows/sonarcloud.yml` is the GitHub action workflow which performs the SonarCloud analysis \ No newline at end of file diff --git a/docs/download_button/main.html b/docs/download_button/main.html new file mode 100644 index 0000000..f223d92 --- /dev/null +++ b/docs/download_button/main.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block content %} +{% if page.nb_url %} + + {% include ".icons/material/download.svg" %} + +{% endif %} + +{{ super() }} +{% endblock content %} \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..57b0c8b --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,26 @@ +# + +## Optimization with diffWOFOST + +We provide an example notebook showing optimization of models' parameters with +`diffWOFOST`. To get familiar with the concepts and implementation, check out +[`Introduction`](./index.md) in the documentation. + +| Open the notebook | Access the source | View the notebook | +|-------|------------|---------------| +| [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)][colab_link] | [![Access the source code](https://img.shields.io/badge/GitHub_Repository-000.svg?logo=github&labelColor=gray&color=blue)][source_link] | [![here](https://img.shields.io/badge/View_Notebook-orange.svg?logo=jupyter&labelColor=gray)](./notebooks/optimization.ipynb) | + +!!! note + When calculating gradients, it is important to ensure that the predicted + physical parameters are within realistic bounds regarding the crop and + environmental conditions. + + Also, when calculating gradients of an output w.r.t. parameters, it would be + good to know in advance how the parameters in a model influence the outputs. + If a parameter has little to no influence on an output, the gradient of the + output w.r.t the parameter will be close to zero, which may not provide + useful information for optimization. + +[colab_link]: https://colab.research.google.com/github/WUR-AI/diffWOFOST/blob/main/docs/notebooks/optimization.ipynb + +[source_link]: https://github.com/WUR-AI/diffWOFOST/blob/main/docs/notebooks/optimization.ipynb diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..11e5a0f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,111 @@ +# + +{% include-markdown "../README.md" end="# diffWOFOST" %} + +## diffWOFOST: Differentiable WOFOST + +The package diffWOFOST contains a differentiable implementation of the WOFOST +crop growth models using [`pytorch`](https://pytorch.org/) and +[`PCSE`](https://pcse.readthedocs.io/en/stable/index.html). The implementation +allows for automatic differentiation, enabling gradient-based optimization, +sensitivity analysis and data assimilation. + +In PCSE, WOFOST models are categorized based on +`version, productionlevel, waterbalance, nitrogenbalance` and each model +contains a set of elements e.g. crop and soil models, see [models available in +PCSE](https://pcse.readthedocs.io/en/stable/available_models.html#models-available-in-pcse) +and [PCSE +Engine](https://pcse.readthedocs.io/en/stable/reference_guide.html#the-engine). + +In diffWOFOST, each element is implemented as a differentiable module, using +[torch.Tensor](https://docs.pytorch.org/docs/stable/tensors.html), allowing for +end-to-end differentiation of the entire WOFOST model. To develop a +differentiable module, we check for look-up tables, hard thresholds, and +mathematical operations, and replace them with differentiable alternatives. + +In addition to differentiability, the implementation also focuses on efficiency, +by leveraging vectorized operations. This is particularly important for +large-scale simulations and training workflows, where the computational cost +can be significant. + +## Hybrid modelling with diffWOFOST + +Hybrid modelling, referring to a combination of process-based and +machine-learning modelling, has recently emerged as a promising line of research +to harness the strengths of both approaches while mitigating their respective +weaknesses, see [Integrating Scientific Knowledge with Machine Learning for +Engineering and Environmental Systems]( +https://doi.org/10.48550/arXiv.2003.04919 ) and [Deep learning and process +understanding for data-driven Earth system +science](https://doi.org/10.1038/s41586-019-0912-1). + +The approach where an machine learning (ML) model predicts physical parameters, which +are then used in a physics-based model, and combines both in a hybrid +architecture, is a state-of-the-art approach and is known under various names, +see [Scientific Machine +Learning](https://sciml.wur.nl/reviews/sciml/sciml.html). The mathematics would +be: + +$$ +\frac{\partial \text{loss}}{\partial \text{(ML model weights)}} = \frac{\partial \text{loss}}{\partial \text{(physics-based model output)}} \cdot \frac{\partial \text{(physics-based model output)}}{\partial \text{(physics-based model parameters)}} \cdot \frac{\partial \text{(physics-based model parameters)}}{\partial \text{(ML model weights)}} +$$ + +And code wise, this would look like: + +```python +import torch.nn as nn + +# Step 1: ML model that outputs physical parameters e.g. LSTM +class MLModel(nn.Module): + def __init__(self, input_size, hidden_size, num_physical_params): + super().__init__() + self.lstm = nn.LSTM(input_size=input_size, hidden_size=hidden_size, batch_first=True) + self.linear = nn.Linear(hidden_size, num_physical_params) + + def forward(self, x): + lstm_out, _ = self.lstm(x) + physical_params = self.linear(lstm_out[:, -1, :]) + return physical_params + +# Step 2: Physical model i.e. a differentiable WOFOST model e.g. Wofost72_PP +class PhysicalModel(nn.Module): + def __init__(self, dt): + super().__init__() + + def forward(self, params): + model = Wofost72_PP(params, ...) # this is differentiable version + model.run_till_terminate() # finish the simulation + output = model.get_output() + return output + +# Step 3: Hybrid model integrating ML and physical model +class HybridModel(nn.Module): + def __init__(self, input_size, hidden_size, num_physical_params): + super().__init__() + self.ml_model = MLModel(input_size, hidden_size, num_physical_params) + self.physical_model = PhysicalModel() + + def forward(self, x): + physical_params = self.ml_model(x) + output = self.physical_model(physical_params) + return output, physical_params +``` + +## Code structure (under development) + +The package is structured as follows: + +```bash +├── physical_models/ + ├── crop/ # differentiable implementation of each crop model + │ ├── leaf_dynamics.py + │ ├── root_dynamics.py + │ ├── ... + ├── soil/ + ├── utils.py # helpers +``` + +!!! note + At the moment only two modules of `leaf_dynamics` and `root_dynamics` are + differentiable w.r.t two parameters of `SPAN` and `TDWI`. But the package is under + continuous development. So make sure that you install the latest version. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..47598ef --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,25 @@ +# Installation + +To install the package, use pip: + +```bash +pip install diffwofost +``` + +if you want to run notebooks locally, you need to install `jupyterlab`: + +```bash +pip install jupyterlab +``` + +If you are a contributor, clone the source code and install the package in +development mode: + +```bash +git clone https://github.com/WUR-AI/diffWOFOST.git +cd diffWOFOST +pip install -e .[dev] +``` + +If you are a contributor, follow the instructions in the `How to Contribute` in +the documentation. diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..92414a9 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,3 @@ +# + +{% include-markdown "../LICENSE" %} diff --git a/docs/project_setup.md b/docs/project_setup.md index 4830b60..bbe1d9a 100644 --- a/docs/project_setup.md +++ b/docs/project_setup.md @@ -1,32 +1,32 @@ -# Project Setup Here we provide some details about the project setup. Most of the choices are -explained in the [guide](https://guide.esciencecenter.nl). Links to the relevant -sections are included below. Feel free to remove this text when the development -of the software package takes off. +explained in the [Turing Way: Guide for Reproducible +Research](https://book.the-turing-way.org/reproducible-research/reproducible-research/). -For a quick reference on software development, we refer to [the software guide -checklist](https://guide.esciencecenter.nl/#/best_practices/checklist). -## Python versions +## Repository structure -This repository is set up with Python versions: +The repository has the following structure: -- 3.10 -- 3.11 -- 3.12 +```bash -Add or remove Python versions based on project requirements. See [the -guide](https://guide.esciencecenter.nl/#/best_practices/language_guides/python) -for more information about Python versions. +├── .github/ # GitHub specific files such as workflows +├── docs/ # Documentation source files +├── my_package/ # Main package code +├── tests/ # Test code +├── .gitignore # Git ignore file +├── CITATION.cff # Citation file +├── LICENSE # License file +├── README.md # User documentation +├── pyproject.toml # Project configuration file and dependencies +├── mkdocs.yml # MkDocs configuration file + +``` ## Package management and dependencies -You can use either pip or conda for installing dependencies and package -management. This repository does not force you to use one or the other, as -project requirements differ. For advice on what to use, please check [the -relevant section of the -guide](https://guide.esciencecenter.nl/#/best_practices/language_guides/python?id=dependencies-and-package-management). +You can use pip for installing dependencies and package +management. - Runtime dependencies should be added to `pyproject.toml` in the `dependencies` list under `[project]`. @@ -36,73 +36,28 @@ guide](https://guide.esciencecenter.nl/#/best_practices/language_guides/python?i ## Packaging/One command install -You can distribute your code using PyPI. [The -guide](https://guide.esciencecenter.nl/#/best_practices/language_guides/python?id=building-and-packaging-code) -can help you decide which tool to use for packaging. - -## Testing and code coverage - -- Tests should be put in the `tests` folder. -- The `tests` folder contains: - - Example tests that you should replace with your own meaningful tests (file: `test_my_module.py`) -- The testing framework used is [PyTest](https://pytest.org) - - [PyTest introduction](https://pythontest.com/pytest-book/) - - PyTest is listed as a development dependency - - This is configured in `pyproject.toml` -- The project uses [GitHub action workflows](https://docs.github.com/en/actions) to automatically run tests on GitHub infrastructure against multiple Python versions - - Workflows can be found in [`.github/workflows`](.github/workflows/) -- [Relevant section in the guide](https://guide.esciencecenter.nl/#/best_practices/language_guides/python?id=testing) - -## Documentation - -- Documentation should be put in the [`docs/`](docs/) directory. -- We recommend writing the documentation using Google style docstrings. -- The documentation is set up with the [MkDocs](https://www.mkdocs.org/). - - `.mkdocs.yml` is the [MkDocs](https://www.mkdocs.org/) configuration file. When MkDocs is building the documentation this package and its development dependencies are installed so the API reference can be rendered. - -## Coding style conventions and code quality - -- [Relevant section in the NLeSC guide](https://guide.esciencecenter.nl/#/best_practices/language_guides/python?id=coding-style-conventions) and [README.dev.md](README.dev.md). - -## Continuous code quality - -[Sonarcloud](https://sonarcloud.io/) is used to perform quality analysis and code coverage report - -- `sonar-project.properties` is the SonarCloud [configuration](https://docs.sonarqube.org/latest/analysis/analysis-parameters/) file -- `.github/workflows/sonarcloud.yml` is the GitHub action workflow which performs the SonarCloud analysis +You can distribute your code using PyPI. This can be done automatically using +GitHub workflows, see `.github/`. ## Package version number -- We recommend using [semantic versioning](https://guide.esciencecenter.nl/#/best_practices/releases?id=semantic-versioning). -- For convenience, the package version is stored in a single place: `pyproject.toml` under the `tool.bumpversion` header. -- Don't forget to update the version number before [making a release](https://guide.esciencecenter.nl/#/best_practices/releases)! - -## Logging - -- We recommend using the logging module for getting useful information from your module (instead of using print). -- The project is set up with a logging example. -- [Relevant section in the guide](https://guide.esciencecenter.nl/#/best_practices/language_guides/python?id=logging) - - +- We recommend using [semantic versioning](https://semver.org/). +- For convenience, the package version is stored in a single place: `pyproject.toml`. +- Don't forget to update the version number before [making a release](./CONTRIBUTING.md)! Also, update `__version__` variable in `diffwofost/__init__.py` to the + same version. ## CITATION.cff - To allow others to cite your software, add a `CITATION.cff` file - It only makes sense to do this once there is something to cite (e.g., a software release with a DOI). -- Follow the [making software citable](https://guide.esciencecenter.nl/#/citable_software/making_software_citable) section in the guide. +- Follow the [Software Citation with CITATION.cff](https://book.the-turing-way.org/communication/citable/citable-cff/) section in the Turing Way guide. ## CODE_OF_CONDUCT.md - Information about how to behave professionally -- [Relevant section in the guide](https://guide.esciencecenter.nl/#/best_practices/documentation?id=code-of-conduct) +- To know more, read [Turing Way guide on Code of Conduct](https://book.the-turing-way.org/community-handbook/coc/) ## CONTRIBUTING.md - Information about how to contribute to this software package -- [Relevant section in the guide](https://guide.esciencecenter.nl/#/best_practices/documentation?id=contribution-guidelines) - - -## NOTICE - -- List of attributions of this project and Apache-license dependencies -- [Relevant section in the guide](https://guide.esciencecenter.nl/#/best_practices/licensing?id=notice) +- To know more, read [Turing Way guide on Contributing](https://book.the-turing-way.org/reproducible-research/code-documentation/code-documentation-project/#contributing-guidelines) diff --git a/docs/references.md b/docs/references.md new file mode 100644 index 0000000..5464fdb --- /dev/null +++ b/docs/references.md @@ -0,0 +1,11 @@ +# + +Here are some useful references related to `WOFOST` and `PCSE`. If you want to +cite the `diffWOFOST` package in your research, please use the software doi. + +- [WOFOST (WOrld FOod STudies)](https://www.wur.nl/en/research-results/research-institutes/environmental-research/facilities-tools/software-models-and-databases/wofost.htm) +- [A gentle introduction to WOFOST](https://www.wur.nl/en/show/a-gentle-introduction-to-wofost.htm) +- [PCSE/ WOFOST documentation](http://pcse.readthedocs.io/) +- [Jupyter notebooks demonstrating the use of PCSE/WOFOST](https://github.com/ajwdewit/pcse_notebooks) +- [PCSE GitHub repository](https://github.com/ajwdewit/pcse) +- [DeltaCrop project](https://research-software-directory.org/projects/deltacrop) diff --git a/docs/run_model.md b/docs/run_model.md new file mode 100644 index 0000000..79fe7df --- /dev/null +++ b/docs/run_model.md @@ -0,0 +1,32 @@ +# + +# How to run a model with PCSE + +To get familiar with WOFOST models and how to run the models, we recommend to +first check out the [PCSE +documentation](https://pcse.readthedocs.io/en/stable/index.html) and explore the +notebooks [01 Getting Started with +PCSE.ipynb](https://github.com/ajwdewit/pcse_notebooks/blob/master/01%20Getting%20Started%20with%20PCSE.ipynb) +and [02 Running with custom input data.ipynb +](https://github.com/ajwdewit/pcse_notebooks/blob/master/02%20Running%20with%20custom%20input%20data.ipynb). + +In a nutshell, we can run a model, for example, `leaf_dynamics` using diffWOFOST as: + +```python +from diffwofost.physical_models.utils import EngineTestHelper + +# create the model +model = EngineTestHelper( + crop_parameters_provider, # this provides the crop parameters + weather_data_provider, + agromanagement_provider, + leaf_dynamics_config_file, # this where the differentiable model is specified + external_states, # any external states if needed +) + +# run the simulation with a fixed time step of one day +model.run_till_terminate() + +# get the output +results = model.get_output() +``` diff --git a/mkdocs.yml b/mkdocs.yml index 4a26e08..1ef6461 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,15 +3,31 @@ repo_url: https://github.com/WUR-AI/diffwofost repo_name: diffWOFOST nav: - - Introduction: intro.md - - Contributing guide: CONTRIBUTING.md + - Introduction: + - diffWOFOST: index.md + - Citation: citation.md + - License: license.md + - Acknowledgements: acknowledgements.md + - References: references.md + - Getting started: + - Installation: installation.md + - How to run a model: run_model.md + - Example notebooks: examples.md + - How to Contribute: + - Contributing guide: CONTRIBUTING.md + - Developer guide: developer_guide.md + - API Reference: api_reference.md theme: name: material + custom_dir: docs/download_button + logo: logo/diffwofost.png features: - navigation.instant - navigation.tabs - navigation.tabs.sticky + - content.code.copy + - content.tabs icon: repo: fontawesome/brands/github-alt @@ -21,7 +37,7 @@ theme: toggle: icon: material/weather-sunny name: Switch to dark mode - primary: light blue + primary: green accent: blue # Palette toggle for dark mode @@ -29,18 +45,48 @@ theme: toggle: icon: material/weather-night name: Switch to light mode - primary: blue grey + primary: green accent: teal plugins: - include-markdown - search +- mkdocs-jupyter: + include_source: True +- mkdocstrings: + handlers: + python: + paths: [src] + options: + docstring_style: google + docstring_options: + ignore_init_summary: true + merge_init_into_class: true + docstring_section_style: list + show_submodules: true + show_root_heading: true + show_source: true + heading_level: 3 + relative_crossrefs: true + parameter_headings: false + separate_signature: true + show_bases: true + show_signature_annotations: true + show_symbol_type_heading: true + signature_crossrefs: true + summary: true + backlinks: tree + scoped_crossrefs: true + members_order: source markdown_extensions: - pymdownx.highlight: anchor_linenums: true + - pymdownx.arithmatex: + generic: true - pymdownx.superfences - pymdownx.details + - pymdownx.emoji - admonition - pymdownx.tabbed: alternate_style: true @@ -48,3 +94,7 @@ markdown_extensions: extra: generator: false + math: mathjax + +extra_javascript: + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js diff --git a/pyproject.toml b/pyproject.toml index c82fa27..38a539c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ docs = [ "mkdocs", "mkdocs-material", "mkdocs-jupyter", - "mkdocstrings[python]", + "mkdocstrings[python]>=0.18", "mkdocs-gen-files", "mkdocs-include-markdown-plugin", "jupyterlab", diff --git a/src/diffwofost/__init__.py b/src/diffwofost/__init__.py index d4bb9a5..52f9e0c 100644 --- a/src/diffwofost/__init__.py +++ b/src/diffwofost/__init__.py @@ -1,9 +1,18 @@ """Documentation about diffwofost.""" import logging +from diffwofost.physical_models import utils +from diffwofost.physical_models.crop import leaf_dynamics +from diffwofost.physical_models.crop import root_dynamics logging.getLogger(__name__).addHandler(logging.NullHandler()) __author__ = "" __email__ = "" -__version__ = "0.1.0" +__version__ = "0.1.1" + +__all__ = [ + "leaf_dynamics", + "root_dynamics", + "utils", +] diff --git a/docs/intro.md b/src/diffwofost/physical_models/__init__.py similarity index 100% rename from docs/intro.md rename to src/diffwofost/physical_models/__init__.py diff --git a/src/diffwofost/physical_models/crop/__init__.py b/src/diffwofost/physical_models/crop/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/diffwofost/physical_models/crop/leaf_dynamics.py b/src/diffwofost/physical_models/crop/leaf_dynamics.py index 8e24d1a..219da47 100644 --- a/src/diffwofost/physical_models/crop/leaf_dynamics.py +++ b/src/diffwofost/physical_models/crop/leaf_dynamics.py @@ -1,10 +1,14 @@ """Leaf dynamics for the WOFOST crop model.""" +import datetime import torch from pcse.base import ParamTemplate from pcse.base import RatesTemplate from pcse.base import SimulationObject from pcse.base import StatesTemplate +from pcse.base.parameter_providers import ParameterProvider +from pcse.base.variablekiosk import VariableKiosk +from pcse.base.weather import WeatherDataContainer from pcse.decorators import prepare_rates from pcse.decorators import prepare_states from pcse.traitlets import Any @@ -29,89 +33,73 @@ class WOFOST_Leaf_Dynamics(SimulationObject): Senescense of the leaves can occur as a result of physiological age, drought stress or self-shading. - *Simulation parameters* (provide in cropdata dictionary) - - ======= ============================================= ======= ============ - Name Description Type Unit - ======= ============================================= ======= ============ - RGRLAI Maximum relative increase in LAI. SCr ha ha-1 d-1 - SPAN Life span of leaves growing at 35 Celsius SCr |d| - TBASE Lower threshold temp. for ageing of leaves SCr |C| - PERDL Max. relative death rate of leaves due to SCr - water stress - TDWI Initial total crop dry weight SCr |kg ha-1| - KDIFTB Extinction coefficient for diffuse visible TCr - light as function of DVS - SLATB Specific leaf area as a function of DVS TCr |ha kg-1| - ======= ============================================= ======= ============ - - *State variables* - - ======= ================================================= ==== ============ - Name Description Pbl Unit - ======= ================================================= ==== ============ - LV Leaf biomass per leaf class N |kg ha-1| - SLA Specific leaf area per leaf class N |ha kg-1| - LVAGE Leaf age per leaf class N |d| - LVSUM Sum of LV N |kg ha-1| - LAIEM LAI at emergence N - - LASUM Total leaf area as sum of LV*SLA, N - - not including stem and pod area N - LAIEXP LAI value under theoretical exponential growth N - - LAIMAX Maximum LAI reached during growth cycle N - - LAI Leaf area index, including stem and pod area Y - - WLV Dry weight of living leaves Y |kg ha-1| - DWLV Dry weight of dead leaves N |kg ha-1| - TWLV Dry weight of total leaves (living + dead) Y |kg ha-1| - ======= ================================================= ==== ============ - - - *Rate variables* - - ======= ================================================= ==== ============ - Name Description Pbl Unit - ======= ================================================= ==== ============ - GRLV Growth rate leaves N |kg ha-1 d-1| - DSLV1 Death rate leaves due to water stress N |kg ha-1 d-1| - DSLV2 Death rate leaves due to self-shading N |kg ha-1 d-1| - DSLV3 Death rate leaves due to frost kill N |kg ha-1 d-1| - DSLV Maximum of DLSV1, DSLV2, DSLV3 N |kg ha-1 d-1| - DALV Death rate leaves due to aging. N |kg ha-1 d-1| - DRLV Death rate leaves as a combination of DSLV and N |kg ha-1 d-1| - DALV - SLAT Specific leaf area for current time step, N |ha kg-1| - adjusted for source/sink limited leaf expansion - rate. - FYSAGE Increase in physiological leaf age N - - GLAIEX Sink-limited leaf expansion rate (exponential N |ha ha-1 d-1| - curve) - GLASOL Source-limited leaf expansion rate (biomass N |ha ha-1 d-1| - increase) - ======= ================================================= ==== ============ - - - *External dependencies:* - - ======== ============================== =============================== =========== - Name Description Provided by Unit - ======== ============================== =============================== =========== - DVS Crop development stage DVS_Phenology - - FL Fraction biomass to leaves DVS_Partitioning - - FR Fraction biomass to roots DVS_Partitioning - - SAI Stem area index WOFOST_Stem_Dynamics - - PAI Pod area index WOFOST_Storage_Organ_Dynamics - - TRA Transpiration rate Evapotranspiration |cm day-1| ? - TRAMX Maximum transpiration rate Evapotranspiration |cm day-1| ? - ADMI Above-ground dry matter CropSimulation |kg ha-1 d-1| - increase - RFTRA Reduction factor for Y - - transpiration (wat & ox) - RF_FROST Reduction factor frost kill FROSTOL(optional) - - ======== ============================== =============================== =========== - - *Outputs:* - LAI, TWLV - """ + **Simulation parameters** (provide in cropdata dictionary) + + | Name | Description | Type | Unit | + |--------|------------------------------------------------------------------ |------|------------| + | RGRLAI | Maximum relative increase in LAI. |SCr | ha ha⁻¹ d⁻¹| + | SPAN | Life span of leaves growing at 35 Celsius |SCr | d | + | TBASE | Lower threshold temp. for ageing of leaves |SCr | C | + | PERDL | Max. relative death rate of leaves due to water stress |SCr | | + | TDWI | Initial total crop dry weight |SCr | kg ha⁻¹ | + | KDIFTB | Extinction coefficient for diffuse visible light as function of DVS|TCr | | + | SLATB | Specific leaf area as a function of DVS |TCr | ha kg⁻¹ | + + **State variables** + + | Name | Description | Pbl | Unit | + |--------|-------------------------------------------------------|------|-------------| + | LV | Leaf biomass per leaf class | N | kg ha⁻¹ | + | SLA | Specific leaf area per leaf class | N | ha kg⁻¹ | + | LVAGE | Leaf age per leaf class | N | d | + | LVSUM | Sum of LV | N | kg ha⁻¹ | + | LAIEM | LAI at emergence | N | - | + | LASUM | Total leaf area as sum of LV*SLA, not including stem and pod area | N | - | + | LAIEXP | LAI value under theoretical exponential growth | N | - | + | LAIMAX | Maximum LAI reached during growth cycle | N | - | + | LAI | Leaf area index, including stem and pod area | Y | - | + | WLV | Dry weight of living leaves | Y | kg ha⁻¹ | + | DWLV | Dry weight of dead leaves | N | kg ha⁻¹ | + | TWLV | Dry weight of total leaves (living + dead) | Y | kg ha⁻¹ | + + **Rate variables** + + | Name | Description | Pbl | Unit | + |--------|-------------------------------------------------------|------|---------------| + | GRLV | Growth rate leaves | N | kg ha⁻¹ d⁻¹ | + | DSLV1 | Death rate leaves due to water stress | N | kg ha⁻¹ d⁻¹ | + | DSLV2 | Death rate leaves due to self-shading | N | kg ha⁻¹ d⁻¹ | + | DSLV3 | Death rate leaves due to frost kill | N | kg ha⁻¹ d⁻¹ | + | DSLV | Maximum of DSLV1, DSLV2, DSLV3 | N | kg ha⁻¹ d⁻¹ | + | DALV | Death rate leaves due to aging | N | kg ha⁻¹ d⁻¹ | + | DRLV | Death rate leaves as a combination of DSLV and DALV | N | kg ha⁻¹ d⁻¹ | + | SLAT | Specific leaf area for current time step, adjusted for source/sink limited leaf expansion rate | N | ha kg⁻¹ | + | FYSAGE | Increase in physiological leaf age | N | - | + | GLAIEX | Sink-limited leaf expansion rate (exponential curve) | N | ha ha⁻¹ d⁻¹ | + | GLASOL | Source-limited leaf expansion rate (biomass increase) | N | ha ha⁻¹ d⁻¹ | + + **External dependencies** + + | Name | Description | Provided by | Unit | + |-----------|-----------------------------------|--------------------------------|----------------| + | DVS | Crop development stage | DVS_Phenology | - | + | FL | Fraction biomass to leaves | DVS_Partitioning | - | + | FR | Fraction biomass to roots | DVS_Partitioning | - | + | SAI | Stem area index | WOFOST_Stem_Dynamics | - | + | PAI | Pod area index | WOFOST_Storage_Organ_Dynamics | - | + | TRA | Transpiration rate | Evapotranspiration | cm day⁻¹ ? | + | TRAMX | Maximum transpiration rate | Evapotranspiration | cm day⁻¹ ? | + | ADMI | Above-ground dry matter increase | CropSimulation | kg ha⁻¹ d⁻¹ | + | RFTRA | Reduction factor for transpiration (water & oxygen) | Y | - | + | RF_FROST | Reduction factor frost kill | FROSTOL (optional) | - | + + **Outputs** + + | Name | Description | Pbl | Unit | + |--------|-------------------------------------------------------|------|-------------| + | LAI | Leaf area index, including stem and pod area | Y | - | + | TWLV | Dry weight of total leaves (living + dead) | Y | kg ha⁻¹ | + """ # noqa: E501 # The following parameters are used to initialize and control the arrays that store information # on the leaf classes during the time integration: leaf area, age, and biomass. @@ -153,13 +141,19 @@ class RateVariables(RatesTemplate): GLAIEX = Any(default_value=torch.tensor(0.0, dtype=DTYPE)) GLASOL = Any(default_value=torch.tensor(0.0, dtype=DTYPE)) - def initialize(self, day, kiosk, parvalues): + def initialize( + self, day: datetime.date, kiosk: VariableKiosk, parvalues: ParameterProvider + ) -> None: """Initialize the WOFOST_Leaf_Dynamics simulation object. - :param day: start date of the simulation - :param kiosk: variable kiosk of this PCSE instance - :param parvalues: `ParameterProvider` object providing parameters as - key/value pairs + Args: + day (datetime.date): The starting date of the simulation. + kiosk (VariableKiosk): A container for registering and publishing + (internal and external) state variables. See PCSE documentation for + details. + parvalues (ParameterProvider): A dictionary-like container holding + all parameter sets (crop, soil, site) as key/value. The values are + arrays or scalars. See PCSE documentation for details. """ self.START_DATE = day self.kiosk = kiosk @@ -223,8 +217,15 @@ def _calc_LAI(self): return total_LAI @prepare_rates - def calc_rates(self, day, drv): - """Calculate the rates of change for the leaf dynamics.""" + def calc_rates(self, day: datetime.date, drv: WeatherDataContainer) -> None: + """Calculate the rates of change for the leaf dynamics. + + Args: + day (datetime.date, optional): The current date of the simulation. + drv (WeatherDataContainer, optional): A dictionary-like container holding + weather data elements as key/value. The values are + arrays or scalars. See PCSE documentation for details. + """ r = self.rates s = self.states p = self.params @@ -295,8 +296,13 @@ def calc_rates(self, day, drv): r.SLAT = torch.where(is_lai_exp & (r.GRLV > 0.0), GLA / r.GRLV, r.SLAT) @prepare_states - def integrate(self, day, delt=1.0): - """Integrate the leaf dynamics state variables.""" + def integrate(self, day: datetime.date, delt=1.0) -> None: + """Integrate the leaf dynamics state variables. + + Args: + day (datetime.date, optional): The current date of the simulation. + delt (float, optional): The time step for integration. Defaults to 1.0. + """ # TODO check if DVS < 0 and skip integration needed rates = self.rates states = self.states @@ -312,17 +318,20 @@ def integrate(self, day, delt=1.0): # find out which leaf classes are dead (negative weights) weight_cumsum = tLV.cumsum(dim=-1) - tDRLV is_alive = weight_cumsum >= 0 + # Adjust value of oldest leaf class, i.e. the first non-zero # weight along the time axis (the last dimension). # Cast argument to int because torch.argmax requires it to be numeric idx_oldest = torch.argmax(is_alive.type(torch.int), dim=-1, keepdim=True) new_biomass = torch.take_along_dim(weight_cumsum, indices=idx_oldest, dim=-1) tLV = torch.scatter(tLV, dim=-1, index=idx_oldest, src=new_biomass) + # Zero out all dead leaf classes # NOTE: conditional statements do not allow for the gradient to be # tracked through the condition. Thus, the gradient with respect to # parameters that contribute to `is_alive` are expected to be incorrect. tLV = torch.where(is_alive, tLV, 0.0) + # Integration of physiological age tLVAGE = tLVAGE + rates.FYSAGE tLVAGE = torch.where(is_alive, tLVAGE, 0.0) @@ -354,7 +363,15 @@ def integrate(self, day, delt=1.0): def _exist_required_external_variables(kiosk): - """Check if all required external variables are available in the kiosk.""" + """Check if all required external variables are available in the kiosk. + + Args: + kiosk (VariableKiosk): The variable kiosk to check. + + Raises: + ValueError: If any required external variable is missing. + + """ required_external_vars_at_init = ["DVS", "FL", "FR", "SAI", "PAI"] for var in required_external_vars_at_init: if var not in kiosk: diff --git a/src/diffwofost/physical_models/crop/root_dynamics.py b/src/diffwofost/physical_models/crop/root_dynamics.py index 81af040..61ba60d 100644 --- a/src/diffwofost/physical_models/crop/root_dynamics.py +++ b/src/diffwofost/physical_models/crop/root_dynamics.py @@ -1,8 +1,12 @@ +from datetime import datetime import torch from pcse.base import ParamTemplate from pcse.base import RatesTemplate from pcse.base import SimulationObject from pcse.base import StatesTemplate +from pcse.base.parameter_providers import ParameterProvider +from pcse.base.variablekiosk import VariableKiosk +from pcse.base.weather import WeatherDataContainer from pcse.decorators import prepare_rates from pcse.decorators import prepare_states from pcse.traitlets import Any @@ -28,44 +32,34 @@ class WOFOST_Root_Dynamics(SimulationObject): **Simulation parameters** - ======= ============================================= ======= ============ - Name Description Type Unit - ======= ============================================= ======= ============ - RDI Initial rooting depth SCr cm - RRI Daily increase in rooting depth SCr |cm day-1| - RDMCR Maximum rooting depth of the crop SCR cm - RDMSOL Maximum rooting depth of the soil SSo cm - TDWI Initial total crop dry weight SCr |kg ha-1| - IAIRDU Presence of air ducts in the root (1) or SCr - - not (0) - RDRRTB Relative death rate of roots as a function TCr - - of development stage - ======= ============================================= ======= ============ - + | Name | Description | Type | Unit | + |--------|-----------------------------------------------------|------|-----------| + | RDI | Initial rooting depth | SCr | cm | + | RRI | Daily increase in rooting depth | SCr | cm day⁻¹ | + | RDMCR | Maximum rooting depth of the crop | SCR | cm | + | RDMSOL | Maximum rooting depth of the soil | SSo | cm | + | TDWI | Initial total crop dry weight | SCr | kg ha⁻¹ | + | IAIRDU | Presence of air ducts in the root (1) or not (0) | SCr | - | + | RDRRTB | Relative death rate of roots as a function of development stage | TCr | - | **State variables** - ======= ================================================= ==== ============ - Name Description Pbl Unit - ======= ================================================= ==== ============ - RD Current rooting depth Y cm - RDM Maximum attainable rooting depth at the minimum N cm - of the soil and crop maximum rooting depth - WRT Weight of living roots Y |kg ha-1| - DWRT Weight of dead roots N |kg ha-1| - TWRT Total weight of roots Y |kg ha-1| - ======= ================================================= ==== ============ + | Name | Description | Pbl | Unit | + |------|------------------------------------------------------------------------------|-----|----------| + | RD | Current rooting depth | Y | cm | + | RDM | Maximum attainable rooting depth at the minimum of the soil and crop maximum rooting depth | N | cm | + | WRT | Weight of living roots | Y | kg ha⁻¹ | + | DWRT | Weight of dead roots | N | kg ha⁻¹ | + | TWRT | Total weight of roots | Y | kg ha⁻¹ | **Rate variables** - ======= ================================================= ==== ============ - Name Description Pbl Unit - ======= ================================================= ==== ============ - RR Growth rate root depth N cm - GRRT Growth rate root biomass N |kg ha-1 d-1| - DRRT Death rate root biomass N |kg ha-1 d-1| - GWRT Net change in root biomass N |kg ha-1 d-1| - ======= ================================================= ==== ============ + | Name | Description | Pbl | Unit | + |------|-----------------------------|-----|--------------| + | RR | Growth rate root depth | N | cm | + | GRRT | Growth rate root biomass | N | kg ha⁻¹ d⁻¹ | + | DRRT | Death rate root biomass | N | kg ha⁻¹ d⁻¹ | + | GWRT | Net change in root biomass | N | kg ha⁻¹ d⁻¹ | **Signals send or handled** @@ -73,22 +67,21 @@ class WOFOST_Root_Dynamics(SimulationObject): **External dependencies:** - ======= =================================== ================= ============ - Name Description Provided by Unit - ======= =================================== ================= ============ - DVS Crop development stage DVS_Phenology - - DMI Total dry matter CropSimulation |kg ha-1 d-1| - increase - FR Fraction biomass to roots DVS_Partitioning - - ======= =================================== ================= ============ + | Name | Description | Provided by | Unit | + |------|---------------------------|------------------|--------------| + | DVS | Crop development stage | DVS_Phenology | - | + | DMI | Total dry matter increase | CropSimulation | kg ha⁻¹ d⁻¹ | + | FR | Fraction biomass to roots | DVS_Partitioning | - | **Outputs:** - - RD, TWRT - """ + | Name | Description | Provided by | Unit | + |------|-------------------------|------------------|--------------| + | RD | Current rooting depth | Y | cm | + | TWRT | Total weight of roots | Y | kg ha⁻¹ | + + **IMPORTANT NOTICE** - """ - IMPORTANT NOTICE Currently root development is linear and depends only on the fraction of assimilates send to the roots (FR) and not on the amount of assimilates itself. This means that roots also grow through the winter when there is no assimilation due to low @@ -110,7 +103,7 @@ class WOFOST_Root_Dynamics(SimulationObject): We conclude that our current knowledge on root development is insufficient to propose a better and more biophysical approach to root development in WOFOST. - """ + """ # noqa: E501 class Parameters(ParamTemplate): RDI = Any(default_value=[torch.tensor(-99.0, dtype=DTYPE)]) @@ -134,13 +127,19 @@ class StateVariables(StatesTemplate): DWRT = Any(default_value=[torch.tensor(-99.0, dtype=DTYPE)]) TWRT = Any(default_value=[torch.tensor(-99.0, dtype=DTYPE)]) - def initialize(self, day, kiosk, parvalues): + def initialize( + self, day: datetime.date, kiosk: VariableKiosk, parvalues: ParameterProvider + ) -> None: """Initialize the model. - :param day: start date of the simulation - :param kiosk: variable kiosk of this PCSE instance - :param parvalues: `ParameterProvider` object providing parameters as - key/value pairs + Args: + day (datetime.date): The starting date of the simulation. + kiosk (VariableKiosk): A container for registering and publishing + (internal and external) state variables. See PCSE documentation for + details. + parvalues (ParameterProvider): A dictionary-like container holding + all parameter sets (crop, soil, site) as key/value. The values are + arrays or scalars. See PCSE documentation for details. """ self.params = self.Parameters(parvalues) self.rates = self.RateVariables(kiosk, publish=["DRRT", "GRRT"]) @@ -164,8 +163,15 @@ def initialize(self, day, kiosk, parvalues): ) @prepare_rates - def calc_rates(self, day, drv): - """Calculate the rates of change of the state variables.""" + def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None) -> None: + """Calculate the rates of change of the state variables. + + Args: + day (datetime.date, optional): The current date of the simulation. + drv (WeatherDataContainer, optional): A dictionary-like container holding + weather data elements as key/value. The values are + arrays or scalars. See PCSE documentation for details. + """ p = self.params r = self.rates s = self.states @@ -191,8 +197,13 @@ def calc_rates(self, day, drv): r.RR = r.RR * mask @prepare_states - def integrate(self, day, delt=1.0): - """Integrate the state variables using the rates of change.""" + def integrate(self, day: datetime.date = None, delt=1.0) -> None: + """Integrate the state variables using the rates of change. + + Args: + day (datetime.date, optional): The current date of the simulation. + delt (float, optional): The time step for integration. Defaults to 1.0. + """ rates = self.rates states = self.states @@ -207,23 +218,3 @@ def integrate(self, day, delt=1.0): # New root depth states.RD = states.RD + rates.RR - - @prepare_states - def _set_variable_WRT(self, nWRT): # FIXEME - """Updates the value of WRT to to the new value provided as input. - - Related state variables will be updated as well and the increments - to all adjusted state variables will be returned as a dict. - """ - states = self.states - - # Store old values of states - oWRT = states.WRT - oTWRT = states.TWRT - - # Apply new root weight and adjust total (dead + live) root weight - states.WRT = nWRT - states.TWRT = states.WRT + states.DWRT - - increments = {"WRT": states.WRT - oWRT, "TWLRT": states.TWRT - oTWRT} - return increments