Monocurl Essays
Series Download

The gradient: which way is uphill?

On a hillside the slope depends on the direction you face. Partial derivatives give two of those slopes, and the gradient packs all of them into one arrow.

22 min·Early college·

AssumesSurfaces and contour maps: functions of two variablesWhat is a derivative? Speed at a single instantThe dot product: projection, angle and why the formula works

Standing at the point $P = (0, -1)$ on the hill and turning on the spot. The arrow on the map is the direction you face, and the plot records the slope of the ground in that direction. The slope is largest, about $3.61$, facing a little north of east.
Monocurl source
let fr = MapFrame([-1.95, -0.15, 0], 3.1)
let pf = Frame(0, 360, -4.3, 4.3, [2.15, -0.25, 0], 2.7, 2.7)
let slope = |deg| 3 * cos(deg * PI / 180) + 2 * sin(deg * PI / 180)
mesh landm = MapLand(fr)
mesh lines = map([3, 4, 5, 6, 7], |c| Contour2(fr, c, CONTOUR, 2))
mesh border = [MapBorder(fr), MapTicks(fr)]
mesh tags = [LevelTag(fr, 7, 1, 1), LevelTag(fr, 6, 1.5, ylev(6, 1.5)), LevelTag(fr, 5, 0, 1), LevelTag(fr, 3, -1, 1)]
mesh plot = block {
    for (s in [-3, 0, 3]) {
        . Seg(at(pf, 0, s), at(pf, 360, s), MIST, 1.5)
        . center{[fx(pf, 0) - 0.3, fy(pf, s), 0]} Math(to_string(s), 0.7, SLATE)
    }
    . Seg(at(pf, 0, -4.3), at(pf, 0, 4.3), SLATE, 2.5)
    . Seg(at(pf, 0, 0), at(pf, 360, 0), SLATE, 2.5)
    for (k in range(0, 5)) {
        . Seg(at(pf, 90 * k, 0), [fx(pf, 90 * k), fy(pf, 0) - 0.09, 0], SLATE, 2)
        . center{[fx(pf, 90 * k), fy(pf, -4.3) - 0.25, 0]} Cap(["E", "N", "W", "S", "E"][k], 0.7, SLATE)
    }
    . Graph(pf, slope, 0, 360, AZURE, 3.5)
    . center{[-1.95, 1.72, 0]} Note("the direction you face", 0.72)
    . center{[1.75, 1.72, 0]} Note("slope that way:", 0.72)
}
let Face = |deg| {
    let u = [cos(deg * PI / 180), sin(deg * PI / 180)]
    var s = slope(deg)
    if (abs(s) < 0.005) { s = 0 }
    let d = mod(deg, 360)
    return [
        Tip(at(fr, 0, -1), at(fr, 0.45 * u[0], -1 + 0.45 * u[1]), EMBER, 4.5, 0.2),
        Pt(at(fr, 0, -1), INK, 0.07),
        Seg(at(pf, d, 0), at(pf, d, s), EMBER, 2.5),
        Pt(at(pf, d, s), EMBER, 0.085),
        center{[3.05, 1.72, 0]} scale{1.15} color{EMBER} Number(s, 2)
    ]
}
mesh face = Face(deg: 90)

slide "turn"
    play Wait(0.8)
    face.deg = 360 + 33.69
    play Lerp(8, [&face])
    play Wait(1.8)

Uses the shared definitions for this lesson's figures.

Ask how steep a road is and one number answers you: it climbs 8 meters for every 100. Stand on an open hillside and ask the same question, and the honest reply is “facing which way?” Facing one direction the ground rises in front of you, facing another it drops, and somewhere in between you can walk without gaining or losing any height. A point on a hillside has a slope for every direction.

That sounds like infinitely many numbers per point. The surprise of this lesson is that for a smooth hill two numbers are enough, and that those two numbers form an arrow on the map that points straight uphill.

Our hill is the one from the lesson on contour maps. At the map position $x$ kilometers east and $y$ kilometers north of a marker post, its height in hundreds of meters is

$$ f(x, y) = 6 + 3x - x^3 - y^2. $$

It has a summit at $(1, 0)$, where the height is 8, and a pass at $(-1, 0)$. We stand at $P = (0, -1)$, on the south slope, where $f(0, -1) = 5$. Slopes will be in hundreds of meters per kilometer, so a slope of 3 means 300 m of climb per kilometer walked.

Two slopes that are easy to define

The derivative of a function of one variable is the limit of $\frac{g(a + h) - g(a)}{h}$ as $h \to 0$, the slope of its graph. We do not have a function of one variable, but we can make one. Walk due east from $P$. Then $y$ stays at $-1$ and only $x$ changes, so the height you see is

$$ f(x, -1) = 6 + 3x - x^3 - 1 = 5 + 3x - x^3, $$

a function of $x$ alone. Geometrically, we have cut the hill with the vertical plane $y = -1$ and kept the curve where they meet.

  1. Stand at P, the point of the hill above (0, -1). The height there is 5.
  2. Cut the hill with the vertical plane y = -1, which runs east-west through P.
  3. At P the cut curve climbs toward the east with slope 3.
  4. Now cut with the plane x = 0, which runs north-south through P.
  5. At P this curve climbs toward the north with slope 2.
Two vertical slices through the point $P$ of the hill above $(0, -1)$. The east-west slice is the curve $z = f(x, -1)$, with slope 3 at $P$. The north-south slice is the curve $z = f(0, y)$, with slope 2 at $P$.
Monocurl source
let v = View([-0.6, -0.45, 0], 1.05, 0.6)
let TOP = 8.2
let ew_pts = map(sample(X0, X1, 60), |x| pr(v, [x, -1, hill(x, -1)]))
let ns_pts = map(sample(Y0, Y1, 50), |y| pr(v, [0, y, hill(0, y)]))
let Sheet = |pts, a, b, col| {
    var poly = pts
    poly .= pr(v, [b[0], b[1], TOP])
    poly .= pr(v, [a[0], a[1], TOP])
    return fill{alpha{0.22} col} stroke{col, 1.5} Polygon(poly)
}
mesh base = [Walls(v), Terrain(v)]
mesh ew = []
mesh ns = []
mesh tang = []
mesh spot = []
mesh notes = []

slide "Stand at P, the point of the hill above (0, -1). The height there is 5."
    spot = [
        Pt(on_hill(v, 0, -1), INK, 0.08),
        center{on_hill(v, 0, -1) + [-0.42, 0.3, 0]} Math("P", 0.9, INK)
    ]
    play Fade(0.6)

slide "Cut the hill with the vertical plane y = -1, which runs east-west through P."
    ew = [Sheet(ew_pts, [X0, -1], [X1, -1], AZURE), Path(ew_pts, AZURE, 3.5)]
    play Fade(0.8, [&ew])

