Skip to content
Jad El Badaoui

CFD · Numerical methods · C++ · Summer 2026

NACA 0012 CFD: From Commercial CFD to a C++ Solver From First Principles

The same aerofoil solved twice. First in ANSYS Fluent and validated against NASA measurements, then with a solver written from scratch in C++, because the first study kept raising the question of what the commercial code was doing between the mesh and the answer.

Velocity magnitude around a NACA 0012 aerofoil at 10 degrees incidence computed by the custom solver, showing the stagnation region below the leading edge, acceleration over the suction surface and the wake deficit behind the trailing edge
The final result: velocity magnitude at 10° incidence and Re = 6×10⁶, from the k-omega SST solver with limited second-order convection. Everything producing this picture, from the aerofoil coordinates to the turbulence model, was written for this project.
3.6 %
Lift against NASA SST

Cl = 1.119 against a three-code NASA mean of 1.079 at 10° and Re = 6×10⁶

539 → 5
Peak cell aspect ratio

The first O-grid was measured, diagnosed and rebuilt rather than used

2.3×10⁻⁷
Lift at zero incidence

Symmetry check on the turbulent solution, where the exact answer is zero

y⁺ ≈ 1
Resolved to the wall

Mean y⁺ of 1.07 in the converged solution, with no wall functions anywhere

01 Overview

The same aerofoil, solved twice

One aerofoil, a NACA 0012 at 10° incidence and a chord Reynolds number of 6×10⁶, and two goes at it. The first was in ANSYS Fluent, validated against NASA surface-pressure data, and it answered whether the result could be trusted. The second was a solver written from scratch, and it exists because the first study kept raising a question it had no way of answering: what was happening between the mesh and the answer.

That precursor is the short section immediately below. The rest of the page is the solver. It implements the numerical machinery a commercial code keeps out of sight: the finite-volume discretisation, the pressure-velocity coupling that makes incompressible flow solvable at all, the correction that keeps a skewed mesh honest, and a two-equation turbulence model with its near-wall treatment. There is no numerical library underneath it. The linear systems are solved by a Gauss-Seidel sweep with over-relaxation written for this project, and no discretisation, no boundary condition and no turbulence closure comes from a library.

The discipline that makes it worth showing is not that it runs. It is that every stage was checked against a problem whose answer was already known, before being trusted on a problem whose answer was not. At the end the two solutions meet: the same section, the same incidence, the same Reynolds number, one answer from a commercial code and one from a few thousand lines of C++.

  1. Aerofoil geometry

    200 surface nodes

    NACA 0012 thickness distribution evaluated analytically, cosine-spaced, closed trailing edge

  2. Structured O-grid

    Quality metrics

    Algebraic generator with surface-normal launch, Bezier radial lines and Laplacian smoothing

  3. Diffusion operator

    p → 2

    Laplace solver verified against an analytical annulus solution for observed order of accuracy

  4. Convection schemes

    p → 2 and 1

    Central differencing and first-order upwind measured against a one-dimensional analytical solution

  5. SIMPLE coupling

    Ghia et al.

    Built on a staggered lid-driven cavity and validated against the Ghia benchmark before going near an aerofoil

  6. Collocated solver

    Re = 100

    Operators generalised to the body-fitted grid with Rhie-Chow interpolation and non-orthogonal correction

  7. k-omega SST

    Re = 6×10⁶

    Two-equation RANS closure on a wall-resolved mesh designed for y⁺ ≈ 1

  8. Second-order convection

    Cl and Cd

    Limited linear-upwind reconstruction applied as a deferred correction, then compared against NASA reference solutions

02 Precursor

The study this came from

Before any of the code below existed, this aerofoil was solved in ANSYS 2026 R1: two-dimensional steady RANS, 10° incidence, chord Reynolds number 6×10⁶, on a mesh of roughly 27,000 cells with ten inflation layers and a sphere of influence around the section. The case was chosen because high-quality NASA experimental data exists for it, so the argument could be carried to the end instead of stopping at plausibility.

It worked. The computed surface pressure distribution overlaps the NASA data across the full chord, and the integrated lift came out at CL ≈ 1.06 against a measured 1.07 to 1.08, about 1.4 % low, having been predicted at 1.097 by a thin-aerofoil hand calculation done before the solver was opened. Matching the whole Cp distribution is the stronger claim: an integrated coefficient can agree through two errors cancelling, a point-by-point match across the chord cannot.

Velocity magnitude

Far-field velocity matches the specified free stream, which is the fastest check that the boundary conditions went on as intended. Flow accelerates to nearly twice free-stream around the upper leading edge.

