Skip to content

Crop modules

Note

At the moment only two modules of leaf_dynamics and root_dynamics are differentiable w.r.t two parameters of SPAN and TDWI. But the package is under continuous development. So make sure that you install the latest version.

diffwofost.physical_models.crop.leaf_dynamics.WOFOST_Leaf_Dynamics

Bases: SimulationObject

Leaf dynamics for the WOFOST crop model.

Implementation of biomass partitioning to leaves, growth and senenscence of leaves. WOFOST keeps track of the biomass that has been partitioned to the leaves for each day (variable LV), which is called a leaf class). For each leaf class the leaf age (variable 'LVAGE') and specific leaf area (variable SLA) are also registered. Total living leaf biomass is calculated by summing the biomass values for all leaf classes. Similarly, leaf area is calculated by summing leaf biomass times specific leaf area (LV * SLA).

Senescense of the leaves can occur as a result of physiological age, drought stress or self-shading.

Simulation parameters (provide in cropdata dictionary)

Name Description Type Unit
RGRLAI Maximum relative increase in LAI. SCr ha ha⁻¹ d⁻¹
SPAN Life span of leaves growing at 35 Celsius SCr d
TBASE Lower threshold temp. for ageing of leaves SCr C
PERDL Max. relative death rate of leaves due to water stress SCr
TDWI Initial total crop dry weight SCr kg ha⁻¹
KDIFTB Extinction coefficient for diffuse visible light as function of DVS TCr
SLATB Specific leaf area as a function of DVS TCr ha kg⁻¹

State variables

Name Description Pbl Unit
LV Leaf biomass per leaf class N kg ha⁻¹
SLA Specific leaf area per leaf class N ha kg⁻¹
LVAGE Leaf age per leaf class N d
LVSUM Sum of LV N kg ha⁻¹
LAIEM LAI at emergence N -
LASUM Total leaf area as sum of LV*SLA, not including stem and pod area N -
LAIEXP LAI value under theoretical exponential growth N -
LAIMAX Maximum LAI reached during growth cycle N -
LAI Leaf area index, including stem and pod area Y -
WLV Dry weight of living leaves Y kg ha⁻¹
DWLV Dry weight of dead leaves N kg ha⁻¹
TWLV Dry weight of total leaves (living + dead) Y kg ha⁻¹

Rate variables

Name Description Pbl Unit
GRLV Growth rate leaves N kg ha⁻¹ d⁻¹
DSLV1 Death rate leaves due to water stress N kg ha⁻¹ d⁻¹
DSLV2 Death rate leaves due to self-shading N kg ha⁻¹ d⁻¹
DSLV3 Death rate leaves due to frost kill N kg ha⁻¹ d⁻¹
DSLV Maximum of DSLV1, DSLV2, DSLV3 N kg ha⁻¹ d⁻¹
DALV Death rate leaves due to aging N kg ha⁻¹ d⁻¹
DRLV Death rate leaves as a combination of DSLV and DALV N kg ha⁻¹ d⁻¹
SLAT Specific leaf area for current time step, adjusted for source/sink limited leaf expansion rate N ha kg⁻¹
FYSAGE Increase in physiological leaf age N -
GLAIEX Sink-limited leaf expansion rate (exponential curve) N ha ha⁻¹ d⁻¹
GLASOL Source-limited leaf expansion rate (biomass increase) N ha ha⁻¹ d⁻¹

External dependencies

Name Description Provided by Unit
DVS Crop development stage DVS_Phenology -
FL Fraction biomass to leaves DVS_Partitioning -
FR Fraction biomass to roots DVS_Partitioning -
SAI Stem area index WOFOST_Stem_Dynamics -
PAI Pod area index WOFOST_Storage_Organ_Dynamics -
TRA Transpiration rate Evapotranspiration cm day⁻¹ ?
TRAMX Maximum transpiration rate Evapotranspiration cm day⁻¹ ?
ADMI Above-ground dry matter increase CropSimulation kg ha⁻¹ d⁻¹
RFTRA Reduction factor for transpiration (water & oxygen) Y -
RF_FROST Reduction factor frost kill FROSTOL (optional) -