slide "At P the cut curve climbs toward the east with slope 3."
    tang = [Seg(pr(v, [-0.42, -1, 5 - 1.26]), pr(v, [0.42, -1, 5 + 1.26]), INK, 4.5)]
    notes = [center{[2.85, 1.75, 0]} Math("f_x(0, -1) = 3", 0.9, AZURE)]
    play Fade(0.6, [&tang, &notes])

slide "Now cut with the plane x = 0, which runs north-south through P."
    ew = [Path(ew_pts, AZURE, 3.5)]
    ns = [Sheet(ns_pts, [0, Y0], [0, Y1], EMBER), Path(ns_pts, EMBER, 3.5)]
    play Fade(0.8, [&ew, &ns])

slide "At P this curve climbs toward the north with slope 2."
    tang .= Seg(pr(v, [0, -1 - 0.42, 5 - 0.84]), pr(v, [0, -1 + 0.42, 5 + 0.84]), INK, 4.5)
    notes .= center{[2.85, 1.15, 0]} Math("f_y(0, -1) = 2", 0.9, EMBER)
    play Fade(0.6, [&tang, &notes])

Uses the shared definitions for this lesson's figures.

The slope of the east-west curve at $P$ is an ordinary derivative, and we can compute it from the definition. The difference quotient at $x = 0$ is

$$ \frac{f(0 + h, -1) - f(0, -1)}{h} = \frac{(5 + 3h - h^3) - 5}{h} = 3 - h^2 \qquad (h \neq 0), $$

which has limit 3 as $h \to 0$. Facing east at $P$, the ground rises at slope 3.

Walking due north instead, $x$ stays at 0 and the height is $f(0, y) = 6 - y^2$. Its difference quotient at $y = -1$ is

$$ \frac{f(0, -1 + k) - f(0, -1)}{k} = \frac{(6 - (-1 + k)^2) - 5}{k} = \frac{2k - k^2}{k} = 2 - k \qquad (k \neq 0), $$

with limit 2. Facing north the ground also rises, at slope 2.

The “open disk” guarantees that $f$ is defined at all the nearby points the quotients use. The first limit is the derivative of the one-variable function $x \mapsto f(x, b)$ at $x = a$, so nothing new is needed to compute it: treat $y$ as a constant and differentiate in $x$. For the hill,

$$ f_x(x, y) = 3 - 3x^2, \qquad f_y(x, y) = -2y, $$

which gives $f_x(0, -1) = 3$ and $f_y(0, -1) = 2$ again.

Pause and try

Compute both partial derivatives of the hill at the summit $(1, 0)$, at the pass $(-1, 0)$, and at $(2, 1)$ on the east edge. What do the first two answers have in common, and why is that reasonable?

Show answerHide answer

At $(1, 0)$: $f_x = 3 - 3 = 0$ and $f_y = 0$. At $(-1, 0)$: $f_x = 3 - 3 = 0$ and $f_y = 0$. At $(2, 1)$: $f_x = 3 - 12 = -9$ and $f_y = -2$. At the summit and at the pass both slices are level. At the summit each slice has a maximum there, and at the pass the east-west slice has a minimum and the north-south slice a maximum; a one-variable function has derivative zero at an interior maximum or minimum.

A slope for every direction

East and north are two directions out of a full circle. To face any other way, describe the direction by a unit vector $\mathbf{u} = (u_1, u_2)$, a vector of length 1, so that $u_1^2 + u_2^2 = 1$. Walking a distance $t$ from $(a, b)$ in the direction $\mathbf{u}$ puts you at $(a + tu_1, b + tu_2)$. Because $\mathbf{u}$ has length 1, the number $t$ is the distance walked, and rise over distance is a slope.

With $\mathbf{u} = (1, 0)$ this is $f_x$, and with $\mathbf{u} = (0, 1)$ it is $f_y$. So at $P$ we know the slope facing east is 3 and facing north is 2.

Pause and try

Northeast is exactly halfway between east and north. Before computing anything, guess the slope at $P$ facing northeast. Then compute it: the unit vector is $\mathbf{u} = \left(\tfrac{1}{\sqrt 2}, \tfrac{1}{\sqrt 2}\right)$, so you need $f\left(\tfrac{t}{\sqrt 2}, -1 + \tfrac{t}{\sqrt 2}\right)$.

Show answerHide answer

Write $s = t/\sqrt 2$. Then $f(s, -1 + s) = 6 + 3s - s^3 - (s - 1)^2 = 5 + 5s - s^2 - s^3$. So the quotient is $\dfrac{5s - s^2 - s^3}{t} = \dfrac{1}{\sqrt 2}\left(5 - s - s^2\right)$, with limit $\dfrac{5}{\sqrt 2} \approx 3.54$ as $t \to 0$. The natural guess, the average of 3 and 2, is $2.5$, and it is wrong: the slope facing northeast is larger than both. The correct value is $3 \cdot \tfrac{1}{\sqrt 2} + 2 \cdot \tfrac{1}{\sqrt 2}$: each partial derivative weighted by the matching component of $\mathbf{u}$.

That pattern suggests a rule: the slope in the direction $\mathbf{u}$ is $f_x u_1 + f_y u_2$. If it holds, the two partial derivatives determine every other slope. It is tempting to accept it and move on, but as stated it is false.

Why two partial derivatives are not enough

Consider the function

$$ g(x, y) = \frac{xy}{x^2 + y^2} \quad\text{for } (x, y) \neq (0, 0), \qquad g(0, 0) = 0. $$

On the $x$-axis, $g(x, 0) = 0$ for every $x$, so $g_x(0, 0) = 0$. On the $y$-axis, likewise, $g_y(0, 0) = 0$. The proposed rule then predicts slope $0 \cdot u_1 + 0 \cdot u_2 = 0$ in every direction. Now walk along the diagonal: $g(s, s) = \dfrac{s^2}{2s^2} = \dfrac{1}{2}$ for every $s \neq 0$, while $g(0, 0) = 0$.

The function g(x, y) = \frac{xy}{x^2 + y^2}, with g(0,0) = 0, colored by value: orange where positive, blue where negative. It is constant along each line through the origin. Along the axes it is 0, so both partial derivatives at the origin are 0, but along the line y = x it jumps from \frac12 to 0 at the origin.
The function $g(x, y) = \frac{xy}{x^2 + y^2}$, with $g(0,0) = 0$, colored by value: orange where positive, blue where negative. It is constant along each line through the origin. Along the axes it is 0, so both partial derivatives at the origin are 0, but along the line $y = x$ it jumps from $\frac12$ to 0 at the origin.
Monocurl source
let g = |x, y| x * y / (x * x + y * y)
let sq = Frame(-1, 1, -1, 1, [-1.75, -0.05, 0], 2.9, 2.9)
mesh heat = stroke{alpha{0} PAPER, 0} point_map{|p| at(sq, p[0], p[1])} ColorGrid(|p, idx| {
    if (p[0] * p[0] + p[1] * p[1] < 0.000001) { return PAPER }
    let val = 2 * g(p[0], p[1])
    if (val > 0) { return lerp(PAPER, tint(EMBER, 0.8), val) }
    return lerp(PAPER, tint(AZURE, 0.8), -val)
}, [-1, 1, 81], [-1, 1, 81], 1)
mesh rays = [
    Seg(at(sq, -1, 0), at(sq, 1, 0), INK, 2),
    Seg(at(sq, 0, -1), at(sq, 0, 1), INK, 2),
    Seg(at(sq, -1, -1), at(sq, 1, 1), INK, 2),
    Outline([at(sq, -1, -1), at(sq, 1, -1), at(sq, 1, 1), at(sq, -1, 1)], SLATE, 2.5),
    Pt(at(sq, 0, 0), INK, 0.07)
]
mesh raylabels = [
    center{[fx(sq, 1) + 0.25, fy(sq, 0), 0]} Math("0", 0.8, INK),
    center{[fx(sq, 0), fy(sq, 1) + 0.28, 0]} Math("0", 0.8, INK),
    center{[fx(sq, 1) + 0.3, fy(sq, 1) + 0.15, 0]} Math("\frac{1}{2}", 0.75, EMBER),
    center{[fx(sq, -1) - 0.35, fy(sq, 1) + 0.15, 0]} Math("-\frac{1}{2}", 0.75, AZURE)
]
let Profile = |cy, title, level, col| {
    let pf = Frame(-1, 1, -0.25, 0.75, [2.0, cy, 0], 2.6, 1.15)
    return [
        Seg(at(pf, -1, 0), at(pf, 1, 0), SLATE, 2),
        Seg(at(pf, 0, -0.2), at(pf, 0, 0.75), SLATE, 2),
        center{[fx(pf, -1) - 0.25, fy(pf, 0), 0]} Math("0", 0.7, SLATE),
        center{[fx(pf, -1) - 0.25, fy(pf, 0.5), 0]} Math("\frac{1}{2}", 0.7, SLATE),
        Seg(at(pf, -1, 0.5), [fx(pf, -1) + 0.1, fy(pf, 0.5), 0], SLATE, 2),
        Seg(at(pf, -1, level), at(pf, 1, level), col, 4),
        center{[2.0, cy + 0.95, 0]} Note(title, 0.72)
    ]
}
mesh profiles = [
    Profile(0.75, "height along the x-axis", 0, AZURE),
    Pt([2.0, 0.75 - 0.2875, 0], AZURE, 0.085),
    Profile(-1.2, "height along the line y = x", 0.5, EMBER),
    Ring([2.0, -1.2 + 0.2875, 0], EMBER, 0.09, 3),
    Pt([2.0, -1.2 - 0.2875, 0], EMBER, 0.085)
]

Uses the shared definitions for this lesson's figures.

Facing northeast at the origin, the quotient is $\dfrac{g(s, s) - g(0, 0)}{t} = \dfrac{1/2}{t}$, which grows without bound as $t \to 0$. There is no slope in that direction at all: the surface has a cliff at the origin along every line except the two axes. The partial derivatives exist and are both 0, and they tell us nothing about the other directions, because they only ever look along two lines.

So the rule cannot be a consequence of the partial derivatives existing. The hill must have some additional property that $g$ lacks, and the rule must depend on it.

What a smooth hill has: a linear approximation

In one variable, a function with a derivative looks like its tangent line when you zoom in. Zoom in on the contour map of the hill at $P$ and watch the level curves.

Zooming in on the contour map at $P = (0, -1)$. The blue curves are level curves of the hill; the green bands are the level lines of the plane $5 + 3x + 2(y + 1)$. As the window shrinks, the level curves become straight, parallel and evenly spaced, and settle onto the green bands. The orange arrow is the gradient $(3, 2)$.
Monocurl source
let L = -3.65
let R = 1.15
let B = -1.8
let T = 1.8
let CX = (L + R) / 2
let Zoom = |a| {
    let sc = 1.8 / a
    let hw = (R - L) / 2 / sc
    var step = 0.01
    for (s in [0.02, 0.05, 0.1, 0.2, 0.5]) {
        if (s <= 2 * a * 1.0001) { step = s }
    }
    var parts = []
    for (j in range(-16, 17)) {
        let dc = j * step
        let xa = max(-hw, (dc - 2 * a) / 3)
        let xb = min(hw, (dc + 2 * a) / 3)
        if (xb > xa + 0.02 * a) {
            parts .= Seg([CX + xa * sc, (dc - 3 * xa) / 2 * sc, 0], [CX + xb * sc, (dc - 3 * xb) / 2 * sc, 0], tint(LEAF, 0.4), 7)
        }
    }
    for (j in range(-16, 17)) {
        let dc = j * step
        let pts = filter(
            map(sample(-hw, hw, 97), |x| [CX + x * sc, (1 - sqrt(max(1 + 3 * x - x * x * x - dc, 0))) * sc, 0]),
            |p| p[1] > B and p[1] < T
        )
        if (len(pts) > 1) {
            parts .= stroke{AZURE, 2.5} Polyline(pts)
        }
    }
    parts .= center{[2.45, 0.95, 0]} scale{1.2} color{INK} Number(2000 * a, 0)
    parts .= center{[2.45, -0.3, 0]} Math(str_join([to_string(100 * step, 0), "\text{ m}"]), 0.85)
    return parts
}
mesh view = Zoom(a: 0.5)
mesh border = Outline([[L, B, 0], [R, B, 0], [R, T, 0], [L, T, 0]], SLATE, 2.5)
mesh arrow = [
    Tip([CX, 0, 0], [CX + 1.15 * 3 / sqrt(13), 1.15 * 2 / sqrt(13), 0], EMBER, 4.5, 0.2),
    Pt([CX, 0, 0], INK, 0.08),
    center{[CX - 0.28, -0.25, 0]} Math("P", 0.85, INK)
]
mesh labels = [
    center{[2.45, 1.45, 0]} Note("window height (m)", 0.72),
    center{[2.45, 0.2, 0]} Note("contour interval", 0.72),
    Seg([1.75, -1.1, 0], [2.15, -1.1, 0], AZURE, 2.5),
    center{[2.85, -1.1, 0]} Note("the hill", 0.72),
    Seg([1.75, -1.55, 0], [2.15, -1.55, 0], tint(LEAF, 0.4), 7),
    center{[2.85, -1.55, 0]} Note("the plane", 0.72)
]

slide "zoom"
    play Wait(0.8)
    view.a = 0.01
    play Lerp(7, [&view], |u| (1 - 0.02 ^ u) / 0.98)
    play Wait(1.8)

Uses the shared definitions for this lesson's figures.

Evenly spaced parallel straight lines are the contour map of a plane, as we saw in the previous lesson. So near $P$ the hill is nearly a plane, and we can find which one. Expanding the formula at a nearby point $(h, -1 + k)$,

