def Vp(self, Vpmax, Cm, Kp): # PEP carboxylation rate, that is the rate of C4 acid generation Vp = (Cm * Vpmax) / (Cm + Kp / U(1, 'atm')) Vpr = U(80, 'umol/m^2/s CO2' ) # PEP regeneration limited Vp, value adopted from vC book Vp = clip(Vp, 0, Vpr) return Vp
class Weight: CO2 = U(44.0098, 'g / umol') C = U(12.0107, 'g / umol') CH2O = U(30.031, 'g / umol') H2O = U(18.01528, 'g / umol') C_to_CH2O_ratio = C / CH2O # 0.40
def temperature_dependence_rate(self, Ea, T, Tb=U(25, 'degC')): R = U(8.314, 'J/K/mol') # universal gas constant (J K-1 mol-1) #HACK handle too low temperature values during optimization Tk = clip(T, lower=0, unit='degK') Tbk = clip(Tb, lower=0, unit='degK') try: return np.exp(Ea * (T - Tb) / (Tbk * R * Tk)) except ZeroDivisionError: return 0
class S(System): @derive(unit='m') def a(self): return 2 * self.x @derive(unit='m') def b(self): return self.x + U(1, 'm') @optimize(lower=U(0, 'm'), upper=U(2, 'm'), unit='m') def x(self): return self.a - self.b
def test_unit(): class S(System): @derive(unit='m') def a(self): return 2 @derive(unit='s') def b(self): return 1 @derive(unit='m/s') def c(self): return self.a / self.b s = instance(S) assert s.a == U(2, 'm') and s.b == U(1, 's') and s.c == U(2, 'm/s')
def test_parameter(): class S(System): a = parameter(1, unit='m') b = parameter(1, unit='m') c = parameter(1, unit='m') d = parameter(1, unit='m') e = parameter(1) @parameter(unit='m') def f(self, ff='1m'): return self.a + ff s = instance(S, config={'S': {'a': 2, 'b': '2', 'c': '2m', 'd': '200cm', 'e': '2m'}}) assert s.a == s.b == s.c == s.d == s.e == U(2, 'm') assert s.f == U(3, 'm')
def temperature_adjustment( self, w='weather', s='stomata', # see Campbell and Norman (1998) pp 224-225 # because Stefan-Boltzman constant is for unit surface area by denifition, # all terms including sbc are multilplied by 2 (i.e., gr, thermal radiation) lamda=U(44.0, 'kJ/mol'), # KJ mole-1 at 25oC Cp=U( 29.3, 'J/mol/degC' ), # thermodynamic psychrometer constant and specific heat of air (J mol-1 C-1) epsilon=0.97, sbc=U(5.6697e-8, 'J/m^2/s/degK^4'), # Stefan-Boltzmann constant (W m-2 K-4) ): T_air = w.T_air Tk = T_air.to('degK') PFD = w.PPFD P_air = w.P_air Jw = self.ET_supply gha = s.boundary_layer_conductance * ( 0.135 / 0.147 ) # heat conductance, gha = 1.4*.135*sqrt(u/d), u is the wind speed in m/s} Mol m-2 s-1 ? gv = s.total_conductance_h2o gr = 4 * epsilon * sbc * Tk**3 / Cp * 2 # radiative conductance, 2 account for both sides ghr = gha + gr thermal_air = epsilon * sbc * Tk**4 * 2 # emitted thermal radiation psc = Cp / lamda # psychrometric constant (C-1) psc1 = psc * ghr / gv # apparent psychrometer constant PAR = U(PFD.to('umol/m^2/s Quanta').magnitude / 4.55, 'J/m^2/s') # W m-2 # If total solar radiation unavailable, assume NIR the same energy as PAR waveband NIR = PAR scatt = 0.15 # shortwave radiation (PAR (=0.85) + NIR (=0.15) solar radiation absorptivity of leaves: =~ 0.5 # times 2 for projected area basis R_abs = (1 - scatt) * PAR + scatt * NIR + 2 * (epsilon * sbc * Tk**4) # debug dt I commented out the changes that yang made for leaf temperature for a test. I don't think they work if Jw == 0: # (R_abs - thermal_air - lamda * gv * w.VPD / P_air) / (Cp * ghr + lamda * w.saturation_slope * gv) # eqn 14.6a # eqn 14.6b linearized form using first order approximation of Taylor series return (psc1 / (w.saturation_slope + psc1)) * ((R_abs - thermal_air) / (ghr * Cp) - w.VPD / (psc1 * P_air)) else: return (R_abs - thermal_air - lamda * Jw) / (Cp * ghr)
def stomatal_conductance(self, g0, g1, gb, m, A_net, CO2, RH, drb, gamma=U(10, 'umol/mol')): Cs = CO2 - (drb * A_net / gb) # surface CO2 in mole fraction Cs = clip(Cs, lower=gamma) a = m * g1 * A_net / Cs b = g0 + gb - a c = (-RH * gb) - g0 #hs = max(np.roots([a, b, c])) #hs = scipy.optimize.brentq(lambda x: np.polyval([a, b, c], x), 0, 1) #hs = scipy.optimize.fsolve(lambda x: np.polyval([a, b, c], x), 0) hs = quadratic_solve_upper(a, b, c) #hs = clip(hs, 0.1, 1.0) # preventing bifurcation: used to be (0.3, 1.0) for C4 maize #FIXME unused? #T_leaf = l.temperature #es = w.vp.saturation(T_leaf) #Ds = (1 - hs) * es # VPD at leaf surface #Ds = w.vp.deficit(T_leaf, hs) gs = g0 + (g1 * m * (A_net * hs / Cs)) gs = clip(gs, lower=g0) return gs
def test_optimize_with_unit(): class S(System): @derive(unit='m') def a(self): return 2 * self.x @derive(unit='m') def b(self): return self.x + U(1, 'm') @optimize(lower=U(0, 'm'), upper=U(2, 'm'), unit='m') def x(self): return self.a - self.b s = instance(S) assert s.x == U(1, 'm') assert s.a == s.b == U(2, 'm')
def optical_air_mass_number(self, elevation_angle): t_s = clip(elevation_angle, lower=0, unit='rad') #FIXME need to do max(0.0001, sin(t_s))? try: #FIXME check 101.3 is indeed in kPa return self.atmospheric_pressure / (U(101.3, 'kPa') * sin(t_s)) except: return 0
def hour_angle_at_horizon(self): c = self._cos_hour_angle(angle=U(90, 'deg')) # in the polar region during the winter, sun does not rise if c > 1: return 0 # white nights during the summer in the polar region elif c < -1: return 180 else: return degrees(arccos(c))
def test_nounit(): class S(System): @derive(unit='m') def a(self): return 1 @derive(nounit='a') def b(self, a): return a s = instance(S) assert isinstance(s.a, U.registry.Quantity) assert s.a == U(1, 'm') assert not isinstance(s.b, U.registry.Quantity) assert s.b == 1
def test_nounit_with_alias(): class S(System): @derive(alias='aa', unit='m') def a(self): return 1 @derive(alias='bb', nounit='aa') def b(self, aa): return aa s = instance(S) assert isinstance(s.aa, U.registry.Quantity) assert s.aa == U(1, 'm') assert not isinstance(s.bb, U.registry.Quantity) assert s.bb == 1
def leaf_angle_coeff(self, zenith_angle): elevation_angle = U(90, 'deg') - zenith_angle #FIXME need to prevent zero like sin_beta / cot_beta? a = elevation_angle.to('rad') t = zenith_angle.to('rad') # leaf angle distribution parameter x = self.leaf_angle_factor return { # When Lt accounts for total path length, division by sin(elev) isn't necessary LeafAngle.spherical: 1 / (2 * sin(a)), LeafAngle.horizontal: 1, LeafAngle.vertical: 1 / (tan(a) * pi / 2), LeafAngle.empirical: 0.667, LeafAngle.diaheliotropic: 1 / sin(a), LeafAngle.ellipsoidal: sqrt(x**2 + tan(t)**2) / (x + 1.774 * (x + 1.182)**-0.733), }[self.leaf_angle]
def maximum_electron_transport_rate(self, T, T_dep, N_dep, Jm25=U(300, 'umol/m^2/s Electron'), Eaj=U(32800, 'J/mol'), Sj=U(702.6, 'J/mol/degK'), Hj=U(220000, 'J/mol')): R = U(8.314, 'J/K/mol') Tb = U(25, 'degC') Tk = T.to('degK') Tbk = Tb.to('degK') r = Jm25 * N_dep \ * T_dep(Eaj) \ * (1 + np.exp((Sj*Tbk - Hj) / (R*Tbk))) \ / (1 + np.exp((Sj*Tk - Hj) / (R*Tk))) return clip(r, lower=0)
def atmospheric_pressure(self, altitude): try: # campbell and Norman (1998), p 41 return 101.3 * exp(-U.magnitude(altitude, 'm') / 8200) except: return 100
def leafp_effect(self, LWP='leaf.soil.WP_leaf', sf=U(2.3, '1/MPa'), phyf=U(-2.0, 'MPa')): return (1 + np.exp(sf * phyf)) / (1 + np.exp(sf * (phyf - LWP)))
def solar_noon(self, LC, ET): return U(12, 'hr') - LC - ET
def b(self): return self.x + U(1, 'm')
def Kp(self, Kp25=U(80, 'ubar')): return Kp25 # T dependence yet to be determined
def test_U(): Q = U.registry.Quantity assert U(1) == U('1') == 1 assert U(1, 'm') == U('1', 'm') == U('1m') == Q(1, 'm') assert U(None) == U(None, None) == U(None, 'm') == None assert U(1, None) == 1 assert U(1, '') == Q(1) == Q(1, None) == Q(1, '') a = U('1m', 'cm') assert a.magnitude == 100 and a.units == Q(1, 'cm').units assert U('N') == 'N' != U('1N') assert U('abc.def') == 'abc.def' assert U(-1) == U('-1') == -1 assert U(.1) == U('.1') == 0.1
def Vcmax(self, N_dep, T_dep, Vcm25=U(50, 'umol/m^2/s CO2'), EaVc=U(55900, 'J/mol')): return Vcm25 * N_dep * T_dep(EaVc)
def Kc(self, T_dep, Kc25=U(650, 'ubar'), Eac=U(59400, 'J/mol')): return Kc25 * T_dep(Eac)
def dark_respiration(self, T_dep, Rd25=U(2, 'umol/m^2/s O2'), Ear=U(39800, 'J/mol')): return Rd25 * T_dep(Ear)
def Ko(self, T_dep, Ko25=U(450, 'mbar'), Eao=U(36000, 'J/mol')): return Ko25 * T_dep(Eao)
def Vpmax(self, N_dep, T_dep, Vpm25=U(70, 'umol/m^2/s CO2'), EaVp=U(75100, 'J/mol')): return Vpm25 * N_dep * T_dep(EaVp)
def co2_mesophyll(self, A_net, w='weather', rvc='stomata.rvc'): P = w.P_air / U(100, 'kPa') Ca = w.CO2 * P # conversion to partial pressure Cm = Ca - A_net * rvc * P #print(f"+ Cm = {Cm}, Ca = {Ca}, A_net = {A_net}, gs = {self.stomata.gs}, gb = {self.stomata.gb}, rvc = {rvc}, P = {P}") return clip(Cm, 0, 2 * Ca)
def _saturation_slope(self, T, *, b, c): return self.es(T) * (b*c)/(c+T)**2 / U(1, 'degC')
def bundle_sheath_o2(self, A_net, gbs, Om, alpha=0.0001): return alpha * A_net / (0.047 * gbs) * U( 1, 'atm') + Om # Bundle sheath O2 partial pressure, mbar