from dataclasses import dataclass, field, replace
import jax.numpy as jnp
from jax import Array
from ...abstracts import AbstractCoupledState
from ...utils import PhysicalConstants as cst
from ...utils import compute_esat, compute_qsat
from ..abstracts import AbstractSurfaceModel, AbstractSurfaceState
[docs]
@dataclass
class StandardSurfaceState(AbstractSurfaceState):
"""Standard surface state."""
alpha: Array = field(
metadata={
"label": r"$\alpha$",
"unit": "-",
"description": "Surface albedo",
}
)
"""Surface albedo [-]."""
surf_temp: Array = field(
metadata={
"label": r"$T_{surf}$",
"unit": "K",
"description": "Surface temperature",
}
)
"""Surface temperature [K]."""
esat: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$e_{sat}$",
"unit": "Pa",
"description": "Saturation vapor pressure",
},
)
"""Saturation vapor pressure [Pa]."""
qsat: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$q_{sat}$",
"unit": "kg kg^{-1}",
"description": "Saturation specific humidity",
},
)
"""Saturation specific humidity [kg/kg]."""
dqsatdT: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$dq_{sat}/dT$",
"unit": "kg kg^{-1} K^{-1}",
"description": "Derivative of saturation specific humidity",
},
)
"""Derivative of saturation specific humidity with respect to temperature [kg/kg/K]."""
e: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$e$",
"unit": "Pa",
"description": "Vapor pressure",
},
)
"""Vapor pressure [Pa]."""
qsatsurf: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$q_{sat}(T_s)$",
"unit": "kg kg^{-1}",
"description": "Saturation specific humidity at surface",
},
)
"""Saturation specific humidity at surface temperature [kg/kg]."""
le_veg: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$LE_{veg}$",
"unit": "W m^{-2}",
"description": "Latent heat flux from vegetation",
},
)
"""Latent heat flux from vegetation [W m-2]."""
le_liq: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$LE_{liq}$",
"unit": "W m^{-2}",
"description": "Latent heat flux from liquid water",
},
)
"""Latent heat flux from liquid water [W m-2]."""
le_soil: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$LE_{soil}$",
"unit": "W m^{-2}",
"description": "Latent heat flux from soil",
},
)
"""Latent heat flux from soil [W m-2]."""
le: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$LE$",
"unit": "W m^{-2}",
"description": "Total latent heat flux",
},
)
"""Total latent heat flux [W m-2]."""
hf: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$H$",
"unit": "W m^{-2}",
"description": "Sensible heat flux",
},
)
"""Sensible heat flux [W m-2]."""
gf: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$G$",
"unit": "W m^{-2}",
"description": "Ground heat flux",
},
)
"""Ground heat flux [W m-2]."""
le_pot: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$LE_{pot}$",
"unit": "W m^{-2}",
"description": "Potential latent heat flux",
},
)
"""Potential latent heat flux [W m-2]."""
le_ref: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$LE_{ref}$",
"unit": "W m^{-2}",
"description": "Reference latent heat flux",
},
)
"""Reference latent heat flux [W m-2]."""
vpd: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$VPD$",
"unit": "Pa",
"description": "Vapor pressure deficit",
},
)
"""Vapor pressure deficit [Pa]."""
wtheta: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$(w'\theta')_s$",
"unit": "K m s^{-1}",
"description": "Kinematic heat flux",
},
)
"""Kinematic heat flux [K m/s]."""
wq: Array = field(
default_factory=lambda: jnp.array(0.0),
metadata={
"label": r"$(w'q')_s$",
"unit": "kg kg^{-1} m s^{-1}",
"description": "Kinematic moisture flux",
},
)
"""Kinematic moisture flux [kg/kg m/s]."""
[docs]
class StandardSurfaceModel(AbstractSurfaceModel[StandardSurfaceState]):
"""Standard surface model calculating skin temperature and energy fluxes.
Args:
lam: thermal diffusivity of the soil/skin layer [W m-1 K-1]. Default is 5.9.
rsmin: minimum stomatal resistance [s m-1]. Default is 110.0.
lai: leaf area index [m2 m-2]. Default is 2.0.
"""
def __init__(
self,
lam: float = 5.9,
rsmin: float = 110.0,
lai: float = 2.0,
):
self.lam = lam
self.rsmin = rsmin
self.lai = lai
[docs]
def init_state(
self,
alpha: float = 0.25,
surf_temp: float = 290.0,
) -> StandardSurfaceState:
"""Initialize the surface state.
Args:
alpha: surface albedo [-]. Default is 0.25.
surf_temp: surface skin temperature [K]. Default is 290.0.
Returns:
The initialized StandardSurfaceState.
"""
return StandardSurfaceState(
alpha=jnp.array(alpha),
surf_temp=jnp.array(surf_temp),
)
[docs]
def run(self, state: AbstractCoupledState) -> StandardSurfaceState:
"""Compute surface energy balance, skin temperature, and fluxes."""
land = state.land
atmos = state.atmos
ra = atmos.ra
esat = compute_esat(atmos.theta)
qsat = compute_qsat(atmos.theta, atmos.surf_pressure)
dqsatdT = self.compute_dqsatdT(esat, atmos.theta, atmos.surf_pressure)
e = self.compute_e(atmos.q, atmos.surf_pressure)
rs = land.biosphere.rs
cliq = land.biosphere.cliq
cveg = land.biosphere.cveg
rssoil = land.soil.rssoil
temp_soil = land.soil.temp_soil
surf_temp = self.compute_skin_temperature(
state.net_rad,
atmos.theta,
atmos.q,
qsat,
dqsatdT,
ra,
rs,
rssoil,
cliq,
temp_soil,
cveg,
)
qsatsurf = compute_qsat(surf_temp, atmos.surf_pressure)
le_veg = self.compute_le_veg(
surf_temp,
atmos.theta,
atmos.q,
qsat,
dqsatdT,
ra,
rs,
cliq,
cveg,
)
le_liq = self.compute_le_liq(
surf_temp,
atmos.theta,
atmos.q,
qsat,
dqsatdT,
ra,
cliq,
cveg,
)
le_soil = self.compute_le_soil(
surf_temp,
atmos.theta,
atmos.q,
qsat,
dqsatdT,
ra,
rssoil,
cveg,
)
le = self.compute_le(le_soil, le_veg, le_liq)
hf = self.compute_hf(surf_temp, atmos.theta, ra)
gf = self.compute_gf(surf_temp, temp_soil)
le_pot = self.compute_le_pot(
state.net_rad,
gf,
dqsatdT,
qsat,
atmos.q,
ra,
)
le_ref = self.compute_le_ref(
state.net_rad,
gf,
dqsatdT,
qsat,
atmos.q,
ra,
)
wtheta = self.compute_wtheta(hf)
wq = self.compute_wq(le)
vpd = self.compute_vpd(atmos.q, qsat)
return replace(
land.surface,
esat=esat,
qsat=qsat,
dqsatdT=dqsatdT,
e=e,
surf_temp=surf_temp,
qsatsurf=qsatsurf,
le_veg=le_veg,
le_liq=le_liq,
le_soil=le_soil,
le=le,
hf=hf,
gf=gf,
le_pot=le_pot,
le_ref=le_ref,
vpd=vpd,
wtheta=wtheta,
wq=wq,
)
[docs]
def compute_dqsatdT(self, esat: Array, theta: float, surf_pressure: float) -> Array:
"""Compute derivative of saturation vapor pressure with respect to temperature."""
num = 17.2694 * (theta - 273.16)
den = (theta - 35.86) ** 2.0
mult = num / den
desatdT = esat * mult
return 0.622 * desatdT / surf_pressure
[docs]
def compute_e(self, q: Array, surf_pressure: Array) -> Array:
"""Compute the vapor pressure."""
return q * surf_pressure / 0.622
[docs]
def compute_skin_temperature(
self,
net_rad: Array,
theta: Array,
q: Array,
qsat: Array,
dqsatdT: Array,
ra: Array,
rs: Array,
rssoil: Array,
cliq: Array,
temp_soil: Array,
cveg: Array,
) -> Array:
"""Compute skin temperature surf_temp."""
return (
net_rad
+ cst.rho * cst.cp / ra * theta
+ cveg
* (1.0 - cliq)
* cst.rho
* cst.lv
/ (ra + rs)
* (dqsatdT * theta - qsat + q)
+ (1.0 - cveg)
* cst.rho
* cst.lv
/ (ra + rssoil)
* (dqsatdT * theta - qsat + q)
+ cveg * cliq * cst.rho * cst.lv / ra * (dqsatdT * theta - qsat + q)
+ self.lam * temp_soil
) / (
cst.rho * cst.cp / ra
+ cveg * (1.0 - cliq) * cst.rho * cst.lv / (ra + rs) * dqsatdT
+ (1.0 - cveg) * cst.rho * cst.lv / (ra + rssoil) * dqsatdT
+ cveg * cliq * cst.rho * cst.lv / ra * dqsatdT
+ self.lam
)
[docs]
def compute_le_veg(
self,
surf_temp: Array,
theta: Array,
q: Array,
qsat: Array,
dqsatdT: Array,
ra: Array,
rs: Array,
cliq: Array,
cveg: Array,
) -> Array:
"""Compute latent heat flux (transpiration) from vegetation."""
term = dqsatdT * (surf_temp - theta) + qsat - q
le_veg = cst.rho * cst.lv / (ra + rs) * term
frac = (1.0 - cliq) * cveg
return frac * le_veg
[docs]
def compute_le_liq(
self,
surf_temp: Array,
theta: Array,
q: Array,
qsat: Array,
dqsatdT: Array,
ra: Array,
cliq: Array,
cveg: Array,
) -> Array:
"""Compute latent heat flux on the leaf (dew) le_liq."""
term = dqsatdT * (surf_temp - theta) + qsat - q
le_liq = cst.rho * cst.lv / ra * term
frac = cliq * cveg
return frac * le_liq
[docs]
def compute_le_soil(
self,
surf_temp: Array,
theta: Array,
q: Array,
qsat: Array,
dqsatdT: Array,
ra: Array,
rssoil: Array,
cveg: Array,
) -> Array:
"""Compute latent heat flux on the soil (evaporation) le_soil."""
term = dqsatdT * (surf_temp - theta) + qsat - q
le_soil = cst.rho * cst.lv / (ra + rssoil) * term
frac = 1.0 - cveg
return frac * le_soil
[docs]
def compute_le(self, le_soil: Array, le_veg: Array, le_liq: Array) -> Array:
"""Compute the total evapotranspiration (latent heat flux) le."""
return jnp.clip(le_soil + le_veg + le_liq, 0.0, None)
[docs]
def compute_hf(self, surf_temp: Array, theta: Array, ra: Array) -> Array:
"""Compute sensible heat flux hf."""
return cst.rho * cst.cp / ra * (surf_temp - theta)
[docs]
def compute_gf(self, surf_temp: Array, temp_soil: Array) -> Array:
"""Compute ground heat flux gf."""
return self.lam * (surf_temp - temp_soil)
[docs]
def compute_le_pot(
self,
net_rad: Array,
gf: Array,
dqsatdT: Array,
qsat: Array,
q: Array,
ra: Array,
) -> Array:
"""Compute potential latent heat flux."""
rad_term = dqsatdT * (net_rad - gf)
aerodynamic_term = cst.rho * cst.cp / ra * (qsat - q)
denominator = dqsatdT + cst.cp / cst.lv
return (rad_term + aerodynamic_term) / denominator
[docs]
def compute_le_ref(
self,
net_rad: Array,
gf: Array,
dqsatdT: Array,
qsat: Array,
q: Array,
ra: Array,
) -> Array:
"""Compute reference latent heat flux."""
rad_term = dqsatdT * (net_rad - gf)
aerodynamic_term = cst.rho * cst.cp / ra * (qsat - q)
den1 = dqsatdT
den2 = cst.cp / cst.lv * (1.0 + self.rsmin / self.lai / ra)
return (rad_term + aerodynamic_term) / (den1 + den2)
[docs]
def compute_wtheta(self, hf: Array) -> Array:
"""Compute kinematic heat flux."""
return hf / (cst.rho * cst.cp)
[docs]
def compute_wq(self, le: Array) -> Array:
"""Compute kinematic moisture flux."""
return le / (cst.rho * cst.lv)
[docs]
def compute_vpd(self, q: Array, qsat: Array) -> Array:
"""Compute vapor pressure deficit."""
return qsat - q