diff --git a/wetb/prepost/windIO.py b/wetb/prepost/windIO.py index 6c2d16d4015774532a95a34579a376eb0afacbed..71b34636af9da4ff1a0a052cfa0804133a6a9896 100755 --- a/wetb/prepost/windIO.py +++ b/wetb/prepost/windIO.py @@ -29,8 +29,9 @@ from time import time import codecs from itertools import chain -import scipy.integrate as integrate import numpy as np +import scipy as sp +import scipy.integrate as integrate import pandas as pd # misc is part of prepost, which is available on the dtu wind gitlab server: @@ -1603,7 +1604,7 @@ class UserWind(object): pass def __call__(self, z_h, r_blade_tip, a_phi=None, shear_exp=None, nr_hor=3, - nr_vert=20, h_ME=500.0, fname=None, wdir=None): + nr_vert=20, h_ME=500.0, io=None, wdir=None): """ Parameters @@ -1629,8 +1630,8 @@ class UserWind(object): Modified Ekman parameter. Take roughly 500 for off shore sites, 1000 for on shore sites. - fname : str, default=None - When specified, the HAWC2 user defined veer input file will be + io : str or io buffer, default=None + When specified, the HAWC2 user defined shear input file will be written. wdir : float, default=None @@ -1640,14 +1641,14 @@ class UserWind(object): Returns ------- - None + uu, vv, ww, xx, zz """ x, z = self.create_coords(z_h, r_blade_tip, nr_vert=nr_vert, nr_hor=nr_hor) if a_phi is not None: - phi_rad = self.veer_ekman_mod(z, z_h, h_ME=h_ME, a_phi=a_phi) + phi_rad = WindProfiles.veer_ekman_mod(z, z_h, h_ME=h_ME, a_phi=a_phi) assert len(phi_rad) == nr_vert else: nr_vert = len(z) @@ -1656,21 +1657,34 @@ class UserWind(object): if wdir is not None: # because wdir cw positive, and phi veer ccw positive phi_rad -= (wdir*np.pi/180.0) - u, v, w, xx, zz = self.decompose_veer(phi_rad, x, z) - # scale the shear on top of that - if shear_exp is not None: - shear = self.shear_powerlaw(zz, z_h, shear_exp) + u, v, w = self.decompose_veer(phi_rad, nr_hor) + # when no shear is defined + if shear_exp is None: + uu = u + vv = v + ww = w + else: + # scale the shear on top of the veer + shear = WindProfiles.powerlaw(z, z_h, shear_exp) uu = u*shear[:,np.newaxis] vv = v*shear[:,np.newaxis] ww = w*shear[:,np.newaxis] # and write to a file - if fname is not None: - self.write_user_defined_shear(fname, uu, vv, ww, xx, zz) + if isinstance(io, str): + with open(io, 'wb') as fid: + fid = self.write(fid, uu, vv, ww, x, z) + self.fid =None + elif io is not None: + io = self.write(io, uu, vv, ww, x, z) + self.fid = io + + return uu, vv, ww, x, z def create_coords(self, z_h, r_blade_tip, nr_vert=3, nr_hor=20): """ Utility to create the coordinates of the wind field based on hub heigth - and blade length. + and blade length. Add 15% to r_blade_tip to make sure horizontal edges + are defined wide enough. """ # take 15% extra space after the blade tip z = np.linspace(0, z_h + r_blade_tip*1.15, nr_vert) @@ -1679,80 +1693,88 @@ class UserWind(object): return x, z - def shear_powerlaw(self, z, z_ref, a): - profile = np.power(z/z_ref, a) - # when a negative, make sure we return zero and not inf - profile[np.isinf(profile)] = 0.0 - return profile + def deltaphi2aphi(self, d_phi, z_h, r_blade_tip, h_ME=500.0): + """For a given `\\Delta \\varphi` over the rotor diameter, estimate + the corresponding `a_{\\varphi}`. - def veer_ekman_mod(self, z, z_h, h_ME=500.0, a_phi=0.5): - """ - Modified Ekman veer profile, as defined by Mark C. Kelly in email on - 10 October 2014 15:10 (RE: veer profile) + Parameters + ---------- - .. math:: - \\varphi(z) - \\varphi(z_H) \\approx a_{\\varphi} - e^{-\sqrt{z_H/h_{ME}}} - \\frac{z-z_H}{\sqrt{z_H*h_{ME}}} - \\left( 1 - \\frac{z-z_H}{2 \sqrt{z_H h_{ME}}} - - \\frac{z-z_H}{4z_H} \\right) + `\\Delta \\varphi` : ndarray or float + Veer angle difference over the rotor plane from lowest to highest + blade tip position. - where: - :math:`h_{ME} \\equiv \\frac{\\kappa u_*}{f}` - and :math:`f = 2 \Omega \sin \\varphi` is the coriolis parameter, - and :math:`\\kappa = 0.41` as the von Karman constant, - and :math:`u_\\star = \\sqrt{\\frac{\\tau_w}{\\rho}}` friction velocity. + z_h : float + Hub height in meters. - For on shore, :math:`h_{ME} \\approx 1000`, for off-shore, - :math:`h_{ME} \\approx 500` + r_blade_tip : float + Blade tip radius/length. - :math:`a_{\\varphi} \\approx 0.5` + h_ME : float, default=500.0 + Modified Ekman parameter. For on shore, + :math:`h_{ME} \\approx 1000`, for off-shore, + :math:`h_{ME} \\approx 500` + + Returns + ------- + + `a_{\\varphi}` : ndarray or float + + """ + + t1 = r_blade_tip * 2.0 * np.exp(-z_h/(h_ME)) + a_phi = d_phi * np.sqrt(h_ME*z_h) / t1 + return a_phi + + def deltaphi2aphi_opt(self, deltaphi, z, z_h, r_blade_tip, h_ME): + """ + convert delta_phi over a given interval z to a_phi using + scipy.optimize.fsolve on veer_ekman_mod. Parameters ---------- - :math:`a_{\\varphi} \\approx 0.5` parameter for the modified - Ekman veer distribution. Values vary between -1.2 and 0.5. - - returns - ------- + deltaphi : float + Desired delta phi in rad over interval z[0] at bottom to z[1] at + the top. - phi_rad : ndarray - veer angle in radians """ - t1 = np.exp(-math.sqrt(z_h / h_ME)) - t2 = (z - z_h) / math.sqrt(z_h * h_ME) - t3 = (1.0 - (z-z_h)/(2.0*math.sqrt(z_h*h_ME)) - (z-z_h)/(4.0*z_h)) + def func(a_phi, z, z_h, h_ME, deltaphi_target): + phis = WindProfiles.veer_ekman_mod(z, z_h, h_ME=h_ME, a_phi=a_phi) + return np.abs(deltaphi_target - (phis[1] - phis[0])) - return a_phi * t1 * t2 * t3 + args = (z, z_h, h_ME, deltaphi) + return sp.optimize.fsolve(func, [0], args=args)[0] - def decompose_veer(self, phi_rad, x, z): + def decompose_veer(self, phi_rad, nr_hor): """ Convert a veer angle into u, v, and w components, ready for the - HAWC2 user defined veer input file. + HAWC2 user defined veer input file. nr_vert refers to the number of + vertical grid points. Paramters --------- - phi_rad : ndarray - veer angle in radians + phi_rad : ndarray(nr_vert) + veer angle in radians as function of height - method : str, default=linear - 'linear' for a linear veer, 'ekman_mod' for modified ekman method + nr_hor : int + Number of horizontal grid points Returns ------- - u, v, w, v_coord, w_coord + u : ndarray(nr_hor, nr_vert) - """ + v : ndarray(nr_hor, nr_vert) + + w : ndarray(nr_hor, nr_vert) - nr_hor = len(x) - nr_vert = len(z) - assert len(phi_rad) == nr_vert + """ + nr_vert = len(phi_rad) tan_phi = np.tan(phi_rad) # convert veer angles to veer components in v, u. Make sure the @@ -1777,11 +1799,11 @@ class UserWind(object): v_full = v[:, np.newaxis] + np.zeros((3,))[np.newaxis, :] w_full = np.zeros((nr_vert, nr_hor)) - return u_full, v_full, w_full, x, z + return u_full, v_full, w_full - def load_user_defined_veer(self, fname): + def read(self, fname): """ - Load a user defined veer and shear file as used for HAWC2 + Read a user defined shear input file as used for HAWC2. Returns ------- @@ -1818,9 +1840,9 @@ class UserWind(object): return u_comp, v_comp, w_comp, v_coord, w_coord, phi_deg - def write_user_defined_shear(self, fname, u, v, w, v_coord, w_coord, - fmt_uvw='% 08.05f', fmt_coord='% 8.02f'): - """ + def write(self, fid, u, v, w, v_coord, w_coord, fmt_uvw='% 08.05f', + fmt_coord='% 8.02f'): + """Write a user defined shear input file for HAWC2. """ nr_hor = len(v_coord) nr_vert = len(w_coord) @@ -1835,40 +1857,37 @@ class UserWind(object): 'nr_hor and nr_vert: u.shape: %s, nr_hor: %i, ' 'nr_vert: %i' % (str(u.shape), nr_hor, nr_vert)) - # and create the input file - with open(fname, 'wb') as fid: - fid.write(b'# User defined shear file\n') - fid.write(b'%i %i # nr_hor (v), nr_vert (w)\n' % (nr_hor, nr_vert)) - h1 = b'normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns' - fid.write(b'# v component, %s\n' % h1) - np.savetxt(fid, v, fmt=fmt_uvw, delimiter=' ') - fid.write(b'# u component, %s\n' % h1) - np.savetxt(fid, u, fmt=fmt_uvw, delimiter=' ') - fid.write(b'# w component, %s\n' % h1) - np.savetxt(fid, w, fmt=fmt_uvw, delimiter=' ') - h2 = b'# v coordinates (along the horizontal, nr_hor, 0 rotor center)' - fid.write(b'%s\n' % h2) - np.savetxt(fid, v_coord.reshape((v_coord.size, 1)), fmt=fmt_coord) - h3 = b'# w coordinates (zero is at ground level, height, nr_hor)' - fid.write(b'%s\n' % h3) - np.savetxt(fid, w_coord.reshape((w_coord.size, 1)), fmt=fmt_coord) + fid.write(b'# User defined shear file\n') + fid.write(b'%i %i # nr_hor (v), nr_vert (w)\n' % (nr_hor, nr_vert)) + h1 = b'normalized with U_mean, nr_hor (v) rows, nr_vert (w) columns' + fid.write(b'# v component, %s\n' % h1) + np.savetxt(fid, v, fmt=fmt_uvw, delimiter=' ') + fid.write(b'# u component, %s\n' % h1) + np.savetxt(fid, u, fmt=fmt_uvw, delimiter=' ') + fid.write(b'# w component, %s\n' % h1) + np.savetxt(fid, w, fmt=fmt_uvw, delimiter=' ') + h2 = b'# v coordinates (along the horizontal, nr_hor, 0 rotor center)' + fid.write(b'%s\n' % h2) + np.savetxt(fid, v_coord.reshape((v_coord.size, 1)), fmt=fmt_coord) + h3 = b'# w coordinates (zero is at ground level, height, nr_hor)' + fid.write(b'%s\n' % h3) + np.savetxt(fid, w_coord.reshape((w_coord.size, 1)), fmt=fmt_coord) + + return fid class WindProfiles(object): - def __init__(self): - pass - - def logarithmic(self, z, z_ref, r_0): + def logarithmic(z, z_ref, r_0): return np.log10(z/r_0)/np.log10(z_ref/r_0) - def powerlaw(self, z, z_ref, a): + def powerlaw(z, z_ref, a): profile = np.power(z/z_ref, a) # when a negative, make sure we return zero and not inf profile[np.isinf(profile)] = 0.0 return profile - def veer_ekman_mod(self, z, z_h, h_ME=500.0, a_phi=0.5): + def veer_ekman_mod(z, z_h, h_ME=500.0, a_phi=0.5): """ Modified Ekman veer profile, as defined by Mark C. Kelly in email on 10 October 2014 15:10 (RE: veer profile) @@ -1894,20 +1913,29 @@ class WindProfiles(object): Parameters ---------- - :math:`a_{\\varphi} \\approx 0.5` parameter for the modified - Ekman veer distribution. Values vary between -1.2 and 0.5. + z : ndarray(n) + z-coordinates (height) of the grid on which the veer angle should + be calculated. - returns + z_h : float + Hub height in meters. + + :math:`a_{\\varphi}` : default=0.5 + Parameter for the modified Ekman veer distribution. Value varies + between -1.2 and 0.5. + + Returns ------- phi_rad : ndarray - veer angle in radians as function of height + Veer angle in radians as function of z. + """ t1 = np.exp(-math.sqrt(z_h / h_ME)) t2 = (z - z_h) / math.sqrt(z_h * h_ME) - t3 = ( 1.0 - (z-z_h)/(2.0*math.sqrt(z_h*h_ME)) - (z-z_h)/(4.0*z_h) ) + t3 = (1.0 - (z-z_h)/(2.0*math.sqrt(z_h*h_ME)) - (z-z_h)/(4.0*z_h)) return a_phi * t1 * t2 * t3