Static pressure

Higher pressure below, lower above, and that difference is the lift. Pressure changes very little across the thin boundary layer, which is why the pressure field is comparatively forgiving of near-wall mesh quality.
The NASA experimental upper-surface Cp used for validation, plotted with the inverted axis convention that puts stronger suction higher on the page.

Validation was against the NASA NACA 0012 resources at matched Reynolds number and incidence: Gregory and O’Reilly for surface pressure, Ladson for force coefficients.

Verification came first and separately. Mass imbalance settled at order 10⁻⁷ of the incoming flow, residuals reached about 10⁻⁶ with flat force monitors rather than residuals alone, and grid and domain independence were set out as a controlled six-case matrix using Richardson extrapolation and the grid convergence index.

That matrix is a plan rather than a result. It was specified and not completed, so the numerical uncertainty on the lift coefficient was never quantified, and the write-up said so.

The rest of the ANSYS study

The chain the study followed. The dashed returns matter as much as the forward path: verification failures send you back to the mesh, validation failures back to the physics.
Two different questions, routinely conflated. Verification is answered from inside the simulation; validation only against independent measurement.
The domain: velocity inlet, pressure outlet, no-slip surfaces, far field at roughly 12.5 chords. That outer boundary is a numerical stand-in for infinity, and its adequacy has to be shown rather than assumed.
The viscous law and the log law in wall units, with the band standard wall functions require. This is the plot that sets the first-cell height, and the one the y⁺ audit was measured against.
Turbulent kinetic energy, the most diagnostic of the four fields and the one most often skipped. It doubles as a visual mesh check: on an adequate near-wall mesh the sheet is sharp, on a coarse one the peak smears.
Flow turning around the leading edge, displaced toward the lower surface by the positive incidence.

So the study answered its own question: the result is trustworthy for lift and not for drag, and it can say which is which. What it could not answer is what happened between the mesh and the answer. Choosing a second-order scheme, a coupled solver or a y⁺ target was still selecting from a menu. Everything below is what came of writing the thing each of those options switches on.

03 Numerical method

What the solver actually does

The solver is cell-centred and collocated: velocity components and pressure all live at cell centres. Every transport equation is integrated over a control volume and converted by the divergence theorem into a sum of fluxes through faces, so the discrete equations conserve mass and momentum by construction rather than by accident.

Σf (ρ uf · Sf) φf − Σf Γf (∇φ)f · Sf = Σf Sφ VP

Convection and diffusion of any quantity φ become sums over the faces of a cell. Everything after this point is a choice about how to evaluate φ and its gradient at a face.

Sf = Ef + Tf

The face area vector is split into a part aligned with the line joining the two cell centres, treated implicitly, and a remainder handling the skewness, treated explicitly. Without this the solver silently mis-computes diffusion on any mesh that is not orthogonal.

μt = ρ a1 k / max(a1 ω, S F2)

The SST eddy viscosity. The limiter in the denominator is what stops the model over-predicting turbulent stress in strong adverse pressure gradients, which is the reason SST is used for aerofoils in the first place.
Why pressure needs its own equation
In incompressible flow there is no equation for pressure: it appears in the momentum equations but never in continuity. SIMPLE resolves this by predicting a velocity field from a guessed pressure, deriving a pressure correction that forces the face fluxes to conserve mass, then correcting both. The solver iterates that loop with under-relaxation until continuity holds to a set tolerance.
Why Rhie-Chow is unavoidable here
Storing velocity and pressure at the same location decouples them on a collocated grid: a checkerboard pressure field produces no force and the solver has no way to reject it. Rhie-Chow interpolation builds the face velocity from a momentum-weighted average that reintroduces the pressure difference between adjacent cells, which suppresses the mode.
Wall treatment
The turbulent cases resolve the boundary layer rather than modelling it. Turbulent kinetic energy is set to zero at the wall and the specific dissipation rate is imposed from the near-wall asymptote ω = 60ν / (β₁ y²). This requires the first cell centre to sit at y⁺ of order one, which is what the high-Reynolds-number mesh was designed to deliver.
Convection, both ways
The turbulent case is solved twice. First with first-order upwind, which is unconditionally bounded and robust but strongly diffusive. Then with a limited linear-upwind reconstruction added as a deferred correction, keeping the robust operator in the matrix and moving the higher-order contribution to the source term, ramped in gradually.

04 Grid generation

The mesh that had to be rebuilt

The aerofoil is generated analytically from the NACA 0012 thickness equation with the closed-trailing-edge coefficient, distributed with cosine spacing so that points cluster where the curvature is: the leading edge and the trailing edge.

