diff --git a/src/diffwofost/physical_models/crop/assimilation.py b/src/diffwofost/physical_models/crop/assimilation.py index 698af9f..d6a7a19 100644 --- a/src/diffwofost/physical_models/crop/assimilation.py +++ b/src/diffwofost/physical_models/crop/assimilation.py @@ -6,14 +6,12 @@ from pcse.base import SimulationObject from pcse.base.parameter_providers import ParameterProvider from pcse.base.variablekiosk import VariableKiosk -from pcse.base.weather import WeatherDataContainer from diffwofost.physical_models.base import TensorParamTemplate from diffwofost.physical_models.base import TensorRatesTemplate from diffwofost.physical_models.config import ComputeConfig from diffwofost.physical_models.traitlets import Tensor from diffwofost.physical_models.utils import AfgenTrait from diffwofost.physical_models.utils import _broadcast_to -from diffwofost.physical_models.utils import _get_drv from diffwofost.physical_models.utils import astro # --------------------------------------------------------------------------- @@ -343,7 +341,7 @@ def initialize( # elements (which share the same weather driver). self._astro_cache: dict = {} - def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None) -> None: + def calc_rates(self, day: datetime.date, drv: dict) -> torch.Tensor: """Compute the potential gross assimilation rate (PGASS).""" p = self.params r = self.rates @@ -356,9 +354,9 @@ def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None lai = _broadcast_to(k["LAI"], self.params.shape, dtype=self.dtype, device=self.device) # Weather drivers - irrad = _get_drv(drv.IRRAD, self.params.shape, dtype=self.dtype, device=self.device) - dtemp = _get_drv(drv.DTEMP, self.params.shape, dtype=self.dtype, device=self.device) - tmin = _get_drv(drv.TMIN, self.params.shape, dtype=self.dtype, device=self.device) + irrad = drv["IRRAD"] + dtemp = drv["DTEMP"] + tmin = drv["TMIN"] # Assimilation is zero before crop emergence (DVS < 0) dvs_mask = dvs >= 0 @@ -373,15 +371,9 @@ def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None # latitude and radiation are passed directly – they may be scalars or # tensors; the function returns torch.Tensor results in all cases. dayl, _daylp, sinld, cosld, difpp, _atmtr, dsinbe, _angot = astro( - day, drv.LAT, drv.IRRAD, dtype=self.dtype, device=self.device + day, drv["LAT"], drv["IRRAD"], dtype=self.dtype, device=self.device ) - dayl_t = _broadcast_to(dayl, self.params.shape, dtype=self.dtype, device=self.device) - sinld_t = _broadcast_to(sinld, self.params.shape, dtype=self.dtype, device=self.device) - cosld_t = _broadcast_to(cosld, self.params.shape, dtype=self.dtype, device=self.device) - difpp_t = _broadcast_to(difpp, self.params.shape, dtype=self.dtype, device=self.device) - dsinbe_t = _broadcast_to(dsinbe, self.params.shape, dtype=self.dtype, device=self.device) - # Parameter tables amax = p.AMAXTB(dvs) amax = amax * p.TMPFTB(dtemp) @@ -389,16 +381,16 @@ def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None eff = p.EFFTB(dtemp) dtga = totass7( - dayl_t, + dayl, amax, eff, lai, kdif, irrad, - difpp_t, - dsinbe_t, - sinld_t, - cosld_t, + difpp, + dsinbe, + sinld, + cosld, epsilon=self._epsilon, dtype=self.dtype, device=self.device, @@ -414,11 +406,11 @@ def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None r.PGASS = pgass * dvs_mask return r.PGASS - def __call__(self, day: datetime.date = None, drv: WeatherDataContainer = None) -> torch.Tensor: + def __call__(self, day: datetime.date, drv: dict) -> torch.Tensor: """Calculate and return the potential gross assimilation rate (PGASS).""" return self.calc_rates(day, drv) - def integrate(self, day: datetime.date = None, delt=1.0) -> None: + def integrate(self, day: datetime.date, delt: float = 1.0) -> None: """No state variables to integrate for this module.""" return diff --git a/src/diffwofost/physical_models/crop/evapotranspiration.py b/src/diffwofost/physical_models/crop/evapotranspiration.py index 6ddf471..b73e96d 100644 --- a/src/diffwofost/physical_models/crop/evapotranspiration.py +++ b/src/diffwofost/physical_models/crop/evapotranspiration.py @@ -3,7 +3,6 @@ from pcse.base import SimulationObject from pcse.base.parameter_providers import ParameterProvider from pcse.base.variablekiosk import VariableKiosk -from pcse.base.weather import WeatherDataContainer from pcse.traitlets import Any from pcse.traitlets import Bool from pcse.traitlets import Instance @@ -14,7 +13,6 @@ from diffwofost.physical_models.traitlets import Tensor from diffwofost.physical_models.utils import AfgenTrait from diffwofost.physical_models.utils import _broadcast_to -from diffwofost.physical_models.utils import _get_drv def SWEAF(ET0: torch.Tensor, DEPNR: torch.Tensor) -> torch.Tensor: @@ -102,22 +100,21 @@ def initialize( else: self.etmodule = Evapotranspiration(day, kiosk, parvalues, shape=shape) - def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None): + def calc_rates(self, day: datetime.date, drv: dict): """Delegate rate calculation to the selected evapotranspiration module. Args: - day (datetime.date, optional): The current date of the simulation. - drv (WeatherDataContainer, optional): A dictionary-like container holding - weather data elements as key/value. The values are - arrays or scalars. See PCSE documentation for details. + day (datetime.date): The current date of the simulation. + drv (dict): A container holding weather data elements as key/value. The values are + arrays or scalars. """ return self.etmodule.calc_rates(day, drv) - def __call__(self, day: datetime.date = None, drv: WeatherDataContainer = None): + def __call__(self, day: datetime.date, drv: dict): """Callable interface for rate calculation.""" return self.calc_rates(day, drv) - def integrate(self, day: datetime.date = None, delt=1.0) -> None: + def integrate(self, day: datetime.date, delt: float = 1.0) -> None: """Delegate state integration to the selected evapotranspiration module. Args: @@ -190,11 +187,11 @@ def _initialize_base( self._IDWST = torch.zeros(shape, dtype=self.dtype, device=self.device) self._IDOST = torch.zeros(shape, dtype=self.dtype, device=self.device) - def __call__(self, day: datetime.date = None, drv: WeatherDataContainer = None): + def __call__(self, day: datetime.date, drv: dict): """Callable interface for rate calculation.""" return self.calc_rates(day, drv) - def integrate(self, day: datetime.date = None, delt=1.0) -> None: + def integrate(self, day: datetime.date, delt: float = 1.0) -> None: """Accumulate stress-day counters for water and oxygen stress.""" rfws_stress = (self.rates.RFWS < 1.0).to(dtype=self.dtype) rfos_stress = (self.rates.RFOS < 1.0).to(dtype=self.dtype) @@ -211,11 +208,11 @@ def finalize(self, day: datetime.date) -> None: class _BaseEvapotranspirationNonLayered(_BaseEvapotranspiration): """Shared implementation for non-layered evapotranspiration.""" - def _rf_tramx_co2(self, drv: WeatherDataContainer, et0: torch.Tensor) -> torch.Tensor: + def _rf_tramx_co2(self, drv: dict, et0: torch.Tensor) -> torch.Tensor: """Return CO2 reduction factor for TRAMX (no CO2 effect in base implementation).""" return torch.ones_like(et0) - def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None): + def calc_rates(self, day: datetime.date, drv: dict): p = self.params r = self.rates k = self.kiosk @@ -227,9 +224,9 @@ def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None # TODO see #22 dvs = _broadcast_to(k["DVS"], self.params_shape, dtype=self.dtype, device=self.device) - et0 = _get_drv(drv.ET0, self.params_shape, dtype=self.dtype, device=self.device) - e0 = _get_drv(drv.E0, self.params_shape, dtype=self.dtype, device=self.device) - es0 = _get_drv(drv.ES0, self.params_shape, dtype=self.dtype, device=self.device) + et0 = drv["ET0"] + e0 = drv["E0"] + es0 = drv["ES0"] rf_tramx_co2 = self._rf_tramx_co2(drv, et0) # If DVS < 0, the crop has not yet emerged, so we zero the rates using a mask @@ -483,10 +480,10 @@ def initialize( shape=shape, ) - def _rf_tramx_co2(self, drv: WeatherDataContainer, et0: torch.Tensor) -> torch.Tensor: + def _rf_tramx_co2(self, drv: dict, et0: torch.Tensor) -> torch.Tensor: """Calculate CO2 reduction factor for TRAMX based on atmospheric CO2 concentration.""" - if hasattr(drv, "CO2") and drv.CO2 is not None: - co2 = _get_drv(drv.CO2, self.params_shape, dtype=self.dtype, device=self.device) + if "CO2" in drv and drv["CO2"] is not None: + co2 = drv["CO2"] else: co2 = self.params.CO2 return self.params.CO2TRATB(co2) @@ -634,15 +631,15 @@ def initialize( # Internal DSOS tracker for layered oxygen-stress response self._dsos = torch.zeros(self.params_shape, dtype=self.dtype, device=self.device) - def _rf_tramx_co2(self, drv: WeatherDataContainer, et0: torch.Tensor) -> torch.Tensor: + def _rf_tramx_co2(self, drv: dict, et0: torch.Tensor) -> torch.Tensor: """Calculate CO2 reduction factor for TRAMX using CO2 from driver or parameters.""" - if hasattr(drv, "CO2") and drv.CO2 is not None: - co2 = _get_drv(drv.CO2, self.params_shape, dtype=self.dtype, device=self.device) + if "CO2" in drv and drv["CO2"] is not None: + co2 = drv["CO2"] else: co2 = self.params.CO2 return self.params.CO2TRATB(co2) - def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None): + def calc_rates(self, day: datetime.date, drv: dict): """Calculate daily evapotranspiration rates per soil layer with CO2 effects. Computes transpiration and stress factors for each soil layer based on root @@ -658,9 +655,9 @@ def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None n_layers = self._n_layers - et0 = _get_drv(drv.ET0, self.params_shape, dtype=self.dtype, device=self.device) - e0 = _get_drv(drv.E0, self.params_shape, dtype=self.dtype, device=self.device) - es0 = _get_drv(drv.ES0, self.params_shape, dtype=self.dtype, device=self.device) + et0 = drv["ET0"] + e0 = drv["E0"] + es0 = drv["ES0"] # reduction factor for CO2 on TRAMX rf_tramx_co2 = self._rf_tramx_co2(drv, et0) @@ -786,11 +783,11 @@ def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None r.IDOS = bool(torch.any(r.RFOS < 1.0)) return r.TRA, r.TRAMX - def __call__(self, day: datetime.date = None, drv: WeatherDataContainer = None): + def __call__(self, day: datetime.date, drv: dict): """Callable interface for rate calculation.""" return self.calc_rates(day, drv) - def integrate(self, day: datetime.date = None, delt=1.0) -> None: + def integrate(self, day: datetime.date, delt: float = 1.0) -> None: """Accumulate stress-day counters based on any layer experiencing stress.""" rfws_stress = (self.rates.RFWS < 1.0).any(dim=0).to(dtype=self.dtype) rfos_stress = (self.rates.RFOS < 1.0).any(dim=0).to(dtype=self.dtype) diff --git a/src/diffwofost/physical_models/crop/leaf_dynamics.py b/src/diffwofost/physical_models/crop/leaf_dynamics.py index 9897aad..e5fe7db 100644 --- a/src/diffwofost/physical_models/crop/leaf_dynamics.py +++ b/src/diffwofost/physical_models/crop/leaf_dynamics.py @@ -5,14 +5,12 @@ from pcse.base import SimulationObject from pcse.base.parameter_providers import ParameterProvider from pcse.base.variablekiosk import VariableKiosk -from pcse.base.weather import WeatherDataContainer from diffwofost.physical_models.base import TensorParamTemplate from diffwofost.physical_models.base import TensorRatesTemplate from diffwofost.physical_models.base import TensorStatesTemplate from diffwofost.physical_models.config import ComputeConfig from diffwofost.physical_models.traitlets import Tensor from diffwofost.physical_models.utils import AfgenTrait -from diffwofost.physical_models.utils import _get_drv class WOFOST_Leaf_Dynamics(SimulationObject): @@ -248,14 +246,13 @@ def _calc_LAI(self): total_LAI = self.states.LASUM + SAI + PAI return total_LAI - def calc_rates(self, day: datetime.date, drv: WeatherDataContainer) -> None: + def calc_rates(self, day: datetime.date, drv: dict) -> None: """Calculate the rates of change for the leaf dynamics. Args: - day (datetime.date, optional): The current date of the simulation. - drv (WeatherDataContainer, optional): A dictionary-like container holding - weather data elements as key/value. The values are - arrays or scalars. See PCSE documentation for details. + day (datetime.date): The current date of the simulation. + drv (dict): A container holding weather data elements as key/value. The values are + arrays or scalars. """ r = self.rates s = self.states @@ -326,7 +323,7 @@ def calc_rates(self, day: datetime.date, drv: WeatherDataContainer) -> None: r.DRLV = torch.maximum(r.DSLV, r.DALV) # Get the temperature from the drv - TEMP = _get_drv(drv.TEMP, p.shape, self.dtype, self.device) + TEMP = drv["TEMP"] # physiologic ageing of leaves per time step FYSAGE = (TEMP - p.TBASE) / (35.0 - p.TBASE) diff --git a/src/diffwofost/physical_models/crop/phenology.py b/src/diffwofost/physical_models/crop/phenology.py index 474a872..7f04015 100644 --- a/src/diffwofost/physical_models/crop/phenology.py +++ b/src/diffwofost/physical_models/crop/phenology.py @@ -19,8 +19,6 @@ from diffwofost.physical_models.config import ComputeConfig from diffwofost.physical_models.traitlets import Tensor from diffwofost.physical_models.utils import AfgenTrait -from diffwofost.physical_models.utils import _broadcast_to -from diffwofost.physical_models.utils import _get_drv from diffwofost.physical_models.utils import _restore_state from diffwofost.physical_models.utils import _snapshot_state from diffwofost.physical_models.utils import daylength @@ -182,7 +180,8 @@ def calc_rates(self, day, drv): VERNBASE = params.VERNBASE DVS = self.kiosk["DVS"] - TEMP = _get_drv(drv.TEMP, self.params.shape, self.dtype, self.device) + TEMP = drv["TEMP"] + print(TEMP) # Operate elementwise only on elements not yet vernalised not_vernalised = ~self.states.ISVERNALISED @@ -505,15 +504,13 @@ def calc_rates(self, day, drv): p = self.params r = self.rates s = self.states - # Day length sensitivity # daylength returns a Tensor directly; broadcast to parameter shape. - DAYLP = daylength(day, drv.LAT, dtype=self.dtype, device=self.device) - DAYLP_t = _broadcast_to(DAYLP, p.shape, dtype=self.dtype, device=self.device) + DAYLP = daylength(day, drv["LAT"], dtype=self.dtype, device=self.device) # Compute DVRED conditionally based on IDSL >= 1 safe_den = p.DLO - p.DLC safe_den = safe_den.sign() * torch.maximum(torch.abs(safe_den), self._epsilon) - dvred_active = torch.clamp((DAYLP_t - p.DLC) / safe_den, 0.0, 1.0) + dvred_active = torch.clamp((DAYLP - p.DLC) / safe_den, 0.0, 1.0) DVRED = torch.where(p.IDSL >= 1, dvred_active, self._ones) # Vernalisation factor - always compute if module exists @@ -529,7 +526,7 @@ def calc_rates(self, day, drv): self._ones, ) - TEMP = _get_drv(drv.TEMP, p.shape, self.dtype, self.device) + TEMP = drv["TEMP"] # Initialize all rate variables r.DTSUME = self._zeros diff --git a/src/diffwofost/physical_models/crop/respiration.py b/src/diffwofost/physical_models/crop/respiration.py index 45e8e5b..058a35d 100644 --- a/src/diffwofost/physical_models/crop/respiration.py +++ b/src/diffwofost/physical_models/crop/respiration.py @@ -5,14 +5,12 @@ from pcse.base import SimulationObject from pcse.base.parameter_providers import ParameterProvider from pcse.base.variablekiosk import VariableKiosk -from pcse.base.weather import WeatherDataContainer from diffwofost.physical_models.base import TensorParamTemplate from diffwofost.physical_models.base import TensorRatesTemplate from diffwofost.physical_models.config import ComputeConfig from diffwofost.physical_models.traitlets import Tensor from diffwofost.physical_models.utils import AfgenTrait from diffwofost.physical_models.utils import _broadcast_to -from diffwofost.physical_models.utils import _get_drv class WOFOST_Maintenance_Respiration(SimulationObject): @@ -112,7 +110,7 @@ def initialize( self.rates = self.RateVariables(kiosk, shape=shape) self.kiosk = kiosk - def calc_rates(self, day: datetime.date, drv: WeatherDataContainer): + def calc_rates(self, day: datetime.date, drv: dict): """Calculate maintenance respiration rates. Args: @@ -138,7 +136,7 @@ def calc_rates(self, day: datetime.date, drv: WeatherDataContainer): # TODO see #22 DVS = _broadcast_to(kk["DVS"], p.shape, self.dtype, self.device) - TEMP = _get_drv(drv.TEMP, p.shape, self.dtype, self.device) + TEMP = drv["TEMP"] RMRES = RMR * WRT + RML * WLV + RMS * WST + RMO * WSO RMRES = RMRES * p.RFSETB(DVS) @@ -148,7 +146,7 @@ def calc_rates(self, day: datetime.date, drv: WeatherDataContainer): # No maintenance respiration before emergence (DVS < 0). r.PMRES = torch.where(DVS < 0, torch.zeros_like(PMRES), PMRES) - def __call__(self, day: datetime.date, drv: WeatherDataContainer): + def __call__(self, day: datetime.date, drv: dict): """Calculate and return maintenance respiration (PMRES).""" self.calc_rates(day, drv) return self.rates.PMRES diff --git a/src/diffwofost/physical_models/crop/root_dynamics.py b/src/diffwofost/physical_models/crop/root_dynamics.py index 2926206..3936647 100644 --- a/src/diffwofost/physical_models/crop/root_dynamics.py +++ b/src/diffwofost/physical_models/crop/root_dynamics.py @@ -3,7 +3,6 @@ from pcse.base import SimulationObject from pcse.base.parameter_providers import ParameterProvider from pcse.base.variablekiosk import VariableKiosk -from pcse.base.weather import WeatherDataContainer from diffwofost.physical_models.base import TensorParamTemplate from diffwofost.physical_models.base import TensorRatesTemplate from diffwofost.physical_models.base import TensorStatesTemplate @@ -193,14 +192,13 @@ def initialize( shape=shape, ) - def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None) -> None: + def calc_rates(self, day: datetime.date, drv: dict) -> None: """Calculate the rates of change of the state variables. Args: - day (datetime.date, optional): The current date of the simulation. - drv (WeatherDataContainer, optional): A dictionary-like container holding - weather data elements as key/value. The values are - arrays or scalars. See PCSE documentation for details. + day (datetime.date): The current date of the simulation. + drv (dict): A container holding weather data elements as key/value. The values are + arrays or scalars. """ p = self.params r = self.rates diff --git a/src/diffwofost/physical_models/crop/stem_dynamics.py b/src/diffwofost/physical_models/crop/stem_dynamics.py index 8b0bb37..5b52b06 100644 --- a/src/diffwofost/physical_models/crop/stem_dynamics.py +++ b/src/diffwofost/physical_models/crop/stem_dynamics.py @@ -3,7 +3,6 @@ from pcse.base import SimulationObject from pcse.base.parameter_providers import ParameterProvider from pcse.base.variablekiosk import VariableKiosk -from pcse.base.weather import WeatherDataContainer from diffwofost.physical_models.base import TensorParamTemplate from diffwofost.physical_models.base import TensorRatesTemplate from diffwofost.physical_models.base import TensorStatesTemplate @@ -160,14 +159,13 @@ def initialize( kiosk, publish=["TWST", "WST", "SAI"], WST=WST, DWST=DWST, TWST=TWST, SAI=SAI ) - def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None) -> None: + def calc_rates(self, day: datetime.date, drv: dict) -> None: """Calculate the rates of change of the state variables. Args: day (datetime.date, optional): The current date of the simulation. - drv (WeatherDataContainer, optional): A dictionary-like container holding - weather data elements as key/value. The values are - arrays or scalars. See PCSE documentation for details. + drv (dict, optional): A container holding weather data elements as key/value. The values + are arrays or scalars. """ r = self.rates s = self.states @@ -196,11 +194,11 @@ def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None r.GWST = r.GRST - r.DRST - REALLOC_ST - def integrate(self, day: datetime.date = None, delt=1.0) -> None: + def integrate(self, day: datetime.date, delt: float = 1.0) -> None: """Integrate the state variables using the rates of change. Args: - day (datetime.date, optional): The current date of the simulation. + day (datetime.date): The current date of the simulation. delt (float, optional): The time step for integration. Defaults to 1.0. """ p = self.params diff --git a/src/diffwofost/physical_models/crop/storage_organ_dynamics.py b/src/diffwofost/physical_models/crop/storage_organ_dynamics.py index 5e48979..7488f0a 100644 --- a/src/diffwofost/physical_models/crop/storage_organ_dynamics.py +++ b/src/diffwofost/physical_models/crop/storage_organ_dynamics.py @@ -4,7 +4,6 @@ from pcse.base import SimulationObject from pcse.base.parameter_providers import ParameterProvider from pcse.base.variablekiosk import VariableKiosk -from pcse.base.weather import WeatherDataContainer from diffwofost.physical_models.base import TensorParamTemplate from diffwofost.physical_models.base import TensorStatesTemplate from diffwofost.physical_models.config import ComputeConfig @@ -144,13 +143,12 @@ def initialize( kiosk, publish=["TWSO", "WSO", "PAI"], WSO=WSO, DWSO=DWSO, TWSO=TWSO, PAI=PAI ) - def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None) -> None: + def calc_rates(self, day: datetime.date, drv: dict) -> None: """Calculate the rates of change of the state variables. Args: day (datetime.date, optional): The current date of the simulation. - drv (WeatherDataContainer, optional): A dictionary-like container holding - weather data elements as key/value. + drv (dict, optional): A container holding weather data elements as key/value. """ rates = self.rates k = self.kiosk diff --git a/src/diffwofost/physical_models/crop/wofost72.py b/src/diffwofost/physical_models/crop/wofost72.py index 59f1fdd..bf77536 100644 --- a/src/diffwofost/physical_models/crop/wofost72.py +++ b/src/diffwofost/physical_models/crop/wofost72.py @@ -5,7 +5,6 @@ from pcse.base import SimulationObject from pcse.base.parameter_providers import ParameterProvider from pcse.base.variablekiosk import VariableKiosk -from pcse.base.weather import WeatherDataContainer from pcse.traitlets import Instance from pcse.traitlets import Unicode from diffwofost.physical_models.base import TensorParamTemplate @@ -239,14 +238,13 @@ def _check_carbon_balance(day, DMI, GASS, MRES, CVF, pf): ) raise exc.CarbonBalanceError(msg) - def calc_rates(self, day: datetime.date, drv: WeatherDataContainer) -> None: + def calc_rates(self, day: datetime.date, drv: dict) -> None: """Calculate the rates of change of the state variables. Args: day (datetime.date): The current date of the simulation. - drv (WeatherDataContainer): A dictionary-like container holding - weather data elements as key/value. The values are - arrays or scalars. See PCSE documentation for details. + drv (dict): A container holding weather data elements as key/value. The values are + arrays or scalars. """ p = self.params r = self.rates diff --git a/src/diffwofost/physical_models/engine.py b/src/diffwofost/physical_models/engine.py index a84ef32..42295f4 100644 --- a/src/diffwofost/physical_models/engine.py +++ b/src/diffwofost/physical_models/engine.py @@ -6,8 +6,10 @@ """ import gc +from collections.abc import Iterator from collections.abc import MutableMapping from pathlib import Path +from typing import Any import torch from pcse import signals from pcse.base import BaseEngine @@ -28,6 +30,7 @@ class Engine(PcseEngine): """ mconf = Instance(Configuration) + weatherdataprovider = Instance(Iterator) parameterprovider = Instance(MutableMapping) def __init__( @@ -100,7 +103,7 @@ def setup( Args: parameterprovider: Provider with crop and soil parameter values. - weatherdataprovider: Provider used to retrieve daily driving + weatherdataprovider: Iterator used to provide daily driving weather variables. agromanagement: AgroManagement definition passed to the configured agromanagement component. @@ -113,7 +116,6 @@ def setup( self._reset_runtime_state() self.parameterprovider = parameterprovider - self._shape = _get_params_shape(self.parameterprovider) # Variable kiosk for registering and publishing variables self.kiosk = VariableKiosk(external_states) @@ -139,6 +141,9 @@ def setup( self.weatherdataprovider = weatherdataprovider self.drv = self._get_driving_variables(self.day) + # Determine common shape for the parameters and weather data + self._shape = _get_shape(self.parameterprovider, self.drv) + # Call AgroManagement module for management actions at initialization self.agromanager(self.day, None) @@ -217,27 +222,57 @@ def _finish_cropsimulation(self, day): self.crop.finalize(day) self._save_summary_output() + def _get_driving_variables(self, day): + """Get driving variables and return it.""" + drv = next(self.weatherdataprovider) + if "DAY" in drv: + assert drv["DAY"] == day, "Wrong day!" + return drv + + +def _get_shape(parameterprovider: MutableMapping, drivingvariables: dict[str, Any]) -> tuple: + """Infer common tensor shape from the parameter provider and the driving variables. + + Args: + parameterprovider: Parameter provider. + drivingvariables: Weather data. + + Raises: + ValueError: If non-matching shapes are found for the data providers. + + Returns: + tuple: Shared tensor shape. + """ + params_shape = _get_params_shape(parameterprovider) + weather_shape = _get_params_shape(drivingvariables) + if params_shape and weather_shape: + if params_shape != weather_shape: + raise ValueError( + "Non-matching shapes between parameter and weather data: " + f"{params_shape} and {weather_shape}" + ) + return params_shape or weather_shape + -def _get_params_shape(parameterprovider): +def _get_params_shape(provider: MutableMapping) -> tuple: """Infer the common tensor batch shape from a parameter provider. Afgen table parameters are expected to have an extra trailing dimension for table coordinates, which is ignored when determining the simulation shape. Args: - parameterprovider: Parameter provider containing scalar and tensor - parameters. + provider: Parameter provider containing scalar and tensor parameters. Returns: tuple: Shared tensor shape for all tensor-valued parameters, or an - empty tuple when all parameters are scalar. + empty tuple when all parameters are scalar. Raises: ValueError: If tensor parameters do not share a common shape. """ shape = () - for paramname in parameterprovider.keys(): - param = parameterprovider[paramname] + for paramname in provider.keys(): + param = provider[paramname] if isinstance(param, torch.Tensor): # We need to drop the last dimension from the Afgen table parameters param_shape = param.shape[:-1] if paramname.endswith("TB") else param.shape diff --git a/src/diffwofost/physical_models/soil/classic_waterbalance.py b/src/diffwofost/physical_models/soil/classic_waterbalance.py index 89ceaef..e16126a 100644 --- a/src/diffwofost/physical_models/soil/classic_waterbalance.py +++ b/src/diffwofost/physical_models/soil/classic_waterbalance.py @@ -81,8 +81,8 @@ def calc_rates(self, day, drv): # the potential soil/water evaporation rates directly because there is # no shading by the canopy. if "TRA" not in self.kiosk: - r.WTRA = torch.zeros_like(torch.as_tensor(drv.ES0)) - EVSMX = torch.as_tensor(drv.ES0) + r.WTRA = torch.zeros_like(drv["ES0"]) + EVSMX = drv["ES0"] else: r.WTRA = self.kiosk["TRA"] EVSMX = torch.as_tensor(self.kiosk["EVSMX"]) @@ -101,7 +101,7 @@ def calc_rates(self, day, drv): self.DSLR = torch.where(rain_ge_1, torch.ones_like(dslr_inc), dslr_inc) # Hold rainfall amount to keep track of soil surface wetness and reset self.DSLR if needed - self.RAINold = torch.as_tensor(drv.RAIN) + self.RAINold = drv["RAIN"] def integrate(self, day, delt=1.0): """Integrate state variables over one time step.""" @@ -405,9 +405,8 @@ def calc_rates(self, day, drv): Args: day (datetime.date): The current date of the simulation. - drv (WeatherDataContainer): A dictionary-like container holding - weather data elements as key/value. The values are - arrays or scalars. See PCSE documentation for details. + drv (dict): A container holding weather data elements as key/value. The values are + arrays or scalars. """ s = self.states @@ -424,9 +423,9 @@ def calc_rates(self, day, drv): # Transpiration and maximum evaporation rates from crop module. # Before emergence there is no canopy shading yet, so the water balance # must fall back to the weather-driven soil and surface evaporation. - weather_wtra = torch.zeros_like(torch.as_tensor(drv.ES0, dtype=dtype, device=device)) - weather_evwmx = torch.as_tensor(drv.E0, dtype=dtype, device=device) - weather_evsmx = torch.as_tensor(drv.ES0, dtype=dtype, device=device) + weather_wtra = torch.zeros_like(drv["ES0"], dtype=dtype, device=device) + weather_evwmx = drv["E0"] + weather_evsmx = drv["ES0"] if "TRA" not in k: r.WTRA = weather_wtra @@ -472,7 +471,7 @@ def calc_rates(self, day, drv): ) # Potentially infiltrating rainfall - RAIN_t = torch.as_tensor(drv.RAIN, dtype=dtype, device=device) + RAIN_t = drv["RAIN"] RINPRE_fixed = (1.0 - p.NOTINF) * RAIN_t RINPRE_storm = (1.0 - p.NOTINF * self.NINFTB(RAIN_t)) * RAIN_t # IFUNRN: 0 = fixed non-infiltrating fraction, 1 = function of storm size diff --git a/src/diffwofost/physical_models/test.py b/src/diffwofost/physical_models/test.py index 1dbbd7c..4f4de70 100644 --- a/src/diffwofost/physical_models/test.py +++ b/src/diffwofost/physical_models/test.py @@ -1,12 +1,11 @@ +import pandas as pd import torch import yaml from pcse import signals -from pcse.base.weather import WeatherDataContainer -from pcse.base.weather import WeatherDataProvider -from pcse.settings import settings from diffwofost.physical_models.config import ComputeConfig from diffwofost.physical_models.engine import Engine from diffwofost.physical_models.parameter_providers import ParameterProvider +from diffwofost.physical_models.weather import iterator_from_dataframe class EngineTestHelper(Engine): @@ -41,22 +40,6 @@ def _run(self): self._terminate_simulation(self.day) -class WeatherDataProviderTestHelper(WeatherDataProvider): - """It stores the weatherdata contained within the YAML tests.""" - - def __init__(self, yaml_weather, meteo_range_checks=True): - super().__init__() - # This is a temporary workaround. The `METEO_RANGE_CHECKS` logic in - # `__setattr__` method in `WeatherDataContainer` is not vector compatible - # yet. So we can disable it here when creating the `WeatherDataContainer` - # instances with arrays. - settings.METEO_RANGE_CHECKS = meteo_range_checks - for weather in yaml_weather: - weather_inputs = {k: v for k, v in weather.items() if k != "SNOWDEPTH"} - wdc = WeatherDataContainer(**weather_inputs) - self._store_WeatherDataContainer(wdc, wdc.DAY) - - def prepare_engine_input( test_data, crop_model_params, device=None, dtype=None, meteo_range_checks=True ): @@ -70,33 +53,11 @@ def prepare_engine_input( agro_management_inputs = test_data["AgroManagement"] cropd = test_data["ModelParameters"] - weather_data_provider = WeatherDataProviderTestHelper( - test_data["WeatherVariables"], meteo_range_checks=meteo_range_checks - ) + weather_data = pd.DataFrame(test_data["WeatherVariables"]) + if "DTEMP" not in weather_data.columns: + weather_data["DTEMP"] = (weather_data["TEMP"] + weather_data["TMAX"]) / 2.0 + weather_data_provider = iterator_from_dataframe(weather_data, check=False) - # The PCSE WeatherDataContainer stores required variables as Python floats. - # Some of our tests rely on weather inputs being torch.Tensors (e.g. to - # broadcast/batch weather variables). We only do this conversion when - # METEO_RANGE_CHECKS is disabled because the PCSE range checks assume - # scalar floats. - if not meteo_range_checks: - for (_, _), wdc in weather_data_provider.store.items(): - for varname in ( - "IRRAD", - "TMIN", - "TMAX", - "TEMP", - "VAP", - "RAIN", - "WIND", - "E0", - "ES0", - "ET0", - ): - if hasattr(wdc, varname): - value = getattr(wdc, varname) - if not isinstance(value, torch.Tensor): - setattr(wdc, varname, torch.tensor(value, dtype=dtype, device=device)) crop_model_params_provider = ParameterProvider(cropdata=cropd) external_states = test_data.get("ExternalStates") or [] diff --git a/src/diffwofost/physical_models/weather.py b/src/diffwofost/physical_models/weather.py new file mode 100644 index 0000000..9597a86 --- /dev/null +++ b/src/diffwofost/physical_models/weather.py @@ -0,0 +1,88 @@ +from collections.abc import Iterator +from dataclasses import dataclass +import pandas as pd +import torch +from diffwofost.physical_models.config import ComputeConfig + + +@dataclass(frozen=True) +class WeatherVariable: + unit: str + min: float + max: float + + +WEATHER_VARIABLES = dict( + LAT=WeatherVariable("Degrees", -90.0, 90.0), + LON=WeatherVariable("Degrees", -180.0, 180.0), + ELEV=WeatherVariable("m", -300, 6000), + IRRAD=WeatherVariable("J/m2/day", 0.0, 40e6), + TMIN=WeatherVariable("Celsius", -50.0, 60.0), + TMAX=WeatherVariable("Celsius", -50.0, 60.0), + VAP=WeatherVariable("hPa", 0.06, 199.3), + RAIN=WeatherVariable("cm/day", 0, 25), + E0=WeatherVariable("cm/day", 0.0, 2.5), + ES0=WeatherVariable("cm/day", 0.0, 2.5), + ET0=WeatherVariable("cm/day", 0.0, 2.5), + SNOWDEPTH=WeatherVariable("cm", 0.0, 250.0), + TEMP=WeatherVariable("Celsius", -50.0, 60.0), + TMINRA=WeatherVariable("Celsius", -50.0, 60.0), + WIND=WeatherVariable("m/s", 0.0, 100.0), + DTEMP=WeatherVariable("Celsius", -50.0, 60.0), +) + + +def iterator_from_dataframe(df: pd.DataFrame, check: bool = True) -> Iterator: + """Weather data generator from a Pandas DataFrame. + + This utility function transforms weather data from tabular format to an iterator of torch + tensors that can be fed to diffWOFOST's engine. + + Args: + df (pd.DataFrame): DataFrame containing weather data. Weather variables should be listed + along columns. In order to be interpreted as weather variables, columns should be named + as the keys of `diffwofost.physical_models.weather.WEATHER_VARIABLES`. Rows are expected + to represent daily time steps (an optional column named "DAY" should list the + corresponding dates). + check (bool, optional): Optionally carry out validity checks for the dataset. Defaults to + True. + + Yields: + dict[str, typing.Any]: Weather variables as key-value pairs. Variables will be converted + to torch tensors, using dtype and device as configured in `ComputeConfig`. + """ + if "DAY" in df: + days = pd.to_datetime(df["DAY"]) + else: + days = None + + if check: + # Check range of weather variables + for var_name, var in WEATHER_VARIABLES.items(): + if var_name in df.columns: + col = df[var_name] + assert ((var.min <= col) & (col <= var.max)).all(), ( + f"Values for `{var_name}` outside the range [{var.min}, {var.max}]" + f"(expected unit is {var.unit})." + ) + + # Check dates + if days is not None: + expected = pd.date_range(start=days.iloc[0], periods=len(days), freq="D") + assert (days == expected).all(), ( + "Column `DAY` must contain consecutive daily dates with no gaps or duplicates." + ) + + device = ComputeConfig.get_device() + dtype = ComputeConfig.get_dtype() + + vars = { + var_name: torch.tensor(df[var_name].to_numpy(), device=device, dtype=dtype) + for var_name in WEATHER_VARIABLES.keys() + if var_name in df.columns + } + if days is not None: + vars["DAY"] = days.dt.date.to_numpy() + + for n in range(len(df)): + yield {k: v[n] for k, v in vars.items()} diff --git a/tests/physical_models/crop/test_assimilation.py b/tests/physical_models/crop/test_assimilation.py index 2231911..19f81f0 100644 --- a/tests/physical_models/crop/test_assimilation.py +++ b/tests/physical_models/crop/test_assimilation.py @@ -1,7 +1,5 @@ -from unittest.mock import patch import pytest import torch -from pcse.models import Wofost72_PP from diffwofost.physical_models.config import Configuration from diffwofost.physical_models.crop.assimilation import WOFOST72_Assimilation from diffwofost.physical_models.test import EngineTestHelper @@ -249,12 +247,6 @@ def test_assimilation_with_multiple_parameter_arrays(self, device): repeated = crop_model_params_provider[param].repeat(30, 5, 1) crop_model_params_provider.set_override(param, repeated, check=False) - # Make weather drivers match (30, 5) so _get_drv validates/broadcasts. - for (_, _), wdc in weather_data_provider.store.items(): - wdc.IRRAD = torch.ones((30, 5), device=device, dtype=torch.float64) * wdc.IRRAD - wdc.TEMP = torch.ones((30, 5), device=device, dtype=torch.float64) * wdc.TEMP - wdc.TMIN = torch.ones((30, 5), device=device, dtype=torch.float64) * wdc.TMIN - engine = EngineTestHelper(config=assimilation_config) engine.setup( crop_model_params_provider, @@ -317,44 +309,31 @@ def test_assimilation_with_incompatible_weather_parameter_vectors(self): crop_model_params_provider.set_override( "AMAXTB", crop_model_params_provider["AMAXTB"].repeat(10, 1), check=False ) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones(5, dtype=torch.float64) * wdc.TEMP + + # Broadcast weather variables to a shape that does not match the parameters + shape = (5,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + broadcasted = broadcast(weather_data_provider) with pytest.raises(ValueError): engine = EngineTestHelper(config=assimilation_config) engine.setup( crop_model_params_provider, - weather_data_provider, + broadcasted, agro_management_inputs, external_states, ) - @pytest.mark.parametrize("test_data_url", wofost72_data_urls) - def test_wofost_pp_with_assimilation(self, test_data_url): - test_data = get_test_data(test_data_url) - crop_model_params = ["AMAXTB", "EFFTB", "KDIFTB", "TMPFTB", "TMNFTB"] - (crop_model_params_provider, weather_data_provider, agro_management_inputs, _) = ( - prepare_engine_input(test_data, crop_model_params) - ) - - expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] - - with patch("pcse.crop.wofost72.Assimilation", WOFOST72_Assimilation): - model = Wofost72_PP( - crop_model_params_provider, weather_data_provider, agro_management_inputs - ) - model.run_till_terminate() - actual_results = model.get_output() - - assert len(actual_results) == len(expected_results) - - for reference, model in zip(expected_results, actual_results, strict=False): - assert reference["DAY"] == model["day"] - assert all( - abs(reference[var] - model[var]) < precision - for var, precision in expected_precision.items() - ) - @pytest.mark.usefixtures("fast_mode") class TestDiffAssimilationGradients: diff --git a/tests/physical_models/crop/test_evapotranspiration.py b/tests/physical_models/crop/test_evapotranspiration.py index 1a83b08..115bee9 100644 --- a/tests/physical_models/crop/test_evapotranspiration.py +++ b/tests/physical_models/crop/test_evapotranspiration.py @@ -1,12 +1,10 @@ import datetime import warnings from types import SimpleNamespace -from unittest.mock import patch import pytest import torch from pcse.base.parameter_providers import ParameterProvider from pcse.base.variablekiosk import VariableKiosk -from pcse.models import Wofost72_PP from diffwofost.physical_models.config import Configuration from diffwofost.physical_models.crop.evapotranspiration import Evapotranspiration from diffwofost.physical_models.crop.evapotranspiration import EvapotranspirationCO2 @@ -268,23 +266,25 @@ def test_evapotranspiration_with_one_parameter_vector(self, param, device): ) if param == "ET0": - for (_, _), wdc in weather_data_provider.store.items(): - wdc.ET0 = torch.ones(10, dtype=torch.float64, device=wdc.ET0.device) * wdc.ET0 - with pytest.raises(ValueError): - engine = EngineTestHelper(config=evapotranspiration_config) - engine.setup( - crop_model_params_provider, - weather_data_provider, - agro_management_inputs, - external_states, - ) - return - - if param == "KDIFTB": + shape = (10,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + weather_data_provider = broadcast(weather_data_provider) + elif param == "KDIFTB": repeated = crop_model_params_provider[param].repeat(10, 1) + crop_model_params_provider.set_override(param, repeated, check=False) else: repeated = crop_model_params_provider[param].repeat(10) - crop_model_params_provider.set_override(param, repeated, check=False) + crop_model_params_provider.set_override(param, repeated, check=False) engine = EngineTestHelper(config=evapotranspiration_config) engine.setup( @@ -455,11 +455,6 @@ def test_evapotranspiration_with_multiple_parameter_arrays(self, device): repeated = crop_model_params_provider[param].broadcast_to(batch_shape) crop_model_params_provider.set_override(param, repeated, check=False) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.ET0 = torch.ones(batch_shape, dtype=torch.float64, device=wdc.ET0.device) * wdc.ET0 - wdc.E0 = torch.ones(batch_shape, dtype=torch.float64, device=wdc.E0.device) * wdc.E0 - wdc.ES0 = torch.ones(batch_shape, dtype=torch.float64, device=wdc.ES0.device) * wdc.ES0 - engine = EngineTestHelper(config=evapotranspiration_config) engine.setup( crop_model_params_provider, @@ -541,8 +536,21 @@ def test_evapotranspiration_with_incompatible_weather_parameter_vectors(self): crop_model_params_provider.set_override( "CFET", crop_model_params_provider["CFET"].repeat(10), check=False ) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.ET0 = torch.ones(5, dtype=torch.float64, device=wdc.ET0.device) * wdc.ET0 + + # Broadcast weather variables to a shape that does not match the parameters + shape = (5,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + weather_data_provider = broadcast(weather_data_provider) with pytest.raises(ValueError): engine = EngineTestHelper(config=evapotranspiration_config) @@ -553,48 +561,6 @@ def test_evapotranspiration_with_incompatible_weather_parameter_vectors(self): external_states, ) - @pytest.mark.parametrize("test_data_url", wofost72_data_urls) - def test_wofost_pp_with_evapotranspiration(self, test_data_url): - test_data = get_test_data(test_data_url) - crop_model_params = [ - "CFET", - "DEPNR", - "KDIFTB", - "IAIRDU", - "IOX", - "CRAIRC", - "SM0", - "SMW", - "SMFCF", - ] - (crop_model_params_provider, weather_data_provider, agro_management_inputs, _) = ( - prepare_engine_input(test_data, crop_model_params, meteo_range_checks=False) - ) - - expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] - - with patch("pcse.crop.wofost72.Evapotranspiration", EvapotranspirationWrapper): - model = Wofost72_PP( - crop_model_params_provider, weather_data_provider, agro_management_inputs - ) - model.run_till_terminate() - actual_results = model.get_output() - - assert len(actual_results) == len(expected_results) - for reference, model in zip(expected_results, actual_results, strict=False): - assert reference["DAY"] == model["day"] - for var, precision in expected_precision.items(): - if abs(reference[var] - model[var]) >= precision: - print( - f"Mismatch for {var} on day {model['day']}: expected {reference[var]}," - + f" got {model[var]}, diff {abs(reference[var] - model[var])}" - + f", precision {precision}" - ) - assert all( - abs(reference[var] - model[var]) < precision - for var, precision in expected_precision.items() - ) - def _minimal_parvalues(device: str, *, include_co2: bool = False, include_layers: bool = False): dtype = torch.float64 @@ -659,7 +625,7 @@ def _kiosk_with_states(): kiosk.set_variable(oid, "SM", torch.tensor(0.25, dtype=torch.float64, device=device)) return kiosk - drv = SimpleNamespace( + drv = dict( ET0=torch.tensor(0.5, dtype=torch.float64, device=device), E0=torch.tensor(0.6, dtype=torch.float64, device=device), ES0=torch.tensor(0.55, dtype=torch.float64, device=device), diff --git a/tests/physical_models/crop/test_leaf_dynamics.py b/tests/physical_models/crop/test_leaf_dynamics.py index 6e28f60..3abaa01 100644 --- a/tests/physical_models/crop/test_leaf_dynamics.py +++ b/tests/physical_models/crop/test_leaf_dynamics.py @@ -1,8 +1,6 @@ import warnings -from unittest.mock import patch import pytest import torch -from pcse.models import Wofost72_PP from diffwofost.physical_models.config import Configuration from diffwofost.physical_models.crop.leaf_dynamics import WOFOST_Leaf_Dynamics from diffwofost.physical_models.test import EngineTestHelper @@ -135,9 +133,20 @@ def test_leaf_dynamics_with_one_parameter_vector(self, param, device): # Setting a vector (with one value) for the selected parameter if param == "TEMP": - # Vectorize weather variable - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones(10, device=device, dtype=torch.float64) * wdc.TEMP + # Broadcast weather variables + shape = (10,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + weather_data_provider = broadcast(weather_data_provider) elif param in ["KDIFTB", "SLATB"]: # AfgenTrait parameters need to have shape (N, M) repeated = crop_model_params_provider[param].repeat(10, 1) @@ -146,48 +155,32 @@ def test_leaf_dynamics_with_one_parameter_vector(self, param, device): repeated = crop_model_params_provider[param].repeat(10) crop_model_params_provider.set_override(param, repeated, check=False) - if param == "TEMP": - # Expect error due to incompatible shapes - # (By defaults parameters are not reshaped following weather variables) - with pytest.raises(ValueError): - engine = EngineTestHelper(config=leaf_dynamics_config) - engine.setup( - crop_model_params_provider, - weather_data_provider, - agro_management_inputs, - external_states, - ) - engine.run_till_terminate() - actual_results = engine.get_output() - else: - engine = EngineTestHelper(config=leaf_dynamics_config) - engine.setup( - crop_model_params_provider, - weather_data_provider, - agro_management_inputs, - external_states, + engine = EngineTestHelper(config=leaf_dynamics_config) + engine.setup( + crop_model_params_provider, + weather_data_provider, + agro_management_inputs, + external_states, + ) + engine.run_till_terminate() + actual_results = engine.get_output() + + # get expected results from YAML test data + expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] + + assert len(actual_results) == len(expected_results) + + for reference, model in zip(expected_results, actual_results, strict=False): + assert reference["DAY"] == model["day"] + # Verify output is on the correct device + for var in expected_precision.keys(): + assert model[var].device.type == device, f"{var} should be on {device}" + # Move to CPU for comparison + model_cpu = {k: v.cpu() if isinstance(v, torch.Tensor) else v for k, v in model.items()} + assert all( + all(abs(reference[var] - model_cpu[var]) < precision) + for var, precision in expected_precision.items() ) - engine.run_till_terminate() - actual_results = engine.get_output() - - # get expected results from YAML test data - expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] - - assert len(actual_results) == len(expected_results) - - for reference, model in zip(expected_results, actual_results, strict=False): - assert reference["DAY"] == model["day"] - # Verify output is on the correct device - for var in expected_precision.keys(): - assert model[var].device.type == device, f"{var} should be on {device}" - # Move to CPU for comparison - model_cpu = { - k: v.cpu() if isinstance(v, torch.Tensor) else v for k, v in model.items() - } - assert all( - all(abs(reference[var] - model_cpu[var]) < precision) - for var, precision in expected_precision.items() - ) @pytest.mark.parametrize( "param,delta", @@ -316,9 +309,6 @@ def test_leaf_dynamics_with_multiple_parameter_arrays(self, device): repeated = crop_model_params_provider[param].broadcast_to((30, 5)) crop_model_params_provider.set_override(param, repeated, check=False) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones((30, 5), dtype=torch.float64, device=device) * wdc.TEMP - engine = EngineTestHelper(config=leaf_dynamics_config) engine.setup( crop_model_params_provider, @@ -390,8 +380,20 @@ def test_leaf_dynamics_with_incompatible_weather_parameter_vectors(self): crop_model_params_provider.set_override( "TDWI", crop_model_params_provider["TDWI"].repeat(10), check=False ) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones(5, dtype=torch.float64) * wdc.TEMP + # Broadcast weather variables to a shape that does not match the parameters + shape = (5,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + weather_data_provider = broadcast(weather_data_provider) with pytest.raises(ValueError): engine = EngineTestHelper(config=leaf_dynamics_config) @@ -402,34 +404,6 @@ def test_leaf_dynamics_with_incompatible_weather_parameter_vectors(self): external_states, ) - @pytest.mark.parametrize("test_data_url", wofost72_data_urls) - def test_wofost_pp_with_leaf_dynamics(self, test_data_url): - # prepare model input - test_data = get_test_data(test_data_url) - crop_model_params = ["SPAN", "TDWI", "TBASE", "PERDL", "RGRLAI", "KDIFTB", "SLATB"] - (crop_model_params_provider, weather_data_provider, agro_management_inputs, _) = ( - prepare_engine_input(test_data, crop_model_params) - ) - - # get expected results from YAML test data - expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] - - with patch("pcse.crop.wofost72.Leaf_Dynamics", WOFOST_Leaf_Dynamics): - model = Wofost72_PP( - crop_model_params_provider, weather_data_provider, agro_management_inputs - ) - model.run_till_terminate() - actual_results = model.get_output() - - assert len(actual_results) == len(expected_results) - - for reference, model in zip(expected_results, actual_results, strict=False): - assert reference["DAY"] == model["day"] - assert all( - abs(reference[var] - model[var]) < precision - for var, precision in expected_precision.items() - ) - @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. diff --git a/tests/physical_models/crop/test_partitioning.py b/tests/physical_models/crop/test_partitioning.py index d794082..a427a81 100644 --- a/tests/physical_models/crop/test_partitioning.py +++ b/tests/physical_models/crop/test_partitioning.py @@ -1,9 +1,7 @@ import warnings -from unittest.mock import patch import pytest import torch from numpy.testing import assert_array_almost_equal -from pcse.models import Wofost72_PP from diffwofost.physical_models.config import Configuration from diffwofost.physical_models.crop.partitioning import DVS_Partitioning from diffwofost.physical_models.test import EngineTestHelper @@ -287,34 +285,6 @@ def test_partitioning_with_incompatible_parameter_vectors(self): external_states, ) - @pytest.mark.parametrize("test_data_url", wofost72_data_urls[:1]) - def test_wofost_pp_with_partitioning(self, test_data_url): - # prepare model input - test_data = get_test_data(test_data_url) - crop_model_params = ["FRTB", "FLTB", "FSTB", "FOTB"] - (crop_model_params_provider, weather_data_provider, agro_management_inputs, _) = ( - prepare_engine_input(test_data, crop_model_params) - ) - - # get expected results from YAML test data - expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] - - with patch("pcse.crop.wofost72.Partitioning", DVS_Partitioning): - model = Wofost72_PP( - crop_model_params_provider, weather_data_provider, agro_management_inputs - ) - model.run_till_terminate() - actual_results = model.get_output() - - assert len(actual_results) == len(expected_results) - - for reference, model in zip(expected_results, actual_results, strict=False): - assert reference["DAY"] == model["day"] - assert all( - abs(reference[var] - model[var]) < precision - for var, precision in expected_precision.items() - ) - @pytest.mark.usefixtures("fast_mode") class TestDiffPartitioningGradients: diff --git a/tests/physical_models/crop/test_phenology.py b/tests/physical_models/crop/test_phenology.py index b25afe5..273e518 100644 --- a/tests/physical_models/crop/test_phenology.py +++ b/tests/physical_models/crop/test_phenology.py @@ -1,8 +1,6 @@ import warnings -from unittest.mock import patch import pytest import torch -from pcse.models import Wofost72_PP from diffwofost.physical_models.config import Configuration from diffwofost.physical_models.crop.phenology import DVS_Phenology from diffwofost.physical_models.test import EngineTestHelper @@ -233,10 +231,19 @@ def test_phenology_with_one_parameter_vector(self, param, device): if param == "TEMP": if device == "cuda": pytest.skip("Weather parameter vector tests are CPU-only") - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones(10, dtype=torch.float64, device=device) * torch.as_tensor( - wdc.TEMP, dtype=torch.float64, device=device - ) + shape = (10,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + weather_data_provider = broadcast(weather_data_provider) elif param == "DTSMTB": repeated = crop_model_params_provider[param].repeat(10, 1) crop_model_params_provider.set_override(param, repeated, check=False) @@ -244,30 +251,19 @@ def test_phenology_with_one_parameter_vector(self, param, device): repeated = crop_model_params_provider[param].repeat(10) crop_model_params_provider.set_override(param, repeated, check=False) - if param == "TEMP": - with pytest.raises(ValueError): - engine = EngineTestHelper(config=phenology_config) - engine.setup( - crop_model_params_provider, - weather_data_provider, - agro_management_inputs, - ) - engine.run_till_terminate() - _ = engine.get_output() - else: - engine = EngineTestHelper(config=phenology_config) - engine.setup( - crop_model_params_provider, - weather_data_provider, - agro_management_inputs, - ) - engine.run_till_terminate() - actual_results = engine.get_output() - expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] + engine = EngineTestHelper(config=phenology_config) + engine.setup( + crop_model_params_provider, + weather_data_provider, + agro_management_inputs, + ) + engine.run_till_terminate() + actual_results = engine.get_output() + expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] - assert len(actual_results) == len(expected_results) - for reference, model in zip(expected_results, actual_results, strict=False): - assert_reference_match(reference, model, expected_precision) + assert len(actual_results) == len(expected_results) + for reference, model in zip(expected_results, actual_results, strict=False): + assert_reference_match(reference, model, expected_precision) @pytest.mark.parametrize( "param,delta", @@ -443,9 +439,6 @@ def test_phenology_with_multiple_parameter_arrays(self, device): repeated = crop_model_params_provider[param].broadcast_to((30, 5)) crop_model_params_provider.set_override(param, repeated, check=False) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones((30, 5), device=device, dtype=torch.float64) * wdc.TEMP - engine = EngineTestHelper(config=phenology_config) engine.setup( crop_model_params_provider, @@ -535,8 +528,21 @@ def test_phenology_with_incompatible_weather_parameter_vectors(self): crop_model_params_provider.set_override( "TSUM1", crop_model_params_provider["TSUM1"].repeat(10), check=False ) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones(5, dtype=torch.float64) * wdc.TEMP + + # Broadcast weather variables to a shape that does not match the parameters + shape = (5,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + weather_data_provider = broadcast(weather_data_provider) with pytest.raises(ValueError): engine = EngineTestHelper(config=phenology_config) @@ -546,45 +552,6 @@ def test_phenology_with_incompatible_weather_parameter_vectors(self): agro_management_inputs, ) - @pytest.mark.parametrize("test_data_url", wofost72_data_urls) - def test_wofost_pp_with_phenology(self, test_data_url, monkeypatch): - test_data = get_test_data(test_data_url) - crop_model_params = [ - "TSUMEM", - "TBASEM", - "TEFFMX", - "TSUM1", - "TSUM2", - "IDSL", - "DLO", - "DLC", - "DVSI", - "DVSEND", - "DTSMTB", - "VERNSAT", - "VERNBASE", - "VERNDVS", - ] - (crop_model_params_provider, weather_data_provider, agro_management_inputs, _) = ( - prepare_engine_input(test_data, crop_model_params) - ) - expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] - - # Keep this integration test on CPU. - monkeypatch.setattr(DVS_Phenology, "device", "cpu") - monkeypatch.setattr(DVS_Phenology, "dtype", torch.float64) - - with patch("pcse.crop.wofost72.Phenology", DVS_PhenologyForPCSE): - model = Wofost72_PP( - crop_model_params_provider, weather_data_provider, agro_management_inputs - ) - model.run_till_terminate() - actual_results = model.get_output() - - assert len(actual_results) == len(expected_results) - for reference, model_day in zip(expected_results, actual_results, strict=False): - assert_reference_match(reference, model_day, expected_precision) - @pytest.mark.usefixtures("fast_mode") class TestDiffPhenologyDynamicsGradients: diff --git a/tests/physical_models/crop/test_respiration.py b/tests/physical_models/crop/test_respiration.py index 0edb19e..29203df 100644 --- a/tests/physical_models/crop/test_respiration.py +++ b/tests/physical_models/crop/test_respiration.py @@ -1,8 +1,6 @@ import warnings -from unittest.mock import patch import pytest import torch -from pcse.models import Wofost72_PP from diffwofost.physical_models.config import Configuration from diffwofost.physical_models.crop.respiration import WOFOST_Maintenance_Respiration from diffwofost.physical_models.test import EngineTestHelper @@ -128,25 +126,25 @@ def test_respiration_with_one_parameter_vector(self, param, device): ) = prepare_engine_input(test_data, crop_model_params, meteo_range_checks=False) if param == "TEMP": - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones(10, dtype=torch.float64, device=device) * wdc.TEMP - with pytest.raises(ValueError): - engine = EngineTestHelper(config=respiration_config) - engine.setup( - crop_model_params_provider, - weather_data_provider, - agro_management_inputs, - external_states, - ) - engine.run_till_terminate() - _ = engine.get_output() - return - - if param == "RFSETB": + shape = (10,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + weather_data_provider = broadcast(weather_data_provider) + elif param == "RFSETB": repeated = crop_model_params_provider[param].repeat(10, 1) + crop_model_params_provider.set_override(param, repeated, check=False) else: repeated = crop_model_params_provider[param].repeat(10) - crop_model_params_provider.set_override(param, repeated, check=False) + crop_model_params_provider.set_override(param, repeated, check=False) engine = EngineTestHelper(config=respiration_config) engine.setup( @@ -271,9 +269,6 @@ def test_respiration_with_multiple_parameter_arrays(self, device): "RFSETB", crop_model_params_provider["RFSETB"].repeat(30, 5, 1), check=False ) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones((30, 5), dtype=torch.float64, device=device) * wdc.TEMP - engine = EngineTestHelper(config=respiration_config) engine.setup( crop_model_params_provider, @@ -336,8 +331,19 @@ def test_respiration_with_incompatible_weather_parameter_vectors(self): crop_model_params_provider.set_override( "RMR", crop_model_params_provider["RMR"].repeat(10), check=False ) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones(5, dtype=torch.float64) * wdc.TEMP + shape = (5,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + weather_data_provider = broadcast(weather_data_provider) with pytest.raises(ValueError): engine = EngineTestHelper(config=respiration_config) @@ -348,34 +354,6 @@ def test_respiration_with_incompatible_weather_parameter_vectors(self): external_states, ) - @pytest.mark.parametrize("test_data_url", wofost72_data_urls) - def test_wofost_pp_with_respiration(self, test_data_url): - # prepare model input - test_data = get_test_data(test_data_url) - crop_model_params = ["Q10", "RMR", "RML", "RMS", "RMO", "RFSETB"] - (crop_model_params_provider, weather_data_provider, agro_management_inputs, _) = ( - prepare_engine_input(test_data, crop_model_params) - ) - - # get expected results from YAML test data - expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] - - with patch("pcse.crop.wofost72.MaintenanceRespiration", WOFOST_Maintenance_Respiration): - model = Wofost72_PP( - crop_model_params_provider, weather_data_provider, agro_management_inputs - ) - model.run_till_terminate() - actual_results = model.get_output() - - assert len(actual_results) == len(expected_results) - - for reference, model in zip(expected_results, actual_results, strict=False): - assert reference["DAY"] == model["day"] - assert all( - abs(reference[var] - model[var]) < precision - for var, precision in expected_precision.items() - ) - @pytest.mark.usefixtures("fast_mode") class TestDiffRespirationGradients: diff --git a/tests/physical_models/crop/test_root_dynamics.py b/tests/physical_models/crop/test_root_dynamics.py index ecead5d..7b36cee 100644 --- a/tests/physical_models/crop/test_root_dynamics.py +++ b/tests/physical_models/crop/test_root_dynamics.py @@ -1,9 +1,7 @@ import warnings -from unittest.mock import patch import pytest import torch from numpy.testing import assert_array_almost_equal -from pcse.models import Wofost72_PP from diffwofost.physical_models.config import Configuration from diffwofost.physical_models.crop.root_dynamics import WOFOST_Root_Dynamics from diffwofost.physical_models.test import EngineTestHelper @@ -345,34 +343,6 @@ def test_root_dynamics_with_incompatible_parameter_vectors(self, device): external_states, ) - @pytest.mark.parametrize("test_data_url", wofost72_data_urls) - def test_wofost_pp_with_root_dynamics(self, test_data_url): - # prepare model input - test_data = get_test_data(test_data_url) - crop_model_params = ["RDI", "RRI", "RDMCR", "RDMSOL", "TDWI", "IAIRDU", "RDRRTB"] - (crop_model_params_provider, weather_data_provider, agro_management_inputs, _) = ( - prepare_engine_input(test_data, crop_model_params) - ) - - # get expected results from YAML test data - expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] - - with patch("pcse.crop.wofost72.Root_Dynamics", WOFOST_Root_Dynamics): - model = Wofost72_PP( - crop_model_params_provider, weather_data_provider, agro_management_inputs - ) - model.run_till_terminate() - actual_results = model.get_output() - - assert len(actual_results) == len(expected_results) - - for reference, model in zip(expected_results, actual_results, strict=False): - assert reference["DAY"] == model["day"] - assert all( - abs(reference[var] - model[var]) < precision - for var, precision in expected_precision.items() - ) - @pytest.mark.usefixtures("fast_mode") class TestDiffRootDynamicsGradients: diff --git a/tests/physical_models/crop/test_stem_dynamics.py b/tests/physical_models/crop/test_stem_dynamics.py index 2c5268f..8ac40e8 100644 --- a/tests/physical_models/crop/test_stem_dynamics.py +++ b/tests/physical_models/crop/test_stem_dynamics.py @@ -1,8 +1,6 @@ import warnings -from unittest.mock import patch import pytest import torch -from pcse.models import Wofost72_PP from diffwofost.physical_models.config import Configuration from diffwofost.physical_models.crop.stem_dynamics import WOFOST_Stem_Dynamics from diffwofost.physical_models.test import EngineTestHelper @@ -185,9 +183,20 @@ def test_stem_dynamics_with_one_parameter_vector(self, param, device): # Setting a vector (with one value) for the selected parameter if param == "TEMP": - # Vectorize weather variable - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones(10, dtype=torch.float64, device=device) * wdc.TEMP + # Broadcast weather variables + shape = (10,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + weather_data_provider = broadcast(weather_data_provider) else: # Broadcast all parameters to match the batch size of 10 # This ensures compatibility for all parameters including table traits @@ -203,35 +212,21 @@ def test_stem_dynamics_with_one_parameter_vector(self, param, device): p_name, p_val.repeat(10, 1), check=False ) - if param == "TEMP": - # Vectorize weather variable - # We expect the model to handle scalar parameters with vectorized weather - # via implicit broadcasting or explicit checks passing. - engine = EngineTestHelper(config=stem_dynamics_config) - engine.setup( - crop_model_params_provider, - weather_data_provider, - agro_management_inputs, - external_states, - ) - engine.run_till_terminate() - actual_results = engine.get_output() - else: - engine = EngineTestHelper(config=stem_dynamics_config) - engine.setup( - crop_model_params_provider, - weather_data_provider, - agro_management_inputs, - external_states, - ) - engine.run_till_terminate() - actual_results = engine.get_output() + engine = EngineTestHelper(config=stem_dynamics_config) + engine.setup( + crop_model_params_provider, + weather_data_provider, + agro_management_inputs, + external_states, + ) + engine.run_till_terminate() + actual_results = engine.get_output() - # get expected results from YAML test data - expected_results = test_data["ModelResults"] + # get expected results from YAML test data + expected_results = test_data["ModelResults"] - # Assertions on values removed as test data is not appropriate for this module - assert len(actual_results) == len(expected_results) + # Assertions on values removed as test data is not appropriate for this module + assert len(actual_results) == len(expected_results) @pytest.mark.parametrize( "param,delta", @@ -359,9 +354,6 @@ def test_stem_dynamics_with_multiple_parameter_arrays(self, device): repeated = crop_model_params_provider[param].broadcast_to((30, 5)) crop_model_params_provider.set_override(param, repeated, check=False) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones((30, 5), dtype=torch.float64, device=device) * wdc.TEMP - engine = EngineTestHelper(config=stem_dynamics_config) engine.setup( crop_model_params_provider, @@ -422,8 +414,21 @@ def test_stem_dynamics_with_incompatible_weather_parameter_vectors(self): crop_model_params_provider.set_override( "TDWI", crop_model_params_provider["TDWI"].repeat(10), check=False ) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones(5, dtype=torch.float64) * wdc.TEMP + + # Broadcast weather variables + shape = (5,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + weather_data_provider = broadcast(weather_data_provider) with pytest.raises((AssertionError, ValueError)): engine = EngineTestHelper(config=stem_dynamics_config) @@ -434,34 +439,6 @@ def test_stem_dynamics_with_incompatible_weather_parameter_vectors(self): external_states, ) - @pytest.mark.parametrize("test_data_url", wofost72_data_urls) - def test_wofost_pp_with_stem_dynamics(self, test_data_url): - # prepare model input - test_data = get_test_data(test_data_url) - crop_model_params = ["TDWI", "RDRSTB", "SSATB"] - (crop_model_params_provider, weather_data_provider, agro_management_inputs, _) = ( - prepare_engine_input(test_data, crop_model_params) - ) - - # get expected results from YAML test data - expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] - - with patch("pcse.crop.wofost72.Stem_Dynamics", WOFOST_Stem_Dynamics): - model = Wofost72_PP( - crop_model_params_provider, weather_data_provider, agro_management_inputs - ) - model.run_till_terminate() - actual_results = model.get_output() - - assert len(actual_results) == len(expected_results) - - for reference, model in zip(expected_results, actual_results, strict=False): - assert reference["DAY"] == model["day"] - assert all( - abs(reference[var] - model[var]) < precision - for var, precision in expected_precision.items() - ) - @pytest.mark.usefixtures("fast_mode") class TestDiffStemDynamicsGradients: diff --git a/tests/physical_models/crop/test_storage_organ_dynamics.py b/tests/physical_models/crop/test_storage_organ_dynamics.py index 1d7f5ef..426cd06 100644 --- a/tests/physical_models/crop/test_storage_organ_dynamics.py +++ b/tests/physical_models/crop/test_storage_organ_dynamics.py @@ -1,8 +1,6 @@ import warnings -from unittest.mock import patch import pytest import torch -from pcse.models import Wofost72_PP from diffwofost.physical_models.config import Configuration from diffwofost.physical_models.crop.storage_organ_dynamics import WOFOST_Storage_Organ_Dynamics from diffwofost.physical_models.test import EngineTestHelper @@ -181,9 +179,20 @@ def test_storage_dynamics_with_one_parameter_vector(self, param, device): # Setting a vector (with one value) for the selected parameter if param == "TEMP": - # Vectorize weather variable - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones(10, dtype=torch.float64, device=device) * wdc.TEMP + # Broadcast weather variable + shape = (10,) + + def broadcast(wdp): + for weather_data in wdp: + out = {} + for k, v in weather_data.items(): + if isinstance(v, torch.Tensor): + out[k] = torch.broadcast_to(v, shape) + else: + out[k] = v + yield out + + weather_data_provider = broadcast(weather_data_provider) else: # Broadcast all parameters to match the batch size of 10 for p_name in ["TDWI", "SPA"]: @@ -198,35 +207,21 @@ def test_storage_dynamics_with_one_parameter_vector(self, param, device): p_name, p_val.repeat(10, 1), check=False ) - if param == "TEMP": - # Vectorize weather variable - # We expect the model to handle scalar parameters with vectorized weather - # via implicit broadcasting or explicit checks passing. - engine = EngineTestHelper(config=storage_dynamics_config) - engine.setup( - crop_model_params_provider, - weather_data_provider, - agro_management_inputs, - external_states, - ) - engine.run_till_terminate() - actual_results = engine.get_output() - else: - engine = EngineTestHelper(config=storage_dynamics_config) - engine.setup( - crop_model_params_provider, - weather_data_provider, - agro_management_inputs, - external_states, - ) - engine.run_till_terminate() - actual_results = engine.get_output() + engine = EngineTestHelper(config=storage_dynamics_config) + engine.setup( + crop_model_params_provider, + weather_data_provider, + agro_management_inputs, + external_states, + ) + engine.run_till_terminate() + actual_results = engine.get_output() - # get expected results from YAML test data - expected_results = test_data["ModelResults"] + # get expected results from YAML test data + expected_results = test_data["ModelResults"] - # Assertions on values removed as test data is not appropriate for this module - assert len(actual_results) == len(expected_results) + # Assertions on values removed as test data is not appropriate for this module + assert len(actual_results) == len(expected_results) @pytest.mark.parametrize( "param,delta", @@ -338,9 +333,6 @@ def test_storage_dynamics_with_multiple_parameter_arrays(self, device): repeated = crop_model_params_provider[param].broadcast_to((30, 5)) crop_model_params_provider.set_override(param, repeated, check=False) - for (_, _), wdc in weather_data_provider.store.items(): - wdc.TEMP = torch.ones((30, 5), dtype=torch.float64, device=device) * wdc.TEMP - engine = EngineTestHelper(config=storage_dynamics_config) engine.setup( crop_model_params_provider, @@ -386,34 +378,6 @@ def test_storage_dynamics_with_incompatible_parameter_vectors(self): external_states, ) - @pytest.mark.parametrize("test_data_url", wofost72_data_urls) - def test_wofost_pp_with_storage_dynamics(self, test_data_url): - # prepare model input - test_data = get_test_data(test_data_url) - crop_model_params = ["TDWI", "SPA"] - (crop_model_params_provider, weather_data_provider, agro_management_inputs, _) = ( - prepare_engine_input(test_data, crop_model_params) - ) - - # get expected results from YAML test data - expected_results, expected_precision = test_data["ModelResults"], test_data["Precision"] - - with patch("pcse.crop.wofost72.Storage_Organ_Dynamics", WOFOST_Storage_Organ_Dynamics): - model = Wofost72_PP( - crop_model_params_provider, weather_data_provider, agro_management_inputs - ) - model.run_till_terminate() - actual_results = model.get_output() - - assert len(actual_results) == len(expected_results) - - for reference, model in zip(expected_results, actual_results, strict=False): - assert reference["DAY"] == model["day"] - assert all( - abs(reference[var] - model[var]) < precision - for var, precision in expected_precision.items() - ) - @pytest.mark.usefixtures("fast_mode") class TestDiffStorageDynamicsGradients: