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)
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) |
Single-point analysis |
.polar(alpha, Re, mach, ncrit, max_iter, viscous, store, parallel) |
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
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file flexfoil-1.1.5.tar.gz.
File metadata
- Download URL: flexfoil-1.1.5.tar.gz
- Upload date:
- Size: 551.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dc106e18784a1bf1c45fbe1794310ceeda76602a86e48611641fb69a2cdcb847
|
|
| MD5 |
cb48d994f42299a4f08fc636bec599ab
|
|
| BLAKE2b-256 |
fa6846e27e8e2bae70410cc7c1220b12f8781a6c9705ca74e3e376451f23b002
|
Provenance
The following attestation bundles were made for flexfoil-1.1.5.tar.gz:
Publisher:
pypi-publish.yml on flexcompute/flexfoil
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flexfoil-1.1.5.tar.gz -
Subject digest:
dc106e18784a1bf1c45fbe1794310ceeda76602a86e48611641fb69a2cdcb847 - Sigstore transparency entry: 1163846038
- Sigstore integration time:
-
Permalink:
flexcompute/flexfoil@44ba888b31a1aeb50c1fb42554ac660c53b91831 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/flexcompute
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@44ba888b31a1aeb50c1fb42554ac660c53b91831 -
Trigger Event:
push
-
Statement type:
File details
Details for the file flexfoil-1.1.5-cp311-cp311-win_amd64.whl.
File metadata
- Download URL: flexfoil-1.1.5-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.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72744f54ebd2727f793951ea2025866cdff45c87615d158a2dcb58a4dcc18a17
|
|
| MD5 |
764080058a1c9c18b4f74f3c7e80ab17
|
|
| BLAKE2b-256 |
60ee641925a13d14b9815fd26f45de32a4c2861d19bc1e7a15267ee1736db137
|
Provenance
The following attestation bundles were made for flexfoil-1.1.5-cp311-cp311-win_amd64.whl:
Publisher:
pypi-publish.yml on flexcompute/flexfoil
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flexfoil-1.1.5-cp311-cp311-win_amd64.whl -
Subject digest:
72744f54ebd2727f793951ea2025866cdff45c87615d158a2dcb58a4dcc18a17 - Sigstore transparency entry: 1163846126
- Sigstore integration time:
-
Permalink:
flexcompute/flexfoil@44ba888b31a1aeb50c1fb42554ac660c53b91831 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/flexcompute
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@44ba888b31a1aeb50c1fb42554ac660c53b91831 -
Trigger Event:
push
-
Statement type:
File details
Details for the file flexfoil-1.1.5-cp311-cp311-macosx_11_0_arm64.whl.
File metadata
- Download URL: flexfoil-1.1.5-cp311-cp311-macosx_11_0_arm64.whl
- Upload date:
- Size: 5.5 MB
- Tags: CPython 3.11, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1e9b59a3095f80e9380df3c63f7988e96860287331a9133eb60c4d56df5984e1
|
|
| MD5 |
70f527625721a7c27aab27988f630c5e
|
|
| BLAKE2b-256 |
91fc5ca39dd318af8d6d0cb89aaa8cad0f3d8b6b2cea0a243c0ba2a9dbd4db8c
|
Provenance
The following attestation bundles were made for flexfoil-1.1.5-cp311-cp311-macosx_11_0_arm64.whl:
Publisher:
pypi-publish.yml on flexcompute/flexfoil
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flexfoil-1.1.5-cp311-cp311-macosx_11_0_arm64.whl -
Subject digest:
1e9b59a3095f80e9380df3c63f7988e96860287331a9133eb60c4d56df5984e1 - Sigstore transparency entry: 1163846218
- Sigstore integration time:
-
Permalink:
flexcompute/flexfoil@44ba888b31a1aeb50c1fb42554ac660c53b91831 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/flexcompute
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@44ba888b31a1aeb50c1fb42554ac660c53b91831 -
Trigger Event:
push
-
Statement type:
File details
Details for the file flexfoil-1.1.5-cp311-cp311-macosx_10_12_x86_64.whl.
File metadata
- Download URL: flexfoil-1.1.5-cp311-cp311-macosx_10_12_x86_64.whl
- Upload date:
- Size: 5.5 MB
- Tags: CPython 3.11, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d2b2beed111d523e56a5951087377ccad942842ff8c8b5f8da0ef0435f73f3f
|
|
| MD5 |
76aae3d2abc633221da6a0d9e26d7e9c
|
|
| BLAKE2b-256 |
6d2693de1bdba4c1555e067af111400a0a30895930f2092fb756381731b80046
|
Provenance
The following attestation bundles were made for flexfoil-1.1.5-cp311-cp311-macosx_10_12_x86_64.whl:
Publisher:
pypi-publish.yml on flexcompute/flexfoil
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flexfoil-1.1.5-cp311-cp311-macosx_10_12_x86_64.whl -
Subject digest:
4d2b2beed111d523e56a5951087377ccad942842ff8c8b5f8da0ef0435f73f3f - Sigstore transparency entry: 1163846181
- Sigstore integration time:
-
Permalink:
flexcompute/flexfoil@44ba888b31a1aeb50c1fb42554ac660c53b91831 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/flexcompute
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@44ba888b31a1aeb50c1fb42554ac660c53b91831 -
Trigger Event:
push
-
Statement type:
File details
Details for the file flexfoil-1.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: flexfoil-1.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 5.5 MB
- Tags: CPython 3.8, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1afd545b1b2cf497c8d3a9e9e377dfffd86442482a074653a4ffac370bf8fe94
|
|
| MD5 |
ea90598603e68110ed12726904071b97
|
|
| BLAKE2b-256 |
c7345adbe89f911144337152fd3961eaa1835bbd67061041886c87e8243c485e
|
Provenance
The following attestation bundles were made for flexfoil-1.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
pypi-publish.yml on flexcompute/flexfoil
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flexfoil-1.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
1afd545b1b2cf497c8d3a9e9e377dfffd86442482a074653a4ffac370bf8fe94 - Sigstore transparency entry: 1163846086
- Sigstore integration time:
-
Permalink:
flexcompute/flexfoil@44ba888b31a1aeb50c1fb42554ac660c53b91831 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/flexcompute
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@44ba888b31a1aeb50c1fb42554ac660c53b91831 -
Trigger Event:
push
-
Statement type: