Skip to content

evapotranspiration

PENMAN potential evaporation and transpiration with CO₂ correction.

Computes Penman reference fluxes for open-water, bare-soil, and a closed-canopy reference, then partitions them into potential canopy transpiration and potential soil evaporation according to the fraction of incoming light intercepted by the crop.

Reference

Penman (1948), with the van Kraalingen modifications.

Outputs

  • E0 — potential evaporation from open water [mm d⁻¹].
  • ES0 — potential evaporation from bare soil [mm d⁻¹].
  • ETC — potential transpiration of a closed canopy, CO₂-corrected [mm d⁻¹].

Equations

Canopy transpiration and soil evaporation are split by the fractional light interception \(F_{\text{INT}}\):

\[ P_t = \text{CFET} \cdot \text{ETC} \cdot F_{\text{INT}} \]
\[ P_s = \text{ES}_0 \cdot (1 - F_{\text{INT}}) \]

where CFET is a crop-specific transpiration correction factor.

PotentialEvapoTranspiration (Module)

PENMAN-based potential evapotranspiration calculation.

Implements the full PENMAN formula and outputs reference ET (E0, ES0, ETC), which is then split into potential transpiration and soil evaporation based on light interception.

The site/crop drivers (altitude, cfet, co2, fpenmtb) are supplied per call through forward rather than at construction, so they flow directly from the SiteParameters dataclass and can be made batch-varying or learnable.

