Kovasznay Flow (steady Navier–Stokes)
Kovasznay (1948) found a rare closed-form solution of the full nonlinear incompressible Navier–Stokes equations — the laminar wake behind a 2-D grid — which makes it the canonical verification problem for an incompressible flow solver: there is an exact field to compare against. At \(Re=1/\nu=40\),
\[u = 1 - e^{\lambda x}\cos 2\pi y,\quad v = \tfrac{\lambda}{2\pi}e^{\lambda x}\sin 2\pi y,\quad
p = \tfrac12(1-e^{2\lambda x}),\quad \lambda = \tfrac{Re}{2}-\sqrt{\tfrac{Re^2}{4}+4\pi^2}.\]
The weak form
The convective term \((\mathbf{u}\cdot\nabla)\mathbf{u}\) — the unknown contracted with itself — makes
the form nonlinear, so jno.fem returns a coupled residual operator with an autodiff Jacobian,
solved by Newton on the inf–sup-stable Taylor–Hood pair (P2 velocity / P1 pressure):
conv = inner(gu, ub, n_contract=1) # (u.grad) u
momentum = inner(conv, vv, n_contract=1) + nu * inner(gu, gv, n_contract=2) - pp * trace(gv)
fem = jno.fem([momentum, -qq * trace(gu),
u(xb, yb)[0] - bx, u(xb, yb)[1] - by, # analytic velocity on the boundary
p.pin(p0)]) # gauge-fix pressure to the analytic value p0