$$ f(h, -1 + k) = 6 + 3h - h^3 - (k - 1)^2 = \underbrace{5 + 3h + 2k}_{\text{a plane}} \; \underbrace{-\, h^3 - k^2}_{\text{error}}. $$

The plane part uses exactly the two partial derivatives, 3 and 2. The error is small in a strong sense. One step of size $0.1$ in each direction changes the plane part by tenths, while the error is $-0.001 - 0.01$. Shrink the step by 10 and the error shrinks by at least 100. That is the property to isolate: the error is small compared with the distance $\sqrt{h^2 + k^2}$ from the point.

The tangent plane to the hill at P, the graph of the linear approximation L(x, y) = 5 + 3x + 2(y + 1), drawn over a square one kilometer on a side. Near P the plane and the surface are hard to tell apart; farther away the surface curves off it.
The tangent plane to the hill at $P$, the graph of the linear approximation $L(x, y) = 5 + 3x + 2(y + 1)$, drawn over a square one kilometer on a side. Near $P$ the plane and the surface are hard to tell apart; farther away the surface curves off it.
Monocurl source
let v = View([-0.1, -0.4, 0], 1.3)
let plane = |h, k| pr(v, [h, -1 + k, 5 + 3 * h + 2 * k])
mesh base = [Walls(v), Terrain(v)]
mesh patch = block {
    . fill{alpha{0.5} tint(LEAF, 0.5)} stroke{LEAF, 2.5} Polygon([plane(-0.5, -0.5), plane(0.5, -0.5), plane(0.5, 0.5), plane(-0.5, 0.5)])
    for (s in [-0.25, 0, 0.25]) {
        . Seg(plane(s, -0.5), plane(s, 0.5), LEAF, 1.5)
        . Seg(plane(-0.5, s), plane(0.5, s), LEAF, 1.5)
    }
    . Pt(on_hill(v, 0, -1), INK, 0.08)
    . center{on_hill(v, 0, -1) + [-0.05, 0.3, 0]} Math("P", 0.9, INK)
}

Uses the shared definitions for this lesson's figures.

The important word is hidden in the limit: $(h, k)$ may approach $(0, 0)$ from any direction and along any path, and the same bound must hold for all of them. That is what the partial derivatives alone could not see.

For the hill at $P$, with $A = 3$ and $B = 2$, the error is $E = -h^3 - k^2$. Since $|h| \le \sqrt{h^2 + k^2}$ and $|k| \le \sqrt{h^2 + k^2}$,

$$ \frac{|E(h, k)|}{\sqrt{h^2 + k^2}} \le \frac{|h|\,h^2 + |k|\,|k|}{\sqrt{h^2 + k^2}} \le h^2 + |k|, $$

which tends to 0. So the hill is differentiable at $P$. The same is true at every point for every polynomial. In general there is a convenient test, which we cite without proof: if $f_x$ and $f_y$ exist throughout a disk around the point and are continuous at the point, then $f$ is differentiable there. (Analysis texts prove it with the mean value theorem, one variable at a time.) The function $g$ fails the definition at the origin, as it must; a problem below asks you to show why.

With this definition the rule we guessed becomes a theorem.

The proof shows where differentiability earns its keep: one bound on the error, valid in all directions at once, handles every $\mathbf{u}$ with the same two numbers.

The gradient and the steepest direction

The expression $f_x u_1 + f_y u_2$ is a dot product: the sum of the products of matching components of two vectors.

For the hill, $\nabla f(x, y) = (3 - 3x^2, -2y)$, and $\nabla f(0, -1) = (3, 2)$. The theorem now reads $D_{\mathbf{u}} f = \nabla f \cdot \mathbf{u}$. The gradient is a vector in the map plane, with an east component and a north component. It is drawn on the map, and it does not point up into the air.

Writing the slope as a dot product pays off at once, because a dot product has a geometric meaning: for vectors $\mathbf{a}$ and $\mathbf{b}$ with angle $\theta$ between them, $\mathbf{a} \cdot \mathbf{b} = |\mathbf{a}|\,|\mathbf{b}| \cos\theta$, where $|\mathbf{a}|$ is the length of $\mathbf{a}$.

Pause and try

At $P$ the slope in direction $\mathbf{u}$ is $3u_1 + 2u_2$. Find the unit vectors $\mathbf{u}$ for which the slope is 0, and say how they are related to the vector $(3, 2)$.

Show answerHide answer

We need $3u_1 + 2u_2 = 0$, so $\mathbf{u}$ is a multiple of $(2, -3)$. Its length must be 1, and $|(2, -3)| = \sqrt{13}$, so $\mathbf{u} = \pm\left(\tfrac{2}{\sqrt{13}}, \tfrac{-3}{\sqrt{13}}\right) \approx \pm(0.555, -0.832)$. These are the two directions perpendicular to $(3, 2)$, since $(3, 2) \cdot (2, -3) = 0$. Facing either way you walk level.

At $P$ this says: the steepest climb has slope $|(3, 2)| = \sqrt{13} \approx 3.61$ and is in the direction $(3, 2)/\sqrt{13} \approx (0.83, 0.55)$, about $34^\circ$ north of east. The animation at the top of the page is this theorem in motion. As the arrow turns, the slope traces one full cosine wave of height $\sqrt{13}$, and its peak is where the arrow lies along the gradient.

The hypothesis $\nabla f(P) \neq \mathbf{0}$ is needed. At the summit and at the pass the gradient is $(0, 0)$, every directional derivative is 0, and no direction is steepest.

The gradient crosses level curves at right angles

The two level directions at $P$ were $\pm(2, -3)/\sqrt{13}$. On the map, the direction in which the height does not change ought to be the direction of the contour line through $P$. Check it: $P$ is at height 5, and the level curve there is $y = -\sqrt{1 + 3x - x^3}$, whose derivative at $x = 0$ is $-\dfrac{3 - 3x^2}{2\sqrt{1 + 3x - x^3}} = -\dfrac{3}{2}$. A line of slope $-\tfrac32$ has direction $(2, -3)$. So the contour is perpendicular to the gradient at $P$. To prove this in general we need the rate of change of $f$ along a curve, which need not be a straight line.

