Monocurl Essays
Series Download

Surfaces and contour maps: functions of two variables

A hiking map shows a mountain on flat paper. Its contour lines are the level sets of a function of two variables, and their spacing measures steepness.

24 min·Early college·

AssumesWhat is a derivative? Speed at a single instantMaxima and minima: where the slope is zero

A flooded valley drains. Each time the water has dropped another 100 m, its shoreline is marked on the hill. Every mark is level, and together the marks are the contour lines of the hill.
Monocurl source
let v = View([-0.95, -0.6, 0], 1.25)
let Drain = |level| {
    var parts = [Walls(v, level), Terrain(v, level)]
    for (c in [7, 6, 5, 4, 3]) {
        if (c > level) { parts .= Contour3(v, c) }
    }
    parts .= center{[2.85, 1.72, 0]} Note("water level", 0.76)
    if (level > 1.78) {
        parts .= Contour3(v, level, DEEP, 2)
        parts .= center{[2.67, 1.24, 0]} scale{1.25} color{INK} Number(100 * level, 0)
        parts .= center{[3.27, 1.24, 0]} Math("\text{m}", 0.95)
    } else {
        parts .= center{[2.85, 1.24, 0]} Cap("drained", 0.8, DEEP)
    }
    # the legend of shorelines: grey until the water has dropped past each one
    parts .= center{[2.85, 0.5, 0]} Note("shorelines", 0.76)
    for (c in [7, 6, 5, 4, 3]) {
        let row = 0.02 - 0.46 * (7 - c)
        var col = MIST
        if (c > level) { col = CONTOUR }
        parts .= Seg([2.2, row, 0], [2.62, row, 0], col, 3)
        parts .= center{[3.1, row, 0]} Math(str_join([to_string(100 * c), "\,\text{m}"]), 0.8, col)
    }
    return parts
}
mesh drain = Drain(level: 7.5)

slide "drain"
    play Wait(0.8)
    drain.level = 1.7
    play Lerp(7.5, [&drain], linear)
    play Wait(1.8)

Uses the shared definitions for this lesson's figures.

A sheet of paper is flat and a mountain is not. Yet a hiker with a good map can tell, before leaving the parking lot, where the summit is, which slope is too steep to climb, and where the easiest crossing between two peaks lies. All of that information is carried by thin brown lines with numbers on them. How does a flat drawing hold a third dimension?

Temperature across a metal plate and pressure on a weather chart also depend on two inputs at once, and the contour map is the standard way to see any such quantity. We will build the map for one hill, then say exactly what was built.

A height for every point of the map

Start with what a surveyor would collect. Put the origin at a marker post, measure $x$ in kilometers east of it and $y$ in kilometers north, and at each of a grid of positions write down the height of the ground. To keep the numbers small we measure height in hundreds of meters, so a height of 6 means 600 m.

Surveyed heights on the hill, in hundreds of meters, at points half a kilometer apart. The position x is kilometers east of the marker post and y is kilometers north.
Surveyed heights on the hill, in hundreds of meters, at points half a kilometer apart. The position $x$ is kilometers east of the marker post and $y$ is kilometers north.
Monocurl source
let fr = MapFrame([0.2, 0.3, 0], 4.1)
mesh landm = MapLand(fr, 0.4)
mesh border = MapBorder(fr)
mesh ticks = MapTicks(fr)
mesh spots = block {
    for (x in [-1.5, -1, -0.5, 0, 0.5, 1, 1.5]) {
        for (y in [-1, 0, 1]) {
            . Pt(at(fr, x, y), INK, 0.045)
            . center{[fx(fr, x), fy(fr, y) + 0.24, 0]} Math(to_string(hill(x, y), 1), 0.7, INK)
        }
    }
}

Uses the shared definitions for this lesson's figures.

Read along the middle row, where $y = 0$. Going east the heights run 4.9, 4.0, 4.6, 6.0, 7.4, 8.0, 7.1. So the ground dips, climbs to 8.0 at the point $(1, 0)$, and falls again. The rows above and below are each lower than the middle row by exactly 1.

Pause and try

Using only the 21 numbers in the figure, where would you guess the summit is? A hiker walks from the west edge to the summit along the middle row. What is the lowest height on that walk, and is the ground at that place lower or higher than the ground just north and south of it?

Show answerHide answer

The largest number is 8.0 at $(1, 0)$, and its neighbors on all four sides are lower, so the summit is at or near $(1, 0)$. On the walk along the middle row the lowest height is 4.0, at $(-1, 0)$. The points north and south of it, $(-1, 1)$ and $(-1, -1)$, are at 3.0. So that place is the low point of the east-west walk and at the same time the high point of the north-south line through it. A place like that is called a pass.

Twenty-one numbers are a sample. The hill itself has a height above every point of the map, and for this lesson the hill is given by a formula:

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

Check it against the figure: $f(0, 0) = 6$, $f(1, 0) = 6 + 3 - 1 = 8$, and $f(-1, 1) = 6 - 3 + 1 - 1 = 3$. The term $-y^2$ is why the rows at $y = \pm 1$ are lower than the middle row by 1. The map covers $-1.75 \le x \le 2$ and $-1.5 \le y \le 1.5$.

A rule like this, which takes a pair of numbers and returns one number, is the object the rest of the series studies.

For the hill, $D$ is the rectangle the map covers, and the range is the set of heights that occur on it, from 1.75 up to 8 at the summit. (The value 1.75 occurs at the eastern corners $(2, \pm 1.5)$ and at $(-1, \pm 1.5)$, due north and south of the pass.) “Exactly one” matters: the ground cannot be at 300 m and at 400 m over the same spot, and we will use that below.

The graph is a surface

The graph of a function of one variable is a curve in the plane: the input uses one axis and the output the other. Our input already uses two axes, so the output needs a third.

The graph of f(x, y) = 6 + 3x - x^3 - y^2 over the map: above each point (x, y) of the base, the surface is at height z = f(x, y). The summit is the point (1, 0, 8) and the pass is (-1, 0, 4).
The graph of $f(x, y) = 6 + 3x - x^3 - y^2$ over the map: above each point $(x, y)$ of the base, the surface is at height $z = f(x, y)$. The summit is the point $(1, 0, 8)$ and the pass is $(-1, 0, 4)$.
Monocurl source
let v = View([0.25, -0.05, 0], 1.25)
mesh walls = Walls(v)
mesh terrain = Terrain(v)
mesh scales = block {
    for (x in [-1, 0, 1, 2]) {
        let p = pr(v, [x, Y0, ZB])
        . Seg(p, [p[0], p[1] - 0.09, 0], SLATE, 2)
        . center{[p[0], p[1] - 0.31, 0]} Math(to_string(x), 0.7, SLATE)
    }
    let px = pr(v, [-1.5, Y0, ZB])
    . center{[px[0], px[1] - 0.31, 0]} Math("x", 0.8, SLATE)
    for (y in [-1, 0, 1]) {
        let p = pr(v, [X1, y, ZB])
        . Seg(p, [p[0] + 0.09, p[1], 0], SLATE, 2)
        . center{[p[0] + 0.36, p[1] - 0.03, 0]} Math(to_string(y), 0.7, SLATE)
    }
    let py = pr(v, [X1, 1.5, ZB])
    . center{[py[0] + 0.36, py[1] + 0.1, 0]} Math("y", 0.8, SLATE)
    . Seg(pr(v, [X0, Y0, ZB]), pr(v, [X0, Y0, 8.6]), SLATE, 2.5)
    for (z in [2, 4, 6, 8]) {
        let p = pr(v, [X0, Y0, z])
        . Seg(p, [p[0] - 0.09, p[1], 0], SLATE, 2)
        . center{[p[0] - 0.32, p[1], 0]} Math(to_string(z), 0.7, SLATE)
    }
    let pz = pr(v, [X0, Y0, 8.6])
    . center{[pz[0], pz[1] + 0.25, 0]} Math("z", 0.8, SLATE)
}
let Spot = |x, y, txt, dx, dy| [
    Pt(on_hill(v, x, y), INK, 0.07),
    center{on_hill(v, x, y) + [dx, dy, 0]} Math(txt, 0.75, INK)
]
mesh spots = [
    Spot(1, 0, "(1, 0, 8)", 0, 0.32),
    Spot(-1, 0, "(-1, 0, 4)", 0, 0.32),
    Spot(0, -1, "(0, -1, 5)", 0, -0.32)
]

Uses the shared definitions for this lesson's figures.

The graph of a function of two variables is a surface, and for our $f$ it is the hill itself, with a summit over $(1, 0)$, a pass over $(-1, 0)$, and ground that rises again toward the west edge, the shoulder of a neighboring hill.

The picture is easy to like and hard to use. Try to read from it the height at $(0.5, 1)$, or to decide whether the north face is steeper than the east face. A drawing of a surface distorts distances and cannot be measured with a ruler. That is the map maker’s problem: the graph lives in space, and paper does not.

Slicing the hill at a fixed height

The map maker’s solution is to stop asking “how high is this point?” and ask the reverse question: “which points are at this height?”

Flood the valley until the water surface stands at height 6. The water surface is a horizontal plane, and its edge, the shoreline, is the set of points of the hill that are exactly at height 6. Now drain the water and keep the shoreline as a mark on the ground.

  1. Flood the valley to height 6. The shoreline is every point of the hill at exactly that height.
  2. Drain the water and keep the mark. It stays at height 6 all the way around.
  3. Do the same at heights 3, 4, 5 and 7.
  4. Look straight down. Height drops out of view, and the marks are the contour lines of a map.
