Skip to main content

Python tools for 2D airfoil Openfoam Simulation

Project description

AirfoilRANSSolver

Python tooling that automates the full pipeline for running 2D airfoil RANS simulations in OpenFOAM — from airfoil geometry and mesh generation, through case setup and parallel solving, to post-processing of force coefficients and surface distributions.

Given a Reynolds number (and, for compressible cases, a Mach number) plus a set of geometry and mesh parameters, a single function call will:

  1. Generate the airfoil geometry and mesh (via the companion airfoilgmesher package).
  2. Copy a solver-specific OpenFOAM case template.
  3. Compute and patch the freestream and turbulence initial/boundary values from Re (and Mach).
  4. Run OpenFOAM pre-processing (gmshToFoam, boundary-patch typing, checkMesh, potentialFoam, decomposePar).
  5. Launch the solver in parallel with MPI.
  6. Monitor the aerodynamic force coefficients and stop the solver automatically once Cl/Cd are statistically stationary (the coefficient stopping criterion).
  7. Reconstruct the latest time and export Cl/Cd, the surface Cp/Cf distributions, the coefficient history, and convergence plots.
pip install airfoilranssolver

Solvers & validation status

The package ships several launchers, one per OpenFOAM solver. Each is a thin, self-contained module under airfoilranssolver/.

Launcher (module → function) Solver Regime Turbulence Stopping Status
launchers_simplefoamlaunch_simplefoam simpleFoam Incompressible, steady Spalart–Allmaras Coefficient criterion Validated
launchers_simplefoam_uranslaunch_simplefoam_urans simpleFoampimpleFoam Steady → URANS (incompressible) Spalart–Allmaras Steady: fixed-point or 3× limit cycle; URANS: mean-stationary + cap 🧪 Experimental
launchers_no_coefficient_controllaunch_pimplefoam pimpleFoam Incompressible, (pseudo-)transient k‑ω SST Fixed iteration count (Allrun) Validated
launchers_pimplefoamlaunch_pimplefoam pimpleFoam Incompressible, (pseudo-)transient k‑ω SST Coefficient criterion 🧪 Experimental
launchers_rhosimplefoamlaunch_rhosimplefoam rhoSimpleFoam Compressible / transonic, steady k‑ω SST Coefficient criterion 🧪 Experimental
launchers_rhopimplefoamlaunch_rho_pimplefoam rhoPimpleFoam Compressible / transonic, transient k‑ω SST Coefficient criterion 🧪 Experimental

The incompressible pipelines are validated: launch_simplefoam (including the automatic coefficient stopping criterion) and the launch_pimplefoam from launchers_no_coefficient_control (which runs a fixed number of iterations to completion). The compressible/transonic launchers are under active development.

The package has no top-level exports — import directly from the launcher module you need, e.g. from airfoilranssolver.launchers_simplefoam import launch_simplefoam.


Requirements

  • OpenFOAM (ESI / openfoam.com; templates target v2312, dictionaries are v2006-compatible) sourced in your shell, so that simpleFoam, pimpleFoam, gmshToFoam, checkMesh, potentialFoam, decomposePar, reconstructPar, foamDictionary and $WM_PROJECT_DIR are available.
  • MPI (mpirun) for parallel runs.
  • Python ≥ 3.9 with the dependencies below (installed automatically by pip): numpy, scipy, pandas, matplotlib, gmsh, fluidfoam, and airfoilgmesher (geometry + mesh).

How the pipeline works

The coefficient-controlled launchers (launch_simplefoam, launch_pimplefoam, launch_rhosimplefoam, launch_rho_pimplefoam) split execution into two phases so that Python — not OpenFOAM — owns the solver loop:

Phase 1 — Allrun_pre (pre-processing only). Converts the Gmsh mesh (gmshToFoam), rewrites the boundary patch types (e.g. airfoil → wall, inlet/outlet → patch, front/back → empty), runs checkMesh and potentialFoam, then decomposePar into n_subdomains. It does not run the solver or reconstructPar.