Outputs

Name Description Pbl Unit
LAI Leaf area index, including stem and pod area Y -
TWLV Dry weight of total leaves (living + dead) Y kg ha⁻¹

Methods:

  • initialize

    Initialize the WOFOST_Leaf_Dynamics simulation object.

  • calc_rates

    Calculate the rates of change for the leaf dynamics.

  • integrate

    Integrate the leaf dynamics state variables.

initialize

initialize(day: date, kiosk: VariableKiosk, parvalues: ParameterProvider) -> None

Initialize the WOFOST_Leaf_Dynamics simulation object.

Parameters:

  • day (date) –

    The starting date of the simulation.

  • kiosk (VariableKiosk) –

    A container for registering and publishing (internal and external) state variables. See PCSE documentation for details.

  • parvalues (ParameterProvider) –

    A dictionary-like container holding all parameter sets (crop, soil, site) as key/value. The values are arrays or scalars. See PCSE documentation for details.

Source code in src/diffwofost/physical_models/crop/leaf_dynamics.py
def initialize(
    self, day: datetime.date, kiosk: VariableKiosk, parvalues: ParameterProvider
) -> None:
    """Initialize the WOFOST_Leaf_Dynamics simulation object.

    Args:
        day (datetime.date): The starting date of the simulation.
        kiosk (VariableKiosk): A container for registering and publishing
            (internal and external) state variables. See PCSE documentation for
            details.
        parvalues (ParameterProvider): A dictionary-like container holding
            all parameter sets (crop, soil, site) as key/value. The values are
            arrays or scalars. See PCSE documentation for details.
    """
    self.START_DATE = day
    self.kiosk = kiosk
    # TODO check if parvalues are already torch.nn.Parameters
    self.params = self.Parameters(parvalues)
    self.rates = self.RateVariables(kiosk)

    # CALCULATE INITIAL STATE VARIABLES
    # check for required external variables
    _exist_required_external_variables(self.kiosk)
    # TODO check if external variables are already torch tensors

    FL = self.kiosk["FL"]
    FR = self.kiosk["FR"]
    DVS = self.kiosk["DVS"]

    params = self.params
    shape = _get_params_shape(params)

    # Initial leaf biomass
    WLV = (params.TDWI * (1 - FR)) * FL
    DWLV = torch.zeros(shape, dtype=DTYPE)
    TWLV = WLV + DWLV

    # Initialize leaf classes (SLA, age and weight)
    SLA = torch.zeros((*shape, self.MAX_DAYS), dtype=DTYPE)
    LVAGE = torch.zeros((*shape, self.MAX_DAYS), dtype=DTYPE)
    LV = torch.zeros((*shape, self.MAX_DAYS), dtype=DTYPE)
    SLA[..., 0] = params.SLATB(DVS)
    LV[..., 0] = WLV

    # Initial values for leaf area
    LAIEM = LV[..., 0] * SLA[..., 0]
    LASUM = LAIEM
    LAIEXP = LAIEM
    LAIMAX = LAIEM
    LAI = LASUM + self.kiosk["SAI"] + self.kiosk["PAI"]

    # Initialize StateVariables object
    self.states = self.StateVariables(
        kiosk,
        publish=["LAI", "TWLV", "WLV"],
        LV=LV,
        SLA=SLA,
        LVAGE=LVAGE,
        LAIEM=LAIEM,
        LASUM=LASUM,
        LAIEXP=LAIEXP,
        LAIMAX=LAIMAX,
        LAI=LAI,
        WLV=WLV,
        DWLV=DWLV,
        TWLV=TWLV,
    )

calc_rates

calc_rates(day: date, drv: WeatherDataContainer) -> None

Calculate the rates of change for the leaf dynamics.

