codex/wing-aspect-ratio-induced-drag.py

wing-aspect-ratio-induced-drag.py
"""Equal-area wings and a fixed-lift induced-drag model; write PNG to cwd."""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np


def induced_drag(span, lift=4800.0, pressure=500.0, efficiency=0.8):
    return lift**2 / (np.pi * efficiency * pressure * np.asarray(span)**2)


def main():
    area = 16.0
    spans = np.array([8.0, 12.0])
    chords = area / spans
    aspect_ratios = spans**2 / area
    drag = induced_drag(spans)
    np.testing.assert_allclose(spans * chords, [area, area])
    np.testing.assert_allclose(aspect_ratios, [4.0, 9.0])
    np.testing.assert_allclose(drag, [286.478897565, 127.323954474], rtol=1e-10)
    np.testing.assert_allclose(drag[1] / drag[0], 4.0 / 9.0)
    # Compare both equivalent versions, including the varying coefficient.
    for changed_area in [12.0, 16.0, 24.0]:
        lift_coefficient = 4800.0 / (500.0 * changed_area)
        ar = 8.0**2 / changed_area
        from_coefficients = 500.0 * changed_area * lift_coefficient**2 / (np.pi * 0.8 * ar)
        np.testing.assert_allclose(from_coefficients, drag[0])

    plt.rcParams.update({"font.size": 10, "figure.facecolor": "white", "axes.facecolor": "white"})
    fig = plt.figure(figsize=(10, 4), dpi=100)
    grid = fig.add_gridspec(2, 2, width_ratios=[1.05, 1.0], hspace=0.72, wspace=0.33)
    colors = ["#276FBF", "#D15D21"]
    for i, (span, chord, ar, color) in enumerate(zip(spans, chords, aspect_ratios, colors)):
        ax = fig.add_subplot(grid[i, 0])
        ax.add_patch(Rectangle((-span / 2, 0), span, chord, facecolor=color, alpha=0.28,
                               edgecolor=color, linewidth=2))
        ax.axvline(0, color="#777777", linewidth=0.7, linestyle=":")
        ax.set_xlim(-6.7, 6.7)
        ax.set_ylim(-0.35, 2.65)
        ax.set_aspect("equal", adjustable="box")
        ax.set_xticks([-6, -4, -2, 0, 2, 4, 6])
        ax.set_yticks([0, 1, 2])
        ax.set_ylabel("Chord (m)")
        ax.set_title(f"Span {span:g} m; area 16 m²; AR = {ar:g}", fontsize=10, pad=5)
        if i == 1:
            ax.set_xlabel("Spanwise coordinate (m)")
        ax.spines[["top", "right"]].set_visible(False)

    ax = fig.add_subplot(grid[:, 1])
    span_grid = np.linspace(6.5, 13, 200)
    ax.plot(span_grid, induced_drag(span_grid), color="#343434", linewidth=2)
    for span, value, color in zip(spans, drag, colors):
        ax.scatter(span, value, color=color, s=45, zorder=3)
        ax.annotate(f"{value:.2f} N", (span, value), xytext=((8, 12) if span == 8 else (-8, 12)),
                    ha=("left" if span == 8 else "right"),
                    textcoords="offset points", color=color, fontsize=11)
    ax.set_xlim(6.5, 13.2)
    ax.set_ylim(0, 470)
    ax.set_xlabel("Wingspan b (m)")
    ax.set_ylabel("Lift-induced drag (N)")
    ax.set_title("Fixed L = 4.8 kN, q = 500 Pa, e = 0.8", fontsize=10)
    ax.text(0.98, 0.96, "Dᵢ = L² / (π e q b²)", transform=ax.transAxes,
            ha="right", va="top", fontsize=11)
    ax.grid(alpha=0.18)
    ax.spines[["top", "right"]].set_visible(False)
    fig.suptitle("Longer span at equal wing area: a model comparison", fontsize=13, y=0.98)
    fig.subplots_adjust(left=0.075, right=0.98, top=0.86, bottom=0.15)
    output = Path(__file__).with_suffix(".png").name
    fig.savefig(output, dpi=100, facecolor="white")
    plt.close(fig)
    print(f"{output}: 1000×400; AR={aspect_ratios.tolist()}; drag={drag.tolist()}")


if __name__ == "__main__":
    main()