Phase 2 — monitored solve. Python launches mpirun -np <n_subdomains> <solver> -parallel, and every check_every_seconds reads postProcessing/forces/0/coefficient.dat. Once the convergence test passes (or max_wall_seconds is hit), it cleanly terminates the whole MPI process group, runs reconstructPar -latestTime, and post-processes.

The launchers_no_coefficient_control variant instead runs the template's full Allrun script (pre-processing + solve + reconstruct) to a fixed endTime, then post-processes the result.

The coefficient stopping criterion

Implemented in airfoilranssolver/utils.py::force_coefficients_converged. Over the last window samples of the force history, the run is declared converged when all of the following hold:

  • at least min_samples samples are available;
  • the peak-to-peak ranges satisfy Cl_range < cl_range_tol and Cd_range < cd_range_tol;
  • the relative standard deviations satisfy Cl_rel_std < cl_rel_std_tol and Cd_rel_std < cd_rel_std_tol.

Defaults: min_samples=5000, window=5000, cl_range_tol=2e-4, cd_range_tol=2e-5, cl_rel_std_tol=2e-4, cd_rel_std_tol=5e-4. Looser tolerances are appropriate for noisier multi-element cases (see the 30P30N benchmark).


Quick start

Incompressible steady — simpleFoam (validated, coefficient-controlled)

from airfoilranssolver.launchers_simplefoam import launch_simplefoam

geometry_parameters = {
    "airfoil_type": "NACA",
    "m": 0.0,    # max camber
    "p": 0.0,    # max-camber location
    "t": 0.12,   # max thickness (NACA 0012)
    "N": 400,    # surface points
}

mesh_parameters = {
    "type": "Structured",
    "xmax": 20, "xmin": -10, "ymax": 10,
    "alpha": 10,                 # angle of attack [deg]
    "Inflation-y": 2, "Inflation-x": 2,
    "airfoil_divisions": 200, "wake_divisions": 200,
    "vertical_divisions": 150, "front_divisions": 150,
    "horizontal_boundary_divisions": 75, "vertical_boundary_divisions": 50,
    "airfoil_progression": 0.01, "front_displacement": -0.35,
}

result = launch_simplefoam(
    "cases/naca0012_re6e6_aoa10",
    Re=6e6,
    geometry_parameters=geometry_parameters,
    mesh_parameters=mesh_parameters,
    refinement_parameters=["LE", "TE"],   # refine leading/trailing edges
    n_subdomains=20,                       # MPI ranks
)

print(result["Cl"], result["Cd"])

Incompressible — pimpleFoam, fixed iterations (validated)

from airfoilranssolver.launchers_no_coefficient_control import launch_pimplefoam

geometry_parameters = {"airfoil_type": "NACA", "m": 0.0, "p": 0.0, "t": 0.12, "N": 400}

mesh_parameters = {
    "xmax": 10, "xmin": -5, "ymax": 7,
    "alpha": 10,
    "BL_t": 0.02, "Inflation-y": 1.3, "Inflation-x": 1.5, "r_BL": 1.2,
    "AR_target": 0.08, "boundary_ratio": 0.2, "infl_ratio": 2.5,
}

launch_pimplefoam(
    "cases/naca0012_re6e6_aoa10/",
    Re=6e6,
    geometry_parameters=geometry_parameters,
    mesh_parameters=mesh_parameters,
    refinement_parameters=["LE", "TE"],
    n_subdomains=48,
)

Steady → URANS restart (launch_simplefoam_urans)

For cases where the steady solver never reaches a true fixed point (Cl/Cd settle into a periodic limit cycle), this launcher runs a two-phase scheme:

  1. Steady simpleFoam until either the Cl/Cd stabilization criterion is met or a repeating limit cycle is detected (the same cycle repeated n_cycles times, default 3) — whichever comes first.
  2. URANS — the same case is continued in time-accurate transient mode (pimpleFoam, still Spalart–Allmaras, adaptive maxCo time step) starting from the last steady iteration. It stops once the running time-averaged Cl/Cd become stationary, bounded by urans_max_time.
