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
Optional extras:
pip install flexfoil[all] # server + matplotlib + pandas
pip install flexfoil[plotting] # matplotlib for polar.plot()
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 = foil.polar(alpha=(-5, 15, 0.5), Re=1e6)
print(polar)
# PolarResult('NACA 2412', Re=1e+06, 40/41 converged)
# 4-panel matplotlib figure: CL-α, CD-α, CL-CD, CM-α
polar.plot()
# 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)
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 http://localhost:8420 in your browser.
# The full flexfoil web app reads from the same local SQLite database.
Or from the command line:
flexfoil serve
flexfoil serve --port 9000 --no-browser
The browser-based WASM solver still works for interactive exploration. Any
runs you solve from the web UI are also written to the shared SQLite, so
flexfoil.runs() in Python will see them.
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) |
.solve(alpha, Re, mach, ncrit, max_iter, viscous, store) |
Single-point analysis |
.polar(alpha, Re, mach, ncrit, max_iter, viscous, store) |
Sweep over alpha range |
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) |
4-panel matplotlib figure |
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 |
Development
cd packages/flexfoil-python
# Install Rust toolchain and maturin
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
pip install maturin
# Build and install in development mode
maturin develop --extras dev
# Run 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.1.tar.gz.
File metadata
- Download URL: flexfoil-1.1.1.tar.gz
- Upload date:
- Size: 536.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a583c647f280db36a00754dc0e2a70046b4edfb781e21820b6c756fe23611b6
|
|
| MD5 |
6db5b4612c052fd90fd297605ef50f7c
|
|
| BLAKE2b-256 |
6529ae3799e71f9939e2b02834072a6ff166ee62569df476623fc27d16e2bd3d
|
File details
Details for the file flexfoil-1.1.1-cp311-cp311-macosx_11_0_arm64.whl.
File metadata
- Download URL: flexfoil-1.1.1-cp311-cp311-macosx_11_0_arm64.whl
- Upload date:
- Size: 4.7 MB
- Tags: CPython 3.11, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
56536e7c11005ec1909b6b954164250df9ac0bb9a43867e6e44ba6068d6fbd86
|
|
| MD5 |
5845c820009ba40c5bd3e6a49a03c68a
|
|
| BLAKE2b-256 |
17040cdbdcd0b3709f770a19290c09148581313a4796cd9f285faf733fa72b2b
|
File details
Details for the file flexfoil-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl.
File metadata
- Download URL: flexfoil-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl
- Upload date:
- Size: 4.7 MB
- Tags: CPython 3.11, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
840b49a6d340654c590d50fbf14759cb0f80cf269a019caf7af012be2c570dbd
|
|
| MD5 |
0871b63d4592eddf18be15e11ca006d2
|
|
| BLAKE2b-256 |
4edf34ad5a9044e737d525d8d39e8d80b3a6baea783210e4b5300a8d8771df8b
|
File details
Details for the file flexfoil-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: flexfoil-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 4.7 MB
- Tags: CPython 3.8, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66c12fe211489880cff4341550d5153dd2fbc982d24815c4294a230bfbac95e5
|
|
| MD5 |
7af72eaf091437357f488b735885e7d8
|
|
| BLAKE2b-256 |
09706a87af9d47575f2c2f04c4372b70320f2e620f298996292d0614e5b8bc79
|