Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 113 additions & 40 deletions pysces/body.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ def flat_plate(num_points):
y = np.zeros_like(x)
return Body(np.array([x, y]).T)

def joukowski_foil(xcenter=-.1, ycenter=.1, a=1, numpoints=32):
def joukowski_foil(xcenter=-.1, ycenter=.1, a=1, num_points=32):
"""Return a Joukowski foil Body.

The foil has its trailing edge at (2a,0). The foil has a total of
numpoints along the boundary. Refer to chapter 4 of [1]_ for details.
num_points along the boundary. Refer to chapter 4 of [1]_ for details.

Parameters
----------
Expand All @@ -74,15 +74,15 @@ def joukowski_foil(xcenter=-.1, ycenter=.1, a=1, numpoints=32):
a : float
radius of the Joukowski preimage circle

numpoints : int
num_points : int
number of points along the boundary

References
----------
.. [1] Acheson, D. J., "Elementary Fluid Dynamics", Oxford, 1990.
"""

t = np.linspace(0,2*np.pi,numpoints)
t = np.linspace(0,2*np.pi,num_points)
r = np.sqrt((a-xcenter)**2+ycenter**2)
chi = xcenter + r*np.cos(t)
eta = ycenter + r*np.sin(t)
Expand All @@ -91,7 +91,7 @@ def joukowski_foil(xcenter=-.1, ycenter=.1, a=1, numpoints=32):
y = eta*(1-a**2/mag2)
return Body(np.array([x,y]).T)

def karman_trefftz_foil(xcenter=-.1, ycenter=0, a=.1, angle_deg=10, numpoints=32):
def karman_trefftz_foil(xcenter=-.1, ycenter=0, a=.1, angle_deg=10, num_points=32):
"""Return a Karman-Trefftz foil Body.

The Karman-Trefftz foil is a modified version of the Joukowski
Expand All @@ -106,7 +106,7 @@ def karman_trefftz_foil(xcenter=-.1, ycenter=0, a=.1, angle_deg=10, numpoints=32
angle_deg : float
The interior angle, in degrees, at the trailing edge.

numpoints : int
num_points : int
Number of points along the boundary

See Also
Expand All @@ -120,7 +120,7 @@ def karman_trefftz_foil(xcenter=-.1, ycenter=0, a=.1, angle_deg=10, numpoints=32

angle_rad = angle_deg*np.pi/180
n = 2-angle_rad/np.pi
t = np.linspace(0,2*np.pi,numpoints)
t = np.linspace(0,2*np.pi,num_points)
ctr = xcenter + 1j*ycenter
r = np.linalg.norm(ctr-a)
zeta = ctr+r*np.exp(1j*t)
Expand All @@ -131,7 +131,7 @@ def karman_trefftz_foil(xcenter=-.1, ycenter=0, a=.1, angle_deg=10, numpoints=32
return Body(np.array([x,y]).T)

def van_de_vooren_foil(semichord=1.0, thickness=0.15, angle_deg=5,
numpoints=32):
num_points=32):
"""Return a van de Vooren foil Body.

