A surface plot is the same idea every time: build a floor grid with meshgrid , compute a height z = f ( x , y ) at each cell, then drape it. But the cases that trip people up are always the same handful: wrong shapes, empty or single-point grids, negative heights, flat degenerate functions, and axis-mixups. This page walks through every one of those cells so you never meet a scenario cold.
Everything below builds only on the parent note and these prerequisites: the parent topic , NumPy vectorization , Matplotlib figure and axes objects , Colormaps in Matplotlib , and Contour plots .
Before any code, here is the full space of cases a 3D-surface problem can throw at you. Each later example is tagged with the cell it covers.
#
Case class
What's special
Example
A
Positive bowl (normal case)
z ≥ 0 everywhere, smooth
Ex 1
B
Negative / saddle heights
z takes both signs — sign matters
Ex 2
C
Zero / degenerate input — flat function
z = constant, surface is a plane
Ex 3
D
Single-point / empty grid
len(x)=1 or len=0 — edge inputs
Ex 4
E
Shape / transpose bug
Z.shape vs (len(y),len(x))
Ex 5
F
Limiting behaviour — ripple far out
r → ∞ , crest radii
Ex 6
G
Real-world word problem
terrain / height reading
Ex 7
H
Exam twist — wireframe stride count
how many lines actually drawn
Ex 8
We hit every cell A–H below.
Worked example The normal case
Build x = y = np.linspace(-2, 2, 5), X, Y = np.meshgrid(x, y), Z = X**2 + Y**2.
What is Z.shape, the minimum height, and its location?
Forecast: guess the shape and where the bowl bottoms out before reading on.
x has len 5, y has len 5. meshgrid returns shape (len(y), len(x)) = (5, 5).
Why this step? The parent formula says shape is ( n , m ) = ( len(y), len(x)) . We must know this to sanity-check Z.
Z = X**2 + Y**2 is vectorized — it runs on the whole 5 × 5 array elementwise, so Z.shape == (5,5) too.
Why this step? plot_surface demands X,Y,Z all the same 2D shape.
Each term is a square, so z ≥ 0 . The smallest possible is 0 + 0 = 0 , reached only where x = 0 and y = 0 .
Why this step? The plot is literally f drawn; the bowl's floor sits at the minimum of f .
Look at the figure: a symmetric bowl, deepest at the centre.
Verify: f ( 0 , 0 ) = 0 2 + 0 2 = 0 . Any other point, e.g. f ( 2 , 0 ) = 4 > 0 . So the min is 0 at the origin. ✓
Worked example Signs both ways — the saddle
Z = X**2 - Y**2 on the same grid. Find the height at ( 2 , 0 ) , at ( 0 , 2 ) , and at ( 0 , 0 ) . Is there a single lowest point?
Forecast: guess the sign of z along the two axes before computing.
Along the x -axis (y = 0 ): z = x 2 ≥ 0 . At ( 2 , 0 ) : z = 4 .
Why this step? Fixing y = 0 collapses f to a 1D parabola opening up — a valley wall.
Along the y -axis (x = 0 ): z = − y 2 ≤ 0 . At ( 0 , 2 ) : z = − 4 .
Why this step? Fixing x = 0 gives − y 2 , a parabola opening down — a ridge falling away.
At the origin: z = 0 − 0 = 0 . This point is a saddle : up in one direction, down in the perpendicular one.
Why this step? Because signs disagree along the two axes, no single minimum exists — the surface has both positive and negative regions.
Look at the figure — the classic Pringle-chip shape.
[!mistake] "A saddle has a lowest point at the middle."
No — the centre is neither a max nor a min. Go along + x and it rises; along + y it falls. That's why z spans both signs and a diverging colormap (like coolwarm) reads best here.
Verify: f ( 2 , 0 ) = 4 , f ( 0 , 2 ) = − 4 , f ( 0 , 0 ) = 0 ; signs opposite ⇒ saddle. ✓
Worked example A constant-height plane
Z = np.full(X.shape, 3.0) (every height is 3). What does the surface look like, and does plot_surface still work?
Forecast: guess the shape drawn.
np.full((5,5), 3.0) makes a 5 × 5 array of all 3's — same shape as X,Y.
Why this step? The shape rule still applies even when the function is constant.
Every ( x , y ) maps to z = 3 , so the "surface" is a flat horizontal plane floating at height 3.
Why this step? Degenerate input (a constant function) is still valid — the drape just has no bumps.
cmap will paint the whole sheet a single color, because there's no height variation to map.
Why this step? A colormap encodes differences in z ; with z constant there's nothing to differentiate.
Verify: Z.min() == Z.max() == 3.0 and Z.shape == (5,5). A valid, if boring, surface. ✓
Worked example Edge inputs
(a) x = np.linspace(0, 0, 1), y = np.linspace(-1, 1, 3). What is X.shape?
(b) x = np.array([]), y = np.array([1,2]). What is X.shape?
Forecast: predict both shapes.
(a) len(x)=1, len(y)=3. Shape is (len(y), len(x)) = (3, 1).
Why this step? The rule ( n , m ) never breaks; a single-column grid is legal but too thin to draw a real surface.
(a) With only one x-column you cannot form a patch (a patch needs ≥ 2 rows and ≥ 2 columns). plot_surface runs without error but draws essentially a line, not a sheet.
Why this step? A surface facet is a little quadrilateral spanning two rows × two columns; with one column there's nothing to fill.
(b) len(x)=0, len(y)=2. Shape is (2, 0) — an empty grid with zero columns. Nothing gets plotted.
Why this step? Empty input is degenerate: Z is empty too, so the drape covers no area.
Verify: (a) shape (3,1); (b) shape (2,0). Both follow the ( l e n ( y ) , l e n ( x )) rule. ✓
Worked example Shape mismatch caught by
.shape
x = np.linspace(0,1,4), y = np.linspace(0,1,3). A student builds Z by looping as Z[j][i] (swapped) and gets shape (4,3). Why does the plot come out garbled, and what shape should Z have?
Forecast: guess the correct Z.shape.
meshgrid(x,y) gives X.shape == Y.shape == (len(y), len(x)) == (3, 4).
Why this step? This is the target every Z must match.
The student's Z is (4,3) — the transpose . plot_surface(X, Y, Z) needs all three equal; (3,4) ≠ (4,3) (they're only equal by luck when the grid is square).
Why this step? Mismatched shapes either raise an error or silently misalign heights to the wrong floor cells → garbled surface.
Fix: prefer vectorized Z = f(X, Y) (auto-correct shape), or if you must loop, index Z[i, j] with i over rows (y) and j over columns (x).
Why this step? Letting broadcasting handle it removes the whole class of transpose bugs (see meshgrid and broadcasting ).
Verify: X.shape == (3,4) but the buggy Z.shape == (4,3); (3,4) != (4,3) ⇒ mismatch detected. ✓
Worked example Where are the crests of
Z = sin(sqrt(X**2+Y**2))?
Let r = x 2 + y 2 be the distance from the origin. At which radii r is the surface at a crest (z = 1 ), and where is the first trough (z = − 1 )?
Forecast: guess the radius of the first ring crest.
z = sin ( r ) . A sine crest is where its argument equals 2 π plus full turns: r = 2 π + 2 π k .
Why this step? sin peaks at π /2 ; because r only depends on distance, every point at that radius has the same height → concentric rings .
First crest: k = 0 ⇒ r = π /2 ≈ 1.5708 .
Why this step? This is the innermost bright ring; it's the Forecast-then-Verify target from the parent note.
First trough (z = − 1 ): sin r = − 1 at r = 2 3 π ≈ 4.712 .
Why this step? The wave keeps oscillating outward; troughs and crests alternate every π in r .
Limiting behaviour: as r → ∞ the surface keeps rippling forever between − 1 and + 1 — it never settles. Look at the concentric rings in the figure.
Verify: sin ( π /2 ) = 1 (crest), sin ( 3 π /2 ) = − 1 (trough), and ∣ sin r ∣ ≤ 1 always. ✓
Worked example A hill's height map
A hill's altitude in metres is z = 100 − 4 x 2 − y 2 where x , y are km east/north of a marker. What is the summit altitude and its location? At x = 3 , y = 0 km, are you above or below sea level (z = 0 )?
Forecast: guess the summit height before computing.
The two subtracted squares are largest-subtracted when x = y = 0 ; there they subtract nothing, so z is maximised there.
Why this step? Both − 4 x 2 and − y 2 only lower the height, so removing them (setting them to 0) gives the peak.
Summit: z = 100 − 0 − 0 = 100 m at the marker ( 0 , 0 ) .
Why this step? Plug the maximising point into f — the plot is just f .
At ( 3 , 0 ) : z = 100 − 4 ( 9 ) − 0 = 100 − 36 = 64 m — still above sea level.
Why this step? We evaluate f at the queried coordinate and compare to 0.
Units check: x 2 is km², coefficient 4 carries m/km² so 4 x 2 is metres — consistent with z in metres. ✓
Verify: summit = 100 m at origin; f ( 3 , 0 ) = 64 > 0 (above sea level). ✓
Worked example How many mesh lines does
rstride actually draw?
A grid from linspace(-3,3,61) in both axes gives a 61 × 61 grid. You call plot_wireframe(X,Y,Z, rstride=10, cstride=10). How many horizontal (row) lines are drawn?
Forecast: guess the count before dividing.
There are 61 rows (indices 0…60). Stride 10 keeps rows 0 , 10 , 20 , 30 , 40 , 50 , 60 .
Why this step? rstride=k draws every k -th row line, always including the first and last.
Count: 0 , 10 , 20 , 30 , 40 , 50 , 60 → that's ⌊ 60/10 ⌋ + 1 = 6 + 1 = 7 lines.
Why this step? The number of kept indices from 0 to N − 1 with step k is ⌊( N − 1 ) / k ⌋ + 1 .
So 7 row-lines (and by symmetry 7 column-lines) form the cage — clean and see-through, exactly the point of thinning a dense grid .
Why this step? Confirms the parent's claim that stride trades detail for readability.
[!mnemonic]
Lines drawn = ⌊ stride N − 1 ⌋ + 1 . "Divide the gaps, add one for the fencepost."
Verify: ⌊ 60/10 ⌋ + 1 = 7 . ✓
Recall Min of
Z = X**2 + Y**2?
0 at the origin — sum of two squares can't go below zero.
Recall Is
X**2 - Y**2 a bowl or a saddle?
A saddle: up along x , down along y ; z takes both signs.
Recall
meshgrid shape for len(x)=1, len(y)=3?
(3, 1) — too thin to draw a real surface patch.
Recall First crest radius of
sin(sqrt(X**2+Y**2))?
r = π /2 ≈ 1.57 , where sin = 1 .
Recall Lines drawn by
rstride=10 on a 61-row grid?
⌊ 60/10 ⌋ + 1 = 7 .
Prediction line — cover the answer:
Summit of z = 100 − 4 x 2 − y 2 and its height? at ( 0 , 0 ) , height 100 m.
3D plots — surface, wireframe (parent)
meshgrid and broadcasting
NumPy vectorization
Matplotlib figure and axes objects
Colormaps in Matplotlib
Contour plots