Metadata-Version: 2.4
Name: airfoilranssolver
Version: 0.2.0
Summary: Python tools for 2D airfoil Openfoam Simulation
Author-email: Isaac <isaac.robledo.martin@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/ipatazas/AirfoilRANSSolver
Project-URL: Repository, https://github.com/ipatazas/AirfoilRANSSolver
Project-URL: Issues, https://github.com/ipatazas/AirfoilRANSSolver/issues
Keywords: airfoil,aerodynamics,cfd
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy
Requires-Dist: matplotlib
Requires-Dist: airfoilgmesher
Requires-Dist: scipy
Requires-Dist: fluidfoam
Requires-Dist: pandas
Requires-Dist: gmsh
Dynamic: license-file

# 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`](https://pypi.org/project/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.

```bash
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_simplefoam` → `launch_simplefoam` | `simpleFoam` | Incompressible, steady | Spalart–Allmaras | Coefficient criterion | ✅ **Validated** |
| `launchers_simplefoam_urans` → `launch_simplefoam_urans` | `simpleFoam` → `pimpleFoam` | Steady → URANS (incompressible) | Spalart–Allmaras | Steady: fixed-point **or** 3× limit cycle; URANS: mean-stationary + cap | 🧪 Experimental |
| `launchers_no_coefficient_control` → `launch_pimplefoam` | `pimpleFoam` | Incompressible, (pseudo-)transient | k‑ω SST | Fixed iteration count (`Allrun`) | ✅ **Validated** |
| `launchers_pimplefoam` → `launch_pimplefoam` | `pimpleFoam` | Incompressible, (pseudo-)transient | k‑ω SST | Coefficient criterion | 🧪 Experimental |
| `launchers_rhosimplefoam` → `launch_rhosimplefoam` | `rhoSimpleFoam` | Compressible / transonic, steady | k‑ω SST | Coefficient criterion | 🧪 Experimental |
| `launchers_rhopimplefoam` → `launch_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`](https://pypi.org/project/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)

```python
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)

```python
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`.

```python
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`](test.py); the
[`test/`](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`](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`](LICENSE).
