"""Analytical Functions Analytical solutions for 2D aerofoil based on thin plates theory Author: Salvatore Maraniello Date: 23 May 2017 References: 1. Simpson, R.J.S., Palacios, R. & Murua, J., 2013. Induced-Drag Calculations in the Unsteady Vortex Lattice Method. AIAA Journal, 51(7), pp.1775–1779. 2. Gulcat, U., 2009. Propulsive Force of a Flexible Flapping Thin Airfoil. Journal of Aircraft, 46(2), pp.465–473. """ import numpy as np import scipy.special as scsp # imaginary variable j = 1.0j def theo_fun(k): r"""Returns the value of Theodorsen's function at a reduced frequency :math:`k`. .. math:: \mathcal{C}(jk) = \frac{H_1^{(2)}(k)}{H_1^{(2)}(k) + jH_0^{(2)}(k)} where :math:`H_0^{(2)}(k)` and :math:`H_1^{(2)}(k)` are Hankel functions of the second kind. Args: k (np.array): Reduced frequency/frequencies at which to evaluate the function. Returns: np.array: Value of Theodorsen's function evaluated at the desired reduced frequencies. """ H1 = scsp.hankel2(1, k) H0 = scsp.hankel2(0, k) C = H1 / (H1 + j * H0) return C def qs_derivs(x_ea_perc, x_fh_perc): """ Provides quasi-steady aerodynamic lift and moment coefficients derivatives Ref. Palacios and Cesnik, Chap 3. Args: x_ea_perc: position of axis of rotation in percentage of chord (measured from LE) x_fc_perc: position of flap axis of rotation in percentage of chord (measured from LE) """ # parameters nu_ea = 2.0 * x_ea_perc - 1.0 nu_fh = 2.0 * x_fh_perc - 1.0 th = np.arccos(-nu_fh) # pitch/pitch rate related quantities CLa = 2. * np.pi # ok CLda = np.pi * (1. - 2. * nu_ea) CMda = -0.25 * np.pi # flap related quantities CLb = 2. * (np.pi - th + np.sin(th)) CLdb = (0.5 - nu_fh) * 2. * (np.pi - th) + (2. - nu_fh) * np.sin(th) CMb = -0.5 * (1 + nu_fh) * np.sin(th) CMdb = -.25 * (np.pi - th + 2. / 3. * np.sin(th) * (0.5 - nu_fh) * (2. + nu_fh)) return CLa, CLda, CLb, CLdb, CMda, CMb, CMdb def nc_derivs(x_ea_perc, x_fh_perc): """ Provides non-circulatory aerodynamic lift and moment coefficients derivatives Ref. Palacios and Cesnik, Chap 3. Args: x_ea_perc: position of axis of rotation in percentage of chord (measured from LE) x_fc_perc: position of flap axis of rotation in percentage of chord (measured from LE) """ # parameters nu_ea = 2.0 * x_ea_perc - 1.0 nu_fh = 2.0 * x_fh_perc - 1.0 th = np.arccos(-nu_fh) # pitch/pitch rate related quantities CLda = np.pi # ok CLdda = -np.pi * nu_ea CMda = -.25 * np.pi CMdda = -0.25 * np.pi * (0.25 - nu_ea) # flap related quantities CLdb = np.pi - th - nu_fh * np.sin(th) CLddb = -nu_fh * (np.pi - th) + 1. / 3. * (2. + nu_fh ** 2) * np.sin(th) CMdb = -0.25 * (np.pi - th + (2. / 3. - nu_fh - 2. / 3. * nu_fh ** 2) * np.sin(th)) CMddb = -0.25 * ((0.25 - nu_fh) * (np.pi - th) + \ (2. / 3. - 5. / 12. * nu_fh + nu_fh ** 2 / 3. + nu_fh ** 3 / 6.) * np.sin(th)) return CLda, CLdda, CLdb, CLddb, CMda, CMdda, CMdb, CMddb def theo_CL_freq_resp(k, x_ea_perc, x_fh_perc): """ Frequency response of lift coefficient according Theodorsen's theory. The output is a 3 elements array containing the CL frequency response w.r.t. to pitch, plunge and flap motion, respectively. Sign conventions are as follows: * plunge: positive when moving upward * x_ea_perc: position of axis of rotation in percentage of chord (measured from LE) * x_fc_perc: position of flap axis of rotation in percentage of chord (measured from LE) Warning: this function uses different input/output w.r.t. theo_lift """ df, ddf = j * k, -k ** 2 # get quasi-steady derivatives CLa_qs, CLda_qs, CLb_qs, CLdb_qs, void, void, void = qs_derivs(x_ea_perc, x_fh_perc) # quasi-steady lift CLqs = np.array([ CLa_qs + CLda_qs * df, CLa_qs * df, CLb_qs + CLdb_qs * df, ]) # get non-circulatory derivatives CLda_nc, CLdda_nc, CLdb_nc, CLddb_nc, void, void, void, void \ = nc_derivs(x_ea_perc, x_fh_perc) # unsteady lift df, ddf = j * k, -k ** 2 CLun = np.array([ CLda_nc * df + CLdda_nc * ddf, CLda_nc * ddf, CLdb_nc * df + CLddb_nc * ddf ]) ### Total response Y = theo_fun(k) * CLqs + CLun # sign convention update Y[1] = -Y[1] # plunge dof positive upward return Y def theo_CM_freq_resp(k, x_ea_perc, x_fh_perc): """ Frequency response of moment coefficient according Theodorsen's theory. The output is a 3 elements array containing the CL frequency response w.r.t. to pitch, plunge and flap motion, respectively. """ df, ddf = j * k, -k ** 2 # get quasi-steady derivatives void, void, void, void, CMda_qs, CMb_qs, CMdb_qs = qs_derivs(x_ea_perc, x_fh_perc) # quasi-steady lift CMqs = np.array([ CMda_qs * df, 0.0 * k, CMb_qs + CMdb_qs * df, ]) # get non-circulatory coefficients void, void, void, void, CMda_nc, CMdda_nc, CMdb_nc, CMddb_nc = \ nc_derivs(x_ea_perc, x_fh_perc) # unsteady lift CMun = np.array([ CMda_nc * df + CMdda_nc * ddf, CMda_nc * ddf, CMdb_nc * df + CMddb_nc * ddf ]) ### Total response Y = CMqs + CMun # sign convention update Y[1] = -Y[1] return Y def theo_lift(w, A, H, c, rhoinf, uinf, x12): r""" Theodorsen's solution for lift of aerofoil undergoing sinusoidal motion. Time histories are built assuming: * ``a(t)=+/- A cos(w t) ??? not verified`` * :math:`h(t)=-H\cos(w t)` Args: w: frequency (rad/sec) of oscillation A: amplitude of angle of attack change H: amplitude of plunge motion c: aerofoil chord rhoinf: flow density uinf: flow speed x12: distance of elastic axis from mid-point of aerofoil (positive if the elastic axis is ahead) """ # reduced frequency k = 0.5 * w * c / uinf # compute theodorsen's function Ctheo = theo_fun(k) # Lift: circulatory Lcirc = np.pi * rhoinf * uinf * c * Ctheo * ((uinf + w * j * (0.25 * c + x12)) * A + w * H * j) Lmass = 0.25 * np.pi * rhoinf * c ** 2 * ((j * w * uinf - x12 * w ** 2) * A - H * w ** 2) Ltot = Lcirc + Lmass return Ltot, Lcirc, Lmass def garrick_drag_plunge(w, H, c, rhoinf, uinf, time): r""" Returns Garrick solution for drag coefficient at a specific time. Ref.[1], eq.(8) (see also eq.(1) and (2)) or Ref[2], eq.(2) The aerofoil vertical motion is assumed to be: .. math:: h(t)=-H\cos(wt) The :math:`C_d` is such that: * :math:`C_d>0`: drag * :math:`C_d<0`: suction """ b = 0.5 * c k = b * w / uinf Hast = H / b s = uinf * time / b # compute theodorsen's function Ctheo = theo_fun(k) Cd = -2. * np.pi * k ** 2 * Hast ** 2 * ( Ctheo.imag * np.cos(k * s) + Ctheo.real * np.sin(k * s)) ** 2 return Cd def garrick_drag_pitch(w, A, c, rhoinf, uinf, x12, time): r""" Returns Garrick solution for drag coefficient at a specific time. Ref.[1], eq.(9), (10) and (11) The aerofoil pitching motion is assumed to be: .. math:: a(t)=A\sin(\omegat)=A\sin(ks) The :math:`C_d` is such that: * :math:`C_d>0`: drag * :math:`C_d<0`: suction """ x12 = x12 / c b = 0.5 * c k = b * w / uinf s = uinf * time / b # compute theodorsen's function Ctheo = theo_fun(k) F, G = Ctheo.real, Ctheo.imag sks, cks = np.sin(k * s), np.cos(k * s) # angle of attack a = A * sks # lift term Cl = np.pi * A * (k * cks + x12 * k ** 2 * sks + 2. * F * (sks + (0.5 - x12) * k * cks) + 2. * G * (cks - (0.5 - x12) * k * sks)) # suction force Y1 = 2. * (F - k * G * (0.5 - x12)) Y2 = 2. * (G - k * F * (0.5 - x12)) - k Cs = 0.5 * np.pi * A ** 2 * (Y1 * sks + Y2 * cks) ** 2 Cd = a * Cl - Cs return Cd def sears_fun(kg): """ Produces Sears function """ S12 = 2. / np.pi / kg / (scsp.hankel1(0, kg) + 1.j * scsp.hankel1(1, kg)) S = np.exp(-1.j * kg) * S12.conj() return S def sears_lift_sin_gust(w0, L, Uinf, chord, tv): """ Returns the lift coefficient for a sinusoidal gust (see set_gust.sin) as the imaginary part of the CL complex function defined below. The input gust must be the imaginary part of .. math:: wgust = w0*\exp(1.0j*C*(Ux*S.time[tt] - xcoord) ) with: .. math:: C=2\pi/L and ``xcoord=0`` at the aerofoil half-chord. """ # reduced frequency kg = np.pi * chord / L # Theo's funciton Ctheo = theo_fun(kg) # Sear's function J0, J1 = scsp.j0(kg), scsp.j1(kg) S = (J0 - 1.0j * J1) * Ctheo + 1.0j * J1 phase = np.angle(S) CL = 2. * np.pi * w0 / Uinf * np.abs(S) * np.sin(2. * np.pi * Uinf / L * tv + phase) return CL def sears_CL_freq_resp(k): """ Frequency response of lift coefficient according Sear's solution. Ref. Palacios and Cesnik, Chap.3 """ # hanckel functions H1 = scsp.hankel1(1, k) H0 = scsp.hankel1(0, k) # Sear's function S12star = 2. / (np.pi * k * (H0 + 1.j * H1)) S0 = np.exp(-1.0j * k) * S12star.conj(S12star) # CL frequency response CL = 2. * np.pi * S0 return CL def wagner_imp_start(aeff, Uinf, chord, tv): """ Lift coefficient resulting from impulsive start solution. """ sv = 2.0 * Uinf / chord * tv fiv = 1.0 - 0.165 * np.exp(-0.0455 * sv) - 0.335 * np.exp(-0.3 * sv) CLv = 2. * np.pi * aeff * fiv return CLv def flat_plate_analytical(kv, x_ea_perc, x_fh_perc, input_seq, output_seq, output_scal=None, plunge_deriv=True): r""" Computes the analytical frequency response of a plat plate for the input output sequences in ``input_seq`` and ``output_seq`` over the frequency points ``kv``, if available. The output complex values array ``Yan`` has shape ``(Nout, Nin, Nk)``; if an analytical solution is not available, the response is assumed to be zero. If ``plunge_deriv`` is ``True``, the plunge response is expressed in terms of first derivative dh. Args: kv (np.array): Frequency range of length ``Nk``. x_ea_perc (float): Elastic axis location along the chord as chord length percentage. x_fh_perc (float): Flap hinge location along the chord as chord length percentage. input_seq (list(str)): List of ``Nin`` number of inputs. Supported inputs include: * ``gust_sears``: Response to a continuous sinusoidal gust. * ``pitch``: Response to an oscillatory pitching motion. * ``plunge``: Response to an oscillatory plunging motion. output_seq (list(str)): List of ``Nout`` number of outputs. Supported outputs include: * ``Fy``: Vertical force. * ``Mz``: Pitching moment. output_scal (np.array): Array of factors by which to divide the desired outputs. Dimensions of ``Nout``. plunge_deriv (bool): If ``True`` expresses the plunge response in terms of the first derivative, i.e. the rate of change of plunge :math:`d\dot{h}`. Returns: np.array: A ``(Nout, Nin, Nk)`` array containing the scaled frequency response for the inputs and outputs specified. See Also: The lift coefficient due to pitch and plunging motions is calculated using :func:`sharpy.utils.analytical.theo_CL_freq_resp`. In turn, the pitching moment is found using :func:`sharpy.utils.analytical.theo_CM_freq_resp`. The response to the continuous sinusoidal gust is calculated using :func:`sharpy.utils.analytical.sears_CL_freq_resp`. """ Nout = len(output_seq) Nin = len(input_seq) Nk = len(kv) Yfreq_an = np.zeros((Nout, Nin, Nk), dtype=np.complex) # Get Theodorsen solutions CLtheo = theo_CL_freq_resp(kv, x_ea_perc, x_fh_perc) CMtheo = theo_CM_freq_resp(kv, x_ea_perc, x_fh_perc) # scaling if output_scal is None: output_scal = np.ones((Nout,)) for oo in range(Nout): for ii in range(Nin): ### Sears if input_seq[ii] == 'gust_sears': # Fx,Mz null if output_seq[oo] == 'Fy': Yfreq_an[oo, ii, :] = sears_CL_freq_resp(kv) ### Theodorsen if input_seq[ii] == 'pitch': if output_seq[oo] == 'Fy': Yfreq_an[oo, ii, :] = CLtheo[0] if output_seq[oo] == 'Mz': Yfreq_an[oo, ii, :] = CMtheo[0] if input_seq[ii] == 'plunge': Fact = 1.0 if plunge_deriv: Fact = -1.j / kv if output_seq[oo] == 'Fy': Yfreq_an[oo, ii, :] = Fact * CLtheo[1] if output_seq[oo] == 'Mz': Yfreq_an[oo, ii, :] = Fact * CMtheo[1] # scale output Yfreq_an[oo, :, :] = Yfreq_an[oo, :, :] / output_scal[oo] return Yfreq_an # if __name__ == '__main__': # import matplotlib.pyplot as plt # # kv = np.linspace(0.001, 50, 1001) # # ### test Sear's function # sv = sears_fun(kv) # plt.plot(kv, sv.real, '-') # plt.plot(kv, sv.imag, '--') # plt.show() # # CL = theo_CL_freq_resp(kv, 0.25, 0.8) # # kv = np.linspace(0.001, 2.0, 101) # # data for dimensional analysis # b = 1.3 # U = 15.0 # H = 0.3 # A = 5.0 * np.pi / 180. # rho = 1.225 # tref = b / U # # ### plunge frequency response # Ltheo = -theo_lift(kv / tref, 0, H, 2. * b, rho, U, 0.0)[0] # Fref = b * rho * U ** 2 # CLfreq = Ltheo / Fref / (H / b) # # Yfreq = theo_CL_freq_resp(kv, x_ea_perc=1.0, x_fh_perc=0.9) # CLfreq02 = Yfreq[1] # Er = np.max(np.abs(CLfreq - CLfreq02)) # print('Max error for CL plunge freq response: %.2e' % Er) # # # moments # Yfreq = theo_CM_freq_resp(kv, x_ea_perc=1.0, x_fh_perc=0.9) # CMfreq = Yfreq[1] # plunge motion # CMfreq_vel = 1.j / kv * CMfreq # CMmag_vel, CMph_vel = np.abs(CMfreq_vel), np.angle(CMfreq_vel, deg=True) # # fig = plt.figure('Momentum coefficient frequency response') # ax = fig.add_subplot(121) # ax.plot(kv, CMmag_vel, 'k', label='magnitude') # ax.legend() # ax = fig.add_subplot(122) # ax.plot(kv, CMph_vel, 'r', label='phase') # ax.legend() # plt.show() # # ### pitching frequency response # Yfreq = theo_CL_freq_resp(kv, x_ea_perc=.5, x_fh_perc=0.9) # CLfreq02 = Yfreq[0] # # ### geometry # c = 3. # m # b = 0.5 * c # # ### motion # ktarget = 1. # H = 0.02 * b # m Ref.[1] # A = 1. * np.pi / 180. # rad - Ref.[1] # x12 = -0.5 * c # f0 = 5. # Hz # w0 = 2. * np.pi * f0 # rad/s # # uinf = b * w0 / ktarget # rhoinf = 1.225 # kg/m3 # qinf = 0.5 * c * rhoinf * uinf ** 2 # # C=theo_fun(k=ktarget) # # L=theo_lift(w0,A,H,c,rhoinf,uinf,x12) # # ##### Plunge Induced drag # Ncicles = 5 # tv = np.linspace(0., 2. * np.pi * Ncicles / w0, 200 * Ncicles + 1) # Cdv = garrick_drag_plunge(w0, H, c, rhoinf, uinf, tv) # hv = -H * np.cos(w0 * tv) # dhv = w0 * H * np.sin(w0 * tv) # aeffv = np.arctan(-dhv / uinf) # # fig = plt.figure('Induced drag - plunge motion',(10,6)) # # ax=fig.add_subplot(111) # # ax.plot(tv,hv/c,'r',label=r'h/c') # # ax.plot(tv,Cdv,'k',label=r'Induced Drag') # # ax.legend() # # plt.show() # fig = plt.figure('Plunge motion - Phase vs kinematics', (10, 6)) # ax = fig.add_subplot(111) # # ax.plot(aeffv,hv/c,'r',label=r'h/c') # ax.plot(180. / np.pi * aeffv, Cdv, 'k', label=r'Induced Drag') # ax.set_xlabel('deg') # ax.legend() # plt.close() # # ##### Pitching Induced drag # Ncicles = 5 # tv = np.linspace(0., 2. * np.pi * Ncicles / w0, 200 * Ncicles + 1) # Cdv = garrick_drag_pitch(w0, A, c, rhoinf, uinf, x12, tv) # aeffv = A * np.sin(w0 * tv) # fig = plt.figure('Pitch motion - Phase vs kinematics', (10, 6)) # ax = fig.add_subplot(111) # # ax.plot(aeffv,hv/c,'r',label=r'h/c') # ax.plot(180. / np.pi * aeffv, Cdv, 'k', label=r'Induced Drag') # ax.set_xlabel('deg') # ax.legend() # # ##### Sear's solution test # L = .5 * c # w0 = 0.3 # uinf = 6.0 # # # gust profile at LE # tv = np.linspace(0., 2., 300) # C = 2. * np.pi / L # wgustLE = w0 * np.sin(C * uinf * tv) # CLv = sears_lift_sin_gust(w0, L, uinf, c, tv) # # fig = plt.figure('Gust response', (10, 6)) # ax = fig.add_subplot(111) # ax.plot(tv, wgustLE, 'k', label=r'vertical gust velocity at LE [m/s]') # ax.plot(tv, CLv, 'r', label=r'CL') # ax.set_xlabel('time') # ax.legend() # # plt.show() # plt.close('all') # # ##### Wagner impulsive start # uinf = 20.0 # chord = 3.0 # aeff = 2.0 * np.pi / 180. # tv = np.linspace(0., 10., 300) # # CLv = wagner_imp_start(aeff, uinf, chord, tv) # CLv_inf = wagner_imp_start(aeff, uinf, chord, 1e3 * tv[-1]) # # fig = plt.figure('Impulsive start', (10, 6)) # ax = fig.add_subplot(111) # ax.plot(tv, CLv / CLv_inf, 'r', label=r'CL') # ax.set_xlabel('time') # ax.legend() # plt.show()