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'] rotorse_options = modeling_options['WISDEM']['RotorSE'] self.n_span = n_span = rotorse_options['n_span'] self.n_aoa = n_aoa = rotorse_options[ 'n_aoa'] # Number of angle of attacks self.n_Re = n_Re = rotorse_options[ 'n_Re'] # Number of Reynolds, so far hard set at 1 self.n_tab = n_tab = rotorse_options[ 'n_tab'] # Number of tabulated data. For distributed aerodynamic control this could be > 1 self.n_pitch = n_pitch = rotorse_options['n_pitch_perf_surfaces'] self.n_tsr = n_tsr = rotorse_options['n_tsr_perf_surfaces'] self.n_U = n_U = rotorse_options['n_U_perf_surfaces'] self.min_TSR = rotorse_options['min_tsr_perf_surfaces'] self.max_TSR = rotorse_options['max_tsr_perf_surfaces'] self.min_pitch = rotorse_options['min_pitch_perf_surfaces'] self.max_pitch = rotorse_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 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["WISDEM"]["RotorSE"]["n_span"] self.n_aoa = n_aoa = modeling_options["WISDEM"]["RotorSE"][ "n_aoa"] # Number of angle of attacks self.n_Re = n_Re = modeling_options["WISDEM"]["RotorSE"][ "n_Re"] # Number of Reynolds, so far hard set at 1 self.n_tab = n_tab = modeling_options["WISDEM"]["RotorSE"][ "n_tab"] # Number of tabulated data. For distributed aerodynamic control this could be > 1 self.regulation_reg_III = modeling_options["WISDEM"]["RotorSE"][ "regulation_reg_III"] self.n_pc = modeling_options["WISDEM"]["RotorSE"]["n_pc"] self.n_pc_spline = modeling_options["WISDEM"]["RotorSE"]["n_pc_spline"] # 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=1.0) self.add_input( "generator_efficiency", val=np.ones(self.n_pc), desc= "Generator efficiency at various rpm values to support table lookup", ) self.add_input( "lss_rpm", val=np.zeros(self.n_pc), units="rpm", desc= "Low speed shaft RPM values at which the generator efficiency values are given", ) 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("P_aero", val=np.zeros(self.n_pc), units="W", desc="rotor mechanical 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("rated_mech", val=0.0, units="W", desc="Mechanical shaft power 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.add_output("rated_efficiency", val=1.0, desc="Efficiency at rated conditions") # 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.0, np.pi / 2.0, 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"] # 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.0 ]) # Apply maximum and minimum rotor speed limits Omega = np.maximum(np.minimum(Omega_tsr, Omega_max), inputs["omega_min"] * np.pi / 30.0) Omega_rpm = Omega * 30.0 / np.pi # Create table lookup of total drivetrain efficiency, where rpm is first column and second column is gearbox*generator lss_rpm = inputs["lss_rpm"] gen_eff = inputs["generator_efficiency"] if not np.any(lss_rpm): lss_rpm = np.linspace(np.maximum(0.1, Omega_rpm[0]), Omega_rpm[-1], self.n_pc) _, gen_eff = compute_P_and_eff(P_rated * lss_rpm / lss_rpm[-1], P_rated, np.zeros(self.n_pc), driveType, np.zeros((self.n_pc, 2))) # driveEta = np.c_[lss_rpm, float(inputs['gearbox_efficiency'])*gen_eff] driveEta = float(inputs["gearbox_efficiency"]) * gen_eff # 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, Omega_rpm, driveType, driveEta) eff = np.interp(Omega_rpm, lss_rpm, driveEta) P = P_aero * eff 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.0, pitch0 + 10.0] pitch[i] = minimize_scalar( lambda x: maximizePower(x, Uhub[i], Omega_rpm[i]), bounds=bnds, method="bounded", options={ "disp": False, "xatol": TOL, "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[i] = compute_P_and_eff(P_aero[i], P_rated, Omega_rpm[i], driveType, driveEta) eff[i] = np.interp(Omega_rpm[i], lss_rpm, driveEta) P[i] = P_aero[i] * eff[i] Cp[i] = Cp_aero[i] * eff[i] # 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_i = x[0] Uhub_i = x[1] Omega_i = min([Uhub_i * tsr / R_tip, Omega_max]) Omega_i_rpm = Omega_i * 30.0 / np.pi myout, _ = self.ccblade.evaluate([Uhub_i], [Omega_i_rpm], [pitch_i], coefficients=False) P_aero_i = float(myout["P"]) # P_i,_ = compute_P_and_eff(P_aero_i.flatten(), P_rated, Omega_i_rpm, driveType, driveEta) eff_i = np.interp(Omega_i_rpm, lss_rpm, driveEta) P_i = float(P_aero_i * eff_i) 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=TOL) 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-1 * TOL, rtol=1e-2 * TOL, 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": TOL, "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.0 / 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[i] = compute_P_and_eff(P_aero[i], P_rated, Omega_rpm[i], driveType, driveEta) eff[i] = np.interp(Omega_rpm[i], lss_rpm, driveEta) P[i] = P_aero[i] * eff[i] Cp[i] = Cp_aero[i] * eff[i] 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] outputs["rated_mech"] = P_aero[i] outputs["rated_efficiency"] = eff[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_i, Uhub_i, Omega_rpm_i): myout, _ = self.ccblade.evaluate([Uhub_i], [Omega_rpm_i], [pitch_i], coefficients=False) P_aero_i = myout["P"] # P_i, _ = compute_P_and_eff(P_aero_i, P_rated, Omega_rpm_i, driveType, driveEta) eff_i = np.interp(Omega_rpm_i, lss_rpm, driveEta) P_i = P_aero_i * eff_i return P_i - 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.0, xtol=1e-1 * TOL, rtol=1e-2 * TOL, 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.0, pitch0 + 15.0], method="bounded", options={ "disp": False, "xatol": TOL, "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[i] = compute_P_and_eff(P_aero[i], P_rated, Omega_rpm[i], driveType, driveEta) eff[i] = np.interp(Omega_rpm[i], lss_rpm, driveEta) P[i] = P_aero[i] * eff[i] Cp[i] = Cp_aero[i] * eff[i] # 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["P_aero"] = P_aero 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.0 * 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 Rotor: def __init__(self, turbine, w, old=False): ''' >>>> add mean offset parameters add move this to runCCBlade<<<< ''' # Should inherit these from raft_model or _env? self.w = np.array(w) self.Zhub = turbine['Zhub'] # [m] self.shaft_tilt = turbine['shaft_tilt'] # [deg] self.overhang = turbine['overhang'] self.R_rot = turbine['blade']['Rtip'] # rotor radius [m] #yaw = 0 self.Uhub = np.array(turbine['wt_ops']['v']) self.Omega_rpm = np.array(turbine['wt_ops']['omega_op']) self.pitch_deg = np.array(turbine['wt_ops']['pitch_op']) self.I_drivetrain = float(turbine['I_drivetrain']) self.aeroServoMod = getFromDict( turbine, 'aeroServoMod', default=1 ) # flag for aeroservodynamics (0=none, 1=aero only, 2=aero and control) # Add parked pitch, rotor speed, assuming fully shut down by 40% above cut-out self.Uhub = np.r_[self.Uhub, self.Uhub.max() * 1.4, 100] self.Omega_rpm = np.r_[self.Omega_rpm, 0, 0] self.pitch_deg = np.r_[self.pitch_deg, 90, 90] # Set default control gains self.kp_0 = np.zeros_like(self.Uhub) self.ki_0 = np.zeros_like(self.Uhub) self.k_float = 0 # np.zeros_like(self.Uhub), right now this is a single value, but this may change <<< what is this? # Set CCBlade flags tiploss = True # Tip loss model True/False hubloss = True # Hub loss model, True/False wakerotation = True # Wake rotation, True/False usecd = True # Use drag coefficient within BEMT, True/False # Set discretization parameters nSector = 4 # [-] - number of equally spaced azimuthal positions where CCBlade should be interrogated. The results are averaged across the n positions. 4 is a good first guess n_span = 30 # [-] - number of blade stations along span grid = np.linspace( 0., 1., n_span) # equally spaced grid along blade span, root=0 tip=1 # ----- AIRFOIL STUFF ------ n_aoa = 200 # [-] - number of angles of attack to discretize airfoil polars - MUST BE MULTIPLE OF 4 n_af = len(turbine["airfoils"]) af_used = [b for [a, b] in turbine['blade']["airfoils"]] af_position = [a for [a, b] in turbine['blade']["airfoils"]] #af_used = turbine['blade']["airfoils"]["labels"] #af_position = turbine['blade']["airfoils"]["grid"] n_af_span = len(af_used) # One fourth of the angles of attack from -pi to -pi/6, half between -pi/6 to pi/6, and one fourth from pi/6 to pi aoa = np.unique( np.hstack([ np.linspace(-180, -30, int(n_aoa / 4.0 + 1)), np.linspace( -30, 30, int(n_aoa / 2.0), ), np.linspace(30, 180, int(n_aoa / 4.0 + 1)) ])) af_name = n_af * [""] r_thick = np.zeros(n_af) for i in range(n_af): af_name[i] = turbine["airfoils"][i]["name"] r_thick[i] = turbine["airfoils"][i]["relative_thickness"] cl = np.zeros((n_af, n_aoa, 1)) cd = np.zeros((n_af, n_aoa, 1)) cm = np.zeros((n_af, n_aoa, 1)) # Interp cl-cd-cm along predefined grid of angle of attack for i in range(n_af): #cl[i, :, 0] = np.interp(aoa, turbine["airfoils"][i]["alpha"], turbine["airfoils"][i]["c_l"]) #cd[i, :, 0] = np.interp(aoa, turbine["airfoils"][i]["alpha"], turbine["airfoils"][i]["c_d"]) #cm[i, :, 0] = np.interp(aoa, turbine["airfoils"][i]["alpha"], turbine["airfoils"][i]["c_m"]) polar_table = np.array(turbine["airfoils"][i]['data']) # Note: polar_table[:,0] must be in degrees cl[i, :, 0] = np.interp(aoa, polar_table[:, 0], polar_table[:, 1]) cd[i, :, 0] = np.interp(aoa, polar_table[:, 0], polar_table[:, 2]) cm[i, :, 0] = np.interp(aoa, polar_table[:, 0], polar_table[:, 3]) #plt.figure() #plt.plot(polar_table[:,0], polar_table[:,1]) #plt.plot(polar_table[:,0], polar_table[:,2]) #plt.title(af_name[i]) if abs(cl[i, 0, 0] - cl[i, -1, 0]) > 1.0e-5: print( "WARNING: Ai " + af_name[i] + " has the lift coefficient different between + and - pi rad. This is fixed automatically, but please check the input data." ) cl[i, 0, 0] = cl[i, -1, 0] if abs(cd[i, 0, 0] - cd[i, -1, 0]) > 1.0e-5: print( "WARNING: Airfoil " + af_name[i] + " has the drag coefficient different between + and - pi rad. This is fixed automatically, but please check the input data." ) cd[i, 0, 0] = cd[i, -1, 0] if abs(cm[i, 0, 0] - cm[i, -1, 0]) > 1.0e-5: print( "WARNING: Airfoil " + af_name[i] + " has the moment coefficient different between + and - pi rad. This is fixed automatically, but please check the input data." ) cm[i, 0, 0] = cm[i, -1, 0] # Interpolate along blade span using a pchip on relative thickness r_thick_used = np.zeros(n_af_span) cl_used = np.zeros((n_af_span, n_aoa, 1)) cd_used = np.zeros((n_af_span, n_aoa, 1)) cm_used = np.zeros((n_af_span, n_aoa, 1)) for i in range(n_af_span): for j in range(n_af): if af_used[i] == af_name[j]: r_thick_used[i] = r_thick[j] cl_used[i, :, :] = cl[j, :, :] cd_used[i, :, :] = cd[j, :, :] cm_used[i, :, :] = cm[j, :, :] break # Pchip does have an associated derivative method built-in: # https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.PchipInterpolator.derivative.html#scipy.interpolate.PchipInterpolator.derivative spline = PchipInterpolator rthick_spline = spline(af_position, r_thick_used) # GB: HAVE TO TALK TO PIETRO ABOUT THIS #r_thick_interp = rthick_spline(grid[1:-1]) r_thick_interp = rthick_spline(grid) # Spanwise interpolation of the airfoil polars with a pchip r_thick_unique, indices = np.unique(r_thick_used, return_index=True) cl_spline = spline(r_thick_unique, cl_used[indices, :, :]) self.cl_interp = np.flip(cl_spline(np.flip(r_thick_interp)), axis=0) cd_spline = spline(r_thick_unique, cd_used[indices, :, :]) self.cd_interp = np.flip(cd_spline(np.flip(r_thick_interp)), axis=0) cm_spline = spline(r_thick_unique, cm_used[indices, :, :]) self.cm_interp = np.flip(cm_spline(np.flip(r_thick_interp)), axis=0) self.aoa = aoa # split out blade geometry info from table geometry_table = np.array(turbine['blade']['geometry']) blade_r = geometry_table[:, 0] blade_chord = geometry_table[:, 1] blade_theta = geometry_table[:, 2] blade_precurve = geometry_table[:, 3] blade_presweep = geometry_table[:, 4] af = [] for i in range(self.cl_interp.shape[0]): af.append( CCAirfoil(self.aoa, [], self.cl_interp[i, :, :], self.cd_interp[i, :, :], self.cm_interp[i, :, :])) self.ccblade = CCBlade( blade_r, # (m) locations defining the blade along z-axis of blade coordinate system blade_chord, # (m) corresponding chord length at each section blade_theta, # (deg) corresponding :ref:`twist angle <blade_airfoil_coord>` at each section---positive twist decreases angle of attack. af, # CCAirfoil object turbine['Rhub'], # (m) radius of hub turbine['blade']['Rtip'], # (m) radius of tip turbine['nBlades'], # number of blades turbine['rho_air'], # (kg/m^3) freestream fluid density turbine['mu_air'], # (kg/m/s) dynamic viscosity of fluid turbine['precone'], # (deg) hub precone angle self.shaft_tilt, # (deg) hub tilt angle 0.0, # (deg) nacelle yaw angle turbine[ 'shearExp'], # shear exponent for a power-law wind profile across hub turbine[ 'Zhub'], # (m) hub height used for power-law wind profile. U = Uref*(z/hubHt)**shearExp nSector, # 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. blade_precurve, # (m) location of blade pitch axis in x-direction of :ref:`blade coordinate system <azimuth_blade_coord>` turbine['blade'] ['precurveTip'], # (m) location of blade pitch axis in x-direction at the tip (analogous to Rtip) blade_presweep, # (m) location of blade pitch axis in y-direction of :ref:`blade coordinate system <azimuth_blade_coord>` turbine['blade'] ['presweepTip'], # (m) location of blade pitch axis in y-direction at the tip (analogous to Rtip) tiploss=tiploss, # if True, include Prandtl tip loss model hubloss=hubloss, # if True, include Prandtl hub loss model wakerotation= wakerotation, # if True, include effect of wake rotation (i.e., tangential induction factor is nonzero) usecd= usecd, # If True, use drag coefficient in computing induction factors (always used in evaluating distributed loads from the induction factors). derivatives= True, # if True, derivatives along with function values will be returned for the various methods ) # pull control gains out of dictionary self.setControlGains(turbine) def runCCBlade(self, Uhub, ptfm_pitch=0, yaw_misalign=0): '''This performs a single CCBlade evaluation at specified conditions. ptfm_pitch mean platform pitch angle to be included in rotor tilt angle [rad] yaw_misalign turbine yaw misalignment angle [deg] ''' # find turbine operating point at the provided wind speed Omega_rpm = np.interp(Uhub, self.Uhub, self.Omega_rpm) # rotor speed [rpm] pitch_deg = np.interp(Uhub, self.Uhub, self.pitch_deg) # blade pitch angle [deg] # adjust rotor angles based on provided info (I think this intervention in CCBlade should work...) self.ccblade.tilt = np.deg2rad(self.shaft_tilt) + ptfm_pitch self.ccblade.yaw = np.deg2rad(yaw_misalign) # evaluate aero loads and derivatives with CCBlade loads, derivs = self.ccblade.evaluate(Uhub, Omega_rpm, pitch_deg, coefficients=True) # organize and save the relevant outputs... self.U_case = Uhub self.Omega_case = Omega_rpm self.aero_torque = loads["Q"][0] self.aero_power = loads["P"][0] self.pitch_case = pitch_deg outputs = {} outputs["P"] = loads["P"] outputs["Mb"] = loads["Mb"] outputs["CP"] = loads["CP"] outputs["CMb"] = loads["CMb"] outputs["Fhub"] = np.array( [loads["T"][0], loads["Y"][0], loads["Z"][0]]) outputs["Mhub"] = np.array( [loads["Q"][0], loads["My"][0], loads["Mz"][0]]) outputs["CFhub"] = np.array( [loads["CT"][0], loads["CY"][0], loads["CZ"][0]]) outputs["CMhub"] = np.array( [loads["CQ"][0], loads["CMy"][0], loads["CMz"][0]]) print( f"Wind speed: {Uhub} m/s, Aerodynamic power coefficient: {loads['CP'][0]:4.3f}" ) J = {} # Jacobian/derivatives dP = derivs["dP"] J["P", "r"] = dP["dr"] # J["P", "chord"] = dP["dchord"] # J["P", "theta"] = dP["dtheta"] # J["P", "Rhub"] = np.squeeze(dP["dRhub"]) # J["P", "Rtip"] = np.squeeze(dP["dRtip"]) # J["P", "hub_height"] = np.squeeze(dP["dhubHt"]) # J["P", "precone"] = np.squeeze(dP["dprecone"]) # J["P", "tilt"] = np.squeeze(dP["dtilt"]) # J["P", "yaw"] = np.squeeze(dP["dyaw"]) # J["P", "shearExp"] = np.squeeze(dP["dshear"]) # J["P", "V_load"] = np.squeeze(dP["dUinf"]) # J["P", "Omega_load"] = np.squeeze(dP["dOmega"]) # J["P", "pitch_load"] = np.squeeze(dP["dpitch"]) # J["P", "precurve"] = dP["dprecurve"] # J["P", "precurveTip"] = dP["dprecurveTip"] # J["P", "presweep"] = dP["dpresweep"] # J["P", "presweepTip"] = dP["dpresweepTip"] dQ = derivs["dQ"] J["Q", "Uhub"] = np.atleast_1d(np.diag(dQ["dUinf"])) J["Q", "pitch_deg"] = np.atleast_1d(np.diag(dQ["dpitch"])) J["Q", "Omega_rpm"] = np.atleast_1d(np.diag(dQ["dOmega"])) dT = derivs["dT"] J["T", "Uhub"] = np.atleast_1d(np.diag(dT["dUinf"])) J["T", "pitch_deg"] = np.atleast_1d(np.diag(dT["dpitch"])) J["T", "Omega_rpm"] = np.atleast_1d(np.diag(dT["dOmega"])) # dT = derivs["dT"] # .J["Fhub", "r"][0,:] = dT["dr"] # 0 is for thrust force, 1 would be y, 2 z # .J["Fhub", "chord"][0,:] = dT["dchord"] # .J["Fhub", "theta"][0,:] = dT["dtheta"] # .J["Fhub", "Rhub"][0,:] = np.squeeze(dT["dRhub"]) # .J["Fhub", "Rtip"][0,:] = np.squeeze(dT["dRtip"]) # .J["Fhub", "hub_height"][0,:] = np.squeeze(dT["dhubHt"]) # .J["Fhub", "precone"][0,:] = np.squeeze(dT["dprecone"]) # .J["Fhub", "tilt"][0,:] = np.squeeze(dT["dtilt"]) # .J["Fhub", "yaw"][0,:] = np.squeeze(dT["dyaw"]) # .J["Fhub", "shearExp"][0,:] = np.squeeze(dT["dshear"]) # .J["Fhub", "V_load"][0,:] = np.squeeze(dT["dUinf"]) # .J["Fhub", "Omega_load"][0,:] = np.squeeze(dT["dOmega"]) # .J["Fhub", "pitch_load"][0,:] = np.squeeze(dT["dpitch"]) # .J["Fhub", "precurve"][0,:] = dT["dprecurve"] # .J["Fhub", "precurveTip"][0,:] = dT["dprecurveTip"] # .J["Fhub", "presweep"][0,:] = dT["dpresweep"] # .J["Fhub", "presweepTip"][0,:] = dT["dpresweepTip"] self.J = J return loads, derivs def setControlGains(self, turbine): ''' Use flipped sign version of ROSCO ''' # Convert gain-scheduling wrt pitch to wind speed, Add zero gains for parked "control" pc_angles = np.array(turbine['pitch_control']['GS_Angles']) * rad2deg self.kp_0 = np.interp(self.pitch_deg, pc_angles, turbine['pitch_control']['GS_Kp'], left=0, right=0) self.ki_0 = np.interp(self.pitch_deg, pc_angles, turbine['pitch_control']['GS_Ki'], left=0, right=0) self.k_float = -turbine['pitch_control']['Fl_Kp'] # Torque control self.kp_tau = -turbine['torque_control']['VS_KP'] self.ki_tau = -turbine['torque_control']['VS_KI'] self.Ng = turbine['gear_ratio'] def calcAeroServoContributions(self, case, ptfm_pitch=0, display=0):
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)
dNp_dRhub = dNp["dRhub"] dNp_dRtip = dNp["dRtip"] 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"]
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__)), "../../../examples/_airfoil_files") # 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 outputs, derivs = self.rotor.evaluate(Uinf, Omega, pitch) P, T, Q = [outputs[key] for key in ("P", "T", "Q")] # 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)