The gradient \nabla f = (3 - 3x^2, -2y) of the hill, drawn as an arrow at each point of a grid on the contour map. Every arrow crosses the contour through its tail at a right angle and points to higher ground. The arrows are long where the contours are close and shrink to nothing at the summit and the pass.
The gradient $\nabla f = (3 - 3x^2, -2y)$ of the hill, drawn as an arrow at each point of a grid on the contour map. Every arrow crosses the contour through its tail at a right angle and points to higher ground. The arrows are long where the contours are close and shrink to nothing at the summit and the pass.
Monocurl source
let fr = MapFrame([0.1, 0.2, 0], 4.3)
mesh landm = MapLand(fr)
mesh lines = map([3, 4, 5, 6, 7], |c| Contour2(fr, c, CONTOUR, 2))
mesh border = [MapBorder(fr), MapTicks(fr)]
mesh field = block {
    for (x in [-1.5, -1, -0.5, 0, 0.5, 1, 1.5]) {
        for (y in [-1.25, -0.75, -0.25, 0.25, 0.75, 1.25]) {
            let g = [hill_x(x, y), hill_y(x, y)]
            let n = sqrt(g[0] * g[0] + g[1] * g[1])
            let len = min(0.085 * n, 0.44)
            let a = at(fr, x, y)
            . Tip(a, [a[0] + len * g[0] / n, a[1] + len * g[1] / n, 0], INK, 2.2, 0.1)
        }
    }
}
mesh special = [
    Tip(at(fr, 0, -1), [fx(fr, 0) + 0.085 * 3, fy(fr, -1) + 0.085 * 2, 0], EMBER, 4, 0.15),
    Pt(at(fr, 0, -1), EMBER, 0.06),
    Ring(at(fr, 1, 0), INK, 0.07, 2.5),
    Ring(at(fr, -1, 0), INK, 0.07, 2.5)
]
mesh tags = [LevelTag(fr, 7, 0.75, -ylev(7, 0.75)), LevelTag(fr, 6, 0.25, ylev(6, 0.25)), LevelTag(fr, 5, 1.75, ylev(5, 1.75)), LevelTag(fr, 3, -0.75, ylev(3, -0.75))]

Uses the shared definitions for this lesson's figures.

Part (2) is why the arrows in the figure meet the contour lines squarely. It also ties this lesson to the previous one. There we estimated steepness as the contour interval divided by the spacing of the lines, measured straight across them. “Straight across” is the gradient direction, and the slope in that direction is $|\nabla f|$. So close contours and a long gradient are the same fact.

A hiker who always walks in the direction of the gradient crosses every contour at a right angle and gains height as fast as possible at each step.

Paths of steepest ascent on the hill, each starting from a ring and following the gradient at every step. They cross the contour lines at right angles. Five of the starts lead to the summit; the two that start west of the pass climb the neighboring hill instead.
Monocurl source
let fr = MapFrame([0.1, 0.2, 0], 4.3)
let starts = [[-0.6, 1.38], [-0.6, -1.38], [1.9, 1.2], [1.9, -1.2], [0.4, -1.42], [-1.3, 1.25], [-1.45, -0.7]]
var paths = []
for (s in starts) {
    var p = s
    var pts = [at(fr, p[0], p[1])]
    var go = 1
    for (i in range(0, 220)) {
        if (go) {
            let g = [hill_x(p[0], p[1]), hill_y(p[0], p[1])]
            let n = sqrt(g[0] * g[0] + g[1] * g[1])
            let q = [p[0] + 0.02 * g[0] / n, p[1] + 0.02 * g[1] / n]
            if (n < 0.15 or q[0] < X0 or q[0] > X1 or abs(q[1]) > Y1) {
                go = 0
            } else {
                p = q
                pts .= at(fr, p[0], p[1])
            }
        }
    }
    paths .= pts
}
let PATHS = paths
mesh landm = MapLand(fr)
mesh lines = map([3, 4, 5, 6, 7], |c| Contour2(fr, c, CONTOUR, 2))
mesh border = [MapBorder(fr), MapTicks(fr)]
mesh tags = [LevelTag(fr, 7, 1.5321, 0), LevelTag(fr, 6, 0, 0), LevelTag(fr, 5, -0.3473, 0), LevelTag(fr, 3, -1, -1)]
mesh rings = map(starts, |s| Ring(at(fr, s[0], s[1]), INK, 0.08, 2.5))
let Climb = |u| block {
    for (path in PATHS) {
        let n = max(2, to_int(u * len(path)))
        let part = take(path, n)
        . Path(part, EMBER, 3.5)
        . Pt(part[n - 1], EMBER, 0.085)
    }
}
mesh climb = Climb(u: 0.02)

slide "climb"
    play Wait(0.6)
    climb.u = 1
    play Lerp(6, [&climb], linear)
    play Wait(1.8)

Uses the shared definitions for this lesson's figures.

Worked examples

Check yourself

At a point of a differentiable function, $\nabla f = (4, -3)$. What is the slope facing due north?

Step back to see what happened. A point on a hillside seemed to need a slope for every direction. For a function that is differentiable, meaning one that a plane approximates with an error small compared with the distance, all of those slopes come from two numbers by a dot product, and the geometry of the dot product then hands us the steepest direction and its relation to the contour lines. The work was in finding the right hypothesis; the function $g$ shows that the two partial derivatives alone do not justify any of it. The next lesson uses the perpendicularity of gradients and level curves to find the highest point on a trail.

Problems

The hill is $f(x, y) = 6 + 3x - x^3 - y^2$. Problems 1 and 2 are warm-ups, 3 to 5 combine ideas, and 6 and 7 go further.

Problem 1

Let $q(x, y) = x^3 y - 2y^2$. Find $q_x$, $q_y$ and $\nabla q(1, 2)$. Then compute $q_x(1, 2)$ a second time, directly from the limit definition.

Show solutionHide solution

Treating $y$ as constant, $q_x = 3x^2 y$. Treating $x$ as constant, $q_y = x^3 - 4y$. At $(1, 2)$: $\nabla q = (6, -7)$. From the definition, $q(1 + h, 2) = 2(1 + h)^3 - 8 = -6 + 6h + 6h^2 + 2h^3$ and $q(1, 2) = -6$, so the quotient is $6 + 6h + 2h^2$ for $h \neq 0$, with limit 6.

Problem 2

At the point $(0, 1)$ of the hill, on the north slope, find the slope facing due west, the slope facing northeast, and the largest slope in any direction.

Show solutionHide solution

$\nabla f(0, 1) = (3 - 0, -2) = (3, -2)$. Due west is $\mathbf{u} = (-1, 0)$: slope $-3$. Northeast is $\mathbf{u} = (1, 1)/\sqrt 2$: slope $(3 - 2)/\sqrt 2 = 1/\sqrt 2 \approx 0.71$. The largest slope is $|\nabla f| = \sqrt{13} \approx 3.61$, in the direction $(3, -2)/\sqrt{13}$, about $34^\circ$ south of east. Northeast is about $78.7^\circ$ away from that direction, which is why the slope there is so much smaller: $3.61 \cos 78.7^\circ \approx 0.71$.

Problem 3

Find the equation of the tangent line to the contour line of the hill through $(2, 1)$. Confirm your answer by solving the contour’s equation for $y$ and differentiating.

Show solutionHide solution

$\nabla f(2, 1) = (3 - 12, -2) = (-9, -2)$. The tangent line passes through $(2, 1)$ and is perpendicular to the gradient, so it consists of the points with $(-9, -2) \cdot (x - 2, y - 1) = 0$, that is, $9(x - 2) + 2(y - 1) = 0$, or $y = 1 - 4.5(x - 2)$. For the check: $f(2, 1) = 3$, so the contour is $y^2 = 3 + 3x - x^3$, and near $(2, 1)$, $y = \sqrt{3 + 3x - x^3}$ with $\dfrac{dy}{dx} = \dfrac{3 - 3x^2}{2y} = \dfrac{-9}{2} = -4.5$ at $x = 2$. The slopes agree.

