Skip to main content

Airfoil analysis in Python — XFOIL-faithful solver with a local web UI

Project description

flexfoil

Airfoil analysis in Python -- XFOIL-faithful viscous solver with a local web UI.

import flexfoil

foil = flexfoil.naca("2412")
result = foil.solve(alpha=5.0, Re=1e6)
print(result)
# SolveResult(α=5.00°, Re=1e+06, CL=0.8094, CD=0.00775, CM=-0.0540, converged)

Install

pip install flexfoil

Pre-built wheels are available for:

  • macOS (Apple Silicon and Intel)
  • Linux x86_64
  • Windows x86_64

Plotly is included by default for interactive plots. Optional extras:

pip install "flexfoil[all]"        # server + matplotlib + pandas
pip install "flexfoil[matplotlib]" # matplotlib for polar.plot(backend="matplotlib")
pip install "flexfoil[dataframe]"  # pandas for polar.to_dataframe()
pip install "flexfoil[server]"     # starlette + uvicorn for flexfoil.serve()

What is this?

flexfoil wraps the RustFoil solver (an XFOIL-faithful Rust reimplementation) in native Python bindings via PyO3. Every solve is cached in a local SQLite database (~/.flexfoil/runs.db). The same web UI from foil.flexcompute.com can be launched locally with flexfoil.serve(), reading from the same database. No data leaves your machine unless you explicitly export it.

Quick start

Single-point solve

import flexfoil

foil = flexfoil.naca("2412")
result = foil.solve(alpha=5.0, Re=1e6)

print(result.cl)          # 0.8094
print(result.cd)          # 0.00775
print(result.cm)          # -0.0540
print(result.converged)   # True
print(result.ld)          # 104.5
print(result.x_tr_upper)  # 0.315  (transition location, upper surface)

Polar sweep

