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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
os: ['ubuntu-latest', 'windows-latest']
python-version: ['3.10', '3.11', '3.12']
python-version: ['3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand Down
4 changes: 4 additions & 0 deletions docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ hide:

## **Utility (under development)**

::: diffwofost.physical_models.config.Configuration

::: diffwofost.physical_models.engine.Engine

::: diffwofost.physical_models.utils.EngineTestHelper
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ classifiers = [
"Intended Audience :: Science/Research",
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"torch",
Expand All @@ -33,7 +33,7 @@ keywords = ["wofost", "pytorch", "differentiable", "crop", "optimization"]
license = {file = "LICENSE"}
name = "diffwofost"
readme = {file = "README.md", content-type = "text/markdown"}
requires-python = ">=3.10"
requires-python = ">=3.11"
version = "0.2.0"

[project.optional-dependencies]
Expand Down
129 changes: 129 additions & 0 deletions src/diffwofost/physical_models/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from dataclasses import dataclass
from dataclasses import field
from pathlib import Path
from typing import Self
import pcse
from pcse.agromanager import AgroManager
from pcse.base import AncillaryObject
from pcse.base import SimulationObject


@dataclass(frozen=True)
class Configuration:
"""Class to store model configuration from a PCSE configuration files."""

CROP: type[SimulationObject]
SOIL: type[SimulationObject] | None = None
AGROMANAGEMENT: type[AncillaryObject] = AgroManager
OUTPUT_VARS: list = field(default_factory=list)
SUMMARY_OUTPUT_VARS: list = field(default_factory=list)
TERMINAL_OUTPUT_VARS: list = field(default_factory=list)
OUTPUT_INTERVAL: str = "daily" # "daily"|"dekadal"|"monthly"
OUTPUT_INTERVAL_DAYS: int = 1
OUTPUT_WEEKDAY: int = 0
model_config_file: str | Path | None = None
description: str | None = None

@classmethod
def from_pcse_config_file(cls, filename: str | Path) -> Self:
"""Load the model configuration from a PCSE configuration file.

Args:
filename (str | pathlib.Path): Path to the configuraiton file. The path is first
interpreted with respect to the current working directory and, if not found, it will
then be interpreted with respect to the `conf` folder in the PCSE package.

Returns:
Configuration: Model configuration instance

Raises:
FileNotFoundError: if the configuraiton file does not exist
RuntimeError: if parsing the configuration file fails
"""
config = {}

path = Path(filename)
if path.is_absolute() or path.is_file():
model_config_file = path
else:
pcse_dir = Path(pcse.__path__[0])
model_config_file = pcse_dir / "conf" / path
model_config_file = model_config_file.resolve()

# check that configuration file exists
if not model_config_file.exists():
msg = f"PCSE model configuration file does not exist: {model_config_file.name}"
raise FileNotFoundError(msg)
# store for later use
config["model_config_file"] = model_config_file

# Load file using execfile
try:
loc = {}
bytecode = compile(open(model_config_file).read(), model_config_file, "exec")
exec(bytecode, {}, loc)
except Exception as e:
msg = f"Failed to load configuration from file {model_config_file}"
raise RuntimeError(msg) from e

# Add the descriptive header for later use
if "__doc__" in loc:
desc = loc.pop("__doc__")
if len(desc) > 0:
description = desc
if description[-1] != "\n":
description += "\n"
config["description"] = description

# Loop through the attributes in the configuration file
for key, value in loc.items():
if key.isupper():
config[key] = value
return cls(**config)

def update_output_variable_lists(
self,
output_vars: str | list | tuple | set | None = None,
summary_vars: str | list | tuple | set | None = None,
terminal_vars: str | list | tuple | set | None = None,
):
"""Updates the lists of output variables that are defined in the configuration file.

This is useful because sometimes you want the flexibility to get access to an additional
model variable which is not in the standard list of variables defined in the model
configuration file. The more elegant way is to define your own configuration file, but this
adds some flexibility particularly for use in jupyter notebooks and exploratory analysis.

Note that there is a different behaviour given the type of the variable provided. List and
string inputs will extend the list of variables, while set/tuple inputs will replace the
current list.

Args:
output_vars: the variable names to add/replace for the OUTPUT_VARS configuration
variable
summary_vars: the variable names to add/replace for the SUMMARY_OUTPUT_VARS
configuration variable
terminal_vars: the variable names to add/replace for the TERMINAL_OUTPUT_VARS
configuration variable

Raises:
TypeError: if the type of the input arguments is not recognized
"""
config_varnames = ["OUTPUT_VARS", "SUMMARY_OUTPUT_VARS", "TERMINAL_OUTPUT_VARS"]
for varitems, config_varname in zip(
[output_vars, summary_vars, terminal_vars], config_varnames, strict=True
):
if varitems is None:
continue
else:
if isinstance(varitems, str): # A string: we extend the current list
getattr(self, config_varname).extend(varitems.split())
elif isinstance(varitems, list): # a list: we extend the current list
getattr(self, config_varname).extend(varitems)
elif isinstance(varitems, tuple | set): # tuple/set we replace the current list
attr = getattr(self, config_varname)
attr.clear()
attr.extend(list(varitems))
else:
msg = f"Unrecognized input for `output_vars` to engine(): {output_vars}"
raise TypeError(msg)
67 changes: 67 additions & 0 deletions src/diffwofost/physical_models/engine.py
Copy link
Collaborator Author

@fnattino fnattino Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just an initial placeholder for the "new" diffwofost engine. Right now it is identical to the pcse.engine.Engine, with exception for the use of the Configuration instead of the ConfigurationLoader, with exception for the use of the actual VariableKiosk vs the VariableKioskTestHelper (which allows to set in external variables).

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from pathlib import Path
from pcse import signals
from pcse.base import BaseEngine
from pcse.base.variablekiosk import VariableKiosk
from pcse.engine import Engine
from pcse.timer import Timer
from pcse.traitlets import Instance
from .config import Configuration


class Engine(Engine):
mconf = Instance(Configuration)

def __init__(
self,
parameterprovider,
weatherdataprovider,
agromanagement,
config: str | Path | Configuration,
):
BaseEngine.__init__(self)

# If a path is given, load the model configuration from a PCSE config file
if isinstance(config, str | Path):
self.mconf = Configuration.from_pcse_config_file(config)
else:
self.mconf = config

self.parameterprovider = parameterprovider

# Variable kiosk for registering and publishing variables
self.kiosk = VariableKiosk()

# Placeholder for variables to be saved during a model run
self._saved_output = []
self._saved_summary_output = []
self._saved_terminal_output = {}

# register handlers for starting/finishing the crop simulation, for
# handling output and terminating the system
self._connect_signal(self._on_CROP_START, signal=signals.crop_start)
self._connect_signal(self._on_CROP_FINISH, signal=signals.crop_finish)
self._connect_signal(self._on_OUTPUT, signal=signals.output)
self._connect_signal(self._on_TERMINATE, signal=signals.terminate)

# Component for agromanagement
self.agromanager = self.mconf.AGROMANAGEMENT(self.kiosk, agromanagement)
start_date = self.agromanager.start_date
end_date = self.agromanager.end_date

# Timer: starting day, final day and model output
self.timer = Timer(self.kiosk, start_date, end_date, self.mconf)
self.day, _ = self.timer()

# Driving variables
self.weatherdataprovider = weatherdataprovider
self.drv = self._get_driving_variables(self.day)

# Component for simulation of soil processes
if self.mconf.SOIL is not None:
self.soil = self.mconf.SOIL(self.day, self.kiosk, parameterprovider)

# Call AgroManagement module for management actions at initialization
self.agromanager(self.day, self.drv)

# Calculate initial rates
self.calc_rates(self.day, self.drv)
43 changes: 11 additions & 32 deletions src/diffwofost/physical_models/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
It contains:
- VariableKioskTestHelper: A subclass of the VariableKiosk that can use externally
forced states/rates
- ConfigurationLoaderTestHelper: An subclass of ConfigurationLoader that allows to
specify the simbojects to be test dynamically
- EngineTestHelper: engine specifically for running the YAML tests.
- WeatherDataProviderTestHelper: a weatherdata provides that takes the weather
inputs from the YAML file.
Expand All @@ -13,42 +11,34 @@
"""

import logging
import os
from collections.abc import Iterable
from pathlib import Path
import torch
import yaml
from pcse import signals
from pcse.agromanager import AgroManager
from pcse.base import ConfigurationLoader
from pcse.base.parameter_providers import ParameterProvider
from pcse.base.variablekiosk import VariableKiosk
from pcse.base.weather import WeatherDataContainer
from pcse.base.weather import WeatherDataProvider
from pcse.engine import BaseEngine
from pcse.engine import Engine
from pcse.settings import settings
from pcse.timer import Timer
from pcse.traitlets import Enum
from pcse.traitlets import TraitType
from .config import Configuration
from .engine import Engine

DTYPE = torch.float64 # Default data type for tensors in this module

logging.disable(logging.CRITICAL)

this_dir = os.path.dirname(__file__)


def nothing(*args, **kwargs):
"""A function that does nothing."""
pass


class VariableKioskTestHelper(VariableKiosk):
"""Variable Kiosk for testing purposes which allows to use external states."""

external_state_list = None

def __init__(self, external_state_list):
def __init__(self, external_state_list=None):
super().__init__()
self.current_externals = {}
if external_state_list:
Expand Down Expand Up @@ -98,21 +88,6 @@ def __contains__(self, key):
return key in self.current_externals or dict.__contains__(self, key)


class ConfigurationLoaderTestHelper(ConfigurationLoader):
def __init__(self, YAML_test_inputs, simobject, waterbalance=None):
self.model_config_file = "Test config"
self.description = "Configuration loader for running YAML tests"
self.CROP = simobject
self.SOIL = waterbalance
self.AGROMANAGEMENT = AgroManager
self.OUTPUT_INTERVAL = "daily"
self.OUTPUT_INTERVAL_DAYS = 1
self.OUTPUT_WEEKDAY = 0
self.OUTPUT_VARS = list(YAML_test_inputs["Precision"].keys())
self.SUMMARY_OUTPUT_VARS = []
self.TERMINAL_OUTPUT_VARS = []


class EngineTestHelper(Engine):
"""An engine which is purely for running the YAML unit tests."""

Expand All @@ -121,13 +96,17 @@ def __init__(
parameterprovider,
weatherdataprovider,
agromanagement,
test_config,
config,
external_states=None,
):
BaseEngine.__init__(self)

# Load the model configuration
self.mconf = ConfigurationLoader(test_config)
# If a path is given, load the model configuration from a PCSE config file
if isinstance(config, str | Path):
self.mconf = Configuration.from_pcse_config_file(config)
else:
self.mconf = config

self.parameterprovider = parameterprovider

# Variable kiosk for registering and publishing variables
Expand Down
Loading