from dataclasses import dataclass, field, replace
import jax
import jax.numpy as jnp
from jax import Array
from jax.scipy.special import exp1
from ...abstracts import AbstractCoupledState
from ...utils import PhysicalConstants as cst
from ...utils import compute_esat
from ..abstracts import AbstractBiosphereModel, AbstractBiosphereState
[docs]
@dataclass
class AgsState(AbstractBiosphereState):
"""A-gs biosphere state."""
rs: Array = field(
metadata={
"label": r"$r_s$",
"unit": "s m^{-1}",
"description": "Surface resistance",
},
)
"""Surface resistance [s m-1]."""
wl: Array = field(
metadata={
"label": r"$w_l$",
"unit": "m",
"description": "Canopy water content",
},
)
"""Canopy water content [m]."""
cliq: Array = field(
metadata={
"label": r"$dw_l$",
"unit": "-",
"description": "Wet fraction of canopy",
},
)
"""Wet fraction of canopy [-]."""
wCO2: Array = field(
metadata={
"label": r"$w'CO_2'$",
"unit": "mol m^{-2} s^{-1}",
"description": "Total CO2 flux",
},
)
"""Total CO2 flux [mol m-2 s-1]."""
cveg: Array = field(
metadata={
"label": r"$c_{veg}$",
"unit": "-",
"description": "Vegetation fraction",
},
)
"""Vegetation fraction [-]."""
rsCO2: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$r_{s,CO2}$",
"unit": "s m^{-1}",
"description": "Stomatal resistance to CO2",
},
)
"""Stomatal resistance to CO2."""
gcco2: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$g_{c,CO2}$",
"unit": "s m^{-1}",
"description": "Conductance to CO2",
},
)
"""Conductance to CO2."""
ci: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$C_i$",
"unit": "ppm",
"description": "Intercellular CO2 concentration",
},
)
"""Intercellular CO2 concentration."""
co2abs: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$CO_{2,abs}$",
"unit": "kg m^{-3}",
"description": "CO2 assimilation rate (or concentration?)",
},
)
"""CO2 assimilation rate / concentration."""
wCO2A: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$A_n$",
"unit": "mol m^{-2} s^{-1}",
"description": "Net assimilation flux",
},
)
"""Net assimilation flux [mol m-2 s-1]."""
wCO2R: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$R_{soil}$",
"unit": "mol m^{-2} s^{-1}",
"description": "Respiration flux",
},
)
"""Respiration flux [mol m-2 s-1]."""
wltend: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$dw_l$",
"unit": "m",
"description": "Canopy water content tendency",
},
)
"""Canopy water content tendency [m]."""
[docs]
class AgsModel(AbstractBiosphereModel[AgsState]):
"""Ags land surface biosphere model with coupled photosynthesis and stomatal conductance.
Args:
c3c4: string indicating whether the model should use C3 or C4 photosynthesis. Default is "c3".
lai: leaf area index [m2 m-2]. Default is 2.0.
cveg: vegetation fraction [-]. Default is 0.85.
wmax: maximum water storage capacity of the canopy [m]. Default is 0.0002.
wwilt: soil moisture content at wilting point [m3 m-3]. Default is 0.171.
wfc: soil moisture content at field capacity [m3 m-3]. Default is 0.323.
w2: soil moisture content at the second layer [m3 m-3]. Default is 0.21.
"""
def __init__(
self,
c3c4: str = "c3",
lai: float = 2.0,
cveg: float = 0.85,
wmax: float = 0.0002,
wwilt: float = 0.171,
wfc: float = 0.323,
w2: float = 0.21,
):
self.lai = lai
self.cveg = cveg
self.wmax = wmax
self.wwilt = wwilt
self.wfc = wfc
self.w2 = w2
self.c_beta = 0.0
if c3c4 == "c3":
self.c3c4 = 0
elif c3c4 == "c4":
self.c3c4 = 1
else:
raise ValueError(f'''Invalid option "{c3c4}" for "c3c4".''')
self.co2comp298 = 68.5 if c3c4 == "c3" else 4.3
self.net_rad10CO2 = 1.5
self.gm298 = 7.0 if c3c4 == "c3" else 17.5
self.ammax298 = 2.2 if c3c4 == "c3" else 1.7
self.net_rad10gm = 2.0
self.temp1gm = 278.0 if c3c4 == "c3" else 286.0
self.temp2gm = 301.0 if c3c4 == "c3" else 309.0
self.net_rad10Am = 2.0
self.temp1Am = 281.0 if c3c4 == "c3" else 286.0
self.temp2Am = 311.0
self.f0 = 0.89 if c3c4 == "c3" else 0.85
self.ad = 0.07 if c3c4 == "c3" else 0.15
self.alpha0 = 0.017 if c3c4 == "c3" else 0.014
self.kx = 0.7
self.gmin = 0.25e-3
self.nuco2q = 1.6
self.cw = 0.0016
self.wmax = 0.55
self.wmin = 0.005
self.r10 = 0.23
self.e0 = 53.3e3
[docs]
def init_state(
self,
rs: float = 1.0e6,
wl: float = 0.0,
cliq: float = 0.0,
wCO2: float = 0.0,
) -> AgsState:
"""Initialize the Ags state.
Args:
rs: Surface resistance [s m-1]. Default is 1.0e6.
wl: Canopy water content [m]. Default is 0.0.
cliq: Wet fraction of canopy [-]. Default is 0.0.
wCO2: Total CO2 flux [mol m-2 s-1]. Default is 0.0.
Returns:
The initialized AgsState.
"""
return AgsState(
rs=jnp.array(rs),
wl=jnp.array(wl),
cliq=jnp.array(cliq),
wCO2=jnp.array(wCO2),
cveg=jnp.array(self.cveg), # this is a dirty move...
)
[docs]
def run(self, state: AbstractCoupledState) -> AgsState:
"""Compute stomatal resistance and CO2 fluxes."""
land = state.land
atmos = state.atmos
thetasurf = atmos.thetasurf
co2comp = self.compute_co2comp(thetasurf)
gm = self.compute_gm(thetasurf)
fmin = self.compute_fmin(gm)
ds = self.compute_ds(thetasurf, land.e)
d0 = self.compute_d0(fmin)
ci, co2abs = self.compute_internal_co2(ds, d0, fmin, atmos.co2, co2comp)
ammax = self.compute_max_gross_primary_production(thetasurf)
fstr = self.compute_soil_moisture_stress_factor(self.w2)
am = self.compute_gross_assimilation(ammax, gm, ci, co2comp)
rdark = self.compute_dark_respiration(am)
par = self.compute_absorbed_par(state.in_srad)
alphac = self.compute_light_use_efficiency(co2abs, co2comp)
gcco2 = self.compute_canopy_co2_conductance(
alphac,
par,
am,
rdark,
fstr,
co2abs,
co2comp,
ds,
d0,
fmin,
)
rs = self.compute_rs(gcco2)
rsCO2 = self.compute_surface_co2_resistance(gcco2)
an = self.compute_net_assimilation(co2abs, ci, atmos.ra, rsCO2)
fw = self.compute_soil_water_fraction(land.wg)
resp = self.compute_respiration(land.soil.temp_soil, fw)
wCO2A = self.scale_flux_to_mol(an)
wCO2R = self.scale_flux_to_mol(resp)
wCO2 = wCO2A + wCO2R
cliq = self.compute_cliq(land.wl)
return replace(
land.biosphere,
rs=rs,
cliq=cliq,
rsCO2=rsCO2,
gcco2=gcco2,
ci=ci,
co2abs=co2abs,
wCO2A=wCO2A,
wCO2R=wCO2R,
wCO2=wCO2,
)
[docs]
def compute_co2comp(self, thetasurf: Array) -> Array:
"""Compute the CO₂ compensation concentration ``co2comp``.
Notes:
The CO₂ compensation point :math:`\\Gamma` is the CO₂
concentration at which net photosynthesis is zero. It follows
a Q₁₀ temperature response:
.. math::
\\Gamma = \\Gamma_{298} \\cdot \\rho \\cdot
Q_{10}^{\\,0.1\\,(\\theta_s - 298)}
where :math:`\\Gamma_{298}` is the compensation point at 298 K,
:math:`\\rho` is the air density, :math:`Q_{10}` is the relative
increase per 10 K, and :math:`\\theta_s` is the surface potential
temperature.
"""
temp_diff = 0.1 * (thetasurf - 298.0)
exp_term = jnp.power(self.net_rad10CO2, temp_diff)
return self.co2comp298 * cst.rho * exp_term
[docs]
def compute_gm(self, thetasurf: Array) -> Array:
"""Compute the mesophyll conductance ``gm``.
Notes:
Mesophyll conductance :math:`g_m` controls the diffusion of
CO₂ from intercellular spaces to the sites of carboxylation.
It follows a temperature response with a Q₁₀ factor and high/
low temperature inhibition:
.. math::
g_m = \\frac{g_{m,298} \\cdot
Q_{10}^{\\,0.1\\,(\\theta_s - 298)}}
{\\bigl(1 + e^{0.3\\,(T_1 - \\theta_s)}\\bigr)
\\bigl(1 + e^{0.3\\,(\\theta_s - T_2)}\\bigr)}
where :math:`g_{m,298}` is the mesophyll conductance at 298 K,
:math:`\\theta_s` is the surface potential temperature, and
:math:`T_1, T_2` are temperature thresholds.
"""
temp_diff = 0.1 * (thetasurf - 298.0)
exp_term = jnp.power(self.net_rad10gm, temp_diff)
temp_factor1 = 1.0 + jnp.exp(0.3 * (self.temp1gm - thetasurf))
temp_factor2 = 1.0 + jnp.exp(0.3 * (thetasurf - self.temp2gm))
gm = self.gm298 * exp_term / (temp_factor1 * temp_factor2)
return gm / 1000.0
[docs]
def compute_fmin(self, gm: Array) -> Array:
"""Compute the minimum stomatal conductance factor ``fmin``.
Notes:
The minimum conductance factor :math:`f_{\\min}` is derived
from the quadratic relation between minimum stomatal
conductance :math:`g_{\\min}` and mesophyll conductance
:math:`g_m`:
.. math::
f_{\\min} = \\frac{-f_0 + \\sqrt{f_0^2 +
\\dfrac{4 g_{\\min} g_m}{\\nu}}}{2 g_m},
\\qquad
f_0 = \\frac{g_{\\min}}{\\nu} - \\frac{g_m}{9},
where :math:`\\nu = 1.6` is the ratio of diffusivity of water
vapour to CO₂, and :math:`g_{\\min}` is the minimum stomatal
conductance.
"""
fmin0 = self.gmin / self.nuco2q - 1.0 / 9.0 * gm
fmin_sq_term = jnp.power(fmin0, 2.0) + 4 * self.gmin / self.nuco2q * gm
fmin = -fmin0 + jnp.power(fmin_sq_term, 0.5) / (2.0 * gm)
return fmin
[docs]
def compute_ds(self, surf_temp: Array, e: Array) -> Array:
"""Compute the vapour pressure deficit ``ds``.
Notes:
The vapour pressure deficit at the surface is given by
.. math::
D_s = \\frac{e_{\\text{sat}}(T_s) - e}{1000},
where :math:`e_{\\text{sat}}` is the saturation vapour pressure
at the surface temperature :math:`T_s` and :math:`e` is the
actual vapour pressure. The result is in kPa.
"""
ds = (compute_esat(surf_temp) - e) / 1000.0 # kPa
return ds
[docs]
def compute_d0(self, fmin: Array) -> Array:
"""Compute the reference vapour pressure deficit ``d0``.
Notes:
The reference VPD is derived from the minimum conductance
factor:
.. math::
D_0 = \\frac{f_0 - f_{\\min}}{a_d},
where :math:`f_0` is a shape parameter and :math:`a_d` is
the sensitivity of the VPD response.
"""
d0 = (self.f0 - fmin) / self.ad
return d0
[docs]
def compute_internal_co2(
self,
ds: Array,
d0: Array,
fmin: Array,
co2: Array,
co2comp: Array,
) -> tuple[Array, Array]:
"""Compute the intercellular CO₂ concentration ``ci``.
Notes:
The intercellular CO₂ concentration :math:`C_i` is computed
from the CO₂ absorption concentration and the compensation
point:
.. math::
c_f &= f_0 \\left(1 - \\frac{D_s}{D_0}\\right) +
f_{\\min} \\frac{D_s}{D_0} \\\\
\\text{CO}_{2,\\text{abs}} &=
\\text{CO}_2 \\frac{M_{\\text{CO}_2}}
{M_{\\text{air}}} \\rho \\\\
C_i &= c_f (\\text{CO}_{2,\\text{abs}} - \\Gamma) + \\Gamma
where :math:`c_f` is the fractional reduction factor,
:math:`\\text{CO}_2` is the atmospheric CO₂ concentration,
:math:`M_{\\text{CO}_2}` and :math:`M_{\\text{air}}` are the
molar masses of CO₂ and dry air, and :math:`\\rho` is the
air density.
Returns:
A tuple ``(ci, co2abs)``.
"""
cfrac = self.f0 * (1.0 - (ds / d0)) + fmin * (ds / d0)
co2abs = co2 * (cst.mco2 / cst.mair) * cst.rho
ci = cfrac * (co2abs - co2comp) + co2comp
return ci, co2abs
[docs]
def compute_max_gross_primary_production(self, thetasurf: Array) -> Array:
"""Compute the maximal gross primary production ``ammax``.
Notes:
The maximum gross primary production :math:`A_{m,\\max}`
follows a temperature response identical in structure to
mesophyll conductance:
.. math::
A_{m,\\max} = \\frac{A_{m,298} \\cdot
Q_{10}^{\\,0.1\\,(\\theta_s - 298)}}
{\\bigl(1 + e^{0.3\\,(T_1 - \\theta_s)}\\bigr)
\\bigl(1 + e^{0.3\\,(\\theta_s - T_2)}\\bigr)}
where :math:`A_{m,298}` is the value at 298 K and
:math:`\\theta_s` is the surface potential temperature.
"""
temp_diff = 0.1 * (thetasurf - 298.0)
exp_term = jnp.power(self.net_rad10Am, temp_diff)
temp_factor1 = 1.0 + jnp.exp(0.3 * (self.temp1Am - thetasurf))
temp_factor2 = 1.0 + jnp.exp(0.3 * (thetasurf - self.temp2Am))
ammax = self.ammax298 * exp_term / (temp_factor1 * temp_factor2)
return ammax
[docs]
def compute_soil_moisture_stress_factor(self, w2: float) -> Array:
"""Compute the soil moisture stress factor ``fstr``.
Notes:
The soil moisture stress factor :math:`\\beta_w` is computed
from the relative soil moisture:
.. math::
\\beta_w = \\frac{w_2 - w_{\\text{wilt}}}
{w_{\\text{fc}} - w_{\\text{wilt}}}
clipped to :math:`[\\varepsilon, 1]`. The stress factor is then
adjusted using a piecewise function depending on the parameter
:math:`c_{\\beta}`:
.. math::
f_{\\text{str}} = \\begin{cases}
\\beta_w & c_\\beta = 0 \\\\[4pt]
\\dfrac{1 - e^{-p \\beta_w}}{1 - e^{-p}} & c_\\beta > 0
\\end{cases}
where the shape parameter :math:`p` increases with
:math:`c_{\\beta}`.
"""
soil_moisture_ratio = (w2 - self.wwilt) / (self.wfc - self.wwilt)
betaw = jnp.clip(soil_moisture_ratio, 1e-3, 1.0)
def case_zero(_):
return betaw
def case_low(_):
p = 6.4 * self.c_beta
numerator = 1.0 - jnp.exp(-p * betaw)
denominator = 1.0 - jnp.exp(-p)
return numerator / denominator
def case_medium(_):
p = 7.6 * self.c_beta - 0.3
numerator = 1.0 - jnp.exp(-p * betaw)
denominator = 1.0 - jnp.exp(-p)
return numerator / denominator
def case_high(_):
p = 2.0 ** (3.66 * self.c_beta + 0.34) - 1.0
numerator = 1.0 - jnp.exp(-p * betaw)
denominator = 1.0 - jnp.exp(-p)
return numerator / denominator
branch_index = jnp.where(
self.c_beta == 0,
0,
jnp.where(self.c_beta < 0.25, 1, jnp.where(self.c_beta < 0.50, 2, 3)),
)
result = jax.lax.switch(
branch_index, [case_zero, case_low, case_medium, case_high], None
)
return result
[docs]
def compute_gross_assimilation(
self,
ammax: Array,
gm: Array,
ci: Array,
co2comp: Array,
) -> Array:
"""Compute the gross assimilation rate ``am``.
Notes:
The gross assimilation rate follows an exponential approach
to saturation (Collatz et al., 1991):
.. math::
A_m = A_{m,\\max} \\left(1 - \\exp\\!\\left(
-\\frac{g_m\\,(C_i - \\Gamma)}{A_{m,\\max}}
\\right)\\right)
where :math:`A_{m,\\max}` is the maximal gross primary
production, :math:`g_m` is the mesophyll conductance,
:math:`C_i` is the intercellular CO₂ concentration, and
:math:`\\Gamma` is the CO₂ compensation concentration.
"""
assimilation_factor = -(gm * (ci - co2comp) / ammax)
am = ammax * (1.0 - jnp.exp(assimilation_factor))
return am
[docs]
def compute_dark_respiration(self, am: Array) -> Array:
"""Compute the dark respiration rate ``rdark``.
Notes:
Dark respiration is proportional to the gross assimilation
rate:
.. math::
R_{\\text{dark}} = \\frac{A_m}{9}
where :math:`A_m` is the gross assimilation rate.
"""
rdark = (1.0 / 9.0) * am
return rdark
[docs]
def compute_absorbed_par(self, in_srad: Array) -> Array:
"""Compute the absorbed photosynthetically active radiation ``par``.
Notes:
The absorbed PAR is estimated as 50% of the incoming solar
radiation, scaled by the vegetation fraction, with a lower
bound:
.. math::
\\text{PAR} = \\max\\!\\left(0.5 \\cdot R_s \\cdot
c_{\\text{veg}},\\, 0.1\\right)
where :math:`R_s` is the incoming solar radiation and
:math:`c_{\\text{veg}}` is the vegetation fraction.
"""
par = 0.5 * jnp.maximum(1e-1, in_srad * self.cveg)
return par
[docs]
def compute_canopy_co2_conductance(
self,
alphac: Array,
par: Array,
am: Array,
rdark: Array,
fstr: Array,
co2abs: Array,
co2comp: Array,
ds: Array,
d0: Array,
fmin: Array,
) -> Array:
"""Compute the canopy CO₂ conductance ``gcco2``.
Notes:
The canopy CO₂ conductance :math:`g_{c,\\text{CO}_2}` is
computed by scaling leaf-level photosynthesis to the canopy
using the big-leaf approach with the exponential integral
:math:`E_1` (Sellers et al., 1996):
.. math::
y &= \\frac{\\alpha_c k_x \\text{PAR}}{A_m + R_{\\text{dark}}}
\\\\
A_n &= (A_m + R_{\\text{dark}})
\\left(1 - \\frac{E_1(y e^{-k_x L}) - E_1(y)}{k_x L}
\\right) \\\\
D_* &= \\frac{D_0}{a_1 (f_0 - f_{\\min})} \\\\
g_{c,\\text{CO}_2} &=
L \\left(\\frac{g_{\\min}}{\\nu} +
\\frac{a_1 f_{\\text{str}} A_n}
{(\\text{CO}_{2,\\text{abs}} - \\Gamma)
(1 + D_s / D_*)}\\right)
where :math:`L` is the leaf area index, :math:`k_x` is the
extinction coefficient, :math:`\\nu = 1.6` is the diffusivity
ratio, and :math:`a_1 = 1 / (1 - f_0)`.
"""
y = alphac * self.kx * par / (am + rdark)
exp1_arg1 = jnp.array([y * jnp.exp(-self.kx * self.lai)])
exp1_arg2 = jnp.array([y])
exp1_term = exp1(exp1_arg1) - exp1(exp1_arg2)
exp1_term = jnp.squeeze(exp1_term)
an = (am + rdark) * (1.0 - (1.0 / (self.kx * self.lai)) * exp1_term)
a1 = 1.0 / (1.0 - self.f0)
dstar = d0 / (a1 * (self.f0 - fmin))
conductance_factor = a1 * fstr * an / ((co2abs - co2comp) * (1.0 + ds / dstar))
gcco2 = self.lai * (self.gmin / self.nuco2q + conductance_factor)
return gcco2
[docs]
def compute_rs(self, gcco2: Array) -> Array:
"""Compute the surface resistance ``rs``.
Notes:
The surface (stomatal) resistance is related to the canopy
CO₂ conductance by
.. math::
r_s = \\frac{1}{1.6 \\, g_{c,\\text{CO}_2}}
where the factor :math:`1.6` is the ratio of diffusivity of
water vapour to CO₂, and :math:`g_{c,\\text{CO}_2}` is the
canopy CO₂ conductance.
"""
return 1.0 / (1.6 * gcco2)
[docs]
def compute_light_use_efficiency(
self,
co2abs: Array,
co2comp: Array,
) -> Array:
"""Compute the light use efficiency ``alphac``.
Notes:
The light use efficiency depends on the CO₂ concentration:
.. math::
\\alpha_c = \\alpha_0 \\,
\\frac{\\text{CO}_{2,\\text{abs}} - \\Gamma}
{\\text{CO}_{2,\\text{abs}} + 2\\Gamma}
where :math:`\\alpha_0` is the quantum efficiency and
:math:`\\Gamma` is the CO₂ compensation concentration.
"""
co2_ratio = (co2abs - co2comp) / (co2abs + 2.0 * co2comp)
alphac = self.alpha0 * co2_ratio
return alphac
[docs]
def compute_surface_co2_resistance(self, gcco2: Array) -> Array:
"""Compute the surface resistance to CO₂ ``rsCO2``.
Notes:
The surface resistance to CO₂ is the reciprocal of the
canopy CO₂ conductance:
.. math::
r_{s,\\text{CO}_2} = \\frac{1}{g_{c,\\text{CO}_2}}
where :math:`g_{c,\\text{CO}_2}` is the canopy CO₂ conductance.
"""
return 1.0 / gcco2
[docs]
def compute_net_assimilation(
self, co2abs: Array, ci: Array, ra: Array, rsCO2: Array
) -> Array:
"""Compute the net CO₂ assimilation rate ``an``.
Notes:
The net assimilation rate follows Fick's law of diffusion:
.. math::
A_n = -\\frac{\\text{CO}_{2,\\text{abs}} - C_i}
{r_a + r_{s,\\text{CO}_2}}
where :math:`\\text{CO}_{2,\\text{abs}}` is the CO₂ absorption
concentration, :math:`C_i` is the intercellular CO₂
concentration, :math:`r_a` is the aerodynamic resistance,
and :math:`r_{s,\\text{CO}_2}` is the surface resistance to
CO₂.
"""
return -(co2abs - ci) / (ra + rsCO2)
[docs]
def compute_soil_water_fraction(self, wg: Array) -> Array:
"""Compute the soil water fraction ``fw``.
Notes:
The soil water fraction used for respiration scaling is
.. math::
f_w = \\frac{c_w \\, w_{\\max}}{w_g + w_{\\min}}
where :math:`w_g` is the surface soil moisture,
:math:`w_{\\max}` and :math:`w_{\\min}` are empirical
parameters, and :math:`c_w` is a scaling constant.
"""
return self.cw * self.wmax / (wg + self.wmin)
[docs]
def compute_respiration(
self,
temp_soil: Array,
fw: Array,
) -> Array:
"""Compute the soil respiration rate ``resp``.
Notes:
Soil respiration follows an Arrhenius-type temperature
response with a moisture limitation:
.. math::
R = R_{10} \\cdot (1 - f_w) \\cdot
\\exp\\!\\left(\\frac{E_0}{283.15 \\cdot R}
\\left(1 - \\frac{283.15}{T_{\\text{soil}}}
\\right)\\right)
where :math:`R_{10}` is the respiration rate at 283.15 K,
:math:`f_w` is the soil water fraction, :math:`E_0` is the
activation energy, and :math:`T_{\\text{soil}}` is the soil
temperature.
"""
temp_ratio = 1.0 - 283.15 / temp_soil
resp_factor = jnp.exp(self.e0 / (283.15 * 8.314) * temp_ratio)
resp = self.r10 * (1.0 - fw) * resp_factor
return resp
[docs]
def scale_flux_to_mol(self, flux: Array) -> Array:
"""Scale a flux to mol m⁻² s⁻¹."""
return flux * (cst.mair / (cst.rho * cst.mco2))
[docs]
def compute_cliq(self, wl: Array) -> Array:
"""Compute the wet fraction ``cliq``.
Notes:
The wet fraction is defined as
.. math::
c_{\\text{liq}} = \\frac{W_l}{\\text{LAI}\\cdot W_{\\text{max}}},
where :math:`W_l` is the water layer depth,
:math:`\\text{LAI}` is the leaf area index and
:math:`W_{\\text{max}}` is the thickness of the water layer on wet vegetation.
In case :math:`W_l > \\text{LAI}\\cdot W_{\\text{max}}`, the wet fraction is set to 1.
References:
Equation 9.19 from the CLASS book.
"""
wlmx = self.lai * self.wmax
return jnp.minimum(1.0, wl / wlmx)
[docs]
def compute_wltend(self, le_liq: Array) -> Array:
"""Compute the water layer depth tendency ``wltend``.
Notes:
The water layer depth tendency is the rate at which water is added to or taken from the vegetation,
described by
.. math::
\\frac{\\text{d} w}{\\text{d} t} = -\\frac{LE_{\\text{liq}}}{\\rho_w L_v},
where :math:`LE_{\\text{liq}}` is dew, :math:`\\rho_w` is water density and :math:`L_v` is the latent heat of vaporization.
References:
Equation 9.20 from the CLASS book, with sign convention.
"""
return -le_liq / (cst.rhow * cst.lv)
[docs]
def run_tends(self, state: AgsState, surf_state) -> AgsState:
"""Compute biosphere tendencies that depend on surface fluxes."""
wltend = self.compute_wltend(surf_state.le_liq)
return replace(state, wltend=wltend)
[docs]
def integrate(self, state: AgsState, dt: float) -> AgsState:
"""Integrate canopy water content forward in time."""
wl = state.wl + dt * state.wltend
return replace(state, wl=wl)