Source code in torchcrop/processes/evapotranspiration.py
class PotentialEvapoTranspiration(nn.Module):
    """PENMAN-based potential evapotranspiration calculation.

    Implements the full PENMAN formula and outputs reference ET
    (``E0``, ``ES0``, ``ETC``), which is then split into potential
    transpiration and soil evaporation based on light interception.

    The site/crop drivers (``altitude``, ``cfet``, ``co2``, ``fpenmtb``)
    are supplied per call through `forward` rather than at construction,
    so they flow directly from the `SiteParameters` dataclass and can be
    made batch-varying or learnable.
    """

    # Default Penman ET0 CO₂-correction table (concentration [ppm] →
    # factor), used when ``fpenmtb`` is not supplied. Mirrors the
    # ``SiteParameters.fpenmtb`` default (SIMPLACE ``cFPENMTB``).
    _DEFAULT_FPENMTB = (
        (40.0, 1.05),
        (360.0, 1.00),
        (720.0, 0.95),
        (1000.0, 0.92),
        (2000.0, 0.92),
    )

    def forward(
        self,
        tmin: torch.Tensor,
        tmax: torch.Tensor,
        wind: torch.Tensor,
        vap: torch.Tensor,
        avrad: torch.Tensor,
        atmtr: torch.Tensor,
        frac_int: torch.Tensor,
        co2: torch.Tensor | float = 370.0,
        altitude: torch.Tensor | float = 0.0,
        cfet: torch.Tensor | float = 1.0,
        fpenmtb: torch.Tensor | None = None,
    ) -> dict[str, torch.Tensor]:
        """Compute PENMAN potential ET and split into canopy/soil fluxes.

        Args:
            tmin: Minimum daily air temperature [°C], shape ``[B]``.
            tmax: Maximum daily air temperature [°C], shape ``[B]``.
            wind: Average wind speed [m s⁻¹], shape ``[B]``.
            vap: Vapour pressure [kPa], shape ``[B]``.
            avrad: Daily total irradiation [J m⁻² d⁻¹], shape ``[B]``.
            atmtr: Atmospheric transmission fraction [-], shape ``[B]``.
            frac_int: Fractional light interception [-], shape ``[B]``.
            co2: Atmospheric CO₂ concentration [ppm], a scalar or
                shape broadcastable to ``[B]`` (default ``370``).
                Supplied by ``SiteParameters.co2`` in the model.
            altitude: Site altitude [m a.s.l.], scalar or broadcastable
                to ``[B]`` (default ``0``). Supplied by
                ``SiteParameters.altitude``; enters the barometric
                pressure used by the psychrometric constant.
            cfet: Crop-specific transpiration correction factor [-],
                scalar or broadcastable to ``[B]`` (default ``1.0``).
                Supplied by ``CropParameters.cfet``.
            fpenmtb: Penman ET0 CO₂-correction table ``[N, 2]``
                (concentration [ppm] → factor). Supplied by
                ``SiteParameters.fpenmtb`` (SIMPLACE ``cFPENMTB``);
                falls back to the standard C3 table when ``None``.

        Returns:
            Dict of ``[B]`` tensors:

            * ``e0`` [mm d⁻¹] — Potential evaporation from open water.
            * ``es0`` [mm d⁻¹] — Potential evaporation from bare soil.
            * ``etc`` [mm d⁻¹] — Potential transpiration
              (CO₂-corrected).
            * ``ptran`` [mm d⁻¹] — Potential canopy transpiration
              (``= CFET · ETC · frac_int``).
            * ``pevap`` [mm d⁻¹] — Potential soil evaporation
              (``= ES0 · (1 − frac_int)``).
        """
        # Constants from the PENMAN formula.
        A = 0.20
        B = 0.56
        REFCFW = 0.05  # Albedo for water
        REFCFS = 0.15  # Albedo for soil
        REFCFC = 0.25  # Albedo for canopy
        LHVAP = 2.45e6  # Latent heat of evaporation [J kg-1]
        STBC = 4.9e-3  # Stefan-Boltzmann constant [J m-2 d-1 K-4]
        PSYCON = 0.000662  # Psychrometric constant [K-1]

        # Convert vapour pressure from kPa to mbar (1 kPa = 10 mbar)
        vap_mbar = vap * 10.0

        # Average daily temperature
        tmpa = (tmin + tmax) / 2.0

        # Temperature difference
        tdif = tmax - tmin

        # Wind function coefficient (depends on temperature range)
        bu = 0.54 + 0.35 * torch.clamp((tdif - 12.0) / 4.0, min=0.0, max=1.0)

        # Barometric pressure [mbar]
        altitude_t = torch.as_tensor(altitude, dtype=tmpa.dtype, device=tmpa.device)
        pbar = 1013.0 * torch.exp(-0.034 * altitude_t / (tmpa + 273.0))

        # Psychrometric constant [mbar K-1]
        gamma = PSYCON * pbar

        # Saturated vapour pressure [mbar] per Goudriaan (1977)
        svap = 6.11 * torch.exp(17.4 * tmpa / (tmpa + 239.0))

        # Measured vapour pressure should not exceed saturated vapour pressure
        vap_clamped = torch.clamp(vap_mbar, max=svap)

        # Slope of saturation vapour pressure curve [mbar K-1]
        delta = 239.0 * 17.4 * svap / torch.clamp((tmpa + 239.0) ** 2, min=1e-6)

        # Relative sunshine duration (from Angstrom formula)
        relssd = torch.clamp((atmtr - A) / B, min=0.0, max=1.0)

        # Net outgoing long-wave radiation [J m-2 d-1]
        rb = (
            STBC
            * (tmpa + 273.0) ** 4
            * (0.56 - 0.079 * torch.sqrt(torch.clamp(vap_clamped, min=0.0)))
            * (0.1 + 0.9 * relssd)
        )

        # Net absorbed radiation [J m-2 d-1]
        rnw = avrad * (1.0 - REFCFW) - rb
        rns = avrad * (1.0 - REFCFS) - rb
        rnc = avrad * (1.0 - REFCFC) - rb

        # Evaporative demand of atmosphere [mm d-1]
        ea = 0.26 * (svap - vap_clamped) * (0.5 + bu * wind)
        eac = 0.26 * (svap - vap_clamped) * (1.0 + bu * wind)

        # PENMAN formula [mm d-1]
        e0 = (delta * (rnw / LHVAP) + gamma * ea) / torch.clamp(delta + gamma, min=1e-6)
        es0 = (delta * (rns / LHVAP) + gamma * ea) / torch.clamp(
            delta + gamma, min=1e-6
        )
        et0 = (delta * (rnc / LHVAP) + gamma * eac) / torch.clamp(
            delta + gamma, min=1e-6
        )

        # Ensure non-negative
        e0 = torch.clamp(e0, min=0.0)
        es0 = torch.clamp(es0, min=0.0)
        et0 = torch.clamp(et0, min=0.0)

        # CO2 correction for ET0 (Penman ET0 × FPENMTB(CO2);
        # SIMPLACE PotentialEvapoTranspiration.java:194).
        if fpenmtb is None:
            fpenmtb = torch.tensor(
                self._DEFAULT_FPENMTB, dtype=e0.dtype, device=e0.device
            )
        co2_t = torch.as_tensor(co2, dtype=e0.dtype, device=e0.device)
        co2_b = co2_t.expand_as(e0) if co2_t.dim() == 0 else co2_t
        co2_factor = interpolate(fpenmtb, co2_b)
        etc = et0 * co2_factor

        # Potential transpiration and soil evaporation split by light interception
        ptran = torch.clamp(cfet * etc * frac_int, min=0.0001)
        pevap = es0 * (1.0 - frac_int)

        return {
            "e0": e0,
            "es0": es0,
            "etc": etc,
            "ptran": ptran,
            "pevap": pevap,
        }

