Airfoil in potential flow
Joukowski conformal map, Kutta condition, 8° angle of attack
Example from a field guide to quiver · shared helpers
Python Code
"""Airfoil in potential flow — Joukowski conformal map, Kutta condition, 8° angle of attack."""
from pathlib import Path
from _shared import save, themed_layout, SPEED_SCALE, TEXT
import numpy as np
CHART_NUM = 6
ALPHA = np.deg2rad(8) # angle of attack
C = -0.1 + 0.1j # circle center in the ζ-plane → camber + thickness
R = abs(1 - C) # circle passes through ζ = 1 (the trailing edge)
U_INF = 1.0
def w_zeta(zeta, gamma):
"""Complex velocity in the circle plane."""
d = zeta - C
return (
U_INF * (np.exp(-1j * ALPHA) - R**2 * np.exp(1j * ALPHA) / d**2)
- 1j * gamma / (2 * np.pi * d)
)
def generate():
# Kutta condition: the rear stagnation point sits on the trailing edge,
# which fixes the circulation Γ (and therefore the lift).
gamma = float(np.real(-2j * np.pi * (1 - C) * (
U_INF * (np.exp(-1j * ALPHA) - R**2 * np.exp(1j * ALPHA) / (1 - C)**2)
)))
# Sample on rings around the circle in ζ, then conformally map to the
# airfoil plane z = ζ + 1/ζ. The points land on a curved lattice hugging
# the airfoil — quiver doesn't care that it isn't a rectangular grid.
radii = R * np.geomspace(1.06, 3.4, 13)
thetas = np.linspace(0, 2 * np.pi, 62, endpoint=False)
RR, TT = np.meshgrid(radii, thetas)
zeta = C + (RR * np.exp(1j * TT)).ravel()
z = zeta + 1 / zeta
wz = w_zeta(zeta, gamma) / (1 - 1 / zeta**2) # velocity in the z-plane
u, v = np.real(wz), -np.imag(wz)
speed = np.hypot(u, v)
keep = speed < 2.6 * U_INF # clip the singular TE spike
z, u, v, speed = z[keep], u[keep], v[keep], speed[keep]
arrows = {
"type": "quiver",
"x": np.real(z).tolist(),
"y": np.imag(z).tolist(),
"u": u.tolist(),
"v": v.tolist(),
"arrowref": "paper",
"lengthmode": "scaled",
"lengthfactor": 1.35,
"anchor": "center",
"marker": {
"colorscale": SPEED_SCALE,
"showscale": True,
"colorbar": {"title": {"text": "|V|/V∞"}, "thickness": 12, "len": 0.7},
"line": {"width": 1.4},
},
"customdata": [f"{s:.2f} V∞" for s in speed],
"hovertemplate": "%{customdata}<extra></extra>",
"showlegend": False,
}
# The airfoil itself: the circle |ζ − C| = R mapped through the same transform
tb = np.linspace(0, 2 * np.pi, 200)
zb = (C + R * np.exp(1j * tb))
zb = zb + 1 / zb
body = {
"type": "scatter",
"x": np.real(zb).tolist(),
"y": np.imag(zb).tolist(),
"mode": "lines",
"fill": "toself",
"fillcolor": TEXT,
"line": {"color": TEXT, "width": 1},
"hoverinfo": "skip",
"showlegend": False,
}
layout = themed_layout(
xaxis={"showgrid": False, "zeroline": False, "visible": False,
"range": [-2.7, 3.0]},
yaxis={"showgrid": False, "zeroline": False, "visible": False,
"range": [-1.9, 2.1], "scaleanchor": "x"},
)
save(CHART_NUM, {"data": [arrows, body], "layout": layout})
Made with Plotly