Does anyone have a script to generate an Airfoil ?

Landon Mitchell Kanner
Landon Mitchell Kanner Member, Employee, GitHub-issue-creator Posts: 319
50 Answers 100 Comments Second Anniversary 25 Likes
✭✭✭✭

Has anyone developed a script to generate the Airfoil based on input parameters like Max camber, Camber position etc. and worked on parameterization of the Airfoil? (e.g.: NACA 4-digit airfoil generator)

Answers

  • Landon Mitchell Kanner
    Landon Mitchell Kanner Member, Employee, GitHub-issue-creator Posts: 319
    50 Answers 100 Comments Second Anniversary 25 Likes
    ✭✭✭✭

    Solution from @Adam Anderson :

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # Copyright Ansys Inc 2024
    
    class NACA_Profile:
    
        """
    Generate NACA 4-digit profiles
    (refer to https://en.wikipedia.org/wiki/NACA_airfoil for details)
    """
    
        def __init__(self, thickness=15, chord_length=1.0, num_pts=50, 
                     camber=0, pos_max_camber=30, # Percent values
                     closed_trailing_edge=False):
    
            import math
    
            self.profile = [(0.0, 0.0)]
            self.num_pts = num_pts
    
            # Percent values
    
            self.chord_length = chord_length
            self.thickness = thickness / 100
            self.camber = camber / 100
    
            if camber:
                self.pos_max_camber = max(min(pos_max_camber,99),1)/100
            else:
                self.pos_max_camber = 0
    
            self.closed_trailing_edge = closed_trailing_edge
    
    
            delta_x = 1.0/(self.num_pts-1)
            x = 0.0
    
            if self.closed_trailing_edge:
                coeffs = (0.2969, -0.1260, -0.3516, +0.2843, -0.1036)
            else:
                coeffs = (0.2969, -0.1260, -0.3516, +0.2843, -0.1015)
    
            for i in range(1,self.num_pts):
    
                x += delta_x
    
                y = coeffs[0] * math.sqrt(x)
                y += coeffs[1] * x
                x2 = x*x
                y += coeffs[2] * x2
                x3 = x2*x
                y += coeffs[3] * x3
                x4 = x3*x
                y += coeffs[4] * x4
    
                self.profile.append((x, 5*y*self.thickness))
    
    
            # Add camber
    
            self.upper_profile = [(0.0, 0.0)]
            self.lower_profile = [(0.0, 0.0)]
            self.camber_line = [(0.0, 0.0)]
    
            for x,y in self.profile[1:]:
    
                yc, dyc_dx = self.get_camber_and_gradient(x)
    
                cost = 1/math.sqrt(1 + dyc_dx*dyc_dx)
                sint = dyc_dx * cost
    
                y_sint = y * sint
                y_cost = y * cost
    
                self.upper_profile.append((x - y_sint, yc + y_cost))
                self.lower_profile.append((x + y_sint, yc - y_cost))
                self.camber_line.append((x, yc))
    
    
        def get_camber_and_gradient(self, x):
    
                if x < self.pos_max_camber:
                    xc = x
                    mx = self.pos_max_camber
                    mx2 = mx*mx
                else:
                    xc = 1 - x
                    mx = 1 - self.pos_max_camber
                    mx2 = -mx*mx
    
                yc = self.camber*(2*mx*xc - xc*xc)/(mx*mx)
    
                dyc_dx = 2*self.camber*(mx-xc)/mx2
    
                return yc, dyc_dx
    
    
        def get_points_upper_profile(self):
    
            for x,y in self.upper_profile:
                    yield x*self.chord_length, y*self.chord_length 
    
        def get_points_lower_profile(self):
    
            for x,y in self.lower_profile:
                    yield x*self.chord_length, y*self.chord_length
    
    
        def get_points_te_to_te(self, upper_profile_first=False):
    
            # Points from trailing edge to leading edge.
    
            profile = self.upper_profile if upper_profile_first else self.lower_profile
    
            for x,y in profile[:0:-1]:  # Excluding the leading edge point
                yield x*self.chord_length, y*self.chord_length
    
            # Points from leading edge to trailing edge
    
            profile = self.lower_profile if upper_profile_first else self.upper_profile
    
            for x,y in profile:
                yield x*self.chord_length, y*self.chord_length
    
    
        def get_camberline_circles(self, limit_circles=False):
            # Returns x, y of camberline and circle radii to fill profile.
    
            import math
    
            for (x,y),r in zip(self.camber_line, [p[1] for p in self.profile]):
    
                    if limit_circles:
                        # r could make circle go outside of chord, so limit r here if it looks better
                        rl = math.sqrt(x*x+y*y)
                        rt = math.sqrt((1-x)*(1-x)+(1-y)*(1-y))
                        r = min(r,rl,rt)
    
                    yield x*self.chord_length, y*self.chord_length, r*self.chord_length
    
    
        def get_profile_name(self, basename="NACA"):
    
            # Return NACA standard name
            return "{0}{1:1d}{2:1d}{3:02d}".format(basename,
                                                    round(self.camber*100),
                                                    round(self.pos_max_camber*10),
                                                    round(self.thickness*100))
    
    
        def write_te_to_te(self, filename, upper_profile_first=False):
    
            with open(filename, "w") as f:
    
                for x,y in self.get_points_te_to_te(upper_profile_first=upper_profile_first):
                    f.write(f"{x} {y}\n")
    
    
        def write_upper_lower(self, filename):
            # Leading edge to trailing edge
    
            with open(filename, "w") as f:
    
                for x,y in self.get_points_upper_profile():
                    f.write(f"{x} {y}\n")
    
                f.write("\n")
    
                for x,y in self.get_points_lower_profile():
                    f.write(f"{x} {y}\n")
    
    
        def write_lower_upper(self, filename):
            # Leading edge to trailing edge
    
            with open(filename, "w") as f:
    
                for x,y in self.get_points_lower_profile():
                    f.write(f"{x} {y}\n")
    
                f.write("\n")
    
                for x,y in self.get_points_upper_profile():
                    f.write(f"{x} {y}\n")
    
    
        def write_circles(self, filename, limit_circles=True):
    
            with open(filename, "w") as f:
    
                # Points on the camber line with profile as radius
                for x,y,r in self.get_camberline_circles(limit_circles=limit_circles):
    
                    f.write(f"{x} {y} {r}\n")