From a hill to its map. The shoreline at each water level is a curve of constant height, and seen from directly above those curves are the contour lines.
Monocurl source
let Hill = |el, az, ox, oy, s, level| {
    let v = View([ox, oy, 0], s, az, el)
    var parts = [Walls(v, level), Terrain(v, level)]
    if (level > 1.78) { parts .= Contour3(v, level, DEEP, 2) }
    return parts
}
let Marks = |el, az, ox, oy, s, levels| {
    let v = View([ox, oy, 0], s, az, el)
    return block {
        for (c in levels) {
            if (c == 6) { . Contour3(v, c, EMBER, 3.5) } else { . Contour3(v, c) }
        }
    }
}
let top = Frame(X0, X1, Y0, Y1, [0, 0, 0], 3.75 * 1.15, 3 * 1.15)
mesh hillm = Hill(el: 0.65, az: 0.2, ox: -0.18, oy: -0.45, s: 1.3, level: 1)
mesh marks = []
mesh tags = []

slide "Flood the valley to height 6. The shoreline is every point of the hill at exactly that height."
    hillm.level = 6
    play Lerp(2, [&hillm])
    marks = Marks(el: 0.65, az: 0.2, ox: -0.18, oy: -0.45, s: 1.3, [6])
    play Fade(0.5, [&marks])

slide "Drain the water and keep the mark. It stays at height 6 all the way around."
    hillm.level = 1
    play Lerp(2, [&hillm])

slide "Do the same at heights 3, 4, 5 and 7."
    marks = Marks(el: 0.65, az: 0.2, ox: -0.18, oy: -0.45, s: 1.3, [6, 3, 4, 5, 7])
    play Fade(0.8, [&marks])

slide "Look straight down. Height drops out of view, and the marks are the contour lines of a map."
    hillm.el = PI / 2
    hillm.az = 0
    hillm.ox = -0.144
    hillm.oy = 0
    hillm.s = 1.15
    marks.el = PI / 2
    marks.az = 0
    marks.ox = -0.144
    marks.oy = 0
    marks.s = 1.15
    play Lerp(2.5, [&hillm, &marks])
    tags = [
        LevelTag(top, 7, 1, 1, 1),
        LevelTag(top, 6, 0, 0, 1, EMBER),
        LevelTag(top, 5, 0, -1, 1),
        LevelTag(top, 4, -0.5, 0.5 * sqrt(2.5), 1),
        LevelTag(top, 3, -1, 1, 1)
    ]
    play Fade(0.5, [&tags])

Uses the shared definitions for this lesson's figures.

Seen from directly above, a shoreline is a curve in the $(x, y)$ plane, which paper can show without distortion. The height that the view from above throws away is put back as a label on the curve.

We can find the height-6 curve from the formula. A point $(x, y)$ is on it when $f(x, y) = 6$, that is, when

$$ 6 + 3x - x^3 - y^2 = 6, \qquad\text{or}\qquad y^2 = 3x - x^3 = x(3 - x^2). $$

For $y$ to exist the right side must not be negative. Since $x(3 - x^2) \ge 0$ exactly when $x \le -\sqrt 3$ or $0 \le x \le \sqrt 3 \approx 1.73$, the main part of the curve lies over $0 \le x \le \sqrt 3$. For each such $x$ there are two points, $y = \pm\sqrt{3x - x^3}$, one north of the $x$-axis and one south. At $x = 1$ they are at $y = \pm\sqrt 2 \approx \pm 1.41$. The two halves join at $(0, 0)$ and at $(\sqrt 3, 0)$, so the curve is a closed loop around the summit that passes through the marker post. (The other range, $x \le -\sqrt 3$, contributes a sliver at the far west edge of the map, where the neighboring hill pokes above 6.)

Pause and try

Find the points where the height-7 curve crosses the $x$-axis and where it crosses the line $x = 1$. Is the height-7 curve inside or outside the height-6 loop?

Show answerHide answer

On the line $x = 1$ the height is $f(1, y) = 8 - y^2$, which equals 7 when $y = \pm 1$. On the $x$-axis we need $6 + 3x - x^3 = 7$, or $x^3 - 3x + 1 = 0$. A calculator gives $x \approx 0.347$ and $x \approx 1.532$ on the map (check: $3(0.347) - 0.347^3 \approx 1.041 - 0.042 = 0.999$). All four points are inside the height-6 loop, which reaches $y = \pm 1.41$ on $x = 1$ and runs from $x = 0$ to $x = 1.73$ on the axis. Higher ground is nearer the summit, so the 7 curve is nested inside the 6 curve.

These curves have a name that does not depend on hills.

Notice where each object lives. The graph is a surface in space. A level set is a set of points in the domain, in the plane. The mark on the hillside is where the graph meets the horizontal plane $z = c$, and the level set is that mark seen from directly above.

