def compute(self, desvars): with open(run_dir + f"CCBlade_inputs_{self.n_span}.pkl", "rb") as f: saved_dict = dill.load(f) chord_opt_gain = desvars["blade.opt_var.chord_opt_gain"] chord_original = saved_dict["chord_original"] s = saved_dict["s"] s_opt_chord = np.linspace(0.0, 1.0, len(chord_opt_gain)) spline = PchipInterpolator chord_spline = spline(s_opt_chord, chord_opt_gain) chord = chord_original * chord_spline(s) get_cp_cm = CCBladeOrig( saved_dict["r"], chord, saved_dict["twist"], saved_dict["af"], saved_dict["Rhub"], saved_dict["Rtip"], saved_dict["nBlades"], saved_dict["rho"], saved_dict["mu"], saved_dict["precone"], saved_dict["tilt"], saved_dict["yaw"], saved_dict["shearExp"], saved_dict["hub_height"], saved_dict["nSector"], saved_dict["precurve"], saved_dict["precurveTip"], saved_dict["presweep"], saved_dict["presweepTip"], saved_dict["tiploss"], saved_dict["hubloss"], saved_dict["wakerotation"], saved_dict["usecd"], ) get_cp_cm.inverse_analysis = False get_cp_cm.induction = True # Compute omega given TSR Omega = (saved_dict["Uhub"] * saved_dict["tsr"] / saved_dict["Rtip"] * 30.0 / np.pi) myout, derivs = get_cp_cm.evaluate([saved_dict["Uhub"]], [Omega], [saved_dict["pitch"]], coefficients=True) outputs = {} outputs["CP"] = myout["CP"] return outputs
class Performance_coef(unittest.TestCase): def setup(self): """ Parameters ---------- r : array_like (m) locations defining the blade along z-axis of :ref:`blade coordinate system <azimuth_blade_coord>` (values should be increasing). chord : array_like (m) corresponding chord length at each section theta : array_like (deg) corresponding :ref:`twist angle <blade_airfoil_coord>` at each section--- positive twist decreases angle of attack. Rhub : float (m) location of hub Rtip : float (m) location of tip B : int, optional number of blades rho : float, optional (kg/m^3) freestream fluid density mu : float, optional (kg/m/s) dynamic viscosity of fluid precone : float, optional (deg) :ref:`hub precone angle <azimuth_blade_coord>` tilt : float, optional (deg) nacelle :ref:`tilt angle <yaw_hub_coord>` yaw : float, optional (deg) nacelle :ref:`yaw angle<wind_yaw_coord>` shearExp : float, optional shear exponent for a power-law wind profile across hub hubHt : float, optional hub height used for power-law wind profile. U = Uref*(z/hubHt)**shearExp nSector : int, optional number of azimuthal sectors to descretize aerodynamic calculation. automatically set to ``1`` if tilt, yaw, and shearExp are all 0.0. Otherwise set to a minimum of 4. """ self.Rhub = 1.5 self.Rtip = 63 self.r = np.array([ self.Rtip * 0.045503175, self.Rtip * 0.088888889, self.Rtip * 0.132274603, self.Rtip * 0.186507937, self.Rtip * 0.251587302, self.Rtip * 0.316666667, self.Rtip * 0.381746032, self.Rtip * 0.446825397, self.Rtip * 0.511904762, self.Rtip * 0.576984127, self.Rtip * 0.642063492, self.Rtip * 0.707142857, self.Rtip * 0.772222222, self.Rtip * 0.837301587, self.Rtip * 0.891534921, self.Rtip * 0.934920635, self.Rtip * 0.978306349 ]) chord = np.array([ 3.542, 3.854, 4.167, 4.557, 4.652, 4.458, 4.249, 4.007, 3.748, 3.502, 3.256, 3.010, 2.764, 2.518, 2.313, 2.086, 1.419 ]) theta = np.array([ 13.308, 13.308, 13.308, 13.308, 11.480, 10.162, 9.011, 7.795, 6.544, 5.361, 4.188, 3.125, 2.319, 1.526, 0.863, 0.370, 0.106 ]) afinit = CCAirfoil.initFromAerodynFile # just for shorthand basepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), "5MW_AFFiles/") # load all airfoils airfoil_types = [0] * 8 airfoil_types[0] = afinit(basepath + 'Cylinder1.dat') airfoil_types[1] = afinit(basepath + 'Cylinder2.dat') airfoil_types[2] = afinit(basepath + 'DU40_A17.dat') airfoil_types[3] = afinit(basepath + 'DU35_A17.dat') airfoil_types[4] = afinit(basepath + 'DU30_A17.dat') airfoil_types[5] = afinit(basepath + 'DU25_A17.dat') airfoil_types[6] = afinit(basepath + 'DU21_A17.dat') airfoil_types[7] = afinit(basepath + 'NACA64_A17.dat') # place at appropriate radial stations af_idx = [0, 0, 1, 2, 3, 3, 4, 5, 5, 6, 6, 7, 7, 7, 7, 7, 7] af = [0] * len(self.r) for i in range(len(self.r)): af[i] = airfoil_types[af_idx[i]] B = 3 # number of blades # atmosphere rho = 1.225 mu = 1.81206e-5 tilt = 5.0 precone = 2.5 yaw = 0.0 shearExp = 0.2 hubHt = (self.Rtip * 2) * 0.92 nSector = 8 # create CCBlade object self.aeroanalysis = CCBlade(self.r, chord, theta, af, self.Rhub, self.Rtip, B, rho, mu, precone, tilt, yaw, shearExp, hubHt, nSector) def compute(self): ''' Uinf : array_like (m/s) hub height wind speed Omega : array_like (RPM) rotor rotation speed pitch : array_like (deg) blade pitch setting ''' # set conditions Uinf = np.array([ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 ]) Omega = np.array([ 6.972, 7.183, 7.506, 7.942, 8.469, 9.156, 10.296, 11.431, 11.890, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100 ]) pitch = np.array([ 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 3.823, 6.602, 8.668, 10.450, 12.055, 13.536, 14.920, 16.226, 17.473, 18.699, 19.941, 21.177, 22.347, 23.469 ]) Pref = np.array([ 42.9, 188.2, 427.9, 781.3, 1257.6, 1876.2, 2668.0, 3653.0, 4833.2, 5296.6, 5296.6, 5296.6, 5296.6, 5296.6, 5296.6, 5296.6, 5296.6, 5296.7, 5296.6, 5296.7, 5296.6, 5296.6, 5296.7 ]) Tref = np.array([ 171.7, 215.9, 268.9, 330.3, 398.6, 478.0, 579.2, 691.5, 790.6, 690.0, 608.4, 557.9, 520.5, 491.2, 467.7, 448.4, 432.3, 418.8, 406.7, 395.3, 385.1, 376.7, 369.3 ]) Qref = np.array([ 58.8, 250.2, 544.3, 939.5, 1418.1, 1956.9, 2474.5, 3051.1, 3881.3, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1 ]) P, T, Q, M, CP, CT, CQ, CM = self.aeroanalysis.evaluate( Uinf, Omega, pitch, coefficients=True) print('CP:' + str(CP)) path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "Output\Cp_res.txt") with open(path, 'w') as f: f.write('Cp:\n') for i in CP: f.write("%s\n" % str(i)) f.write('\n') f.write('Radius: \n') f.write(str(self.Rtip)) f.close() '''
class Turbine(): """ Class Turbine defines a turbine in terms of what is needed to design the controller and to run the 'tiny' simulation. Primary functions (at a high level): - Reads an OpenFAST input deck - Runs cc-blade rotor performance analysis - Writes a text file containing Cp, Ct, and Cq tables Methods: ------- __str__ load save load_from_fast load_from_ccblade load_from_txt write_rotor_performance Parameters: ----------- turbine_params : dict Dictionary containing known turbine paramaters that are not directly available from OpenFAST input files """ def __init__(self, turbine_params): """ Load turbine parameters from input dictionary """ print('-----------------------------------------------------------------------------') print('Loading wind turbine data for NREL\'s ROSCO tuning and simulation processeses') print('-----------------------------------------------------------------------------') # ------ Turbine Parameters------ self.rotor_inertia = turbine_params['rotor_inertia'] self.rated_rotor_speed = turbine_params['rated_rotor_speed'] self.v_min = turbine_params['v_min'] self.v_rated = turbine_params['v_rated'] self.v_max = turbine_params['v_max'] self.max_pitch_rate = turbine_params['max_pitch_rate'] self.min_pitch_rate = -1 * self.max_pitch_rate self.max_torque_rate = turbine_params['max_torque_rate'] self.rated_power = turbine_params['rated_power'] self.bld_edgewise_freq = turbine_params['bld_edgewise_freq'] if turbine_params['twr_freq']: self.twr_freq = turbine_params['twr_freq'] else: self.twr_freq = 0.0 if turbine_params['ptfm_freq']: self.ptfm_freq = turbine_params['ptfm_freq'] else: self.ptfm_freq = 0.0 if turbine_params['TSR_operational']: self.TSR_operational = turbine_params['TSR_operational'] else: self.TSR_operational = None if turbine_params['bld_flapwise_freq']: self.bld_flapwise_freq = turbine_params['bld_flapwise_freq'] else: self.bld_flapwise_freq = 0.0 # Allow print out of class def __str__(self): ''' Print some data about the turbine. ''' print('-------------- Turbine Info --------------') print('Turbine Name: {}'.format(self.TurbineName)) print('Rated Power: {} [W]'.format(self.rated_power)) print('Total Inertia: {:.1f} [kg m^2]'.format(self.J)) print('Rotor Radius: {:.1f} [m]'.format(self.rotor_radius)) print('Rated Rotor Speed: {:.1f} [rad/s]'.format(self.rated_rotor_speed)) print('Max Cp: {:.2f}'.format(self.Cp.max)) print('------------------------------------------') return ' ' # Save function def save(self,filename): ''' Save turbine to pickle Parameters: ---------- filename : str Name of file to save pickle # ''' pickle.dump(self, open( filename, "wb" ) ) # Load function @staticmethod def load(filename): ''' Load turbine from pickle - outdated, but might be okay!! Parameters: ---------- filename : str Name of pickle file ''' turbine = pickle.load(open(filename,'rb')) return turbine # Load data from fast input deck def load_from_fast(self, FAST_InputFile,FAST_directory, FAST_ver='OpenFAST',dev_branch=True,rot_source=None, txt_filename=None): """ Load the parameter files directly from a FAST input deck Parameters: ----------- Fast_InputFile: str Primary fast model input file (*.fst) FAST_directory: str Directory for primary fast model input file FAST_ver: string, optional fast version, usually OpenFAST dev_branch: bool, optional dev_branch input to InputReader_OpenFAST, probably True rot_source: str, optional desired source for rotor to get Cp, Ct, Cq tables. Default is to run cc-blade. options: cc-blade - run cc-blade txt - from *.txt file txt_filename: str, optional filename for *.txt, only used if rot_source='txt' """ from wisdem.aeroelasticse.FAST_reader import InputReader_OpenFAST print('Loading FAST model: %s ' % FAST_InputFile) self.TurbineName = FAST_InputFile.strip('.fst') fast = self.fast = InputReader_OpenFAST(FAST_ver=FAST_ver,dev_branch=dev_branch) fast.FAST_InputFile = FAST_InputFile fast.FAST_directory = FAST_directory fast.execute() if txt_filename: self.rotor_performance_filename = txt_filename else: self.rotor_performance_filename = 'Cp_Ct_Cq.txt' # Grab general turbine parameters self.TipRad = fast.fst_vt['ElastoDyn']['TipRad'] self.Rhub = fast.fst_vt['ElastoDyn']['HubRad'] self.hubHt = fast.fst_vt['ElastoDyn']['TowerHt'] self.NumBl = fast.fst_vt['ElastoDyn']['NumBl'] self.TowerHt = fast.fst_vt['ElastoDyn']['TowerHt'] self.shearExp = 0.2 #HARD CODED FOR NOW self.rho = fast.fst_vt['AeroDyn15']['AirDens'] self.mu = fast.fst_vt['AeroDyn15']['KinVisc'] self.Ng = fast.fst_vt['ElastoDyn']['GBRatio'] self.GenEff = fast.fst_vt['ServoDyn']['GenEff'] self.GBoxEff = fast.fst_vt['ElastoDyn']['GBoxEff'] self.DTTorSpr = fast.fst_vt['ElastoDyn']['DTTorSpr'] self.generator_inertia = fast.fst_vt['ElastoDyn']['GenIner'] self.tilt = fast.fst_vt['ElastoDyn']['ShftTilt'] try: self.precone = fast.fst_vt['ElastoDyn']['PreCone1'] # May need to change to PreCone(1) depending on OpenFAST files except: self.precone = fast.fst_vt['ElastoDyn']['PreCone(1)'] self.yaw = 0.0 self.J = self.rotor_inertia + self.generator_inertia * self.Ng**2 self.rated_torque = self.rated_power/(self.GenEff/100*self.rated_rotor_speed*self.Ng) self.max_torque = self.rated_torque * 1.1 self.rotor_radius = self.TipRad # self.omega_dt = np.sqrt(self.DTTorSpr/self.J) # Load Cp, Ct, Cq tables if rot_source == 'cc-blade': # Use cc-blade self.load_from_ccblade() elif rot_source == 'txt': # Use specified text file file_processing = ROSCO_utilities.FileProcessing() self.pitch_initial_rad, self.TSR_initial, self.Cp_table, self.Ct_table, self.Cq_table = file_processing.load_from_txt( txt_filename) else: # Use text file from DISCON.in if os.path.exists(os.path.join(FAST_directory, fast.fst_vt['ServoDyn']['DLL_InFile'])): if os.path.exists(fast.fst_vt['DISCON_in']['PerfFileName']): self.pitch_initial_rad = fast.fst_vt['DISCON_in']['Cp_pitch_initial_rad'] self.TSR_initial = fast.fst_vt['DISCON_in']['Cp_TSR_initial'] self.Cp_table = fast.fst_vt['DISCON_in']['Cp_table'] self.Ct_table = fast.fst_vt['DISCON_in']['Ct_table'] self.Cq_table = fast.fst_vt['DISCON_in']['Cq_table'] else: # Load from cc-blade print('No rotor performance data source available, running CC-Blade.') self.load_from_ccblade() # Parse rotor performance data self.Cp = RotorPerformance(self.Cp_table,self.pitch_initial_rad,self.TSR_initial) self.Ct = RotorPerformance(self.Ct_table,self.pitch_initial_rad,self.TSR_initial) self.Cq = RotorPerformance(self.Cq_table,self.pitch_initial_rad,self.TSR_initial) # Define operational TSR if not self.TSR_operational: self.TSR_operational = self.Cp.TSR_opt # Pull out some floating-related data wave_tp = fast.fst_vt['HydroDyn']['WaveTp'] try: self.wave_peak_period = 1/wave_tp # Will work if HydroDyn exists and a peak period is defined... except: self.wave_peak_period = 0.0 # Set as 0.0 when HydroDyn doesn't exist (fixed bottom) # Load rotor performance data from CCBlade def load_from_ccblade(self): ''' Loads rotor performance information by running cc-blade aerodynamic analysis. Designed to work with Aerodyn15 blade input files. Parameters: ----------- fast: dict Dictionary containing fast model details - defined using from InputReader_OpenFAST (distributed as a part of AeroelasticSE) ''' print('Loading rotor performance data from CC-Blade.') # Create CC-Blade Rotor r0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlSpn']) chord0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlChord']) theta0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlTwist']) # -- Adjust for Aerodyn15 r = r0 + self.Rhub chord_intfun = interpolate.interp1d(r0,chord0, bounds_error=None, fill_value='extrapolate', kind='zero') chord = chord_intfun(r) theta_intfun = interpolate.interp1d(r0,theta0, bounds_error=None, fill_value='extrapolate', kind='zero') theta = theta_intfun(r) af_idx = np.array(self.fast.fst_vt['AeroDynBlade']['BlAFID']).astype(int) - 1 #Reset to 0 index AFNames = self.fast.fst_vt['AeroDyn15']['AFNames'] # Use airfoil data from FAST file read, assumes AeroDyn 15, assumes 1 Re num per airfoil af_dict = {} try: for i, _ in enumerate(self.fast.fst_vt['AeroDyn15']['af_data']): Re = [self.fast.fst_vt['AeroDyn15']['af_data'][i][0]['Re']] Alpha = self.fast.fst_vt['AeroDyn15']['af_data'][i][0]['Alpha'] Cl = self.fast.fst_vt['AeroDyn15']['af_data'][i][0]['Cl'] Cd = self.fast.fst_vt['AeroDyn15']['af_data'][i][0]['Cd'] Cm = self.fast.fst_vt['AeroDyn15']['af_data'][i][0]['Cm'] af_dict[i] = CCAirfoil(Alpha, Re, Cl, Cd, Cm) except: # Read airfoil tables without tab cabalities (will remove once wisdem master branch cleans up) for i, _ in enumerate(self.fast.fst_vt['AeroDyn15']['af_data']): Re = [self.fast.fst_vt['AeroDyn15']['af_data'][i]['Re']] Alpha = self.fast.fst_vt['AeroDyn15']['af_data'][i]['Alpha'] Cl = self.fast.fst_vt['AeroDyn15']['af_data'][i]['Cl'] Cd = self.fast.fst_vt['AeroDyn15']['af_data'][i]['Cd'] Cm = self.fast.fst_vt['AeroDyn15']['af_data'][i]['Cm'] af_dict[i] = CCAirfoil(Alpha, Re, Cl, Cd, Cm) # define airfoils for CCBlade af = [0]*len(r) for i in range(len(r)): af[i] = af_dict[af_idx[i]] # Now save the CC-Blade rotor nSector = 8 # azimuthal discretizations self.cc_rotor = CCBlade(r, chord, theta, af, self.Rhub, self.rotor_radius, self.NumBl, rho=self.rho, mu=self.mu, precone=-self.precone, tilt=-self.tilt, yaw=self.yaw, shearExp=self.shearExp, hubHt=self.hubHt, nSector=nSector) print('CCBlade initiated successfully.') # Generate the look-up tables, mesh the grid and flatten the arrays for cc_rotor aerodynamic analysis TSR_initial = np.arange(3, 15,0.5) pitch_initial = np.arange(-1,25,0.5) pitch_initial_rad = pitch_initial * deg2rad ws_array = np.ones_like(TSR_initial) * self.v_rated # evaluate at rated wind speed omega_array = (TSR_initial * ws_array / self.rotor_radius) * RadSec2rpm ws_mesh, pitch_mesh = np.meshgrid(ws_array, pitch_initial) ws_flat = ws_mesh.flatten() pitch_flat = pitch_mesh.flatten() omega_mesh, _ = np.meshgrid(omega_array, pitch_initial) omega_flat = omega_mesh.flatten() # tsr_flat = (omega_flat * rpm2RadSec * self.rotor_radius) / ws_flat # Get values from cc-blade print('Running CCBlade aerodynamic analysis, this may take a minute...') try: _, _, _, _, CP, CT, CQ, _ = self.cc_rotor.evaluate(ws_flat, omega_flat, pitch_flat, coefficients=True) except ValueError: # On IEAontology4all outputs, derivs = self.cc_rotor.evaluate(ws_flat, omega_flat, pitch_flat, coefficients=True) CP = outputs['CP'] CT = outputs['CT'] CQ = outputs['CQ'] print('CCBlade aerodynamic analysis run successfully.') # Reshape Cp, Ct and Cq Cp = np.transpose(np.reshape(CP, (len(pitch_initial), len(TSR_initial)))) Ct = np.transpose(np.reshape(CT, (len(pitch_initial), len(TSR_initial)))) Cq = np.transpose(np.reshape(CQ, (len(pitch_initial), len(TSR_initial)))) # Store necessary metrics for analysis self.pitch_initial_rad = pitch_initial_rad self.TSR_initial = TSR_initial self.Cp_table = Cp self.Ct_table = Ct self.Cq_table = Cq # Save some blade parameters self.span = r self.chord = chord self.twist = theta def load_blade_info(self): ''' Loads wind turbine blade data from an OpenFAST model. Should be used if blade information is needed (i.e.) for flap controller tuning, but a rotor performance file is defined and and cc-blade is not run already. Parameters: ----------- self - note: needs to contain fast input file info provided by load_from_fast. ''' from wisdem.aeroelasticse.FAST_reader import InputReader_OpenFAST # Load Fast input deck # self.TurbineName = FAST_InputFile.strip('.fst') # fast = InputReader_OpenFAST(FAST_ver=FAST_ver,dev_branch=dev_branch) # self.fast.FAST_InputFile = FAST_InputFile # self.fast.FAST_directory = FAST_directory # self.fast.execute() # Make sure cc_rotor exists for DAC analysis try: if self.cc_rotor: self.af_data = self.fast.fst_vt['AeroDyn15']['af_data'] self.bld_flapwise_damp = self.fast.fst_vt['ElastoDynBlade']['BldFlDmp1']/100 * 0.7 except AttributeError: # Create CC-Blade Rotor r0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlSpn']) chord0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlChord']) theta0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlTwist']) # -- Adjust for Aerodyn15 r = r0 + self.Rhub chord_intfun = interpolate.interp1d(r0,chord0, bounds_error=None, fill_value='extrapolate', kind='zero') chord = chord_intfun(r) theta_intfun = interpolate.interp1d(r0,theta0, bounds_error=None, fill_value='extrapolate', kind='zero') theta = theta_intfun(r) af_idx = np.array(self.fast.fst_vt['AeroDynBlade']['BlAFID']).astype(int) - 1 #Reset to 0 index AFNames = self.fast.fst_vt['AeroDyn15']['AFNames'] # Use airfoil data from FAST file read, assumes AeroDyn 15, assumes 1 Re num per airfoil af_dict = {} for i, section in enumerate(self.fast.fst_vt['AeroDyn15']['af_data']): Re = [section[0]['Re']] Alpha = section[0]['Alpha'] if section[0]['NumTabs'] > 1: # sections with flaps ref_tab = int(np.floor(section[0]['NumTabs']/2)) Cl = section[ref_tab]['Cl'] Cd = section[ref_tab]['Cd'] Cm = section[ref_tab]['Cm'] af_dict[i] = CCAirfoil(Alpha, Re, Cl, Cd, Cm) else: # sections without flaps Cl = section[0]['Cl'] Cd = section[0]['Cd'] Cm = section[0]['Cm'] af_dict[i] = CCAirfoil(Alpha, Re, Cl, Cd, Cm) # define airfoils for CCBlade af = [0]*len(r) for i in range(len(r)): af[i] = af_dict[af_idx[i]] # Now save the CC-Blade rotor nSector = 8 # azimuthal discretizations self.cc_rotor = CCBlade(r, chord, theta, af, self.Rhub, self.rotor_radius, self.NumBl, rho=self.rho, mu=self.mu, precone=-self.precone, tilt=-self.tilt, yaw=self.yaw, shearExp=self.shearExp, hubHt=self.hubHt, nSector=nSector) # Save some blade data self.af_data = self.fast.fst_vt['AeroDyn15']['af_data'] self.span = r self.chord = chord self.twist = theta self.bld_flapwise_damp = self.fast.fst_vt['ElastoDynBlade']['BldFlDmp1']/100 * 0.7
class Cp_Ct_Cq_Tables(ExplicitComponent): def initialize(self): self.options.declare('modeling_options') # self.options.declare('n_span') # self.options.declare('n_pitch', default=20) # self.options.declare('n_tsr', default=20) # self.options.declare('n_U', default=1) # self.options.declare('n_aoa') # self.options.declare('n_re') def setup(self): modeling_options = self.options['modeling_options'] blade_init_options = modeling_options['blade'] servose_init_options = modeling_options['servose'] airfoils = modeling_options['airfoils'] self.n_span = n_span = blade_init_options['n_span'] self.n_aoa = n_aoa = airfoils['n_aoa'] # Number of angle of attacks self.n_Re = n_Re = airfoils[ 'n_Re'] # Number of Reynolds, so far hard set at 1 self.n_tab = n_tab = airfoils[ 'n_tab'] # Number of tabulated data. For distributed aerodynamic control this could be > 1 self.n_pitch = n_pitch = servose_init_options['n_pitch_perf_surfaces'] self.n_tsr = n_tsr = servose_init_options['n_tsr_perf_surfaces'] self.n_U = n_U = servose_init_options['n_U_perf_surfaces'] self.min_TSR = servose_init_options['min_tsr_perf_surfaces'] self.max_TSR = servose_init_options['max_tsr_perf_surfaces'] self.min_pitch = servose_init_options['min_pitch_perf_surfaces'] self.max_pitch = servose_init_options['max_pitch_perf_surfaces'] # parameters self.add_input('v_min', val=0.0, units='m/s', desc='cut-in wind speed') self.add_input('v_max', val=0.0, units='m/s', desc='cut-out wind speed') self.add_input( 'r', val=np.zeros(n_span), units='m', desc= 'radial locations where blade is defined (should be increasing and not go all the way to hub or tip)' ) self.add_input('chord', val=np.zeros(n_span), units='m', desc='chord length at each section') self.add_input( 'theta', val=np.zeros(n_span), units='deg', desc= 'twist angle at each section (positive decreases angle of attack)') self.add_input('Rhub', val=0.0, units='m', desc='hub radius') self.add_input('Rtip', val=0.0, units='m', desc='tip radius') self.add_input('hub_height', val=0.0, units='m', desc='hub height') self.add_input('precone', val=0.0, units='deg', desc='precone angle') self.add_input('tilt', val=0.0, units='deg', desc='shaft tilt') self.add_input('yaw', val=0.0, units='deg', desc='yaw error') self.add_input('precurve', val=np.zeros(n_span), units='m', desc='precurve at each section') self.add_input('precurveTip', val=0.0, units='m', desc='precurve at tip') self.add_input('presweep', val=np.zeros(n_span), units='m', desc='presweep at each section') self.add_input('presweepTip', val=0.0, units='m', desc='presweep at tip') self.add_input('rho', val=1.225, units='kg/m**3', desc='density of air') self.add_input('mu', val=1.81e-5, units='kg/(m*s)', desc='dynamic viscosity of air') self.add_input('shearExp', val=0.0, desc='shear exponent') # self.add_discrete_input('airfoils', val=[0]*n_span, desc='CCAirfoil instances') self.add_input('airfoils_cl', val=np.zeros((n_span, n_aoa, n_Re, n_tab)), desc='lift coefficients, spanwise') self.add_input('airfoils_cd', val=np.zeros((n_span, n_aoa, n_Re, n_tab)), desc='drag coefficients, spanwise') self.add_input('airfoils_cm', val=np.zeros((n_span, n_aoa, n_Re, n_tab)), desc='moment coefficients, spanwise') self.add_input('airfoils_aoa', val=np.zeros((n_aoa)), units='deg', desc='angle of attack grid for polars') self.add_input('airfoils_Re', val=np.zeros((n_Re)), desc='Reynolds numbers of polars') self.add_discrete_input('nBlades', val=0, desc='number of blades') self.add_discrete_input( 'nSector', val=4, desc= 'number of sectors to divide rotor face into in computing thrust and power' ) self.add_discrete_input('tiploss', val=True, desc='include Prandtl tip loss model') self.add_discrete_input('hubloss', val=True, desc='include Prandtl hub loss model') self.add_discrete_input( 'wakerotation', val=True, desc= 'include effect of wake rotation (i.e., tangential induction factor is nonzero)' ) self.add_discrete_input( 'usecd', val=True, desc='use drag coefficient in computing induction factors') self.add_input('pitch_vector_in', val=np.zeros(n_pitch), units='deg', desc='pitch vector specified by the user') self.add_input('tsr_vector_in', val=np.zeros(n_tsr), desc='tsr vector specified by the user') self.add_input('U_vector_in', val=np.zeros(n_U), units='m/s', desc='wind vector specified by the user') # outputs self.add_output('Cp', val=np.zeros((n_tsr, n_pitch, n_U)), desc='table of aero power coefficient') self.add_output('Ct', val=np.zeros((n_tsr, n_pitch, n_U)), desc='table of aero thrust coefficient') self.add_output('Cq', val=np.zeros((n_tsr, n_pitch, n_U)), desc='table of aero torque coefficient') self.add_output('pitch_vector', val=np.zeros(n_pitch), units='deg', desc='pitch vector used') self.add_output('tsr_vector', val=np.zeros(n_tsr), desc='tsr vector used') self.add_output('U_vector', val=np.zeros(n_U), units='m/s', desc='wind vector used') def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # Create Airfoil class instances af = [None] * self.n_span for i in range(self.n_span): if self.n_tab > 1: ref_tab = int(np.floor(self.n_tab / 2)) af[i] = CCAirfoil(inputs['airfoils_aoa'], inputs['airfoils_Re'], inputs['airfoils_cl'][i, :, :, ref_tab], inputs['airfoils_cd'][i, :, :, ref_tab], inputs['airfoils_cm'][i, :, :, ref_tab]) else: af[i] = CCAirfoil(inputs['airfoils_aoa'], inputs['airfoils_Re'], inputs['airfoils_cl'][i, :, :, 0], inputs['airfoils_cd'][i, :, :, 0], inputs['airfoils_cm'][i, :, :, 0]) n_pitch = self.n_pitch n_tsr = self.n_tsr n_U = self.n_U min_TSR = self.min_TSR max_TSR = self.max_TSR min_pitch = self.min_pitch max_pitch = self.max_pitch U_vector = inputs['U_vector_in'] V_in = inputs['v_min'] V_out = inputs['v_max'] tsr_vector = inputs['tsr_vector_in'] pitch_vector = inputs['pitch_vector_in'] self.ccblade = CCBlade( inputs['r'], inputs['chord'], inputs['theta'], af, inputs['Rhub'], inputs['Rtip'], discrete_inputs['nBlades'], inputs['rho'], inputs['mu'], inputs['precone'], inputs['tilt'], inputs['yaw'], inputs['shearExp'], inputs['hub_height'], discrete_inputs['nSector'], inputs['precurve'], inputs['precurveTip'], inputs['presweep'], inputs['presweepTip'], discrete_inputs['tiploss'], discrete_inputs['hubloss'], discrete_inputs['wakerotation'], discrete_inputs['usecd']) if max(U_vector) == 0.: U_vector = np.linspace(V_in[0], V_out[0], n_U) if max(tsr_vector) == 0.: tsr_vector = np.linspace(min_TSR, max_TSR, n_tsr) if max(pitch_vector) == 0.: pitch_vector = np.linspace(min_pitch, max_pitch, n_pitch) outputs['pitch_vector'] = pitch_vector outputs['tsr_vector'] = tsr_vector outputs['U_vector'] = U_vector R = inputs['Rtip'] k = 0 for i in range(n_U): for j in range(n_tsr): k += 1 # if k/2. == int(k/2.) : print('Cp-Ct-Cq surfaces completed at ' + str(int(k / (n_U * n_tsr) * 100.)) + ' %') U = U_vector[i] * np.ones(n_pitch) Omega = tsr_vector[j] * U_vector[ i] / R * 30. / np.pi * np.ones(n_pitch) myout, _ = self.ccblade.evaluate(U, Omega, pitch_vector, coefficients=True) outputs['Cp'][j, :, i], outputs['Ct'][j, :, i], outputs['Cq'][ j, :, i] = [myout[key] for key in ['CP', 'CT', 'CQ']]
class Turbine(): """ Class Turbine defines a turbine in terms of what is needed to design the controller and to run the 'tiny' simulation. Primary functions (at a high level): - Reads an OpenFAST input deck - Runs cc-blade rotor performance analysis - Writes a text file containing Cp, Ct, and Cq tables Methods: ------- __str__ load save load_from_fast load_from_ccblade load_from_txt generate_rotperf_fast write_rotor_performance Parameters: ----------- turbine_params : dict Dictionary containing known turbine paramaters that are not directly available from OpenFAST input files """ def __init__(self, turbine_params): """ Load turbine parameters from input dictionary """ print('-----------------------------------------------------------------------------') print('Loading wind turbine data for NREL\'s ROSCO tuning and simulation processeses') print('-----------------------------------------------------------------------------') # ------ Turbine Parameters------ self.rotor_inertia = turbine_params['rotor_inertia'] self.rated_rotor_speed = turbine_params['rated_rotor_speed'] self.v_min = turbine_params['v_min'] self.v_rated = turbine_params['v_rated'] self.v_max = turbine_params['v_max'] self.max_pitch_rate = turbine_params['max_pitch_rate'] self.min_pitch_rate = -1 * self.max_pitch_rate self.max_torque_rate = turbine_params['max_torque_rate'] self.rated_power = turbine_params['rated_power'] self.bld_edgewise_freq = turbine_params['bld_edgewise_freq'] if turbine_params['twr_freq']: self.twr_freq = turbine_params['twr_freq'] else: self.twr_freq = 0.0 if turbine_params['ptfm_freq']: self.ptfm_freq = turbine_params['ptfm_freq'] else: self.ptfm_freq = 0.0 if turbine_params['TSR_operational']: self.TSR_operational = turbine_params['TSR_operational'] else: self.TSR_operational = None if turbine_params['bld_flapwise_freq']: self.bld_flapwise_freq = turbine_params['bld_flapwise_freq'] else: self.bld_flapwise_freq = 0.0 # Allow print out of class def __str__(self): ''' Print some data about the turbine. ''' print('-------------- Turbine Info --------------') print('Turbine Name: {}'.format(self.TurbineName)) print('Rated Power: {} [W]'.format(self.rated_power)) print('Total Inertia: {:.1f} [kg m^2]'.format(self.J)) print('Rotor Radius: {:.1f} [m]'.format(self.rotor_radius)) print('Rated Rotor Speed: {:.1f} [rad/s]'.format(self.rated_rotor_speed)) print('Max Cp: {:.2f}'.format(self.Cp.max)) print('------------------------------------------') return ' ' # Save function def save(self,filename): ''' Save turbine to pickle Parameters: ---------- filename : str Name of file to save pickle # ''' pickle.dump(self, open( filename, "wb" ) ) # Load function @staticmethod def load(filename): ''' Load turbine from pickle - outdated, but might be okay!! Parameters: ---------- filename : str Name of pickle file ''' turbine = pickle.load(open(filename,'rb')) return turbine # Load data from fast input deck def load_from_fast(self, FAST_InputFile,FAST_directory, FAST_ver='OpenFAST',dev_branch=True,rot_source=None, txt_filename=None): """ Load the parameter files directly from a FAST input deck Parameters: ----------- Fast_InputFile: str Primary fast model input file (*.fst) FAST_directory: str Directory for primary fast model input file FAST_ver: string, optional fast version, usually OpenFAST dev_branch: bool, optional dev_branch input to InputReader_OpenFAST, probably True rot_source: str, optional desired source for rotor to get Cp, Ct, Cq tables. Default is to run cc-blade. options: cc-blade - run cc-blade txt - from *.txt file txt_filename: str, optional filename for *.txt, only used if rot_source='txt' """ from wisdem.aeroelasticse.FAST_reader import InputReader_OpenFAST print('Loading FAST model: %s ' % FAST_InputFile) self.TurbineName = FAST_InputFile.strip('.fst') fast = self.fast = InputReader_OpenFAST(FAST_ver=FAST_ver,dev_branch=dev_branch) fast.FAST_InputFile = FAST_InputFile fast.FAST_directory = FAST_directory fast.execute() if txt_filename: self.rotor_performance_filename = txt_filename else: self.rotor_performance_filename = 'Cp_Ct_Cq.txt' # Grab general turbine parameters self.TipRad = fast.fst_vt['ElastoDyn']['TipRad'] self.Rhub = fast.fst_vt['ElastoDyn']['HubRad'] self.hubHt = fast.fst_vt['ElastoDyn']['TowerHt'] + fast.fst_vt['ElastoDyn']['Twr2Shft'] self.NumBl = fast.fst_vt['ElastoDyn']['NumBl'] self.TowerHt = fast.fst_vt['ElastoDyn']['TowerHt'] self.shearExp = 0.2 #HARD CODED FOR NOW self.rho = fast.fst_vt['AeroDyn15']['AirDens'] self.mu = fast.fst_vt['AeroDyn15']['KinVisc'] self.Ng = fast.fst_vt['ElastoDyn']['GBRatio'] self.GenEff = fast.fst_vt['ServoDyn']['GenEff'] self.GBoxEff = fast.fst_vt['ElastoDyn']['GBoxEff'] self.DTTorSpr = fast.fst_vt['ElastoDyn']['DTTorSpr'] self.generator_inertia = fast.fst_vt['ElastoDyn']['GenIner'] self.tilt = fast.fst_vt['ElastoDyn']['ShftTilt'] try: self.precone = fast.fst_vt['ElastoDyn']['PreCone1'] # May need to change to PreCone(1) depending on OpenFAST files except: self.precone = fast.fst_vt['ElastoDyn']['PreCone(1)'] self.yaw = 0.0 self.J = self.rotor_inertia + self.generator_inertia * self.Ng**2 self.rated_torque = self.rated_power/(self.GenEff/100*self.rated_rotor_speed*self.Ng*self.GBoxEff/100) self.max_torque = self.rated_torque * 1.1 self.rotor_radius = self.TipRad # self.omega_dt = np.sqrt(self.DTTorSpr/self.J) # Load Cp, Ct, Cq tables if rot_source == 'cc-blade': # Use cc-blade self.load_from_ccblade() elif rot_source == 'txt': # Use specified text file file_processing = ROSCO_utilities.FileProcessing() self.pitch_initial_rad, self.TSR_initial, self.Cp_table, self.Ct_table, self.Cq_table = file_processing.load_from_txt( txt_filename) else: # Use text file from DISCON.in if os.path.exists(os.path.join(FAST_directory, fast.fst_vt['ServoDyn']['DLL_InFile'])): try: self.pitch_initial_rad = fast.fst_vt['DISCON_in']['Cp_pitch_initial_rad'] self.TSR_initial = fast.fst_vt['DISCON_in']['Cp_TSR_initial'] self.Cp_table = fast.fst_vt['DISCON_in']['Cp_table'] self.Ct_table = fast.fst_vt['DISCON_in']['Ct_table'] self.Cq_table = fast.fst_vt['DISCON_in']['Cq_table'] except: # Load from cc-blade print('No rotor performance data source available, running CC-Blade.') self.load_from_ccblade() # Parse rotor performance data self.Cp = RotorPerformance(self.Cp_table,self.pitch_initial_rad,self.TSR_initial) self.Ct = RotorPerformance(self.Ct_table,self.pitch_initial_rad,self.TSR_initial) self.Cq = RotorPerformance(self.Cq_table,self.pitch_initial_rad,self.TSR_initial) # Define operational TSR if not self.TSR_operational: self.TSR_operational = self.Cp.TSR_opt # Pull out some floating-related data wave_tp = fast.fst_vt['HydroDyn']['WaveTp'] try: self.wave_peak_period = 1/wave_tp # Will work if HydroDyn exists and a peak period is defined... except: self.wave_peak_period = 0.0 # Set as 0.0 when HydroDyn doesn't exist (fixed bottom) # Load rotor performance data from CCBlade def load_from_ccblade(self): ''' Loads rotor performance information by running cc-blade aerodynamic analysis. Designed to work with Aerodyn15 blade input files. Parameters: ----------- fast: dict Dictionary containing fast model details - defined using from InputReader_OpenFAST (distributed as a part of AeroelasticSE) ''' print('Loading rotor performance data from CC-Blade.') # Create CC-Blade Rotor r0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlSpn']) chord0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlChord']) theta0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlTwist']) # -- Adjust for Aerodyn15 r = r0 + self.Rhub chord_intfun = interpolate.interp1d(r0,chord0, bounds_error=None, fill_value='extrapolate', kind='zero') chord = chord_intfun(r) theta_intfun = interpolate.interp1d(r0,theta0, bounds_error=None, fill_value='extrapolate', kind='zero') theta = theta_intfun(r) af_idx = np.array(self.fast.fst_vt['AeroDynBlade']['BlAFID']).astype(int) - 1 #Reset to 0 index AFNames = self.fast.fst_vt['AeroDyn15']['AFNames'] # Use airfoil data from FAST file read, assumes AeroDyn 15, assumes 1 Re num per airfoil af_dict = {} try: for i, _ in enumerate(self.fast.fst_vt['AeroDyn15']['af_data']): Re = [self.fast.fst_vt['AeroDyn15']['af_data'][i][0]['Re']] Alpha = self.fast.fst_vt['AeroDyn15']['af_data'][i][0]['Alpha'] Cl = self.fast.fst_vt['AeroDyn15']['af_data'][i][0]['Cl'] Cd = self.fast.fst_vt['AeroDyn15']['af_data'][i][0]['Cd'] Cm = self.fast.fst_vt['AeroDyn15']['af_data'][i][0]['Cm'] af_dict[i] = CCAirfoil(Alpha, Re, Cl, Cd, Cm) except: # Read airfoil tables without tab cabalities (will remove once wisdem master branch cleans up) for i, _ in enumerate(self.fast.fst_vt['AeroDyn15']['af_data']): Re = [self.fast.fst_vt['AeroDyn15']['af_data'][i]['Re']] Alpha = self.fast.fst_vt['AeroDyn15']['af_data'][i]['Alpha'] Cl = self.fast.fst_vt['AeroDyn15']['af_data'][i]['Cl'] Cd = self.fast.fst_vt['AeroDyn15']['af_data'][i]['Cd'] Cm = self.fast.fst_vt['AeroDyn15']['af_data'][i]['Cm'] af_dict[i] = CCAirfoil(Alpha, Re, Cl, Cd, Cm) # define airfoils for CCBlade af = [0]*len(r) for i in range(len(r)): af[i] = af_dict[af_idx[i]] # Now save the CC-Blade rotor nSector = 8 # azimuthal discretizations self.cc_rotor = CCBlade(r, chord, theta, af, self.Rhub, self.rotor_radius, self.NumBl, rho=self.rho, mu=self.mu, precone=-self.precone, tilt=-self.tilt, yaw=self.yaw, shearExp=self.shearExp, hubHt=self.hubHt, nSector=nSector) print('CCBlade initiated successfully.') # Generate the look-up tables, mesh the grid and flatten the arrays for cc_rotor aerodynamic analysis TSR_initial = np.arange(3, 15,0.5) pitch_initial = np.arange(-1,25,0.5) pitch_initial_rad = pitch_initial * deg2rad ws_array = np.ones_like(TSR_initial) * self.v_rated # evaluate at rated wind speed omega_array = (TSR_initial * ws_array / self.rotor_radius) * RadSec2rpm ws_mesh, pitch_mesh = np.meshgrid(ws_array, pitch_initial) ws_flat = ws_mesh.flatten() pitch_flat = pitch_mesh.flatten() omega_mesh, _ = np.meshgrid(omega_array, pitch_initial) omega_flat = omega_mesh.flatten() # tsr_flat = (omega_flat * rpm2RadSec * self.rotor_radius) / ws_flat # Get values from cc-blade print('Running CCBlade aerodynamic analysis, this may take a minute...') try: _, _, _, _, CP, CT, CQ, _ = self.cc_rotor.evaluate(ws_flat, omega_flat, pitch_flat, coefficients=True) except ValueError: # On IEAontology4all outputs, derivs = self.cc_rotor.evaluate(ws_flat, omega_flat, pitch_flat, coefficients=True) CP = outputs['CP'] CT = outputs['CT'] CQ = outputs['CQ'] print('CCBlade aerodynamic analysis run successfully.') # Reshape Cp, Ct and Cq Cp = np.transpose(np.reshape(CP, (len(pitch_initial), len(TSR_initial)))) Ct = np.transpose(np.reshape(CT, (len(pitch_initial), len(TSR_initial)))) Cq = np.transpose(np.reshape(CQ, (len(pitch_initial), len(TSR_initial)))) # Store necessary metrics for analysis self.pitch_initial_rad = pitch_initial_rad self.TSR_initial = TSR_initial self.Cp_table = Cp self.Ct_table = Ct self.Cq_table = Cq # Save some blade parameters self.span = r self.chord = chord self.twist = theta def generate_rotperf_fast(self, openfast_path, FAST_runDirectory=None, run_BeamDyn=False, debug_level=1, run_type='multi'): ''' Use openfast to generate Cp surface data. Will be slow, especially if using BeamDyn, but may be necessary if cc-blade is not sufficient. Parameters: ----------- openfast_path: str path to openfast FAST_runDirectory: str directory to run openfast simulations in run_BeamDyn: bool Flag to run beamdyn - does not exist yet debug_level: float 0 - no outputs, 1 - simple outputs, 2 - all outputs run_type: str 'serial' - run in serial, 'multi' - run using python multiprocessing tools, 'mpi' - run using mpi tools ''' # Load additional WISDEM tools from wisdem.aeroelasticse import runFAST_pywrapper, CaseGen_General from wisdem.aeroelasticse.Util import FileTools # Load pCrunch tools from pCrunch import pdTools, Processing # setup values for surface v0 = self.v_rated + 2 TSR_initial = np.arange(3, 15,1) pitch_initial = np.arange(-1,25,1) rotspeed_initial = TSR_initial*v0/self.rotor_radius * RadSec2rpm # rpms # Specify Case Inputs case_inputs = {} # ------- Setup OpenFAST inputs -------- case_inputs[('Fst','TMax')] = {'vals': [330], 'group': 0} case_inputs[('Fst','Compinflow')] = {'vals': [1], 'group': 0} case_inputs[('Fst','CompAero')] = {'vals': [2], 'group': 0} case_inputs[('Fst','CompServo')] = {'vals': [1], 'group': 0} case_inputs[('Fst','CompHydro')] = {'vals': [0], 'group': 0} if run_BeamDyn: case_inputs[('Fst','CompElast')] = {'vals': [2], 'group': 0} else: case_inputs[('Fst','CompElast')] = {'vals': [1], 'group': 0} case_inputs[('Fst', 'OutFileFmt')] = {'vals': [2], 'group': 0} # AeroDyn15 case_inputs[('AeroDyn15', 'WakeMod')] = {'vals': [1], 'group': 0} case_inputs[('AeroDyn15', 'AfAeroMod')] = {'vals': [1], 'group': 0} case_inputs[('AeroDyn15', 'TwrPotent')] = {'vals': [0], 'group': 0} # ElastoDyn case_inputs[('ElastoDyn', 'FlapDOF1')] = {'vals': ['True'], 'group': 0} case_inputs[('ElastoDyn', 'FlapDOF2')] = {'vals': ['True'], 'group': 0} case_inputs[('ElastoDyn', 'EdgeDOF')] = {'vals': ['True'], 'group': 0} case_inputs[('ElastoDyn', 'TeetDOF')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'DrTrDOF')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'GenDOF')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'YawDOF')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'TwFADOF1')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'TwFADOF2')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'TwSSDOF1')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'TwSSDOF2')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'PtfmSgDOF')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'PtfmSwDOF')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'PtfmHvDOF')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'PtfmRDOF')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'PtfmPDOF')] = {'vals': ['False'], 'group': 0} case_inputs[('ElastoDyn', 'PtfmYDOF')] = {'vals': ['False'], 'group': 0} # BeamDyn # NEEDED # InflowWind case_inputs[('InflowWind', 'WindType')] = {'vals': [1], 'group': 0} case_inputs[('InflowWind', 'HWindSpeed')] = {'vals': [v0], 'group': 0} case_inputs[('InflowWind', 'PLexp')] = {'vals': [0], 'group': 0} # ServoDyn case_inputs[('ServoDyn', 'PCMode')] = {'vals': [0], 'group': 0} case_inputs[('ServoDyn', 'VSContrl')] = {'vals': [0], 'group': 0} case_inputs[('ServoDyn', 'HSSBrMode')] = {'vals': [0], 'group': 0} case_inputs[('ServoDyn', 'YCMode')] = {'vals': [0], 'group': 0} # ------- Setup sweep values inputs -------- case_inputs[('ElastoDyn', 'BlPitch1')] = {'vals': list(pitch_initial), 'group': 1} case_inputs[('ElastoDyn', 'BlPitch2')] = {'vals': list(pitch_initial), 'group': 1} case_inputs[('ElastoDyn', 'BlPitch3')] = {'vals': list(pitch_initial), 'group': 1} case_inputs[('ElastoDyn', 'RotSpeed')] = {'vals': list(rotspeed_initial), 'group': 2} # FAST details fastBatch = runFAST_pywrapper.runFAST_pywrapper_batch(FAST_ver='OpenFAST', dev_branch=True) fastBatch.FAST_exe = openfast_path # Path to executable fastBatch.FAST_InputFile = self.fast.FAST_InputFile fastBatch.FAST_directory = self.fast.FAST_directory if not FAST_runDirectory: FAST_runDirectory = os.path.join(os.getcwd(), 'RotPerf_OpenFAST') fastBatch.FAST_runDirectory = FAST_runDirectory fastBatch.debug_level = debug_level # Generate cases case_name_base = self.TurbineName + '_rotperf' case_list, case_name_list = CaseGen_General.CaseGen_General( case_inputs, dir_matrix=fastBatch.FAST_runDirectory, namebase=case_name_base) fastBatch.case_list = case_list fastBatch.case_name_list = case_name_list # Make sure proper outputs exist var_out = [ # ElastoDyn (this is probably overkill on the outputs) "BldPitch1", "BldPitch2", "BldPitch3", "Azimuth", "RotSpeed", "GenSpeed", "NacYaw", "OoPDefl1", "IPDefl1", "TwstDefl1", "OoPDefl2", "IPDefl2", "TwstDefl2", "OoPDefl3", "IPDefl3", "TwstDefl3", "RootFxc1", "RootFyc1", "RootFzc1", "RootMxc1", "RootMyc1", "RootMzc1", "RootFxc2", "RootFyc2", "RootFzc2", "RootMxc2", "RootMyc2", "RootMzc2", "RootFxc3", "RootFyc3", "RootFzc3", "RootMxc3", "RootMyc3", "RootMzc3", "Spn1MLxb1", "Spn1MLyb1", "Spn1MLzb1", "Spn1MLxb2", "Spn1MLyb2", "Spn1MLzb2", "Spn1MLxb3", "Spn1MLyb3", "Spn1MLzb3", "RotThrust", "LSSGagFya", "LSSGagFza", "RotTorq", "LSSGagMya", "LSSGagMza", # ServoDyn "GenPwr", "GenTq", # AeroDyn15 "RtArea", "RtVAvgxh", "B1N3Clrnc", "B2N3Clrnc", "B3N3Clrnc", "RtAeroCp", 'RtAeroCq', 'RtAeroCt', 'RtTSR', # NECESSARY # InflowWind "Wind1VelX", ] channels = {} for var in var_out: channels[var] = True fastBatch.channels = channels # Run OpenFAST if run_type.lower() == 'multi': fastBatch.run_multi() elif run_type.lower()=='mpi': fastBatch.run_mpi() elif run_type.lower()=='serial': fastBatch.run_serial() # ========== Post Processing ========== # Save statistics fp = Processing.FAST_Processing() # Find all outfiles fname_case_matrix = os.path.join(FAST_runDirectory, 'case_matrix.yaml') case_matrix = FileTools.load_yaml(fname_case_matrix, package=1) cm = pd.DataFrame(case_matrix) # Parse case matrix and find outfiles names outfiles = [] case_names = cm['Case_Name'] outfiles = [] for name in case_names: outfiles.append(os.path.join(FAST_runDirectory, name + '.outb')) # Set some processing parameters fp.OpenFAST_outfile_list = outfiles fp.namebase = case_name_base fp.t0 = 270 fp.parallel_analysis = True fp.results_dir = os.path.join(FAST_runDirectory,'stats') fp.verbose = True # Save for debug! fp.save_LoadRanking = False fp.save_SummaryStats = False print('Processing openfast data on {} cores.'.format(fp.parallel_cores)) # Load and save statistics and load rankings stats, load_rankings = fp.batch_processing() # Get means of last 30 seconds of 300 second simulation CP = stats[0]['RtAeroCp']['mean'] CT = stats[0]['RtAeroCt']['mean'] CQ = stats[0]['RtAeroCq']['mean'] # Reshape Cp, Ct and Cq Cp = np.transpose(np.reshape(CP, (len(pitch_initial), len(TSR_initial)))) Ct = np.transpose(np.reshape(CT, (len(pitch_initial), len(TSR_initial)))) Cq = np.transpose(np.reshape(CQ, (len(pitch_initial), len(TSR_initial)))) # Store necessary metrics for analysis self.pitch_initial_rad = pitch_initial * deg2rad self.TSR_initial = TSR_initial self.Cp_table = Cp self.Ct_table = Ct self.Cq_table = Cq def load_blade_info(self): ''' Loads wind turbine blade data from an OpenFAST model. Should be used if blade information is needed (i.e.) for flap controller tuning, but a rotor performance file is defined and and cc-blade is not run already. Parameters: ----------- self - note: needs to contain fast input file info provided by load_from_fast. ''' from wisdem.aeroelasticse.FAST_reader import InputReader_OpenFAST # Load Fast input deck # self.TurbineName = FAST_InputFile.strip('.fst') # fast = InputReader_OpenFAST(FAST_ver=FAST_ver,dev_branch=dev_branch) # self.fast.FAST_InputFile = FAST_InputFile # self.fast.FAST_directory = FAST_directory # self.fast.execute() # Make sure cc_rotor exists for DAC analysis try: if self.cc_rotor: self.af_data = self.fast.fst_vt['AeroDyn15']['af_data'] self.bld_flapwise_damp = self.fast.fst_vt['ElastoDynBlade']['BldFlDmp1']/100 * 0.7 except AttributeError: # Create CC-Blade Rotor r0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlSpn']) chord0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlChord']) theta0 = np.array(self.fast.fst_vt['AeroDynBlade']['BlTwist']) # -- Adjust for Aerodyn15 r = r0 + self.Rhub chord_intfun = interpolate.interp1d(r0,chord0, bounds_error=None, fill_value='extrapolate', kind='zero') chord = chord_intfun(r) theta_intfun = interpolate.interp1d(r0,theta0, bounds_error=None, fill_value='extrapolate', kind='zero') theta = theta_intfun(r) af_idx = np.array(self.fast.fst_vt['AeroDynBlade']['BlAFID']).astype(int) - 1 #Reset to 0 index AFNames = self.fast.fst_vt['AeroDyn15']['AFNames'] # Use airfoil data from FAST file read, assumes AeroDyn 15, assumes 1 Re num per airfoil af_dict = {} for i, section in enumerate(self.fast.fst_vt['AeroDyn15']['af_data']): Re = [section[0]['Re']] Alpha = section[0]['Alpha'] if section[0]['NumTabs'] > 1: # sections with flaps ref_tab = int(np.floor(section[0]['NumTabs']/2)) Cl = section[ref_tab]['Cl'] Cd = section[ref_tab]['Cd'] Cm = section[ref_tab]['Cm'] af_dict[i] = CCAirfoil(Alpha, Re, Cl, Cd, Cm) else: # sections without flaps Cl = section[0]['Cl'] Cd = section[0]['Cd'] Cm = section[0]['Cm'] af_dict[i] = CCAirfoil(Alpha, Re, Cl, Cd, Cm) # define airfoils for CCBlade af = [0]*len(r) for i in range(len(r)): af[i] = af_dict[af_idx[i]] # Now save the CC-Blade rotor nSector = 8 # azimuthal discretizations self.cc_rotor = CCBlade(r, chord, theta, af, self.Rhub, self.rotor_radius, self.NumBl, rho=self.rho, mu=self.mu, precone=-self.precone, tilt=-self.tilt, yaw=self.yaw, shearExp=self.shearExp, hubHt=self.hubHt, nSector=nSector) # Save some blade data self.af_data = self.fast.fst_vt['AeroDyn15']['af_data'] self.span = r self.chord = chord self.twist = theta self.bld_flapwise_damp = self.fast.fst_vt['ElastoDynBlade']['BldFlDmp1']/100 * 0.7
class Performance_coef(unittest.TestCase): def setup(self): """ Parameters ---------- r : array_like (m) locations defining the blade along z-axis of :ref:`blade coordinate system <azimuth_blade_coord>` (values should be increasing). chord : array_like (m) corresponding chord length at each section theta : array_like (deg) corresponding :ref:`twist angle <blade_airfoil_coord>` at each section--- positive twist decreases angle of attack. Rhub : float (m) location of hub Rtip : float (m) location of tip B : int, optional number of blades rho : float, optional (kg/m^3) freestream fluid density mu : float, optional (kg/m/s) dynamic viscosity of fluid precone : float, optional (deg) :ref:`hub precone angle <azimuth_blade_coord>` tilt : float, optional (deg) nacelle :ref:`tilt angle <yaw_hub_coord>` yaw : float, optional (deg) nacelle :ref:`yaw angle<wind_yaw_coord>` shearExp : float, optional shear exponent for a power-law wind profile across hub hubHt : float, optional hub height used for power-law wind profile. U = Uref*(z/hubHt)**shearExp nSector : int, optional number of azimuthal sectors to descretize aerodynamic calculation. automatically set to ``1`` if tilt, yaw, and shearExp are all 0.0. Otherwise set to a minimum of 4. """ self.Rhub = 1.5 self.Rtip = 55.60721094924956 B = 3 # number of blades # atmosphere rho = 1.225 mu = 1.81206e-5 tilt = 5.0 precone = 2.5 yaw = 0.0 shearExp = 0.2 hubHt = (self.Rtip * 2) * 0.92 nSector = 8 tsr = 7.55 # max chord length c_max = 5 # lift coefficients for the given airfoils Cl = [ 1.3871, 1.3871, 1.3871, 1.3871, 1.464, 1.464, 1.464, 1.464, 1.4509, 1.4509, 1.4509, 1.4509, 1.4509, 1.4509 ] self.r = np.array([ self.Rtip * 0.045503175, self.Rtip * 0.088888889, self.Rtip * 0.132274603, self.Rtip * 0.186507937, self.Rtip * 0.251587302, self.Rtip * 0.316666667, self.Rtip * 0.381746032, self.Rtip * 0.446825397, self.Rtip * 0.511904762, self.Rtip * 0.576984127, self.Rtip * 0.642063492, self.Rtip * 0.707142857, self.Rtip * 0.772222222, self.Rtip * 0.837301587, self.Rtip * 0.891534921, self.Rtip * 0.934920635, self.Rtip * 0.978306349 ]) afinit = CCAirfoil.initFromAerodynFile # just for shorthand basepath = 'C:/Users/Thomas/OneDrive/Tieto/Airfoils/' # load all airfoils airfoil_types = [0] * 5 airfoil_types[0] = afinit(basepath + 'Cylinder1.dat') airfoil_types[1] = afinit(basepath + 'Cylinder2.dat') airfoil_types[2] = afinit(basepath + 'S818_output3_extrap.txt') airfoil_types[3] = afinit(basepath + 'S830_output_extrap.txt') airfoil_types[4] = afinit(basepath + 'S831_output_extrap.txt') # place at appropriate radial sections af_idx = [0, 0, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4] af = [0] * len(self.r) for i in range(len(self.r)): af[i] = airfoil_types[af_idx[i]] # chord = np.array([3.542, 3.854, 4.167, 4.557, 4.652, 4.458, 4.249, 4.007, 3.748, # 3.502, 3.256, 3.010, 2.764, 2.518, 2.313, 2.086, 1.419]) chord = np.array( blade_geometry(self.Rtip, self.r, B, Cl, tsr, c_max, plot=False)) theta = np.array([ 13.308, 13.308, 13.308, 13.308, 11.480, 10.162, 9.011, 7.795, 6.544, 5.361, 4.188, 3.125, 2.319, 1.526, 0.863, 0.370, 0.106 ]) # create CCBlade object self.aeroanalysis = CCBlade(self.r, chord, theta, af, self.Rhub, self.Rtip, B, rho, mu, precone, tilt, yaw, shearExp, hubHt, nSector) def compute(self): ''' Uinf : array_like (m/s) hub height wind speed Omega : array_like (RPM) rotor rotation speed pitch : array_like (deg) blade pitch setting ''' # set conditions Uinf = np.array([ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 ]) Omega = np.array([ 6.972, 7.183, 7.506, 7.942, 8.469, 9.156, 10.296, 11.431, 11.890, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100 ]) pitch = np.array([ 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 3.823, 6.602, 8.668, 10.450, 12.055, 13.536, 14.920, 16.226, 17.473, 18.699, 19.941, 21.177, 22.347, 23.469 ]) P, T, Q, M, CP, CT, CQ, CM = self.aeroanalysis.evaluate( Uinf, Omega, pitch, coefficients=True) print('CP:' + str(CP)) path = "C:/Users/Thomas/OneDrive/Tieto/Scripts/Output/Cp_res.txt" with open(path, 'w') as f: f.write('Cp:\n') for i in CP: f.write("%s\n" % str(i)) f.write('\n') f.write('Radius: \n') f.write(str(self.Rtip)) f.close()
cm[i, :, 0, 0] = np.interp(aoa, airfoils[i]['polars'][0]['c_m']['grid'], airfoils[i]['polars'][0]['c_m']['values']) af = [None] * len(s) for i in range(len(s)): af[i] = CCAirfoil(np.rad2deg(aoa), [1e+6], cl[i, :, 0, 0], cd[i, :, 0, 0], cm[i, :, 0, 0]) ########## Run CCBlade ########## get_cp_cm = CCBlade(r, chord, twist, af, hub_r, Rtip, B, rho, mu, cone, tilt, 0., shearExp, hub_height, nSector, presweep, precurve[-1], presweep, presweep[-1], tiploss, hubloss, wakerotation, usecd) # get_cp_cm.induction = True Omega = Uhub * tsr / Rtip * 30.0 / np.pi # Rotor speed myout, derivs = get_cp_cm.evaluate([Uhub], [Omega], [pitch], coefficients=True) P, T, Q, M, CP, CT, CQ, CM = [ myout[key] for key in ['P', 'T', 'Q', 'M', 'CP', 'CT', 'CQ', 'CM'] ] get_cp_cm.induction_inflow = True loads, deriv = get_cp_cm.distributedAeroLoads([Uhub], Omega, pitch, 0.) ########## Post Process Output ########## # Compute forces in the airfoil coordinate system, pag 21 of https://www.nrel.gov/docs/fy13osti/58819.pdf P_b = DirectionVector(loads['Np'], -loads['Tp'], 0) P_af = P_b.bladeToAirfoil(twist) # Compute lift and drag forces F = P_b.bladeToAirfoil(twist + loads['alpha'] + pitch) # Print output .dat file with the quantities distributed along span np.savetxt( os.path.join(output_folder, 'distributed_quantities.dat'),
dNp_dprecone = dNp['dprecone'] dNp_dtilt = dNp['dtilt'] dNp_dhubHt = dNp['dhubHt'] dNp_dyaw = dNp['dyaw'] dNp_dazimuth = dNp['dazimuth'] dNp_dUinf = dNp['dUinf'] dNp_dOmega = dNp['dOmega'] dNp_dpitch = dNp['dpitch'] # 5 ---------- # 6 ---------- loads, derivs \ = rotor.evaluate([Uinf], [Omega], [pitch]) P = loads['P'] T = loads['T'] Q = loads['Q'] dP = derivs['dP'] dT = derivs['dT'] dQ = derivs['dQ'] npts = len(P) # npts x 1 dP_dprecone = dP['dprecone'] dP_dtilt = dP['dtilt'] dP_dhubHt = dP['dhubHt'] dP_dRhub = dP['dRhub']
if plot_flag: plt.plot(rstar, Tp / 1e3, label='lead-lag') plt.plot(rstar, Np / 1e3, label='flapwise') plt.xlabel('blade fraction') plt.ylabel('distributed aerodynamic loads (kN)') plt.legend(loc='upper left') plt.grid() plt.show() # 5 ---------- # plt.savefig('/Users/sning/Dropbox/NREL/SysEng/TWISTER/ccblade-beta-setup/docs/images/distributedAeroLoads.pdf') # plt.savefig('/Users/sning/Dropbox/NREL/SysEng/TWISTER/ccblade-beta-setup/docs/images/distributedAeroLoads.png') # 6 ---------- outputs, derivs = rotor.evaluate([Uinf], [Omega], [pitch]) P = outputs['P'] T = outputs['T'] Q = outputs['Q'] outputs, derivs = rotor.evaluate([Uinf], [Omega], [pitch], coefficients=True) CP = outputs['CP'] CT = outputs['CT'] CQ = outputs['CQ'] print('CP =', CP) print('CT =', CT) print('CQ =', CQ)
class TestNREL5MW(unittest.TestCase): def setUp(self): # geometry Rhub = 1.5 Rtip = 63.0 r = np.array([ 2.8667, 5.6000, 8.3333, 11.7500, 15.8500, 19.9500, 24.0500, 28.1500, 32.2500, 36.3500, 40.4500, 44.5500, 48.6500, 52.7500, 56.1667, 58.9000, 61.6333 ]) chord = np.array([ 3.542, 3.854, 4.167, 4.557, 4.652, 4.458, 4.249, 4.007, 3.748, 3.502, 3.256, 3.010, 2.764, 2.518, 2.313, 2.086, 1.419 ]) theta = np.array([ 13.308, 13.308, 13.308, 13.308, 11.480, 10.162, 9.011, 7.795, 6.544, 5.361, 4.188, 3.125, 2.319, 1.526, 0.863, 0.370, 0.106 ]) B = 3 # number of blades # atmosphere rho = 1.225 mu = 1.81206e-5 afinit = CCAirfoil.initFromAerodynFile # just for shorthand basepath = path.join(path.dirname(path.realpath(__file__)), '5MW_AFFiles') # load all airfoils airfoil_types = [0] * 8 airfoil_types[0] = afinit(path.join(basepath, 'Cylinder1.dat')) airfoil_types[1] = afinit(path.join(basepath, 'Cylinder2.dat')) airfoil_types[2] = afinit(path.join(basepath, 'DU40_A17.dat')) airfoil_types[3] = afinit(path.join(basepath, 'DU35_A17.dat')) airfoil_types[4] = afinit(path.join(basepath, 'DU30_A17.dat')) airfoil_types[5] = afinit(path.join(basepath, 'DU25_A17.dat')) airfoil_types[6] = afinit(path.join(basepath, 'DU21_A17.dat')) airfoil_types[7] = afinit(path.join(basepath, 'NACA64_A17.dat')) # place at appropriate radial stations af_idx = [0, 0, 1, 2, 3, 3, 4, 5, 5, 6, 6, 7, 7, 7, 7, 7, 7] af = [0] * len(r) for i in range(len(r)): af[i] = airfoil_types[af_idx[i]] tilt = -5.0 precone = 2.5 yaw = 0.0 # create CCBlade object self.rotor = CCBlade(r, chord, theta, af, Rhub, Rtip, B, rho, mu, precone, tilt, yaw, shearExp=0.2, hubHt=90.0) def test_thrust_torque(self): Uinf = np.array([ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 ]) Omega = np.array([ 6.972, 7.183, 7.506, 7.942, 8.469, 9.156, 10.296, 11.431, 11.890, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100, 12.100 ]) pitch = np.array([ 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 3.823, 6.602, 8.668, 10.450, 12.055, 13.536, 14.920, 16.226, 17.473, 18.699, 19.941, 21.177, 22.347, 23.469 ]) Pref = np.array([ 42.9, 188.2, 427.9, 781.3, 1257.6, 1876.2, 2668.0, 3653.0, 4833.2, 5296.6, 5296.6, 5296.6, 5296.6, 5296.6, 5296.6, 5296.6, 5296.6, 5296.7, 5296.6, 5296.7, 5296.6, 5296.6, 5296.7 ]) Tref = np.array([ 171.7, 215.9, 268.9, 330.3, 398.6, 478.0, 579.2, 691.5, 790.6, 690.0, 608.4, 557.9, 520.5, 491.2, 467.7, 448.4, 432.3, 418.8, 406.7, 395.3, 385.1, 376.7, 369.3 ]) Qref = np.array([ 58.8, 250.2, 544.3, 939.5, 1418.1, 1956.9, 2474.5, 3051.1, 3881.3, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1, 4180.1 ]) m_rotor = 110.0 # kg g = 9.81 tilt = 5 * math.pi / 180.0 Tref -= m_rotor * g * math.sin( tilt ) # remove weight of rotor that is included in reported results P, T, Q, M = self.rotor.evaluate(Uinf, Omega, pitch) # import matplotlib.pyplot as plt # plt.plot(Uinf, P/1e6) # plt.plot(Uinf, Pref/1e3) # plt.figure() # plt.plot(Uinf, T/1e6) # plt.plot(Uinf, Tref/1e3) # plt.show() idx = (Uinf < 15) np.testing.assert_allclose(Q[idx] / 1e6, Qref[idx] / 1e3, atol=0.15) np.testing.assert_allclose(P[idx] / 1e6, Pref[idx] / 1e3, atol=0.2) # within 0.2 of 1MW np.testing.assert_allclose(T[idx] / 1e6, Tref[idx] / 1e3, atol=0.15)
def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): r = inputs['r'] chord = inputs['chord'] theta = inputs['theta'] Rhub = inputs['Rhub'] Rtip = inputs['Rtip'] hub_height = inputs['hub_height'] precone = inputs['precone'] tilt = inputs['tilt'] yaw = inputs['yaw'] precurve = inputs['precurve'] precurveTip = inputs['precurveTip'] # airfoils = discrete_inputs['airfoils'] B = discrete_inputs['nBlades'] rho = inputs['rho'] mu = inputs['mu'] shearExp = inputs['shearExp'] nSector = discrete_inputs['nSector'] tiploss = discrete_inputs['tiploss'] hubloss = discrete_inputs['hubloss'] wakerotation = discrete_inputs['wakerotation'] usecd = discrete_inputs['usecd'] Uhub = inputs['Uhub'] Omega = inputs['Omega'] pitch = inputs['pitch'] af = [None] * self.naero for i in range(self.naero): af[i] = CCAirfoil(inputs['airfoils_aoa'], inputs['airfoils_Re'], inputs['airfoils_cl'][:, i, :], inputs['airfoils_cd'][:, i, :], inputs['airfoils_cm'][:, i, :]) ccblade = CCBlade(r, chord, theta, af, Rhub, Rtip, B, rho, mu, precone, tilt, yaw, shearExp, hub_height, nSector, precurve, precurveTip, tiploss=tiploss, hubloss=hubloss, wakerotation=wakerotation, usecd=usecd, derivatives=True) # power, thrust, torque myoutputs, self.derivs = ccblade.evaluate(Uhub, Omega, pitch, coefficients=False) outputs['T'] = myoutputs['T'] outputs['Q'] = myoutputs['Q'] outputs['P'] = myoutputs['P']
class ComputePowerCurve(ExplicitComponent): """ Iteratively call CCBlade to compute the power curve. """ def initialize(self): self.options.declare('modeling_options') def setup(self): modeling_options = self.options['modeling_options'] self.n_span = n_span = modeling_options['blade']['n_span'] # self.n_af = n_af = af_init_options['n_af'] # Number of airfoils self.n_aoa = n_aoa = modeling_options['airfoils'][ 'n_aoa'] # Number of angle of attacks self.n_Re = n_Re = modeling_options['airfoils'][ 'n_Re'] # Number of Reynolds, so far hard set at 1 self.n_tab = n_tab = modeling_options['airfoils'][ 'n_tab'] # Number of tabulated data. For distributed aerodynamic control this could be > 1 # self.n_xy = n_xy = af_init_options['n_xy'] # Number of coordinate points to describe the airfoil geometry self.regulation_reg_III = modeling_options['servose'][ 'regulation_reg_III'] # n_span = self.n_span = self.options['n_span'] self.n_pc = modeling_options['servose']['n_pc'] self.n_pc_spline = modeling_options['servose']['n_pc_spline'] # n_aoa = self.options['n_aoa'] # n_re = self.options['n_re'] # parameters self.add_input('v_min', val=0.0, units='m/s', desc='cut-in wind speed') self.add_input('v_max', val=0.0, units='m/s', desc='cut-out wind speed') self.add_input('rated_power', val=0.0, units='W', desc='electrical rated power') self.add_input('omega_min', val=0.0, units='rpm', desc='minimum allowed rotor rotation speed') self.add_input('omega_max', val=0.0, units='rpm', desc='maximum allowed rotor rotation speed') self.add_input('control_maxTS', val=0.0, units='m/s', desc='maximum allowed blade tip speed') self.add_input( 'tsr_operational', val=0.0, desc='tip-speed ratio in Region 2 (should be optimized externally)' ) self.add_input( 'control_pitch', val=0.0, units='deg', desc= 'pitch angle in region 2 (and region 3 for fixed pitch machines)') self.add_discrete_input('drivetrainType', val='GEARED') self.add_input('gearbox_efficiency', val=0.0, desc='Gearbox efficiency') self.add_input('generator_efficiency', val=0.0, desc='Generator efficiency') self.add_input( 'r', val=np.zeros(n_span), units='m', desc= 'radial locations where blade is defined (should be increasing and not go all the way to hub or tip)' ) self.add_input('chord', val=np.zeros(n_span), units='m', desc='chord length at each section') self.add_input( 'theta', val=np.zeros(n_span), units='deg', desc= 'twist angle at each section (positive decreases angle of attack)') self.add_input('Rhub', val=0.0, units='m', desc='hub radius') self.add_input('Rtip', val=0.0, units='m', desc='tip radius') self.add_input('hub_height', val=0.0, units='m', desc='hub height') self.add_input( 'precone', val=0.0, units='deg', desc='precone angle', ) self.add_input( 'tilt', val=0.0, units='deg', desc='shaft tilt', ) self.add_input( 'yaw', val=0.0, units='deg', desc='yaw error', ) self.add_input('precurve', val=np.zeros(n_span), units='m', desc='precurve at each section') self.add_input('precurveTip', val=0.0, units='m', desc='precurve at tip') self.add_input('presweep', val=np.zeros(n_span), units='m', desc='presweep at each section') self.add_input('presweepTip', val=0.0, units='m', desc='presweep at tip') # self.add_discrete_input('airfoils', val=[0]*n_span, desc='CCAirfoil instances') self.add_input('airfoils_cl', val=np.zeros((n_span, n_aoa, n_Re, n_tab)), desc='lift coefficients, spanwise') self.add_input('airfoils_cd', val=np.zeros((n_span, n_aoa, n_Re, n_tab)), desc='drag coefficients, spanwise') self.add_input('airfoils_cm', val=np.zeros((n_span, n_aoa, n_Re, n_tab)), desc='moment coefficients, spanwise') self.add_input('airfoils_aoa', val=np.zeros((n_aoa)), units='deg', desc='angle of attack grid for polars') self.add_input('airfoils_Re', val=np.zeros((n_Re)), desc='Reynolds numbers of polars') self.add_discrete_input('nBlades', val=0, desc='number of blades') self.add_input('rho', val=1.225, units='kg/m**3', desc='density of air') self.add_input('mu', val=1.81e-5, units='kg/(m*s)', desc='dynamic viscosity of air') self.add_input('shearExp', val=0.0, desc='shear exponent') self.add_discrete_input( 'nSector', val=4, desc= 'number of sectors to divide rotor face into in computing thrust and power' ) self.add_discrete_input('tiploss', val=True, desc='include Prandtl tip loss model') self.add_discrete_input('hubloss', val=True, desc='include Prandtl hub loss model') self.add_discrete_input( 'wakerotation', val=True, desc= 'include effect of wake rotation (i.e., tangential induction factor is nonzero)' ) self.add_discrete_input( 'usecd', val=True, desc='use drag coefficient in computing induction factors') # outputs self.add_output('V', val=np.zeros(self.n_pc), units='m/s', desc='wind vector') self.add_output('Omega', val=np.zeros(self.n_pc), units='rpm', desc='rotor rotational speed') self.add_output('pitch', val=np.zeros(self.n_pc), units='deg', desc='rotor pitch schedule') self.add_output('P', val=np.zeros(self.n_pc), units='W', desc='rotor electrical power') self.add_output('T', val=np.zeros(self.n_pc), units='N', desc='rotor aerodynamic thrust') self.add_output('Q', val=np.zeros(self.n_pc), units='N*m', desc='rotor aerodynamic torque') self.add_output('M', val=np.zeros(self.n_pc), units='N*m', desc='blade root moment') self.add_output('Cp', val=np.zeros(self.n_pc), desc='rotor electrical power coefficient') self.add_output('Cp_aero', val=np.zeros(self.n_pc), desc='rotor aerodynamic power coefficient') self.add_output('Ct_aero', val=np.zeros(self.n_pc), desc='rotor aerodynamic thrust coefficient') self.add_output('Cq_aero', val=np.zeros(self.n_pc), desc='rotor aerodynamic torque coefficient') self.add_output('Cm_aero', val=np.zeros(self.n_pc), desc='rotor aerodynamic moment coefficient') self.add_output('V_R25', val=0.0, units='m/s', desc='region 2.5 transition wind speed') self.add_output('rated_V', val=0.0, units='m/s', desc='rated wind speed') self.add_output('rated_Omega', val=0.0, units='rpm', desc='rotor rotation speed at rated') self.add_output('rated_pitch', val=0.0, units='deg', desc='pitch setting at rated') self.add_output('rated_T', val=0.0, units='N', desc='rotor aerodynamic thrust at rated') self.add_output('rated_Q', val=0.0, units='N*m', desc='rotor aerodynamic torque at rated') self.add_output( 'ax_induct_regII', val=np.zeros(n_span), desc='rotor axial induction at cut-in wind speed along blade span') self.add_output( 'tang_induct_regII', val=np.zeros(n_span), desc= 'rotor tangential induction at cut-in wind speed along blade span') self.add_output( 'aoa_regII', val=np.zeros(n_span), units='deg', desc= 'angle of attack distribution along blade span at cut-in wind speed' ) self.add_output('Cp_regII', val=0.0, desc='power coefficient at cut-in wind speed') self.add_output( 'cl_regII', val=np.zeros(n_span), desc= 'lift coefficient distribution along blade span at cut-in wind speed' ) self.add_output( 'cd_regII', val=np.zeros(n_span), desc= 'drag coefficient distribution along blade span at cut-in wind speed' ) # self.declare_partials('*', '*', method='fd', form='central', step=1e-6) def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # Create Airfoil class instances af = [None] * self.n_span for i in range(self.n_span): if self.n_tab > 1: ref_tab = int(np.floor(self.n_tab / 2)) af[i] = CCAirfoil(inputs['airfoils_aoa'], inputs['airfoils_Re'], inputs['airfoils_cl'][i, :, :, ref_tab], inputs['airfoils_cd'][i, :, :, ref_tab], inputs['airfoils_cm'][i, :, :, ref_tab]) else: af[i] = CCAirfoil(inputs['airfoils_aoa'], inputs['airfoils_Re'], inputs['airfoils_cl'][i, :, :, 0], inputs['airfoils_cd'][i, :, :, 0], inputs['airfoils_cm'][i, :, :, 0]) self.ccblade = CCBlade( inputs['r'], inputs['chord'], inputs['theta'], af, inputs['Rhub'], inputs['Rtip'], discrete_inputs['nBlades'], inputs['rho'], inputs['mu'], inputs['precone'], inputs['tilt'], inputs['yaw'], inputs['shearExp'], inputs['hub_height'], discrete_inputs['nSector'], inputs['precurve'], inputs['precurveTip'], inputs['presweep'], inputs['presweepTip'], discrete_inputs['tiploss'], discrete_inputs['hubloss'], discrete_inputs['wakerotation'], discrete_inputs['usecd']) # JPJ: what is this grid for? Seems to be a special distribution of velocities # for the hub grid0 = np.cumsum( np.abs( np.diff( np.cos(np.linspace(-np.pi / 4., np.pi / 2., self.n_pc + 1))))) grid1 = (grid0 - grid0[0]) / (grid0[-1] - grid0[0]) Uhub = grid1 * (inputs['v_max'] - inputs['v_min']) + inputs['v_min'] P_aero = np.zeros(Uhub.shape) Cp_aero = np.zeros(Uhub.shape) Ct_aero = np.zeros(Uhub.shape) Cq_aero = np.zeros(Uhub.shape) Cm_aero = np.zeros(Uhub.shape) P = np.zeros(Uhub.shape) Cp = np.zeros(Uhub.shape) T = np.zeros(Uhub.shape) Q = np.zeros(Uhub.shape) M = np.zeros(Uhub.shape) pitch = np.zeros(Uhub.shape) + inputs['control_pitch'] # Unpack variables P_rated = inputs['rated_power'] R_tip = inputs['Rtip'] tsr = inputs['tsr_operational'] driveType = discrete_inputs['drivetrainType'] driveEta = inputs['gearbox_efficiency'] * inputs['generator_efficiency'] # Set rotor speed based on TSR Omega_tsr = Uhub * tsr / R_tip # Determine maximum rotor speed (rad/s)- either by TS or by control input Omega_max = min([ inputs['control_maxTS'] / R_tip, inputs['omega_max'] * np.pi / 30. ]) # Apply maximum and minimum rotor speed limits Omega = np.maximum(np.minimum(Omega_tsr, Omega_max), inputs['omega_min'] * np.pi / 30.) Omega_rpm = Omega * 30. / np.pi # Set baseline power production myout, derivs = self.ccblade.evaluate(Uhub, Omega_rpm, pitch, coefficients=True) P_aero, T, Q, M, Cp_aero, Ct_aero, Cq_aero, Cm_aero = [ myout[key] for key in ['P', 'T', 'Q', 'M', 'CP', 'CT', 'CQ', 'CM'] ] P, eff = compute_P_and_eff(P_aero, P_rated, driveType, driveEta) Cp = Cp_aero * eff # Find Region 3 index region_bool = np.nonzero(P >= P_rated)[0] if len(region_bool) == 0: i_3 = self.n_pc region3 = False else: i_3 = region_bool[0] + 1 region3 = True # Guess at Region 2.5, but we will do a more rigorous search below if Omega_max < Omega_tsr[-1]: U_2p5 = np.interp(Omega[-1], Omega_tsr, Uhub) outputs['V_R25'] = U_2p5 else: U_2p5 = Uhub[-1] i_2p5 = np.nonzero(U_2p5 <= Uhub)[0][0] # Find rated index and guess at rated speed if P_aero[-1] > P_rated: U_rated = np.interp(P_rated, P_aero * eff, Uhub) else: U_rated = Uhub[-1] i_rated = np.nonzero(U_rated <= Uhub)[0][0] # Function to be used inside of power maximization until Region 3 def maximizePower(pitch, Uhub, Omega_rpm): myout, _ = self.ccblade.evaluate([Uhub], [Omega_rpm], [pitch], coefficients=False) return -myout['P'] # Maximize power until Region 3 region2p5 = False for i in range(i_3): # No need to optimize if already doing well if Omega[i] == Omega_tsr[i]: continue # Find pitch value that gives highest power rating pitch0 = pitch[i] if i == 0 else pitch[i - 1] bnds = [pitch0 - 10., pitch0 + 10.] pitch[i] = minimize_scalar( lambda x: maximizePower(x, Uhub[i], Omega_rpm[i]), bounds=bnds, method='bounded', options={ 'disp': False, 'xatol': 1e-2, 'maxiter': 40 })['x'] # Find associated power myout, _ = self.ccblade.evaluate([Uhub[i]], [Omega_rpm[i]], [pitch[i]], coefficients=True) P_aero[i], T[i], Q[i], M[i], Cp_aero[i], Ct_aero[i], Cq_aero[ i], Cm_aero[i] = [ myout[key] for key in ['P', 'T', 'Q', 'M', 'CP', 'CT', 'CQ', 'CM'] ] P[i], eff = compute_P_and_eff(P_aero[i], P_rated, driveType, driveEta) Cp[i] = Cp_aero[i] * eff # Note if we find Region 2.5 if ((not region2p5) and (Omega[i] == Omega_max) and (P[i] < P_rated)): region2p5 = True i_2p5 = i # Stop if we find Region 3 early if P[i] > P_rated: i_3 = i + 1 i_rated = i break # Solve for rated velocity # JPJ: why rename i_rated to i here? It removes clarity in the following 50 lines that we're looking at the rated properties i = i_rated if i < self.n_pc - 1: def const_Urated(x): pitch = x[0] Uhub_i = x[1] Omega_i = min([Uhub_i * tsr / R_tip, Omega_max]) myout, _ = self.ccblade.evaluate([Uhub_i], [Omega_i * 30. / np.pi], [pitch], coefficients=False) P_aero_i = myout['P'] P_i, eff = compute_P_and_eff(P_aero_i.flatten(), P_rated, driveType, driveEta) return (P_i - P_rated) if region2p5: # Have to search over both pitch and speed x0 = [0.0, Uhub[i]] bnds = [ np.sort([pitch[i - 1], pitch[i + 1]]), [Uhub[i - 1], Uhub[i + 1]] ] const = {} const['type'] = 'eq' const['fun'] = const_Urated params_rated = minimize(lambda x: x[1], x0, method='slsqp', bounds=bnds, constraints=const, tol=1e-3) if params_rated.success and not np.isnan(params_rated.x[1]): U_rated = params_rated.x[1] pitch[i] = params_rated.x[0] else: U_rated = U_rated # Use guessed value earlier pitch[i] = 0.0 else: # Just search over speed pitch[i] = 0.0 try: U_rated = brentq(lambda x: const_Urated([0.0, x]), Uhub[i - 1], Uhub[i + 1], xtol=1e-4, rtol=1e-5, maxiter=40, disp=False) except ValueError: U_rated = minimize_scalar( lambda x: np.abs(const_Urated([0.0, x])), bounds=[Uhub[i - 1], Uhub[i + 1]], method='bounded', options={ 'disp': False, 'xatol': 1e-3, 'maxiter': 40 })['x'] Omega_rated = min([U_rated * tsr / R_tip, Omega_max]) Omega[i:] = np.minimum( Omega[i:], Omega_rated) # Stay at this speed if hit rated too early Omega_rpm = Omega * 30. / np.pi myout, _ = self.ccblade.evaluate([U_rated], [Omega_rpm[i]], [pitch[i]], coefficients=True) P_aero[i], T[i], Q[i], M[i], Cp_aero[i], Ct_aero[i], Cq_aero[ i], Cm_aero[i] = [ myout[key] for key in ['P', 'T', 'Q', 'M', 'CP', 'CT', 'CQ', 'CM'] ] P[i], eff = compute_P_and_eff(P_aero[i], P_rated, driveType, driveEta) Cp[i] = Cp_aero[i] * eff P[i] = P_rated # Store rated speed in array Uhub[i_rated] = U_rated # Store outputs outputs['rated_V'] = np.float64(U_rated) outputs['rated_Omega'] = Omega_rpm[i] outputs['rated_pitch'] = pitch[i] outputs['rated_T'] = T[i] outputs['rated_Q'] = Q[i] # JPJ: this part can be converted into a BalanceComp with a solver. # This will be less expensive and allow us to get derivatives through the process. if region3: # Function to be used to stay at rated power in Region 3 def rated_power_dist(pitch, Uhub, Omega_rpm): myout, _ = self.ccblade.evaluate([Uhub], [Omega_rpm], [pitch], coefficients=False) P_aero = myout['P'] P, eff = compute_P_and_eff(P_aero, P_rated, driveType, driveEta) return (P - P_rated) # Solve for Region 3 pitch options = {'disp': False} if self.regulation_reg_III: for i in range(i_3, self.n_pc): pitch0 = pitch[i - 1] try: pitch[i] = brentq(lambda x: rated_power_dist( x, Uhub[i], Omega_rpm[i]), pitch0, pitch0 + 10., xtol=1e-4, rtol=1e-5, maxiter=40, disp=False) except ValueError: pitch[i] = minimize_scalar( lambda x: np.abs( rated_power_dist(x, Uhub[i], Omega_rpm[i])), bounds=[pitch0 - 5., pitch0 + 15.], method='bounded', options={ 'disp': False, 'xatol': 1e-3, 'maxiter': 40 })['x'] myout, _ = self.ccblade.evaluate([Uhub[i]], [Omega_rpm[i]], [pitch[i]], coefficients=True) P_aero[i], T[i], Q[i], M[i], Cp_aero[i], Ct_aero[ i], Cq_aero[i], Cm_aero[i] = [ myout[key] for key in ['P', 'T', 'Q', 'M', 'CP', 'CT', 'CQ', 'CM'] ] P[i], eff = compute_P_and_eff(P_aero[i], P_rated, driveType, driveEta) Cp[i] = Cp_aero[i] * eff #P[i] = P_rated else: P[i_3:] = P_rated T[i_3:] = 0 Q[i_3:] = P[i_3:] / Omega[i_3:] M[i_3:] = 0 pitch[i_3:] = 0 Cp[i_3:] = P[i_3:] / (0.5 * inputs['rho'] * np.pi * R_tip**2 * Uhub[i_3:]**3) Ct_aero[i_3:] = 0 Cq_aero[i_3:] = 0 Cm_aero[i_3:] = 0 outputs['T'] = T outputs['Q'] = Q outputs['Omega'] = Omega_rpm outputs['P'] = P outputs['Cp'] = Cp outputs['Cp_aero'] = Cp_aero outputs['Ct_aero'] = Ct_aero outputs['Cq_aero'] = Cq_aero outputs['Cm_aero'] = Cm_aero outputs['V'] = Uhub outputs['M'] = M outputs['pitch'] = pitch self.ccblade.induction_inflow = True tsr_vec = Omega_rpm / 30. * np.pi * R_tip / Uhub id_regII = np.argmin(abs(tsr_vec - inputs['tsr_operational'])) loads, derivs = self.ccblade.distributedAeroLoads( Uhub[id_regII], Omega_rpm[id_regII], pitch[id_regII], 0.0) # outputs outputs['ax_induct_regII'] = loads['a'] outputs['tang_induct_regII'] = loads['ap'] outputs['aoa_regII'] = loads['alpha'] outputs['cl_regII'] = loads['Cl'] outputs['cd_regII'] = loads['Cd'] outputs['Cp_regII'] = Cp_aero[id_regII]
class CCBladePower(ExplicitComponent): def initialize(self): self.options.declare('naero') self.options.declare('npower') self.options.declare('n_aoa_grid') self.options.declare('n_Re_grid') def setup(self): self.naero = naero = self.options['naero'] npower = self.options['npower'] n_aoa_grid = self.options['n_aoa_grid'] n_Re_grid = self.options['n_Re_grid'] """blade element momentum code""" # inputs self.add_input('Uhub', val=np.zeros(npower), units='m/s', desc='hub height wind speed') self.add_input('Omega', val=np.zeros(npower), units='rpm', desc='rotor rotation speed') self.add_input('pitch', val=np.zeros(npower), units='deg', desc='blade pitch setting') # outputs self.add_output('T', val=np.zeros(npower), units='N', desc='rotor aerodynamic thrust') self.add_output('Q', val=np.zeros(npower), units='N*m', desc='rotor aerodynamic torque') self.add_output('P', val=np.zeros(npower), units='W', desc='rotor aerodynamic power') # (potential) variables self.add_input( 'r', val=np.zeros(naero), units='m', desc= 'radial locations where blade is defined (should be increasing and not go all the way to hub or tip)' ) self.add_input('chord', val=np.zeros(naero), units='m', desc='chord length at each section') self.add_input( 'theta', val=np.zeros(naero), units='deg', desc= 'twist angle at each section (positive decreases angle of attack)') self.add_input('Rhub', val=0.0, units='m', desc='hub radius') self.add_input('Rtip', val=0.0, units='m', desc='tip radius') self.add_input('hub_height', val=0.0, units='m', desc='hub height') self.add_input('precone', val=0.0, desc='precone angle', units='deg') self.add_input('tilt', val=0.0, desc='shaft tilt', units='deg') self.add_input('yaw', val=0.0, desc='yaw error', units='deg') # TODO: I've not hooked up the gradients for these ones yet. self.add_input('precurve', val=np.zeros(naero), units='m', desc='precurve at each section') self.add_input('precurveTip', val=0.0, units='m', desc='precurve at tip') # parameters self.add_input('airfoils_cl', val=np.zeros((n_aoa_grid, naero, n_Re_grid)), desc='lift coefficients, spanwise') self.add_input('airfoils_cd', val=np.zeros((n_aoa_grid, naero, n_Re_grid)), desc='drag coefficients, spanwise') self.add_input('airfoils_cm', val=np.zeros((n_aoa_grid, naero, n_Re_grid)), desc='moment coefficients, spanwise') self.add_input('airfoils_aoa', val=np.zeros((n_aoa_grid)), units='deg', desc='angle of attack grid for polars') self.add_input('airfoils_Re', val=np.zeros((n_Re_grid)), desc='Reynolds numbers of polars') # self.add_discrete_input('airfoils', val=[0]*naero, desc='CCAirfoil instances') self.add_discrete_input('nBlades', val=0, desc='number of blades') self.add_input('rho', val=0.0, units='kg/m**3', desc='density of air') self.add_input('mu', val=0.0, units='kg/(m*s)', desc='dynamic viscosity of air') self.add_input('shearExp', val=0.0, desc='shear exponent') self.add_discrete_input( 'nSector', val=4, desc= 'number of sectors to divide rotor face into in computing thrust and power' ) self.add_discrete_input('tiploss', val=True, desc='include Prandtl tip loss model') self.add_discrete_input('hubloss', val=True, desc='include Prandtl hub loss model') self.add_discrete_input( 'wakerotation', val=True, desc= 'include effect of wake rotation (i.e., tangential induction factor is nonzero)' ) self.add_discrete_input( 'usecd', val=True, desc='use drag coefficient in computing induction factors') #self.declare_partials(['P', 'T', 'Q'],['precone', 'tilt', 'hub_height', 'Rhub', 'Rtip', 'yaw', # 'Uhub', 'Omega', 'pitch', 'r', 'chord', 'theta', # 'precurve', 'precurveTip']) def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): self.r = inputs['r'] self.chord = inputs['chord'] self.theta = inputs['theta'] self.Rhub = inputs['Rhub'] self.Rtip = inputs['Rtip'] self.hub_height = inputs['hub_height'] self.precone = inputs['precone'] self.tilt = inputs['tilt'] self.yaw = inputs['yaw'] self.precurve = inputs['precurve'] self.precurveTip = inputs['precurveTip'] # self.airfoils = discrete_inputs['airfoils'] self.B = discrete_inputs['nBlades'] self.rho = inputs['rho'] self.mu = inputs['mu'] self.shearExp = inputs['shearExp'] self.nSector = discrete_inputs['nSector'] self.tiploss = discrete_inputs['tiploss'] self.hubloss = discrete_inputs['hubloss'] self.wakerotation = discrete_inputs['wakerotation'] self.usecd = discrete_inputs['usecd'] self.Uhub = inputs['Uhub'] self.Omega = inputs['Omega'] self.pitch = inputs['pitch'] af = [None] * self.naero for i in range(self.naero): af[i] = CCAirfoil(inputs['airfoils_aoa'], inputs['airfoils_Re'], inputs['airfoils_cl'][:, i, :], inputs['airfoils_cd'][:, i, :], inputs['airfoils_cm'][:, i, :]) self.ccblade = CCBlade(self.r, self.chord, self.theta, af, self.Rhub, self.Rtip, self.B, self.rho, self.mu, self.precone, self.tilt, self.yaw, self.shearExp, self.hub_height, self.nSector, self.precurve, self.precurveTip, tiploss=self.tiploss, hubloss=self.hubloss, wakerotation=self.wakerotation, usecd=self.usecd, derivatives=True) # power, thrust, torque self.P, self.T, self.Q, self.M, self.dP, self.dT, self.dQ \ = self.ccblade.evaluate(self.Uhub, self.Omega, self.pitch, coefficients=False) outputs['T'] = self.T outputs['Q'] = self.Q outputs['P'] = self.P '''