Problem 4

Suppose $f$ is differentiable at $P$ and $\nabla f(P) \neq \mathbf{0}$. In which directions is the slope exactly half the maximum slope? Find them explicitly for the hill at $(0, 1)$, to two decimal places.

Show solutionHide solution

By the steepest ascent theorem the slope is $|\nabla f| \cos\theta$, which is half of $|\nabla f|$ when $\cos\theta = \tfrac12$, that is, $\theta = \pm 60^\circ$: the two directions $60^\circ$ either side of the gradient. At $(0, 1)$ the gradient $(3, -2)$ points at angle $\arctan(-2/3) \approx -33.69^\circ$ from east. The two directions are at $26.31^\circ$ and $-93.69^\circ$: $\mathbf{u} \approx (0.90, 0.44)$ and $\mathbf{u} \approx (-0.06, -1.00)$. Check the first: $3(0.896) - 2(0.443) = 1.80$, which is half of $\sqrt{13} \approx 3.61$.

Problem 5

Find every point of the hill’s map where $\nabla f = \mathbf{0}$. For each, describe what the contour lines look like nearby, and explain why the steepest ascent theorem says nothing there.

Show solutionHide solution

We need $3 - 3x^2 = 0$ and $-2y = 0$, so $x = \pm 1$ and $y = 0$: the points $(1, 0)$ and $(-1, 0)$. At $(1, 0)$, the summit, the nearby contours are closed loops around the point. At $(-1, 0)$, the pass, the contour through the point crosses itself. In both cases every directional derivative is $\mathbf{0} \cdot \mathbf{u} = 0$, so no direction has a larger slope than another, and the angle $\theta$ between $\mathbf{u}$ and the zero vector is not defined. The theorem’s hypothesis $\nabla f(P) \neq \mathbf{0}$ excludes exactly these points.

Problem 6

Prove that if $f$ is differentiable at $(a, b)$ then $f$ is continuous there, meaning $f(a + h, b + k) \to f(a, b)$ as $(h, k) \to (0, 0)$. Deduce that $g(x, y) = \dfrac{xy}{x^2 + y^2}$, $g(0, 0) = 0$, is not differentiable at the origin.

Show solutionHide solution

By definition $f(a + h, b + k) - f(a, b) = Ah + Bk + E(h, k)$. As $(h, k) \to (0, 0)$, $Ah \to 0$ and $Bk \to 0$. Also $|E(h, k)| = \dfrac{|E(h, k)|}{\sqrt{h^2 + k^2}} \cdot \sqrt{h^2 + k^2}$ for $(h, k) \ne (0, 0)$, a product of a factor tending to 0 and a factor tending to 0. So the difference tends to 0. For $g$: along the diagonal, $g(s, s) = \tfrac12$ for all $s \neq 0$, which does not tend to $g(0, 0) = 0$ as $s \to 0$. So $g$ is not continuous at the origin, and therefore not differentiable there.

Problem 7

Let $w(x, y) = \dfrac{x^2 y}{x^2 + y^2}$ for $(x, y) \neq (0, 0)$ and $w(0, 0) = 0$. (a) Show that $D_{\mathbf{u}} w(0, 0)$ exists for every unit vector $\mathbf{u}$ and equals $u_1^2 u_2$. (b) Find $\nabla w(0, 0)$. (c) Is $w$ differentiable at the origin?

Show solutionHide solution

(a) For $t \neq 0$, $w(tu_1, tu_2) = \dfrac{t^3 u_1^2 u_2}{t^2 (u_1^2 + u_2^2)} = t\,u_1^2 u_2$, since $u_1^2 + u_2^2 = 1$. The quotient $\dfrac{w(tu_1, tu_2) - 0}{t}$ equals $u_1^2 u_2$ for every $t \neq 0$, so that is its limit. (b) With $\mathbf{u} = (1, 0)$: $w_x(0, 0) = 1^2 \cdot 0 = 0$. With $\mathbf{u} = (0, 1)$: $w_y(0, 0) = 0$. So $\nabla w(0, 0) = (0, 0)$. (c) No. If $w$ were differentiable at the origin, the theorem would give $D_{\mathbf{u}} w = \nabla w \cdot \mathbf{u} = 0$ for every $\mathbf{u}$. But for $\mathbf{u} = (1, 1)/\sqrt 2$, part (a) gives $\tfrac12 \cdot \tfrac{1}{\sqrt 2} \approx 0.35 \neq 0$. So here a function has a slope in every direction and is still not differentiable: the slopes do not fit together into a single plane.

Shared definitions used by this lesson's figures
# house kit: prepended to every essay figure after the std imports

# palette. AZURE and EMBER are the two actors, LEAF marks an answer, ROSE a problem
let PAPER = [1, 1, 1, 1]
let INK = [0.08, 0.09, 0.11, 1]
let SLATE = [0.40, 0.44, 0.50, 1]
let MIST = [0.85, 0.86, 0.88, 1]
let AZURE = [0.13, 0.42, 0.68, 1]
let EMBER = [0.89, 0.42, 0.12, 1]
let LEAF = [0.13, 0.55, 0.36, 1]
let ROSE = [0.78, 0.22, 0.24, 1]
let PLUM = [0.47, 0.30, 0.66, 1]
let GOLD = [0.85, 0.62, 0.08, 1]

background = PAPER

# an opaque pastel of col; use for fills so overlaps do not darken
let tint = |col, amount = 0.18| [
    1 - (1 - col[0]) * amount,
    1 - (1 - col[1]) * amount,
    1 - (1 - col[2]) * amount,
    1
]

# text: Cap for labels, Note for quiet labels, Math for typeset maths
let Cap = |txt, size = 0.75, col = INK| color{col} Text(txt, size, "Charter")
let Note = |txt, size = 0.65| color{SLATE} Text(txt, size, "Charter")
let Math = |tex, size = 0.9, col = INK| color{col} Tex(tex, size)

# a paper rectangle to sit behind text that crosses lines
let Plate = |pos, size| center{pos} fill{PAPER} stroke{alpha{0} PAPER, 0} Rect(size)

