def cal_cp(qa_kgkg, ta_K, pres_hPa): from atmosp import calculate as ac temp_C = ta_K - 273.15 rh_pct = ac("RH", qv=qa_kgkg, T=ta_K, p=pres_hPa * 100) # Garratt equation a20(1992) cpd = 1005.0 + ((temp_C + 23.16)**2) / 3364.0 # Beer(1990) for water vapour cpm = (1859 + 0.13 * rh_pct + (19.3 + 0.569 * rh_pct) * (temp_C / 100.0) + (10.0 + 0.5 * rh_pct) * (temp_C / 100.0)**2) # air density rho = ac("rho", qv=qa_kgkg, T=ta_K, p=pres_hPa * 100) # water vapour mixing ratio rv = ac("rv", qv=qa_kgkg, T=ta_K, p=pres_hPa * 100) # dry air density rho_d = rv / (1 + rv) * rho # water vapour density rho_v = rho - rho_d # heat capacity of air cp = cpd * (rho_d / (rho_d + rho_v)) + cpm * (rho_v / (rho_d + rho_v)) return cp
def cal_dq(rh_pct, ta_c, pres_hPa): from atmosp import calculate as ac ta_k = ta_c + 273.16 pa = pres_hPa * 100 dq = ac("qvs", T=ta_k, p=pa) - ac("qv", T=ta_k, p=pa, RH=rh_pct) return dq
def cal_rs_iPM(qh, qe, ta, rh, pa, ra): """Calculate surface resistance based on observations, notably turbulent fluxes. Parameters ---------- qh : numeric sensible heat flux [W m-2] qe : numeric latent heat flux [W m-2] ta : numeric air temperature [degC] rh : numeric relative humidity [%] pa : numeric air pressure [Pa] ra : numeric aerodynamic resistance [m s-1] Returns ------- numeric Surface resistance based on observations [s m-1] """ from atmosp import calculate as ac # psychrometric constant [Pa K-1] as a function of air pressure ser_gamma = 0.665e-3 * pa # air density [kg m-3] val_rho = 1.27 # heat capacity of air [J kg-1 K-1] val_cp = 1005 # convert temp from C to K ta_K = ta + 273.15 # slope of es(Ta) curve at Ta ser_des_dTa = cal_des_dta(ta_K, pa, dta=1.0) # arr_e = ac("e", p=pa, T=ta_K, RH=rh) arr_es = ac("es", p=pa, T=ta_K) arr_vpd = arr_es - arr_e # ser_rs_1 = (ser_des_dTa / ser_gamma) * (qh / qe - 1) * ra ser_rs_2 = val_rho * val_cp * arr_vpd / (ser_gamma * qe) ser_rs = ser_rs_1 + ser_rs_2 try: # try to pack as Series ser_rs = pd.Series(ser_rs, index=ta_K.index) except AttributeError as ex: print(ex, "cannot pack into pd.Series") pass return ser_rs
def cal_vpd(Temp_C, RH_pct, Press_hPa): ta = Temp_C + 273.16 pa = Press_hPa * 100 rh = RH_pct e = ac('e', p=pa, T=ta, RH=rh) es = ac('es', p=pa, T=ta) vpd = es - e des_vpd = pd.Series(vpd, index=ta.index) return des_vpd
def cal_rs_obs(qh, qe, ta, rh, pa): """Calculate surface resistance based on observations, notably turbulent fluxes. Parameters ---------- qh : numeric sensible heat flux [W m-2] qe : numeric latent heat flux [W m-2] ta : numeric air temperature [K] rh : numeric relative humidity [%] pa : numeric air pressure [Pa] Returns ------- numeric Surface resistance based on observations [s m-1] """ # surface resistance at water surface [s m-1] rav = 50 # psychrometric constant [Pa K-1] as a function of air pressure ser_gamma = 0.665e-3 * pa # air density [kg m-3] val_rho = 1.27 # heat capacity of air [J kg-1 K-1] val_cp = 1005 # slope of es(Ta) curve at Ta ser_des_dTa = cal_des_dta(ta, pa, dta=1.0) # arr_e = ac('e', p=pa, T=ta, RH=rh) arr_es = ac('es', p=pa, T=ta) arr_vpd = arr_es-arr_e # ser_rs_1 = (ser_des_dTa / ser_gamma) * (qh / qe - 1) * rav ser_rs_2 = (val_rho * val_cp * arr_vpd / (ser_gamma * qe)) ser_rs = ser_rs_1 + ser_rs_2 try: # try to pack as Series ser_rs = pd.Series(ser_rs, index=ta.index) except AttributeError as ex: print(ex, 'cannot pack into pd.Series') pass return ser_rs
def cal_des_dta(Temp_C, Press_hPa, dta=1.0): ta = Temp_C + 273.16 pa = Press_hPa * 100 des = ac('es', p=pa, T=ta + dta / 2) - ac('es', p=pa, T=ta - dta / 2) des_dta = des / dta try: # try to pack as Series des_dta = pd.Series(des_dta, index=ta.index) except AttributeError as ex: print(ex, 'cannot pack into pd.Series') pass return des_dta
def cal_lat_vap(qa_kgkg, theta_K, pres_hPa): from atmosp import calculate as ac # wel-bulb temperature tw = ac("Tw", qv=qa_kgkg, p=pres_hPa, theta=theta_K, remove_assumptions=("constant Lv")) # latent heat [J kg-1] Lv = 2.501e6 - 2370.0 * (tw - 273.15) return Lv
def cal_des_dta(ta, pa, dta=1.0): """Calculate slope of es(Ta), i.e., saturation evaporation pressure `es` as function of air temperature `ta [K]` Parameters ---------- ta : numeric Air temperature [K] pa : numeric Air pressure [Pa] dta : float, optional change in ta for calculating that in es, by default 1.0 K """ des = ac('es', p=pa, T=ta + dta / 2) - ac('es', p=pa, T=ta - dta / 2) des_dta = des / dta try: # try to pack as Series des_dta = pd.Series(des_dta, index=ta.index) except AttributeError as ex: print(ex, 'cannot pack into pd.Series') pass return des_dta
def diag_era5_simple(z0m, ustar, pres_z0, uv10, t2, q2, z): from atmosp import calculate as ac import xarray as xr # constants # environmental lapse rate [K m^-1] env_lapse = 6.5 / 1000.0 # gravity [m s^-2] grav = 9.80616 # Gas constant for dry air [J K^-1 kg^-1] rd = 287.04 # correct temperature using lapse rate t_z = t2 - (z - 2) * env_lapse # barometric equation with varying temperature: # (https://en.wikipedia.org/wiki/Barometric_formula) # p_z = pres_z0 * np.exp((grav * (0 - z)) / (rd * t2)) p_z = pres_z0 * (t2 / (t2 + env_lapse * (z - 2)))**(grav / (rd * env_lapse)) # correct humidity assuming invariable relative humidity RH_z = ac("RH", qv=q2, p=pres_z0, T=t2) + 0 * t_z q_z = ac("qv", RH=RH_z, p=p_z, T=t_z) + 0 * t_z # correct wind speed using log law; assuming neutral condition (without stability correction) uv_z = uv10 * (np.log((z + z0m) / z0m) / np.log((10 + z0m) / z0m)) # generate dataset ds_diag = xr.merge([ uv_z.rename("uv_z"), t_z.rename("t_z"), q_z.rename("q_z"), RH_z.rename("RH_z"), p_z.rename("p_z"), ]) return ds_diag
def diag_era5(za, uv_za, t_za, q_za, pres_za, qh, qe, z0m, ustar, pres_z0, uv10, t2, q2, z): # reference: # Section 3.10.2 and 3.10.3 in # IFS Documentation CY41R2: Part IV: Physical Processes # https://www.ecmwf.int/en/elibrary/16648-part-iv-physical-processes from atmosp import calculate as ac import xarray as xr from ._atm import cal_lat_vap, cal_cp, cal_psi_mom, cal_psi_heat # von Karman constant kappa = 0.4 # gravity acceleration g = 9.8 # note the roughness correction: see EC technical report z0m = np.where(z0m < 0.03, z0m, 0.03) # air density avdens = ac("rho", qv=q2, p=pres_z0, theta=t2) # vapour pressure # lv_j_kg = cal_lat_vap(q2, t2, pres_z0) # heat capacity avcp = cal_cp(q2, t2, pres_z0) # temperature/humidity scales tstar = -qh / (avcp * avdens) / ustar # qstar = -qe / (lv_j_kg * avdens) / ustar l_mod = ustar**2 / (g / t2 * kappa * tstar) zoL = np.where( np.abs((z + z0m) / l_mod) < 5, (z + z0m) / l_mod, np.sign((z + z0m) / l_mod) * 5, ) # `stab_psi_mom`, `stab_psi_heat` # stability correction for momentum psim_z = cal_psi_mom(zoL) psim_z0 = cal_psi_mom(z0m / l_mod) psim_10 = cal_psi_mom((10 + z0m) / l_mod) # wind speed uv_z = uv10 * ((np.log((z + z0m) / z0m) - psim_z + psim_z0) / (np.log( (10 + z0m) / z0m) - psim_10 + psim_z0)) # stability correction for heat psih_z = cal_psi_heat(zoL) psih_2 = cal_psi_heat(2 / l_mod) psih_z0 = cal_psi_heat(z0m / l_mod) psih_za = cal_psi_heat(za / l_mod) # atmospheric pressure: assuming same air density at `za` # using iteration to get `p_z` p_z = pres_z0 + (pres_za - pres_z0) * z / za # specific humidity q_z = q2 + (q_za - q2) * ((np.log(z / z0m) - psih_z + psih_z0) / (np.log(za / z0m) - psih_za + psih_z0)) # dry static energy: eq 3.5 in EC tech report; # also AMS ref: http://glossary.ametsoc.org/wiki/Dry_static_energy # 2 m agl: cp2 = cal_cp(q2, t2, pres_z0 / 100) s2 = g * 2 + cp2 * t2 # za: cp_za = cal_cp(q_za, t_za, pres_za / 100) s_za = g * za + cp_za * t_za s_z = s2 + (s_za - s2) * ((np.log(z / 2) - psih_z + psih_2) / (np.log(za / 2) - psih_za + psih_2)) # calculate temperature at z tx_z = t_za dif = 10 while dif > 0.1: cp_z = cal_cp(q_z, tx_z, p_z / 100) t_z = (s_z - g * z) / cp_z dif = np.mean(np.abs(t_z - tx_z)) tx_z = t_z # get final `t_z` and store in the conformity to `t_za` t_z = tx_z + t_za * 0 # relative humidity; cap at 105% if above RH_z = ac("RH", qv=q_z, p=p_z, T=t_z) + 0 * q_z RH_z = RH_z.where(RH_z < 105, 105) # generate dataset ds_diag = xr.merge([ uv_z.rename("uv_z"), t_z.rename("t_z"), q_z.rename("q_z"), RH_z.rename("RH_z"), p_z.rename("p_z"), ]) return ds_diag
def gen_ds_diag_era5(list_fn_sfc, list_fn_ml, hgt_agl_diag=100, simple_mode=True): import xarray as xr from atmosp import calculate as ac # load data from from `sfc` files ds_sfc = xr.open_mfdataset(list_fn_sfc, concat_dim="time", combine="by_coords").load() # close dangling handlers ds_sfc.close() # load data from from `ml` files ds_ml = xr.open_mfdataset(list_fn_ml, concat_dim="time", combine="by_coords").load() # close dangling handlers ds_ml.close() # surface level atmospheric pressure pres_z0 = ds_sfc.sp # hgt_agl_diag: height where to calculate diagnostics # hgt_agl_diag = 100 # determine a lowest level higher than surface at all times level_sel = get_level_diag(ds_sfc, ds_ml, hgt_agl_diag) # retrieve variables from the identified lowest level ds_ll = ds_ml.sel(level=level_sel) # altitude alt_z0 = geopotential2geometric(ds_sfc.z, ds_sfc.latitude) alt_za = geopotential2geometric(ds_ll.z, ds_ll.latitude) # atmospheric pressure [Pa] pres_za = pres_z0 * 0 + ds_ll.level * 100 # u-wind [m s-1] u_za = ds_ll.u # u-wind [m s-1] v_za = ds_ll.v # wind speed [m s-1] uv_za = np.sqrt(u_za**2 + v_za**2) # temperature [K] t_za = ds_ll.t # specific humidity [kg kg-1] q_za = ds_ll.q # ------------------------ # retrieve surface data # wind speed u10 = ds_sfc.u10 v10 = ds_sfc.v10 uv10 = np.sqrt(u10**2 + v10**2) # sensible/latent heat flux [W m-2] # conversion from cumulative value to hourly average qh = -ds_sfc.sshf / 3600 qe = -ds_sfc.slhf / 3600 # surface roughness [m] z0m = ds_sfc.fsr # friction velocity [m s-1] ustar = ds_sfc.zust # air temperature t2 = ds_sfc.t2m # dew point d2 = ds_sfc.d2m # specific humidity q2 = ac("qv", Td=d2, T=t2, p=pres_z0) # diagnose wind, temperature and humidity at 100 m agl or `hgt_agl_max` (see below) # conform dimensionality using an existing variable za = alt_za - alt_z0 z = za * 0 + hgt_agl_diag da_alt_z = (alt_z0 + z).rename("alt_z") ds_alt_z = da_alt_z.to_dataset() # get dataset of diagnostics if simple_mode: ds_diag = diag_era5_simple(z0m, ustar, pres_z0, uv10, t2, q2, z) else: ds_diag = diag_era5(za, uv_za, t_za, q_za, pres_za, qh, qe, z0m, ustar, pres_z0, uv10, t2, q2, z) # merge altitude ds_diag = ds_diag.merge(ds_alt_z).drop_vars("level") return ds_diag
def cal_gs_mod(kd, ta_k, rh, pa, smd, lai, g_cst, g_max=30., lai_max=6., s1=5.56): """Model surface conductance/resistance using phenology and atmospheric forcing conditions. Parameters ---------- kd : numeric Incoming solar radiation [W m-2] ta_k : numeric Air temperature [K] rh : numeric Relative humidity [%] pa : numeric Air pressure smd : numeric Soil moisture deficit [mm] lai : numeric Leaf area index [m2 m-2] g_cst : size-6 array Parameters to determine surface conductance/resistance: g1 (LAI related), g2 (solar radiation related), g3 (humidity related), g4 (humidity related), g5 (air temperature related), g6 (soil moisture related) g_max : numeric, optional Maximum surface conductance [mm s-1], by default 30 lai_max : numeric, optional Maximum LAI [m2 m-2], by default 6 s1 : numeric, optional Wilting point (WP=s1/g6, indicated as deficit [mm]) related parameter, by default 5.56 Returns ------- numeric Modelled surface conductance [mm s-1] """ # broadcast g1 – g6 # print('g_cst', g_cst) g1, g2, g3, g4, g5, g6 = g_cst # print(g1, g2, g3, g4, g5, g6) # lai related g_lai = cal_g_lai(lai, g1, lai_max) # print('g_lai', g_lai) # kdown related g_kd = cal_g_kd(kd, g2) # print('g_kd', g_kd) # dq related # ta_k = ta_c+273.15 dq = ac('qvs', T=ta_k, p=pa) - ac('qv', T=ta_k, p=pa, RH=rh) g_dq = cal_g_dq(dq, g3, g4) # print('g_dq', g_dq) # ta related ta_c = ta_k - 273.15 g_ta = cal_g_ta(ta_c, g5) # print('g_ta', g_ta) # smd related g_smd = cal_g_smd(smd, g6, s1) # print('g_smd', g_smd) # combine all corrections gs_c = g_lai * g_kd * g_dq * g_ta * g_smd gs = g_max * gs_c return gs, g_lai, g_kd, g_dq, g_ta, g_smd, g_max
def format_df_forcing(df_forcing_raw): from atmosp import calculate as ac df_forcing_grid = df_forcing_raw.copy().round(3) # convert energy fluxes: [J m-2] to [W m-2] df_forcing_grid.loc[:, ["ssrd", "strd", "sshf", "slhf"]] /= 3600 # reverse the sign of qh and qe df_forcing_grid.loc[:, ["sshf", "slhf"]] *= -1 # convert rainfall: from [m] to [mm] df_forcing_grid.loc[:, "tp"] *= 1000 # get dry bulb temperature and relative humidity df_forcing_grid = df_forcing_grid.assign(Tair=df_forcing_grid.t_z - 273.15) df_forcing_grid = df_forcing_grid.assign(RH=ac( "RH", qv=df_forcing_grid.q_z, T=df_forcing_grid.t_z, p=df_forcing_grid.p_z, )) # convert atmospheric pressure: [Pa] to [kPa] df_forcing_grid.loc[:, "p_z"] /= 1000 # renaming for consistency with SUEWS df_forcing_grid = df_forcing_grid.rename( { "ssrd": "kdown", "strd": "ldown", "sshf": "qh", "slhf": "qe", "tp": "rain", "uv_z": "U", "p_z": "pres", }, axis=1, ) col_suews = [ "iy", "id", "it", "imin", "qn", "qh", "qe", "qs", "qf", "U", "RH", "Tair", "pres", "rain", "kdown", "snow", "ldown", "fcld", "Wuh", "xsmd", "lai", "kdiff", "kdir", "wdir", "alt_z", ] df_forcing_grid = df_forcing_grid.reindex(col_suews, axis=1) df_forcing_grid = df_forcing_grid.assign( iy=df_forcing_grid.index.year, id=df_forcing_grid.index.dayofyear, it=df_forcing_grid.index.hour, imin=df_forcing_grid.index.minute, ) # corrections df_forcing_grid.loc[:, "RH"] = df_forcing_grid.loc[:, "RH"].where( df_forcing_grid.loc[:, "RH"].between(0.001, 105), 105) df_forcing_grid.loc[:, "kdown"] = df_forcing_grid.loc[:, "kdown"].where( df_forcing_grid.loc[:, "kdown"] > 0, 0) # trim decimals df_forcing_grid.iloc[:, 4:] = df_forcing_grid.iloc[:, 4:].round(2) df_forcing_grid = df_forcing_grid.replace(np.nan, -999).asfreq("1h") return df_forcing_grid
def cal_rh(qa_kgkg, theta_K, pres_hPa): from atmosp import calculate as ac RH = ac("RH", av=qa_kgkg, p=pres_hPa * 100, theta=theta_K) return RH
def cal_qa(rh_pct, theta_K, pres_hPa): from atmosp import calculate as ac qa = ac("qv", RH=rh_pct, p=pres_hPa * 100, theta=theta_K) return qa
def cal_rs_FG(qh, qe, ta, rh, pa, ra): """Calculate surface resistance based on observations, notably turbulent fluxes. Parameters ---------- qh : numeric sensible heat flux [W m-2] qe : numeric latent heat flux [W m-2] ta : numeric air temperature [degC] rh : numeric relative humidity [%] pa : numeric air pressure [Pa] ra : numeric aerodynamic resistance [m s-1] Returns ------- numeric Surface resistance based on observations [s m-1] """ from atmosp import calculate as ac from ._atm import cal_dens_vap, cal_lat_vap, cal_qa # air density [kg m-3] val_rho = 1.27 # heat capacity of air [J kg-1 K-1] val_cp = 1005 # convert temp from C to K ta_K = ta + 273.15 # canopy bulk surface temperature tc_K = qh / (val_rho * val_cp) * ra + ta_K # actual atmospheric vapour pressure [Pa] ser_qa = ac("qv", p=pa, T=ta_K, RH=rh) + rh * 0 # saturated atmospheric vapour pressure at canopy surface [Pa] ser_qs_c = ac("qvs", p=pa, T=tc_K) + rh * 0 # # vapour pressure deficit [Pa] # arr_vpd = arr_es_c - arr_ea # specific humidity [kg kg-1] # ser_qa = cal_qa(rh, ta_K, pa / 100) # latent heat of vapour [J kg-1] ser_lv = cal_lat_vap(ser_qa, ta_K, pa / 100) + pa * 0 # vapour density [kg m-3] rho_v = ac("rho", RH=rh, T=ta_K, p=pa) + pa * 0 ser_et = qe / ser_lv ser_rs = rho_v * (ser_qs_c - ser_qa) / ser_et - ra print( # ser_qa.median(), # ta_K.median(), # pa.median(), # tc_K.median(), # rho_v.median(), # (ser_qs_c - ser_qa).median(), # ser_et.median(), # ra.median(), # ser_lv.median(), ) try: # try to pack as Series ser_rs = pd.Series(ser_rs, index=ta_K.index) except AttributeError as ex: print(ex, "cannot pack into pd.Series") pass return ser_rs