CUDA-based discrete-adjoint CFD. A single-precision GPU finite-volume RANS/LES solver
with an exact discrete adjoint whose residual derivatives are produced by
cuad, a source-to-source automatic-differentiation compiler for CUDA. The adjoint drives
gradient-based shape optimization, inverse design, and field inversion (FIML)
directly from Python.
- Flow solver (GPU, FP32): structured multiblock, overset/chimera, AUSMPW+ with up to 5th-order MUSCL / eMLP-VC reconstruction, full viscous fluxes, Spalart–Allmaras (RANS) and SA-DDES/IDDES/Vreman (LES), implicit red-black / line (DADI) smoothing and pseudo-time multigrid, dual-time unsteady.
- Exact discrete adjoint: every amplified derivative in the gradient chain — the Jacobian
∂R/∂Q, the objective seed ∂J/∂Q, the mesh sensitivity ∂R/∂X and the wall-distance term
∂R/∂wd — is produced by
cuadAD (not finite difference). The ill-conditioned RANS adjoint amplifies any FD term, so exact derivatives are what make the shape gradient usable. cuad— CUDA AD compiler: Tapenade-style forward (tangent) and reverse (adjoint) mode via Clang LibTooling; handles arrays, structs, cross-call,atomicAdd, ternaries and 2-D array access.- Optimization front-end (Python): objectives
cd, cl, cf, cp, qwand design variables (FFD control points or the SA field β), driven by pyOptSparse. Objective and constraints are plain Python callables of the CFD functionals. - PETSc adjoint solve: the assembled block Jacobian is solved in double precision with GMRES + ILU (the only FP64 part; the flow stays FP32).
- NVIDIA GPU + CUDA toolkit (
nvcc), compute capability ≥ 8.0 (edit-arch=sm_86in theMakefilefor your card). - PETSc (real, optimized build) + an MPI (OpenMPI). Set the paths in the
Makefile(PETSC_DIR,PETSC_ARCH) or the environment. - Python 3 with
numpyand (for optimization)pyoptsparse— only needed for the optimizer. - (Only to regenerate the AD sources) Clang/LLVM dev libraries, to build
cuad.
make -j # -> ./cudaflowThe cuad-generated AD sources (src/cuad_*_d.cuh, src/cuad_*_b.cuh) are committed, so the
solver builds out of the box. To regenerate them after editing a cuad input:
cd cuad && ./build.sh # -> cuad/cuad (Clang LibTooling)
./cuad/cuad src/cuad_flux_src.cu > src/cuad_flux_d.cuh
./cuad/cuad -r src/cuad_flux_src.cu > src/cuad_flux_b.cuh
make./cudaflow case/plate_sk_bcm/plate_sk_lex.cfgA .cfg is key = value lines (grid, BC, Mach, AoA, scheme, turbulence, adjoint options, …).
Output is base64-binary VTK: volume *.vts/.vtm and a wall surface *_surf.vtp (Cp + Cf),
readable in ParaView.
| Case | Configuration | Result |
|---|---|---|
| Schubauer–Klebanoff flat plate | SA-BCM transition, M = 0.3, LE-refined grid | Skin friction matches the S&K transitional experiment |
| Axisymmetric Mach-7 SWBLI (Kussoy) | Spalart–Allmaras, Re = 57 000 / cm | Wall pressure / heating within ~1–2% of WIND-US SA |
| ONERA M6 transonic wing | M = 0.84, α = 3.06°, multiblock, eMLP-VC | λ-shock captured; Cp in line with CFL3D / FUN3D |
Schubauer–Klebanoff transitional flat plate — skin friction (case/plate_sk_bcm):
Kussoy Mach-7 axisymmetric SWBLI, 20° flare — wall pressure and heat transfer (case/aswbli_m7):
ONERA M6 transonic wing — chordwise Cp at seven span stations (case/onera_m6):
Each figure is regenerated from the case's .cf/solution output by the post_*.py script in
that directory.
Short freestream-preservation and restart-and-check solves, plus adjoint checks, compared to golden metrics:
python3 regression/run_regression.py # all tiers
python3 regression/run_regression.py --tier 0,1 # fast tiers (run on every change)
python3 regression/run_regression.py --listThe adjoint cases (adj_naca, fiml_naca) check the AD-Jacobian assembly consistency
||gJac·q − dR|| ≈ 3.6e-4, the PETSc adjoint convergence, and — for FIML — that the loss
actually descends. All grids for tiers 0–1 are included, so they run out of the box.
Enable the exact AD adjoint in the .cfg:
adjoint = 1 adj_ad = 1 adj_petsc = 1 mf_adjoint = 0 sens_p = 7
order = 3 limiter = none # scheme currently covered by the cuad extraction
- Shape optimization (drag min with a lift floor), from Python:
Design variables = FFD control-point displacements; objective/constraints are Python callables of
python3 opt/optimize_naca.py --grad adjoint --niter 8000 --nx 5 --ny 2 --cl-min 0.2
{cd, cl}; gradients come from the exact adjoint via theffd_evalhook. - Inverse design (match a target
cf/cp): dump a target withcf_target_out, then run withobjective = cf(orcp) andobj_ref = target.bin—ffd_evalreports the match functional and its exact-AD shape gradient. - Field inversion (FIML):
objective = cf,obj_ref = experiment.bin,fiml = <steps>— recovers the SA correction field β with the exact adjoint ψ each iteration.
src/ GPU solver + discrete adjoint (CUDA/C++) + committed cuad-generated AD sources
cuad/ CUDA source-to-source AD compiler (Clang LibTooling) + examples/tests
opt/ Python optimization front-end (fvopt.py) + worked example (optimize_naca.py)
regression/ regression harness (run_regression.py, metrics, golden, cases)
case/ example cases: plate_sk_bcm, aswbli_m7, onera_m6 (+ NACA grids)
tools/ plotting utilities
Large volume grids/solutions are not stored in the repo; regenerate example grids with the
gen*.py / convert*.py scripts in each case/ directory.
CuDAFLOW is released under the GNU General Public License v3.0 — see LICENSE.
Seunghyun Joo — M.S. student, Aerospace Vehicle Design Laboratory, Department of Aerospace Engineering, Seoul National University — chlrh45351@gmail.com
Questions, bug reports and collaboration proposals are welcome — please use the GitHub issue tracker for anything reproducible, or e-mail for the rest.


