Metadata-Version: 2.4
Name: flexfoil
Version: 1.1.2
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Rust
Requires-Dist: numpy>=1.24
Requires-Dist: flexfoil[server,plotting,dataframe] ; extra == 'all'
Requires-Dist: pandas>=2.0 ; extra == 'dataframe'
Requires-Dist: flexfoil[all] ; extra == 'dev'
Requires-Dist: pytest>=7 ; extra == 'dev'
Requires-Dist: maturin>=1.5 ; extra == 'dev'
Requires-Dist: matplotlib>=3.7 ; extra == 'plotting'
Requires-Dist: starlette>=0.36 ; extra == 'server'
Requires-Dist: uvicorn[standard]>=0.27 ; extra == 'server'
Provides-Extra: all
Provides-Extra: dataframe
Provides-Extra: dev
Provides-Extra: plotting
Provides-Extra: server
Summary: Airfoil analysis in Python — XFOIL-faithful solver with a local web UI
Keywords: airfoil,aerodynamics,xfoil,cfd,panel-method
Author: Harry
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://github.com/flexcompute/flexfoil/tree/main/packages/flexfoil-python#readme
Project-URL: Homepage, https://foil.flexcompute.com/flexfoil/
Project-URL: Issues, https://github.com/flexcompute/flexfoil/issues
Project-URL: Repository, https://github.com/flexcompute/flexfoil

# flexfoil

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

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

```bash
pip install flexfoil
```

Optional extras:

```bash
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](https://foil.flexcompute.com/flexfoil/) solver
(an XFOIL-faithful Rust reimplementation) in native Python bindings via
[PyO3](https://pyo3.rs). Every solve is cached in a local SQLite database
(`~/.flexfoil/runs.db`). The same web UI from
[foil.flexcompute.com](https://foil.flexcompute.com/flexfoil/) 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

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

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

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

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

### Custom coordinates

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

### Inviscid analysis

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

## Launch the web UI

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

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

```python
# 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/`](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

```bash
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
```

