def __init__(self): self._db = pth.db.airfoil self.name = None self.pts = None self.ptsUp = None self.ptsLo = None self.thickness = None self.camber = None self.camberLocation = None self.leRadius = None self.zTrailingEdge = 0.0 self._curveUp = None self._curveLo = None self.polar = AirfoilPolar() self._distribution = 'cos'
class Airfoil: def __init__(self): self._db = pth.db.airfoil self.name = None self.pts = None self.ptsUp = None self.ptsLo = None self.thickness = None self.camber = None self.camberLocation = None self.leRadius = None self.zTrailingEdge = 0.0 self._curveUp = None self._curveLo = None self.polar = AirfoilPolar() self._distribution = 'cos' def read_db(self,name,xlsPath=None): """ Reads airfoil from xls database. If additional geometry information and aerodynamic tables exist then existing tables are used otherwise information will be created using internal tools of module. """ if xlsPath==None: xlsPath = pth.db.airfoil db = db_tools.ReadDatabase(xlsPath,name) self.name = name xCoord = db.read_row(0,1) yCoord = db.read_row(1,1) self.pts = np.transpose([xCoord,yCoord]) self._separate_coordinates() # geometry i = db.find_header('GEOMETRY') if i==-1: self._analyze_geometry() else: self.thickness = db.read_row(i+1,1) self.camber = db.read_row(i+2,1) self._analyze_geometry() # analysis i = db.find_header('ANALYSIS') if i==-1: self.build_aero_table() else: self.polar.source = db.read_row(i+1,1) i = db.find_header('LIFT COEFFICIENT') nAlpha = db.find_header('DRAG COEFFICIENT') - i-2 self.polar.Mach = np.array(db.read_row(i+1,1)) self.polar.alpha = np.array(db.read_column(0,i+2,nAlpha)) self.polar.cl = db.read_row_range(i+2,1,nAlpha) i = db.find_header('DRAG COEFFICIENT') self.polar.cd = db.read_row_range(i+2,1,nAlpha) i = db.find_header('MOMENT COEFFICIENT') self.polar.cm = db.read_row_range(i+2,1,nAlpha) self.polar.cl = np.transpose(self.polar.cl) self.polar.cd = np.transpose(self.polar.cd) self.polar.cm = np.transpose(self.polar.cm) self.polar._create_splines() self.polar._calc_clmax() def write_db(self,xlsPath=None,includePolars=True): """ Writes airfoil to xls format database. """ if xlsPath==None: xlsPath=pth.db.airfoil db = db_tools.WriteDatabase(xlsPath,self.name) db.write_row('X',self.pts[:,0]) db.write_row('Y',self.pts[:,1]) db.write_row('GEOMETRY') db.write_row('thickness',self.thickness) db.write_row('camber' ,self.camber) if includePolars: db.write_row('ANALYSIS') db.write_row('method', self.polar.source) db.write_row('LIFT COEFFICIENT') db.write_row('Mach list',self.polar.Mach) i = db._irowPrev db.write_column(self.polar.alpha) db._irowPrev = i db.write_row_range(np.transpose(self.polar.cl),-1,1) db.write_row('DRAG COEFFICIENT') db.write_row('Mach list',self.polar.Mach) i = db._irowPrev db.write_column(self.polar.alpha) db._irowPrev = i db.write_row_range(np.transpose(self.polar.cd),-1,1) db.write_row('MOMENT COEFFICIENT') db.write_row('Mach list',self.polar.Mach) i = db._irowPrev db.write_column(self.polar.alpha) db._irowPrev = i db.write_row_range(np.transpose(self.polar.cm),-1,1) db.save() def read_txt(self,airfoilPath): """ Reads airfoil coordinates from text file. Two formats of coordinates are supported. """ fid = open(airfoilPath,'rt') self.name = str(fid.readline()[:-1]) lines = fid.readlines() fid.close() seg = lines[0].split() if float(seg[0])>3 and float(seg[1])>3: iup = int(round(float(seg[0]))) ilo = int(round(float(seg[1]))) self._read_txt_type2(lines[1:],iup,ilo) else: self._read_txt_type1(lines) self._analyze_geometry() def write_txt(self,filePath,tab=True,writeName=True): """ writes airfoil coordinates to text file. Parameters ---------- filePath : path airfoil text file path to write coordinates tab : bool white space parameter. If True then x,y coordinates are separated by tabbing otherwise separated by 4 spacings as required for X-foil input. writeName : bool if True then airfoil name will be written to the text file header otherwise only coordinates will be stored """ afFile = open(filePath,'wt') if writeName: afFile.write('%s\n'%self.name) if tab: whiteSpace = '\t' else: whiteSpace = ' ' for point in self.pts: afFile.write('%.6f%s%.6f\n'%(point[0],whiteSpace,point[1])) afFile.close() def _line_coord_to_float(self,lines): output = list() for line in lines: if line.strip()!='': seg = line.split() outline = np.zeros(2) outline[0] = float(seg[0]) outline[1] = float(seg[1]) output.append(outline) return np.array(output) def _separate_coordinates(self): self.ptsUp, self.ptsLo = geom.separate_coordinates(self.pts) def _join_coordinates(self): self.pts = geom.join_coordinates(self.ptsUp, self.ptsLo) def _read_txt_type1(self,lines): """ reads airfoil coordinates from text file stored in xfoil and javafoil like format. Text format as follows: 1. airfoil name 2. coordinates starting from upper curve trailing edge (xx yy) Parameters ---------- filePath : path airfoil coordinates file path Returns ------- coord : 2d array airfoil coordinates name : string airfoil name """ self.pts = self._line_coord_to_float(lines) self._separate_coordinates() def _read_txt_type2(self,lines,nPtsUp,nPtsLo): """ reads airfoil coordinates from text file stored in format similar to uiuc db file format. Text format as follows: 1. airfoil name 2. number of upper and lower curve points (n n) 3. upper curve points starting from leading edge (xx yy) 4. lower curve points starting from leading edge (xx yy) Parameters ---------- filePath : path airfoil coordinates file path Returns ------- coord : 2d array airfoil coordinates name : string airfoil name """ coordRaw = self._line_coord_to_float(lines) self.ptsUp = coordRaw[:nPtsUp] self.ptsLo = coordRaw[nPtsUp:nPtsUp+nPtsLo] self._join_coordinates() def display(self,linetype='ko-'): fig = plt.figure() ax = fig.add_subplot(111) ax.grid(True) ax.axis('equal') ax.set_title(self.name) ax.plot(self.pts[:,0],self.pts[:,1],linetype) plt.show() def _create_splines(self): """ create cubic splines for x and y coordinate with respect to curve length parameter """ # parametric splines x = x(t), y = y(t) lenUp = geom.curve_pt_dist_normalized(self.ptsUp) lenLo = geom.curve_pt_dist_normalized(self.ptsLo) self._curveUp = CurveXyt(self.ptsUp[:,0],self.ptsUp[:,1],lenUp) self._curveLo = CurveXyt(self.ptsLo[:,0],self.ptsLo[:,1],lenLo) ptsUp,ptsLo = self._sort_pts() self._curveUp2 = interp1d(ptsUp[:,0],ptsUp[:,1],'cubic') self._curveLo2 = interp1d(ptsLo[:,0],ptsLo[:,1],'cubic') self._xmin = max([ptsUp[0,0],ptsLo[0,0]]) self._xmax = min([ptsUp[-1,0],ptsLo[-1,0]]) def _analyze_geometry(self): """ calculates airfoil geometry parameters """ self._create_splines() tc = lambda x: self._curveLo2(x) - self._curveUp2(x) camber = lambda x: -(self._curveLo2(x) + self._curveUp2(x)) self.thicknessLocation = fminbound(tc,self._xmin,self._xmax) self.camberLocation = fminbound(camber,self._xmin,self._xmax) self.thickness = -tc(self.thicknessLocation) self.camber = -camber(self.camberLocation)/2.0 n = len(self.pts)-1 L = np.zeros(n) for i in range(n): L[i] = ((self.pts[i,0]-self.pts[i+1,0])**2.0 + (self.pts[i,1]-self.pts[i+1,1])**2.0)**0.5 self.length = L.sum() self.zTrailingEdge = self.ptsUp[-1,1] - self.ptsLo[-1,1] # self._calc_leadingedge_radius() # # def _calc_leadingedge_radius(self): # dt = 1e-5 # pt1 = self._curveLo(dt) # pt2 = self._curveLo(0.0) # pt3 = self._curveUp(dt) # dx1,dx2 = pt3[0]-pt2[0], pt2[0]-pt1[0] # print dx1, dx2 def _sort_pts(self): xmin = 0.05 xmax = 0.95 def sort_pts(pts,xmin,xmax): for i,pt in enumerate(pts): if pt[0]>=xmin: idxStart = i break for i,pt in enumerate(pts[idxStart:]): if pt[0]>=xmax: idxEnd = i+idxStart break return pts[idxStart:idxEnd] ptsUp = sort_pts(self.ptsUp,xmin,xmax) ptsLo = sort_pts(self.ptsLo,xmin,xmax) return ptsUp,ptsLo def _get_point_distribution(self,nPts=30,distribution=None): if distribution==None: distribution = self._distribution if distribution=='sin': return geom.get_sine_distribution(nPts) elif distribution=='cos': return geom.get_cosine_distribution(nPts) def redim(self,nPts,overwrite=True,distribution='sin'): """ Redimension and redistribution of airfoil points using cosine function. More points are located at leading and trailing edge. Parameters ---------- numPts : integer number of points for target airfoil. If number of points is same as source airfoil, points will be redistributed to make smooth surface overwrite : bool If True then self.pts, self.upPts and self.loPts will be overwritten, otherwise will return array of redimensioned points in format of self.pts """ if nPts%2==0: nPts1 = nPts/2 nPts2 = nPts/2+1 else: nPts1 = (nPts+1)/2 nPts2 = nPts1 #nPts *= 0.5 t1 = self._get_point_distribution(nPts1,distribution) t2 = self._get_point_distribution(nPts2,distribution) self._create_splines() xUp, yUp = self._curveUp(t1) xLo, yLo = self._curveLo(t2) upPts = np.transpose(np.vstack([xUp,yUp])) loPts = np.transpose(np.vstack([xLo,yLo])) coord = geom.join_coordinates(upPts,loPts) if overwrite: self.pts = coord self.ptsUp = upPts self.ptsLo = loPts else: return coord def scale_thickness(self,thicknessNew,analysis=False): """ Scales airfoil in Y-direction to achieve required thickness. Useful to be used with propeller analysis when same airfoil has different thickness at different *x* stations. Parameters ---------- newThickness: float thickness/chord of result airfoil analysis : bool If True then aerodynamic table will be generated using self.build_aero_table, otherwise only geometry will be changed. """ scale = thicknessNew / self.thickness self.name = self.name + '_tc%.2f'%(thicknessNew*100.0) self.pts[:,1] = self.pts[:,1]*scale self._separate_coordinates() self.redim(50,overwrite=True) if analysis: self.build_aero_table() def build_aero_table(self,MachSeq=[0.1,0.9,0.1],alphaSeq=[-20,20,1.0], mode='javafoil'): """ builds full table of aerodynamic coefficients using *fast* solvers Coefficients to be caclulated are as follows: - lift coefficient vs. alpha, Mach - drag coefficient vs. alpha, Mach - moment coefficient vs. alpha, Mach Parameters ---------- MachSeq : array float array of Mach number to be analyzed in format [start, end, step] alphaSeq : array float array of angle of attack sequence to be analyzed in format [start, end, step] mode : string if mode='javafoil' then coefficients will be generated using javafoil, if 'xfoil' then using xfoil """ Mach = np.arange(MachSeq[0], MachSeq[1], MachSeq[2]) Re = list() for i,M in enumerate(Mach): Re.append(fc.FlightConditions(M,0.0).Re) if mode=='javafoil': tmpPolar = self.get_jfoil_polar(M, Re[i], alphaSeq) elif mode=='xfoil': tmpPolar = self.get_xfoil_polar(M, Re[i], alphaSeq, 200) if i==0: self.polar.cl = tmpPolar.cl self.polar.cd = tmpPolar.cd self.polar.cm = tmpPolar.cm else: self.polar.cl = np.vstack([self.polar.cl, tmpPolar.cl]) self.polar.cd = np.vstack([self.polar.cd, tmpPolar.cd]) self.polar.cm = np.vstack([self.polar.cm, tmpPolar.cm]) self.polar.Re = Re self.polar.source = 'javafoil' self.polar.Mach = Mach self.polar.alpha = tmpPolar.alpha self.polar._create_splines() def get_xfoil_polar(self,Mach,Re,alphaSeq=[-15,15,1],nIter=10,graphic=False,smooth=False, flapChordRatio=0.3,flapDefl=None): """ Calculates aerodynamic coefficients at given flight conditions using Xfoil. Parameters ---------- Mach : float Mach number Re : float Reynolds number alphaSeq : array float array of angle of attack sequence to be analyzed in format [start, end, step] nIter : integer number of xfoil iterations. Default value is that is enough for conventional airfoil configurations. For complex airfoil shapes this number should be increased (for example while optimization using genetic algorithm) Returns ------- polar : AirfoilPolar airfoil polar with all aerodynamic data at specified flight conditions """ return xf.get_xfoil_analysis(self,Mach,Re,alphaSeq,nIter,graphic,smooth,flapChordRatio,flapDefl) def get_jfoil_polar(self,Mach,Re,alphaSeq=[-15,15,1], stall='eppler',transition='drelaAfter1991',surface='NACAstandard', flapDefl=None,flapChordRatio=30.0): """ Calculates aerodynamic coefficients at given flight conditions using Xfoil. Parameters ---------- Mach : float Mach number Re : float Reynolds number alphaSeq : array float array of angle of attack sequence to be analyzed in format [start, end, step] stall : string stall model. Available options: calcfoil, eppler transition : string transition model. Availabel options: epplerStandard, epplerExtended, michel1, michel2, H12Re, granville, drelaBefore1991, drelaAfter1991, arnal surface : string surface type. Available options: smooth, paintedFabrid, NACAstandard, bugsDirt Returns ------- polar : AirfoilPolar airfoil polar with all aerodynamic data at specified flight conditions Note ---- Javafoil requires path to java installed in system in :file:`javapath.txt`. For linux OS file should contain *java* keyword only """ return jf.get_javafoil_analysis(self,Mach,Re,alphaSeq,stall,transition, surface,flapChordRatio,flapDefl) def create_cst(self,Au,Al,nPts=25,zTE=None): self.upCurve = geom.CstCurve(Au) self.loCurve = geom.CstCurve(Al) x = self._get_point_distribution(nPts) self.ptsUp = self.upCurve.get_coordinates(x) self.ptsLo = self.loCurve.get_coordinates(x) self._join_coordinates() if not zTE==None: self.set_trailing_edge(zTE) self._analyze_geometry() self.name = 'CST airfoil' def create_naca4(self,thickness=12.0,camber=0.0,camberLoc=0.0,nPts=25): t = thickness / 100.0 m = camber / 100.0 p = camberLoc / 100.0 x = self._get_point_distribution(nPts) y = t/0.2*(0.2969*x**0.5 - 0.1281*x - 0.3516*x*x + 0.2843*x**3.0 - 0.1015*x**4.0) self.thickness = t self.camber = m self.camberLocation = p self.name = 'NACA%.0f%.0f%.0f'%(camber,camberLoc/10.0,thickness) if not m*p==0.0: yc = np.zeros(len(x)) tanTheta = np.zeros(len(x)) for i,xpt in enumerate(x): if xpt<=p: yc[i] = m*xpt/(p*p)*(2.0*p - xpt) tanTheta[i] = 2.0*m/(p*p)*(p-xpt) else: yc[i] = m*(1.0-xpt)/((1.0-p)**2.0)*(1.0+xpt-2.0*p) tanTheta[i] = -2.0*m/((1.0-p)**2.0)*(xpt-p) theta = np.arctan(tanTheta) xu = x - y*np.sin(theta) xl = x + y*np.sin(theta) yu = yc + y*np.cos(theta) yl = yc - y*np.cos(theta) self.ptsUp = np.transpose(np.vstack([xu,yu])) self.ptsLo = np.transpose(np.vstack([xl,yl])) else: self.ptsUp = np.transpose(np.vstack([x,y])) self.ptsLo = np.transpose(np.vstack([x,-y])) self._join_coordinates() def set_trailing_edge(self,zTEnew=0.0): zTEcurrent = self.ptsUp[-1,1] - self.ptsLo[-1,1] dzTE = zTEnew - zTEcurrent ptsUpNew = self.ptsUp[:,1] + self.ptsUp[:,0]*dzTE/2. ptsLoNew = self.ptsLo[:,1] - self.ptsLo[:,0]*dzTE/2. self.ptsUp[:,1] = ptsUpNew self.ptsLo[:,1] = ptsLoNew self._join_coordinates() self._analyze_geometry() def get_clmax(self,velocity,altitude,flapToChord,flapDefl): """ analysis using javafoil since it's more robust than xfoil with calcfoil stall model. Note that only plain flap are possible. Parameters ---------- velocity : m/sec or Mach airspeed altitude : m flapToChord : [0;1] flap to chord ratio flapDefl : deg flap deflection angle. """ fc1 = fc.FlightConditions(velocity,altitude,1.0) Mach = fc1.Mach Re = fc1.Re pol = self.get_jfoil_polar(Mach,Re,[0,20,1],'eppler', flapChordRatio=flapToChord, flapDefl=flapDefl) return float(pol.clmax), pol.alphaClmax def fit_cst(self, nBPO=3): """ generates CST coefficients of given BPO for current airfoil. Parameters ---------- nBPO : int Bernstein polynomial order """ nvar = nBPO*2+1 # ---- def get_error(curve,pts): sqErr = np.zeros(len(pts[:,0])) for i,x in enumerate(pts[:,0]): ynew = curve(x) sqErr[i] = (ynew-pts[i,1])**2.0 return sqErr def obj(x): n = len(x) if n%2==0: Au = x[:n/2] Al = x[n/2:] else: Au = np.array(x[:int(n/2)+1]) Al = np.hstack([-x[0],x[int(n/2)+1:]]) upCurve = geom.CstCurve(Au,TEgap=self.zTrailingEdge) loCurve = geom.CstCurve(Al,TEgap=self.zTrailingEdge) sqErrUp = get_error(upCurve,self.ptsUp) sqErrLo = get_error(loCurve,self.ptsLo) err = np.sum(sqErrUp) + np.sum(sqErrLo) return err # ---- xl = -0.5*np.ones(nvar) xu = 0.5*np.ones(nvar) bnds = np.vstack([xl,xu]) bnds = np.transpose(bnds) x0 = np.zeros(nvar) rslt = minimize(obj,x0,bounds=bnds,method='SLSQP') xopt = rslt.x return xopt, obj(xopt)/(len(self.pts)+1)