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}}\):
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 |
required |
tmax |
torch.Tensor |
Maximum daily air temperature [°C], shape |
required |
wind |
torch.Tensor |
Average wind speed [m s⁻¹], shape |
required |
vap |
torch.Tensor |
Vapour pressure [kPa], shape |
required |
avrad |
torch.Tensor |
Daily total irradiation [J m⁻² d⁻¹], shape |
required |
atmtr |
torch.Tensor |
Atmospheric transmission fraction [-], shape |
required |
frac_int |
torch.Tensor |
Fractional light interception [-], shape |
required |
co2 |
torch.Tensor | float |
Atmospheric CO₂ concentration [ppm], a scalar or
shape broadcastable to |
370.0 |
altitude |
torch.Tensor | float |
Site altitude [m a.s.l.], scalar or broadcastable
to |
0.0 |
cfet |
torch.Tensor | float |
Crop-specific transpiration correction factor [-],
scalar or broadcastable to |
1.0 |
fpenmtb |
torch.Tensor | None |
Penman ET0 CO₂-correction table |
None |
Returns:
| Type | Description |
|---|---|
Dict of ``[B]`` tensors |
|
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,
}