forward(self, tmin, tmax, wind, vap, avrad, atmtr, frac_int, co2=370.0, altitude=0.0, cfet=1.0, fpenmtb=None)

Compute PENMAN potential ET and split into canopy/soil fluxes.

Parameters:

Name Type Description Default
tmin torch.Tensor

Minimum daily air temperature [°C], shape [B].

required
tmax torch.Tensor

Maximum daily air temperature [°C], shape [B].

required
wind torch.Tensor

Average wind speed [m s⁻¹], shape [B].

required
vap torch.Tensor

Vapour pressure [kPa], shape [B].

required
avrad torch.Tensor

Daily total irradiation [J m⁻² d⁻¹], shape [B].

required
atmtr torch.Tensor

Atmospheric transmission fraction [-], shape [B].

required
frac_int torch.Tensor

Fractional light interception [-], shape [B].

required
co2 torch.Tensor | float

Atmospheric CO₂ concentration [ppm], a scalar or shape broadcastable to [B] (default 370). Supplied by SiteParameters.co2 in the model.

370.0
altitude torch.Tensor | float

Site altitude [m a.s.l.], scalar or broadcastable to [B] (default 0). Supplied by SiteParameters.altitude; enters the barometric pressure used by the psychrometric constant.

0.0
cfet torch.Tensor | float

Crop-specific transpiration correction factor [-], scalar or broadcastable to [B] (default 1.0). Supplied by CropParameters.cfet.

1.0
fpenmtb torch.Tensor | None

Penman ET0 CO₂-correction table [N, 2] (concentration [ppm] → factor). Supplied by SiteParameters.fpenmtb (SIMPLACE cFPENMTB); falls back to the standard C3 table when None.

None

Returns:

Type Description
Dict of ``[B]`` tensors
  • e0 [mm d⁻¹] — Potential evaporation from open water.
  • es0 [mm d⁻¹] — Potential evaporation from bare soil.
  • etc [mm d⁻¹] — Potential transpiration (CO₂-corrected).
  • ptran [mm d⁻¹] — Potential canopy transpiration (= CFET · ETC · frac_int).
  • pevap [mm d⁻¹] — Potential soil evaporation (= ES0 · (1 − frac_int)).
