Build a Navier-Stokes solver in Python from scratch

Most developers treat Computational Fluid Dynamics (CFD) as a black box. You hand a mesh to commercial software and wait for the colored plots to come back. The usual advice is “just use a library,” which is how people end up with slow, unstable simulations they cannot debug. If you want to understand how fluid motion is actually computed, build a Navier-Stokes Solver in Python yourself.

I have spent 14 years wrestling with complex systems, and one lesson keeps repeating: when a simulation breaks, restarting the software is not a fix, it is giving up. Translating the partial differential equations into discretized code is what lets you see the physics instead of guessing at it. So that is what the rest of this post does, in NumPy.

The physics: momentum and continuity

The Navier-Stokes equations are Newton’s second law applied to fluids. For incompressible flow, two things matter: momentum, which balances pressure against viscosity, and continuity, which keeps mass from appearing out of nowhere. Pressure and velocity are coupled, and that coupling is the hardest part of any Navier-Stokes Solver in Python.

The derivation is written out on Physics StackExchange, but the part that bites you in code is the Pressure-Poisson equation. It has to be solved at every timestep, or the velocity field stops being divergence free.

Discretization on a grid

On a computer this turns into a finite difference scheme over a uniform grid. Each term gets its own treatment:

  • Time: forward difference, explicit Euler.
  • Advection: backward or upwind difference, which is what keeps it stable.
  • Diffusion: central difference.

As I wrote in the post on Fast Python performance, raw Python loops are the bottleneck here. Everything below is vectorized with NumPy, otherwise the run crawls.

Implementing the solver logic

The loop always runs in the same order: build the source term, solve for pressure with Jacobi iteration, then update the velocity field. Vectorized, the source term calculation comes out like this.

# bbioon_calc_source_term
# rho: density, dt: time step, dx/dy: grid spacing
b[1:-1, 1:-1] = (rho * (
    1 / dt * ((u[1:-1, 2:] - u[1:-1, 0:-2]) / (2 * dx) +
              (v[2:, 1:-1] - v[0:-2, 1:-1]) / (2 * dy)) -
    ((u[1:-1, 2:] - u[1:-1, 0:-2]) / (2 * dx))**2 -
    2 * ((u[2:, 1:-1] - u[0:-2, 1:-1]) / (2 * dy) *
         (v[1:-1, 2:] - v[1:-1, 0:-2]) / (2 * dx)) -
    ((v[2:, 1:-1] - v[0:-2, 1:-1]) / (2 * dy))**2
))

With the source term in hand, the pressure gets iterated. People cut the Jacobi iteration count to save runtime, which is a mistake. Too few iterations and the pressure field never balances the velocity, so the fluid blows up numerically.

# bbioon_pressure_poisson
for _ in range(nit):
    pn = p.copy()
    p[1:-1, 1:-1] = (
        (pn[1:-1, 2:] + pn[1:-1, 0:-2]) * dy**2 +
        (pn[2:, 1:-1] + pn[0:-2, 1:-1]) * dx**2 -
        b[1:-1, 1:-1] * dx**2 * dy**2
    ) / (2 * (dx**2 + dy**2))
    # Gauge pressure boundary conditions
    p[:, -1] = 0; p[:, 0] = 0; p[-1, :] = 0; p[0, :] = 0

Simulating airflow around a wing

A wing is really just a mask. You mark a set of grid points as solid and force the velocity there to zero, which is the no-slip condition. Run this Navier-Stokes Solver in Python and the pressure field does what the textbook says it should: high underneath the wing, low above it, Bernoulli’s principle showing up on its own.

The solver is laminar, though, so it has no turbulence model. Push the Reynolds number too high and it starts to oscillate, then crashes. Worth knowing before you trust anything it prints.

If this kind of work is eating your dev hours, I can take it on. I have been working with WordPress and high-performance computation since the 4.x days.

Pragmatic takeaway

Writing a solver by hand pays off well past the coursework. You learn to handle ordering problems in your data and to manage large arrays with NumPy discretization. Mostly, the commercial tools stop looking like magic, and a failing simulation becomes something you can debug instead of restart.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.