Chris Parmer — home

Airfoil in potential flow

Joukowski conformal map, Kutta condition, 8° angle of attack

Example from a field guide to quiver · shared helpers

Airfoil in potential flow — Joukowski conformal map, Kutta condition, 8° angle of attack

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