def setUp(self): ''' Common piece of code to initialise the test ''' # build reference state Mw=4 M=3 alpha=0.*np.pi/180. ainf=15.0*np.pi/180. Uinf=20.*np.array([np.cos(ainf),np.sin(ainf)]) chord=5.0 b=0.5*chord self.S0=uvlm2d_sta.solver(M,Mw,b,Uinf,alpha,rho=1.225) #self.S0.build_flat_plate() self.S0.build_camber_plate(Mcamb=50,Pcamb=4) self.S0.solve_static_Gamma2d()
def test_camber_aerofoil_speed(self): ''' Calculate steady case / very short wake can be used. A cambered aerofoil, rotated of a alpha0 angle wrt the horizontal line is taken as a reference. The aerofoil has a nonzero velocity, such that an effective angle of attack, alpha_eff, is achieved. The flow speed magnitude is kept constant. Two linear models are created. Both must be built around the aerofoil at a geometrical angle of alpha0. However, the initial velocity profiles used for the linearisations are different and such to produce effective angles of attack of alpha_eff01 and alpha_eff02. ''' DAlphaList = [ 0.0, 0.1, 0.2, 0.3, 0.5, 1., 2., 4., 6., 8., 9.5, 9.7, 9.8, 9.9, 10., 10.1, 10.2, 10.3, 10.5, 12., 14., 16., 18. ] # degs MList = [20] Nm, mm = 1, 0 Na = len(DAlphaList) CFXlin01, CFZlin01 = np.zeros((Na, Nm)), np.zeros((Na, Nm)) CFXlin02, CFZlin02 = np.zeros((Na, Nm)), np.zeros((Na, Nm)) CFXnnl, CFZnnl = np.zeros((Na, Nm)), np.zeros((Na, Nm)) FZlin01, FXlin01 = np.zeros((Na, Nm)), np.zeros((Na, Nm)) FZlin02, FXlin02 = np.zeros((Na, Nm)), np.zeros((Na, Nm)) FZnnl, FXnnl = np.zeros((Na, Nm)), np.zeros((Na, Nm)) # Reference input: Mw = 2 M = MList[mm] chord = 3. b = 0.5 * chord Uinf0 = np.array([20., 0.]) rho = 1.225 alpha0 = 4.0 * np.pi / 180. # angle of reference aerofoil w.r.t. horizontal line # reference condition 1/2: aerofoil at 0 deg angle S01 = uvlm.solver(M, Mw, b, Uinf0, alpha0, rho=1.225) S01.build_camber_plate(Mcamb=10, Pcamb=4) S02 = copy.deepcopy(S01) alpha_eff01 = 0. * np.pi / 180. alpha_inf = alpha_eff01 - alpha0 Uinf_here = geo.rotate_speed(Uinf0, alpha_inf) S01.dZetadt[:, :] = -(Uinf_here - Uinf0) S01.solve_static_Gamma2d() Fref01 = np.sum(S01.Faero, 0) alpha_eff02 = 10. * np.pi / 180. alpha_inf = alpha_eff02 - alpha0 Uinf_here = geo.rotate_speed(Uinf0, alpha_inf) S02.dZetadt[:, :] = -(Uinf_here - Uinf0) S02.solve_static_Gamma2d() Fref02 = np.sum(S02.Faero, 0) print('Testing steady aerofoil for M=%d...' % M) for aa in range(len(DAlphaList)): # Perturbation alpha_eff = np.pi / 180. * DAlphaList[aa] alpha_inf = alpha_eff - alpha0 Uinf_here = geo.rotate_speed(Uinf0, alpha_inf) qinf_tot = 0.5 * rho * (S01.Uabs)**2 print('\tAlpha effective=%.2f deg' % DAlphaList[aa]) ### Reference nonlinear solution Sref = uvlm.solver(M, Mw, b, Uinf0, alpha0, rho) Sref.build_camber_plate(Mcamb=10, Pcamb=4) Sref.Wzeta[:, :] = Uinf_here - Uinf0 Sref.solve_static_Gamma2d() FXnnl[aa, mm], FZnnl[aa, mm] = np.sum(Sref.Faero, 0) ### Linearised solution 01: Slin01 = linuvlm.solver(S01) Slin01.dZetadt[:, :] = -(Uinf_here - (S01.Uzeta[0, :] - S01.dZetadt[0, :])) Slin01.solve_static_Gamma2d() dFtot01 = np.sum(Slin01.Faero, 0) FXlin01[aa, mm], FZlin01[aa, mm] = dFtot01 + Fref01 ### Linearised solution 02: Slin02 = linuvlm.solver(S02) Slin02.dZetadt[:, :] = -(Uinf_here - (S02.Uzeta[0, :] - S02.dZetadt[0, :])) Slin02.solve_static_Gamma2d() FXlin02[aa, mm], FZlin02[aa, mm] = np.sum(Slin02.Faero, 0) dFtot02 = np.sum(Slin02.Faero, 0) FXlin02[aa, mm], FZlin02[aa, mm] = dFtot02 + Fref02 clist = [ 'k', 'r', 'b', '0.6', ] ### Aerodynamic forces fig1 = plt.figure('Drag', (12, 4)) fig2 = plt.figure('Lift', (12, 4)) ax1 = fig1.add_subplot(111) ax2 = fig2.add_subplot(111) ax1.plot(DAlphaList, FXlin01[:, mm], 'k', lw=2, ls='--', label=r'M=%.2d lin 0 deg' % MList[mm]) ax1.plot(DAlphaList, FXlin02[:, mm], 'b', lw=2, ls=':', label=r'M=%.2d lin 10 deg' % MList[mm]) ax1.plot(DAlphaList, FXnnl[:, mm], 'r', lw=Nm - mm, label=r'M=%.2d nnl' % MList[mm]) # ax2.plot(DAlphaList, FZlin01[:, mm], 'k', lw=2, ls='--', label=r'M=%.2d lin 0 deg' % MList[mm]) ax2.plot(DAlphaList, FZlin02[:, mm], 'b', lw=Nm - mm, ls=':', label=r'M=%.2d lin 10 deg' % MList[mm]) ax2.plot(DAlphaList, FZnnl[:, mm], 'r', lw=2, label=r'M=%.2d nnl' % MList[mm]) ax1.set_xlabel(r'\alpha [deg]') ax1.set_title(r'Horizontal Force [N]') ax1.legend() ax2.set_xlabel(r'\alpha [deg]') ax2.set_title(r'Vertical Force [N]') ax2.legend() if self.SHOW_PLOT: plt.show() else: plt.close()
def test_camber_rotation(self): ''' Calculate steady case / very short wake can be used. A cambered aerofoil is tested at different angles of attack. Two linearisations, about zero and 10deg are used. ''' DAlphaList = [ 0.0, 0.1, 0.2, 0.3, 0.5, 1., 2., 4., 6., 8., 9.5, 9.7, 9.8, 9.9, 10., 10.1, 10.2, 10.3, 10.5, 12., 14., 16., 18. ] # degs MList = [20] Nm, mm = 1, 0 Na = len(DAlphaList) Llin01, Dlin01 = np.zeros((Na, Nm)), np.zeros((Na, Nm)) Llin02, Dlin02 = np.zeros((Na, Nm)), np.zeros((Na, Nm)) Lnnl, Dnnl = np.zeros((Na, Nm)), np.zeros((Na, Nm)) # input: Mw = 2 M = MList[mm] chord = 3. b = 0.5 * chord Uinf = np.array([20., 0.]) rho = 1.225 # reference condition 1: aerofoil at 0 deg angle alpha01 = 0. * np.pi / 180. S01 = uvlm.solver(M, Mw, b, Uinf, alpha01, rho=1.225) S01.build_camber_plate(Mcamb=10, Pcamb=4) S01.solve_static_Gamma2d() Fref01 = np.sum(S01.Faero, 0) # reference condition 2: aerofoil at 10 deg angle alpha02 = 10. * np.pi / 180. S02 = uvlm.solver(M, Mw, b, Uinf, alpha02, rho=1.225) S02.build_camber_plate(Mcamb=10, Pcamb=4) S02.solve_static_Gamma2d() Fref02 = np.sum(S02.Faero, 0) print('Testing steady aerofoil for M=%d...' % M) for aa in range(len(DAlphaList)): # Perturbation alpha_tot = np.pi / 180. * DAlphaList[aa] dUinf = 0.0 # velocity [m/s] qinf_tot = 0.5 * rho * (S01.Uabs + dUinf)**2 print('\talpha==%.2f deg' % DAlphaList[aa]) ### Linearised solution 01: # Perturb reference state dalpha01 = alpha_tot - alpha01 # angle [rad] ZetaRot = geo.rotate_aerofoil(S01.Zeta, dalpha01) dZeta = ZetaRot - S01.Zeta Slin01 = linuvlm.solver(S01) Slin01.Zeta = dZeta # solve Slin01.solve_static_Gamma2d() # store data dFtot01 = np.sum(Slin01.Faero, 0) Dlin01[aa, mm], Llin01[aa, mm] = dFtot01 + Fref01 ### Linearised solution 02: # Perturb reference state dalpha02 = alpha_tot - alpha02 # angle [rad] ZetaRot = geo.rotate_aerofoil(S02.Zeta, dalpha02) dZeta = ZetaRot - S02.Zeta Slin02 = linuvlm.solver(S02) Slin02.Zeta = dZeta # solve Slin02.solve_static_Gamma2d() # store data dFtot02 = np.sum(Slin02.Faero, 0) Dlin02[aa, mm], Llin02[aa, mm] = dFtot02 + Fref02 ### Reference nonlinear solution Sref = uvlm.solver(M, Mw, b, Uinf, alpha_tot, rho) Sref.build_camber_plate(Mcamb=10, Pcamb=4) # solve Sref.solve_static_Gamma2d() # store Dnnl[aa, mm], Lnnl[aa, mm] = np.sum(Sref.Faero, 0) clist = [ 'k', 'r', 'b', '0.6', ] ### Aerodynamic forces fig1 = plt.figure('Drag', (12, 4)) fig2 = plt.figure('Lift', (12, 4)) ax1 = fig1.add_subplot(111) ax2 = fig2.add_subplot(111) ax1.plot(DAlphaList, Dnnl[:, mm], 'r', lw=Nm - mm, label=r'M=%.2d nnl' % MList[mm]) ax1.plot(DAlphaList, Dlin01[:, mm], 'k', lw=2, ls='--', label=r'M=%.2d lin 0 deg' % MList[mm]) ax1.plot(DAlphaList, Dlin02[:, mm], 'b', lw=2, ls=':', label=r'M=%.2d lin 10 deg' % MList[mm]) # ax2.plot(DAlphaList, Lnnl[:, mm], 'r', lw=2, label=r'M=%.2d nnl' % MList[mm]) ax2.plot(DAlphaList, Llin01[:, mm], 'k', lw=2, ls='--', label=r'M=%.2d lin 0 deg' % MList[mm]) ax2.plot(DAlphaList, Llin02[:, mm], 'b', lw=Nm - mm, ls=':', label=r'M=%.2d lin 10 deg' % MList[mm]) ax1.set_xlabel(r'\alpha [deg]') ax1.set_title(r'Drag') ax1.legend() ax2.set_xlabel(r'\alpha [deg]') ax2.set_title(r'Lift') ax2.legend() fig1 = plt.figure('Relative error lift', (12, 4)) ax1 = fig1.add_subplot(111) ax1.plot(DAlphaList, Llin01[:, mm] / Lnnl[:, mm] - 1., 'k', lw=2, ls='--', label=r'M=%.2d lin 0 deg' % MList[mm]) ax1.plot(DAlphaList, Llin02[:, mm] / Lnnl[:, mm] - 1., 'b', lw=2, ls=':', label=r'M=%.2d lin 10 deg' % MList[mm]) ax1.set_xlabel(r'$\alpha$ [deg]') ax1.set_ylabel(r'relative error') ax1.set_title(r'Lift error w.r.t. exact solution') ax1.legend() ax2.legend() if self.SHOW_PLOT: plt.show() else: plt.close()
def test_flat_rotation_about_zero(self): ''' Calculate steady case / very short wake can be used. A flat plate at at zero angle of attack is taken as reference condition. The state is perturbed of an angle dalpha. ''' MList = [1, 8, 16] DAlphaList = [0.1, 0.5, 1., 2., 4., 6., 8., 10., 12., 14.] # degs Nm = len(MList) Na = len(DAlphaList) CDlin, CLlin = np.zeros((Na, Nm)), np.zeros((Na, Nm)) CDnnl, CLnnl = np.zeros((Na, Nm)), np.zeros((Na, Nm)) for mm in range(len(MList)): # input: reference condition: flat plate at zero angle Mw = 2 M = MList[mm] alpha = 0. * np.pi / 180. chord = 3. b = 0.5 * chord Uinf = np.array([20., 0.]) rho = 1.225 S0 = uvlm.solver(M, Mw, b, Uinf, alpha, rho=1.225) S0.build_flat_plate() S0.solve_static_Gamma2d() print('Testing steady aerofoil for M=%d...' % M) for aa in range(len(DAlphaList)): # Perturbation dalpha = np.pi / 180. * DAlphaList[aa] # angle [rad] dUinf = 0.0 # velocity [m/s] qinf_tot = 0.5 * rho * (S0.Uabs + dUinf)**2 print('\talpha==%.2f deg' % DAlphaList[aa]) ### Linearised solution # Perturb reference state ZetaRot = geo.rotate_aerofoil(S0.Zeta, dalpha) dZeta = ZetaRot - S0.Zeta Slin = linuvlm.solver(S0) Slin.Zeta = dZeta # solve Slin.solve_static_Gamma2d() # store data CDlin[aa,mm],CLlin[aa,mm]=np.sum(Slin.Faero,0)/\ (qinf_tot*Slin.S0.chord*(alpha+dalpha)) ### Reference nonlinear solution Sref = uvlm.solver(M, Mw, b, Uinf, alpha + dalpha, rho) Sref.build_flat_plate() # solve Sref.solve_static_Gamma2d() # store CDnnl[aa,mm],CLnnl[aa,mm]=np.sum(Sref.Faero,0)/\ (qinf_tot*Sref.chord*(alpha+dalpha)) clist = [ 'k', 'r', 'b', '0.6', ] fig = plt.figure('Aerodynamic coefficients', (12, 4)) ax1 = fig.add_subplot(121) ax2 = fig.add_subplot(122) for mm in range(Nm): # ax1.plot(DAlphaList, CDlin[:, mm], clist[mm], lw=Nm - mm, ls='--', label=r'M=%.2d lin' % MList[mm]) ax1.plot(DAlphaList, CDnnl[:, mm], clist[mm], lw=Nm - mm, label=r'M=%.2d nnl' % MList[mm]) # ax2.plot(DAlphaList, CLlin[:, mm], clist[mm], lw=Nm - mm, ls='--', label=r'M=%.2d lin' % MList[mm]) ax2.plot(DAlphaList, CLnnl[:, mm], clist[mm], lw=Nm - mm, label=r'M=%.2d nnl' % MList[mm]) ax1.set_xlabel(r'\alpha [deg]') ax1.set_title(r'CD') ax1.legend() ax2.set_xlabel(r'\alpha [deg]') ax2.set_title(r'CL') ax2.legend() if self.SHOW_PLOT: plt.show() else: plt.close()
def test_impulse(self): ''' Impulsive start (with/out Hall's correction) against Wagner solution ''' print('Testing aerofoil impulsive start...') # set-up M = 20 WakeFact = 20 c = 3. b = 0.5 * c uinf = 20.0 aeff = 0.1 * np.pi / 180. T = 8.0 ### reference static solution - at zero - Hall's correction S0 = uvlm2d_sta.solver(M=M, Mw=M * WakeFact, b=b, Uinf=np.array([uinf, 0.]), alpha=0.0, rho=1.225) S0.build_flat_plate() S0.solve_static_Gamma2d() Ftot0 = np.sum(S0.Faero, 0) Slin1 = lin_uvlm2d_dyn.solver(S0, T=T) ZetaRot = geo.rotate_aerofoil(S0.Zeta, aeff) dZeta = ZetaRot - S0.Zeta Slin1.Zeta = dZeta for tt in range(Slin1.NT): Slin1.THZeta[tt, :, :] = dZeta Slin1._imp_start = True Slin1.eps_Hall = 0.003 Slin1.solve_dyn_Gamma2d() ### reference static solution - at zero - no Hall's correction S0 = uvlm2d_sta.solver(M=M, Mw=M * WakeFact, b=b, Uinf=np.array([uinf, 0.]), alpha=0.0, rho=1.225) S0.build_flat_plate() S0.eps_Hall = 1.0 S0.solve_static_Gamma2d() Slin2 = lin_uvlm2d_dyn.solver(S0, T=T) Slin2.Zeta = dZeta for tt in range(Slin2.NT): Slin2.THZeta[tt, :, :] = dZeta Slin2._imp_start = True Slin2.eps_Hall = 1.0 Slin2.solve_dyn_Gamma2d() ### Analytical solution CLv_an = an.wagner_imp_start(aeff, uinf, c, Slin1.time) ##### Post-process numerical solution - Hall's correction THFtot1 = 0.0 * Slin1.THFaero for tt in range(Slin1.NT): THFtot1[tt, :] = Slin1.THFaero[tt, :] + Ftot0 THCFtot1 = THFtot1 / S0.qinf / S0.chord # Mass and circulatory contribution THCFmass1 = Slin1.THFaero_m / S0.qinf / S0.chord THCFcirc1 = THCFtot1 - THCFmass1 ##### Post-process numerical solution - no Hall's correction THFtot2 = 0.0 * Slin2.THFaero for tt in range(Slin2.NT): THFtot2[tt, :] = Slin2.THFaero[tt, :] + Ftot0 THCFtot2 = THFtot2 / S0.qinf / S0.chord # Mass and circulatory contribution THCFmass2 = Slin2.THFaero_m / S0.qinf / S0.chord THCFcirc2 = THCFtot2 - THCFmass2 plt.close('all') # non-dimensional time sv = 2.0 * S0.Uabs * Slin1.time / S0.chord fig = plt.figure('Lift coefficient', (10, 6)) ax = fig.add_subplot(111) # Wagner ax.plot(0.5 * sv, CLv_an, '0.6', lw=3, label='An Tot') # numerical ax.plot(0.5 * sv, THCFtot1[:, 1], 'k', lw=1, label='Num Tot - Hall') ax.plot(0.5 * sv, THCFmass1[:, 1], 'b', lw=1, label='Num Mass - Hall') ax.plot(0.5 * sv, THCFcirc1[:, 1], 'r', lw=1, label='Num Jouk - Hall') # numerical ax.plot(0.5 * sv, THCFtot2[:, 1], 'k--', lw=2, label='Num Tot') ax.plot(0.5 * sv, THCFmass2[:, 1], 'b--', lw=2, label='Num Mass') ax.plot(0.5 * sv, THCFcirc2[:, 1], 'r--', lw=2, label='Num Jouk') ax.set_xlabel(r'$s/2=U_\infty t/c$') ax.set_ylabel('force') ax.set_title('Lift') ax.legend() fig2 = plt.figure('Lift coefficient - zoom', (10, 6)) ax = fig2.add_subplot(111) # Wagner ax.plot(0.5 * sv, CLv_an, '0.6', lw=3, label='An Tot') # numerical ax.plot(0.5 * sv, THCFtot1[:, 1], 'k', lw=1, label='Num Tot - Hall') ax.plot(0.5 * sv, THCFmass1[:, 1], 'b', lw=1, label='Num Mass - Hall') ax.plot(0.5 * sv, THCFcirc1[:, 1], 'r', lw=1, label='Num Jouk - Hall') # numerical ax.plot(0.5 * sv, THCFtot2[:, 1], 'k--', lw=2, label='Num Tot') ax.plot(0.5 * sv, THCFmass2[:, 1], 'b--', lw=2, label='Num Mass') ax.plot(0.5 * sv, THCFcirc2[:, 1], 'r--', lw=2, label='Num Jouk') ax.set_xlabel(r'$s/2=U_\infty t/c$') ax.set_ylabel('force') ax.set_title('Lift') ax.legend() ax.set_xlim(0., 6.) fig3 = plt.figure("Lift coefficient - Hall's correction effect", (10, 6)) ax = fig3.add_subplot(111) # Wagner ax.plot(0.5 * sv, CLv_an, '0.6', lw=3, label='An Tot') # numerical ax.plot(0.5 * sv, THCFtot1[:, 1], 'k', lw=1, label='Num Tot - Hall') ax.plot(0.5 * sv, THCFmass1[:, 1], 'b', lw=1, label='Num Mass - Hall') ax.plot(0.5 * sv, THCFcirc1[:, 1], 'r', lw=1, label='Num Jouk - Hall') # numerical ax.plot(0.5 * sv, THCFtot2[:, 1], 'k--', lw=2, label='Num Tot') ax.plot(0.5 * sv, THCFmass2[:, 1], 'b--', lw=2, label='Num Mass') ax.plot(0.5 * sv, THCFcirc2[:, 1], 'r--', lw=2, label='Num Jouk') ax.set_xlabel(r'$s/2=U_\infty t/c$') ax.set_ylabel('force') ax.set_title('Lift') ax.legend() ax.set_xlim(15., 0.5 * sv[-1]) ax.set_ylim(0.10, 0.116) fig = plt.figure('Drag coefficient', (10, 6)) ax = fig.add_subplot(111) ax.plot(Slin1.time, THCFtot1[:, 0], 'k', label='Num Tot') ax.plot(Slin1.time, THCFmass1[:, 0], 'b', label='Num Mass') ax.plot(Slin1.time, THCFcirc1[:, 0], 'r', label='Num Jouk') ax.plot(Slin1.time, THCFtot2[:, 0], 'k', label='Num Tot') ax.plot(Slin1.time, THCFmass2[:, 0], 'b', label='Num Mass') ax.plot(Slin1.time, THCFcirc2[:, 0], 'r', label='Num Jouk') ax.set_xlabel('time') ax.set_ylabel('force') ax.set_title('Drag') ax.legend() fig = plt.figure('Vortex rings circulation time history', (10, 6)) ax = fig.add_subplot(111) clist = ['0.2', '0.4', '0.6'] Mlist = [0, int(S0.M / 2), S0.M - 1] for kk in range(len(Mlist)): mm = Mlist[kk] ax.plot(Slin1.time, Slin1.THGamma[:, mm], color=clist[kk], label='M=%.2d - Hall' % (mm)) clist = ['r', 'y', 'b'] MWlist = [0, int(S0.Mw / 2), S0.Mw - 1] for kk in range(len(MWlist)): mm = Mlist[kk] ax.plot(Slin1.time, Slin1.THGammaW[:, mm], color=clist[kk], label='Mw=%.2d - Hall' % (mm)) clist = ['0.2', '0.4', '0.6'] Mlist = [0, int(S0.M / 2), S0.M - 1] for kk in range(len(Mlist)): mm = Mlist[kk] ax.plot(Slin2.time, Slin2.THGamma[:, mm], color=clist[kk], linestyle='--', label='M=%.2d' % (mm)) clist = ['r', 'y', 'b'] MWlist = [0, int(S0.Mw / 2), S0.Mw - 1] for kk in range(len(MWlist)): mm = Mlist[kk] ax.plot(Slin2.time, Slin2.THGammaW[:, mm], color=clist[kk], linestyle='--', label='Mw=%.2dl' % (mm)) ax.set_xlabel('time') ax.set_ylabel('Gamma') ax.legend(ncol=2) if self.SHOW_PLOT: plt.show() else: plt.close() # Final error ErCDend = np.abs(THCFtot1[-1, 0]) ErCDend2 = np.abs(THCFtot2[-1, 0]) ErCLend = np.abs(THCFtot1[-1, 1] - CLv_an[-1]) / CLv_an[-1] ErCLend2 = np.abs(THCFtot2[-1, 1] - CLv_an[-1]) / CLv_an[-1] # Expected values CLexp = 0.010914578545119 CLexp2 = 0.010964883644861 changeCLnum = np.abs(THCFtot1[-1, 1] - CLexp) changeCLnum2 = np.abs(THCFtot2[-1, 1] - CLexp2) # Check/raise error self.assertTrue( ErCDend < 1e-15, msg= 'Final CD %.2e (with Hall correction) above limit value of %.2e!' % (ErCDend, self.TOL_analytical)) self.assertTrue( ErCDend2 < 1e-15, msg= 'Final CD %.2e (without Hall correction) above limit value of %.2e!' % (ErCDend2, self.TOL_analytical)) tolCL, tolCL2 = 0.4e-2, 0.2e-2 self.assertTrue(ErCLend<tolCL,msg='Relative CL error %.2e '\ '(with Hall correction) above tolerance %.2e!' %(ErCLend,tolCL)) self.assertTrue(ErCLend2<tolCL2,msg='Relative CL error %.2e '\ '(without Hall correction) above tolerance %.2e!'%(ErCLend2,tolCL2)) if changeCLnum > self.TOL_numerical: warnings.warn('Relative change in CL numerical solution '\ '(with Hall correction) of %.2e !!!'%changeCLnum) if changeCLnum2 > self.TOL_numerical: warnings.warn('Relative change in CL numerical solution '\ '(without Hall correction) of %.2e !!!'%changeCLnum2)
def test_steady(self): ''' Calculate steady case / very short wake can be used. ''' ### random geometry c = 3. b = 0.5 * c uinf = 20.0 T = 1.0 WakeFact = 2 alpha0 = 1.00 * np.pi / 180. dalpha = 2.00 * np.pi / 180. alpha_tot = alpha0 + dalpha TimeList = [] THCFList = [] MList = [4, 8, 16] for mm in range(len(MList)): M = MList[mm] print('Testing steady aerofoil M=%d...' % M) ### reference solution (static) S0 = uvlm2d_sta.solver(M=M, Mw=M * WakeFact, b=b, Uinf=np.array([uinf, 0.]), alpha=alpha0, rho=1.225) S0.build_flat_plate() S0.eps_Hall = 1.0 # no correction S0.solve_static_Gamma2d() Ftot0 = np.sum(S0.Faero, 0) ### linearisation Slin = lin_uvlm2d_dyn.solver(S0, T) # perturb reference state ZetaRot = geo.rotate_aerofoil(S0.Zeta, dalpha) dZeta = ZetaRot - S0.Zeta Slin.Zeta = dZeta for tt in range(Slin.NT): Slin.THZeta[tt, :, :] = dZeta # solve Slin.solve_dyn_Gamma2d() # total force THFtot = 0.0 * Slin.THFaero for tt in range(Slin.NT): THFtot[tt, :] = Slin.THFaero[tt, :] + Ftot0 TimeList.append(Slin.time) THCFList.append(THFtot / S0.qinf / S0.chord) clist = [ 'k', 'r', 'b', '0.6', ] fig = plt.figure('Aerodynamic forces', (12, 4)) ax = fig.add_subplot(131) for mm in range(len(MList)): ax.plot(TimeList[mm], THCFList[mm][:, 1], clist[mm], label=r'M=%.2d' % MList[mm]) ax.set_xlabel('time [s]') ax.set_title('Lift coefficient') ax.legend() ax = fig.add_subplot(132) for mm in range(len(MList)): ax.plot(TimeList[mm], THCFList[mm][:, 1] / alpha_tot, clist[mm], label=r'M=%.2d' % MList[mm]) ax.set_xlabel('time [s]') ax.set_title('CL alpha') ax.legend() ax = fig.add_subplot(133) for mm in range(len(MList)): ax.plot(TimeList[mm], THCFList[mm][:, 0], clist[mm], label=r'M=%.2d' % MList[mm]) ax.set_xlabel('time [s]') ax.set_title('Drag coefficient') ax.legend() if self.SHOW_PLOT: plt.show() else: plt.close() ### Testing maxDrag = 0.0 ClaExpected = 2. * np.pi ClaError = 0.0 for mm in range(len(MList)): # zero drag maxDragHere = np.max(np.abs(THCFList[mm][:, 0])) if maxDragHere > maxDrag: maxDrag = maxDragHere # Cla relative error maxClaErrorHere = np.max( np.abs(THCFList[mm][:, 1] / alpha_tot / ClaExpected - 1.0)) if maxClaErrorHere > ClaError: ClaError = maxClaErrorHere self.assertTrue(maxDragHere<self.TOL_zero,msg=\ 'Max Drag %.2e above tolerance %.2e!' %(maxDragHere,self.TOL_zero)) self.assertTrue(maxClaErrorHere < self.TOL_analytical, msg='Cla error %.2e' ' above tolerance %.2e!' % (maxClaErrorHere, self.TOL_analytical))
def test_sin_gust(self): ''' Plunge motion at a fixed reduced frequency ''' print('Testing aerofoil in sinusoidal gust...') # random geometry c = 3. b = 0.5 * c # gust profile w0 = 0.001 uinf = 20.0 L = 10. * c # <--- gust wakelength # discretisation WakeFact = 20 Ncycles = 18 # number of "cycles" Mfact = 14 if c > L: M = int(np.ceil(4 * Mfact * c / L)) else: M = Mfact * 4 # Reference state S0 = uvlm2d_sta.solver(M=M, Mw=M * WakeFact, b=b, Uinf=np.array([uinf, 0.]), alpha=0.0, rho=1.225) S0.build_flat_plate() S0._saveout = False S0.solve_static_Gamma2d() Ftot0 = np.sum(S0.Faero, 0) # Linearised model Slin = lin_uvlm2d_dyn.solver(S0, T=Ncycles * L / uinf) Slin = set_gust.sin(Slin, w0, L, ImpStart=False) Slin.eps_Hall = 0.002 Slin._savename='lindyn_sin_gust_M%.3d_wk%.2d_ccl%.2d.h5'\ %(M,WakeFact,Ncycles) Slin.solve_dyn_Gamma2d() # total force THFtot = 0.0 * Slin.THFaero for tt in range(Slin.NT): THFtot[tt, :] = Slin.THFaero[tt, :] + Ftot0 THCF = THFtot / S0.qinf / S0.chord # Analytical solution CLv = an.sears_lift_sin_gust(w0, L, uinf, c, Slin.time) fig = plt.figure('Aerodynamic force coefficients', (10, 6)) ax = fig.add_subplot(111) ax.set_title(r'Lift') ax.plot(Slin.time, CLv, 'k', lw=2, label=r"Analytical") ax.plot(Slin.time, THCF[:, 1], 'b', label=r'Numerical') ax.legend() if self.SHOW_PLOT: plt.show() else: plt.close() ### Detect peak and tim eof peak in last cycle # error on CL time-history not representative (amplified by lag) ttvec = Slin.time > float(Ncycles - 2) / Ncycles * Slin.T # num sol. ttmax = np.argmax(THCF[ttvec, 1]) Tmax = Slin.time[ttvec][ttmax] CLmax = THCF[ttvec, 1][ttmax] # an. sol ttmax = np.argmax(CLv[ttvec]) Tmax_an = Slin.time[ttvec][ttmax] CLmax_an = CLv[ttvec][ttmax] # Expected values at low fidelity TmaxExp = 25.9205357 CLmaxExp = 0.00019761846109 ### Error ErCLmax = np.abs(CLmax / CLmax_an - 1.0) Period = L / uinf ErTmax = np.abs((Tmax - Tmax_an) / Period) tolCL = 0.5e-2 # tolTmax=6.e-2 self.assertTrue(ErCLmax<tolCL,msg='Sinusoidal Gust: relative CLmax '\ 'error %.2e above tolerance %.2e!' %(ErCLmax,tolCL)) # self.assertTrue(ErTmax<tolTmax,msg='Sinusoidal Gust: relative Tmax '\ # 'error %.2e above tolerance %.2e!' %(ErTmax,tolTmax)) # Issue warnings changeTmaxnum = np.abs(Tmax - TmaxExp) changeCLnum = np.abs(CLmax - CLmaxExp) if changeCLnum > self.TOL_numerical: warnings.warn('Relative change in CLmax numerical solution '\ 'of %.2e !!!'%changeCLnum) if changeTmaxnum > self.TOL_numerical: warnings.warn('Relative change in Tmax numerical solution '\ 'of %.2e !!!'%changeTmaxnum)
def test_plunge(self): ''' Plunge motion at low, medium-high and high reduced frequencies. Especially at high reduced frequencies, the discretisation used are not refined enought to track correctly the lift. The accuracy of the induced drag is, instead, higher. Increasing the mesh (M) provides a big improvement, but the test case will be very slow. @warning: these test cases are different from those implemented in test_dyn.py for the geometrically-exact solution. ''' # random geometry/frequency c = 3. b = 0.5 * c f0 = 2. #Hz w0 = 2. * np.pi * f0 #rad/s for case in ['low', 'medium-high', 'high']: print('Testing aerofoil in plunge motion at %s frequency...' % case) if case is 'low': ktarget = 0.1 H = 0.001 * b Ncycles = 5. WakeFact = 16 M = 40 if case is 'medium-high': ktarget = .75 H = 0.01 * b Ncycles = 9. WakeFact = 20 M = 60 if case is 'high': ktarget = 1.0 H = 0.001 * b Ncycles = 10. WakeFact = 20 M = 70 # speed/time uinf = b * w0 / ktarget T = 2. * np.pi * Ncycles / w0 ### reference static solution S0 = uvlm2d_sta.solver(M=M, Mw=M * WakeFact, b=b, Uinf=np.array([uinf, 0.]), alpha=0.0 * np.pi / 180., rho=1.225) S0.build_flat_plate() S0._saveout = False S0.solve_static_Gamma2d() Ftot0 = np.sum(S0.Faero, 0) # Linearised solution Slin = lin_uvlm2d_dyn.solver(S0, T) Slin = set_dyn.plunge(Slin, f0, H) Slin.eps_Hall = 0.003 Slin._savename='lindyn_plunge_%s_M%.3d_wk%.2d_ccl%.2d_hall%.2e.h5'\ %(case,M,WakeFact,Ncycles,Slin.eps_Hall) Slin.solve_dyn_Gamma2d() # Total force THFtot = 0.0 * Slin.THFaero for tt in range(Slin.NT): THFtot[tt, :] = Slin.THFaero[tt, :] + Ftot0 THCF = THFtot / S0.qinf / S0.chord THCFmass = Slin.THFaero_m / S0.qinf / S0.chord THCFcirc = THCF - THCFmass ### post-process hc_num = (Slin.THZeta[:, 0, 1] - H) / S0.chord aeffv_num = np.zeros((Slin.NT)) aeffv_num = -np.arctan(Slin.THdZetadt[:, 0, 1] / S0.Uinf[0]) ### Analytical solution hv_an = -H * np.cos(w0 * Slin.time) hc_an = hv_an / S0.chord dhv = w0 * H * np.sin(w0 * Slin.time) aeffv_an = np.arctan(-dhv / S0.Uabs) # drag - Garrik Cdv = an.garrick_drag_plunge(w0, H, S0.chord, S0.rho, uinf, Slin.time) # lift - Theodorsen Ltot_an, Lcirc_an, Lmass_an = an.theo_lift(w0, 0, H, S0.chord, S0.rho, S0.Uinf[0], 0.0) ph_tot = np.angle(Ltot_an) ph_circ = np.angle(Lcirc_an) ph_mass = np.angle(Lmass_an) CLtot_an = np.abs(Ltot_an) * np.cos(w0 * Slin.time + ph_tot) / (S0.chord * S0.qinf) CLcirc_an = np.abs(Lcirc_an) * np.cos(w0 * Slin.time + ph_circ) / ( S0.chord * S0.qinf) CLmass_an = np.abs(Lmass_an) * np.cos(w0 * Slin.time + ph_mass) / ( S0.chord * S0.qinf) ### Phase plots plt.close('all') fig = plt.figure( 'Induced drag in plunge motion -' ' Phase vs kinematics', (10, 6)) ax = fig.add_subplot(111) ax.plot(180. / np.pi * aeffv_an, Cdv, 'k', lw=2, label=r'Analytical') ax.plot(180. / np.pi * aeffv_num, THCF[:, 0], 'b', label=r'Numerical') ax.set_xlabel('deg') ax.set_title('M=%s' % M) ax.legend() fig = plt.figure('Lift in plunge motion - ' 'Phase vs kinematics', (10, 6)) ax = fig.add_subplot(111) # analytical ax.plot(hc_an, CLtot_an, 'k', lw=2, marker='o', markevery=(.3), label=r'An Tot') # numerical ax.plot(hc_num, THCF[:, 1], 'b', label=r'Num Tot') ax.set_xlabel('h/c') ax.legend() ### Time histories fig = plt.figure('Time histories', (12, 5)) ax = fig.add_subplot(121) ax.plot(Slin.time, hc_num, 'y', lw=2, label='h/c') ax.plot(Slin.time, aeffv_num, '0.6', lw=2, label='Angle of attack [10 deg]') ax.plot(Slin.time, CLtot_an, 'k', lw=2, label='An Tot') ax.plot(Slin.time, THCF[:, 1], 'b', lw=1, label='Num Tot') ax.set_xlabel('time') ax.set_xlim((1. - 1. / Ncycles) * T, T) ax.set_title('Lift coefficient') ax.legend() ax = fig.add_subplot(122) ax.plot(Slin.time, Cdv, 'k', lw=2, label='An Tot') ax.plot(Slin.time, THCF[:, 0], 'b', lw=1, label='Num Tot') ax.set_xlim((1. - 1. / Ncycles) * T, T) ax.set_xlabel('time') ax.set_title('Drag coefficient') ax.legend() if self.SHOW_PLOT: plt.show() else: plt.close() ### Error in last cycle ttvec = Slin.time > float(Ncycles - 1) / Ncycles * Slin.T ErCL = np.abs(THCF[ttvec, 1] - CLtot_an[ttvec]) / np.max( CLtot_an[ttvec]) #ErCD=np.abs(THCF[ttvec,0]-Cdv[ttvec])/np.max(np.abs(Cdv[ttvec])) # plt.plot(Slin.time[ttvec],ErCD,'b',label='CD') # plt.plot(Slin.time[ttvec],ErCL,'k',label='CL') # plt.legend() # plt.show() ErCLmax = np.max(ErCL) #ErCDmax=np.max(ErCD) # Set tolerance and compare vs. expect results at low refinement if case is 'low': tolCL = 1.0e-2 ErCLnumExp = 0.0090214585275968876 if case is 'medium-high': tolCL = 1.8e-2 ErCLnumExp = 0.017774364218129 if case is 'high': tolCL = 2.0e-2 ErCLnumExp = 0.019179543357842 # Check/raise error self.assertTrue(ErCLmax<tolCL,msg='Plunge case %s: relative CL '\ 'error %.2e above tolerance %.2e!' %(case,ErCLmax,tolCL)) # Issue warnings changeCLnum = np.abs(ErCLmax - ErCLnumExp) if changeCLnum > self.TOL_numerical: warnings.warn('Relative change in CL numerical solution '\ 'of %.2e !!!'%changeCLnum)
self.THFdistr = self.THFdistr * S0.Fref ### wake # self.WzetaW=self.WzetaW*self.Uabs # self.UzetaW=self.UzetaW*self.Uabs if __name__ == '__main__': import time import uvlm2d_sta import pp_uvlm2d as pp ### build reference state Mw = 8 M = 3 alpha = 2. * np.pi / 180. ainf = 2.0 * np.pi / 180. Uinf = 20. * np.array([np.cos(ainf), np.sin(ainf)]) chord = 3. b = 0.5 * chord # static exact solution S0 = uvlm2d_sta.solver(M, Mw, b, Uinf, alpha, rho=1.225) S0.build_camber_plate(Mcamb=30, Pcamb=4) S0.solve_static_Gamma2d() # linearise dynamic T = 2.0 Slin = solver(S0, T) Slin.solve_dyn_Gamma2d()