The section and its discretisation. Cosine spacing is not cosmetic: uniform spacing puts the same number of points on the flat mid-chord as on the leading edge, where the surface turns through ninety degrees in a few percent of chord.

The first O-grid ran. It produced a converged Laplace solution and had no inverted cells, which is exactly the situation in which a mesh gets used without further thought. Because the generator also computed quality metrics, it was measured instead, and the measurement was bad enough to send the generator back for a rewrite.

First attempt

The first generator interpolated radially from the surface to a circular far field. Near the trailing edge the grid lines fold together and the cells become both extremely stretched and strongly skewed.

After the redesign

The rebuilt generator launches grid lines along the true surface normal, distributes the far field uniformly in angle, joins the two with cubic Bezier curves and finishes with Laplacian smoothing.
Peak aspect ratio
539.3 → 5.0

A hundredfold reduction in the worst cell

Peak non-orthogonality
81.7° → 29.7°

Below the threshold where explicit correction can cope

Face-vector closure
5.6×10⁻¹⁷

Cell faces sum to zero at machine precision

Both meshes carry 200 × 79 cells. The improvement is entirely in how the points are distributed, not how many there are.
The whole population moves, not just the worst cell. Quoting a maximum alone can hide a mesh that is bad everywhere or bad in one place, and those need different fixes.
Leading edge of the rebuilt mesh. Cells meet the surface close to perpendicular, which is what keeps the diffusion term well conditioned.

05 Verification

Checking the operators before trusting them

Verification asks a question that has nothing to do with aerodynamics: given these equations, does the code solve them correctly, and at the rate the discretisation promises? It is answered against problems with closed-form solutions, where any discrepancy is the code's fault rather than the physics model's.

Steady diffusion between two concentric circles has the exact solution φ(r) = ln(R/r) / ln(R/a). Refining the grid four times drives the measured order of accuracy to 1.9988, confirming the diffusion operator is genuinely second order rather than merely convergent.
Central differencing lands on the second-order slope; first-order upwind climbs to 0.95 and stops. The inset shows where upwind's error lives: concentrated in the boundary layer, roughly twenty times larger than central differencing on the same grid. This measurement is what the turbulent scheme comparison later cashes in.

Once the operators were generalised to the body-fitted grid, the geometric machinery was tested on its own terms: face ownership and connectivity, cell areas, the closure of face vectors around every cell, mass conservation under a uniform flow imposed exactly, and the Green-Gauss gradient of a linear pressure field with a known answer.

Uniform-flow mass residual
1.1×10⁻¹⁶

31,400 interior faces, 200 wall, 200 far field

Linear pressure-gradient error
2.3×10⁻¹²

Against the exact ∇p = (2, −3)

Invalid or collapsed cells
0

Every cell closes and carries the expected face count

Geometric and operator tests on the 15,800-cell body-fitted grid. These are the checks that should return machine zero, so anything larger is a bug rather than a discretisation error.
Laplace's equation solved on the aerofoil grid with the surface held at one and the far field at zero.

The same diffusion operator was then run on the aerofoil grid itself, where no analytical solution exists. What can still be checked is conservation: whatever diffuses out of the wall must arrive at the far field.

Integrating the flux over each boundary gives 1.7904 leaving the wall and 1.7904 arriving at the far field, a net imbalance of 8.2×10⁻⁷, or a relative imbalance of 4.6×10⁻⁷. A discretisation that leaked would show up here even though the exact field is unknown.

06 Validation

The cavity before the aerofoil

SIMPLE was not written directly onto the aerofoil grid. It was built first on the lid-driven cavity, a square box with a sliding top, on a staggered Cartesian arrangement where the pressure-velocity coupling is simplest and the failure modes are best understood. The cavity at Re = 100 has been a reference case since Ghia, Ghia and Shin published tabulated centreline velocities in 1982.

Velocity magnitude and streamlines at Re = 100. The two secondary corner vortices are the sensitive part: they are weak, and a solver with a broken pressure correction loses them entirely.
Centreline velocities against Ghia. At 25 × 25 the solver visibly under-predicts the velocity extrema; by 100 × 100 the curves pass through the benchmark points.
Benchmark error against grid spacing, with a first-order reference slope.

Agreement on one grid is weak evidence: a solver can match a benchmark at a single resolution by luck or by compensating errors. What is worth more is the way the error behaves under refinement.

Root-mean-square error against the benchmark falls from 0.0157 to 0.0078 to 0.0033 on the u profile as the grid doubles twice, which is close to first order.

