Two Functions Added, and a Cylinder Appeared — Complex Potential and d'Alembert's Paradox
Why a uniform stream plus one tiny point gives you cylinder flow for free.
Two functions added, and a cylinder appears out of nowhere. Take a uniform stream, drop one tiny doublet on top, and a perfect circle materializes in the middle — even though nobody drew a circle. This post is about why that conjuring trick is really a single line of nineteenth-century complex analysis. We follow how the Cauchy–Riemann equations turn out to be the backbone of 2D inviscid flow, how the one formula produces the exact flow around a cylinder, and how that result ends in the "zero drag" paradox that broke d'Alembert in 1752. At the end, sixty lines of NumPy reproduce the whole picture.
Why streamlines and equipotentials cross at right angles#
In 2D incompressible, inviscid, irrotational flow there are two scalar fields living side by side: the velocity potential and the stream function . Velocity ties them together:
The slope of an equipotential line is . The slope of a streamline is . Their product is . The two families are everywhere orthogonal.
That perpendicular grid is not just decoration. It is exactly what the real and imaginary parts of a holomorphic function look like. A single condition — that the complex derivative gives the same value no matter which direction you approach from — pinned these two scalar fields together as one pair.
Complex velocity potential — the bridge Cauchy and Riemann built#
Let and consider . For to be differentiable in the complex sense, every direction of approach to has to give the same derivative. Equating the limits along the real and imaginary axes yields two equations:
These are the Cauchy–Riemann relations. And those are the very same lines we just wrote down for the velocities. Wrap the potential and stream function of an inviscid 2D flow into a single complex function, and that function is holomorphic. We call the complex velocity potential.
Its complex derivative carries meaning too. Writing ,
This is the complex velocity. It is the conjugate of the velocity vector — and it still remembers both components.
One line — draws a cylinder#
Start with the two simplest building blocks. A uniform stream . Splitting real and imaginary parts gives , . The equipotentials are vertical lines, the streamlines horizontal. A doublet (the limit of a source–sink pair collapsing to a point) . In polar form , so and .
Add them:
In polar coordinates,
with the length scale set by the doublet-to-stream ratio. Look for the zero contour of . Either (the -axis) or . That second condition is the decisive one. The entire circle of radius is a single streamline.
In an inviscid model, any streamline can be replaced by a solid wall without changing the rest of the flow. So we promote to be the surface of a cylinder. Uniform flow plus one doublet — the sum of two elementary functions — is the exact inviscid solution for a circular cylinder in a uniform stream. Nobody drew a circle; the math drew one for us.
Try it directly in the simulation below.
W(z) = Uz + m/z + iΓ/(2π)·ln(z). When Γ=0 the picture is symmetric — d'Alembert's zero drag. Slide Γ to break the symmetry and lift appears (Kutta–Joukowski).
Pushing U and m together changes the radius . Sliding Γ away from zero breaks the up–down symmetry — that is the lift mechanism in the next section. Turn stream and equipotential on at the same time and the perpendicular lattice they form is visible everywhere.
Add a circulation, and lift appears#
On the cylinder surface the speed distribution is symmetric top-to-bottom. Bernoulli's theorem turns that into a symmetric pressure distribution. The net force is zero. That is the d'Alembert paradox waiting in the next section. To break symmetry, you need circulation (the line integral of tangential velocity around the body). A point vortex has complex potential . Add it on top:
Now the speed is higher above the cylinder than below. The pressure is lower above and higher below. Lift appears. The Kutta–Joukowski theorem nails it down: the per-span lift is — Kutta in 1902, Joukowski in 1906, independently. The asymmetry that the Γ slider creates above is exactly this.
Superposition — every inviscid flow is an addition#
The stream function obeys Laplace's equation , which is linear. The sum of two solutions is another solution. That single fact carries half of nineteenth-century fluid mechanics on its back.
Four building blocks suffice. Uniform . Source/Sink . Vortex . Doublet . Place these on the plane and you get half-bodies, Rankine ovals, and rotating cylinders in turn.
Each preset is a sum of elementary flows. The same machinery — superposition of ψ — builds half-bodies, ovals, and lifting cylinders.
Cylinder = U + doublet is the picture above. Rankine half-body is uniform plus a single source — a nose-shaped stagnation point grows on the upstream side. Rankine oval puts a source and a sink on the same axis and the result closes into a finite body. Lifting cylinder adds a vortex on top of the cylinder pair to make the up–down asymmetry.
d'Alembert's paradox — what zero drag taught us#
Integrate the surface pressure on the cylinder from and you get exactly zero drag. d'Alembert derived that result himself, in 1752. He also knew it was a contradiction. Anyone who drags a stick through water can feel the resistance. Inviscid, irrotational, incompressible assumptions cannot manufacture it.
The resolution took nearly a century and a half. In 1904 Prandtl identified the boundary layer — a thin region near a wall where viscosity, no matter how small, dominates. Inside that layer the flow can separate, vortices spin off downstream, and the wake breaks the upstream–downstream pressure symmetry. That asymmetry is the drag.
The d'Alembert answer is not wrong. It is the exact answer for an inviscid fluid. That the answer disagrees with reality is what opened modern fluid mechanics.
Sixty lines of NumPy — tracing #
import numpy as np
import matplotlib.pyplot as plt
def cylinder_potential(z, U=1.0, m=1.0, gamma=0.0):
"""Return (phi, psi) for W = Uz + m/z - i*gamma/(2π)*ln(z)."""
safe_z = np.where(np.abs(z) < 1e-9, 1e-9 + 0j, z)
W = U * safe_z + m / safe_z - 1j * gamma / (2 * np.pi) * np.log(safe_z)
return W.real, W.imag # (phi, psi)
def velocity_from_W(z, U=1.0, m=1.0, gamma=0.0):
"""dW/dz = u - iv. Take the conjugate to recover (u, v)."""
safe_z = np.where(np.abs(z) < 1e-9, 1e-9 + 0j, z)
dW = U - m / safe_z**2 - 1j * gamma / (2 * np.pi * safe_z)
return dW.real, -dW.imag
def cylinder_surface_pressure(theta, U=1.0, R=1.0, gamma=0.0):
"""Bernoulli surface pressure coefficient on the cylinder (with Γ)."""
u_t = -2 * U * np.sin(theta) - gamma / (2 * np.pi * R)
return 1 - (u_t / U) ** 2 # cp = 1 - (V/U)^2
def integrate_drag_lift(U=1.0, R=1.0, gamma=0.0, N=1024):
"""Inviscid surface-pressure integral → (drag, lift) / (rho U^2 R)."""
theta = np.linspace(0, 2 * np.pi, N, endpoint=False)
cp = cylinder_surface_pressure(theta, U, R, gamma)
drag = -np.trapz(cp * np.cos(theta), theta)
lift = -np.trapz(cp * np.sin(theta), theta)
return drag, lift
if __name__ == "__main__":
U, m, gamma = 1.0, 1.0, 0.0
R = np.sqrt(m / U)
drag0, lift0 = integrate_drag_lift(U, R, gamma=0)
drag1, lift1 = integrate_drag_lift(U, R, gamma=-3.0)
print(f"Γ=0 : drag = {drag0:+.4e}, lift = {lift0:+.4e}") # both ~ 0
print(f"Γ=-3.0 : drag = {drag1:+.4e}, lift = {lift1:+.4e}") # lift = -ρUΓ
x = np.linspace(-3, 3, 400)
y = np.linspace(-2, 2, 240)
X, Y = np.meshgrid(x, y)
Z = X + 1j * Y
Z_out = np.where(np.abs(Z) < R, np.nan, Z)
_, psi = cylinder_potential(Z_out, U, m, gamma=-3.0)
fig, ax = plt.subplots(figsize=(7, 4))
ax.contour(X, Y, psi, levels=24, linewidths=0.7)
ax.add_patch(plt.Circle((0, 0), R, color="white", ec="k"))
ax.set_aspect("equal"); plt.show()Run it and you see:
Γ=0 : drag = +1.7e-15, lift = -3.5e-16
Γ=-3.0 : drag = -2.0e-14, lift = +3.000e+00The drag is always zero. The lift locks onto exactly (with , that gives ). d'Alembert was right. What was wrong was the model.
Three things to remember#
- The two-term complex potential is the exact inviscid solution for a cylinder in a uniform stream — nobody drew a circle, yet a circle appears.
- The Cauchy–Riemann equations are not a mere differentiability condition; they are the reason and of a 2D irrotational flow come paired in a single holomorphic function.
- The "zero drag" answer is not a mistake. It is the honest portrait of a fluid without viscosity, and that portrait pointed the way to boundary-layer theory.
Share if you found it helpful.