Polar sweeps are parallelized by default using all available CPU cores (via Rust's rayon thread pool). A 41-point polar runs ~3x faster than sequential.

polar = foil.polar(alpha=(-5, 15, 0.5), Re=1e6)
print(polar)
# PolarResult('NACA 2412', Re=1e+06, 40/41 converged)

# Interactive plotly figure (default): CL-α, CD-α, CL-CD, CM-α
polar.plot()

# Or use matplotlib
polar.plot(backend="matplotlib")

# Sequential mode (for debugging or progress output)
polar = foil.polar(alpha=(-5, 15, 0.5), Re=1e6, parallel=False)

# Export to pandas
df = polar.to_dataframe()
df.to_csv("polar.csv", index=False)

Compare multiple airfoils

import matplotlib.pyplot as plt
import flexfoil

for naca in ["0012", "2412", "4412"]:
    foil = flexfoil.naca(naca)
    polar = foil.polar(alpha=(-4, 14, 1.0), Re=1e6)
    plt.plot(polar.alpha, polar.cl, ".-", label=foil.name)

plt.xlabel("α (°)")
plt.ylabel("CL")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

Load a .dat file

foil = flexfoil.load("e387.dat")   # Selig or Lednicer format
result = foil.solve(alpha=4.0, Re=2e5)

Custom coordinates

foil = flexfoil.from_coordinates(x_list, y_list, name="my shape")
result = foil.solve(alpha=3.0, Re=1e6)

Flap deflection

flapped = foil.with_flap(hinge_x=0.75, deflection=10)
print(flapped)
# Airfoil('NACA 2412 +flap(75%, +10.0°)', n_panels=160)

result = flapped.solve(alpha=5.0, Re=1e6)
print(result.cl)  # ~1.27 (vs 0.81 clean)

# Sweep flap deflections
for defl in [0, 5, 10, 15]:
    f = foil.with_flap(hinge_x=0.75, deflection=defl)
    polar = f.polar(alpha=(-4, 14, 1.0), Re=1e6)

Forced transition (trip strips)

Force the boundary layer to transition at a specific chord location, equivalent to XFOIL's XSTRIP (VPAR menu). Default 1.0 = free transition.

# Trip strips at 5% chord on both surfaces
result = foil.solve(alpha=5.0, Re=1e6, xstrip_upper=0.05, xstrip_lower=0.05)
print(result.x_tr_upper)  # ≈ 0.05

# Force upper only — lower uses natural (e^N) transition
polar = foil.polar(alpha=(-5, 15, 0.5), Re=1e6, xstrip_upper=0.1)

Inviscid analysis

result = foil.solve(alpha=5.0, viscous=False)
# result.cd == 0.0 (no drag in potential flow)

Launch the web UI

flexfoil.serve()
# Opens the full flexfoil web app in your browser.
# Defaults to port 8420; automatically picks the next free port if busy.

Or from the command line:

flexfoil serve
flexfoil serve --port 9000 --no-browser

The web UI reads from the same local SQLite database as the Python API. Runs you solve from the browser (via the built-in WASM solver) are also written to the shared database, so flexfoil.runs() in Python sees them and vice versa.

Query the run database

Every solve (from Python or the web UI) is cached in ~/.flexfoil/runs.db.

# All runs as a pandas DataFrame (or list[dict] if pandas not installed)
df = flexfoil.runs()
print(f"{len(df)} runs cached")

# Filter by airfoil
naca_runs = df[df.airfoil_name == "NACA 2412"]

CLI

flexfoil solve 2412 -a 5 -r 1e6    # quick solve from the terminal
flexfoil serve                       # launch the web UI
flexfoil info                        # show config and DB location

API reference

Top-level functions

Function Description
flexfoil.naca(designation, n_panels=160) Create airfoil from NACA 4-digit string
flexfoil.load(path, n_panels=160) Load from .dat file
flexfoil.from_coordinates(x, y, name, n_panels) Create from raw x/y arrays
flexfoil.runs() All cached runs (DataFrame or list)
flexfoil.serve(port, host, open_browser) Launch local web UI + API server

Airfoil

Property / Method Description
.name Airfoil name
.n_panels Number of panel nodes
.raw_coords Original coordinates list[(x, y)]
.panel_coords Repaneled coordinates list[(x, y)]
.hash SHA-256 hash of panel coords (cache key)
.with_flap(hinge_x, deflection, hinge_y_frac, n_panels) Return new Airfoil with flap deflected
.solve(alpha, Re, mach, ncrit, max_iter, viscous, store, re_type, xstrip_upper, xstrip_lower) Single-point analysis
.polar(alpha, Re, mach, ncrit, max_iter, viscous, store, parallel, re_type, xstrip_upper, xstrip_lower) Sweep over alpha range (parallel by default)

SolveResult

Field Type Description
.cl float Lift coefficient
.cd float Drag coefficient
.cm float Moment coefficient (quarter-chord)
.converged bool Whether the Newton solve converged
.iterations int Newton iterations used
.residual float Final Newton residual
.x_tr_upper float Transition x/c, upper surface
.x_tr_lower float Transition x/c, lower surface
.alpha float Angle of attack (degrees)
.reynolds float Reynolds number
.mach float Mach number
.ncrit float e^N transition criterion
.ld float | None Lift-to-drag ratio
.success bool Overall success flag
.error str | None Error message if failed

PolarResult

Property / Method Description
.alpha list[float] — angles (converged only)
.cl list[float] — lift coefficients
.cd list[float] — drag coefficients
.cm list[float] — moment coefficients
.ld list[float] — lift-to-drag ratios
.converged list[SolveResult] — converged results only
.results list[SolveResult] — all results
.to_dict() Export as dict
.to_dataframe() Export as pandas.DataFrame
.plot(show=True, backend="plotly") 4-panel figure (plotly default, or "matplotlib")

RunDatabase

Method Description
.insert_run(...) Insert a solver run
.lookup_cache(...) Cache lookup by (hash, alpha, Re, ...)
.query_all_runs() All runs as list[dict]
.query_runs(airfoil_name, limit, offset) Filtered query
.row_count() Number of cached runs
.delete_all_runs() Clear all runs
.save_airfoil(name, coords_json) Save a named airfoil
.list_airfoils() List saved airfoils
.export_bytes() Export SQLite as bytes
.import_bytes(data) Import SQLite from bytes

Configuration

Environment variable Default Description
FLEXFOIL_DATA_DIR ~/.flexfoil Directory for runs.db

How it works

┌──────────────────────────────────┐     ┌──────────────────────────┐
│  Python                          │     │  Browser                 │
│                                  │     │                          │
│  import flexfoil                 │     │  localhost:8420           │
│  foil.solve() ──► Rust solver    │     │  React SPA               │
│        │         (PyO3)          │     │  WASM solver ──┐         │
│        ▼                         │     │                │         │
│  ~/.flexfoil/runs.db ◄──────────────── REST API ◄──────┘         │
│  (SQLite, WAL mode)              │     │                          │
│                                  │     │  SSE live updates ◄──┐  │
│  flexfoil.serve() ───► Starlette ──────►                     │  │
│                        uvicorn   │     │  (runs appear as     │  │
│                                  │     │   Python solves them) │  │
│  foil.polar() ──► insert rows ───────────────────────────────┘  │
└──────────────────────────────────┘     └──────────────────────────┘

The Rust solver called from Python is the exact same code as the WASM solver in the browser — both are compiled from the rustfoil-xfoil crate. Results are byte-identical.

Examples

See the examples/ directory:

Script What it does
01_quickstart.py Single-point solve
02_polar_sweep.py Full polar with table + plot
03_compare_airfoils.py Overlay multiple NACA foils
04_reynolds_sweep.py Re effect on drag polar
05_dat_file.py Load from .dat file
06_pandas_export.py Export to CSV via pandas
07_inviscid_vs_viscous.py Inviscid vs viscous comparison
08_batch_matrix.py Batch sweep: airfoils x Re x alpha
09_custom_coordinates.py Build airfoil from x, y arrays
10_flap_study.py Sweep flap deflections (0-20 deg)
11_flap_hinge_sweep.py Find optimal hinge position for L/D
12_matrix_sweep.py Alpha x Re matrix for a flapped airfoil

Supported platforms

Platform Status
macOS Apple Silicon (arm64) Pre-built wheel
macOS Intel (x86_64) Pre-built wheel
Linux x86_64 Pre-built wheel
Windows x86_64 Pre-built wheel
Linux aarch64 (ARM) Build from source (requires Rust toolchain)

Development

cd packages/flexfoil-python

# Create a venv
python -m venv .venv
source .venv/bin/activate  # or .venv\Scripts\activate on Windows

# Install Rust toolchain and maturin
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
pip install maturin

# Install dependencies
pip install starlette 'uvicorn[standard]' matplotlib pandas pytest

# Build and install in development mode
maturin develop

# Run tests (112 tests)
pytest tests/ -v

# Bundle the web UI (optional, for `flexfoil serve`)
./build_frontend.sh

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

flexfoil-1.1.6.tar.gz (553.9 kB view details)

Uploaded Source

Built Distributions

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

flexfoil-1.1.6-cp311-cp311-win_amd64.whl (5.4 MB view details)

Uploaded CPython 3.11Windows x86-64

flexfoil-1.1.6-cp311-cp311-macosx_11_0_arm64.whl (5.5 MB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

flexfoil-1.1.6-cp311-cp311-macosx_10_12_x86_64.whl (5.5 MB view details)

Uploaded CPython 3.11macOS 10.12+ x86-64

flexfoil-1.1.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (5.5 MB view details)

Uploaded CPython 3.8manylinux: glibc 2.17+ x86-64

File details

Details for the file flexfoil-1.1.6.tar.gz.

File metadata

  • Download URL: flexfoil-1.1.6.tar.gz
  • Upload date:
  • Size: 553.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for flexfoil-1.1.6.tar.gz
Algorithm Hash digest
SHA256 cf703b72fccc29c365dc6064a8f4608ee5f1b4889314265116edec22de8744b1
MD5 ffb042fca257598c08d5819033a842d2
BLAKE2b-256 a154c3cff46b9202a78c6d358356c4d6efce7569b0749996ea48084923dae627

See more details on using hashes here.

Provenance

The following attestation bundles were made for flexfoil-1.1.6.tar.gz:

Publisher: pypi-publish.yml on flexcompute/flexfoil

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file flexfoil-1.1.6-cp311-cp311-win_amd64.whl.

File metadata

  • Download URL: flexfoil-1.1.6-cp311-cp311-win_amd64.whl
  • Upload date:
  • Size: 5.4 MB
  • Tags: CPython 3.11, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for flexfoil-1.1.6-cp311-cp311-win_amd64.whl
Algorithm Hash digest
SHA256 baac603efa56e17bd3d641b7409027712fda5e99c386b4ce987c1d114e2c8a56
MD5 e0756c3e45a9881214fbd081a268995f
BLAKE2b-256 d64e1df042e3e9f8552c0958e4bf43f219cf11fcb56cb668c0d3919660ac7d5c

See more details on using hashes here.

Provenance

The following attestation bundles were made for flexfoil-1.1.6-cp311-cp311-win_amd64.whl:

Publisher: pypi-publish.yml on flexcompute/flexfoil

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file flexfoil-1.1.6-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for flexfoil-1.1.6-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ca1f9fb75a3ff84ae8846fd314e44d56204a4f410cd6c379dd07ecd77c83d09d
MD5 a6fff0cbbc27e687f2bef2ac42699f58
BLAKE2b-256 478da8b7a0900446d79d7b35b9f437ae919b98cf76beb04dd68be6c87fec0152

See more details on using hashes here.

Provenance

The following attestation bundles were made for flexfoil-1.1.6-cp311-cp311-macosx_11_0_arm64.whl:

Publisher: pypi-publish.yml on flexcompute/flexfoil

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file flexfoil-1.1.6-cp311-cp311-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for flexfoil-1.1.6-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 9ec5f7a2f9f7afd4e102dd6624900ab1ee74e062b45951489c4da9b6d7c10aff
MD5 4238c4d3a40a9e7104ba12c71d988e9f
BLAKE2b-256 42c2b6b81949164b6f4dd07095995281a5f9ece48f16e8d5fb63b1bc73b03f62

See more details on using hashes here.

Provenance

The following attestation bundles were made for flexfoil-1.1.6-cp311-cp311-macosx_10_12_x86_64.whl:

Publisher: pypi-publish.yml on flexcompute/flexfoil

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file flexfoil-1.1.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for flexfoil-1.1.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 dd324a39fb2c3d3717e73594bec7dae4104d1b5d5ed762feb1db8fea8fb627ed
MD5 41609ba71b1db0fd3cc16ab82adf307f
BLAKE2b-256 5fe671e5dc3bd06b99293fcf93801ea720f4029debe0f7fc60fb1d773cb52769

See more details on using hashes here.

Provenance

The following attestation bundles were made for flexfoil-1.1.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: pypi-publish.yml on flexcompute/flexfoil

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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