def profit_psi(p, photo='Farquhar', res='low', inf_gb=False, deriv=False): """ Finds the instateneous profit maximization, following the optmization criterion for which, at each instant in time, the stomata regulate canopy gas exchange and pressure to achieve the maximum profit, which is the maximum difference between the normalized photosynthetic gain (gain) and the hydraulic cost function (cost). That is when d(gain)/dP = d(cost)/dP. Arguments: ---------- p: recarray object or pandas series or class containing the data time step's met data & params photo: string either the Farquhar model for photosynthesis, or the Collatz model res: string either 'low' (default), 'med', or 'high' to run the optimising solver onopt: boolean if True, the optimisation is performed. If Fall, fall back on previously performed optimisation for the value of the maximum profit. inf_gb: bool if True, gb is prescrived and very large Returns: -------- E_can: float transpiration [mmol m-2 s-1] at maximum profit across leaves gs_can: float stomatal conductance [mol m-2 s-1] at maximum profit across leaves An_can: float net photosynthetic assimilation rate [umol m-2 s-1] at maximum profit across leaves Ci_can: float intercellular CO2 concentration [Pa] at maximum profit across leaves rublim_can: string 'True' if the C assimilation is rubisco limited, 'False' otherwise """ # hydraulics P, trans = hydraulics(p, res=res) cost, __ = hydraulic_cost(p, P) # look for the most net profit gain, Ci, mask = photo_gain(p, trans, photo, res, inf_gb=inf_gb) expr = gain - cost[mask] if deriv: expr = np.abs(np.gradient(expr, P[mask])) # deal with edge cases by rebounding the solution gc, gs, gb, __ = leaf_energy_balance(p, trans[mask], inf_gb=inf_gb) try: if inf_gb: # check on valid range check = expr[gc > cst.zero] else: # further constrain the realm of possible gs check = expr[np.logical_and(gc > cst.zero, gs < 1.5 * gb)] idx = np.isclose(expr, max(check)) if deriv: idx = np.isclose(expr, min(check)) idx = [list(idx).index(e) for e in idx if e] if inf_gb: # check for algo. "overshooting" due to inf. gb while Ci[idx[0]] < 2. * p.gamstar25: idx[0] -= 1 if idx[0] < 3: break # optimized where Ci for both photo models are close Ci = Ci[idx[0]] trans = trans[mask][idx[0]] # mol.m-2.s-1 gs = gs[idx[0]] Pleaf = P[mask][idx[0]] # rubisco- or electron transport-limitation? An, Aj, Ac = calc_photosynthesis(p, trans, Ci, photo, inf_gb=inf_gb) rublim = rubisco_limit(Aj, Ac) # leaf temperature? Tleaf, __ = leaf_temperature(p, trans, inf_gb=inf_gb) if (np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero) and (An > 0.)) or (idx[0] == len(P) - 1) or any( np.isnan([An, Ci, trans, gs, Tleaf, Pleaf])): An, Ci, trans, gs, gb, Tleaf, Pleaf = (9999., ) * 7 elif not np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero): trans *= conv.MILI # mmol.m-2.s-1 return An, Ci, rublim, trans, gs, gb, Tleaf, Pleaf except ValueError: # no opt return (9999., ) * 8
(abs(Tleaf - new_Tleaf) <= threshold_conv) and not np.isclose(gs, cst.zero, rtol=cst.zero, atol=cst.zero))): break # no convergence, iterate on leaf temperature Tleaf = new_Tleaf iter += 1 if case == 1: # infer leaf water potential, MPa Pleaf = P[bn.nanargmin(np.abs(E - trans))] else: Pleaf = P[iopt] ksc = p.kmaxS2 * cost[iopt] rublim = rubisco_limit(Aj, Ac) # lim? if ((np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero) and (An > 0.)) or np.isclose(Ci, 0., rtol=cst.zero, atol=cst.zero) or (Ci < 0.) or np.isclose(Ci, p.CO2, rtol=cst.zero, atol=cst.zero) or (Ci > p.CO2) or (real_zero is None) or (not real_zero) or any(np.isnan([An, Ci, trans, gs, new_Tleaf, Pleaf]))): An, Ci, trans, gs, gb, new_Tleaf, Pleaf = (9999., ) * 7 elif not np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero): trans *= conv.MILI # mmol.m-2.s-1 if case == 1: return An, Ci, rublim, trans, gs, gb, new_Tleaf, Pleaf
def Tuzet(p, photo='Farquhar', res='low', inf_gb=False): """ Checks the energy balance by looking for convergence of the new leaf temperature with the leaf temperature predicted by the previous iteration. Then returns the corresponding An, E, Ci, etc. Arguments: ---------- p: recarray object or pandas series or class containing the data time step's met data & params photo: string either the Farquhar model for photosynthesis, or the Collatz model threshold_conv: float convergence threshold for the new leaf temperature to be in energy balance iter_max: int maximum number of iterations allowed on the leaf temperature before reaching the conclusion that the system is not energy balanced inf_gb: bool if True, gb is prescrived and very large Returns: -------- trans_can: float transpiration rate of canopy [mmol m-2 s-1] across leaves gs_can: float stomatal conductance of canopy [mol m-2 s-1] across leaves An_can: float C assimilation rate of canopy [umol m-2 s-1] across leaves Ci_can: float average intercellular CO2 concentration of canopy [Pa] across leaves rublim_can: string 'True' if the C assimilation is rubisco limited, 'False' otherwise. """ # stability pre-requisites fw = fLWP_stable(p, photo=photo, res=res, inf_gb=inf_gb) # run the energy balance sub-routine An, Aj, Ac, Ci, trans, gs, gb, Tleaf, Pleaf = gas_exchange(p, fw, photo=photo, res=res, inf_gb=inf_gb) rublim = rubisco_limit(Aj, Ac) # lim? if not np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero): trans *= conv.MILI # mmol.m-2.s-1 return An, Ci, rublim, trans, gs, gb, Tleaf, Pleaf
def Cmax_gs(p, photo='Farquhar', res='low', inf_gb=False): """ Finds the instantaneous optimal C gain for a given C cost. First, the C gain equation is derived for gs, beta, Ci unknown. Then, the derived form of the equation is solved for Ci over a range of possible betas, gs, all of which are directly or indirectly leaf water potential P dependent. A check (check_solve) is performed to verify that the optimization satisfies the zero equality criteria and, finally, results are bound via a range of physically possible Ci values. N.B.: there can be several possible optimizations Arguments: ---------- p: recarray object or pandas series or class containing the data time step's met data & params photo: string either the Farquhar model for photosynthesis, or the Collatz model inf_gb: bool if True, gb is prescrived and very large Returns: -------- gsOPT: float stomatal conductance [mol.m-2.s-1] for which the A(gs) is maximized AnOPT: float maximum C assimilation rate [μmol.m-2.s-1] given by the diffusive supply of CO2 transOPT: float transpiration rate [mmol.m-2.s-1] for which the A(gs) is maximized CiOPT: float intercellular CO2 concentration [Pa] for which the A(gs) is maximized """ # energy balance P, trans = hydraulics(p, res=res, kmax=p.kmaxCM) # expression of optimization Ci, mask = Ci_sup_dem(p, trans, photo=photo, res=res, inf_gb=inf_gb) gc, gs, gb, __ = leaf_energy_balance(p, trans[mask], inf_gb=inf_gb) expr = np.abs( np.gradient(A_trans(p, trans[mask], Ci, inf_gb=inf_gb), P[mask]) - dcost_dpsi(p, P[mask], gs)) try: if inf_gb: # check on valid range check = expr[gc > cst.zero] else: # further constrain the realm of possible gs check = expr[np.logical_and(gc > cst.zero, gs < 1.5 * gb)] idx = np.isclose(expr, min(check)) idx = [list(idx).index(e) for e in idx if e] if inf_gb: # check for algo. "overshooting" due to inf. gb while Ci[idx[0]] < 2. * p.gamstar25: idx[0] -= 1 if idx[0] < 3: break # optimized where Ci for both photo models are close Ci = Ci[idx[0]] trans = trans[mask][idx[0]] # mol.m-2.s-1 gs = gs[idx[0]] Pleaf = P[mask][idx[0]] # rubisco limitation or electron transport-limitation? An, Aj, Ac = calc_photosynthesis(p, trans, Ci, photo=photo, inf_gb=inf_gb) rublim = rubisco_limit(Aj, Ac) # leaf temperature? Tleaf, __ = leaf_temperature(p, trans, inf_gb=inf_gb) if (np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero) and (An > 0.)) or (idx[0] == len(P) - 1) or any( np.isnan([An, Ci, trans, gs, Tleaf, Pleaf])): An, Ci, trans, gs, gb, Tleaf, Pleaf = (9999., ) * 7 elif not np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero): trans *= conv.MILI # mmol.m-2.s-1 return An, Ci, rublim, trans, gs, gb, Tleaf, Pleaf except ValueError: # no opt return (9999., ) * 8
def solve_std(p, sw, photo='Farquhar', res='low', iter_max=40, threshold_conv=0.1, inf_gb=False): """ Checks the energy balance by looking for convergence of the new leaf temperature with the leaf temperature predicted by the previous iteration. Then returns the corresponding An, E, Ci, etc. Arguments: ---------- p: recarray object or pandas series or class containing the data time step's met data & params sw: float mean volumetric soil moisture content [m3 m-3] photo: string either the Farquhar model for photosynthesis, or the Collatz model threshold_conv: float convergence threshold for the new leaf temperature to be in energy balance iter_max: int maximum number of iterations allowed on the leaf temperature before reaching the conclusion that the system is not energy balanced inf_gb: bool if True, gb is prescrived and very large Returns: -------- trans_can: float transpiration rate of canopy [mmol m-2 s-1] across leaves gs_can: float stomatal conductance of canopy [mol m-2 s-1] across leaves An_can: float C assimilation rate of canopy [umol m-2 s-1] across leaves Ci_can: float average intercellular CO2 concentration of canopy [Pa] across leaves rublim_can: string 'True' if the C assimilation is rubisco limited, 'False' otherwise. """ # initial state Cs = p.CO2 # Pa Tleaf = p.Tair # deg C Dleaf = np.maximum(0.05, p.VPD) # gs model not valid < 0.05 # hydraulics P, E = hydraulics(p, res=res) if sw >= p.fc: g1 = p.g1 else: g1 = p.g1 * fwWP(p, p.Ps) # initialise gs over A g0 = 1.e-9 # g0 ~ 0, removing it entirely introduces errors Cs_umol_mol = Cs * conv.MILI / p.Patm # umol mol-1 gsoA = g0 + (1. + g1 / (Dleaf ** 0.5)) / Cs_umol_mol # iter on the solution until it is stable enough iter = 0 while True: An, Aj, Ac, Ci = calc_photosynthesis(p, 0., Cs, photo, Tleaf=Tleaf, gs_over_A=gsoA) # stomatal conductance, with moisture stress effect gs = np.maximum(cst.zero, conv.GwvGc * gsoA * An) # calculate new trans, gw, gb, mol.m-2.s-1 trans, real_zero, gw, gb, Dleaf = calc_trans(p, Tleaf, gs, inf_gb=inf_gb) new_Tleaf, __ = leaf_temperature(p, trans, Tleaf=Tleaf, inf_gb=inf_gb) # update Cs (Pa) boundary_CO2 = p.Patm * conv.FROM_MILI * An / (gb * conv.GbcvGb) Cs = np.maximum(cst.zero, np.minimum(p.CO2, p.CO2 - boundary_CO2)) Cs_umol_mol = Cs * conv.MILI / p.Patm # new leaf-air vpd, kPa if (np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero) or np.isclose(gw, cst.zero, rtol=cst.zero, atol=cst.zero) or np.isclose(gs, cst.zero, rtol=cst.zero, atol=cst.zero)): Dleaf = np.maximum(0.05, p.VPD) # kPa # update gs over A gsoA = g0 + (1. + g1 / (Dleaf ** 0.5)) / Cs_umol_mol # force stop when atm. conditions yield E < 0. (non-physical) if (iter < 1) and (not real_zero): real_zero = None # check for convergence if ((real_zero is None) or (iter >= iter_max) or ((iter >= 1) and real_zero and (abs(Tleaf - new_Tleaf) <= threshold_conv) and not np.isclose(gs, cst.zero, rtol=cst.zero, atol=cst.zero))): break # no convergence, iterate on leaf temperature Tleaf = new_Tleaf iter += 1 Pleaf = P[bn.nanargmin(np.abs(trans - E))] rublim = rubisco_limit(Aj, Ac) # lim? if ((np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero) and (An > 0.)) or np.isclose(Ci, 0., rtol=cst.zero, atol=cst.zero) or (Ci < 0.) or np.isclose(Ci, p.CO2, rtol=cst.zero, atol=cst.zero) or (Ci > p.CO2) or (real_zero is None) or (not real_zero) or any(np.isnan([An, Ci, trans, gs, new_Tleaf, Pleaf]))): An, Ci, trans, gs, gb, new_Tleaf, Pleaf = (9999.,) * 7 elif not np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero): trans *= conv.MILI # mmol.m-2.s-1 return An, Ci, rublim, trans, gs, gb, new_Tleaf, Pleaf