from airfoilranssolver.launchers_simplefoam_urans import launch_simplefoam_urans

result = launch_simplefoam_urans(
    "cases/naca0012_urans",
    Re=6e6,
    geometry_parameters=geometry_parameters,
    mesh_parameters=mesh_parameters,
    refinement_parameters=["LE", "TE"],
    n_subdomains=20,
    n_cycles=3,            # limit cycle repeated 3x => steady "converged"
    urans_max_co=5.0,      # adaptive time-step target Courant number
    urans_max_time=200.0,  # physical-time cap for the URANS phase
)
print(result["steady_stop_reason"], result["Cl"], result["Cd"])

result["steady_stop_reason"] is fixed_point, limit_cycle, or max_wall. Set run_urans=False to stop after the steady phase. Reported Cl/Cd are the time-average over the converged URANS window (or the steady tail when URANS is skipped). The transient overrides live in Template_simplefoam/system/urans/.

A runnable end-to-end example lives in test.py; the test/ directory contains a completed incompressible case for reference.


API at a glance

All launchers share the same core arguments:

Argument Meaning
output_path Case directory (created if missing; the template is copied here).
Re Chord-based Reynolds number. Incompressible cases use c = U_inf = rho = 1, so nu = 1/Re.
Mach Freestream Mach number (compressible launchers only).
geometry_parameters Airfoil definition passed to airfoilgmesher (airfoil_type: NACA, RAE2822, 30P30N, …).
mesh_parameters Mesh controls passed to airfoilgmesher (domain extent, angle of attack alpha, inflation, divisions, …).
splitting_parameters, permutation_parameters, refinement_parameters Optional airfoilgmesher controls; e.g. refinement_parameters=["LE", "TE"].
n_subdomains Number of MPI ranks / mesh subdomains (default 48).
max_iterations Hard cap on solver iterations. Patches endTime in the copied controlDict (endTime = max_iterations × deltaT; for the deltaT = 1 solvers this is simply the iteration count). The coefficient criterion can still stop the run earlier.
min_samples, window, cl_range_tol, cd_range_tol, cl_rel_std_tol, cd_rel_std_tol Convergence criterion controls (coefficient-controlled launchers).
check_every_seconds, min_wall_seconds, max_wall_seconds Polling cadence and wall-clock guards for the monitored solve.

Compressible launchers additionally accept c, T_inf, I (turbulence intensity), nut_over_nu, and iteration/time-stepping controls. For the transient launch_rho_pimplefoam the acoustic CFL time step is derived automatically from the minimum mesh edge.

The coefficient-controlled launchers return a dict with Cl, Cd, Re (and Mach), the case directory, the freestream/turbulence properties, and the convergence_info.

Freestream & turbulence initialization

Initial/inlet turbulence quantities are derived from a turbulence intensity I and a target eddy-viscosity ratio nut/nu (see utils.py):

  • Spalart–Allmaras (simpleFoam): sets nu, nuTilda, nut (compute_sa_initial_properties_from_nut_ratio).
  • k‑ω SST (pimpleFoam): sets nu, k, omega, nut (compute_sst_initial_properties_from_nut_ratio).
  • Compressible (rho*Foam): uses Sutherland viscosity and sets U_inf, p_inf, T_inf, rho_inf, k, omega, nut, alphat, plus rhoInf/magUInf in forceCoeffs (compute_compressible_freestream).

Outputs

Each run writes into output_path:

File Contents
mesh.png Rendered Gmsh mesh.
airfoil.geo, airfoil.msh Generated geometry and mesh.
Cp.csv / Cp_<patch>.csv Surface x, y, Cp, Cf (per airfoil patch for multi-element cases).
outputs_processed.csv Full Cl/Cd (and components) time history.
coefficients.npz Converged Cd, Cl, Re/Mach, freestream props, convergence diagnostics.
Cl_Cd_history.png Cl/Cd convergence plot.
log.* OpenFOAM logs (log.Allrun_pre, log.<solver>, log.reconstructPar, …).