What to notice
- The convective nonlinearity routes the whole system to the nonlinear coupled operator; the Jacobian is autodiffed and Newton converges from rest.
- The recovered velocity matches the analytic Kovasznay field to relative \(L^2 \approx 8\times10^{-5}\)
— the order of accuracy of this solve is certified in
tests/test_fem_convergence.py.
Full script
"""Kovasznay flow -- a closed-form steady Navier-Stokes benchmark via ``jno.fem``.
(u.grad)u - nu lap u + grad p = 0, div u = 0, Re = 1/nu = 40.
Kovasznay (1948) found a rare *exact* solution of the full nonlinear, incompressible Navier-Stokes
equations -- the laminar wake behind a 2D grid:
u = 1 - e^{lam x} cos(2 pi y), v = (lam/2pi) e^{lam x} sin(2 pi y), p = (1 - e^{2 lam x})/2,
lam = Re/2 - sqrt(Re^2/4 + 4 pi^2).
This makes it the canonical *verification* problem for an incompressible NS solver: there is a true
field to compare against. The convective term ``(u.grad)u`` -- the unknown contracted with itself --
is a genuine nonlinearity, so ``jno.fem`` routes the inf-sup-stable Taylor-Hood (P2 velocity / P1
pressure) system to its coupled nonlinear operator and the Jacobian comes from autodiff. We solve it
with a Newton root-find from rest, and check the recovered velocity against the analytic field.
Reference: L. I. G. Kovasznay, "Laminar flow behind a two-dimensional grid", Proc. Camb. Phil. Soc.
44(1), 58-62 (1948).
"""
import os
os.environ["MPLBACKEND"] = "Agg"
import jax
jax.config.update("jax_enable_x64", True) # the assembler builds in float64
from pathlib import Path # noqa: E402
import matplotlib.pyplot as plt # noqa: E402
import numpy as np # noqa: E402
from scipy.interpolate import griddata # noqa: E402
from shapely.geometry import box # noqa: E402
import jno # noqa: E402
inner, grad, trace = jno.np.inner, jno.np.grad, jno.np.trace
dense = lambda A: np.asarray(A.todense()) if hasattr(A, "todense") else np.asarray(A) # noqa: E731
nu = 0.025 # Re = 1/nu = 40
Re = 1.0 / nu
lam = Re / 2 - np.sqrt(Re**2 / 4 + 4 * np.pi**2)
ue = lambda x, y: 1.0 - np.exp(lam * x) * np.cos(2 * np.pi * y) # noqa: E731 analytic velocity
ve = lambda x, y: lam / (2 * np.pi) * np.exp(lam * x) * np.sin(2 * np.pi * y) # noqa: E731
pe = lambda x, y: 0.5 * (1.0 - np.exp(2 * lam * x)) # noqa: E731
x0, y0 = -0.5, -0.5 # the channel [-0.5, 1] x [-0.5, 1.5]
d = jno.domain(box(x0, y0, 1.0, 1.5), mesh_size=0.08)
u, v = d.fem_symbols(value_shape=(2,), names=("u", "v"), order=2) # P2 velocity
p, q = d.fem_symbols(names=("p", "q"), order=1) # P1 pressure (inf-sup stable)
xi, yi, _ = d.variable("interior", split=True)
xb, yb, _ = d.variable("boundary", split=True)
ub = u.bind(x=xi, y=yi)
gu, gv = grad(u, [xi, yi]), grad(v, [xi, yi])
pp, qq, vv = p.bind(x=xi, y=yi), q.bind(x=xi, y=yi), v.bind(x=xi, y=yi)
conv = inner(gu, ub, n_contract=1) # (u.grad)u -- the convective nonlinearity
momentum = inner(conv, vv, n_contract=1) + nu * inner(gu, gv, n_contract=2) - pp * trace(gv)
bx = 1.0 - jno.np.exp(lam * xb) * jno.np.cos(2 * np.pi * yb) # analytic velocity on the boundary
by = lam / (2 * np.pi) * jno.np.exp(lam * xb) * jno.np.sin(2 * np.pi * yb)
fem = jno.fem(
[
momentum,
-qq * trace(gu), # incompressibility
u(xb, yb)[0] - bx, # Dirichlet velocity = the analytic field
u(xb, yb)[1] - by,
p.pin(float(pe(x0, y0))), # gauge-fix to the analytic value (min-corner) so recovered p == p* exactly
]
)
assert not fem.is_linear and not fem.is_transient, "Kovasznay is a steady nonlinear system"
# Newton from rest on the assembled residual / autodiff Jacobian (direct linear solve per step)
w = np.zeros(fem.dofs)
for _ in range(15):
G = np.asarray(fem.residual(w))
dw = np.linalg.solve(dense(fem.jacobian(w)), -G)
w = w + dw
if float(np.linalg.norm(dw)) < 1e-10:
break
resid = float(np.linalg.norm(np.asarray(fem.residual(w))))
off = fem.offsets
pts_v = np.asarray(fem.field_points[0])
uu = w[off[0] : off[1]].reshape(-1, 2)
u_ex = np.stack([ue(pts_v[:, 0], pts_v[:, 1]), ve(pts_v[:, 0], pts_v[:, 1])], axis=-1)
rel = float(np.linalg.norm(uu - u_ex) / np.linalg.norm(u_ex))
print("\nKovasznay flow (steady Navier-Stokes, Taylor-Hood P2/P1)")
print(f" Re={Re:.0f} dofs={fem.dofs} Newton |residual|={resid:.1e}")
print(f" recovered velocity vs analytic Kovasznay field: rel-L2 = {rel:.2e}")
# ---- render the computed flow: speed + streamlines (the actual FEM solution) ----
gx, gy = np.meshgrid(np.linspace(x0, 1.0, 220), np.linspace(y0, 1.5, 220))
GU = griddata(pts_v, uu[:, 0], (gx, gy), method="cubic")
GV = griddata(pts_v, uu[:, 1], (gx, gy), method="cubic")
fig, ax = plt.subplots(figsize=(5.4, 4.6))
im = ax.imshow(np.hypot(GU, GV), origin="lower", extent=(x0, 1.0, y0, 1.5), cmap="cividis", aspect="auto")
ax.streamplot(gx, gy, GU, GV, color="white", density=1.1, linewidth=0.6, arrowsize=0.7)
fig.colorbar(im, ax=ax, shrink=0.85, label=r"$|\mathbf{u}|$")
ax.set_title(f"Kovasznay flow (Re={Re:.0f}) — computed speed & streamlines")
ax.set_xticks([])
ax.set_yticks([])
fig.savefig(Path(__file__).parents[2] / "assets" / "kovasznay_flow_2d.png", dpi=130, bbox_inches="tight")
assert resid < 1e-7 and rel < 1e-3, f"Kovasznay velocity not recovered: rel-L2 {rel:.2e}, residual {resid:.1e}"