#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Created on Wed Jun 23 11:20:28 2021 @author: dave """ import os from os.path import join as pjoin import numpy as np import pandas as pd # from wetb.prepost import misc from wetb.hawc2 import (HTCFile, AEFile, PCFile, StFile) from wetb.bladed.readprj import ReadBladedProject # TODO: move to misc? def get_circular_inertia(d1, t, m): d2 = d1 - 2*t A = np.pi/4*(d1**2-d2**2) # area moment of inertia x=y I_g = np.pi/64*(d1**4-d2**4) # mass moment of inertia I_m = I_g/A*m return A, I_g, I_m # TODO: move to HTCFile? def add_c2_arr_htc(htc, c2_arr, body): """add a c2_def in the form of an array to a htc c2_def section. Needs to be an existing body, and the current c2_def will be overwritten. """ nr_nodes = c2_arr.shape[0] c2_def = htc['new_htc_structure'].get_subsection_by_name(body)['c2_def'] keys = ['sec'] + [f'sec__{i}' for i in range(2, nr_nodes+1)] c2_def['nsec'].values[0] = nr_nodes for i, key in enumerate(keys): # control the formatting, otherwise it looks very messy in the htc file # this we do by converting the values to strings vals_fmt = [f'{k: 15.07f}' for k in c2_arr[i,:].tolist()] # node nr is stripped from leading spaces, so it doesn't help adding them values = [i+1] + vals_fmt if key in c2_def: c2_def[key].values[:] = values[:] else: c2_def.add_line(name=key, values=values) return htc # TODO: move to HTCFile? def get_c2def(htc, body_name): """ """ # select the c2def section from the shaft c2_def = htc['new_htc_structure'].get_subsection_by_name(body_name)['c2_def'] # safe all c2_def positions in one array c2_arr = np.zeros((len(c2_def.keys())-1,4)) # iterate over all nsec lines, but ignore the line that gives the number of points for i, sec in enumerate(c2_def.keys()[1:]): c2_arr[i,:] = c2_def[sec].values[1:] return pd.DataFrame(c2_arr, columns=['x', 'y', 'z', 'twist']) class Convert2Hawc(ReadBladedProject): """Based on the BLADED user manual v4.8 """ def __init__(self, fname): super().__init__(fname) def get_pc(self, setname_filter=None): # --------------------------------------------------------------------- # we ignore the airfoils blocks at the start of the ADAT sub-section # --------------------------------------------------------------------- # nr_airfoils = prj.get_key('ADAT', 'NFOILS')[0,0] # airfoil_keys = ['NFOIL', 'FTYPE', 'NTHICK', 'NREYN'] # airfoils = {} # for k in range(nr_airfoils): # key_app = '' # if k > 0: # key_app = f'__{k+1}' # airfoils[f'NFOIL_{k+1}'] = {} # for key in airfoil_keys: # --------------------------------------------------------------------- # total number of pc sets nsets = self.get_key('ADAT', 'NSETS')[0,0] key_app = [''] + [f'__{k}' for k in range(2, nsets+1)] # FIXME: are all sets always named consistantly? Meaning only the # thickness changes in the setname, and the rest remains the same? # as a work-around: ignore sets that contains setname_filter in SETNAME if setname_filter is not None: drop = [] for i, ka in enumerate(key_app): setname = self.get_key('ADAT', f'SETNAME{ka}')[0,0] if setname.find(setname_filter) > -1: drop.append(ka) key_app = set(key_app) - set(drop) # update nsets nsets = len(key_app) # initiate PCFile objects pc = PCFile() tc_arr = np.ndarray((nsets), dtype=np.float32) pc_lst = [] for i, ka in enumerate(key_app): # and assure cm is around quarter chord point assert self.get_key('ADAT', 'XA')[0,0]==25 # prj.get_key('ADAT', f'SETNAME{ka}') # prj.get_key('ADAT', f'SETNB{ka}') # prj.get_key('ADAT', f'COMMENTS{ka}') # prj.get_key('ADAT', f'DATE{ka}') # prj.get_key('ADAT', f'REYN{ka}') # prj.get_key('ADAT', f'DEPL{ka}') # prj.get_key('ADAT', f'NALPHA{ka}') # prj.get_key('ADAT', f'NVALS{ka}') tc_arr[i] = self.get_key('ADAT', f'THICK{ka}') shape = (self.get_key('ADAT', f'NALPHA{ka}')[0,0], 4) pcs = np.ndarray(shape, dtype=np.float32) pcs[:,0] = self.get_key('ADAT', f'ALPHA{ka}')[0,:] pcs[:,1] = self.get_key('ADAT', f'CL{ka}')[0,:] pcs[:,2] = self.get_key('ADAT', f'CD{ka}')[0,:] pcs[:,3] = self.get_key('ADAT', f'CM{ka}')[0,:] pc_lst.append(pcs) # in BGEOM the airfoil number is referred to and not the thickness # sort properly on thickness isort = tc_arr.argsort() pc.pc_sets = {1:(tc_arr[isort], [pc_lst[k] for k in isort])} return pc def get_ae(self): # BGEOMMB defines for each element the start and end point. We only # need half that of course. chord = self.get_key('BGEOMMB', 'CHORD')[0,0::2] # FIXME: is it radius or curved lenght radius = self.get_key('BGEOMMB', 'RJ')[0,0::2] thickness = self.get_key('BGEOMMB', 'BTHICK')[0,0::2] # FIXME: multiple pc sets can be defined but are ignored for now. pc_set_id = np.ones(chord.shape) ae = AEFile() ae.add_set(radius, chord, thickness, pc_set_id, set_id=None) return ae def get_blade_c2_def(self): # BLADENAME # DISTMODE RJ # NBE 100 # FOIL # MOVING # CE_X # CE_Y diam = self.get_key('RCON', 'DIAM')[0,0] cone = self.get_key('RCON', 'CONE')[0,0] tilt = self.get_key('RCON', 'TILT')[0,0] # in BLADED all is wrt LE, so we need chord chord = self.get_key('BGEOMMB', 'CHORD')[0,0::2] # c2_def = htc['new_htc_structure'].get_subsection_by_name('blade1')['c2_def'] twist = self.get_key('BGEOMMB', 'TWIST')[0,0::2] # FIXME: what is the difference between RJ and DIST? # radius can be given eiter as curved length or radius # Distance: This can be entered as a distance along the blade or as a # distance along the blade root Z-axis. Select the appropriate option # at the base of the screen. dist = self.get_key('BGEOMMB', 'DIST')[0,0::2] rj = self.get_key('BGEOMMB', 'RJ')[0,0::2] # reference frame is EC, that seems to be indicated on the plots of the # blade rotor coordinates/pitch/axis tc (fig 4-1 and 4-2) ref_x = self.get_key('BGEOMMB', 'REF_X')[0,0::2] ref_y = self.get_key('BGEOMMB', 'REF_Y')[0,0::2] # EC wrt to LE in percentage of chord ce_x = self.get_key('BGEOMMB', 'CE_X')[0,0::2] ce_y = self.get_key('BGEOMMB', 'CE_Y')[0,0::2] # HAWC2 variables theta = twist*-1 # theta_d = theta*180/np.pi ea_x = (50-ce_y)*0.01*chord ea_y = ce_x*0.01*chord # safe all c2_def positions in one array c2_arr = np.zeros((len(twist),4)) c2_arr[:,0] = -1*(ref_y + ((ea_x*np.cos(theta) - ea_y*np.sin(theta)))) c2_arr[:,1] = (ref_x - ((ea_x*np.sin(theta) + ea_y*np.cos(theta)))) # assume DIST is curved length c2_arr[:,2] = rj #dist c2_arr[:,3] = theta*180/np.pi return c2_arr def get_blade_st(self): # BMASSMB # BSTIFFMB # in HAWC2 structural pitch is relative to the twist twist = self.get_key('BGEOMMB', 'TWIST')[0,0::2] dist = self.get_key('BGEOMMB', 'DIST')[0,0::2] chord = self.get_key('BGEOMMB', 'CHORD')[0,0::2] thickness = self.get_key('BGEOMMB', 'BTHICK')[0,0::2] # CG location, in percentage chord wrt LE cm_x = self.get_key('BMASSMB', 'CM_X')[0,0::2] cm_y = self.get_key('BMASSMB', 'CM_Y')[0,0::2] mass = self.get_key('BMASSMB', 'MASS')[0,0::2] siner = self.get_key('BMASSMB', 'SINER')[0,0::2] # Radii of gyration ratio: the radius of gyration of mass about y_m # divided by the radius of gyration of mass about x_m. This defaults to # the relative profile thickness but can be over-written by un-checking # the “Use default radii of gyration ratio” checkbox. rgratio = self.get_key('BMASSMB', 'RGRATIO')[0,0::2] # FIXME: DEF_RGRATIO but which value belongs to which option? # assume 0 means "default is off" def_rgratio = self.get_key('BMASSMB', 'DEF_RGRATIO')[0,0] # FIXME: BETA_M: figure 3-2 from v4.8: mass axis orientation, radians? beta_m = self.get_key('BMASSMB', 'BETA_M')[0,0::2] # Mass axis orientation: the orientation of the principle axis of inertia. # This defaults to the orientation of aerodynamic twist, but can be # over-written by un-checking the “Use default mass axis orientation” # checkbox. (See diagram below) # FIXME: DEF_BETA_M but which value belongs to which option? # assume 0 means "default is off" def_beta_m = self.get_key('BMASSMB', 'DEF_BETA_M')[0,0] # DEF_RGRATIO 0 # DEF_BETA_M 0 # RADPOS # PITCHPOS # DEF_RGRATIO 0 # DEF_BETA_M 0 # ICEDBLADES 0, 0, 0 # ICEDENSITY 700 # TIPCHORD 0 # NPOINT 3 # RADPOS .2, 30.12345, 40.12345 # PITCHPOS .2, 30.1, 40.1 # PM_X 0, 0, 0 # PM_Y 20, 10, 20 # PMASS 1000, 30, 1 # elastic axis, in percentage chord wrt LE ce_x = self.get_key('BGEOMMB', 'CE_X')[0,0::2] ce_y = self.get_key('BGEOMMB', 'CE_Y')[0,0::2] # 3.5 Blade stiffness distribution (page 19) # The stiffness must be defined about the principal axis of inertia at # each blade station (see 3.1). The stiffness is the product of Young’s # Modulus for the material and the second moment of area for the xp or # yp directions as appropriate. The principal axis orientation is defined # as an input, and defaults to the aerodynamic twist. In this case it is # assumed to be parallel and perpendicular to the chord line. If the # principal axis orientation is different from the aerodynamic twist, click # the Use default principal axis orientation to off. (see diagram below) eiflap = self.get_key('BSTIFFMB', 'EIFLAP')[0,0::2] eiedge = self.get_key('BSTIFFMB', 'EIEDGE')[0,0::2] beta_s = self.get_key('BSTIFFMB', 'BETA_S')[0,0::2] gj = self.get_key('BSTIFFMB', 'GJ')[0,0::2] cs_x = self.get_key('BSTIFFMB', 'CS_X')[0,0::2] cs_y = self.get_key('BSTIFFMB', 'CS_Y')[0,0::2] gaflap = self.get_key('BSTIFFMB', 'GAFLAP')[0,0::2] gaedge = self.get_key('BSTIFFMB', 'GAEDGE')[0,0::2] # FIXME: DEF_BETA_S probably says what the frame of reference is # assume 0 means "default is off" # self.get_key('BMASSMB', 'DEF_BETA_S')[0,0::2] # for plotting purposes sel = beta_m < -1 beta_m[sel] += np.pi sel = beta_s < -1 beta_s[sel] += np.pi # calculate the curved length # c2_arr = self.get_blade_c2_def() # curved_len_arr = curved_length(c2_arr) ea_x = (50-ce_y)*0.01*chord ea_y = ce_x*0.01*chord cg_x = (50-cm_y)*0.01*chord cg_y = cm_x*0.01*chord # mass moments Ipm_y = siner + (mass*(cg_x-ea_x)**2) ri_y = np.sqrt( (Ipm_y / (1+rgratio**2)) / mass ) ri_x = rgratio * ri_y # in BLADED rgratio = y/x, but in HAWC2 it x=y so we swap them # just choose E, G so the rest follows E, G = 1e10, 1e09 # however, we don't know EA (probably because stiff?), so assume A is # a full box based on chord length and maximum airfoil thickness # this will make the extensional stiffness EA quite large, but the user # would still end up with a physical meaningful value A = chord*chord*thickness/100 starr = np.ndarray((len(dist),19)) starr[:,0] = dist starr[:,1] = mass starr[:,2] = cg_x starr[:,3] = cg_y # radius of gyration starr[:,4] = ri_x starr[:,5] = ri_y # shear center starr[:,6] = (50-cs_y)*0.01*chord starr[:,7] = cs_x*0.01*chord # E, G: choose starr[:,8] = E starr[:,9] = G starr[:,10] = eiflap/E starr[:,11] = eiedge/E starr[:,12] = gj/G # shear factor starr[:,13] = gaflap/(G*A) starr[:,14] = gaedge/(G*A) starr[:,15] = A starr[:,16] = -1*(beta_s - twist) # structural pitch starr[:,17] = ea_x starr[:,18] = ea_y st = StFile() st.main_data_sets = {1:{1:starr}} # st.cols = wetb.hawc2.st_file.stc.split() return st def get_tower_c2_def_st(self): """Ignores nodes that are not at the centerline Returns ------- c2_arr : TYPE DESCRIPTION. st : TYPE DESCRIPTION. """ t_n_e = int(self.get_key('TGEOM', 'NTE')) t_n_n = int(self.get_key('TGEOM', 'NTS')) t_el_mat = self.get_key('TGEOM', 'ELSTNS') t_n_coord = self.get_key('TGEOM', 'TCLOCAL') # tdiam = self.get_key('TGEOM', 'SEAL') # tdiam = self.get_key('TGEOM', 'HYDRO') towmgt = self.get_key('TGEOM', 'TOWMGT') # MATERIAL, NOMATS material = self.get_key('TMASS', 'MATERIAL')[0,0::2] nomats = self.get_key('TMASS', 'NOMATS') # Index of tower nodes (nodes at zero x-y positions) t_a = np.where((abs(t_n_coord[:,0])+abs(t_n_coord[:,1])) == 0)[0] t_nodes = t_n_coord[t_a] # sort index according to z position of the nodes n_ind = t_a[np.argsort(t_nodes[:,2])] t_nodes = t_n_coord[n_ind] # Index for tower elemetns # Elemet indexes which does not contain both tower nodes ind_not = [i for i,j in enumerate(t_el_mat) if not (j[0]-1) in n_ind or not (j[1]-1) in n_ind] # The elemetn indexes excluding the ind_not ind_init = np.array([i for i in range(t_el_mat.shape[0]) if not i in ind_not]) sort_el = [] for i in n_ind[:-1]: sort_el.append(np.where(t_el_mat[ind_init][:,0] == i+1)[0][0]) ind_el = ind_init[sort_el] # safe all c2_def positions in one array c2_arr = np.zeros((t_nodes.shape[0],4)) c2_arr[:,0:2] = t_nodes[:,:-1].copy() c2_arr[:,2] = -t_nodes[:,-1] + t_nodes[0,-1] # It is minus for HAWC2 model c2_arr[:,3] = 0.0 # element properties t_d = self.get_key('TGEOM', 'TDIAM')[ind_el] material = self.get_key('TMASS', 'MATERIAL')[0,0::2][ind_el] t_thick = self.get_key('TMASS', 'WALLTHICK')[ind_el] towmgt = self.get_key('TGEOM', 'TOWMGT')[ind_el] # Mass t_m = self.get_key('TMASS', 'TOWM')[ind_el] t_p_m = self.get_key('TMASS', 'TOWPMI')[ind_el] # point mass for elements and nodes t_flood = self.get_key('TMASS', 'FLOODED')[ind_el] # material props from prj file name_mat = np.unique(material) mat_prop = {} for i in name_mat: mat_prop[i] = self.get_key('TMASS', "%s"%i) # rho, E, G t_mat = np.array([mat_prop[i][0] for i in material]) if np.sum(t_thick[1:,0] - t_thick[:-1,1]) > 1e-9: print('Diameters are not continous along the tower') if (name_mat.shape[0] > 1): print('There are %i materials in tower!' %name_mat.shape[0]) d = np.zeros(t_d.shape[0]+1) t = np.zeros(t_thick.shape[0]+1) mat = np.zeros((t_mat.shape[0]+1,t_mat.shape[1])) d[1:] = t_d[:,1] t[1:] = t_thick[:,1] mat[1:,:] = t_mat.copy() d[0] = t_d[0,1] t[0] = t_thick[0,1] mat[0,:] = t_mat[0,:].copy() A, I_g, I_m = get_circular_inertia(d, t, mat[:,0]) # FIXME: MAYBE ANOTHER FIX FOR DIFFERENT MATERIALS # return c2_arr starr = np.zeros((c2_arr.shape[0],19)) starr[:,0] = -c2_arr[:,2] # starr[:,1] = A*mat[:,0] starr[:,2] = 0.0 # no cg offset starr[:,3] = 0.0 # no cg offset # radius of gyration starr[:,4] = np.sqrt(I_m/mat[:,0]) # ri_x = (I_m/m)**0.5 = (I_g/A)**0.5 starr[:,5] = np.sqrt(I_m/mat[:,0]) # ri_y = (I_m/m)**0.5 = (I_g/A)**0.5 # shear center starr[:,6] = 0.0 # no shear center offset starr[:,7] = 0.0 # no shear center offset # E, G: choose starr[:,8] = mat[:,1] starr[:,9] = mat[:,2] starr[:,10] = I_g starr[:,11] = I_g starr[:,12] = I_g*2 # shear factor starr[:,13] = 3/4 # for circular section check it again starr[:,14] = 3/4 # for circular section check it again starr[:,15] = A starr[:,16] = 0.0 # structural pitch starr[:,17] = 0.0 starr[:,18] = 0.0 st = StFile() st.main_data_sets = {1:{1:starr}} return c2_arr, st def get_hub(self): # Blade root length, in blade root coordinates (with tilt and cone) # see figure 4-1 in Blade User Manual 4.8 root = self.get_key('RCON', 'ROOT')[0,0] # Hub mass: the mass of the hub, including the spinner and any blade # root section. # Hub mass centre: the distance from the intersection of the shaft and # blade axes to the centre of mass of the hub, in a direction measured # away from the tower. # Moments of inertia: the moment of inertia of the hub mass about the # shaft axis must be defined. # The inertia about an axis perpendicular to the shaft may also be # entered with its origin about the hub centre of mass. hubmas = self.get_key('RMASS', 'HUBMAS')[0,0] hubine = self.get_key('RMASS', 'HUBINE')[0,0] # FIXME: which axis is this? Would that translate to both xx and yy in H2? hubine2 = self.get_key('RMASS', 'HUBINE2')[0,0] # Enter the Spinner diameter. This is the diameter of any spinner or # nose-cone, within which the blades themselves experience no # aerodynamic forces. spind = self.get_key('RCON', 'SPIND')[0,0] cmass_hub = {'x':0, 'y':0, 'z':0, 'm':hubmas, 'Ixx':0, 'Iyy':0, 'Izz':hubine} return cmass_hub, root def get_drivertain(self, len_shaft): # TODO: brake # MSTART BRAKE # DTRAIN # dtrain = self.get_key('DTRAIN') ginert = self.get_key('DTRAIN', 'GINERT')[0,0] # generator inertia gratio = self.get_key('DTRAIN', 'GRATIO')[0,0] # The additional inertia of the high speed shaft may also be specified # along with the inertia of the gearbox which is referred to the HSS. gbxinert = self.get_key('DTRAIN', 'GBXINERT')[0,0] # gearbox inertia # FIXME: brake position? bpos = self.get_key('DTRAIN', 'BPOS')[0,0] # LSS seems to have a DOF indicator in LSSDOF, and has 6 elements # 4th element is torsion it seems klss = self.get_key('DTRAIN', 'KLSS')[0,3] # FIXME: what does this damping value mean? Simply C in the dyn system # theta_dt_dt*I + C*theta_dt + K*theta = 0 ? dlss = self.get_key('DTRAIN', 'DLSS')[0,3] # HSS only has one DOF khss = self.get_key('DTRAIN', 'KHSS')[0,0] dhss = self.get_key('DTRAIN', 'DHSS')[0,0] # Convert torsional stiffness definition into a beam stiffness # that applies for the considered shaft length (c2_def) G = 80e9 # typical steel value Ip = klss*len_shaft/G starr = np.zeros((2,19)) starr[:,0] = [0, len_shaft] # st file is always normalised starr[:,1] = 0.5 # very light but not too light starr[:,2] = 0.0 # no cg offset starr[:,3] = 0.0 # no cg offset # radius of gyration: very light but not too light starr[:,4] = 0.001 #ri_x starr[:,5] = 0.001 #ri_y # shear center starr[:,6] = 0.0 # no shear center offset starr[:,7] = 0.0 # no shear center offset # E, G: choose starr[:,8] = 1e18 starr[:,9] = G starr[:,10] = Ip/2 # area inertia's starr[:,11] = Ip/2 starr[:,12] = Ip # shear factor starr[:,13] = 1000 # stiff in shear starr[:,14] = 1000 starr[:,15] = 10 # cross-section area starr[:,16] = 0.0 # structural pitch starr[:,17] = 0.0 # elastic axis starr[:,18] = 0.0 st = StFile() st.main_data_sets = {1:{1:starr}} # FIXME: INERTIATOCLUTCH?? inertiatoclutch = self.get_key('DTRAIN', 'INERTIATOCLUTCH') # FIXME: what is the location of these inertia's? # FIXME: what is the mass? It seems the mass is put elsewhere? # cmass_gbx = {'Izz':Izz} # cmass_gen = {'x':x, 'y':y, 'z':z, 'm':nacmas, # 'Ixx':Ixx, 'Iyy':Iyy, 'Izz':Izz} cmass_lss = {'x':0, 'y':0, 'z':0, 'm':0,'Ixx':0, 'Iyy':0, 'Izz':ginert} # convert HSS inertia to LSS cmass_hss = {'x':0, 'y':0, 'z':0, 'm':0,'Ixx':0, 'Iyy':0, 'Izz':gbxinert*gratio*gratio} return cmass_lss, cmass_hss, st def get_nacelle(self): # also get the nacell concentrated mass nacmas = self.get_key('NMASS', 'NACMAS')[0,0] # includes the nacelle structure and all the machinery within it. # It does not include the rotor blades and hub. If the Direct Drive # (see 4.1) option is selected, the mass of the generator (see 4.2) # is also excluded. # position of cg relative to tower top, nacelle mass coord system # based on figure 4-7 of bladed manual 4.8 # FIXME: is this correct? z means SS, and at the edge of the nacelle? nmx = self.get_key('NMASS', 'NMX')[0,0] # width? nmy = self.get_key('NMASS', 'NMY')[0,0] # length? nmz = self.get_key('NMASS', 'NMZ')[0,0] # height? iyaw = self.get_key('NMASS', 'IYAW')[0,0] inod = self.get_key('NMASS', 'INOD')[0,0] iroll = self.get_key('NMASS', 'IROLL')[0,0] # MSTART NGEOM # NACW x.000 # NACL x.000 # NACH x.000 # NACZ 0 # NACCD x.0 # NAERO N # height of the nacelle/towertop: the BLADED model does not have to # have consistent shaft length with tilt angle arriving at the rotor # center, so adjust towertop length in HAWC2 accordingly # Tower height: from the ground or sea surface to the yaw bearing # (only needed if the tower itself has not been defined) towht = self.get_key('RCON', 'TOWHT')[0,0] # Hub height: from the ground to the centre of the rotor # (i.e. the intersection of the blade and shaft axes) height = self.get_key('RCON', 'HEIGHT')[0,0] # horizontal distance between rotor center and tower center line ovrhng = self.get_key('RCON', 'OVRHNG')[0,0] tilt = self.get_key('RCON', 'TILT')[0,0] # FOR HAWC2, WE HAVE len_shaft = ovrhng / np.cos(tilt) len_towertop = height - towht - len_shaft*np.sin(tilt) assert len_towertop > 0 # assert len_towertop + len_shaft*np.sin(tilt) + towht - height == 0 # concentrated mass in HAWC2 # 1 : nodenr # 2-4: x, y, z offset # 5 : mass # 6-8: Ixx, Iyy, Izz # 9-11: Ixy, Ixz, Iyz (optional) # in HAWC2 we would be in the tower top coordinate system, and is # usually the same as the global coordinate system x = nmx y = -nmy # move upwind wrt tower z = -nmz # move upward Ixx = inod Iyy = iroll Izz = iyaw cmass_nacelle = {'x':x, 'y':y, 'z':z, 'm':nacmas, 'Ixx':Ixx, 'Iyy':Iyy, 'Izz':Izz} return cmass_nacelle, len_shaft, len_towertop def get_towershadow_drag(self): # TODO: tower shadow and tower aerodrag # MSTART TOWSDW # TSMODEL x # TDCORR x # MAXDEF x.x # WIDTH x # LREF x.x return def get_control(self): # MSTART CONTROL # MSTART PCOEFF # TSRMIN x # TSRMAX x # TSRSTP x.x # PITCH -x.0E-00 # PITCH_END -x.0E-00 # PITCH_STEP 0 # OMEGA x.654321 # MSTART IDLING # AEROINFO # pitch actuator rate limits pitch_actuator = self.xmlroot.PitchActuator if pitch_actuator.SetpointTrajectory.HasRateLimits: # pitch_actuator.SetpointTrajectory.LowerRateLimit # pitch_actuator.SetpointTrajectory.UpperRateLimit print(pitch_actuator.SetpointTrajectory.UpperRateLimit*30/np.pi) if pitch_actuator.SetpointTrajectory.HasAccelerationLimits: # pitch_actuator.SetpointTrajectory.LowerAccelerationLimit # pitch_actuator.SetpointTrajectory.UpperAccelerationLimit print(pitch_actuator.SetpointTrajectory.UpperAccelerationLimit*30/np.pi) # pitch_actuator.PositionResponse.values() # 1st or 2nd order # pitch_actuator.PositionResponse.Frequency # pitch_actuator.PositionResponse.Damping # pitch_actuator.RateResponse.values() # 1st or 2nd order # pitch_actuator.RateResponse.LagTime # pitch_actuator.PositionResponse.Damping # and more: bearing friction, etc return def convert(self, fname_htc): fname_prj = os.path.basename(fname_htc).replace('.htc', '') # for convenience, we start from an htc template that otherwise has the # right structure, but has only 2 nodes for each body. # assume it is in the same folder as where the HTC will be written to basepath = os.path.dirname(fname_htc) htc = HTCFile(pjoin(basepath, 'template.htc')) # prj = self.PRJ_to_HAWC(pjoin(version, fname_prj)) # plot_bladed(prj) # cone = prj.get_key('RCON', 'CONE') # Lateral offset: the horizontal offset between the shaft and tower axes. # Brake # steps = prj.get_key('BRAKE', 'STEPS') # TORQUE = prj.get_key('BRAKE', 'TORQUE') # TIME = prj.get_key('BRAKE', 'TIME') # ------------------------------------------------------------------------- # BLADES # extract blade geometry, and add to c2_def section in the htc file c2_blade = self.get_blade_c2_def() htc = add_c2_arr_htc(htc, c2_blade, 'blade1') # ------------------------------------------------------------------------- # NACELLE cmass_nacelle, len_shaft, len_towertop = self.get_nacelle() # set towertop length # nacelle = htc['new_htc_structure'].get_subsection_by_name('towertop') c2_tt = np.zeros((2,4)) c2_tt[1,2] = -len_towertop htc = add_c2_arr_htc(htc, c2_tt, 'towertop') # ------------------------------------------------------------------------- # attach nacelle mass/inertia to towertop, first node (yaw bearing) towertop = htc['new_htc_structure'].get_subsection_by_name('towertop') values = [v for k,v in cmass_nacelle.items()] cmass = towertop.add_line('concentrated_mass', [1] + values) key = cmass.location().split('/')[-1] towertop[key].comments = 'nacelle mass (inc rotating) and inertia (non-rotating)' # ------------------------------------------------------------------------- # set shaft length c2_s = np.zeros((2,4)) c2_s[1,2] = len_shaft htc = add_c2_arr_htc(htc, c2_s, 'shaft') # ------------------------------------------------------------------------- # generator and gearbox inertia's, shaft torsional flexibility cmass_lss, cmass_hss, st_shaft = self.get_drivertain(len_shaft) st_shaft.save(pjoin(basepath, f'{fname_prj}_shaft.st')) shaft = htc['new_htc_structure'].get_subsection_by_name('shaft') # LSS inertia values = [v for k,v in cmass_lss.items()] cmass = shaft.add_line('concentrated_mass', [2] + values) key = cmass.location().split('/')[-1] shaft[key].comments = 'inertia LSS' # Inertia of the HSS is already expressed wrt LSS values = [v for k,v in cmass_hss.items()] cmass = shaft.add_line('concentrated_mass', [2] + values) gratio = self.get_key('DTRAIN', 'GRATIO')[0,0] key = cmass.location().split('/')[-1] shaft[key].comments = f'inertia HSS, expressed in LSS, GBR={gratio}' # ------------------------------------------------------------------------- # set tilt angle tilt = self.get_key('RCON', 'TILT')[0,0] ori = htc['new_htc_structure'].orientation # tilt is on the 2nd relative block, and set angle on 2nd set of eulerang # check comments of the htc file: # print(ori['relative__2']['mbdy2_eulerang__2'].comments) ori['relative__2']['mbdy2_eulerang__2'].values = [tilt*180/np.pi, 0, 0] ori['relative__2']['mbdy2_eulerang__2'].comments = 'tilt angle' # coning angle cone = self.get_key('RCON', 'CONE')[0,0] # coning is on 3, 4 and 5 (all hub orientations) for k in range(3,6): # print(ori[f'relative__{k}']['mbdy2_eulerang__3'].comments) ori[f'relative__{k}']['mbdy2_eulerang__3'].values = [-cone*180/np.pi, 0, 0] ori[f'relative__{k}']['mbdy2_eulerang__3'].comments = 'cone angle' # TODO: blade mounting sweep and cone as well?? # ------------------------------------------------------------------------- # HUB mass as concentrated mass on the shaft end # FIXME: make sure we can get the hub root loads including these inertia's cmass_hub, hublen = self.get_hub() values = [v for k,v in cmass_hub.items()] cmass = shaft.add_line('concentrated_mass', [2] + values) key = cmass.location().split('/')[-1] shaft[key].comments = 'Hub inertia and mass' # ------------------------------------------------------------------------- # hub lenght c2_hub = np.zeros((2,4)) c2_hub[1,2] = hublen htc = add_c2_arr_htc(htc, c2_hub, 'hub1') # ------------------------------------------------------------------------- # TOWER ST FILE and C2_DEF section # WARNING: only includes nodes that are at centerline, side-elements are # ignored # TODO: compare between TGEOM/TMASS and TSTIFF approaches, it seems that # the shear stiffness is not the same, see check_tower() c2_tow, st = self.get_tower_c2_def_st() st.save(pjoin(basepath, f'{fname_prj}_tower.st')) htc = add_c2_arr_htc(htc, c2_tow, 'tower') # hub height htc.wind.center_pos0.values = [0, 0, -self.get_key('RCON', 'HEIGHT')[0,0]] # ------------------------------------------------------------------------- # set all file names correct tower = htc['new_htc_structure'].get_subsection_by_name('tower') tower.timoschenko_input.filename.values[0] = f'data/{fname_prj}_tower.st' shaft = htc['new_htc_structure'].get_subsection_by_name('shaft') shaft.timoschenko_input.filename.values[0] = f'data/{fname_prj}_shaft.st' blade1 = htc['new_htc_structure'].get_subsection_by_name('blade1') blade1.timoschenko_input.filename.values[0] = f'data/{fname_prj}_blade.st' for body_name in ['towertop', 'hub1']: body = htc['new_htc_structure'].get_subsection_by_name(body_name) body.timoschenko_input.filename.values[0] = 'data/template.st' htc['aero'].ae_filename.values[0] = f'data/{fname_prj}.ae' htc['aero'].pc_filename.values[0] = f'data/{fname_prj}.pc' # ------------------------------------------------------------------------- # SAVE HTC FILE htc.save(pjoin(basepath, f'{fname_htc}')) # ------------------------------------------------------------------------- # other data files # Blade st file st = self.get_blade_st() st.save(pjoin(basepath, f'{fname_prj}_blade.st')) # extract profile coefficients and save to pc file pc = self.get_pc(setname_filter='cleanVG') pc.save(pjoin(basepath, f'{fname_prj}.pc')) # extract aerodynamic layout, and safe to ae file ae = self.get_ae() ae.save(pjoin(basepath, f'{fname_prj}.ae')) # # BLADE c2_def # c2_def = htc['new_htc_structure'].get_subsection_by_name('blade1')['c2_def'] # # iterate over all nsec lines, but ignore the line that gives the number of points # for i, sec in enumerate(c2_def.keys()[1:]): # c2_arr[i,:] = c2_def[sec].values[1:] # curvedlength = np.zeros(len(c2_arr[:,2])) # for i in range(1,len(c2_arr[:,2])): # curvedlength[i]=curvedlength[i-1]+np.linalg.norm(c2_arr[i,0:3]-c2_arr[i-1,0:3]) # c2_arr = np.zeros((len(c2_def.keys())-1,4)) # # iterate over all nsec lines, but ignore the line that gives the number of points # for i, sec in enumerate(c2_def.keys()[1:]): # c2_arr[i,:] = c2_def[sec].values[1:]