Skip to content

Pressure distribution on a reversed axis

The pressure coefficient and the surface speed of the potential flow around a Joukowski aerofoil at an angle of attack of 4°, computed exactly by conformal mapping. The pressure coefficient is plotted on a reversed y axis, as aerodynamicists draw it, so that suction (negative \(C_p\)) is upwards; the limits are still given lower bound first, and a datatip reports \(C_p\) as stored rather than its negation. The two panels share the chordwise axis through an x link, which shares limits only, so the speed below keeps its usual direction. Only the bottom panel labels its x ticks, and the plot areas keep the 3 : 2 ratio of the row weights all the same.

Pressure distribution on a reversed axis

The figure above is live, drawn in the browser by the same engine that draws the viewer: drag it to pan, and click it to zoom with the wheel. Download the PDF for the vector figure that IronLAB exports, or read how to embed a figure of your own.

Source

The figure is built by the following code.

use ironlab::prelude::*;

pub const TITLE: &str = "Pressure distribution on a reversed axis";
pub const DESCRIPTION: &str = "The pressure coefficient and the surface speed of the potential flow around a \
     Joukowski aerofoil at an angle of attack of 4°, computed exactly by conformal mapping. The pressure \
     coefficient is plotted on a reversed y axis, as aerodynamicists draw it, so that suction (negative $C_p$) is \
     upwards; the limits are still given lower bound first, and a datatip reports $C_p$ as stored rather than its \
     negation. The two panels share the chordwise axis through an x link, which shares limits only, so the speed \
     below keeps its usual direction. Only the bottom panel labels its x ticks, and the plot areas keep the 3 : 2 \
     ratio of the row weights all the same.";

/// The centre of the circle that the Joukowski map turns into the aerofoil, in the plane of the circle: its real
/// part sets the thickness and its imaginary part the camber.
const CENTRE: (f64, f64) = (-0.09, 0.06);
/// The angle of attack, in degrees.
const ALPHA_DEG: f64 = 4.0;

/// One point of the surface: its chordwise position from 0 at the leading edge to 1 at the trailing edge, its
/// pressure coefficient and its speed relative to the free stream.
struct SurfacePoint {
    x: f64,
    cp: f64,
    speed: f64,
}

/// Computes the flow around the aerofoil at `n` points of the circle and returns the upper and the lower surface,
/// each from the leading edge to the trailing edge.
fn surfaces(n: usize) -> (Vec<SurfacePoint>, Vec<SurfacePoint>) {
    let (mx, my) = CENTRE;
    let radius = ((1.0 - mx).powi(2) + my * my).sqrt();
    let alpha = ALPHA_DEG.to_radians();
    // The angle of the trailing edge on the circle, where the Kutta condition fixes the circulation.
    let beta = (my / (1.0 - mx)).atan();
    let circulation = 4.0 * std::f64::consts::PI * radius * (alpha + beta).sin();

    let mut points: Vec<(f64, f64, f64)> = (1..n)
        .map(|k| {
            let theta = -beta + 2.0 * std::f64::consts::PI * k as f64 / n as f64;
            // A point of the circle, relative to its centre and absolutely.
            let (rx, ry) = (radius * theta.cos(), radius * theta.sin());
            let (zx, zy) = (mx + rx, my + ry);
            // The complex velocity in the plane of the circle: free stream, doublet and vortex.
            let r2 = rx * rx + ry * ry;
            let (inv_x, inv_y) = (rx / r2, -ry / r2); // 1 / (ζ − μ)
            let (inv2_x, inv2_y) = (inv_x * inv_x - inv_y * inv_y, 2.0 * inv_x * inv_y);
            let (ex, ey) = (alpha.cos(), alpha.sin());
            let doublet = (radius * radius * (ex * inv2_x - ey * inv2_y), radius * radius * (ex * inv2_y + ey * inv2_x));
            let vortex = (-circulation / (2.0 * std::f64::consts::PI) * inv_y, circulation / (2.0 * std::f64::consts::PI) * inv_x);
            let w = (ex - doublet.0 + vortex.0, -ey - doublet.1 + vortex.1);
            // The derivative of the map z = ζ + 1/ζ, by which the velocity is divided.
            let d = zx * zx + zy * zy;
            let (iz2_x, iz2_y) = ((zx * zx - zy * zy) / (d * d), -2.0 * zx * zy / (d * d));
            let derivative = (1.0 - iz2_x, -iz2_y);
            let speed = (w.0.hypot(w.1)) / derivative.0.hypot(derivative.1);
            // The position in the plane of the aerofoil.
            let x = zx + zx / d;
            (x, 1.0 - speed * speed, speed)
        })
        .collect();
    let (lo, hi) = points
        .iter()
        .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| (lo.min(p.0), hi.max(p.0)));
    for p in &mut points {
        p.0 = (p.0 - lo) / (hi - lo);
    }
    // The circle is traversed from the trailing edge over the upper surface to the leading edge, then back along
    // the lower surface.
    let leading = points
        .iter()
        .enumerate()
        .min_by(|a, b| a.1.0.total_cmp(&b.1.0))
        .map(|(k, _)| k)
        .expect("the surface has points");
    let point = |&(x, cp, speed): &(f64, f64, f64)| SurfacePoint { x, cp, speed };
    let upper = points[..=leading].iter().rev().map(point).collect();
    let lower = points[leading..].iter().map(point).collect();
    (upper, lower)
}

pub fn figure() -> Figure {
    let (upper, lower) = surfaces(240);
    let mut fig = Figure::new()
        .size_mm(140.0, 120.0)
        .tiles(2, 1)
        .row_weights([3.0, 2.0])
        .title(format!("Joukowski aerofoil at $\\alpha = {ALPHA_DEG}^\\circ$"))
        .tag("line")
        .tag("subplots")
        .tag("legend")
        .tag("linked-axes")
        .tag("ticks")
        .tag("reversed-axes")
        .parameter("dimensionality", "2D")
        .parameter("artists", 4)
        .parameter("data_points", 960)
        .parameter("has_legend", true);

    let column = |points: &[SurfacePoint], value: fn(&SurfacePoint) -> f64| -> Vec<f64> {
        points.iter().map(value).collect()
    };
    let surfaces = [("upper surface", &upper, Dash::Solid), ("lower surface", &lower, Dash::Dashed)];

    let mut cp = fig.axes(0, 0);
    for (name, points, dash) in surfaces {
        cp.plot(column(points, |p| p.x), column(points, |p| p.cp))
            .label(name)
            .color(Color::BLACK)
            .dash(dash);
    }
    cp.ylabel("$C_p$")
        .ylim(-2.0, 1.0)
        .yreverse(true)
        .ytick_step(0.5)
        .xlim(0.0, 1.0)
        .xtick_step(0.2)
        .xtick_labels_hidden()
        .grid(true)
        .legend(LegendLocation::SouthEast);
    let cp = cp.id();

    let mut speed = fig.axes(1, 0);
    for (name, points, dash) in surfaces {
        speed
            .plot(column(points, |p| p.x), column(points, |p| p.speed))
            .label(name)
            .color(Color::BLACK)
            .dash(dash);
    }
    speed
        .xlabel("Chordwise position $x/c$")
        .ylabel("$U_e/U_\\infty$")
        .ylim(0.0, 1.6)
        .xtick_step(0.2)
        .grid(true);
    let speed = speed.id();

    fig.link(Dim::X, &[cp, speed])
        .expect("both axes belong to this figure");
    fig
}

Back to the gallery