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 if c3c4 == "c3" else 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 if c3c4 == "c3" else 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 if c3c4 == "c3" else 2.0
self.temp1Am = 281.0 if c3c4 == "c3" else 286.0
self.temp2Am = 311.0 if c3c4 == "c3" else 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 if c3c4 == "c3" else 0.7
self.gmin = 0.25e-3 if c3c4 == "c3" else 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."""
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."""
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 minimum stomatal conductance factor."""
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 vapor pressure deficit (ds) in kPa."""
ds = (compute_esat(surf_temp) - e) / 1000.0 # kPa
return ds
[docs]
def compute_d0(self, fmin: Array) -> Array:
"""Compute reference vapor pressure deficit."""
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 cfrac, co2abs, and ci (internal CO2 concentration)."""
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 maximal gross primary production."""
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 effect of soil moisture stress."""
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 gross assimilation rate."""
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 dark respiration."""
rdark = (1.0 / 9.0) * am
return rdark
[docs]
def compute_absorbed_par(self, in_srad: Array) -> Array:
"""Compute absorbed photosynthetically active rad."""
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 CO2 conductance at canopy level."""
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 surface resistance."""
return 1.0 / (1.6 * gcco2)
[docs]
def compute_light_use_efficiency(
self,
co2abs: Array,
co2comp: Array,
) -> Array:
"""Compute light use efficiency."""
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 surface resistance to CO₂."""
return 1.0 / gcco2
[docs]
def compute_net_assimilation(
self, co2abs: Array, ci: Array, ra: Array, rsCO2: Array
) -> Array:
"""Compute net CO₂ assimilation rate."""
return -(co2abs - ci) / (ra + rsCO2)
[docs]
def compute_soil_water_fraction(self, wg: Array) -> Array:
"""Compute soil water fraction."""
return self.cw * self.wmax / (wg + self.wmin)
[docs]
def compute_respiration(
self,
temp_soil: Array,
fw: Array,
) -> Array:
"""Compute soil respiration."""
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 wet fraction of canopy."""
wlmx = self.lai * self.wmax
return jnp.minimum(1.0, wl / wlmx)
[docs]
def compute_wltend(self, le_liq: Array) -> Array:
"""Compute canopy water storage tendency."""
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)