Parameters:

  • day (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.

Source code in src/diffwofost/physical_models/crop/leaf_dynamics.py
@prepare_rates
def calc_rates(self, day: datetime.date, drv: WeatherDataContainer) -> None:
    """Calculate the rates of change for the leaf dynamics.

    Args:
        day (datetime.date, optional): The current date of the simulation.
        drv (WeatherDataContainer, optional): A dictionary-like container holding
            weather data elements as key/value. The values are
            arrays or scalars. See PCSE documentation for details.
    """
    r = self.rates
    s = self.states
    p = self.params
    k = self.kiosk

    # If DVS < 0, the crop has not yet emerged, so we zerofy the rates using mask
    # Make a mask (0 if DVS < 0, 1 if DVS >= 0)
    DVS = torch.as_tensor(k["DVS"], dtype=DTYPE)
    mask = (DVS >= 0).to(dtype=DTYPE)

    # Growth rate leaves
    # weight of new leaves
    r.GRLV = mask * k.ADMI * k.FL

    # death of leaves due to water/oxygen stress
    r.DSLV1 = mask * s.WLV * (1.0 - k.RFTRA) * p.PERDL

    # death due to self shading cause by high LAI
    DVS = self.kiosk["DVS"]
    LAICR = 3.2 / p.KDIFTB(DVS)
    r.DSLV2 = mask * s.WLV * torch.clamp(0.03 * (s.LAI - LAICR) / LAICR, 0.0, 0.03)

    # Death of leaves due to frost damage as determined by
    # Reduction Factor Frost "RF_FROST"
    if "RF_FROST" in self.kiosk:
        r.DSLV3 = mask * s.WLV * k.RF_FROST
    else:
        r.DSLV3 = torch.zeros_like(s.WLV, dtype=DTYPE)

    # leaf death equals maximum of water stress, shading and frost
    r.DSLV = torch.maximum(torch.maximum(r.DSLV1, r.DSLV2), r.DSLV3)

    # Determine how much leaf biomass classes have to die in states.LV,
    # given the a life span > SPAN, these classes will be accumulated
    # in DALV.
    # Note that the actual leaf death is imposed on the array LV during the
    # state integration step.
    tSPAN = _broadcast_to(p.SPAN, s.LVAGE.shape)  # Broadcast to same shape
    # Using a sigmoid here instead of a conditional statement on the value of
    # SPAN because the latter would not allow for the gradient to be tracked.
    sharpness = torch.tensor(1000.0, dtype=DTYPE)  # FIXME
    weight = torch.sigmoid((s.LVAGE - tSPAN) * sharpness)
    r.DALV = torch.sum(weight * s.LV, dim=-1)

    # Total death rate leaves
    r.DRLV = torch.maximum(r.DSLV, r.DALV)

    # physiologic ageing of leaves per time step
    FYSAGE = (drv.TEMP - p.TBASE) / (35.0 - p.TBASE)
    r.FYSAGE = mask * torch.clamp(FYSAGE, 0.0)

    # specific leaf area of leaves per time step
    r.SLAT = mask * torch.tensor(p.SLATB(DVS), dtype=DTYPE)

    # leaf area not to exceed exponential growth curve
    is_lai_exp = s.LAIEXP < 6.0
    DTEFF = torch.clamp(drv.TEMP - p.TBASE, 0.0)
    # NOTE: conditional statements do not allow for the gradient to be
    # tracked through the condition. Thus, the gradient with respect to
    # parameters that contribute to `is_lai_exp` (e.g. RGRLAI and TBASE)
    # are expected to be incorrect.
    r.GLAIEX = torch.where(is_lai_exp, s.LAIEXP * p.RGRLAI * DTEFF, r.GLAIEX)
    # source-limited increase in leaf area
    r.GLASOL = torch.where(is_lai_exp, r.GRLV * r.SLAT, r.GLASOL)
    # sink-limited increase in leaf area
    GLA = torch.minimum(r.GLAIEX, r.GLASOL)
    # adjustment of specific leaf area of youngest leaf class
    r.SLAT = torch.where(is_lai_exp & (r.GRLV > 0.0), GLA / r.GRLV, r.SLAT)

integrate

integrate(day: date, delt=1.0) -> None

Integrate the leaf dynamics state variables.

Parameters:

  • day (date) –

    The current date of the simulation.

  • delt (float, default: 1.0 ) –

    The time step for integration. Defaults to 1.0.

Source code in src/diffwofost/physical_models/crop/leaf_dynamics.py
@prepare_states
def integrate(self, day: datetime.date, delt=1.0) -> None:
    """Integrate the leaf dynamics state variables.

    Args:
        day (datetime.date, optional): The current date of the simulation.
        delt (float, optional): The time step for integration. Defaults to 1.0.
    """
    # TODO check if DVS < 0 and skip integration needed
    rates = self.rates
    states = self.states

    # --------- leave death ---------
    tLV = states.LV.clone()
    tSLA = states.SLA.clone()
    tLVAGE = states.LVAGE.clone()
    tDRLV = _broadcast_to(rates.DRLV, tLV.shape)

    # Leaf death is imposed on leaves from the oldest ones.
    # Calculate the cumulative sum of weights after leaf death, and
    # find out which leaf classes are dead (negative weights)
    weight_cumsum = tLV.cumsum(dim=-1) - tDRLV
    is_alive = weight_cumsum >= 0

    # Adjust value of oldest leaf class, i.e. the first non-zero
    # weight along the time axis (the last dimension).
    # Cast argument to int because torch.argmax requires it to be numeric
    idx_oldest = torch.argmax(is_alive.type(torch.int), dim=-1, keepdim=True)
    new_biomass = torch.take_along_dim(weight_cumsum, indices=idx_oldest, dim=-1)
    tLV = torch.scatter(tLV, dim=-1, index=idx_oldest, src=new_biomass)

    # Zero out all dead leaf classes
    # NOTE: conditional statements do not allow for the gradient to be
    # tracked through the condition. Thus, the gradient with respect to
    # parameters that contribute to `is_alive` are expected to be incorrect.
    tLV = torch.where(is_alive, tLV, 0.0)

    # Integration of physiological age
    tLVAGE = tLVAGE + rates.FYSAGE
    tLVAGE = torch.where(is_alive, tLVAGE, 0.0)
    tSLA = torch.where(is_alive, tSLA, 0.0)

    # --------- leave growth ---------
    idx = int((day - self.START_DATE).days / delt)
    tLV[..., idx] = rates.GRLV
    tSLA[..., idx] = rates.SLAT
    tLVAGE[..., idx] = 0.0

    # calculation of new leaf area
    states.LASUM = torch.sum(tLV * tSLA, dim=-1)
    states.LAI = self._calc_LAI()
    states.LAIMAX = torch.maximum(states.LAI, states.LAIMAX)

    # exponential growth curve
    states.LAIEXP = states.LAIEXP + rates.GLAIEX

    # Update leaf biomass states
    states.WLV = torch.sum(tLV, dim=-1)
    states.DWLV = states.DWLV + rates.DRLV
    states.TWLV = states.WLV + states.DWLV

    # Store final leaf biomass deques
    self.states.LV = tLV
    self.states.SLA = tSLA
    self.states.LVAGE = tLVAGE

diffwofost.physical_models.crop.root_dynamics.WOFOST_Root_Dynamics

Bases: SimulationObject

Root biomass dynamics and rooting depth.

Root growth and root biomass dynamics in WOFOST are separate processes, with the only exception that root growth stops when no more biomass is sent to the root system.

Root biomass increase results from the assimilates partitioned to the root system. Root death is defined as the current root biomass multiplied by a relative death rate (RDRRTB). The latter as a function of the development stage (DVS).

Increase in root depth is a simple linear expansion over time until the maximum rooting depth (RDM) is reached.

Simulation parameters

Name Description Type Unit
RDI Initial rooting depth SCr cm
RRI Daily increase in rooting depth SCr cm day⁻¹
RDMCR Maximum rooting depth of the crop SCR cm
RDMSOL Maximum rooting depth of the soil SSo cm
TDWI Initial total crop dry weight SCr kg ha⁻¹
IAIRDU Presence of air ducts in the root (1) or not (0) SCr -
RDRRTB Relative death rate of roots as a function of development stage TCr -

State variables

Name Description Pbl Unit
RD Current rooting depth Y cm
RDM Maximum attainable rooting depth at the minimum of the soil and crop maximum rooting depth N cm
WRT Weight of living roots Y kg ha⁻¹
DWRT Weight of dead roots N kg ha⁻¹
TWRT Total weight of roots Y kg ha⁻¹

Rate variables

Name Description Pbl Unit
RR Growth rate root depth N cm
GRRT Growth rate root biomass N kg ha⁻¹ d⁻¹
DRRT Death rate root biomass N kg ha⁻¹ d⁻¹
GWRT Net change in root biomass N kg ha⁻¹ d⁻¹

Signals send or handled

None

External dependencies:

Name Description Provided by Unit
DVS Crop development stage DVS_Phenology -
DMI Total dry matter increase CropSimulation kg ha⁻¹ d⁻¹
FR Fraction biomass to roots DVS_Partitioning -

Outputs:

Name Description Provided by Unit
RD Current rooting depth Y cm
TWRT Total weight of roots Y kg ha⁻¹

IMPORTANT NOTICE

Currently root development is linear and depends only on the fraction of assimilates send to the roots (FR) and not on the amount of assimilates itself. This means that roots also grow through the winter when there is no assimilation due to low temperatures. There has been a discussion to change this behaviour and make root growth dependent on the assimilates send to the roots: so root growth stops when there are no assimilates available for growth.

Finally, we decided not to change the root model and keep the original WOFOST approach because of the following reasons: - A dry top layer in the soil could create a large drought stress that reduces the assimilates to zero. In this situation the roots would not grow if dependent on the assimilates, while water is available in the zone just below the root zone. Therefore a dependency on the amount of assimilates could create model instability in dry conditions (e.g. Southern-Mediterranean, etc.). - Other solutions to alleviate the problem above were explored: only put this limitation after a certain development stage, putting a dependency on soil moisture levels in the unrooted soil compartment. All these solutions were found to introduce arbitrary parameters that have no clear explanation. Therefore all proposed solutions were discarded.

We conclude that our current knowledge on root development is insufficient to propose a better and more biophysical approach to root development in WOFOST.

Methods:

  • initialize

    Initialize the model.

  • calc_rates

    Calculate the rates of change of the state variables.

  • integrate

    Integrate the state variables using the rates of change.

initialize

initialize(day: date, kiosk: VariableKiosk, parvalues: ParameterProvider) -> None

Initialize the model.

Parameters:

  • day (date) –

    The starting date of the simulation.

  • kiosk (VariableKiosk) –

    A container for registering and publishing (internal and external) state variables. See PCSE documentation for details.

  • parvalues (ParameterProvider) –

    A dictionary-like container holding all parameter sets (crop, soil, site) as key/value. The values are arrays or scalars. See PCSE documentation for details.

Source code in src/diffwofost/physical_models/crop/root_dynamics.py
def initialize(
    self, day: datetime.date, kiosk: VariableKiosk, parvalues: ParameterProvider
) -> None:
    """Initialize the model.

    Args:
        day (datetime.date): The starting date of the simulation.
        kiosk (VariableKiosk): A container for registering and publishing
            (internal and external) state variables. See PCSE documentation for
            details.
        parvalues (ParameterProvider): A dictionary-like container holding
            all parameter sets (crop, soil, site) as key/value. The values are
            arrays or scalars. See PCSE documentation for details.
    """
    self.params = self.Parameters(parvalues)
    self.rates = self.RateVariables(kiosk, publish=["DRRT", "GRRT"])
    self.kiosk = kiosk

    # INITIAL STATES
    params = self.params

    # Initial root depth states
    rdmax = torch.max(params.RDI, torch.min(params.RDMCR, params.RDMSOL))
    RDM = rdmax
    RD = params.RDI

    # initial root biomass states
    WRT = params.TDWI * self.kiosk.FR
    DWRT = torch.tensor(0.0, dtype=DTYPE)
    TWRT = WRT + DWRT

    self.states = self.StateVariables(
        kiosk, publish=["RD", "WRT", "TWRT"], RD=RD, RDM=RDM, WRT=WRT, DWRT=DWRT, TWRT=TWRT
    )

calc_rates

calc_rates(day: date = None, drv: WeatherDataContainer = None) -> None

Calculate the rates of change of the state variables.

Parameters:

  • day (date, default: None ) –

    The current date of the simulation.

  • drv (WeatherDataContainer, default: None ) –

    A dictionary-like container holding weather data elements as key/value. The values are arrays or scalars. See PCSE documentation for details.

Source code in src/diffwofost/physical_models/crop/root_dynamics.py
@prepare_rates
def calc_rates(self, day: datetime.date = None, drv: WeatherDataContainer = None) -> None:
    """Calculate the rates of change of the state variables.

    Args:
        day (datetime.date, optional): The current date of the simulation.
        drv (WeatherDataContainer, optional): A dictionary-like container holding
            weather data elements as key/value. The values are
            arrays or scalars. See PCSE documentation for details.
    """
    p = self.params
    r = self.rates
    s = self.states
    k = self.kiosk

    # If DVS < 0, the crop has not yet emerged, so we zerofy the rates using mask
    # Make a mask (0 if DVS < 0, 1 if DVS >= 0)
    DVS = torch.as_tensor(k["DVS"], dtype=DTYPE)
    mask = (DVS >= 0).to(dtype=DTYPE)

    # Increase in root biomass
    r.GRRT = mask * k.FR * k.DMI
    r.DRRT = mask * s.WRT * p.RDRRTB(k.DVS)
    r.GWRT = r.GRRT - r.DRRT

    # Increase in root depth
    r.RR = mask * torch.min((s.RDM - s.RD), p.RRI)

    # Do not let the roots growth if partioning to the roots
    # (variable FR) is zero.
    FR = torch.as_tensor(k["FR"], dtype=DTYPE)
    mask = (FR > 0.0).to(dtype=DTYPE)
    r.RR = r.RR * mask

integrate

integrate(day: date = None, delt=1.0) -> None

Integrate the state variables using the rates of change.

Parameters:

  • day (date, default: None ) –

    The current date of the simulation.

  • delt (float, default: 1.0 ) –

    The time step for integration. Defaults to 1.0.

Source code in src/diffwofost/physical_models/crop/root_dynamics.py
@prepare_states
def integrate(self, day: datetime.date = None, delt=1.0) -> None:
    """Integrate the state variables using the rates of change.

    Args:
        day (datetime.date, optional): The current date of the simulation.
        delt (float, optional): The time step for integration. Defaults to 1.0.
    """
    rates = self.rates
    states = self.states

    # Dry weight of living roots
    states.WRT = states.WRT + rates.GWRT

    # Dry weight of dead roots
    states.DWRT = states.DWRT + rates.DRRT

    # Total weight dry + living roots
    states.TWRT = states.WRT + states.DWRT

    # New root depth
    states.RD = states.RD + rates.RR

Utility (under development)

diffwofost.physical_models.utils.EngineTestHelper

EngineTestHelper(parameterprovider, weatherdataprovider, agromanagement, test_config, external_states=None)

Bases: Engine

An engine which is purely for running the YAML unit tests.

Source code in src/diffwofost/physical_models/utils.py
def __init__(
    self,
    parameterprovider,
    weatherdataprovider,
    agromanagement,
    test_config,
    external_states=None,
):
    BaseEngine.__init__(self)

    # Load the model configuration
    self.mconf = ConfigurationLoader(test_config)
    self.parameterprovider = parameterprovider

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

    # Placeholder for variables to be saved during a model run
    self._saved_output = list()
    self._saved_summary_output = list()
    self._saved_terminal_output = dict()

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

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

    # Timer: starting day, final day and model output
    self.timer = Timer(self.kiosk, start_date, end_date, self.mconf)
    self.day, delt = self.timer()
    # Update external states in the kiosk
    self.kiosk(self.day)

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

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

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

    # Calculate initial rates
    self.calc_rates(self.day, self.drv)