That rate is itself a check. The cavity solver uses first-order upwind convection, and the one-dimensional verification measured that scheme at order 0.95. The validation error is falling at the rate the verification predicted, which ties the two exercises together rather than leaving them as separate claims.

Verification

Am I solving the equations correctly?

  • Diffusion operator measured at order 1.9988 against an analytical annulus
  • Convection schemes measured at order 2.00 and 0.95 against a one-dimensional exact solution
  • Face-vector closure and uniform-flow conservation at machine precision
  • Green-Gauss gradient of a linear field correct to 2.3×10⁻¹²
  • Boundary flux balance closed to 4.6×10⁻⁷ on the aerofoil grid

Validation

Do the equations describe the real flow?

  • Lid-driven cavity centreline velocities against Ghia et al. (1982) on three grids
  • Zero lift recovered at zero incidence on a symmetric section, to 2.3×10⁻⁷
  • Turbulent lift and drag against three independent NASA reference codes

The numerical operators are verified in the strict sense: measured orders of accuracy, not just converged residuals. Validation is real but partial. The cavity and the symmetry check are solid; the turbulent aerofoil case is compared against reference CFD rather than experiment, on a single grid, and with freestream turbulence settings that do not match the reference. It is a credible solver, not a validated one.

07 Development

A laminar case to develop against

Before turbulence, the collocated solver was exercised on the aerofoil at Re = 100, where the flow is steady and laminar and the equations being solved are unambiguous. This is where Rhie-Chow interpolation and the non-orthogonal correction were shaken out.

An earlier attempt to run the momentum equations on their own, with pressure frozen, is worth recording because of how it failed. Without a pressure correction enforcing continuity there is nothing to stop the velocity field growing without bound, and the run diverged to non-finite values in every one of the 15,800 cells. That is the correct behaviour for an incompressible momentum predictor with no coupling, and it is the most direct demonstration of why SIMPLE exists.

Velocity magnitude at Re = 100. The viscous region is a large fraction of the chord, which is what makes this a useful development case: everything is smooth and well resolved.
Pressure coefficient over the same solution, with the scale centred on the free-stream value.
What the non-orthogonal correction is worth. With a peak skewness near 30°, ignoring it shifts the pressure lift by 1.5 % and the pressure drag by 2.2 %. Small, but it is a bias rather than noise, and it does not go away with iteration.
Three meshes at Re = 100. Successive changes shrink, with the medium to fine step moving lift by 1.7 % and drag by 1.4 %. Not a formal grid convergence index study, but enough to show the solution settling rather than wandering.

08 Turbulence

Resolving the wall at Re = 6×10⁶

Moving to a chord Reynolds number of six million changes the problem entirely. The boundary layer becomes a thin turbulent sheet, and the solver has to gain a turbulence model and a mesh capable of resolving that sheet without wall functions.

Flow conditions

Reynolds number
6.0×10⁶
Incidence
10°
Formulation
Incompressible, non-dimensional
Turbulence model
k-omega SST, fully turbulent

Wall-resolved mesh

Nodes
240 × 180
Cells
42,960
First node spacing
9.23×10⁻⁶ c
Wall-normal growth ratio
1.064
The first cell height was sized from a skin-friction estimate to put the first cell centre at y⁺ ≈ 1 before the mesh was built, rather than discovered afterwards.

A wall-resolved mesh is not produced by refining until it looks fine. The first cell height is computed from an estimate of the wall shear stress, which sets the viscous length scale, which sets the spacing needed to place the first cell centre inside the viscous sublayer.

That calculation gave a first node spacing of 9.23×10⁻⁶ chords, grown outward at a ratio of 1.064. The consequence is cells with edge aspect ratios in the hundreds, which would be a defect on the laminar grid and is the intended design here: the boundary layer varies violently normal to the wall and hardly at all along it, so the cells should be stretched to match.

The wall-resolved mesh. The near-wall clustering is dense enough that individual layers are not separable at this scale.
The same mesh at the leading edge, on axes spanning a few thousandths of a chord. This is the resolution the SST wall treatment requires.

Before running the case of interest, the turbulent solver was run at zero incidence. A symmetric section at zero angle of attack must produce exactly zero lift, so any lift the solver reports is pure numerical and modelling error, with no physics hiding it. It is the cheapest severe test available for this configuration.