Refer to section 6.6 of [1]_
Expand All @@ -147,7 +147,7 @@ def van_de_vooren_foil(semichord=1.0, thickness=0.15, angle_deg=5,
angle_deg : float
interior angle, in degrees, at the trailing edge

numpoints : int
num_points : int
number of points along the boundary

References
Expand All @@ -158,50 +158,123 @@ def van_de_vooren_foil(semichord=1.0, thickness=0.15, angle_deg=5,

k = 2-(angle_deg*np.pi/180)
a = 2*semichord*((1+thickness)**(k-1))*2**(-k)
t = np.linspace(0,2*np.pi,numpoints)
t = np.linspace(0,2*np.pi,num_points)
num = (a*(np.cos(t)-1)+1j*a*np.sin(t))**k
den = (a*(np.cos(t)-thickness)+1j*a*np.sin(t))**(k-1)
z = (num/den)+semichord
x = [w.real for w in z]
y = [w.imag for w in z]
return Body(np.array([x,y]).T)

def naca_airfoil(code, num_points, zero_thick_te=False, uniform=False):
"""Return a NACA 4-digit series airfoil"""
# extract parameters from 4-digit code
def naca_airfoil(code, num_points=20, te_clamp=False, uniform=False, chord=1):
"""Returns a NACA 4-digit series foil.

Refer to [1]_ for a detailed description and formulas.

Parameters:
-----------
code_str : string
The 4-digit NACA code describing the foil. If the first digit is M,
the second digit is P, and the last two digits are TH, then:
M = maximum camber, as a percentage of the chord
P = distance of max camber from leading edge in tens of percent of chord
TH = thickenss of the airfoil as a percentage of chord

For example, 2315 means a cambered airfoil whose max camber is 2% of the
chord, located 30% chordwise from the leading edge, and whose thickness
is 15% of the chord.

num_points : int
The number of points along each surface of the foil. There will be
2*num_points-1 points returned, with num_points along the edge and
num_points-1 along the bottom edge.

te_clamp : boolean
Trailing edge clamp. When false, the trailing edge has a slight
nonzero thickness. When true, the foil adjusted so that the trailing
edge has exactly zero thickness.

uniform : boolean
Distribution of points along chord. When false, points are uniformly
distributd along the chord. When true, points are distributed more
densly near leading edge (where the curvature is large).

chord : double
Length of chord

Returns:
--------
A two-column matrix q, whose first column encodes the x coordinates and
whose second column encodes the y coordinates of the points on the foil.
The rows are ordered so that the points traverse the foil in the
counterclockwise fashion from the trailing edge to the leading edge and
back again.

References:
-----------
.. [1] https://en.wikipedia.org/wiki/NACA_airfoil

Example:
--------
q = naca_foil('2315',50)
"""
code_str = "%04d" % int(code)
if len(code_str) != 4:
raise ValueError("NACA designation is more than 4 digits")
max_camber = 0.01 * int(code_str[0])
p = 0.1 * int(code_str[1]) # location of max camber
thickness = 0.01 * int(code_str[2:])
if uniform:
x = np.linspace(0, 1, num_points)
m = .01*int(code_str[0])
p = .1*int(code_str[1])
thick = .01*int(code_str[2:4])
if (uniform is True):
x = np.linspace(0,chord,num_points)
else:
# closer spacing near leading edge
theta = np.linspace(0, 0.5 * np.pi, num_points)
x = 1 - np.cos(theta)

# thickness
coefs = [-0.1015, 0.2843, -0.3516, -0.1260, 0, 0.2969]
if zero_thick_te:
coefs[0] = -0.1036
y_thick = 5 * thickness * (np.polyval(coefs[:5], x) +
coefs[5] * np.sqrt(x))

# camber
front = np.where(x <= p)
back = np.where(x > p)
y_camber = np.zeros_like(x)
t = np.linspace(0,0.5*np.pi,num_points)
x = chord*(1-np.cos(t))

# Construct the thickness line, yt
xc = x/chord
coefs = [-.1015, .2843, -.3516, -.1260, 0]
sqrt_coef = .2969
if (te_clamp is True):
coefs[0] = -.1036
yt = 5*thick*chord*(sqrt_coef*np.sqrt(xc)+np.polyval(coefs,xc))

# Avoid unnecessary work (and avoid divide-by-zero) when no camber
if p:
y_camber[front] = max_camber * x[front] / p**2 * (2 * p - x[front])
y_camber[back] = max_camber * ((1. - x[back])/(1. - p)**2 *
(1 + x[back] - 2 * p))
x = np.hstack([x[-1:0:-1], x])
y = np.hstack([y_camber[-1:0:-1] + y_thick[-1:0:-1],
y_camber - y_thick])
return Body(np.array([x, y]).T)
# Construct the camber line, yc
front = np.where(x <= p*chord)
back = np.where(x > p*chord)
yc = np.zeros_like(x)
yc[front] = (m/p**2)*x[front]*(2*p-xc[front])
yc[back] = (m/(1-p)**2)*(chord-x[back])*(1+xc[back]-2*p)

# Combine thickness and camber line to produce the NACA airfoil.
# The usual formulas for the camber adjustment involve sin(arctan(.))
# and cos(arctan(.)), but to avoid branch cuts in the arctangent
# function, I use instead the equivalent Cartesian formulations:
# sin(arctan(x)) = x/sqrt(1+x^2)
# cos(arctan(x)) = 1/sqrt(1+x^2)
dycdx = np.zeros_like(x)
dycdx[front] = (2*m/p**2)*(p-xc[front])
dycdx[back] = (2*m/(1-p)**2)*(p-xc[back])
z = np.sqrt(1+dycdx**2)
xU = x - yt*dycdx/z # X coordinates along upper surface
xL = x + yt*dycdx/z # X coordinates along lower surface
yU = yc + yt/z # Y coordinates along upper surface
yL = yc - yt/z # Y coordinates along lower surface
x = np.hstack([xU[-1:0:-1],xL])
y = np.hstack([yU[-1:0:-1],yL])
else:
x = np.hstack([x[-1:0:-1],x])
y = np.hstack([yt[-1:0:-1],-yt])
# Note: The [-1:0:-1] indexing above accomplishes two things: (1) it
# reorders the vectors so the foil is described counterclockwise from the
# trailing edge to the leading edge and back again, and (2) it omits the
# repeated entry at the leading edge that would otherwise be present from
# concatenating two copies of x. It is important not to have two identical
# successive entries, or else the matrix of influence coefficients in the
# panel (boundary element) method becomes ill conditioned.

return Body(np.array([x,y]).T)

class TransformedBody(object):
"""Base class for rigid (Euclidean) transformations of existing bodies
Expand Down
8 changes: 4 additions & 4 deletions pysces/tests/test_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,22 @@ def test_airfoil_uniform(self):
npoints = 20
body = naca_airfoil("0012", npoints, uniform=True)
self.assertEqual(len(body.get_points()), 2*npoints - 1)
body2 = naca_airfoil("2412", npoints, zero_thick_te=True)
body2 = naca_airfoil("2412", npoints, te_clamp=True)
self.assertEqual(len(body2.get_points()), 2*npoints - 1)

def test_joukowski_foil(self):
npts = 32
body = joukowski_foil(numpoints=npts)
body = joukowski_foil(num_points=npts)
self.assertEqual(len(body.get_points()),npts)

def test_karman_trefftz_foil(self):
npts = 32
body = karman_trefftz_foil(numpoints=npts)
body = karman_trefftz_foil(num_points=npts)
self.assertEqual(len(body.get_points()),npts)

def test_van_de_vooren_foil(self):
npts = 32
body = van_de_vooren_foil(numpoints=npts)
body = van_de_vooren_foil(num_points=npts)
self.assertEqual(len(body.get_points()),npts)

class TestBody(unittest.TestCase):
Expand Down
7 changes: 4 additions & 3 deletions scripts/animate_foil.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import matplotlib.animation as animation
import sys

#airfoil = naca_airfoil("0006", 20) # NACA 0012 airfoil with 20 points per side
#airfoil = naca_airfoil("2214", 20)
airfoil = joukowski_foil(-.1,.1,.5,100)
#airfoil = naca_airfoil("0006", 20)
airfoil = naca_airfoil("2214", 20)
airfoil = TransformedBody(airfoil, 0, [-.25,0])
#airfoil = joukowski_foil(-.1,.1,.5,100)
#airfoil = van_de_vooren_foil(0.5, 0.1, 3)
#airfoil = karman_trefftz_foil(-.1,.1,1,10,32)
#airfoil = flat_plate(20)
Expand Down