# shapes with fill and stroke both set, so no stray black appears
let Pt = |pos, col = INK, r = 0.075| center{pos} fill{col} stroke{col, 1} Circle(r, 48)
let Ring = |pos, col = INK, r = 0.1, w = 3| center{pos} fill{PAPER} stroke{col, w} Circle(r, 48)
let Seg = |a, b, col = INK, w = 3| stroke{col, w} Line(a, b)
let Path = |pts, col = INK, w = 3| stroke{col, w} Polyline(pts)
let Region = |pts, col| fill{col} stroke{alpha{0} col, 0} Polygon(pts)
let Outline = |pts, col = INK, w = 3| fill{alpha{0} PAPER} stroke{col, w} Polygon(pts)
let Shape = |pts, col, amount = 0.18, w = 3| fill{tint(col, amount)} stroke{col, w} Polygon(pts)
let Disc = |pos, r, col, amount = 0.18, w = 3| center{pos} fill{tint(col, amount)} stroke{col, w} Circle(r, 96)
let Box = |pos, size, col, amount = 0.18, w = 3| center{pos} fill{tint(col, amount)} stroke{col, w} Rect(size)
# a line from a to b ending in a small solid arrowhead
let Tip = |a, b, col = INK, w = 3, head = 0.17| {
    let d = normalize([b[0] - a[0], b[1] - a[1], 0])
    let n = [-d[1], d[0], 0]
    let base = [b[0] - d[0] * head, b[1] - d[1] * head, 0]
    return [
        stroke{col, w} Line(a, base),
        fill{col} stroke{col, 1} Polygon([
            b,
            [base[0] + n[0] * head * 0.42, base[1] + n[1] * head * 0.42, 0],
            [base[0] - n[0] * head * 0.42, base[1] - n[1] * head * 0.42, 0]
        ])
    ]
}

# plots. a frame is [x0, x1, y0, y1, centre, width, height]: a data window and the scene box it fills
let Frame = |x0, x1, y0, y1, c = [0, 0, 0], w = 6.4, h = 3.6| [x0, x1, y0, y1, c, w, h]
let fx = |fr, x| fr[4][0] + (x - (fr[0] + fr[1]) / 2) * fr[5] / (fr[1] - fr[0])
let fy = |fr, y| fr[4][1] + (y - (fr[2] + fr[3]) / 2) * fr[6] / (fr[3] - fr[2])
let at = |fr, x, y| [fx(fr, x), fy(fr, y), 0]

# axes through the data origin (or the nearest edge of the window), with optional names
let Axes = |fr, x_name = "", y_name = "", col = SLATE| {
    let ox = clamp(fr[0], 0, fr[1])
    let oy = clamp(fr[2], 0, fr[3])
    var parts = [
        Tip(at(fr, fr[0], oy), [fx(fr, fr[1]) + 0.25, fy(fr, oy), 0], col, 2.5, 0.15),
        Tip(at(fr, ox, fr[2]), [fx(fr, ox), fy(fr, fr[3]) + 0.25, 0], col, 2.5, 0.15)
    ]
    if (x_name != "") {
        parts .= center{[fx(fr, fr[1]) + 0.5, fy(fr, oy), 0]} Math(x_name, 0.8, col)
    }
    if (y_name != "") {
        parts .= center{[fx(fr, ox), fy(fr, fr[3]) + 0.5, 0]} Math(y_name, 0.8, col)
    }
    return parts
}

# tick marks with numbers at the listed data values
let XTicks = |fr, xs, col = SLATE, places = 0| block {
    let oy = fy(fr, clamp(fr[2], 0, fr[3]))
    for (x in xs) {
        . Seg([fx(fr, x), oy - 0.07, 0], [fx(fr, x), oy + 0.07, 0], col, 2)
        . center{[fx(fr, x), oy - 0.33, 0]} Math(to_string(x, places), 0.72, col)
    }
}
let YTicks = |fr, ys, col = SLATE, places = 0| block {
    let ox = fx(fr, clamp(fr[0], 0, fr[1]))
    for (y in ys) {
        . Seg([ox - 0.07, fy(fr, y), 0], [ox + 0.07, fy(fr, y), 0], col, 2)
        . center{[ox - 0.36, fy(fr, y), 0]} Math(to_string(y, places), 0.72, col)
    }
}

# faint grid lines at the listed data values
let GridLines = |fr, xs, ys, col = MIST| block {
    for (x in xs) {
        . Seg(at(fr, x, fr[2]), at(fr, x, fr[3]), col, 1.5)
    }
    for (y in ys) {
        . Seg(at(fr, fr[0], y), at(fr, fr[1], y), col, 1.5)
    }
}

# the graph of f over [a, b], drawn in the frame. values are clamped to the window
let Graph = |fr, f, a, b, col = AZURE, w = 4, n = 160|
    stroke{col, w} Polyline(map(sample(a, b, n), |x| at(fr, x, clamp(fr[2], f(x), fr[3]))))

# the region between the graph of f and the x axis over [a, b]
let AreaUnder = |fr, f, a, b, col = AZURE, amount = 0.2, n = 80| {
    var pts = [at(fr, a, 0)]
    for (x in sample(a, b, n)) {
        pts .= at(fr, x, clamp(fr[2], f(x), fr[3]))
    }
    pts .= at(fr, b, 0)
    return fill{tint(col, amount)} stroke{alpha{0} col, 0} Polygon(pts)
}

# the running hill: height in hundreds of meters above the point x km east and y km north
let hill = |x, y| 6 + 3 * x - x * x * x - y * y
let hill_x = |x, y| 3 - 3 * x * x
let hill_y = |x, y| -2 * y
let X0 = -1.75
let X1 = 2
let Y0 = -1.5
let Y1 = 1.5
let WATER = [0.45, 0.68, 0.87, 1]
let DEEP = [0.30, 0.52, 0.74, 1]
let CONTOUR = [0.42, 0.33, 0.24, 1]
# land color by height, shaded as if lit from the northwest
let ramp = |h| keyframe_lerp([1 -> [0.40, 0.60, 0.43, 1], 4 -> [0.66, 0.77, 0.50, 1], 6 -> [0.88, 0.82, 0.60, 1], 8 -> [0.98, 0.96, 0.92, 1]], clamp(h, 1, 8))
let LIGHT = normalize([-0.6, 0.5, 0.62])
let relief = |x, y| clamp(dot(normalize([-0.3 * hill_x(x, y), -0.3 * hill_y(x, y), 1]), LIGHT), 0, 1)
let land = |x, y, soft = 1| {
    let c = ramp(hill(x, y))
    let s = 1 + soft * (0.62 + 0.62 * relief(x, y) - 1)
    return [clamp(c[0] * s, 0, 1), clamp(c[1] * s, 0, 1), clamp(c[2] * s, 0, 1), 1]
}
# a view of the block from the south: [screen origin, scale, azimuth, elevation, vertical scale]
let View = |o = [-0.18, -0.33, 0], s = 1.4, az = 0.2, el = 0.65, zs = 0.22| [o, s, az, el, zs]
let ZB = 1
let pr = |v, p| [
    v[0][0] + v[1] * (p[0] * cos(v[2]) + p[1] * sin(v[2])),
    v[0][1] + v[1] * ((p[1] * cos(v[2]) - p[0] * sin(v[2])) * sin(v[3]) + (p[2] - ZB) * v[4] * cos(v[3])),
    0
]
let on_hill = |v, x, y| pr(v, [x, y, hill(x, y)])
# the terrain, flooded up to `level`
let Terrain = |v, level = 0| stroke{alpha{0} PAPER, 0} point_map{|p| pr(v, [p[0], p[1], max(hill(p[0], p[1]), level)])}
    ColorGrid(|p, idx| {
        if (hill(p[0], p[1]) < level) { return WATER }
        return land(p[0], p[1])
    }, [X0, X1, 91], [Y0, Y1, 73], 1)
# the cut south and east faces of the block, with any water standing against them
let Walls = |v, level = 0| {
    let face = |pts, lvl, a, b| {
        var out = []
        for (p in pts) {
            out .= pr(v, [p[0], p[1], max(hill(p[0], p[1]), lvl)])
        }
        out .= pr(v, [b[0], b[1], ZB])
        out .= pr(v, [a[0], a[1], ZB])
        return out
    }
    let south = map(sample(X0, X1, 60), |x| [x, Y0])
    let east = map(sample(Y0, Y1, 40), |y| [X1, y])
    return [
        fill{DEEP} stroke{DEEP, 1} Polygon(face(south, max(level, ZB), [X0, Y0], [X1, Y0])),
        fill{DEEP} stroke{DEEP, 1} Polygon(face(east, max(level, ZB), [X1, Y0], [X1, Y1])),
        fill{[0.82, 0.78, 0.72, 1]} stroke{SLATE, 1.5} Polygon(face(south, ZB, [X0, Y0], [X1, Y0])),
        fill{[0.72, 0.68, 0.62, 1]} stroke{SLATE, 1.5} Polygon(face(east, ZB, [X1, Y0], [X1, Y1]))
    ]
}
# the level curve hill = c inside the map, as a list of polylines of [x, y]
let contour = |c| {
    let R = |x| 6 + 3 * x - x * x * x - c
    let ok = |r| r >= 0 and r <= Y1 * Y1
    let edge = |a, b| {
        var lo = a
        var hi = b
        for (i in range(0, 34)) {
            let m = (lo + hi) / 2
            if (ok(R(m))) { lo = m } else { hi = m }
        }
        return lo
    }
    let N = 160
    var runs = []
    var cur = []
    var prev = X0
    var prev_ok = 0
    for (i in range(0, N + 1)) {
        let x = X0 + (X1 - X0) * i / N
        let good = ok(R(x))
        if (good) {
            if (not prev_ok and i > 0) { cur .= edge(x, prev) }
            cur .= x
        } else if (prev_ok) {
            cur .= edge(prev, x)
            runs .= cur
            cur = []
        }
        prev = x
        prev_ok = good
    }
    if (prev_ok) { runs .= cur }
    var lines = []
    for (run in runs) {
        let n = len(run)
        if (n >= 2 and run[n - 1] - run[0] > 0.012) {
            # extra samples near both ends, where the curve may turn vertical
            var cand = [run[0]]
            for (t in [0.02, 0.07, 0.16, 0.3, 0.5, 0.75]) { cand .= run[0] + t * (run[1] - run[0]) }
            for (i in range(1, n - 1)) { cand .= run[i] }
            for (t in [0.75, 0.5, 0.3, 0.16, 0.07, 0.02]) { cand .= run[n - 1] - t * (run[n - 1] - run[n - 2]) }
            cand .= run[n - 1]
            var xs = [run[0]]
            var last = run[0]
            for (x in cand) {
                if (x - last > 0.0004 and run[n - 1] - x > 0.0004) {
                    xs .= x
                    last = x
                }
            }
            xs .= run[n - 1]
            let closed0 = R(run[0]) < 0.001
            let closed1 = R(run[n - 1]) < 0.001
            var up = []
            var down = []
            let m = len(xs)
            for (i in range(0, m)) {
                var y = sqrt(max(R(xs[i]), 0))
                if ((i == 0 and closed0) or (i == m - 1 and closed1)) { y = 0 }
                up .= [xs[i], y]
                down .= [xs[i], -y]
            }
            if (closed0 and closed1 and m > 8) {
                var loop = drop(up, 4)
                for (p in drop(reverse(down), 1)) { loop .= p }
                for (p in take(drop(up, 1), 4)) { loop .= p }
                lines .= loop
            } else if (closed1) {
                var one = up
                for (p in drop(reverse(down), 1)) { one .= p }
                lines .= one
            } else if (closed0) {
                var one = reverse(down)
                for (p in drop(up, 1)) { one .= p }
                lines .= one
            } else {
                lines .= up
                lines .= down
            }
        }
    }
    return lines
}
let Contour2 = |fr, c, col = CONTOUR, w = 2.5| map(contour(c), |l| Path(map(l, |p| at(fr, p[0], p[1])), col, w))
let Contour3 = |v, c, col = CONTOUR, w = 2.5| map(contour(c), |l| Path(map(l, |p| pr(v, [p[0], p[1], c])), col, w))
# the y at which the contour of level c passes above the point x
let ylev = |c, x| sqrt(6 + 3 * x - x * x * x - c)
# the flat map: tinted land in a frame, with kilometer ticks
let MapFrame = |c = [0, 0, 0], w = 4.5| Frame(X0, X1, Y0, Y1, c, w, w * 0.8)
let MapLand = |fr, soft = 0.5| stroke{alpha{0} PAPER, 0} point_map{|p| at(fr, p[0], p[1])}
    ColorGrid(|p, idx| land(p[0], p[1], soft), [X0, X1, 91], [Y0, Y1, 73], 1)
let MapBorder = |fr| Outline([at(fr, X0, Y0), at(fr, X1, Y0), at(fr, X1, Y1), at(fr, X0, Y1)], SLATE, 2.5)
let MapTicks = |fr| block {
    for (x in [-1, 0, 1, 2]) {
        . Seg(at(fr, x, Y0), [fx(fr, x), fy(fr, Y0) - 0.09, 0], SLATE, 2)
        . center{[fx(fr, x), fy(fr, Y0) - 0.31, 0]} Math(to_string(x), 0.7, SLATE)
    }
    for (y in [-1, 0, 1]) {
        . Seg(at(fr, X0, y), [fx(fr, X0) - 0.09, fy(fr, y), 0], SLATE, 2)
        . center{[fx(fr, X0) - 0.36, fy(fr, y), 0]} Math(to_string(y), 0.7, SLATE)
    }
    . center{[fx(fr, 1.5), fy(fr, Y0) - 0.31, 0]} Math("x", 0.8, SLATE)
    . center{[fx(fr, X0) - 0.36, fy(fr, 0.5), 0]} Math("y", 0.8, SLATE)
}
# a level number sitting in a gap in its contour
let LevelTag = |fr, c, x, y, soft = 0.5, col = CONTOUR| [
    map([0.2, 0.17, 0.14, 0.11], |r| center{at(fr, x, y)} fill{alpha{0.5} land(x, y, soft)} stroke{alpha{0} PAPER, 0} Circle(r, 40)),
    center{at(fr, x, y)} Math(to_string(c), 0.7, col)
]