Cl/Cd are reported as the mean over the final samples of the history. Steady (simpleFoam/rhoSimpleFoam) cases read instantaneous p and wallShearStress; transient (pimpleFoam/rhoPimpleFoam) cases use the time-averaged pMean/wallShearStressMean fields produced by the fieldAverage function object. Cp = (p − p∞)/q∞, Cf = |τ_w|/q∞.


Benchmarks & validation

Reference cases and analysis scripts live in airfoilranssolver/benchmarking/ (benchmark_*.py to run, analyze_benchmark_*.py to compare against reference data in benchmarking/data/):

Case Conditions Solver Reference
NACA 0012 Re = 6×10⁶, AoA = 10° pimpleFoam (fixed iterations) Cl ≈ 1.08075, Cd ≈ 0.012505
30P30N (three-element) Re = 5×10⁶, AoA = 8.12° simpleFoam (coefficient-controlled) Cl ≈ 3.1; also used for a 5–56 core scaling study
RAE2822 (transonic) Mach = 0.729, Re = 6.5×10⁶ rhoPimpleFoam NASA RAE2822 reference Cp (experimental)

Repository layout

airfoilranssolver/
├── launchers_simplefoam.py            # simpleFoam   (incompressible, SA)        ✅
├── launchers_no_coefficient_control.py# pimpleFoam   (fixed iterations)          ✅
├── launchers_pimplefoam.py            # pimpleFoam   (coefficient-controlled)
├── launchers_rhosimplefoam.py         # rhoSimpleFoam (compressible, steady)
├── launchers_rhopimplefoam.py         # rhoPimpleFoam (compressible, transient)
├── utils.py                           # convergence test, FOAM dict patching, freestream calcs
├── Templates/                         # one OpenFOAM case template per solver
│   ├── Template_simplefoam/
│   ├── Template_pimplefoam/
│   ├── Template_rhoSimpleFoam/
│   └── Template_rhoPimpleFoam/
└── benchmarking/                      # validation cases + reference data
test.py                                # end-to-end usage example
test/                                  # a completed reference case

Each template provides the OpenFOAM dictionaries (0_org/, constant/, system/) plus an Allrun_pre (and, where relevant, Allrun) script. The launchers patch the freestream/turbulence fields and forceCoeffs (rhoInf, magUInf) before solving.


Publishing

See update_pypi.md for the build-and-upload workflow (increment the version in pyproject.toml, python -m build, twine check, twine upload).

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

airfoilranssolver-0.2.0.tar.gz (140.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

airfoilranssolver-0.2.0-py3-none-any.whl (192.9 kB view details)

Uploaded Python 3

File details

Details for the file airfoilranssolver-0.2.0.tar.gz.

File metadata

  • Download URL: airfoilranssolver-0.2.0.tar.gz
  • Upload date:
  • Size: 140.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for airfoilranssolver-0.2.0.tar.gz
Algorithm Hash digest
SHA256 f3b93dd5448ffbe153fe886c3b067a182299237c4bb8d2e0d190b38214128f92
MD5 ccea60eb94e13ecb48d6e4f396ecc319
BLAKE2b-256 fd3b3aa188ff151bec6c7a8eb0f1ae8102f448fb46bd524f8d7e8adb80a9a917

See more details on using hashes here.

File details

Details for the file airfoilranssolver-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for airfoilranssolver-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ff4ba60f6ac88f8e73255ce68cc65a3d249ede1e5cca7f8eb4e6fc5c8ed7c3d2
MD5 666396576c1adad15d24a2a8f13def9f
BLAKE2b-256 2f2d96d74c43d59a4d00024d2e3b2b5cfd5ed467fbfb308f546482511eafa958

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page