Chris Parmer — home

Drag Polar

Aerospace — NACA 0012 airfoil Cl vs. Cd

Example from the compendium of canonical charts

Aerospace — Drag Polar (NACA 0012 airfoil Cl vs. Cd)

Python Code

"""Aerospace — Drag Polar (NACA 0012 airfoil Cl vs. Cd)."""


import numpy as np
import requests
import plotly.graph_objects as go


def _fetch_xfoil_polar():
    """Fetch NACA 0012 polar from airfoiltools.com (plain HTTP)."""
    url = "http://airfoiltools.com/polar/csv?polar=xf-n0012-il-1000000"
    r = requests.get(url, timeout=30, verify=False)
    r.raise_for_status()
    lines = r.text.splitlines()
    # Find the header row
    header_idx = None
    for i, line in enumerate(lines):
        stripped = line.strip()
        if stripped.startswith("Alpha") or (stripped and stripped.split(",")[0].strip() == "Alpha"):
            header_idx = i
            break
    if header_idx is None:
        raise ValueError("header row not found")
    csv_text = "\n".join(lines[header_idx:])
    import io, pandas as pd
    df = pd.read_csv(io.StringIO(csv_text))
    df.columns = [c.strip() for c in df.columns]
    return df


def _synthetic_polar():
    """Thin-airfoil + parasite drag approximation for NACA 0012."""
    import pandas as pd
    alpha_deg = np.linspace(-12, 15, 80)
    alpha_rad = np.radians(alpha_deg)
    # Lift: 2π slope, stall modeled with tanh
    Cl_ideal = 2 * np.pi * np.sin(alpha_rad)
    # Soft stall above ~10°
    stall_factor = 1 - 0.5 * np.clip((np.abs(alpha_deg) - 10) / 5, 0, 1)
    Cl = Cl_ideal * stall_factor
    # Drag: parabolic polar Cd = Cd0 + Cl^2/(pi*e*AR), AR→∞ so use finite span
    Cd0 = 0.006
    e = 0.9
    AR = 10
    Cd = Cd0 + Cl ** 2 / (np.pi * e * AR)
    # Add separation drag at high alpha
    sep = np.clip((np.abs(alpha_deg) - 10) / 5, 0, 1)
    Cd += 0.05 * sep
    return pd.DataFrame({"Alpha": alpha_deg, "Cl": Cl, "Cd": Cd})


def generate():
    try:
        df = _fetch_xfoil_polar()
        print(f"  fetched airfoiltools polar: {len(df)} rows, cols={list(df.columns)}")
        used_synthetic = False
    except Exception as e:
        print(f"  airfoiltools fetch failed ({e}), using synthetic data")
        df = _synthetic_polar()
        used_synthetic = True

    Cd = df["Cd"].values
    Cl = df["Cl"].values
    Alpha = df["Alpha"].values

    # Max L/D point
    with np.errstate(divide="ignore", invalid="ignore"):
        ld = np.where(Cd > 0, Cl / Cd, 0)
    best_idx = np.argmax(ld)
    best_Cd = Cd[best_idx]
    best_Cl = Cl[best_idx]
    best_alpha = Alpha[best_idx]
    best_ld = ld[best_idx]

    fig = go.Figure()

    # Main polar curve
    fig.add_trace(go.Scatter(
        x=Cd,
        y=Cl,
        mode="lines+markers",
        line=dict(color=VIOLET, width=2.5),
        marker=dict(size=5),
        name="NACA 0012 Polar",
        hovertemplate="α=%{customdata:.1f}°<br>Cd=%{x:.5f}<br>Cl=%{y:.4f}<extra></extra>",
        customdata=Alpha,
    ))

    # Max L/D tangent line from origin
    t = np.array([0, best_Cd * 1.6])
    fig.add_trace(go.Scatter(
        x=t,
        y=t * best_ld,
        mode="lines",
        line=dict(color=TEAL, width=1.5, dash="dash"),
        name=f"Max L/D tangent (L/D={best_ld:.1f})",
        hoverinfo="skip",
    ))

    # Max L/D marker
    fig.add_trace(go.Scatter(
        x=[best_Cd],
        y=[best_Cl],
        mode="markers",
        marker=dict(size=13, color=GREEN, symbol="star"),
        name=f"Max L/D @ α={best_alpha:.1f}°",
        hovertemplate=f"Max L/D = {best_ld:.1f}<br>α={best_alpha:.1f}°<br>Cd={best_Cd:.5f}<br>Cl={best_Cl:.4f}<extra></extra>",
    ))

    fig.add_annotation(
        x=best_Cd,
        y=best_Cl + 0.06,
        text=f"Max L/D = {best_ld:.1f}<br>α = {best_alpha:.1f}°",
        showarrow=True,
        arrowhead=2,
        arrowcolor=GREEN,
        font=dict(size=12, color=TEXT),
        ax=40,
        ay=-30,
    )

    source_note = "Synthetic (thin airfoil + parabolic drag)" if used_synthetic else "Source: airfoiltools.com Re=1×10⁶"
    fig.update_layout(
        xaxis=dict(title="Drag Coefficient (Cd)"),
        yaxis=dict(title="Lift Coefficient (Cl)"),
        legend=dict(x=0.98, y=0.02, xanchor="right", yanchor="bottom"),
        margin=dict(t=50, b=60, l=70, r=60),
        annotations=[
            dict(
                x=0.99, y=0.01,
                xref="paper", yref="paper",
                text=source_note,
                showarrow=False,
                font=dict(size=10, color="#888"),
                xanchor="right",
            ),
        ] + [ann for ann in [] if False],  # placeholder; real annotation added above
    )

    # Re-add the annotation properly (update_layout annotations replaces, use add_annotation)
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly