def test_leafscale(method='MEDLYN_FARQUHAR', species='pine', Ebal=False): gamma = 1.0 gfact = 1.0 if species.upper() == 'PINE': photop = { # 'Vcmax': 55.0, # 'Jmax': 105.0, # 'Rd': 1.3, # 'tresp': { # 'Vcmax': [78.0, 200.0, 650.0], # 'Jmax': [56.0, 200.0, 647.0], # 'Rd': [33.0] # }, 'Vcmax': 94.0, # Tarvainen et al. 2018 Physiol. Plant. 'Jmax': 143.0, 'Rd': 1.3, 'tresp': { 'Vcmax': [78.3, 200.0, 650.1], 'Jmax': [56.0, 200.0, 647.9], 'Rd': [33.0] }, 'alpha': gamma * 0.2, 'theta': 0.7, 'La': 1600.0, 'g1': gfact * 2.3, 'g0': 1.0e-3, 'kn': 0.6, 'beta': 0.95, 'drp': 0.7, } leafp = {'lt': 0.02, 'par_alb': 0.12, 'nir_alb': 0.55, 'emi': 0.98} if species.upper() == 'SPRUCE': photop = { # 'Vcmax': 60.0, # 'Jmax': 114.0, # 'Rd': 1.5, # 'tresp': { # 'Vcmax': [53.2, 202.0, 640.3], # Tarvainen et al. 2013 Oecologia # 'Jmax': [38.4, 202.0, 655.8], # 'Rd': [33.0] # }, 'Vcmax': 69.7, # Tarvainen et al. 2013 Oecologia 'Jmax': 130.2, 'Rd': 1.3, 'tresp': { 'Vcmax': [53.2, 200.0, 640.0], 'Jmax': [38.4, 200.0, 655.5], 'Rd': [33.0] }, 'alpha': gamma * 0.2, 'theta': 0.7, 'La': 1600.0, 'g1': gfact * 2.3, 'g0': 1.0e-3, 'kn': 0.6, 'beta': 0.95, 'drp': 0.7, } leafp = {'lt': 0.02, 'par_alb': 0.12, 'nir_alb': 0.55, 'emi': 0.98} if species.upper() == 'DECID': photop = { # 'Vcmax': 50.0, # 'Jmax': 95.0, # 'Rd': 1.3, # 'tresp': { # 'Vcmax': [77.0, 200.0, 636.7], # Medlyn et al 2002. # 'Jmax': [42.8, 200.0, 637.0], # 'Rd': [33.0] # }, 'Vcmax': 69.1, # Medlyn et al 2002. 'Jmax': 116.3, 'Rd': 1.3, 'tresp': { 'Vcmax': [77.0, 200.0, 636.4], 'Jmax': [42.8, 200.0, 636.6], 'Rd': [33.0] }, 'alpha': gamma * 0.2, 'theta': 0.7, 'La': 600.0, 'g1': gfact * 4.5, 'g0': 1.0e-3, 'kn': 0.6, 'beta': 0.95, 'drp': 0.7, } leafp = {'lt': 0.05, 'par_alb': 0.12, 'nir_alb': 0.55, 'emi': 0.98} if species.upper() == 'SHRUBS': photop = { 'Vcmax': 50.0, 'Jmax': 95.0, 'Rd': 1.3, 'alpha': gamma * 0.2, 'theta': 0.7, 'La': 600.0, 'g1': gfact * 4.5, 'g0': 1.0e-3, 'kn': 0.3, 'beta': 0.95, 'drp': 0.7, 'tresp': { 'Vcmax': [77.0, 200.0, 636.7], 'Jmax': [42.8, 200.0, 637.0], 'Rd': [33.0] } } leafp = {'lt': 0.02, 'par_alb': 0.12, 'nir_alb': 0.55, 'emi': 0.98} # env. conditions N = 50 P = 101300.0 Qp = 1000. * np.ones(N) # np.linspace(1.,1800.,50)# CO2 = 400. * np.ones(N) U = 1.0 # np.array([10.0, 1.0, 0.1, 0.01]) T = np.linspace(1., 39., 50) # 10. * np.ones(N) # esat, s = e_sat(T) H2O = (85.0 / 100.0) * esat / P SWabs = 0.5 * (1 - leafp['par_alb']) * Qp / PAR_TO_UMOL + 0.5 * ( 1 - leafp['nir_alb']) * Qp / PAR_TO_UMOL LWnet = -30.0 * np.ones(N) forcing = { 'h2o': H2O, 'co2': CO2, 'air_temperature': T, 'par_incident': Qp, 'sw_absorbed': SWabs, 'lw_net': LWnet, 'wind_speed': U, 'air_pressure': P } controls = {'photo_model': method, 'energy_balance': Ebal} x = leaf_interface(photop, leafp, forcing, controls) # print(x) Y = T plt.figure(5) plt.subplot(421) plt.plot(Y, x['net_co2'], 'o') plt.title('net_co2') plt.subplot(422) plt.plot(Y, x['transpiration'], 'o') plt.title('transpiration') plt.subplot(423) plt.plot(Y, x['net_co2'] + x['dark_respiration'], 'o') plt.title('co2 uptake') plt.subplot(424) plt.plot(Y, x['dark_respiration'], 'o') plt.title('dark_respiration') plt.subplot(425) plt.plot(Y, x['stomatal_conductance'], 'o') plt.title('stomatal_conductance') plt.subplot(426) plt.plot(Y, x['boundary_conductance'], 'o') plt.title('boundary_conductance') plt.subplot(427) plt.plot(Y, x['leaf_internal_co2'], 'o') plt.title('leaf_internal_co2') plt.subplot(428) plt.plot(Y, x['leaf_surface_co2'], 'o') plt.title('leaf_surface_co2') plt.tight_layout()
def photo_c3_bwb(photop, Qp, T, RH, ca, gb_c, gb_v, P=101300.0): """ Leaf gas-exchange by Farquhar-Ball-Woodrow-Berry model, as in standard Farquhar- model IN: photop - parameter dict with keys: Vcmax, Jmax, Rd, alpha, theta, La, tresp can be scalars or arrays. tresp - dictionary with keys: Vcmax, Jmax, Rd: temperature sensitivity parameters. OMIT key if no temperature adjustments for photoparameters. Qp - incident PAR at leaves (umolm-2s-1) Tleaf - leaf temperature (degC) rh - relative humidity at leaf temperature (-) ca - ambient CO2 (ppm) gb_c - boundary-layer conductance for co2 (mol m-2 s-1) gb_v - boundary-layer conductance for h2o (mol m-2 s-1) P - atm. pressure (Pa) OUT: An - net CO2 flux (umolm-2s-1) Rd - dark respiration (umolm-2s-1) fe - leaf transpiration rate (molm-2s-1) gs - stomatal conductance for CO2 (mol/m-2s-1) ci - leaf internal CO2 (ppm) cs - leaf surface CO2 (ppm) """ Tk = T + DEG_TO_KELVIN MaxIter = 50 # --- params ---- Vcmax = photop['Vcmax'] Jmax = photop['Jmax'] Rd = photop['Rd'] alpha = photop['alpha'] theta = photop['theta'] g1 = photop['g1'] # slope parameter g0 = photop['g0'] beta = photop['beta'] # --- CO2 compensation point ------- Tau_c = 42.75 * np.exp(37830 * (Tk - TN) / (TN * GAS_CONSTANT * Tk)) # ---- Kc & Ko (umol/mol), Rubisco activity for CO2 & O2 ------ Kc = 404.9 * np.exp(79430.0 * (Tk - TN) / (TN * GAS_CONSTANT * Tk)) Ko = 2.784e5 * np.exp(36380.0 * (Tk - TN) / (TN * GAS_CONSTANT * Tk)) if 'tresp' in photop: # adjust parameters for temperature tresp = photop['tresp'] Vcmax_T = tresp['Vcmax'] Jmax_T = tresp['Jmax'] Rd_T = tresp['Rd'] Vcmax, Jmax, Rd, Tau_c = photo_temperature_response( Vcmax, Jmax, Rd, Vcmax_T, Jmax_T, Rd_T, Tk) # --- model parameters k1_c, k2_c [umol/m2/s] Km = Kc * (1.0 + O2_IN_AIR / Ko) J = (Jmax + alpha * Qp - ((Jmax + alpha * Qp)**2.0 - (4 * theta * Jmax * alpha * Qp))**(0.5)) / (2 * theta) # --- iterative solution for cs and ci err = 9999.0 cnt = 1 cs = ca # leaf surface CO2 ci = 0.8 * ca # internal CO2 while err > 0.01 and cnt < MaxIter: # -- rubisco -limited rate Av = Vcmax * (ci - Tau_c) / (ci + Km) # -- RuBP -regeneration limited rate Aj = J / 4.0 * (ci - Tau_c) / (ci + 2.0 * Tau_c) #An = np.minimum(Av, Aj) - Rd # single limiting rate # co-limitation x = Av + Aj y = Av * Aj An = (x - (x**2.0 - 4.0 * beta * y)**0.5) / (2.0 * beta) - Rd An1 = np.maximum(An, 0.0) # bwb -scheme gs_opt = g0 + g1 * An1 / ((cs - Tau_c)) * RH gs_opt = np.maximum(g0, gs_opt) # gcut is the lower limit # CO2 supply cs = np.maximum(ca - An1 / gb_c, 0.5 * ca) # through boundary layer ci0 = ci ci = np.maximum(cs - An1 / gs_opt, 0.1 * ca) # through stomata err = max(abs(ci0 - ci)) cnt += 1 # when Rd > photo, assume stomata closed and ci == ca ix = np.where(An < 0) gs_opt[ix] = g0 ci[ix] = ca[ix] cs[ix] = ca[ix] gs_v = H2O_CO2_RATIO * gs_opt geff = (gb_v * gs_v) / (gb_v + gs_v) # molm-2s-1 esat, _ = e_sat(T) VPD = (1.0 - RH) * esat / P # mol mol-1 fe = geff * VPD # leaf transpiration rate return An, Rd, fe, gs_opt, ci, cs
def leaf_interface(photop, leafp, forcing, controls, df=1.0, dict_output=True, logger_info=''): r""" Entry-point to coupled leaf gas-exchange and energy balance functions. CALCULATES leaf photosynthesis (An), respiration (Rd), transpiration (E) and estimates of leaf temperature (Tl) and sensible heat fluxes (H) based onleaf energy balance equation coupled with leaf-level photosynthesis and stomatal control schemes. Energy balance is solved using Taylor's expansion (i.e isothermal net radiation -approximation) which eliminates need for iterations with radiation-sceme. Depending on choise of 'model', photosynthesis is calculated based on biochemical model of Farquhar et al. (1980) coupled with various stomatal control schemes (Medlyn, Ball-Woodrow-Berry, Hari, Katul-Vico et al.) In all these models, stomatal conductance (gs) is directly linked to An, either by optimal stomatal control principles or using semi-empirical models. Args: photop (dict): leaf gas-exchange parameters 'Vcmax': maximum carboxylation velocity [umolm-2s-1] 'Jmax': maximum rate of electron transport [umolm-2s-1] 'Rd': dark respiration rate [umolm-2s-1] 'alpha': quantum yield parameter [mol/mol] 'theta': co-limitation parameter of Farquhar-model 'La': stomatal parameter (Lambda, m, ...) depending on model 'g1': stomatal slope parameter 'g0': residual conductance for CO2 [molm-2s-1] 'kn': ?? not used ?? 'beta': co-limitation parameter of Farquhar-model 'drp': drought response parameters of Medlyn stomatal model and apparent Vcmax; list: [Rew_crit_g1, slope_g1, Rew_crit_appVcmax, slope_appVcmax] 'tresp' (dict): temperature sensitivity parameters 'Vcmax' (list): [Ha, Hd, dS]; activation energy [kJmol-1], deactivation energy [kJmol-1], entropy factor [J mol-1] 'Jmax' (list): [Ha, Hd, dS]; 'Rd' (list): [Ha]; activation energy [kJmol-1)] leafp (dict): leaf properties 'lt': leaf lengthscale [m] forcing (dict): 'h2o': water vapor mixing ratio (mol/mol) 'co2': carbon dioxide mixing ratio (ppm) 'air_temperature': ambient air temperature (degC) 'par_incident': incident PAR at leaves (umolm-2s-1) 'sw_absorbed': absorbed SW (PAR + NIR) at leaves (Wm-2) 'lw_net': net isothermal long-wave radiation (Wm-2) 'wind_speed': mean wind speed (m/s) 'air_pressure': ambient pressure (Pa) 'leaf_temperature': initial guess for leaf temperature (optional) 'average_leaf_temperature': leaf temperature used for computing LWnet (optional) 'radiative_conductance': radiative conductance used in computing LWnet (optional) controls (dict): 'photo_model' (str): photosysthesis model CO_OPTI (Vico et al., 2014) MEDLYN (Medlyn et al., 2011 with co-limitation Farquhar) MEDLYN_FARQUHAR BWB (Ball et al., 1987 with co-limitation Farquhar) others? 'energy_balance' (bool): True computes leaf temperature by solving energy balance dict_output (bool): True returns output as dict, False as separate arrays (optional) logger_info (str): optional OUTPUT: (dict): 'net_co2': net CO2 flux (umol m-2 leaf s-1) 'dark_respiration': CO2 respiration (umol m-2 leaf s-1) 'transpiration': H2O flux (transpiration) (mol m-2 leaf s-1) 'sensible_heat': sensible heat flux (W m-2 leaf) 'fr': non-isothermal radiative flux (W m-2) 'Tl': leaf temperature (degC) 'stomatal_conductance': stomatal conductance for H2O (mol m-2 leaf s-1) 'boundary_conductance': boundary layer conductance for H2O (mol m-2 leaf s-1) 'leaf_internal_co2': leaf internal CO2 mixing ratio (mol/mol) 'leaf_surface_co2': leaf surface CO2 mixing ratio (mol/mol) NOTE: Vectorized code can be used in multi-layer sense where inputs are vectors of equal length Samuli Launiainen LUKE 3/2011 - 5/2017 Last edit 15.11.2019 / SL """ # -- parameters ----- lt = leafp['lt'] T = np.array(forcing['air_temperature'], ndmin=1) H2O = np.array(forcing['h2o'], ndmin=1) Qp = forcing['par_incident'] P = forcing['air_pressure'] U = forcing['wind_speed'] CO2 = forcing['co2'] Ebal = controls['energy_balance'] model = controls['photo_model'] if Ebal: SWabs = np.array(forcing['sw_absorbed'], ndmin=1) LWnet = np.array(forcing['lw_net'], ndmin=1) Rabs = SWabs + LWnet # canopy nodes ic = np.where(abs(LWnet) > 0.0) if 'leaf_temperature' in forcing: Tl_ini = np.array(forcing['leaf_temperature'], ndmin=1).copy() else: Tl_ini = T.copy() Tl = Tl_ini.copy() Told = Tl.copy() if 'radiative_conductance' in forcing: gr = df * np.array(forcing['radiative_conductance'], ndmin=1) else: gr = np.zeros(len(T)) if 'average_leaf_temperature' in forcing: Tl_ave = np.array(forcing['average_leaf_temperature'], ndmin=1) else: Tl_ave = Tl.copy() # vapor pressure esat, s = e_sat(Tl) s = s / P # slope of esat, mol/mol / degC Dleaf = esat / P - H2O Lv = latent_heat(T) * MOLAR_MASS_H2O itermax = 20 err = 999.0 iter_no = 0 while err > 0.01 and iter_no < itermax: iter_no += 1 # boundary layer conductance gb_h, gb_c, gb_v = leaf_boundary_layer_conductance( U, lt, T, 0.5 * (Tl + Told) - T, P) Told = Tl.copy() if model.upper() == 'MEDLYN_FARQUHAR': An, Rd, fe, gs_opt, Ci, Cs = photo_c3_medlyn_farquhar(photop, Qp, Tl, Dleaf, CO2, gb_c, gb_v, P=P) if model.upper() == 'BWB': rh = (1 - Dleaf * P / esat) # rh at leaf (-) An, Rd, fe, gs_opt, Ci, Cs = photo_c3_bwb(photop, Qp, Tl, rh, CO2, gb_c, gb_v, P=P) # --- analytical co-limitation model Vico et al. 2013 if model.upper() == 'CO_OPTI': An, Rd, fe, gs_opt, Ci, Cs = photo_c3_analytical( photop, Qp, Tl, Dleaf, CO2, gb_c, gb_v) gsv = H2O_CO2_RATIO * gs_opt # geff_v = (gb_v*gsv) / (gb_v + gsv) geff_v = np.where( Dleaf > 0.0, (gb_v * gsv) / (gb_v + gsv), df * gb_v) # molm-2s-1, condensation only on dry leaf part gb_h = df * gb_h # sensible heat exchange only through dry leaf part # solve leaf temperature from energy balance if Ebal: Tl[ic] = (Rabs[ic] + SPECIFIC_HEAT_AIR * gr[ic] * Tl_ave[ic] + SPECIFIC_HEAT_AIR * gb_h[ic] * T[ic] - Lv[ic] * geff_v[ic] * Dleaf[ic] + Lv[ic] * s[ic] * geff_v[ic] * Told[ic]) / ( SPECIFIC_HEAT_AIR * (gr[ic] + gb_h[ic]) + Lv[ic] * s[ic] * geff_v[ic]) err = np.nanmax(abs(Tl - Told)) if (err < 0.01 or iter_no == itermax) and abs(np.mean(T) - np.mean(Tl)) > 20.0: logger.debug( logger_info + ' Unrealistic leaf temperature %.2f set to air temperature %.2f, %.2f, %.2f, %.2f, %.2f', np.mean(Tl), np.mean(T), np.mean(LWnet), np.mean(Tl_ave), np.mean(Tl_ini), np.mean(H2O)) Tl = T.copy() Ebal = False # recompute without solving leaf temperature err = 999. elif iter_no == itermax and err > 0.05: logger.debug( logger_info + ' Maximum number of iterations reached: Tl = %.2f (err = %.2f)', np.mean(Tl), err) # vapor pressure esat, s = e_sat(Tl) s = s / P # slope of esat, mol/mol / degC # s[esat / P < H2O] = EPS # Dleaf = np.maximum(EPS, esat / P - H2O) # mol/mol Dleaf = esat / P - H2O else: err = 0.0 # outputs H = SPECIFIC_HEAT_AIR * gb_h * (Tl - T) # Wm-2 Fr = SPECIFIC_HEAT_AIR * gr * ( Tl - Tl_ave) # flux due to radiative conductance (Wm-2) E = geff_v * np.maximum( 0.0, Dleaf) # condensation accounted for in wetleaf water balance LE = E * Lv # condensation accounted for in wetleaf energy balance if dict_output: # return dict x = { 'net_co2': An, 'dark_respiration': Rd, 'transpiration': E, 'sensible_heat': H, 'latent_heat': LE, 'fr': Fr, 'leaf_temperature': Tl, 'stomatal_conductance': np.minimum(gsv, 1.0), # gsv gets high when VPD->0 'boundary_conductance': gb_v, 'leaf_internal_co2': Ci, 'leaf_surface_co2': Cs } return x else: # return 11 arrays return An, Rd, E, H, Fr, Tl, Ci, Cs, gsv, gs_opt, gb_v
def create_forcingfile(meteo_file, output_file, lat, lon, P_unit, timezone=+2.0): """ Create forcing file from meteo. Args: meteo_file (str): name of file with meteo (.csv not included) output_file (str): name of output file (.csv not included) lat (float): latitude lon (float): longitude P_unit (float): unit conversion needed to get to [Pa] """ from canopy.radiation import solar_angles, compute_clouds_rad from canopy.micromet import e_sat fpar = 0.45 forc_fp = direc + "forcing/" + meteo_file + ".csv" dat = pd.read_csv(forc_fp, sep=',', header='infer', encoding='ISO-8859-1') # set to dataframe index dat.index = pd.to_datetime({ 'year': dat['yyyy'], 'month': dat['mo'], 'day': dat['dd'], 'hour': dat['hh'], 'minute': dat['mm'] }) readme = '' cols = [] # day of year dat['doy'] = dat.index.dayofyear cols.append('doy') readme += "\ndoy: Day of year [days]" # precipitaion unit from [mm/dt] to [m/s] dt = (dat.index[1] - dat.index[0]).total_seconds() dat['Prec'] = dat['Prec'] * 1e-3 / dt cols.append('Prec') readme += "\nPrec: Precipitation [m/s]" # atm. pressure unit from [XPa] to [Pa] dat['P'] = dat['P'] * P_unit cols.append('P') readme += "\nP: Ambient pressure [Pa]" # air temperature: instant and daily [degC] cols.append('Tair') readme += "\nTair: Air temperature [degC]" # dat['Tdaily'] = dat['Tair'].rolling(int((24*3600)/dt), 1).mean() dat['Tdaily'] = dat['Tair'].resample('D').mean() dat['Tdaily'] = dat['Tdaily'].fillna(method='ffill') cols.append('Tdaily') readme += "\nTdaily: Daily air temperature [degC]" # wind speend and friction velocity cols.append('U') readme += "\nU: Wind speed [m/s]" cols.append('Ustar') readme += "\nUstar: Friction velocity [m/s]" # ambient H2O [mol/mol] from RH esat, _ = e_sat(dat['Tair']) dat['H2O'] = (dat['RH'] / 100.0) * esat / dat['P'] cols.append('H2O') readme += "\nH2O: Ambient H2O [mol/mol]" # ambient CO2 [ppm] readme += "\nCO2: Ambient CO2 [ppm]" if 'CO2' not in dat: dat['CO2'] = 400.0 readme += " - set constant!" cols.append('CO2') # zenith angle jday = dat.index.dayofyear + dat.index.hour / 24.0 + dat.index.minute / 1440.0 # TEST (PERIOD START) jday = dat.index.dayofyear + dat.index.hour / 24.0 + dat.index.minute / 1440.0 + dt / 2.0 / 86400.0 dat['Zen'], _, _, _, _, _ = solar_angles(lat, lon, jday, timezone=timezone) cols.append('Zen') readme += "\nZen: Zenith angle [rad], (lat = %.2f, lon = %.2f)" % (lat, lon) # radiation components if {'LWin', 'diffPar', 'dirPar', 'diffNir', 'dirNir'}.issubset(dat.columns) == False: f_cloud, f_diff, emi_sky = compute_clouds_rad( dat['doy'].values, dat['Zen'].values, dat['Rg'].values, dat['H2O'].values * dat['P'].values, dat['Tair'].values) if 'LWin' not in dat or dat['LWin'].isnull().any(): if 'LWin' not in dat: dat['LWin'] = np.nan print('Longwave radiation estimated') else: print('Longwave radiation partly estimated') # Downwelling longwve radiation # solar constant at top of atm. So = 1367 # clear sky Global radiation at surface dat['Qclear'] = np.maximum( 0.0, (So * (1.0 + 0.033 * np.cos(2.0 * np.pi * (np.minimum(dat['doy'].values, 365) - 10) / 365)) * np.cos(dat['Zen'].values))) tau_atm = tau_atm = dat['Rg'].rolling( 4, 1).sum() / (dat['Qclear'].rolling(4, 1).sum() + EPS) # cloud cover fraction dat['f_cloud'] = 1.0 - (tau_atm - 0.2) / (0.7 - 0.2) dat['f_cloud'][dat['Qclear'] < 10] = np.nan dat['Qclear_12h'] = dat['Qclear'].resample('12H').sum() dat['Qclear_12h'] = dat['Qclear_12h'].fillna(method='ffill') dat['Rg_12h'] = dat['Rg'].resample('12H').sum() dat['Rg_12h'] = dat['Rg_12h'].fillna(method='ffill') tau_atm = dat['Rg_12h'] / (dat['Qclear_12h'] + EPS) dat['f_cloud_12h'] = 1.0 - (tau_atm - 0.2) / (0.7 - 0.2) dat['f_cloud'] = np.where( (dat.index.hour > 12) & (dat['f_cloud_12h'] < 0.2), 0.0, dat['f_cloud']) dat['f_cloud'] = dat['f_cloud'].fillna(method='ffill') dat['f_cloud'] = dat['f_cloud'].fillna(method='bfill') dat['f_cloud'][dat['f_cloud'] < 0.0] = 0.0 dat['f_cloud'][dat['f_cloud'] > 1.0] = 1.0 emi0 = 1.24 * (dat['H2O'].values * dat['P'].values / 100 / (dat['Tair'].values + 273.15))**(1. / 7.) emi_sky = (1 - 0.84 * dat['f_cloud']) * emi0 + 0.84 * dat['f_cloud'] # estimated long wave budget b = 5.6697e-8 # Stefan-Boltzman constant (W m-2 K-4) dat['LWin_estimated'] = emi_sky * b * ( dat['Tair'] + 273.15)**4 # Wm-2 downwelling LW dat[['LWin', 'LWin_estimated']].plot(kind='line') dat['LWin'] = np.where(np.isfinite(dat['LWin']), dat['LWin'], dat['LWin_estimated']) cols.append('LWin') readme += "\nLWin: Downwelling long wave radiation [W/m2]" # Short wave radiation; separate direct and diffuse PAR & NIR if {'diffPar', 'dirPar', 'diffNir', 'dirNir'}.issubset(dat.columns) == False: print('Shortwave radiation components estimated') dat['diffPar'] = f_diff * fpar * dat['Rg'] dat['dirPar'] = (1 - f_diff) * fpar * dat['Rg'] dat['diffNir'] = f_diff * (1 - fpar) * dat['Rg'] dat['dirNir'] = (1 - f_diff) * (1 - fpar) * dat['Rg'] cols.extend(('diffPar', 'dirPar', 'diffNir', 'dirNir')) readme += "\ndiffPar: Diffuse PAR [W/m2] \ndirPar: Direct PAR [W/m2]" readme += "\ndiffNir: Diffuse NIR [W/m2] \ndirNir: Direct NIR [W/m2]" if {'Tsoil', 'Wliq'}.issubset(dat.columns): cols.extend(('Tsoil', 'Wliq')) dat['Wliq'] = dat['Wliq'] / 100.0 readme += "\nTsoil: Soil surface layer temperature [degC]]" readme += "\nWliq: Soil surface layer moisture content [m3 m-3]" X = np.zeros(len(dat)) DDsum = np.zeros(len(dat)) for k in range(1, len(dat)): if dat['doy'][k] != dat['doy'][k - 1]: X[k] = X[k - 1] + 1.0 / 8.33 * (dat['Tdaily'][k - 1] - X[k - 1]) if dat['doy'][k] == 1: # reset in the beginning of the year DDsum[k] = 0. else: DDsum[k] = DDsum[k - 1] + max(0.0, dat['Tdaily'][k - 1] - 5.0) else: X[k] = X[k - 1] DDsum[k] = DDsum[k - 1] dat['X'] = X cols.append('X') readme += "\nX: phenomodel delayed temperature [degC]" dat['DDsum'] = DDsum cols.append('DDsum') readme += "\nDDsum: degreedays [days]" dat = dat[cols] dat[cols].plot(subplots=True, kind='line') print("NaN values in forcing data:") print(dat.isnull().any()) save_df_to_csv(dat, output_file, readme=readme, fp=direc + "forcing/")
def leaf_gas_exchange(self, forcing, controls, leaftype): r""" Solves leaf gas-exchange and energy balance (optionally). Energy balance is solved using Taylor's expansion (i.e isothermal net radiation -approximation) which eliminates need for iterations with radiation-scheme. Args: forcing (dict): 'h2o': water vapor mixing ratio (mol/mol) 'co2': carbon dioxide mixing ratio (ppm) 'air_temperature': ambient air temperature (degC) 'par_incident': incident PAR at leaves (umolm-2s-1) 'sw_absorbed': absorbed SW (PAR + NIR) at leaves (Wm-2) 'lw_net': net isothermal long-wave radiation (Wm-2) 'wind_speed': mean wind speed (m/s) 'air_pressure': ambient pressure (Pa) 'leaf_temperature': initial guess for leaf temperature (optional) 'average_leaf_temperature': leaf temperature used for computing LWnet (optional) 'radiative_conductance': radiative conductance used in computing LWnet (optional) controls (dict): 'energy_balance' (bool): True computes leaf temperature by solving energy balance 'logger_info' (str) leaftype (str): 'sunlit' / 'shaded' Returns: (dict): 'net_co2': net CO2 flux (umol m-2 leaf s-1) 'dark_respiration': CO2 respiration (umol m-2 leaf s-1) 'transpiration': H2O flux (transpiration) (mol m-2 leaf s-1) 'sensible_heat': sensible heat flux (W m-2 leaf) 'fr': non-isothermal radiative flux (W m-2) 'Tl': leaf temperature (degC) 'stomatal_conductance': stomatal conductance for H2O (mol m-2 leaf s-1) 'boundary_conductance': boundary layer conductance for H2O (mol m-2 leaf s-1) 'leaf_internal_co2': leaf internal CO2 mixing ratio (mol/mol) 'leaf_surface_co2': leaf surface CO2 mixing ratio (mol/mol) Samuli Launiainen & Kersti Haahti, Last edit 25.11.2019 / SL """ Ebal = controls['energy_balance'] logger_info = controls['logger_info'] + 'leaftype: ' + leaftype # -- unpack forcing T = np.array(forcing['air_temperature'], ndmin=1) H2O = np.array(forcing['h2o'], ndmin=1) P = forcing['air_pressure'] U = forcing['wind_speed'] CO2 = forcing['co2'] # incident PAR at leaftype Qp = forcing['par'][leaftype]['incident'] * PAR_TO_UMOL # umolm-2s-1 # solve energy balance iteratively if Ebal: SWabs = forcing['par'][leaftype]['absorbed'] + forcing['nir'][ leaftype]['absorbed'] LWnet = forcing['lw']['net_leaf'] Rabs = SWabs + LWnet gr = forcing['lw']['radiative_conductance'] Tl_ave = forcing[ 'average_leaf_temperature'] # layer mean leaf temperature # initial guess for leaf temperature if leaftype is 'sunlit': Tl_ini = self.Tl_sl if leaftype is 'shaded': Tl_ini = self.Tl_sh # canopy nodes ic = np.where(abs(LWnet) > 0.0) Tl = Tl_ini.copy() Told = Tl.copy() # vapor pressure esat, s = e_sat(Tl) s = s / P # slope of esat, mol/mol / degC Dleaf = esat / P - H2O Lv = latent_heat(T) * MOLAR_MASS_H2O itermax = 20 err = 999.0 iter_no = 0 while err > 0.01 and iter_no < itermax: iter_no += 1 Told = Tl.copy() # boundary layer conductance gb_h, gb_c, gb_v = leaf_boundary_layer_conductance( U, self.leafp['lt'], T, 0.5 * (Tl + Told) - T, P) # solve leaf gas-exchange An, Rd, fe, gs_opt, Ci, Cs = photo_c3_medlyn_farquhar( self.photop, Qp, Tl, Dleaf, CO2, gb_c, gb_v, P=P) gsv = H2O_CO2_RATIO * gs_opt geff_v = np.where(Dleaf > 0.0, (gb_v * gsv) / (gb_v + gsv), gb_v) # molm-2s-1 # solve leaf temperature from energy balance Tl[ic] = (Rabs[ic] + SPECIFIC_HEAT_AIR * gr[ic] * Tl_ave[ic] + SPECIFIC_HEAT_AIR * gb_h[ic] * T[ic] - Lv[ic] * geff_v[ic] * Dleaf[ic] + Lv[ic] * s[ic] * geff_v[ic] * Told[ic]) / ( SPECIFIC_HEAT_AIR * (gr[ic] + gb_h[ic]) + Lv[ic] * s[ic] * geff_v[ic]) err = np.nanmax(abs(Tl - Told)) if (err < 0.01 or iter_no == itermax) and abs(np.mean(T) - np.mean(Tl)) > 20.0: logger.debug( logger_info + ' Unrealistic leaf temperature %.2f set to air temperature %.2f, %.2f, %.2f, %.2f, %.2f', np.mean(Tl), np.mean(T), np.mean(LWnet), np.mean(Tl_ave), np.mean(Tl_ini), np.mean(H2O)) Tl = T.copy() Ebal = False # recompute without solving leaf temperature err = 999. elif iter_no == itermax and err > 0.05: logger.debug( logger_info + ' Maximum number of iterations reached: Tl = %.2f (err = %.2f)', np.mean(Tl), err) # vapor pressure esat, s = e_sat(Tl) s = s / P # slope of esat, mol/mol / degC Dleaf = esat / P - H2O H = SPECIFIC_HEAT_AIR * gb_h * (Tl - T) # Wm-2 Fr = SPECIFIC_HEAT_AIR * gr * ( Tl - Tl_ave) # flux due to radiative conductance (Wm-2) E = geff_v * np.maximum( 0.0, Dleaf ) # mol m-2 s-1, condensation accounted for in wetleaf water balance LE = E * Lv # W m-2 else: # or assume leaves are at air temperature Tl = T.copy() esat, s = e_sat(Tl) s = s / P # slope of esat, mol/mol / degC Dleaf = esat / P - H2O Lv = latent_heat(T) * MOLAR_MASS_H2O # boundary-layer conductances mol m-2 s-1 dT = 0.0 gb_h, gb_c, gb_v = leaf_boundary_layer_conductance( U, self.leafp['lt'], T, dT, P) # solve leaf gas-exchange An, Rd, fe, gs_opt, Ci, Cs = photo_c3_medlyn_farquhar(self.photop, Qp, Tl, Dleaf, CO2, gb_c, gb_v, P=P) gsv = H2O_CO2_RATIO * gs_opt geff_v = np.where(Dleaf > 0.0, (gb_v * gsv) / (gb_v + gsv), gb_v) # molm-2s-1 H = 0.0 Fr = 0.0 # flux due to radiative conductance (Wm-2) E = geff_v * np.maximum( 0.0, Dleaf ) # mol m-2 s-1, condensation accounted for in wetleaf water balance LE = E * Lv # W m-2 # prepare output dict x = { 'net_co2': An, 'dark_respiration': Rd, 'transpiration': E, 'sensible_heat': H, 'latent_heat': LE, 'fr': Fr, 'leaf_temperature': Tl, 'stomatal_conductance': np.minimum(gsv, 1.0), # gsv gets high when VPD->0 'boundary_conductance': gb_v, 'leaf_internal_co2': Ci, 'leaf_surface_co2': Cs } return x
def heat_balance(forcing, parameters, controls, properties, temperature): r""" Solves bare soil surface temperature Uses linearized energy balance equation from soil conditions from previous timestep Args: forcing (dict): 'wind_speed': [m s-1] 'air_temperature': [degC] 'h2o': [mol mol\ :sup:`-1`\ ] 'air_pressure': [Pa] 'forestfloor_temperature': [degC] 'soil_tempereture': [degC] 'soil_water_potential': [m] 'par': [W m-2] 'nir': [W m-2] 'lw_dn': [W m-2] 'lw_up': [W m-2] parameters (dict): 'soil_hydraulic_conductivity': [m s-1] 'soil_thermal_conductivity': [] 'height': [m] height to the first canopy calculation node 'soil_depth': [m] depth to the first soil calculation node controls (dict): 'energy_balance': boolean properties (dict): 'optical_properties': 'emissivity' 'albedo_PAR' 'albedo_NIR' """ U = forcing['wind_speed'] T = forcing['air_temperature'] P = forcing['air_pressure'] T_ave = forcing['forestfloor_temperature'] T_soil = forcing['soil_temperature'] h_soil = forcing['soil_water_potential'] z_soil = parameters['soil_depth'] Kt = parameters['soil_thermal_conductivity'] Kh = parameters['soil_hydraulic_conductivity'] soil_emi = properties['optical_properties']['emissivity'] # radiative conductance [mol m-2 s-1] gr = 4.0 * soil_emi * STEFAN_BOLTZMANN * T_ave**3 / SPECIFIC_HEAT_AIR if controls['energy_balance']: # energy balance switch albedo_par = properties['optical_properties']['albedo_PAR'] albedo_nir = properties['optical_properties']['albedo_NIR'] # absorbed shortwave radiation SW_gr = (1 - albedo_par) * forcing['par'] + ( 1 - albedo_nir) * forcing['nir'] # net longwave radiation LWn = forcing['lw_dn'] - forcing['lw_up'] # initial guess for surface temperature surface_temperature = temperature else: SW_gr, LWn = 0.0, 0.0 # set temperature to average of soil and air surface_temperature = forcing['air_temperature'] # geometric mean of air_temperature and soil_temperature # surface_temperature = ( # np.power(forcing['air_temperature'] * forcing['soil_temperature'], 0.5) # ) dz_soil = -z_soil # change this either to baresoil temperature or baresoil old_temperature # # boundary layer conductances for forcing['h2o'] and heat [mol m-2 s-1] # gb_h, _, gb_v = soil_boundary_layer_conductance( # u=forcing['wind_speed'], # z=parameters['height'], # zo=properties['roughness_length'], # Ta=forcing['air_temperature'], # dT=0.0, # P=forcing['air_pressure'] # ) # OK to assume dt = 0.0? atm_conductance = surface_atm_conductance( wind_speed=forcing['wind_speed'], height=parameters['height'], friction_velocity=forcing['friction_velocity'], dT=0.0) gb_v = atm_conductance['h2o'] gb_h = atm_conductance['heat'] # Maximum LE # atm pressure head in equilibrium with atm. relative humidity es_a, _ = e_sat(T) RH = min(1.0, forcing['h2o'] * P / es_a) # air relative humidity above ground [-] h_atm = GAS_CONSTANT * (DEG_TO_KELVIN + T) * np.log(RH) / ( MOLAR_MASS_H2O * GRAVITY) # [m] # maximum latent heat flux constrained by h_atm LEmax = max(0.0, -LATENT_HEAT * Kh * (h_atm - h_soil - z_soil) / dz_soil * WATER_DENSITY / MOLAR_MASS_H2O) # [W/m2] # LE demand # vapor pressure deficit between leaf and air, and slope of vapor pressure curve at T es, s = e_sat(surface_temperature) Dsurf = es / P - forcing['h2o'] # [mol/mol] - allows condensation s = s / P # [mol/mol/degC] LE = LATENT_HEAT * gb_v * Dsurf if LE > LEmax: LE = LEmax s = 0.0 """ --- solve surface temperature --- """ itermax = 20 err = 999.0 iterNo = 0 while err > 0.01 and iterNo < itermax: iterNo += 1 Told = surface_temperature if controls['energy_balance']: # solve surface temperature [degC] surface_temperature = ( (SW_gr + LWn + SPECIFIC_HEAT_AIR * gr * T_ave + SPECIFIC_HEAT_AIR * gb_h * T - LE + LATENT_HEAT * s * gb_v * Told + Kt / dz_soil * T_soil) / (SPECIFIC_HEAT_AIR * (gr + gb_h) + LATENT_HEAT * s * gb_v + Kt / dz_soil)) err = abs(surface_temperature - Told) es, s = e_sat(surface_temperature) Dsurf = es / P - forcing['h2o'] # [mol/mol] - allows condensation s = s / P # [mol/mol/degC] LE = LATENT_HEAT * gb_v * Dsurf if LE > LEmax: LE = LEmax s = 0.0 if iterNo == itermax: logger.debug( 'Maximum number of iterations reached: T_baresoil = %.2f, err = %.2f', surface_temperature, err) else: err = 0.0 if (abs(surface_temperature - temperature) > 20 or np.isnan(surface_temperature) ): # into iteration loop? chech photo or interception logger.debug( 'Unrealistic baresoil temperature %.2f set to previous value %.2f: %.5f,%.5f,%.5f,%.5f,%.5f,%.5f,%.5f,%.5f,%.5f', surface_temperature, temperature, U, T, forcing['h2o'], P, T_ave, T_soil, h_soil, SW_gr, LWn) surface_temperature = temperature es, s = e_sat(surface_temperature) Dsurf = es / P - forcing['h2o'] # [mol/mol] - allows condensation LE = LATENT_HEAT * gb_v * Dsurf if LE > LEmax: LE = LEmax """ --- energy and water fluxes --- """ # sensible heat flux [W m-2] Hw = SPECIFIC_HEAT_AIR * gb_h * (surface_temperature - T) # non-isothermal radiative flux [W m-2] Frw = SPECIFIC_HEAT_AIR * gr * (surface_temperature - T_ave) # ground heat flux [W m-2] Gw = Kt / dz_soil * (surface_temperature - T_soil) # evaporation rate [mol m-2 s-1] Ep = LE / LATENT_HEAT #gb_v * Dsurf # energy closure closure = SW_gr + LWn - Hw - LE - Gw fluxes = { 'latent_heat': LE, 'energy_closure': closure, 'evaporation': Ep, 'radiative_flux': Frw, 'sensible_heat': Hw, 'ground_heat': Gw } states = {'temperature': surface_temperature} return states, fluxes