Zero incidence. Upper and lower surface pressures are indistinguishable, and the integrated lift comes out at −2.3×10⁻⁷. The measured y⁺ in the converged solution has a mean of 1.07, confirming the mesh design worked in the solution rather than only in the estimate.
Pressure coefficient at 10°. Stagnation below the leading edge, strong suction above it, and recovery towards the trailing edge.
Turbulent viscosity ratio. The model raises the effective viscosity by up to 750 times, confined to the boundary layer and the wake. The radial banding downstream is genuine under-resolution of the far wake by the O-grid.
Wall quantities for both convection schemes. y⁺ stays close to one over almost the whole surface, rising only at the leading edge where the shear stress peaks.

09 Discretisation study

What the convection scheme costs

The turbulent case was solved twice on the same mesh with the same turbulence model, changing only how convected quantities are evaluated at faces. The first-order upwind solution converged to every tolerance and looks entirely reasonable. It is also wrong in a way that only a comparison exposes.

Where the difference lives. The first-order scheme smears the leading-edge suction peak from about −5.7 to −4.4 and depresses suction over the whole upper surface. That missing area under the curve is the missing lift.
QuantityFirst-order upwindLimited second-order
Lift coefficient Cl1.00641.1187
Drag coefficient Cd0.047310.01326
Pressure drag Cd (pressure)0.042720.00733
Viscous drag Cd (viscous)0.004590.00593
Peak turbulent viscosity ratio1438747
Same mesh, same turbulence model, same convergence criteria. Only the face interpolation changes.

The pressure drag falls by a factor of nearly six. That number is the numerical diffusion of the first-order scheme made visible: smearing gradients across cells thickens the effective boundary layer, weakens the suction peak and leaves a pressure imbalance that is a discretisation artefact rather than aerodynamics.

The peak turbulent viscosity ratio tells the same story from the model's side. The first-order solution carries almost twice the peak eddy viscosity of the second-order one. Numerical diffusion thickened the shear layer, the turbulence model saw the thickened profile and responded by producing more turbulence, and the two errors reinforced each other.

10 Comparison

Against the NASA reference solutions

The NASA Turbulence Modeling Resource publishes this exact case, a NACA 0012 at 10° and Re = 6×10⁶, solved with the SST model by three independent codes on a common 897 × 257 grid. Because the three agree closely with each other, they form a usable reference band.

Both schemes against the three NASA reference codes. Second-order convection moves lift from 6.8 % low to 3.6 % high, and drag from nearly four times the reference to 6.4 % above it.
SourceClCd
This solver, second order1.11870.01326
CFL3D1.07780.01236
FUN3D1.08400.01253
NTS1.07650.01251
Deviation from the three-code mean+3.6 %+6.4 %
NASA values from the Turbulence Modeling Resource SST validation page, 897 × 257 grid.

11 Limitations

What this solver is not

  • Two-dimensional and incompressible. No compressibility, no three-dimensional effects, and no route to either without substantial rework.
  • No transition model. The boundary layer is turbulent from the leading edge, which matches the NASA case intent but rules out any case where transition location matters.
  • The second-order result is not fully converged. Continuity met its tolerance; the momentum and turbulence increments did not before the iteration cap.
  • One grid at Re = 6×10⁶. Grid convergence was demonstrated at Re = 100 and not repeated for the turbulent case, so the discretisation uncertainty on the headline coefficients is unquantified.
  • An O-grid, not a C-grid. Production aerofoil meshes use a C-topology precisely because an O-grid under-resolves the wake, which is visible in the turbulent viscosity field.
  • Reference conditions do not match. Compressibility, freestream turbulence and grid density all differ from the NASA solutions being compared against.
  • A custom implementation, not a production code. It reproduces published behaviour on the cases tested here. That is a long way short of the validation coverage behind any code used for engineering decisions.

12 Reflection

What building it changed

  • Convergence is not correctness. The first-order turbulent solution met every tolerance and over-predicted drag by nearly a factor of four. Residuals measure whether iteration has stopped moving, not whether the answer is right.
  • Mesh quality is a physics decision. A mesh that runs cleanly can still bias the answer, and the only way to know is to measure it before solving rather than after.
  • Discretisation order shows up in drag first. Lift is an integrated pressure difference and forgiving; drag depends on gradients and exposes numerical diffusion immediately.
  • Canonical problems earn their keep. The annulus, the one-dimensional profile and the cavity each cost little and each caught something. The measured upwind order later explained the cavity's convergence rate and predicted the turbulent drag error.
  • Commercial CFD reads differently now. Choosing a second-order scheme, a coupled solver or a y⁺ target in Fluent is no longer selecting from a menu, because I have written the thing each option switches on.

The complete write-up lives in the repository: methods, data, code, provenance and limitations.

github.com/Jadbadawi/naca-cfd-solver