From 0b6a93767616e0aa97451dfb41cfe67fe595cd7d Mon Sep 17 00:00:00 2001 From: SarahAlidoost Date: Mon, 15 Dec 2025 09:38:10 +0100 Subject: [PATCH 01/10] update nb --- docs/notebooks/optimization.ipynb | 92 +++++++++++++++++-------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/docs/notebooks/optimization.ipynb b/docs/notebooks/optimization.ipynb index 48fe890..740db66 100644 --- a/docs/notebooks/optimization.ipynb +++ b/docs/notebooks/optimization.ipynb @@ -79,7 +79,8 @@ "import yaml\n", "from pathlib import Path\n", "from diffwofost.physical_models.utils import EngineTestHelper\n", - "from diffwofost.physical_models.utils import prepare_engine_input" + "from diffwofost.physical_models.utils import prepare_engine_input\n", + "from diffwofost.physical_models.utils import get_test_data" ] }, { @@ -114,7 +115,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 3, "id": "0233a048-e5a2-4249-887d-35a37284769c", "metadata": {}, "outputs": [ @@ -146,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 4, "id": "b4a24f1c-77e4-4b05-bde9-229dd497f09e", "metadata": {}, "outputs": [ @@ -168,7 +169,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "5a459489-bfcb-4ad6-9102-1b6be5edeb52", "metadata": {}, "outputs": [], @@ -180,17 +181,18 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "id": "9f3105fb-4fbe-4405-9fd4-e8255b4b119e", "metadata": {}, "outputs": [], "source": [ "# ---- Here we read the test data and set some variables ----\n", + "test_data = get_test_data(test_data_path)\n", "(crop_model_params_provider, weather_data_provider, agro_management_inputs, external_states) = (\n", - " prepare_engine_input(test_data_path, [\"SPAN\", \"TDWI\", \"TBASE\", \"PERDL\", \"RGRLAI\"])\n", + " prepare_engine_input(test_data, [\"SPAN\", \"TDWI\", \"TBASE\", \"PERDL\", \"RGRLAI\"])\n", ")\n", "\n", - "expected_results = yaml.safe_load(open(test_data_path))[\"ModelResults\"]\n", + "expected_results = test_data[\"ModelResults\"]\n", "expected_lai_twlv = torch.tensor(\n", " [[float(item[\"LAI\"]), float(item[\"TWLV\"])] for item in expected_results], dtype=torch.float32\n", ").unsqueeze(0) # shape: [1, time_steps, 2]\n", @@ -215,7 +217,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 16, "id": "e4610238-de0d-42cf-9689-3c074eb2cc0e", "metadata": {}, "outputs": [], @@ -252,7 +254,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 17, "id": "36dd6463-4812-41c0-b2bf-d4769df1136f", "metadata": {}, "outputs": [], @@ -300,38 +302,46 @@ }, { "cell_type": "code", - "execution_count": 7, - "id": "0dcd85c8-624c-4b58-a7c9-893316110d98", + "execution_count": 18, + "id": "3d34c3e8-a8d7-4bc9-94ed-bd2e0234e95c", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Create model ---- \n", + "opt_model = OptDiffLeafDynamics(\n", + " crop_model_params_provider,\n", + " weather_data_provider,\n", + " agro_management_inputs,\n", + " config_path,\n", + " external_states,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "78d797f5-4ac4-4380-85f3-6622a7b0f7fb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Step 0, Loss 0.1348, TDWI 0.4242, SPAN 31.2111\n", - "Step 10, Loss 0.0589, TDWI 0.4890, SPAN 36.7883\n", - "Step 20, Loss 0.0174, TDWI 0.5191, SPAN 34.8344\n", - "Step 30, Loss 0.0113, TDWI 0.5056, SPAN 35.0567\n", - "Step 40, Loss 0.0067, TDWI 0.5100, SPAN 35.4831\n", - "Step 50, Loss 0.0121, TDWI 0.5019, SPAN 34.6534\n", - "Step 60, Loss 0.0005, TDWI 0.5038, SPAN 34.6887\n", - "Step 70, Loss 0.0079, TDWI 0.5015, SPAN 35.7478\n", - "Step 80, Loss 0.0145, TDWI 0.5061, SPAN 34.5574\n", - "Step 90, Loss 0.0110, TDWI 0.4985, SPAN 34.3927\n", - "Step 100, Loss 0.0271, TDWI 0.5064, SPAN 36.3026\n" + "Step 0, Loss 0.1351, TDWI 0.4242, SPAN 30.0000\n", + "Step 10, Loss 0.1348, TDWI 0.5577, SPAN 30.0000\n", + "Step 20, Loss 0.1347, TDWI 0.4957, SPAN 30.0000\n", + "Step 30, Loss 0.1347, TDWI 0.5045, SPAN 30.0000\n", + "Step 40, Loss 0.1347, TDWI 0.5069, SPAN 30.0000\n", + "Step 50, Loss 0.1347, TDWI 0.5071, SPAN 30.0000\n", + "Step 60, Loss 0.1347, TDWI 0.5071, SPAN 30.0000\n", + "Step 70, Loss 0.1347, TDWI 0.5093, SPAN 30.0000\n", + "Step 80, Loss 0.1347, TDWI 0.5092, SPAN 30.0000\n", + "Step 90, Loss 0.1347, TDWI 0.5007, SPAN 30.0000\n", + "Step 100, Loss 0.1347, TDWI 0.5123, SPAN 30.0000\n" ] } ], "source": [ - "# ---- Create model ---- \n", - "opt_model = OptDiffLeafDynamics(\n", - " crop_model_params_provider,\n", - " weather_data_provider,\n", - " agro_management_inputs,\n", - " config_path,\n", - " external_states,\n", - ")\n", - "\n", "# ---- Optimizer ---- \n", "optimizer = torch.optim.Adam(opt_model.parameters(), lr=0.1)\n", "\n", @@ -402,7 +412,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 20, "id": "eaa4c172-9719-4a79-b2f2-d37ea5b6f11d", "metadata": {}, "outputs": [], @@ -414,12 +424,13 @@ "import yaml\n", "from pathlib import Path\n", "from diffwofost.physical_models.utils import EngineTestHelper\n", - "from diffwofost.physical_models.utils import prepare_engine_input" + "from diffwofost.physical_models.utils import prepare_engine_input\n", + "from diffwofost.physical_models.utils import get_test_data" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 21, "id": "26437aed-755d-4ee3-b7b6-82caf8c30ec5", "metadata": {}, "outputs": [], @@ -447,7 +458,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 22, "id": "22000922-68be-47ed-8afa-2e97c56bb502", "metadata": {}, "outputs": [ @@ -479,7 +490,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 23, "id": "ae2de6dc-1294-4a5a-91d0-3e83f28dc892", "metadata": {}, "outputs": [ @@ -501,7 +512,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 24, "id": "a69e9279-49eb-4136-8f15-7cff8bb4af52", "metadata": {}, "outputs": [], @@ -513,17 +524,18 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 25, "id": "d560476b-64f9-422c-9722-8d0778cfc574", "metadata": {}, "outputs": [], "source": [ "# ---- Here we read the test data and set some variables ----\n", + "test_data = get_test_data(test_data_path)\n", "(crop_model_params_provider, weather_data_provider, agro_management_inputs, external_states) = (\n", - " prepare_engine_input(test_data_path, [\"RDI\", \"RRI\", \"RDMCR\", \"RDMSOL\", \"TDWI\", \"IAIRDU\"])\n", + " prepare_engine_input(test_data, [\"RDI\", \"RRI\", \"RDMCR\", \"RDMSOL\", \"TDWI\", \"IAIRDU\"])\n", ")\n", "\n", - "expected_results = yaml.safe_load(open(test_data_path))[\"ModelResults\"]\n", + "expected_results = test_data[\"ModelResults\"]\n", "expected_twrt = torch.tensor(\n", " [float(item[\"TWRT\"]) for item in expected_results], dtype=torch.float32\n", ") # shape: [1, time_steps]\n", From 103377e9ec28a9a7b114201492927fac4bf1dd10 Mon Sep 17 00:00:00 2001 From: SarahAlidoost Date: Tue, 16 Dec 2025 14:09:46 +0100 Subject: [PATCH 02/10] fix docstring in leaf_dynamics --- src/diffwofost/physical_models/crop/leaf_dynamics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diffwofost/physical_models/crop/leaf_dynamics.py b/src/diffwofost/physical_models/crop/leaf_dynamics.py index 4363670..b79f281 100644 --- a/src/diffwofost/physical_models/crop/leaf_dynamics.py +++ b/src/diffwofost/physical_models/crop/leaf_dynamics.py @@ -111,7 +111,7 @@ class WOFOST_Leaf_Dynamics(SimulationObject): [!NOTE] Notice that the following gradients are zero: - - ∂SPAN/∂LAI + - ∂SPAN/∂TWLV - ∂PERDL/∂TWLV - ∂KDIFTB/∂LAI """ # noqa: E501 From bd9ffde4b899be1bbc06554d65ea4fb95127d652 Mon Sep 17 00:00:00 2001 From: SarahAlidoost Date: Tue, 16 Dec 2025 14:12:24 +0100 Subject: [PATCH 03/10] fix a test in test_leaf_dynamics --- tests/physical_models/crop/test_leaf_dynamics.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/physical_models/crop/test_leaf_dynamics.py b/tests/physical_models/crop/test_leaf_dynamics.py index 3eab5a5..009d684 100644 --- a/tests/physical_models/crop/test_leaf_dynamics.py +++ b/tests/physical_models/crop/test_leaf_dynamics.py @@ -3,7 +3,6 @@ from unittest.mock import patch import pytest import torch -from numpy.testing import assert_array_almost_equal from pcse.engine import Engine from pcse.models import Wofost72_PP from diffwofost.physical_models.crop.leaf_dynamics import WOFOST_Leaf_Dynamics @@ -590,8 +589,11 @@ def test_gradients_numerical(self, param_name, output_name, config_type): """Test that analytical gradients match numerical gradients.""" value, _ = self.param_configs[config_type][param_name] param = torch.nn.Parameter(torch.tensor(value, dtype=torch.float64)) + + # we pass `param` and not `param.data` because we want `requires_grad=True` + # for parameter `SPAN` numerical_grad = calculate_numerical_grad( - get_test_diff_leaf_model, param_name, param.data, output_name + get_test_diff_leaf_model, param_name, param, output_name ) model = get_test_diff_leaf_model() @@ -601,7 +603,13 @@ def test_gradients_numerical(self, param_name, output_name, config_type): # this is ∂loss/∂param, for comparison with numerical gradient grads = torch.autograd.grad(loss, param, retain_graph=True)[0] - assert_array_almost_equal(numerical_grad, grads.data, decimal=3) + # assert_array_almost_equal(numerical_grad, grads.data, decimal=3) + torch.testing.assert_close( + numerical_grad, + grads, + rtol=1e-3, + atol=1e-3, + ) # Warn if gradient is zero (but this shouldn't happen for gradient_params) if torch.all(grads == 0): From 9ea633b2a463d11e374ce55d99be2f269c37af94 Mon Sep 17 00:00:00 2001 From: SarahAlidoost Date: Wed, 17 Dec 2025 10:51:25 +0100 Subject: [PATCH 04/10] add STE method --- src/diffwofost/physical_models/crop/leaf_dynamics.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/diffwofost/physical_models/crop/leaf_dynamics.py b/src/diffwofost/physical_models/crop/leaf_dynamics.py index b79f281..550150a 100644 --- a/src/diffwofost/physical_models/crop/leaf_dynamics.py +++ b/src/diffwofost/physical_models/crop/leaf_dynamics.py @@ -292,12 +292,20 @@ def calc_rates(self, day: datetime.date, drv: WeatherDataContainer) -> None: if p.SPAN.requires_grad: # 1e-16 is chosen empirically for cases when s.LVAGE - tSPAN is very # small and mask should be 1 - sharpness = torch.tensor(1e-16, dtype=DTYPE) + # sharpness = torch.tensor(1e-16, dtype=DTYPE) + sharpness = torch.tensor(1e-4, dtype=DTYPE) # 1e-14 is chosen empirically for cases when s.LVAGE - tSPAN is # equal to zero and mask should be 0.0 epsilon = 1e-14 - span_mask = torch.sigmoid((s.LVAGE - tSPAN - epsilon) / sharpness).to(dtype=DTYPE) + # span_mask = torch.sigmoid((s.LVAGE - tSPAN - epsilon) / sharpness).to(dtype=DTYPE) + + soft_mask = torch.sigmoid((s.LVAGE - tSPAN - epsilon) / sharpness) + + hard_mask = (s.LVAGE > tSPAN).to(DTYPE) + + span_mask = hard_mask.detach() + soft_mask - soft_mask.detach() + else: span_mask = (s.LVAGE > tSPAN).to(dtype=DTYPE) From 68f1244ad51ec270b917c8f5e65eafe9c10c52cd Mon Sep 17 00:00:00 2001 From: SarahAlidoost Date: Wed, 17 Dec 2025 13:50:15 +0100 Subject: [PATCH 05/10] split optimization nb to root and leaf nbs --- ...ipynb => optimization_leaf_dynamics.ipynb} | 0 .../optimization_root_dynamics.ipynb | 750 ++++++++++++++++++ 2 files changed, 750 insertions(+) rename docs/notebooks/{optimization.ipynb => optimization_leaf_dynamics.ipynb} (100%) create mode 100644 docs/notebooks/optimization_root_dynamics.ipynb diff --git a/docs/notebooks/optimization.ipynb b/docs/notebooks/optimization_leaf_dynamics.ipynb similarity index 100% rename from docs/notebooks/optimization.ipynb rename to docs/notebooks/optimization_leaf_dynamics.ipynb diff --git a/docs/notebooks/optimization_root_dynamics.ipynb b/docs/notebooks/optimization_root_dynamics.ipynb new file mode 100644 index 0000000..740db66 --- /dev/null +++ b/docs/notebooks/optimization_root_dynamics.ipynb @@ -0,0 +1,750 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c784f1c2-d477-464d-a35f-f2c64e96fb10", + "metadata": {}, + "source": [ + "
\n", + "

Optimizing parameters in a WOFOST crop model using diffWOFOST

\n", + " \n", + "
\n", + "\n", + "\n", + "This Jupyter notebook demonstrates the optimization of parameters in a\n", + "differentiable model using the `diffwofost` package. The package provides\n", + "differentiable implementations of the WOFOST model and its associated\n", + "sub-models. As `diffwofost` is under active development, this notebook focuses on\n", + "two sub-models: `leaf_dynamics` and `root_dynamics`. \n", + "\n", + "To enable these models to operate independently, certain state variables\n", + "required by the model are supplied as \"external states\" derived from the test\n", + "data. Also, at this stage, only a limited subset of model parameters has been made\n", + "differentiable.\n", + "\n", + "The notebook is organized into two standalone sections that can\n", + "be executed independently:\n", + "\n", + " 1. Leaf Dynamics\n", + " 2. Root Dynamics" + ] + }, + { + "cell_type": "markdown", + "id": "41262fbd-270b-4616-91ad-09ee82451604", + "metadata": {}, + "source": [ + "## 1. Leaf dynamics\n", + "\n", + "In this section, we will demonstrate how to optimize two parameters `TWDI` and `SPAN` in\n", + "leaf_dynamics model using a differentiable version of leaf_dynamics.\n", + "The optimization will be done using the Adam optimizer from `torch.optim`." + ] + }, + { + "cell_type": "markdown", + "id": "1b6c3f53-6fab-4537-9177-7b16e0a1ccec", + "metadata": {}, + "source": [ + "### 1.1 software requirements\n", + "\n", + "To run this notebook, we need to install the `diffwofost`; the differentiable\n", + "version of WOFOST models. Since the package is constantly under development, make\n", + "sure you have the latest version of `diffwofost` installed in your\n", + "python environment. You can install it using pip:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4049fea-1d05-41f1-bf9d-f030ae83a324", + "metadata": {}, + "outputs": [], + "source": [ + "# install diffwofost\n", + "!pip install diffwofost" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "21731653-3976-4bb9-b83b-b11d78211700", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- import libraries ----\n", + "import copy\n", + "import torch\n", + "import numpy\n", + "import yaml\n", + "from pathlib import Path\n", + "from diffwofost.physical_models.utils import EngineTestHelper\n", + "from diffwofost.physical_models.utils import prepare_engine_input\n", + "from diffwofost.physical_models.utils import get_test_data" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "82a1ef6b-336e-4902-8bd1-2a1ed2020f9d", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- disable a warning: this will be fixed in the future ----\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\", message=\"To copy construct from a tensor.*\")" + ] + }, + { + "cell_type": "markdown", + "id": "47def7fc-f2dd-4aaf-a572-41cc9d1e4679", + "metadata": {}, + "source": [ + "### 1.2. Data\n", + "\n", + "A test dataset of `LAI` (Leaf area index, including stem and pod area) and\n", + "`TWLV` (Dry weight of total leaves (living + dead)) will be used to optimize\n", + "parametesr `TWDI` (total initial dry weight) and `SPAN` (life span of leaves).\n", + "Note that in leaf_dynamic, changes in `SPAN` dont affect `TWLV`. \n", + "\n", + "The data is stored in PCSE tests folder, and can be doewnloded from PCSE repsository.\n", + "You can select any of the files related to `leaf_dynamics` model with a file name that follwos the pattern\n", + "`test_leafdynamics_wofost72_*.yaml`. Each file contains different data depending on the locatin and crop type.\n", + "For example, you can download the file \"test_leafdynamics_wofost72_01.yaml\" as:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0233a048-e5a2-4249-887d-35a37284769c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloaded: test_leafdynamics_wofost72_01.yaml\n" + ] + } + ], + "source": [ + "import urllib.request\n", + "\n", + "url = \"https://raw.githubusercontent.com/ajwdewit/pcse/refs/heads/master/tests/test_data/test_leafdynamics_wofost72_01.yaml\"\n", + "filename = \"test_leafdynamics_wofost72_01.yaml\"\n", + "\n", + "urllib.request.urlretrieve(url, filename)\n", + "print(f\"Downloaded: {filename}\")" + ] + }, + { + "cell_type": "markdown", + "id": "e4565b6b-523c-49c4-934e-500248317461", + "metadata": {}, + "source": [ + "We also need to download a config file to be able to run each crop module. This will change in the future versions. To donwload the config file, you can use the following command:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b4a24f1c-77e4-4b05-bde9-229dd497f09e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloaded: WOFOST_Leaf_Dynamics.conf\n" + ] + } + ], + "source": [ + "url = \"https://raw.githubusercontent.com/WUR-AI/diffWOFOST/refs/heads/main/tests/physical_models/test_data/WOFOST_Leaf_Dynamics.conf\"\n", + "filename = \"WOFOST_Leaf_Dynamics.conf\"\n", + "\n", + "urllib.request.urlretrieve(url, filename)\n", + "print(f\"Downloaded: {filename}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5a459489-bfcb-4ad6-9102-1b6be5edeb52", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Check the path to the files that are downloaded as explained above ----\n", + "test_data_path = \"test_leafdynamics_wofost72_01.yaml\"\n", + "config_path = \"WOFOST_Leaf_Dynamics.conf\"" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9f3105fb-4fbe-4405-9fd4-e8255b4b119e", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Here we read the test data and set some variables ----\n", + "test_data = get_test_data(test_data_path)\n", + "(crop_model_params_provider, weather_data_provider, agro_management_inputs, external_states) = (\n", + " prepare_engine_input(test_data, [\"SPAN\", \"TDWI\", \"TBASE\", \"PERDL\", \"RGRLAI\"])\n", + ")\n", + "\n", + "expected_results = test_data[\"ModelResults\"]\n", + "expected_lai_twlv = torch.tensor(\n", + " [[float(item[\"LAI\"]), float(item[\"TWLV\"])] for item in expected_results], dtype=torch.float32\n", + ").unsqueeze(0) # shape: [1, time_steps, 2]\n", + "\n", + "# ---- dont change this: in this config file we specified the diffrentiable version of leaf_dynamics ----\n", + "config_path = str(Path(config_path).resolve()) " + ] + }, + { + "cell_type": "markdown", + "id": "52b19ae2-3fe6-4b3f-95a7-aea07a2c0958", + "metadata": {}, + "source": [ + "### 1.3. Helper classes/functions\n", + "\n", + "The model parameters shoudl stay in a valid range. To ensure this, we will use\n", + "`BoundedParameter` class with (min, max) and initial values for each\n", + "parameter. You might change these values depending on the crop type and\n", + "location. But dont use a very small range, otherwise gradiants will be very\n", + "small and the optimization will be very slow." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "e4610238-de0d-42cf-9689-3c074eb2cc0e", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Adjust the values if needed ----\n", + "TDWI_MIN, TDWI_MAX, TDWI_INIT = (0.0, 1.0, 0.40)\n", + "SPAN_MIN, SPAN_MAX, SPAN_INIT = (10.0, 60.0, 30.0)\n", + "\n", + "# ---- Helper for bounded parameters ----\n", + "class BoundedParameter(torch.nn.Module):\n", + " def __init__(self, low, high, init_value):\n", + " super().__init__()\n", + " self.low = low\n", + " self.high = high\n", + "\n", + " # Normalize to [0, 1]\n", + " init_norm = (init_value - low) / (high - low)\n", + "\n", + " # Parameter in raw logit space\n", + " self.raw = torch.nn.Parameter(torch.logit(torch.tensor(init_norm, dtype=torch.float32), eps=1e-6))\n", + "\n", + " def forward(self):\n", + " return self.low + (self.high - self.low) * torch.sigmoid(self.raw)\n" + ] + }, + { + "cell_type": "markdown", + "id": "153e4306-77ed-4278-8797-a04e637e12c8", + "metadata": {}, + "source": [ + "Another helper class is `OptDiffLeafDynamics` which is a subclass of `torch.nn.Module`. \n", + "We use this class to wrap the `EngineTestHelper` function and make it easier to run the model `leaf_dynamic`." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "36dd6463-4812-41c0-b2bf-d4769df1136f", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Wrap the model with torch.nn.Module----\n", + "class OptDiffLeafDynamics(torch.nn.Module):\n", + " def __init__(self, crop_model_params_provider, weather_data_provider, agro_management_inputs, config_path, external_states):\n", + " super().__init__()\n", + " self.crop_model_params_provider = crop_model_params_provider\n", + " self.weather_data_provider = weather_data_provider\n", + " self.agro_management_inputs = agro_management_inputs\n", + " self.config_path = config_path\n", + " self.external_states = external_states\n", + "\n", + " # bounded parameters\n", + " self.tdwi = BoundedParameter(TDWI_MIN, TDWI_MAX, init_value=TDWI_INIT)\n", + " self.span = BoundedParameter(SPAN_MIN, SPAN_MAX, init_value=SPAN_INIT)\n", + "\n", + " def forward(self):\n", + " # currently, copying is needed due to an internal issue in engine\n", + " crop_model_params_provider_ = copy.deepcopy(self.crop_model_params_provider)\n", + " external_states_ = copy.deepcopy(self.external_states)\n", + " \n", + " tdwi_val = self.tdwi()\n", + " span_val = self.span()\n", + " \n", + " # pass new value of parameters to the model\n", + " crop_model_params_provider_.set_override(\"TDWI\", tdwi_val, check=False)\n", + " crop_model_params_provider_.set_override(\"SPAN\", span_val, check=False)\n", + "\n", + " engine = EngineTestHelper(\n", + " crop_model_params_provider_,\n", + " self.weather_data_provider,\n", + " self.agro_management_inputs,\n", + " self.config_path,\n", + " external_states_,\n", + " )\n", + " engine.run_till_terminate()\n", + " results = engine.get_output()\n", + " \n", + " return torch.stack(\n", + " [torch.stack([item[\"LAI\"], item[\"TWLV\"]]) for item in results]\n", + " ).unsqueeze(0) # shape: [1, time_steps, 2]" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "3d34c3e8-a8d7-4bc9-94ed-bd2e0234e95c", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Create model ---- \n", + "opt_model = OptDiffLeafDynamics(\n", + " crop_model_params_provider,\n", + " weather_data_provider,\n", + " agro_management_inputs,\n", + " config_path,\n", + " external_states,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "78d797f5-4ac4-4380-85f3-6622a7b0f7fb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Step 0, Loss 0.1351, TDWI 0.4242, SPAN 30.0000\n", + "Step 10, Loss 0.1348, TDWI 0.5577, SPAN 30.0000\n", + "Step 20, Loss 0.1347, TDWI 0.4957, SPAN 30.0000\n", + "Step 30, Loss 0.1347, TDWI 0.5045, SPAN 30.0000\n", + "Step 40, Loss 0.1347, TDWI 0.5069, SPAN 30.0000\n", + "Step 50, Loss 0.1347, TDWI 0.5071, SPAN 30.0000\n", + "Step 60, Loss 0.1347, TDWI 0.5071, SPAN 30.0000\n", + "Step 70, Loss 0.1347, TDWI 0.5093, SPAN 30.0000\n", + "Step 80, Loss 0.1347, TDWI 0.5092, SPAN 30.0000\n", + "Step 90, Loss 0.1347, TDWI 0.5007, SPAN 30.0000\n", + "Step 100, Loss 0.1347, TDWI 0.5123, SPAN 30.0000\n" + ] + } + ], + "source": [ + "# ---- Optimizer ---- \n", + "optimizer = torch.optim.Adam(opt_model.parameters(), lr=0.1)\n", + "\n", + "# ---- We use relative MAE as loss because there are two outputs with different untis ---- \n", + "denom = torch.mean(torch.abs(expected_lai_twlv), dim=1) \n", + "\n", + "# Training loop (example)\n", + "for step in range(101):\n", + " optimizer.zero_grad()\n", + " results = opt_model() \n", + " mae = torch.mean(torch.abs(results - expected_lai_twlv), dim=1)\n", + " rmae = mae / denom\n", + " loss = rmae.sum() # example: relative mean absolute error\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " if step % 10 == 0:\n", + " print(f\"Step {step}, Loss {loss.item():.4f}, TDWI {opt_model.tdwi().item():.4f}, SPAN {opt_model.span().item():.4f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c2d3a463-43a4-4b29-a71f-696c019343d3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Actual TDWI 0.5100, SPAN 35.0000\n" + ] + } + ], + "source": [ + "# ---- validate the results using test data ---- \n", + "print(f\"Actual TDWI {crop_model_params_provider[\"TDWI\"].item():.4f}, SPAN {crop_model_params_provider[\"SPAN\"].item():.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "fedda7e9-02a6-40e5-8c61-08d7886c9519", + "metadata": {}, + "source": [ + "## 2. Root dynamics \n", + "\n", + "In this section, we will demonstrate how to optimize two parameters `TWDI` in\n", + "root_dynamics model using a differentiable version of root_dynamics.\n", + "The optimization will be done using the Adam optimizer from `torch.optim`." + ] + }, + { + "cell_type": "markdown", + "id": "152ec514-baf1-4213-b98b-76f453d49538", + "metadata": {}, + "source": [ + "### 2.1 software requirements\n", + "\n", + "To run this notebook, we need to install the `diffwofost`; the differentiable\n", + "version of WOFOST models. Since the package is constantly under development, make\n", + "sure you have the latest version of `diffwofost` installed in your\n", + "python environment. You can install it using pip:\n", + "\n", + "```bash\n", + "pip install diffwofost\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "eaa4c172-9719-4a79-b2f2-d37ea5b6f11d", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- import libraries ----\n", + "import copy\n", + "import torch\n", + "import numpy\n", + "import yaml\n", + "from pathlib import Path\n", + "from diffwofost.physical_models.utils import EngineTestHelper\n", + "from diffwofost.physical_models.utils import prepare_engine_input\n", + "from diffwofost.physical_models.utils import get_test_data" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "26437aed-755d-4ee3-b7b6-82caf8c30ec5", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- disable a warning: this will be fixed in the future ----\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\", message=\"To copy construct from a tensor.*\")" + ] + }, + { + "cell_type": "markdown", + "id": "e245765e-f360-4693-b097-b595360471e3", + "metadata": {}, + "source": [ + "### 2.2. Data\n", + "\n", + "A test dataset of `TWRT` (Total weight of roots) will be used to optimize\n", + "parametesr `TWDI` (total initial dry weight). Note that in root_dynamic, changes in `TWDI` dont affect `RD` (Current rooting depth). \n", + "\n", + "The data is stored in PCSE tests folder, and can be doewnloded from PCSE repsository.\n", + "You can select any of the files related to `root_dynamics` model with a file name that follwos the pattern\n", + "`test_rootdynamics_wofost72_*.yaml`. Each file contains different data depending on the locatin and crop type.\n", + "For example, you can download the file \"test_rootdynamics_wofost72_01.yaml\" as:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "22000922-68be-47ed-8afa-2e97c56bb502", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloaded: test_rootdynamics_wofost72_01.yaml\n" + ] + } + ], + "source": [ + "import urllib.request\n", + "\n", + "url = \"https://raw.githubusercontent.com/ajwdewit/pcse/refs/heads/master/tests/test_data/test_rootdynamics_wofost72_01.yaml\"\n", + "filename = \"test_rootdynamics_wofost72_01.yaml\"\n", + "\n", + "urllib.request.urlretrieve(url, filename)\n", + "print(f\"Downloaded: {filename}\")" + ] + }, + { + "cell_type": "markdown", + "id": "c097f6cb-0ded-4b62-844c-795de83df130", + "metadata": {}, + "source": [ + "We also need to download a config file to be able to run each crop module. This will change in the future versions. To donwload the config file, you can use the following command:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "ae2de6dc-1294-4a5a-91d0-3e83f28dc892", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloaded: WOFOST_Root_Dynamics.conf\n" + ] + } + ], + "source": [ + "url = \"https://raw.githubusercontent.com/WUR-AI/diffWOFOST/refs/heads/main/tests/physical_models/test_data/WOFOST_Root_Dynamics.conf\"\n", + "filename = \"WOFOST_Root_Dynamics.conf\"\n", + "\n", + "urllib.request.urlretrieve(url, filename)\n", + "print(f\"Downloaded: {filename}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "a69e9279-49eb-4136-8f15-7cff8bb4af52", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Check the path to the files that are downloaded as explained above ----\n", + "test_data_path = \"test_rootdynamics_wofost72_01.yaml\"\n", + "config_path = \"WOFOST_Root_Dynamics.conf\"" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "d560476b-64f9-422c-9722-8d0778cfc574", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Here we read the test data and set some variables ----\n", + "test_data = get_test_data(test_data_path)\n", + "(crop_model_params_provider, weather_data_provider, agro_management_inputs, external_states) = (\n", + " prepare_engine_input(test_data, [\"RDI\", \"RRI\", \"RDMCR\", \"RDMSOL\", \"TDWI\", \"IAIRDU\"])\n", + ")\n", + "\n", + "expected_results = test_data[\"ModelResults\"]\n", + "expected_twrt = torch.tensor(\n", + " [float(item[\"TWRT\"]) for item in expected_results], dtype=torch.float32\n", + ") # shape: [1, time_steps]\n", + "\n", + "# ---- dont change this: in this config file we specified the diffrentiable version of root_dynamics ----\n", + "config_path = str(Path(config_path).resolve()) " + ] + }, + { + "cell_type": "markdown", + "id": "d1cef2bd-e42b-4edb-b9b5-b367f776336b", + "metadata": {}, + "source": [ + "### 2.3. Helper classes/functions\n", + "\n", + "The model parameters shoudl stay in a valid range. To ensure this, we will use\n", + "`BoundedParameter` class with (min, max) and initial values for each\n", + "parameter. You might change these values depending on the crop type and\n", + "location. But dont use a very small range, otherwise gradiants will be very\n", + "small and the optimization will be very slow." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cbfc9e28-3233-4808-b658-36d6f7d18d75", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Adjust the values if needed ----\n", + "TDWI_MIN, TDWI_MAX, TDWI_INIT = (0.0, 1.0, 0.30)\n", + "\n", + "# ---- Helper for bounded parameters ----\n", + "class BoundedParameter(torch.nn.Module):\n", + " def __init__(self, low, high, init_value):\n", + " super().__init__()\n", + " self.low = low\n", + " self.high = high\n", + "\n", + " # Normalize to [0, 1]\n", + " init_norm = (init_value - low) / (high - low)\n", + "\n", + " # Parameter in raw logit space\n", + " self.raw = torch.nn.Parameter(torch.logit(torch.tensor(init_norm, dtype=torch.float32), eps=1e-6))\n", + "\n", + " def forward(self):\n", + " return self.low + (self.high - self.low) * torch.sigmoid(self.raw)\n" + ] + }, + { + "cell_type": "markdown", + "id": "e978cc84-42b4-479e-9817-e5b4ac9fafc8", + "metadata": {}, + "source": [ + "Another helper class is `OptRootLeafDynamics` which is a subclass of `torch.nn.Module`. \n", + "We use this class to wrap the `EngineTestHelper` function and make it easier to run the model `root_dynamic`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "90296352-4817-4586-842c-12ac6d97d779", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Wrap the model with torch.nn.Module----\n", + "class OptDiffRootDynamics(torch.nn.Module):\n", + " def __init__(self, crop_model_params_provider, weather_data_provider, agro_management_inputs, config_path, external_states):\n", + " super().__init__()\n", + " self.crop_model_params_provider = crop_model_params_provider\n", + " self.weather_data_provider = weather_data_provider\n", + " self.agro_management_inputs = agro_management_inputs\n", + " self.config_path = config_path\n", + " self.external_states = external_states\n", + "\n", + " # bounded parameters\n", + " self.tdwi = BoundedParameter(TDWI_MIN, TDWI_MAX, init_value=TDWI_INIT)\n", + " \n", + " def forward(self):\n", + " # currently, copying is needed due to an internal issue in engine\n", + " crop_model_params_provider_ = copy.deepcopy(self.crop_model_params_provider)\n", + " external_states_ = copy.deepcopy(self.external_states)\n", + " \n", + " tdwi_val = self.tdwi()\n", + " \n", + " # pass new value of parameters to the model\n", + " crop_model_params_provider_.set_override(\"TDWI\", tdwi_val, check=False)\n", + "\n", + " engine = EngineTestHelper(\n", + " crop_model_params_provider_,\n", + " self.weather_data_provider,\n", + " self.agro_management_inputs,\n", + " self.config_path,\n", + " external_states_,\n", + " )\n", + " engine.run_till_terminate()\n", + " results = engine.get_output()\n", + " \n", + " return torch.stack([item[\"TWRT\"] for item in results]) # shape: [1, time_steps]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f2a1a612-f33c-48a0-a3d4-31318d78b9f4", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Create model ---- \n", + "opt_model = OptDiffRootDynamics(\n", + " crop_model_params_provider,\n", + " weather_data_provider,\n", + " agro_management_inputs,\n", + " config_path,\n", + " external_states,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "6c09d1ce-db66-46f3-93d2-1b16f8196e49", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Step 0, Loss 0.00000021, TDWI 0.5340\n", + "Step 10, Loss 0.00000015, TDWI 0.5137\n", + "Step 20, Loss 0.00000164, TDWI 0.5144\n", + "Step 30, Loss 0.00000068, TDWI 0.5107\n", + "Step 40, Loss 0.00000019, TDWI 0.5072\n", + "Step 50, Loss 0.00000080, TDWI 0.5084\n", + "Step 60, Loss 0.00000007, TDWI 0.5071\n", + "Step 70, Loss 0.00000100, TDWI 0.5091\n", + "Step 80, Loss 0.00000131, TDWI 0.5089\n", + "Step 90, Loss 0.00000068, TDWI 0.5115\n", + "Step 100, Loss 0.00000071, TDWI 0.5053\n" + ] + } + ], + "source": [ + "# ---- Optimizer ---- \n", + "optimizer = torch.optim.Adam(opt_model.parameters(), lr=0.1)\n", + "\n", + "# ---- We use relative MAE as loss because there are two outputs with different untis ---- \n", + "denom = torch.mean(torch.abs(expected_twrt)) \n", + "\n", + "# Training loop (example)\n", + "for step in range(101):\n", + " optimizer.zero_grad()\n", + " results = opt_model() \n", + " mae = torch.mean(torch.abs(results - expected_twrt))\n", + " loss = mae / denom # example: relative mean absolute error\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " if step % 10 == 0:\n", + " print(f\"Step {step}, Loss {loss.item():.8f}, TDWI {opt_model.tdwi().item():.4f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2029e2dd-7460-4c71-85a9-b1030a43167b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Actual TDWI 0.5100\n" + ] + } + ], + "source": [ + "# ---- validate the results using test data ---- \n", + "print(f\"Actual TDWI {crop_model_params_provider[\"TDWI\"].item():.4f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6a511a4-f269-4af4-9f51-2dafa9ba38c0", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From a2497127aaf497d4b80d34d20ed7e2a0b47fe4fe Mon Sep 17 00:00:00 2001 From: SarahAlidoost Date: Wed, 17 Dec 2025 15:55:59 +0100 Subject: [PATCH 06/10] rerun the notebooks --- .../optimization_leaf_dynamics.ipynb | 459 +++-------------- .../optimization_root_dynamics.ipynb | 473 +++--------------- 2 files changed, 115 insertions(+), 817 deletions(-) diff --git a/docs/notebooks/optimization_leaf_dynamics.ipynb b/docs/notebooks/optimization_leaf_dynamics.ipynb index 740db66..d146cf2 100644 --- a/docs/notebooks/optimization_leaf_dynamics.ipynb +++ b/docs/notebooks/optimization_leaf_dynamics.ipynb @@ -15,18 +15,12 @@ "differentiable model using the `diffwofost` package. The package provides\n", "differentiable implementations of the WOFOST model and its associated\n", "sub-models. As `diffwofost` is under active development, this notebook focuses on\n", - "two sub-models: `leaf_dynamics` and `root_dynamics`. \n", + "`leaf_dynamics`. \n", "\n", "To enable these models to operate independently, certain state variables\n", "required by the model are supplied as \"external states\" derived from the test\n", "data. Also, at this stage, only a limited subset of model parameters has been made\n", - "differentiable.\n", - "\n", - "The notebook is organized into two standalone sections that can\n", - "be executed independently:\n", - "\n", - " 1. Leaf Dynamics\n", - " 2. Root Dynamics" + "differentiable." ] }, { @@ -78,6 +72,8 @@ "import numpy\n", "import yaml\n", "from pathlib import Path\n", + "from diffwofost.physical_models.config import Configuration\n", + "from diffwofost.physical_models.crop.leaf_dynamics import WOFOST_Leaf_Dynamics\n", "from diffwofost.physical_models.utils import EngineTestHelper\n", "from diffwofost.physical_models.utils import prepare_engine_input\n", "from diffwofost.physical_models.utils import get_test_data" @@ -137,51 +133,20 @@ "print(f\"Downloaded: {filename}\")" ] }, - { - "cell_type": "markdown", - "id": "e4565b6b-523c-49c4-934e-500248317461", - "metadata": {}, - "source": [ - "We also need to download a config file to be able to run each crop module. This will change in the future versions. To donwload the config file, you can use the following command:" - ] - }, { "cell_type": "code", "execution_count": 4, - "id": "b4a24f1c-77e4-4b05-bde9-229dd497f09e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Downloaded: WOFOST_Leaf_Dynamics.conf\n" - ] - } - ], - "source": [ - "url = \"https://raw.githubusercontent.com/WUR-AI/diffWOFOST/refs/heads/main/tests/physical_models/test_data/WOFOST_Leaf_Dynamics.conf\"\n", - "filename = \"WOFOST_Leaf_Dynamics.conf\"\n", - "\n", - "urllib.request.urlretrieve(url, filename)\n", - "print(f\"Downloaded: {filename}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, "id": "5a459489-bfcb-4ad6-9102-1b6be5edeb52", "metadata": {}, "outputs": [], "source": [ "# ---- Check the path to the files that are downloaded as explained above ----\n", - "test_data_path = \"test_leafdynamics_wofost72_01.yaml\"\n", - "config_path = \"WOFOST_Leaf_Dynamics.conf\"" + "test_data_path = \"test_leafdynamics_wofost72_01.yaml\"" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "9f3105fb-4fbe-4405-9fd4-e8255b4b119e", "metadata": {}, "outputs": [], @@ -198,7 +163,10 @@ ").unsqueeze(0) # shape: [1, time_steps, 2]\n", "\n", "# ---- dont change this: in this config file we specified the diffrentiable version of leaf_dynamics ----\n", - "config_path = str(Path(config_path).resolve()) " + "leaf_dynamics_config = Configuration(\n", + " CROP=WOFOST_Leaf_Dynamics,\n", + " OUTPUT_VARS=[\"LAI\", \"TWLV\"],\n", + ")" ] }, { @@ -217,14 +185,14 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 6, "id": "e4610238-de0d-42cf-9689-3c074eb2cc0e", "metadata": {}, "outputs": [], "source": [ "# ---- Adjust the values if needed ----\n", "TDWI_MIN, TDWI_MAX, TDWI_INIT = (0.0, 1.0, 0.40)\n", - "SPAN_MIN, SPAN_MAX, SPAN_INIT = (10.0, 60.0, 30.0)\n", + "SPAN_MIN, SPAN_MAX, SPAN_INIT = (10.0, 60.0, 25.0)\n", "\n", "# ---- Helper for bounded parameters ----\n", "class BoundedParameter(torch.nn.Module):\n", @@ -254,19 +222,19 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 7, "id": "36dd6463-4812-41c0-b2bf-d4769df1136f", "metadata": {}, "outputs": [], "source": [ "# ---- Wrap the model with torch.nn.Module----\n", "class OptDiffLeafDynamics(torch.nn.Module):\n", - " def __init__(self, crop_model_params_provider, weather_data_provider, agro_management_inputs, config_path, external_states):\n", + " def __init__(self, crop_model_params_provider, weather_data_provider, agro_management_inputs, leaf_dynamics_config, external_states):\n", " super().__init__()\n", " self.crop_model_params_provider = crop_model_params_provider\n", " self.weather_data_provider = weather_data_provider\n", " self.agro_management_inputs = agro_management_inputs\n", - " self.config_path = config_path\n", + " self.config = leaf_dynamics_config\n", " self.external_states = external_states\n", "\n", " # bounded parameters\n", @@ -289,7 +257,7 @@ " crop_model_params_provider_,\n", " self.weather_data_provider,\n", " self.agro_management_inputs,\n", - " self.config_path,\n", + " self.config,\n", " external_states_,\n", " )\n", " engine.run_till_terminate()\n", @@ -302,7 +270,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 8, "id": "3d34c3e8-a8d7-4bc9-94ed-bd2e0234e95c", "metadata": {}, "outputs": [], @@ -312,14 +280,14 @@ " crop_model_params_provider,\n", " weather_data_provider,\n", " agro_management_inputs,\n", - " config_path,\n", + " leaf_dynamics_config,\n", " external_states,\n", ")" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 10, "id": "78d797f5-4ac4-4380-85f3-6622a7b0f7fb", "metadata": {}, "outputs": [ @@ -327,21 +295,40 @@ "name": "stdout", "output_type": "stream", "text": [ - "Step 0, Loss 0.1351, TDWI 0.4242, SPAN 30.0000\n", - "Step 10, Loss 0.1348, TDWI 0.5577, SPAN 30.0000\n", - "Step 20, Loss 0.1347, TDWI 0.4957, SPAN 30.0000\n", - "Step 30, Loss 0.1347, TDWI 0.5045, SPAN 30.0000\n", - "Step 40, Loss 0.1347, TDWI 0.5069, SPAN 30.0000\n", - "Step 50, Loss 0.1347, TDWI 0.5071, SPAN 30.0000\n", - "Step 60, Loss 0.1347, TDWI 0.5071, SPAN 30.0000\n", - "Step 70, Loss 0.1347, TDWI 0.5093, SPAN 30.0000\n", - "Step 80, Loss 0.1347, TDWI 0.5092, SPAN 30.0000\n", - "Step 90, Loss 0.1347, TDWI 0.5007, SPAN 30.0000\n", - "Step 100, Loss 0.1347, TDWI 0.5123, SPAN 30.0000\n" + "Step 0, Loss 0.0070, TDWI 0.5341, SPAN 36.0670\n", + "Step 1, Loss 0.0271, TDWI 0.5325, SPAN 35.8151\n", + "Step 2, Loss 0.0216, TDWI 0.5223, SPAN 35.1632\n", + "Step 3, Loss 0.0040, TDWI 0.5078, SPAN 34.3827\n", + "Step 4, Loss 0.0169, TDWI 0.5028, SPAN 34.0915\n", + "Step 5, Loss 0.0262, TDWI 0.5041, SPAN 34.1150\n", + "Step 6, Loss 0.0261, TDWI 0.5098, SPAN 34.3638\n", + "Step 7, Loss 0.0174, TDWI 0.5189, SPAN 34.7887\n", + "Step 8, Loss 0.0083, TDWI 0.5229, SPAN 35.1052\n", + "Step 9, Loss 0.0022, TDWI 0.5224, SPAN 35.2232\n", + "Step 10, Loss 0.0048, TDWI 0.5183, SPAN 35.1611\n", + "Step 11, Loss 0.0040, TDWI 0.5112, SPAN 34.9521\n", + "Step 12, Loss 0.0012, TDWI 0.5019, SPAN 34.6612\n", + "Step 13, Loss 0.0114, TDWI 0.4970, SPAN 34.5845\n", + "Step 14, Loss 0.0126, TDWI 0.4959, SPAN 34.6862\n", + "Step 15, Loss 0.0104, TDWI 0.4981, SPAN 34.9379\n", + "Step 16, Loss 0.0016, TDWI 0.5030, SPAN 35.2729\n", + "Step 17, Loss 0.0059, TDWI 0.5101, SPAN 35.6397\n", + "Step 18, Loss 0.0173, TDWI 0.5137, SPAN 35.7822\n", + "Step 19, Loss 0.0207, TDWI 0.5140, SPAN 35.7163\n", + "Step 20, Loss 0.0194, TDWI 0.5114, SPAN 35.4857\n", + "Step 21, Loss 0.0111, TDWI 0.5062, SPAN 35.1246\n", + "Step 22, Loss 0.0026, TDWI 0.5043, SPAN 34.8779\n", + "Early stopping at step 22\n" ] } ], "source": [ + "# ---- Early stopping ---- \n", + "best_loss = float(\"inf\")\n", + "patience = 10 # Number of steps to wait for improvement\n", + "patience_counter = 0\n", + "min_delta = 1e-4 \n", + "\n", "# ---- Optimizer ---- \n", "optimizer = torch.optim.Adam(opt_model.parameters(), lr=0.1)\n", "\n", @@ -358,8 +345,16 @@ " loss.backward()\n", " optimizer.step()\n", "\n", - " if step % 10 == 0:\n", - " print(f\"Step {step}, Loss {loss.item():.4f}, TDWI {opt_model.tdwi().item():.4f}, SPAN {opt_model.span().item():.4f}\")" + " print(f\"Step {step}, Loss {loss.item():.4f}, TDWI {opt_model.tdwi().item():.4f}, SPAN {opt_model.span().item():.4f}\")\n", + " # Early stopping logic\n", + " if loss.item() < best_loss - min_delta:\n", + " best_loss = loss.item()\n", + " patience_counter = 0\n", + " else:\n", + " patience_counter += 1\n", + " if patience_counter >= patience:\n", + " print(f\"Early stopping at step {step}\")\n", + " break" ] }, { @@ -381,342 +376,6 @@ "print(f\"Actual TDWI {crop_model_params_provider[\"TDWI\"].item():.4f}, SPAN {crop_model_params_provider[\"SPAN\"].item():.4f}\")" ] }, - { - "cell_type": "markdown", - "id": "fedda7e9-02a6-40e5-8c61-08d7886c9519", - "metadata": {}, - "source": [ - "## 2. Root dynamics \n", - "\n", - "In this section, we will demonstrate how to optimize two parameters `TWDI` in\n", - "root_dynamics model using a differentiable version of root_dynamics.\n", - "The optimization will be done using the Adam optimizer from `torch.optim`." - ] - }, - { - "cell_type": "markdown", - "id": "152ec514-baf1-4213-b98b-76f453d49538", - "metadata": {}, - "source": [ - "### 2.1 software requirements\n", - "\n", - "To run this notebook, we need to install the `diffwofost`; the differentiable\n", - "version of WOFOST models. Since the package is constantly under development, make\n", - "sure you have the latest version of `diffwofost` installed in your\n", - "python environment. You can install it using pip:\n", - "\n", - "```bash\n", - "pip install diffwofost\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "eaa4c172-9719-4a79-b2f2-d37ea5b6f11d", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- import libraries ----\n", - "import copy\n", - "import torch\n", - "import numpy\n", - "import yaml\n", - "from pathlib import Path\n", - "from diffwofost.physical_models.utils import EngineTestHelper\n", - "from diffwofost.physical_models.utils import prepare_engine_input\n", - "from diffwofost.physical_models.utils import get_test_data" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "26437aed-755d-4ee3-b7b6-82caf8c30ec5", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- disable a warning: this will be fixed in the future ----\n", - "import warnings\n", - "warnings.filterwarnings(\"ignore\", message=\"To copy construct from a tensor.*\")" - ] - }, - { - "cell_type": "markdown", - "id": "e245765e-f360-4693-b097-b595360471e3", - "metadata": {}, - "source": [ - "### 2.2. Data\n", - "\n", - "A test dataset of `TWRT` (Total weight of roots) will be used to optimize\n", - "parametesr `TWDI` (total initial dry weight). Note that in root_dynamic, changes in `TWDI` dont affect `RD` (Current rooting depth). \n", - "\n", - "The data is stored in PCSE tests folder, and can be doewnloded from PCSE repsository.\n", - "You can select any of the files related to `root_dynamics` model with a file name that follwos the pattern\n", - "`test_rootdynamics_wofost72_*.yaml`. Each file contains different data depending on the locatin and crop type.\n", - "For example, you can download the file \"test_rootdynamics_wofost72_01.yaml\" as:" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "22000922-68be-47ed-8afa-2e97c56bb502", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Downloaded: test_rootdynamics_wofost72_01.yaml\n" - ] - } - ], - "source": [ - "import urllib.request\n", - "\n", - "url = \"https://raw.githubusercontent.com/ajwdewit/pcse/refs/heads/master/tests/test_data/test_rootdynamics_wofost72_01.yaml\"\n", - "filename = \"test_rootdynamics_wofost72_01.yaml\"\n", - "\n", - "urllib.request.urlretrieve(url, filename)\n", - "print(f\"Downloaded: {filename}\")" - ] - }, - { - "cell_type": "markdown", - "id": "c097f6cb-0ded-4b62-844c-795de83df130", - "metadata": {}, - "source": [ - "We also need to download a config file to be able to run each crop module. This will change in the future versions. To donwload the config file, you can use the following command:" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "ae2de6dc-1294-4a5a-91d0-3e83f28dc892", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Downloaded: WOFOST_Root_Dynamics.conf\n" - ] - } - ], - "source": [ - "url = \"https://raw.githubusercontent.com/WUR-AI/diffWOFOST/refs/heads/main/tests/physical_models/test_data/WOFOST_Root_Dynamics.conf\"\n", - "filename = \"WOFOST_Root_Dynamics.conf\"\n", - "\n", - "urllib.request.urlretrieve(url, filename)\n", - "print(f\"Downloaded: {filename}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "a69e9279-49eb-4136-8f15-7cff8bb4af52", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- Check the path to the files that are downloaded as explained above ----\n", - "test_data_path = \"test_rootdynamics_wofost72_01.yaml\"\n", - "config_path = \"WOFOST_Root_Dynamics.conf\"" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "d560476b-64f9-422c-9722-8d0778cfc574", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- Here we read the test data and set some variables ----\n", - "test_data = get_test_data(test_data_path)\n", - "(crop_model_params_provider, weather_data_provider, agro_management_inputs, external_states) = (\n", - " prepare_engine_input(test_data, [\"RDI\", \"RRI\", \"RDMCR\", \"RDMSOL\", \"TDWI\", \"IAIRDU\"])\n", - ")\n", - "\n", - "expected_results = test_data[\"ModelResults\"]\n", - "expected_twrt = torch.tensor(\n", - " [float(item[\"TWRT\"]) for item in expected_results], dtype=torch.float32\n", - ") # shape: [1, time_steps]\n", - "\n", - "# ---- dont change this: in this config file we specified the diffrentiable version of root_dynamics ----\n", - "config_path = str(Path(config_path).resolve()) " - ] - }, - { - "cell_type": "markdown", - "id": "d1cef2bd-e42b-4edb-b9b5-b367f776336b", - "metadata": {}, - "source": [ - "### 2.3. Helper classes/functions\n", - "\n", - "The model parameters shoudl stay in a valid range. To ensure this, we will use\n", - "`BoundedParameter` class with (min, max) and initial values for each\n", - "parameter. You might change these values depending on the crop type and\n", - "location. But dont use a very small range, otherwise gradiants will be very\n", - "small and the optimization will be very slow." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "cbfc9e28-3233-4808-b658-36d6f7d18d75", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- Adjust the values if needed ----\n", - "TDWI_MIN, TDWI_MAX, TDWI_INIT = (0.0, 1.0, 0.30)\n", - "\n", - "# ---- Helper for bounded parameters ----\n", - "class BoundedParameter(torch.nn.Module):\n", - " def __init__(self, low, high, init_value):\n", - " super().__init__()\n", - " self.low = low\n", - " self.high = high\n", - "\n", - " # Normalize to [0, 1]\n", - " init_norm = (init_value - low) / (high - low)\n", - "\n", - " # Parameter in raw logit space\n", - " self.raw = torch.nn.Parameter(torch.logit(torch.tensor(init_norm, dtype=torch.float32), eps=1e-6))\n", - "\n", - " def forward(self):\n", - " return self.low + (self.high - self.low) * torch.sigmoid(self.raw)\n" - ] - }, - { - "cell_type": "markdown", - "id": "e978cc84-42b4-479e-9817-e5b4ac9fafc8", - "metadata": {}, - "source": [ - "Another helper class is `OptRootLeafDynamics` which is a subclass of `torch.nn.Module`. \n", - "We use this class to wrap the `EngineTestHelper` function and make it easier to run the model `root_dynamic`." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "90296352-4817-4586-842c-12ac6d97d779", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- Wrap the model with torch.nn.Module----\n", - "class OptDiffRootDynamics(torch.nn.Module):\n", - " def __init__(self, crop_model_params_provider, weather_data_provider, agro_management_inputs, config_path, external_states):\n", - " super().__init__()\n", - " self.crop_model_params_provider = crop_model_params_provider\n", - " self.weather_data_provider = weather_data_provider\n", - " self.agro_management_inputs = agro_management_inputs\n", - " self.config_path = config_path\n", - " self.external_states = external_states\n", - "\n", - " # bounded parameters\n", - " self.tdwi = BoundedParameter(TDWI_MIN, TDWI_MAX, init_value=TDWI_INIT)\n", - " \n", - " def forward(self):\n", - " # currently, copying is needed due to an internal issue in engine\n", - " crop_model_params_provider_ = copy.deepcopy(self.crop_model_params_provider)\n", - " external_states_ = copy.deepcopy(self.external_states)\n", - " \n", - " tdwi_val = self.tdwi()\n", - " \n", - " # pass new value of parameters to the model\n", - " crop_model_params_provider_.set_override(\"TDWI\", tdwi_val, check=False)\n", - "\n", - " engine = EngineTestHelper(\n", - " crop_model_params_provider_,\n", - " self.weather_data_provider,\n", - " self.agro_management_inputs,\n", - " self.config_path,\n", - " external_states_,\n", - " )\n", - " engine.run_till_terminate()\n", - " results = engine.get_output()\n", - " \n", - " return torch.stack([item[\"TWRT\"] for item in results]) # shape: [1, time_steps]" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "f2a1a612-f33c-48a0-a3d4-31318d78b9f4", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- Create model ---- \n", - "opt_model = OptDiffRootDynamics(\n", - " crop_model_params_provider,\n", - " weather_data_provider,\n", - " agro_management_inputs,\n", - " config_path,\n", - " external_states,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "6c09d1ce-db66-46f3-93d2-1b16f8196e49", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Step 0, Loss 0.00000021, TDWI 0.5340\n", - "Step 10, Loss 0.00000015, TDWI 0.5137\n", - "Step 20, Loss 0.00000164, TDWI 0.5144\n", - "Step 30, Loss 0.00000068, TDWI 0.5107\n", - "Step 40, Loss 0.00000019, TDWI 0.5072\n", - "Step 50, Loss 0.00000080, TDWI 0.5084\n", - "Step 60, Loss 0.00000007, TDWI 0.5071\n", - "Step 70, Loss 0.00000100, TDWI 0.5091\n", - "Step 80, Loss 0.00000131, TDWI 0.5089\n", - "Step 90, Loss 0.00000068, TDWI 0.5115\n", - "Step 100, Loss 0.00000071, TDWI 0.5053\n" - ] - } - ], - "source": [ - "# ---- Optimizer ---- \n", - "optimizer = torch.optim.Adam(opt_model.parameters(), lr=0.1)\n", - "\n", - "# ---- We use relative MAE as loss because there are two outputs with different untis ---- \n", - "denom = torch.mean(torch.abs(expected_twrt)) \n", - "\n", - "# Training loop (example)\n", - "for step in range(101):\n", - " optimizer.zero_grad()\n", - " results = opt_model() \n", - " mae = torch.mean(torch.abs(results - expected_twrt))\n", - " loss = mae / denom # example: relative mean absolute error\n", - " loss.backward()\n", - " optimizer.step()\n", - "\n", - " if step % 10 == 0:\n", - " print(f\"Step {step}, Loss {loss.item():.8f}, TDWI {opt_model.tdwi().item():.4f}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "2029e2dd-7460-4c71-85a9-b1030a43167b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Actual TDWI 0.5100\n" - ] - } - ], - "source": [ - "# ---- validate the results using test data ---- \n", - "print(f\"Actual TDWI {crop_model_params_provider[\"TDWI\"].item():.4f}\")" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/docs/notebooks/optimization_root_dynamics.ipynb b/docs/notebooks/optimization_root_dynamics.ipynb index 740db66..90a0d01 100644 --- a/docs/notebooks/optimization_root_dynamics.ipynb +++ b/docs/notebooks/optimization_root_dynamics.ipynb @@ -15,370 +15,12 @@ "differentiable model using the `diffwofost` package. The package provides\n", "differentiable implementations of the WOFOST model and its associated\n", "sub-models. As `diffwofost` is under active development, this notebook focuses on\n", - "two sub-models: `leaf_dynamics` and `root_dynamics`. \n", + "`root_dynamics`. \n", "\n", "To enable these models to operate independently, certain state variables\n", "required by the model are supplied as \"external states\" derived from the test\n", "data. Also, at this stage, only a limited subset of model parameters has been made\n", - "differentiable.\n", - "\n", - "The notebook is organized into two standalone sections that can\n", - "be executed independently:\n", - "\n", - " 1. Leaf Dynamics\n", - " 2. Root Dynamics" - ] - }, - { - "cell_type": "markdown", - "id": "41262fbd-270b-4616-91ad-09ee82451604", - "metadata": {}, - "source": [ - "## 1. Leaf dynamics\n", - "\n", - "In this section, we will demonstrate how to optimize two parameters `TWDI` and `SPAN` in\n", - "leaf_dynamics model using a differentiable version of leaf_dynamics.\n", - "The optimization will be done using the Adam optimizer from `torch.optim`." - ] - }, - { - "cell_type": "markdown", - "id": "1b6c3f53-6fab-4537-9177-7b16e0a1ccec", - "metadata": {}, - "source": [ - "### 1.1 software requirements\n", - "\n", - "To run this notebook, we need to install the `diffwofost`; the differentiable\n", - "version of WOFOST models. Since the package is constantly under development, make\n", - "sure you have the latest version of `diffwofost` installed in your\n", - "python environment. You can install it using pip:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e4049fea-1d05-41f1-bf9d-f030ae83a324", - "metadata": {}, - "outputs": [], - "source": [ - "# install diffwofost\n", - "!pip install diffwofost" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "21731653-3976-4bb9-b83b-b11d78211700", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- import libraries ----\n", - "import copy\n", - "import torch\n", - "import numpy\n", - "import yaml\n", - "from pathlib import Path\n", - "from diffwofost.physical_models.utils import EngineTestHelper\n", - "from diffwofost.physical_models.utils import prepare_engine_input\n", - "from diffwofost.physical_models.utils import get_test_data" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "82a1ef6b-336e-4902-8bd1-2a1ed2020f9d", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- disable a warning: this will be fixed in the future ----\n", - "import warnings\n", - "warnings.filterwarnings(\"ignore\", message=\"To copy construct from a tensor.*\")" - ] - }, - { - "cell_type": "markdown", - "id": "47def7fc-f2dd-4aaf-a572-41cc9d1e4679", - "metadata": {}, - "source": [ - "### 1.2. Data\n", - "\n", - "A test dataset of `LAI` (Leaf area index, including stem and pod area) and\n", - "`TWLV` (Dry weight of total leaves (living + dead)) will be used to optimize\n", - "parametesr `TWDI` (total initial dry weight) and `SPAN` (life span of leaves).\n", - "Note that in leaf_dynamic, changes in `SPAN` dont affect `TWLV`. \n", - "\n", - "The data is stored in PCSE tests folder, and can be doewnloded from PCSE repsository.\n", - "You can select any of the files related to `leaf_dynamics` model with a file name that follwos the pattern\n", - "`test_leafdynamics_wofost72_*.yaml`. Each file contains different data depending on the locatin and crop type.\n", - "For example, you can download the file \"test_leafdynamics_wofost72_01.yaml\" as:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "0233a048-e5a2-4249-887d-35a37284769c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Downloaded: test_leafdynamics_wofost72_01.yaml\n" - ] - } - ], - "source": [ - "import urllib.request\n", - "\n", - "url = \"https://raw.githubusercontent.com/ajwdewit/pcse/refs/heads/master/tests/test_data/test_leafdynamics_wofost72_01.yaml\"\n", - "filename = \"test_leafdynamics_wofost72_01.yaml\"\n", - "\n", - "urllib.request.urlretrieve(url, filename)\n", - "print(f\"Downloaded: {filename}\")" - ] - }, - { - "cell_type": "markdown", - "id": "e4565b6b-523c-49c4-934e-500248317461", - "metadata": {}, - "source": [ - "We also need to download a config file to be able to run each crop module. This will change in the future versions. To donwload the config file, you can use the following command:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "b4a24f1c-77e4-4b05-bde9-229dd497f09e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Downloaded: WOFOST_Leaf_Dynamics.conf\n" - ] - } - ], - "source": [ - "url = \"https://raw.githubusercontent.com/WUR-AI/diffWOFOST/refs/heads/main/tests/physical_models/test_data/WOFOST_Leaf_Dynamics.conf\"\n", - "filename = \"WOFOST_Leaf_Dynamics.conf\"\n", - "\n", - "urllib.request.urlretrieve(url, filename)\n", - "print(f\"Downloaded: {filename}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "5a459489-bfcb-4ad6-9102-1b6be5edeb52", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- Check the path to the files that are downloaded as explained above ----\n", - "test_data_path = \"test_leafdynamics_wofost72_01.yaml\"\n", - "config_path = \"WOFOST_Leaf_Dynamics.conf\"" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "9f3105fb-4fbe-4405-9fd4-e8255b4b119e", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- Here we read the test data and set some variables ----\n", - "test_data = get_test_data(test_data_path)\n", - "(crop_model_params_provider, weather_data_provider, agro_management_inputs, external_states) = (\n", - " prepare_engine_input(test_data, [\"SPAN\", \"TDWI\", \"TBASE\", \"PERDL\", \"RGRLAI\"])\n", - ")\n", - "\n", - "expected_results = test_data[\"ModelResults\"]\n", - "expected_lai_twlv = torch.tensor(\n", - " [[float(item[\"LAI\"]), float(item[\"TWLV\"])] for item in expected_results], dtype=torch.float32\n", - ").unsqueeze(0) # shape: [1, time_steps, 2]\n", - "\n", - "# ---- dont change this: in this config file we specified the diffrentiable version of leaf_dynamics ----\n", - "config_path = str(Path(config_path).resolve()) " - ] - }, - { - "cell_type": "markdown", - "id": "52b19ae2-3fe6-4b3f-95a7-aea07a2c0958", - "metadata": {}, - "source": [ - "### 1.3. Helper classes/functions\n", - "\n", - "The model parameters shoudl stay in a valid range. To ensure this, we will use\n", - "`BoundedParameter` class with (min, max) and initial values for each\n", - "parameter. You might change these values depending on the crop type and\n", - "location. But dont use a very small range, otherwise gradiants will be very\n", - "small and the optimization will be very slow." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "e4610238-de0d-42cf-9689-3c074eb2cc0e", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- Adjust the values if needed ----\n", - "TDWI_MIN, TDWI_MAX, TDWI_INIT = (0.0, 1.0, 0.40)\n", - "SPAN_MIN, SPAN_MAX, SPAN_INIT = (10.0, 60.0, 30.0)\n", - "\n", - "# ---- Helper for bounded parameters ----\n", - "class BoundedParameter(torch.nn.Module):\n", - " def __init__(self, low, high, init_value):\n", - " super().__init__()\n", - " self.low = low\n", - " self.high = high\n", - "\n", - " # Normalize to [0, 1]\n", - " init_norm = (init_value - low) / (high - low)\n", - "\n", - " # Parameter in raw logit space\n", - " self.raw = torch.nn.Parameter(torch.logit(torch.tensor(init_norm, dtype=torch.float32), eps=1e-6))\n", - "\n", - " def forward(self):\n", - " return self.low + (self.high - self.low) * torch.sigmoid(self.raw)\n" - ] - }, - { - "cell_type": "markdown", - "id": "153e4306-77ed-4278-8797-a04e637e12c8", - "metadata": {}, - "source": [ - "Another helper class is `OptDiffLeafDynamics` which is a subclass of `torch.nn.Module`. \n", - "We use this class to wrap the `EngineTestHelper` function and make it easier to run the model `leaf_dynamic`." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "36dd6463-4812-41c0-b2bf-d4769df1136f", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- Wrap the model with torch.nn.Module----\n", - "class OptDiffLeafDynamics(torch.nn.Module):\n", - " def __init__(self, crop_model_params_provider, weather_data_provider, agro_management_inputs, config_path, external_states):\n", - " super().__init__()\n", - " self.crop_model_params_provider = crop_model_params_provider\n", - " self.weather_data_provider = weather_data_provider\n", - " self.agro_management_inputs = agro_management_inputs\n", - " self.config_path = config_path\n", - " self.external_states = external_states\n", - "\n", - " # bounded parameters\n", - " self.tdwi = BoundedParameter(TDWI_MIN, TDWI_MAX, init_value=TDWI_INIT)\n", - " self.span = BoundedParameter(SPAN_MIN, SPAN_MAX, init_value=SPAN_INIT)\n", - "\n", - " def forward(self):\n", - " # currently, copying is needed due to an internal issue in engine\n", - " crop_model_params_provider_ = copy.deepcopy(self.crop_model_params_provider)\n", - " external_states_ = copy.deepcopy(self.external_states)\n", - " \n", - " tdwi_val = self.tdwi()\n", - " span_val = self.span()\n", - " \n", - " # pass new value of parameters to the model\n", - " crop_model_params_provider_.set_override(\"TDWI\", tdwi_val, check=False)\n", - " crop_model_params_provider_.set_override(\"SPAN\", span_val, check=False)\n", - "\n", - " engine = EngineTestHelper(\n", - " crop_model_params_provider_,\n", - " self.weather_data_provider,\n", - " self.agro_management_inputs,\n", - " self.config_path,\n", - " external_states_,\n", - " )\n", - " engine.run_till_terminate()\n", - " results = engine.get_output()\n", - " \n", - " return torch.stack(\n", - " [torch.stack([item[\"LAI\"], item[\"TWLV\"]]) for item in results]\n", - " ).unsqueeze(0) # shape: [1, time_steps, 2]" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "3d34c3e8-a8d7-4bc9-94ed-bd2e0234e95c", - "metadata": {}, - "outputs": [], - "source": [ - "# ---- Create model ---- \n", - "opt_model = OptDiffLeafDynamics(\n", - " crop_model_params_provider,\n", - " weather_data_provider,\n", - " agro_management_inputs,\n", - " config_path,\n", - " external_states,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "78d797f5-4ac4-4380-85f3-6622a7b0f7fb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Step 0, Loss 0.1351, TDWI 0.4242, SPAN 30.0000\n", - "Step 10, Loss 0.1348, TDWI 0.5577, SPAN 30.0000\n", - "Step 20, Loss 0.1347, TDWI 0.4957, SPAN 30.0000\n", - "Step 30, Loss 0.1347, TDWI 0.5045, SPAN 30.0000\n", - "Step 40, Loss 0.1347, TDWI 0.5069, SPAN 30.0000\n", - "Step 50, Loss 0.1347, TDWI 0.5071, SPAN 30.0000\n", - "Step 60, Loss 0.1347, TDWI 0.5071, SPAN 30.0000\n", - "Step 70, Loss 0.1347, TDWI 0.5093, SPAN 30.0000\n", - "Step 80, Loss 0.1347, TDWI 0.5092, SPAN 30.0000\n", - "Step 90, Loss 0.1347, TDWI 0.5007, SPAN 30.0000\n", - "Step 100, Loss 0.1347, TDWI 0.5123, SPAN 30.0000\n" - ] - } - ], - "source": [ - "# ---- Optimizer ---- \n", - "optimizer = torch.optim.Adam(opt_model.parameters(), lr=0.1)\n", - "\n", - "# ---- We use relative MAE as loss because there are two outputs with different untis ---- \n", - "denom = torch.mean(torch.abs(expected_lai_twlv), dim=1) \n", - "\n", - "# Training loop (example)\n", - "for step in range(101):\n", - " optimizer.zero_grad()\n", - " results = opt_model() \n", - " mae = torch.mean(torch.abs(results - expected_lai_twlv), dim=1)\n", - " rmae = mae / denom\n", - " loss = rmae.sum() # example: relative mean absolute error\n", - " loss.backward()\n", - " optimizer.step()\n", - "\n", - " if step % 10 == 0:\n", - " print(f\"Step {step}, Loss {loss.item():.4f}, TDWI {opt_model.tdwi().item():.4f}, SPAN {opt_model.span().item():.4f}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "c2d3a463-43a4-4b29-a71f-696c019343d3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Actual TDWI 0.5100, SPAN 35.0000\n" - ] - } - ], - "source": [ - "# ---- validate the results using test data ---- \n", - "print(f\"Actual TDWI {crop_model_params_provider[\"TDWI\"].item():.4f}, SPAN {crop_model_params_provider[\"SPAN\"].item():.4f}\")" + "differentiable." ] }, { @@ -386,7 +28,7 @@ "id": "fedda7e9-02a6-40e5-8c61-08d7886c9519", "metadata": {}, "source": [ - "## 2. Root dynamics \n", + "## 1. Root dynamics \n", "\n", "In this section, we will demonstrate how to optimize two parameters `TWDI` in\n", "root_dynamics model using a differentiable version of root_dynamics.\n", @@ -398,7 +40,7 @@ "id": "152ec514-baf1-4213-b98b-76f453d49538", "metadata": {}, "source": [ - "### 2.1 software requirements\n", + "### 1.1 software requirements\n", "\n", "To run this notebook, we need to install the `diffwofost`; the differentiable\n", "version of WOFOST models. Since the package is constantly under development, make\n", @@ -412,7 +54,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 1, "id": "eaa4c172-9719-4a79-b2f2-d37ea5b6f11d", "metadata": {}, "outputs": [], @@ -423,6 +65,8 @@ "import numpy\n", "import yaml\n", "from pathlib import Path\n", + "from diffwofost.physical_models.config import Configuration\n", + "from diffwofost.physical_models.crop.root_dynamics import WOFOST_Root_Dynamics\n", "from diffwofost.physical_models.utils import EngineTestHelper\n", "from diffwofost.physical_models.utils import prepare_engine_input\n", "from diffwofost.physical_models.utils import get_test_data" @@ -430,7 +74,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 2, "id": "26437aed-755d-4ee3-b7b6-82caf8c30ec5", "metadata": {}, "outputs": [], @@ -445,7 +89,7 @@ "id": "e245765e-f360-4693-b097-b595360471e3", "metadata": {}, "source": [ - "### 2.2. Data\n", + "### 1.2. Data\n", "\n", "A test dataset of `TWRT` (Total weight of roots) will be used to optimize\n", "parametesr `TWDI` (total initial dry weight). Note that in root_dynamic, changes in `TWDI` dont affect `RD` (Current rooting depth). \n", @@ -458,7 +102,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 3, "id": "22000922-68be-47ed-8afa-2e97c56bb502", "metadata": {}, "outputs": [ @@ -490,41 +134,18 @@ }, { "cell_type": "code", - "execution_count": 23, - "id": "ae2de6dc-1294-4a5a-91d0-3e83f28dc892", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Downloaded: WOFOST_Root_Dynamics.conf\n" - ] - } - ], - "source": [ - "url = \"https://raw.githubusercontent.com/WUR-AI/diffWOFOST/refs/heads/main/tests/physical_models/test_data/WOFOST_Root_Dynamics.conf\"\n", - "filename = \"WOFOST_Root_Dynamics.conf\"\n", - "\n", - "urllib.request.urlretrieve(url, filename)\n", - "print(f\"Downloaded: {filename}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 24, + "execution_count": 4, "id": "a69e9279-49eb-4136-8f15-7cff8bb4af52", "metadata": {}, "outputs": [], "source": [ "# ---- Check the path to the files that are downloaded as explained above ----\n", - "test_data_path = \"test_rootdynamics_wofost72_01.yaml\"\n", - "config_path = \"WOFOST_Root_Dynamics.conf\"" + "test_data_path = \"test_rootdynamics_wofost72_01.yaml\"" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 5, "id": "d560476b-64f9-422c-9722-8d0778cfc574", "metadata": {}, "outputs": [], @@ -541,7 +162,10 @@ ") # shape: [1, time_steps]\n", "\n", "# ---- dont change this: in this config file we specified the diffrentiable version of root_dynamics ----\n", - "config_path = str(Path(config_path).resolve()) " + "root_dynamics_config = Configuration(\n", + " CROP=WOFOST_Root_Dynamics,\n", + " OUTPUT_VARS=[\"RD\", \"TWRT\"],\n", + ")" ] }, { @@ -549,7 +173,7 @@ "id": "d1cef2bd-e42b-4edb-b9b5-b367f776336b", "metadata": {}, "source": [ - "### 2.3. Helper classes/functions\n", + "### 1.3. Helper classes/functions\n", "\n", "The model parameters shoudl stay in a valid range. To ensure this, we will use\n", "`BoundedParameter` class with (min, max) and initial values for each\n", @@ -560,7 +184,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "cbfc9e28-3233-4808-b658-36d6f7d18d75", "metadata": {}, "outputs": [], @@ -590,25 +214,25 @@ "id": "e978cc84-42b4-479e-9817-e5b4ac9fafc8", "metadata": {}, "source": [ - "Another helper class is `OptRootLeafDynamics` which is a subclass of `torch.nn.Module`. \n", + "Another helper class is `OptRootDynamics` which is a subclass of `torch.nn.Module`. \n", "We use this class to wrap the `EngineTestHelper` function and make it easier to run the model `root_dynamic`." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "90296352-4817-4586-842c-12ac6d97d779", "metadata": {}, "outputs": [], "source": [ "# ---- Wrap the model with torch.nn.Module----\n", "class OptDiffRootDynamics(torch.nn.Module):\n", - " def __init__(self, crop_model_params_provider, weather_data_provider, agro_management_inputs, config_path, external_states):\n", + " def __init__(self, crop_model_params_provider, weather_data_provider, agro_management_inputs, root_dynamics_config, external_states):\n", " super().__init__()\n", " self.crop_model_params_provider = crop_model_params_provider\n", " self.weather_data_provider = weather_data_provider\n", " self.agro_management_inputs = agro_management_inputs\n", - " self.config_path = config_path\n", + " self.config = root_dynamics_config\n", " self.external_states = external_states\n", "\n", " # bounded parameters\n", @@ -628,7 +252,7 @@ " crop_model_params_provider_,\n", " self.weather_data_provider,\n", " self.agro_management_inputs,\n", - " self.config_path,\n", + " self.config,\n", " external_states_,\n", " )\n", " engine.run_till_terminate()\n", @@ -639,7 +263,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "f2a1a612-f33c-48a0-a3d4-31318d78b9f4", "metadata": {}, "outputs": [], @@ -649,14 +273,14 @@ " crop_model_params_provider,\n", " weather_data_provider,\n", " agro_management_inputs,\n", - " config_path,\n", + " root_dynamics_config,\n", " external_states,\n", ")" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "id": "6c09d1ce-db66-46f3-93d2-1b16f8196e49", "metadata": {}, "outputs": [ @@ -664,21 +288,28 @@ "name": "stdout", "output_type": "stream", "text": [ - "Step 0, Loss 0.00000021, TDWI 0.5340\n", - "Step 10, Loss 0.00000015, TDWI 0.5137\n", - "Step 20, Loss 0.00000164, TDWI 0.5144\n", - "Step 30, Loss 0.00000068, TDWI 0.5107\n", - "Step 40, Loss 0.00000019, TDWI 0.5072\n", - "Step 50, Loss 0.00000080, TDWI 0.5084\n", - "Step 60, Loss 0.00000007, TDWI 0.5071\n", - "Step 70, Loss 0.00000100, TDWI 0.5091\n", - "Step 80, Loss 0.00000131, TDWI 0.5089\n", - "Step 90, Loss 0.00000068, TDWI 0.5115\n", - "Step 100, Loss 0.00000071, TDWI 0.5053\n" + "Step 0, Loss 0.00000787, TDWI 0.5207\n", + "Step 1, Loss 0.00000237, TDWI 0.4957\n", + "Step 2, Loss 0.00000316, TDWI 0.4892\n", + "Step 3, Loss 0.00000459, TDWI 0.4919\n", + "Step 4, Loss 0.00000400, TDWI 0.5000\n", + "Step 5, Loss 0.00000220, TDWI 0.5118\n", + "Step 6, Loss 0.00000040, TDWI 0.5165\n", + "Step 7, Loss 0.00000143, TDWI 0.5159\n", + "Step 8, Loss 0.00000132, TDWI 0.5114\n", + "Step 9, Loss 0.00000032, TDWI 0.5038\n", + "Step 10, Loss 0.00000137, TDWI 0.5009\n", + "Early stopping at step 10\n" ] } ], "source": [ + "# ---- Early stopping ---- \n", + "best_loss = float(\"inf\")\n", + "patience = 10 # Number of steps to wait for improvement\n", + "patience_counter = 0\n", + "min_delta = 1e-4 \n", + "\n", "# ---- Optimizer ---- \n", "optimizer = torch.optim.Adam(opt_model.parameters(), lr=0.1)\n", "\n", @@ -694,13 +325,21 @@ " loss.backward()\n", " optimizer.step()\n", "\n", - " if step % 10 == 0:\n", - " print(f\"Step {step}, Loss {loss.item():.8f}, TDWI {opt_model.tdwi().item():.4f}\")" + " print(f\"Step {step}, Loss {loss.item():.8f}, TDWI {opt_model.tdwi().item():.4f}\")\n", + " # Early stopping logic\n", + " if loss.item() < best_loss - min_delta:\n", + " best_loss = loss.item()\n", + " patience_counter = 0\n", + " else:\n", + " patience_counter += 1\n", + " if patience_counter >= patience:\n", + " print(f\"Early stopping at step {step}\")\n", + " break" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 14, "id": "2029e2dd-7460-4c71-85a9-b1030a43167b", "metadata": {}, "outputs": [ From d4365df119828a9207117a7b7104e3dc5840f988 Mon Sep 17 00:00:00 2001 From: SarahAlidoost Date: Wed, 17 Dec 2025 15:56:46 +0100 Subject: [PATCH 07/10] add STE method to sigmoid in leaf_dynamics --- .../physical_models/crop/leaf_dynamics.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/diffwofost/physical_models/crop/leaf_dynamics.py b/src/diffwofost/physical_models/crop/leaf_dynamics.py index 550150a..ff75470 100644 --- a/src/diffwofost/physical_models/crop/leaf_dynamics.py +++ b/src/diffwofost/physical_models/crop/leaf_dynamics.py @@ -288,24 +288,26 @@ def calc_rates(self, day: datetime.date, drv: WeatherDataContainer) -> None: # SPAN because the latter would not allow for the gradient to be tracked. # the if statement `p.SPAN.requires_grad` to avoid unnecessary # approximation when SPAN is not a learnable parameter. + # here we use STE (straight through estimator) method. # TODO: sharpness can be exposed as a parameter if p.SPAN.requires_grad: - # 1e-16 is chosen empirically for cases when s.LVAGE - tSPAN is very - # small and mask should be 1 - # sharpness = torch.tensor(1e-16, dtype=DTYPE) - sharpness = torch.tensor(1e-4, dtype=DTYPE) + # 1000 is chosen empirically to approximate a step function + sharpness = torch.tensor(1000, dtype=DTYPE) # 1e-14 is chosen empirically for cases when s.LVAGE - tSPAN is # equal to zero and mask should be 0.0 epsilon = 1e-14 - # span_mask = torch.sigmoid((s.LVAGE - tSPAN - epsilon) / sharpness).to(dtype=DTYPE) + # soft mask using sigmoid soft_mask = torch.sigmoid((s.LVAGE - tSPAN - epsilon) / sharpness) + # originial hard mask hard_mask = (s.LVAGE > tSPAN).to(DTYPE) + # STE method. Here detach is used to stop the gradient flow. + # This way, during backpropagation, the gradient is computed only through the `soft_mask``, + # while during the forward pass, the `hard_mask`` is used. span_mask = hard_mask.detach() + soft_mask - soft_mask.detach() - else: span_mask = (s.LVAGE > tSPAN).to(dtype=DTYPE) From 3fbf6d7a78f3a543068be632f216a4e4f5710caa Mon Sep 17 00:00:00 2001 From: SarahAlidoost Date: Wed, 17 Dec 2025 15:57:36 +0100 Subject: [PATCH 08/10] fix test in leaf_dynamics --- .../crop/test_leaf_dynamics.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/physical_models/crop/test_leaf_dynamics.py b/tests/physical_models/crop/test_leaf_dynamics.py index 7940a76..4535e9d 100644 --- a/tests/physical_models/crop/test_leaf_dynamics.py +++ b/tests/physical_models/crop/test_leaf_dynamics.py @@ -418,7 +418,11 @@ def test_wofost_pp_with_leaf_dynamics(self, test_data_url): @pytest.mark.parametrize("test_data_url", leafdynamics_data_urls) def test_leaf_dynamics_with_sigmoid_approx(self, test_data_url): - """Test if sigmoid approximation gives same results as leaf dynamics.""" + """Test if sigmoid approximation gives same results as leaf dynamics. + + This should be the case since WOFOST_Leaf_Dynamics uses STE method. + In practice, no approximation is done when not insterested in gradients. + """ # prepare model input test_data = get_test_data(test_data_url) crop_model_params = ["SPAN", "TDWI", "TBASE", "PERDL", "RGRLAI", "KDIFTB", "SLATB"] @@ -578,13 +582,16 @@ def test_gradients_numerical(self, param_name, output_name, config_type): # this is ∂loss/∂param, for comparison with numerical gradient grads = torch.autograd.grad(loss, param, retain_graph=True)[0] - # assert_array_almost_equal(numerical_grad, grads.data, decimal=3) - torch.testing.assert_close( - numerical_grad, - grads, - rtol=1e-3, - atol=1e-3, - ) + # for span, the numerical gradient can't be equal to the pytorch one + # because we are using STE method + if (param_name, output_name) not in {("SPAN", "LAI")}: + # assert_array_almost_equal(numerical_grad, grads.data, decimal=3) + torch.testing.assert_close( + numerical_grad, + grads, + rtol=1e-3, + atol=1e-3, + ) # Warn if gradient is zero (but this shouldn't happen for gradient_params) if torch.all(grads == 0): From 284c639791a6ff1b55b8a506d84eae785d0fceca Mon Sep 17 00:00:00 2001 From: SarahAlidoost Date: Wed, 17 Dec 2025 16:04:09 +0100 Subject: [PATCH 09/10] fix linter errors --- src/diffwofost/physical_models/crop/leaf_dynamics.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/diffwofost/physical_models/crop/leaf_dynamics.py b/src/diffwofost/physical_models/crop/leaf_dynamics.py index ff75470..99b2182 100644 --- a/src/diffwofost/physical_models/crop/leaf_dynamics.py +++ b/src/diffwofost/physical_models/crop/leaf_dynamics.py @@ -304,9 +304,10 @@ def calc_rates(self, day: datetime.date, drv: WeatherDataContainer) -> None: # originial hard mask hard_mask = (s.LVAGE > tSPAN).to(DTYPE) - # STE method. Here detach is used to stop the gradient flow. - # This way, during backpropagation, the gradient is computed only through the `soft_mask``, - # while during the forward pass, the `hard_mask`` is used. + # STE method. Here detach is used to stop the gradient flow. This + # way, during backpropagation, the gradient is computed only through + # the `soft_mask``, while during the forward pass, the `hard_mask`` + # is used. span_mask = hard_mask.detach() + soft_mask - soft_mask.detach() else: span_mask = (s.LVAGE > tSPAN).to(dtype=DTYPE) From a5201cf3b6fab3cbf2f3a42aff5377196f2dc024 Mon Sep 17 00:00:00 2001 From: SarahAlidoost Date: Fri, 9 Jan 2026 12:57:55 +0100 Subject: [PATCH 10/10] rerun nbs --- .../optimization_leaf_dynamics.ipynb | 50 +++++++------ .../optimization_root_dynamics.ipynb | 70 ++++++++++++------- 2 files changed, 67 insertions(+), 53 deletions(-) diff --git a/docs/notebooks/optimization_leaf_dynamics.ipynb b/docs/notebooks/optimization_leaf_dynamics.ipynb index d146cf2..1990dd6 100644 --- a/docs/notebooks/optimization_leaf_dynamics.ipynb +++ b/docs/notebooks/optimization_leaf_dynamics.ipynb @@ -287,7 +287,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "78d797f5-4ac4-4380-85f3-6622a7b0f7fb", "metadata": {}, "outputs": [ @@ -295,30 +295,28 @@ "name": "stdout", "output_type": "stream", "text": [ - "Step 0, Loss 0.0070, TDWI 0.5341, SPAN 36.0670\n", - "Step 1, Loss 0.0271, TDWI 0.5325, SPAN 35.8151\n", - "Step 2, Loss 0.0216, TDWI 0.5223, SPAN 35.1632\n", - "Step 3, Loss 0.0040, TDWI 0.5078, SPAN 34.3827\n", - "Step 4, Loss 0.0169, TDWI 0.5028, SPAN 34.0915\n", - "Step 5, Loss 0.0262, TDWI 0.5041, SPAN 34.1150\n", - "Step 6, Loss 0.0261, TDWI 0.5098, SPAN 34.3638\n", - "Step 7, Loss 0.0174, TDWI 0.5189, SPAN 34.7887\n", - "Step 8, Loss 0.0083, TDWI 0.5229, SPAN 35.1052\n", - "Step 9, Loss 0.0022, TDWI 0.5224, SPAN 35.2232\n", - "Step 10, Loss 0.0048, TDWI 0.5183, SPAN 35.1611\n", - "Step 11, Loss 0.0040, TDWI 0.5112, SPAN 34.9521\n", - "Step 12, Loss 0.0012, TDWI 0.5019, SPAN 34.6612\n", - "Step 13, Loss 0.0114, TDWI 0.4970, SPAN 34.5845\n", - "Step 14, Loss 0.0126, TDWI 0.4959, SPAN 34.6862\n", - "Step 15, Loss 0.0104, TDWI 0.4981, SPAN 34.9379\n", - "Step 16, Loss 0.0016, TDWI 0.5030, SPAN 35.2729\n", - "Step 17, Loss 0.0059, TDWI 0.5101, SPAN 35.6397\n", - "Step 18, Loss 0.0173, TDWI 0.5137, SPAN 35.7822\n", - "Step 19, Loss 0.0207, TDWI 0.5140, SPAN 35.7163\n", - "Step 20, Loss 0.0194, TDWI 0.5114, SPAN 35.4857\n", - "Step 21, Loss 0.0111, TDWI 0.5062, SPAN 35.1246\n", - "Step 22, Loss 0.0026, TDWI 0.5043, SPAN 34.8779\n", - "Early stopping at step 22\n" + "Step 0, Loss 0.2682, TDWI 0.4242, SPAN 26.0705\n", + "Step 1, Loss 0.2400, TDWI 0.4485, SPAN 27.1814\n", + "Step 2, Loss 0.2117, TDWI 0.4727, SPAN 28.3303\n", + "Step 3, Loss 0.1790, TDWI 0.4962, SPAN 29.5140\n", + "Step 4, Loss 0.1502, TDWI 0.5190, SPAN 30.7290\n", + "Step 5, Loss 0.1148, TDWI 0.5351, SPAN 31.8314\n", + "Step 6, Loss 0.0873, TDWI 0.5459, SPAN 32.8227\n", + "Step 7, Loss 0.0633, TDWI 0.5525, SPAN 33.6916\n", + "Step 8, Loss 0.0380, TDWI 0.5559, SPAN 34.4434\n", + "Step 9, Loss 0.0164, TDWI 0.5564, SPAN 35.0537\n", + "Step 10, Loss 0.0016, TDWI 0.5543, SPAN 35.2340\n", + "Step 11, Loss 0.0050, TDWI 0.5500, SPAN 35.0621\n", + "Step 12, Loss 0.0018, TDWI 0.5436, SPAN 34.6755\n", + "Step 13, Loss 0.0109, TDWI 0.5356, SPAN 34.2452\n", + "Step 14, Loss 0.0230, TDWI 0.5261, SPAN 33.8167\n", + "Step 15, Loss 0.0341, TDWI 0.5152, SPAN 33.4017\n", + "Step 16, Loss 0.0472, TDWI 0.5031, SPAN 33.0102\n", + "Step 17, Loss 0.0559, TDWI 0.4950, SPAN 32.9639\n", + "Step 18, Loss 0.0571, TDWI 0.4903, SPAN 33.1766\n", + "Step 19, Loss 0.0519, TDWI 0.4888, SPAN 33.5989\n", + "Step 20, Loss 0.0400, TDWI 0.4899, SPAN 34.1872\n", + "Early stopping at step 20\n" ] } ], @@ -359,7 +357,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "c2d3a463-43a4-4b29-a71f-696c019343d3", "metadata": {}, "outputs": [ diff --git a/docs/notebooks/optimization_root_dynamics.ipynb b/docs/notebooks/optimization_root_dynamics.ipynb index 90a0d01..2923f98 100644 --- a/docs/notebooks/optimization_root_dynamics.ipynb +++ b/docs/notebooks/optimization_root_dynamics.ipynb @@ -37,7 +37,7 @@ }, { "cell_type": "markdown", - "id": "152ec514-baf1-4213-b98b-76f453d49538", + "id": "ab4c452e-7bf2-46ec-b52f-5ba642960c33", "metadata": {}, "source": [ "### 1.1 software requirements\n", @@ -45,16 +45,32 @@ "To run this notebook, we need to install the `diffwofost`; the differentiable\n", "version of WOFOST models. Since the package is constantly under development, make\n", "sure you have the latest version of `diffwofost` installed in your\n", - "python environment. You can install it using pip:\n", - "\n", - "```bash\n", - "pip install diffwofost\n", - "```" + "python environment. You can install it using pip:" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, + "id": "9732ef36-f46f-4c5d-b789-ba0ff0eab0d8", + "metadata": {}, + "outputs": [], + "source": [ + "# install diffwofost\n", + "!pip install diffwofost" + ] + }, + { + "cell_type": "markdown", + "id": "152ec514-baf1-4213-b98b-76f453d49538", + "metadata": {}, + "source": [ + "# install diffwofost\n", + "!pip install diffwofost" + ] + }, + { + "cell_type": "code", + "execution_count": 2, "id": "eaa4c172-9719-4a79-b2f2-d37ea5b6f11d", "metadata": {}, "outputs": [], @@ -74,7 +90,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "26437aed-755d-4ee3-b7b6-82caf8c30ec5", "metadata": {}, "outputs": [], @@ -102,7 +118,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "22000922-68be-47ed-8afa-2e97c56bb502", "metadata": {}, "outputs": [ @@ -134,7 +150,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "a69e9279-49eb-4136-8f15-7cff8bb4af52", "metadata": {}, "outputs": [], @@ -145,7 +161,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "d560476b-64f9-422c-9722-8d0778cfc574", "metadata": {}, "outputs": [], @@ -184,7 +200,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "cbfc9e28-3233-4808-b658-36d6f7d18d75", "metadata": {}, "outputs": [], @@ -220,7 +236,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "90296352-4817-4586-842c-12ac6d97d779", "metadata": {}, "outputs": [], @@ -263,7 +279,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "f2a1a612-f33c-48a0-a3d4-31318d78b9f4", "metadata": {}, "outputs": [], @@ -280,7 +296,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 10, "id": "6c09d1ce-db66-46f3-93d2-1b16f8196e49", "metadata": {}, "outputs": [ @@ -288,17 +304,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "Step 0, Loss 0.00000787, TDWI 0.5207\n", - "Step 1, Loss 0.00000237, TDWI 0.4957\n", - "Step 2, Loss 0.00000316, TDWI 0.4892\n", - "Step 3, Loss 0.00000459, TDWI 0.4919\n", - "Step 4, Loss 0.00000400, TDWI 0.5000\n", - "Step 5, Loss 0.00000220, TDWI 0.5118\n", - "Step 6, Loss 0.00000040, TDWI 0.5165\n", - "Step 7, Loss 0.00000143, TDWI 0.5159\n", - "Step 8, Loss 0.00000132, TDWI 0.5114\n", - "Step 9, Loss 0.00000032, TDWI 0.5038\n", - "Step 10, Loss 0.00000137, TDWI 0.5009\n", + "Step 0, Loss 0.00004644, TDWI 0.3214\n", + "Step 1, Loss 0.00004170, TDWI 0.3436\n", + "Step 2, Loss 0.00003679, TDWI 0.3665\n", + "Step 3, Loss 0.00003172, TDWI 0.3901\n", + "Step 4, Loss 0.00002650, TDWI 0.4143\n", + "Step 5, Loss 0.00002116, TDWI 0.4389\n", + "Step 6, Loss 0.00001571, TDWI 0.4639\n", + "Step 7, Loss 0.00001019, TDWI 0.4891\n", + "Step 8, Loss 0.00000461, TDWI 0.5144\n", + "Step 9, Loss 0.00000099, TDWI 0.5316\n", + "Step 10, Loss 0.00000479, TDWI 0.5424\n", "Early stopping at step 10\n" ] } @@ -339,7 +355,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 11, "id": "2029e2dd-7460-4c71-85a9-b1030a43167b", "metadata": {}, "outputs": [