The same function drawn two ways. Left: its graph, a surface in space. Right: its contour map in the plane, with contour interval 1 (that is, 100 m). The orange curve is the level set at height 6 in both.
The same function drawn two ways. Left: its graph, a surface in space. Right: its contour map in the plane, with contour interval 1 (that is, 100 m). The orange curve is the level set at height 6 in both.
Monocurl source
let v = View([-1.95, -0.2, 0], 0.82)
let fr = MapFrame([1.95, -0.1, 0], 3.0)
mesh walls = Walls(v)
mesh terrain = Terrain(v)
mesh rings = block {
    for (c in [3, 4, 5, 7]) {
        . Contour3(v, c, CONTOUR, 2)
    }
    . Contour3(v, 6, EMBER, 3.5)
}
mesh landm = MapLand(fr)
mesh lines = block {
    for (c in [3, 4, 5, 7]) {
        . Contour2(fr, c, CONTOUR, 2)
    }
    . Contour2(fr, 6, EMBER, 3.5)
}
mesh border = [MapBorder(fr), MapTicks(fr)]
mesh tags = [
    LevelTag(fr, 7, 1, -1),
    LevelTag(fr, 6, 1.5, ylev(6, 1.5), 0.5, EMBER),
    LevelTag(fr, 5, 0, 1),
    LevelTag(fr, 4, -0.5, -0.5 * sqrt(2.5)),
    LevelTag(fr, 3, -1, 1)
]
mesh spots = [
    Pt(on_hill(v, 1, 0), INK, 0.06),
    Pt(on_hill(v, -1, 0), INK, 0.06),
    Pt(at(fr, 1, 0), INK, 0.06),
    Pt(at(fr, -1, 0), INK, 0.06),
    center{[fx(fr, 1), fy(fr, 0) + 0.26, 0]} Cap("summit", 0.7),
    center{[fx(fr, -1) + 0.45, fy(fr, 0), 0]} Cap("pass", 0.7)
]
mesh titles = [
    center{[-1.85, 1.6, 0]} Note("graph: a surface in space", 0.72),
    center{[1.95, 1.6, 0]} Note("contour map: curves in the plane", 0.72)
]

Uses the shared definitions for this lesson's figures.

What the lines can and cannot do

Two rules about contour lines are usually given to hikers as folklore. Both are consequences of the definitions, and the first has a one-line proof.

So the 300 m line and the 400 m line never meet, and the proof used only the words “exactly one” in the definition of a function.

The theorem says nothing about a level set meeting itself, and on our hill one does. Look at level 4, the height of the pass. The equation $f(x, y) = 4$ rearranges to $y^2 = 2 + 3x - x^3$, and the cubic factors:

$$ y^2 = (2 - x)(x + 1)^2, \qquad\text{so}\qquad y = \pm (x + 1)\sqrt{2 - x}. $$

Both signs give $y = 0$ at $x = -1$. The two branches pass through the same point $(-1, 0)$ with different directions, and the level set is shaped like an X there.

The level set at height 4 (purple) crosses itself at the pass. It divides the nearby ground into four sectors: higher ground to the east and west, lower ground to the north and south. Contours at 3.5 and 4.5 show how the neighboring level sets avoid the crossing.
The level set at height 4 (purple) crosses itself at the pass. It divides the nearby ground into four sectors: higher ground to the east and west, lower ground to the north and south. Contours at 3.5 and 4.5 show how the neighboring level sets avoid the crossing.
Monocurl source
let fr = MapFrame([-0.75, 0.15, 0], 4.3)
mesh landm = MapLand(fr)
mesh lines = block {
    for (c in [3, 5, 6, 7]) {
        . Contour2(fr, c, CONTOUR, 1.8)
    }
    . Contour2(fr, 3.5, AZURE, 2.5)
    . Contour2(fr, 4.5, EMBER, 2.5)
    . Contour2(fr, 4, PLUM, 4)
}
mesh border = [MapBorder(fr), MapTicks(fr)]
mesh tags = LevelTag(fr, 4, -0.25, 0.75 * sqrt(2.25), 0.5, PLUM)
mesh pass = Pt(at(fr, -1, 0), INK, 0.07)
let KX = 2.75
mesh key = [
    Seg([KX - 0.7, 1.3, 0], [KX - 0.2, 1.3, 0], EMBER, 3),
    center{[KX + 0.3, 1.3, 0]} Math("4.5", 0.8, EMBER),
    Seg([KX - 0.7, 0.75, 0], [KX - 0.2, 0.75, 0], PLUM, 4),
    center{[KX + 0.3, 0.75, 0]} Math("4", 0.8, PLUM),
    Seg([KX - 0.7, 0.2, 0], [KX - 0.2, 0.2, 0], AZURE, 3),
    center{[KX + 0.3, 0.2, 0]} Math("3.5", 0.8, AZURE),
    center{[KX - 0.05, -0.55, 0]} Note("pass at", 0.72),
    center{[KX - 0.05, -0.98, 0]} Math("(-1, 0)", 0.8, INK)
]

Uses the shared definitions for this lesson's figures.

Stand at the pass. To the east and to the west the ground rises; to the north and south it falls away. The level-4 set is the boundary between “higher than me” and “lower than me”, and at a pass that boundary has to separate four regions, which is why it crosses. A contour line that crosses itself marks a pass (mathematicians say saddle point).