Source code in torchcrop/processes/evapotranspiration.py
def forward(
    self,
    tmin: torch.Tensor,
    tmax: torch.Tensor,
    wind: torch.Tensor,
    vap: torch.Tensor,
    avrad: torch.Tensor,
    atmtr: torch.Tensor,
    frac_int: torch.Tensor,
    co2: torch.Tensor | float = 370.0,
    altitude: torch.Tensor | float = 0.0,
    cfet: torch.Tensor | float = 1.0,
    fpenmtb: torch.Tensor | None = None,
) -> dict[str, torch.Tensor]:
    """Compute PENMAN potential ET and split into canopy/soil fluxes.

    Args:
        tmin: Minimum daily air temperature [°C], shape ``[B]``.
        tmax: Maximum daily air temperature [°C], shape ``[B]``.
        wind: Average wind speed [m s⁻¹], shape ``[B]``.
        vap: Vapour pressure [kPa], shape ``[B]``.
        avrad: Daily total irradiation [J m⁻² d⁻¹], shape ``[B]``.
        atmtr: Atmospheric transmission fraction [-], shape ``[B]``.
        frac_int: Fractional light interception [-], shape ``[B]``.
        co2: Atmospheric CO₂ concentration [ppm], a scalar or
            shape broadcastable to ``[B]`` (default ``370``).
            Supplied by ``SiteParameters.co2`` in the model.
        altitude: Site altitude [m a.s.l.], scalar or broadcastable
            to ``[B]`` (default ``0``). Supplied by
            ``SiteParameters.altitude``; enters the barometric
            pressure used by the psychrometric constant.
        cfet: Crop-specific transpiration correction factor [-],
            scalar or broadcastable to ``[B]`` (default ``1.0``).
            Supplied by ``CropParameters.cfet``.
        fpenmtb: Penman ET0 CO₂-correction table ``[N, 2]``
            (concentration [ppm] → factor). Supplied by
            ``SiteParameters.fpenmtb`` (SIMPLACE ``cFPENMTB``);
            falls back to the standard C3 table when ``None``.

    Returns:
        Dict of ``[B]`` tensors:

        * ``e0`` [mm d⁻¹] — Potential evaporation from open water.
        * ``es0`` [mm d⁻¹] — Potential evaporation from bare soil.
        * ``etc`` [mm d⁻¹] — Potential transpiration
          (CO₂-corrected).
        * ``ptran`` [mm d⁻¹] — Potential canopy transpiration
          (``= CFET · ETC · frac_int``).
        * ``pevap`` [mm d⁻¹] — Potential soil evaporation
          (``= ES0 · (1 − frac_int)``).
    """
    # Constants from the PENMAN formula.
    A = 0.20
    B = 0.56
    REFCFW = 0.05  # Albedo for water
    REFCFS = 0.15  # Albedo for soil
    REFCFC = 0.25  # Albedo for canopy
    LHVAP = 2.45e6  # Latent heat of evaporation [J kg-1]
    STBC = 4.9e-3  # Stefan-Boltzmann constant [J m-2 d-1 K-4]
    PSYCON = 0.000662  # Psychrometric constant [K-1]

    # Convert vapour pressure from kPa to mbar (1 kPa = 10 mbar)
    vap_mbar = vap * 10.0

    # Average daily temperature
    tmpa = (tmin + tmax) / 2.0

    # Temperature difference
    tdif = tmax - tmin

    # Wind function coefficient (depends on temperature range)
    bu = 0.54 + 0.35 * torch.clamp((tdif - 12.0) / 4.0, min=0.0, max=1.0)

    # Barometric pressure [mbar]
    altitude_t = torch.as_tensor(altitude, dtype=tmpa.dtype, device=tmpa.device)
    pbar = 1013.0 * torch.exp(-0.034 * altitude_t / (tmpa + 273.0))

    # Psychrometric constant [mbar K-1]
    gamma = PSYCON * pbar

    # Saturated vapour pressure [mbar] per Goudriaan (1977)
    svap = 6.11 * torch.exp(17.4 * tmpa / (tmpa + 239.0))

    # Measured vapour pressure should not exceed saturated vapour pressure
    vap_clamped = torch.clamp(vap_mbar, max=svap)

    # Slope of saturation vapour pressure curve [mbar K-1]
    delta = 239.0 * 17.4 * svap / torch.clamp((tmpa + 239.0) ** 2, min=1e-6)

    # Relative sunshine duration (from Angstrom formula)
    relssd = torch.clamp((atmtr - A) / B, min=0.0, max=1.0)

    # Net outgoing long-wave radiation [J m-2 d-1]
    rb = (
        STBC
        * (tmpa + 273.0) ** 4
        * (0.56 - 0.079 * torch.sqrt(torch.clamp(vap_clamped, min=0.0)))
        * (0.1 + 0.9 * relssd)
    )

    # Net absorbed radiation [J m-2 d-1]
    rnw = avrad * (1.0 - REFCFW) - rb
    rns = avrad * (1.0 - REFCFS) - rb
    rnc = avrad * (1.0 - REFCFC) - rb

    # Evaporative demand of atmosphere [mm d-1]
    ea = 0.26 * (svap - vap_clamped) * (0.5 + bu * wind)
    eac = 0.26 * (svap - vap_clamped) * (1.0 + bu * wind)

    # PENMAN formula [mm d-1]
    e0 = (delta * (rnw / LHVAP) + gamma * ea) / torch.clamp(delta + gamma, min=1e-6)
    es0 = (delta * (rns / LHVAP) + gamma * ea) / torch.clamp(
        delta + gamma, min=1e-6
    )
    et0 = (delta * (rnc / LHVAP) + gamma * eac) / torch.clamp(
        delta + gamma, min=1e-6
    )

    # Ensure non-negative
    e0 = torch.clamp(e0, min=0.0)
    es0 = torch.clamp(es0, min=0.0)
    et0 = torch.clamp(et0, min=0.0)

    # CO2 correction for ET0 (Penman ET0 × FPENMTB(CO2);
    # SIMPLACE PotentialEvapoTranspiration.java:194).
    if fpenmtb is None:
        fpenmtb = torch.tensor(
            self._DEFAULT_FPENMTB, dtype=e0.dtype, device=e0.device
        )
    co2_t = torch.as_tensor(co2, dtype=e0.dtype, device=e0.device)
    co2_b = co2_t.expand_as(e0) if co2_t.dim() == 0 else co2_t
    co2_factor = interpolate(fpenmtb, co2_b)
    etc = et0 * co2_factor

    # Potential transpiration and soil evaporation split by light interception
    ptran = torch.clamp(cfet * etc * frac_int, min=0.0001)
    pevap = es0 * (1.0 - frac_int)

    return {
        "e0": e0,
        "es0": es0,
        "etc": etc,
        "ptran": ptran,
        "pevap": pevap,
    }