import numpy as np
from .._env import logger_supy
from ._atm import cal_cp, cal_Lob
[docs]
def cal_neutral(
ser_qh,
ser_ustar,
ser_ta_c,
ser_rh_pct,
ser_pres_hpa,
ser_ws,
z_meas,
h_sfc,
):
"""Calculates the rows associated with neutral condition (threshold=0.01)
Parameters
----------
ser_qh: pd.DataFrame
sensible heat flux [W/m^2]
ser_ustar: pd.Series
friction velocity [m/s]
ser_ta_c: pd.Series
air temperature [°C]
ser_rh_pct: pd.Series
relative humidity [%]
ser_pres_hpa: pd.Series
air pressure [hPa]
ser_ws: pd.Series
wind speed [m/s]
z_meas
measurement height [m]
h_sfc
vegetation height [m]
Returns
-------
ser_ws_neutral: pd.Series
observation time series of WS (Neutral conditions)
ser_ustar_neutral: pd.Series
observation time series of u* (Neutral conditions)
"""
# calculate Obukhov length
# ser_Lob = df_val.apply(
# lambda ser: cal_Lob(ser.H, ser.USTAR, ser.TA, ser.RH, ser.PA * 10), axis=1
# )
ser_Lob = cal_Lob(ser_qh, ser_ustar, ser_ta_c, ser_rh_pct, ser_pres_hpa)
# zero-plane displacement: estimated using rule f thumb `d=0.7*h_sfc`
z_d = 0.7 * h_sfc
if z_d >= z_meas:
logger_supy.exception(
"vegetation height is greater than measuring height. Please fix this before continuing ..."
)
# calculate stability scale
ser_zL = (z_meas - z_d) / ser_Lob
# determine periods under quasi-neutral conditions
limit_neutral = 0.01
ind_neutral = ser_zL.between(-limit_neutral, limit_neutral)
ind_neutral = ind_neutral[ind_neutral].index
# df_sel = df_val.loc[ind_neutral.index, ["WS", "USTAR"]].dropna()
ser_ustar_neutral = ser_ustar.loc[ind_neutral]
ser_ws_neutral = ser_ws.loc[ind_neutral]
return ser_ws_neutral, ser_ustar_neutral
# calculate z0 and zd using mutli-objective optimisation
def cal_z0zd_mo(
ser_qh,
ser_ustar,
ser_ta_c,
ser_rh_pct,
ser_pres_hpa,
ser_ws,
z_meas,
h_sfc,
):
"""Calculates surface roughness and zero plane displacement height using mutli-objective optimisation.
Refer to https://suews-parameters-docs.readthedocs.io/en/latest/steps/roughness-SuPy.html for example
Parameters
----------
ser_qh: pd.DataFrame
sensible heat flux [W/m^2]
ser_ustar: pd.Series
friction velocity [m/s]
ser_ta_c: pd.Series
air temperature [°C]
ser_rh_pct: pd.Series
relative humidity [%]
ser_pres_hpa: pd.Series
air pressure [hPa]
z_meas
measurement height in m
h_sfc
vegetation height in m
Returns
-------
z0
surface roughness length for momentum
zd
zero displacement height
ser_ws_neutral: pd.Series
observation time series of WS (Neutral conditions)
ser_ustar_neutral: pd.series
observation time series of u* (Neutral conditions)
"""
from platypus.core import Problem
from platypus.types import Real, random
from platypus.algorithms import NSGAIII
# Calculates rows related to neutral conditions
ser_ws_neutral, ser_ustar_neutral = cal_neutral(
ser_qh,
ser_ustar,
ser_ta_c,
ser_rh_pct,
ser_pres_hpa,
ser_ws,
z_meas,
h_sfc,
)
# function to optimize
def func_uz(params):
z0 = params[0]
d = params[1]
z = z_meas
k = 0.4
uz = (ser_ustar_neutral / k) * np.log((z - d) / z0) # logarithmic law
o1 = abs(1 - np.std(uz) / np.std(ser_ws_neutral)) # objective 1: normalized STD
# objective 2: normalized MAE
o2 = np.mean(abs(uz - ser_ws_neutral)) / (np.mean(ser_ws_neutral))
return [o1, o2], [uz.min(), d - z0]
problem = Problem(2, 2, 2)
problem.types[0] = Real(0, 10) # bounds for first parameter (z0)
problem.types[1] = Real(0, h_sfc) # bounds for second parameter (zd)
problem.constraints[0] = ">=0" # constrain for first parameter
problem.constraints[1] = ">=0" # constrain for second parameter
problem.function = func_uz
random.seed(12345)
algorithm = NSGAIII(problem, divisions_outer=50)
algorithm.run(30000)
z0s = []
ds = []
os1 = []
os2 = []
# getting the solution vaiables
for s in algorithm.result:
z0s.append(s.variables[0])
ds.append(s.variables[1])
os1.append(s.objectives[0])
os2.append(s.objectives[1])
# getting the solution associated with minimum obj2 (can be changed)
idx = os2.index(min(os2, key=lambda x: abs(x - np.mean(os2))))
z0 = z0s[idx]
zd = ds[idx]
return z0, zd
# calculate z0 and d using curve fitting
[docs]
def cal_z0zd(
ser_qh,
ser_ustar,
ser_ta_c,
ser_rh_pct,
ser_pres_hpa,
ser_ws,
z_meas,
h_sfc,
debug=False,
):
"""Calculates surface roughness and zero plane displacement height.
Refer to https://suews-parameters-docs.readthedocs.io/en/latest/steps/roughness-SuPy.html for example
Parameters
----------
ser_qh: pd.DataFrame
sensible heat flux [W/m^2]
ser_ustar: pd.Series
friction velocity [m/s]
ser_ta_c: pd.Series
air temperature [°C]
ser_rh_pct: pd.Series
relative humidity [%]
ser_pres_hpa: pd.Series
air pressure [hPa]
z_meas: number
measurement height in m
h_sfc: number
vegetation height in m
debug : bool, optional
Option to output final calibrated `ModelResult <lmfit:ModelResult>`, by default False
Returns
-------
z0
surface roughness length for momentum
zd
zero displacement height
"""
from lmfit import Model, Parameter, Parameters
# Calculates rows related to neutral conditions
ser_ws_neutral, ser_ustar_neutral = cal_neutral(
ser_qh,
ser_ustar,
ser_ta_c,
ser_rh_pct,
ser_pres_hpa,
ser_ws,
z_meas,
h_sfc,
)
# function to optimize
def cal_uz_neutral(ustar_ntrl, z0, zd, z=z_meas, k=0.4):
# logarithmic law
uz = (ustar_ntrl / k) * np.log((z - zd) / z0)
return uz
model_uz_neutral = Model(
cal_uz_neutral,
independent_vars=["ustar_ntrl"],
param_names=["z0", "zd"],
)
prms = Parameters()
prm_z0 = Parameter(
"z0",
h_sfc * 0.1,
vary=True,
min=0.01 * h_sfc,
max=0.95 * h_sfc,
)
prm_zd = Parameter(
"zd",
h_sfc * 0.7,
vary=True,
min=0.01 * h_sfc,
max=0.95 * h_sfc,
)
prms.add_many(prm_z0, prm_zd)
try:
res_fit = model_uz_neutral.fit(
ser_ws_neutral,
ustar_ntrl=ser_ustar_neutral,
params=prms,
)
# provide full fitted model if debug == True otherwise only a dict with best fit parameters
res = res_fit if debug else res_fit.best_values
if isinstance(res, dict):
return res["z0"], res["zd"]
return res
except Exception as e:
print(e)
print('Fitting failed! Use 0.1h and 0.7h for z0 and zd, respectively')
return h_sfc * 0.1, h_sfc * 0.7