Level sets also need not be curves at all. At level 8 the level set of our $f$ on the map is the single point $(1, 0)$, the summit. At level 9 it is empty, since no ground is that high. And a flat plateau, a region where $f$ is constant, is a level set with area. “Level curve” describes the usual case; “level set” is always correct.

Why close lines mean steep ground

The second piece of folklore is that closely spaced contour lines mean steep ground. This one can be made exact. Walk in a straight line across the map. Each time you pass from one contour line to the next, your height has changed by one contour interval. If the lines are far apart, you walked a long way for that change; if they are close, you got it in a few steps.

A walk due east along the line $y = 0$. Each orange dot on the map (left) is a place where the route crosses a contour line, and the same crossings are marked on the height profile of the route (right). Where the dots crowd together on the map, the profile is steep.
Monocurl source
let fr = MapFrame([-1.95, -0.05, 0], 3.1)
let pf = Frame(X0, X1, 3.4, 8.5, [2.05, -0.05, 0], 2.9, 2.9)
let prof = |x| hill(x, 0)
let crossings = block {
    for (c in [5, 6, 7]) {
        for (i in range(0, 157)) {
            let a = X0 + (X1 - X0) * i / 157
            let b = X0 + (X1 - X0) * (i + 1) / 157
            if ((prof(a) - c) * (prof(b) - c) < 0) {
                var lo = a
                var hi = b
                for (k in range(0, 30)) {
                    let m = (lo + hi) / 2
                    if ((prof(a) - c) * (prof(m) - c) > 0) { lo = m } else { hi = m }
                }
                . [lo, c]
            }
        }
    }
}
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, 0.5, -ylev(6, 0.5)), LevelTag(fr, 5, 0, 1)]
mesh route = Seg(at(fr, X0, 0), at(fr, X1, 0), EMBER, 2.5)
mesh plot = block {
    for (h in [4, 5, 6, 7, 8]) {
        . Seg(at(pf, X0, h), at(pf, X1, h), MIST, 1.5)
        . center{[fx(pf, X0) - 0.28, fy(pf, h), 0]} Math(to_string(h), 0.7, SLATE)
    }
    . Path([at(pf, X0, 8.5), at(pf, X0, 3.4), at(pf, X1, 3.4)], SLATE, 2.5)
    for (x in [-1, 0, 1, 2]) {
        . Seg(at(pf, x, 3.4), [fx(pf, x), fy(pf, 3.4) - 0.09, 0], SLATE, 2)
        . center{[fx(pf, x), fy(pf, 3.4) - 0.31, 0]} Math(to_string(x), 0.7, SLATE)
    }
    . center{[fx(pf, 1.5), fy(pf, 3.4) - 0.31, 0]} Math("x", 0.8, SLATE)
    . center{[2.05, 1.75, 0]} Note("height along the route", 0.72)
    . center{[-1.95, 1.75, 0]} Note("the route on the map", 0.72)
}
let Walk = |x| {
    let n = to_int(max(2, ceil((x - X0) * 40)))
    var parts = [stroke{AZURE, 4} Polyline(map(sample(X0, max(x, X0 + 0.02), n), |t| at(pf, t, prof(t))))]
    for (cr in crossings) {
        if (cr[0] <= x) {
            parts .= Pt(at(fr, cr[0], 0), EMBER, 0.07)
            parts .= Pt(at(pf, cr[0], cr[1]), EMBER, 0.07)
        }
    }
    parts .= Ring(at(fr, x, 0), INK, 0.09, 3)
    parts .= Ring(at(pf, x, prof(x)), INK, 0.09, 3)
    return parts
}
mesh walk = Walk(x: X0)

slide "walk"
    play Wait(0.6)
    walk.x = X1
    play Lerp(7, [&walk], linear)
    play Wait(1.6)

Uses the shared definitions for this lesson's figures.

On this route the height is $p(x) = f(x, 0) = 6 + 3x - x^3$. West of the summit the route crosses the 6 line at $x = 0$ and the 7 line at $x \approx 0.347$. East of the summit it crosses the 7 line again at $x \approx 1.532$ and the 6 line at $x = \sqrt 3 \approx 1.732$.

Pause and try

On which side of the summit is the hill steeper along this route, and by roughly what factor? Use only the four crossing points just listed.

Show answerHide answer

On the west side the height rises by 1 over a distance of $0.347$, an average slope of $1 / 0.347 \approx 2.9$. On the east side it falls by 1 over a distance of $1.732 - 1.532 = 0.200$, an average slope of about $-5.0$. The east side is steeper by a factor of about $1.7$. In ordinary units, a slope of 5 here means 500 m of height per kilometer of map distance, a 50% grade.

That computation is the method: the change in height between adjacent lines is the contour interval $\Delta$, the map distance between them is $d$, and their quotient is the average slope. Here is the precise statement, including what it says about the slope at individual points.

The segment from $P$ to $Q$ is your straight route between two contour lines. The function $\varphi$ is the height profile, the curve in the right panel of the last figure, and $\varphi'$ is its derivative, the slope of the profile. Part (3) is the hiker’s rule with a number in it: on ground nowhere steeper than $M$, contour lines one interval apart are never closer than $\Delta / M$. Turned around, lines closer than that prove the ground is steeper than $M$ somewhere between them.

Check part (2) on the east side of our route. The average slope between the 7 line and the 6 line was $-5.0$. The profile has $p'(x) = 3 - 3x^2$, and $3 - 3x^2 = -5$ at $x = \sqrt{8/3} \approx 1.633$, which does lie between $1.532$ and $1.732$.

One caution. The quotient $\Delta / d$ is the slope along the route you measured. A route that cuts across the lines at a slant covers more map distance between them and gives a smaller slope, and a route along a contour line gives slope zero. The steepest slope comes from measuring straight across the lines. Making “the slope at a point in a given direction” precise is the subject of the next lesson.

Three maps to know by sight

The same construction applies to any function of two variables. Three simple maps are worth recognizing.

Contour maps of three functions on the square -2 \le x, y \le 2, each with its levels labelled. A plane gives evenly spaced parallel lines, a bowl gives circles that crowd together as the bowl steepens, and a saddle gives hyperbolas around a level set that crosses itself.
Contour maps of three functions on the square $-2 \le x, y \le 2$, each with its levels labelled. A plane gives evenly spaced parallel lines, a bowl gives circles that crowd together as the bowl steepens, and a saddle gives hyperbolas around a level set that crosses itself.
Monocurl source
let Mini = |cx, title| {
    let fr = Frame(-2, 2, -2, 2, [cx, -0.3, 0], 2.25, 2.25)
    return [
        Seg(at(fr, -2, 0), at(fr, 2, 0), MIST, 1.5),
        Seg(at(fr, 0, -2), at(fr, 0, 2), MIST, 1.5),
        center{[cx, 1.3, 0]} Math(title, 0.85, INK)
    ]
}
let Edge = |cx| {
    let fr = Frame(-2, 2, -2, 2, [cx, -0.3, 0], 2.25, 2.25)
    return Outline([at(fr, -2, -2), at(fr, 2, -2), at(fr, 2, 2), at(fr, -2, 2)], SLATE, 2.5)
}
let Lab = |cx, x, y, txt| {
    let fr = Frame(-2, 2, -2, 2, [cx, -0.3, 0], 2.25, 2.25)
    return [Plate(at(fr, x, y), [0.3, 0.3]), center{at(fr, x, y)} Math(txt, 0.7, AZURE)]
}
let fa = Frame(-2, 2, -2, 2, [-2.25, -0.3, 0], 2.25, 2.25)
let fb = Frame(-2, 2, -2, 2, [0.2, -0.3, 0], 2.25, 2.25)
let fc = Frame(-2, 2, -2, 2, [2.65, -0.3, 0], 2.25, 2.25)
mesh guides = [Mini(-2.25, "f = 2x + y"), Mini(0.2, "f = x^2 + y^2"), Mini(2.65, "f = xy")]
mesh plane = block {
    for (c in [-4, -2, 0, 2, 4]) {
        let a = max(-2, (c - 2) / 2)
        let b = min(2, (c + 2) / 2)
        . Seg(at(fa, a, c - 2 * a), at(fa, b, c - 2 * b), AZURE, 2.5)
    }
}
mesh bowl = block {
    for (c in [1, 2, 3, 4]) {
        . center{at(fb, 0, 0)} fill{alpha{0} PAPER} stroke{AZURE, 2.5} Circle(sqrt(c) * 2.25 / 4, 96)
    }
}
mesh saddle = block {
    . Seg(at(fc, -2, 0), at(fc, 2, 0), AZURE, 2.5)
    . Seg(at(fc, 0, -2), at(fc, 0, 2), AZURE, 2.5)
    for (c in [1, 2]) {
        for (s in [[1, 1], [-1, -1], [1, -1], [-1, 1]]) {
            . Path(map(sample(c / 2, 2, 40), |x| at(fc, s[0] * x, s[1] * c / x)), AZURE, 2.5)
        }
    }
}
mesh edges = [Edge(-2.25), Edge(0.2), Edge(2.65)]
mesh ticks = block {
    for (fr in [fa, fb, fc]) {
        for (t in [-1, 0, 1]) {
            . Seg(at(fr, t, -2), [fx(fr, t), fy(fr, -2) - 0.08, 0], SLATE, 2)
            . center{[fx(fr, t), fy(fr, -2) - 0.3, 0]} Math(to_string(t), 0.7, SLATE)
        }
        . center{[fx(fr, 1.7), fy(fr, -2) - 0.3, 0]} Math("x", 0.75, SLATE)
    }
    for (t in [-1, 0, 1]) {
        . Seg(at(fa, -2, t), [fx(fa, -2) - 0.08, fy(fa, t), 0], SLATE, 2)
        . center{[fx(fa, -2) - 0.3, fy(fa, t), 0]} Math(to_string(t), 0.7, SLATE)
    }
    . center{[fx(fa, -2) - 0.3, fy(fa, 1.7), 0]} Math("y", 0.75, SLATE)
}
mesh labels = block {
    for (c in [-4, -2, 2, 4]) {
        . Lab(-2.25, 0.4 * c, 0.2 * c, to_string(c))
    }
    . Lab(-2.25, 0.5, -1, "0")
    . Lab(0.2, -sqrt(0.5), sqrt(0.5), "1")
    . Lab(0.2, 1, 1, "2")
    . Lab(0.2, sqrt(1.5), -sqrt(1.5), "3")
    . Lab(0.2, -sqrt(2), -sqrt(2), "4")
    . Lab(2.65, 0.75, 4 / 3, "1")
    . Lab(2.65, -sqrt(2), -sqrt(2), "2")
    . Lab(2.65, -1, 1, "-1")
    . Lab(2.65, sqrt(2), -sqrt(2), "-2")
    . Lab(2.65, 0, -1.3, "0")
}

Uses the shared definitions for this lesson's figures.

The third map, $f(x, y) = xy$, is a pass with nothing else around it: its level set at 0 is the pair of coordinate axes, crossing at the origin as the level-4 set crosses at the pass of our hill. Problem 4 works it out.

Reading errors to avoid

Check yourself

Two adjacent contour lines on a map with a contour interval of 100 m are 250 m apart, measured straight across on the map. What is the average slope of the ground between them along that line?

We began with a mountain that would not fit on paper. The step that made it fit was to turn the question around, from “what is the height here?” to “where is the height equal to $c$?”, because the answers to the second question are sets of points in the plane. The same reversal works for any function of two variables, and later lessons lean on it: the direction of steepest climb and the highest point along a trail can both be read from how the level curves sit.

Problems

Problems 1 and 2 repeat the moves of the lesson. Problems 3 to 5 ask you to combine them or explain something. The last two go further. The hill is $f(x, y) = 6 + 3x - x^3 - y^2$ throughout.

Problem 1

Compute the height of the hill at $(2, 1)$, at $(-1, -1)$ and at $(0.5, 0.5)$. Which two of these points lie on the same level set? Is $(0.5, 0.5)$ inside or outside the level curve at height 7 that surrounds the summit?

Show solutionHide solution

$f(2, 1) = 6 + 6 - 8 - 1 = 3$. $f(-1, -1) = 6 - 3 + 1 - 1 = 3$. $f(0.5, 0.5) = 6 + 1.5 - 0.125 - 0.25 = 7.125$. The first two points are both at height 3, so both belong to the level set at level 3 (they lie on different pieces of it, one near the east edge and one south of the pass). The third point is at height $7.125 > 7$. The ground inside the 7 loop is the ground higher than 7, so $(0.5, 0.5)$ is inside it, though only just.

Problem 2

Let $f(x, y) = 3x - 4y$. Describe the level set at level $c$. Find the distance between the level sets at levels $c$ and $c + 5$, measured straight across them, and the slope of the plane in that direction.

Show solutionHide solution

The level set is the line $3x - 4y = c$, that is, $y = \tfrac34 x - \tfrac c4$. These lines all have slope $\tfrac34$, so measure along the perpendicular through the origin, in the direction of the unit vector $\left(\tfrac35, -\tfrac45\right)$. Its point at distance $t$ from the origin is $\left(\tfrac{3t}{5}, -\tfrac{4t}{5}\right)$, where $f = \tfrac{9t}{5} + \tfrac{16t}{5} = 5t$. So level $c$ is met at $t = c/5$ and level $c + 5$ at $t = c/5 + 1$: the lines are exactly 1 apart for every $c$. The slope straight across is $\Delta / d = 5 / 1 = 5$.

Problem 3

Consider the east-west line $y = 1$ on the hill. Show that the height along it is $7 - (x - 1)^2(x + 2)$. Deduce that this line meets the level curve at height 7 at exactly one point of the map, and that it never enters the region above 7. What does the line look like relative to that level curve?

Show solutionHide solution

Along $y = 1$, $f(x, 1) = 5 + 3x - x^3$. Then $7 - f(x, 1) = x^3 - 3x + 2$. Since $x = 1$ is a root, divide: $x^3 - 3x + 2 = (x - 1)(x^2 + x - 2) = (x - 1)^2(x + 2)$. So $f(x, 1) = 7 - (x - 1)^2(x + 2)$. On the map $x \ge -1.75$, so $x + 2 > 0$, and $(x - 1)^2 \ge 0$; hence $f(x, 1) \le 7$, with equality only at $x = 1$. The line touches the 7 curve at the single point $(1, 1)$ and stays on the low side of it everywhere else. It is tangent to the level curve there: it grazes it without crossing. (A path that just touches a level curve at its highest point is the central picture in the lesson on Lagrange multipliers.)

Problem 4

A student’s sketch of a contour map shows the 300 m line and the 400 m line crossing at a point $A$. Explain why this cannot be the contour map of a function. What feature of real terrain might the student have been trying to draw, and what does a correct map of it look like?

Show solutionHide solution

At $A$ the height would be both 300 m and 400 m, but a function assigns exactly one value to each point of its domain, so level sets at different levels are disjoint. Real terrain that tempts this drawing is a cliff. At a vertical cliff the ground changes height by 100 m over almost no map distance, and the contour lines run together into what looks like a single line, without crossing. An overhang, where the ground really does have two heights above one map position, is not the graph of a function, and a contour map cannot show it.

Problem 5

Let $f(x, y) = xy$. (a) Describe the level set at level $c$ for $c > 0$, $c = 0$ and $c < 0$. (b) For $c > 0$, find the point of the level set nearest the origin in the first quadrant and its distance from the origin. (c) Use (b) to decide whether the surface gets steeper or gentler as you walk away from the origin along the line $y = x$.

Show solutionHide solution

(a) For $c \ne 0$ the level set is the hyperbola $y = c/x$, which has two branches: in the first and third quadrants if $c > 0$, in the second and fourth if $c < 0$. For $c = 0$ the equation $xy = 0$ holds when $x = 0$ or $y = 0$, so the level set is the two coordinate axes, which cross at the origin. (b) By symmetry in $x$ and $y$ the nearest point is on the line $y = x$: there $x^2 = c$, so the point is $(\sqrt c, \sqrt c)$ at distance $\sqrt{2c}$. (Indeed the squared distance of $(x, c/x)$ from the origin is $x^2 + c^2/x^2 = (x - c/x)^2 + 2c \ge 2c$.) (c) Levels $1, 2, 3, 4$ meet the line $y = x$ at distances $1.414$, $2$, $2.449$, $2.828$ from the origin. The gaps are $0.586$, $0.449$, $0.379$, so the average slopes are $1.71$, $2.23$, $2.64$: the surface gets steeper. (Along $y = x$ at distance $s$ from the origin the height is $s^2/2$, with derivative $s$, in agreement.)

Problem 6

The functions $f(x, y) = x^2 + y^2$ and $g(x, y) = \sqrt{x^2 + y^2}$ both have circles about the origin as level sets. (a) Draw, or describe, the contour maps of each at levels $1, 2, 3, 4$. (b) What does the difference between the two maps say about the two surfaces? (c) Show in general that if $g(x, y) = \varphi(f(x, y))$ for some function $\varphi$ of one variable, then every level set of $f$ is contained in a level set of $g$.

Show solutionHide solution

(a) For $f$ the circles have radii $1$, $1.414$, $1.732$, $2$. For $g$ the level set at level $c > 0$ is the circle of radius $c$, so the radii are $1, 2, 3, 4$, evenly spaced. (b) Even spacing means the slope of $g$ straight outward is the same everywhere, $\Delta/d = 1$: its graph is a cone. The crowding circles of $f$ mean its graph steepens outward: a bowl. The shapes of the level curves are identical, and the labels and spacing carry the difference. (c) Suppose $P$ and $Q$ are in the same level set of $f$, so $f(P) = f(Q) = c$. Then $g(P) = \varphi(f(P)) = \varphi(c) = \varphi(f(Q)) = g(Q)$, so $P$ and $Q$ are in the same level set of $g$, the one at level $\varphi(c)$. Here $g = \sqrt{f}$, with $\varphi(t) = \sqrt t$.

Problem 7

For the hill, the lesson showed that the level set at height 4 is $y = \pm(x + 1)\sqrt{2 - x}$. Find the slopes of the two branches at the pass $(-1, 0)$, and hence the angle of each of the four sectors into which they divide the ground near the pass. Which sectors are the higher ground?

Show solutionHide solution

Let $y = (x + 1)\sqrt{2 - x}$. By the product rule, $\dfrac{dy}{dx} = \sqrt{2 - x} + (x + 1)\cdot\dfrac{-1}{2\sqrt{2 - x}}$. At $x = -1$ the second term vanishes and the slope is $\sqrt 3$. The other branch is the negative of this one and has slope $-\sqrt 3$. A line of slope $\sqrt 3$ makes an angle of $60^\circ$ with the $x$-axis, and one of slope $-\sqrt 3$ makes $-60^\circ$. So the two sectors that contain the $x$-axis, to the east and west, each span $120^\circ$, and the two that contain the north and south directions each span $60^\circ$. On the $x$-axis near the pass, $f(x, 0) = 4 + (2 - x)(x + 1)^2 > 4$ for $x \neq -1$, so the east and west sectors are the higher ground, and the narrower north and south sectors are the lower ground.

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)
]