Skip to content
Draft
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
32 changes: 12 additions & 20 deletions src/diffwofost/physical_models/crop/assimilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -373,32 +371,26 @@ 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)
kdif = p.KDIFTB(dvs)
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,
Expand All @@ -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

Expand Down
53 changes: 25 additions & 28 deletions src/diffwofost/physical_models/crop/evapotranspiration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 5 additions & 8 deletions src/diffwofost/physical_models/crop/leaf_dynamics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 5 additions & 8 deletions src/diffwofost/physical_models/crop/phenology.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 3 additions & 5 deletions src/diffwofost/physical_models/crop/respiration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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
Expand Down
10 changes: 4 additions & 6 deletions src/diffwofost/physical_models/crop/root_dynamics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading