Skip to content

Commit 0643e0c

Browse files
Merge pull request #9 from flexcompute/dev
v1.1.2: Tutorial UX fixes + Selig database + Smart Group + Show Me tours
2 parents 297204e + ffa77bc commit 0643e0c

1,743 files changed

Lines changed: 125179 additions & 362 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 56b7a922a088c748a6b6b4278bd106ce89ebfd66

.cursor/rules/pr-to-main.mdc

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ alwaysApply: true
55

66
# PR-to-Main Requirements
77

8-
Every pull request that targets `main` **must** satisfy the two gates below before it can be opened. Treat these as blocking — do not create the PR until both are done.
8+
Every pull request that targets `main` **must** satisfy the three gates below before it can be opened. Treat these as blocking — do not create the PR until all are done.
99

1010
## 1. Update "What's New"
1111

@@ -19,7 +19,20 @@ The in-app changelog lives in `flexfoil-ui/src/lib/version.ts` (the `CHANGELOG`
1919
- `fixed` — bug fix
2020
- Keep descriptions concise (one sentence, no period).
2121

22-
## 2. Keep Documentation in Sync
22+
## 2. Bump Distribution Versions
23+
24+
All version numbers **must** stay in sync with the changelog's latest entry.
25+
26+
| File | Field | Must match |
27+
|------|-------|------------|
28+
| `flexfoil-ui/package.json` | `"version"` | Top `CHANGELOG` entry version (drives `__APP_VERSION__` via Vite) |
29+
| `packages/flexfoil-python/pyproject.toml` | `version` | Bump if the PR touches **anything** in `packages/flexfoil-python/`, `crates/rustfoil-python/`, solver behavior, or the WASM bridge. Skip only if the PR is purely UI cosmetic. |
30+
31+
- Use semver: bump **patch** for fixes, **minor** for new features, **major** for breaking changes.
32+
- The `package.json` version and the top `CHANGELOG` entry version must always be identical after the PR.
33+
- When in doubt about whether the Python package needs a bump, bump it — users should always be able to `pip install --upgrade flexfoil` to get the latest solver/API changes.
34+
35+
## 3. Keep Documentation in Sync
2336

2437
The docs site lives in `docs-site/docs/` (Docusaurus `.mdx` files).
2538

@@ -32,9 +45,11 @@ The docs site lives in `docs-site/docs/` (Docusaurus `.mdx` files).
3245
When drafting the PR description, include this checklist (filled in):
3346

3447
```markdown
35-
## Changelog & Docs
48+
## Changelog, Versions & Docs
3649
- [ ] `flexfoil-ui/src/lib/version.ts` CHANGELOG updated
3750
- [ ] All user-visible changes have a changelog entry
51+
- [ ] `flexfoil-ui/package.json` version matches top CHANGELOG entry
52+
- [ ] `packages/flexfoil-python/pyproject.toml` version bumped (if applicable)
3853
- [ ] New/changed docs pages added or updated in `docs-site/docs/`
3954
- [ ] No stale or aspirational content remains in affected docs
4055
```

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [main, dev]
66
pull_request:
77

88
concurrency:
@@ -13,6 +13,7 @@ jobs:
1313
rust:
1414
runs-on: ubuntu-latest
1515
timeout-minutes: 30
16+
continue-on-error: ${{ github.ref == 'refs/heads/dev' }}
1617
steps:
1718
- name: Checkout
1819
uses: actions/checkout@v5
@@ -29,6 +30,7 @@ jobs:
2930
python:
3031
runs-on: ubuntu-latest
3132
timeout-minutes: 30
33+
continue-on-error: ${{ github.ref == 'refs/heads/dev' }}
3234
steps:
3335
- name: Checkout
3436
uses: actions/checkout@v5

.github/workflows/pypi-publish.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: Build & Publish flexfoil Python wheels
22

33
on:
44
push:
5+
branches: [main]
56
tags:
67
- "pypi-v*"
78
workflow_dispatch:
@@ -161,7 +162,10 @@ jobs:
161162
publish:
162163
needs: [build-wheels, build-sdist]
163164
runs-on: ubuntu-latest
164-
if: startsWith(github.ref, 'refs/tags/pypi-v') || (github.event_name == 'workflow_dispatch' && inputs.publish)
165+
if: >-
166+
github.ref == 'refs/heads/main'
167+
|| startsWith(github.ref, 'refs/tags/pypi-v')
168+
|| (github.event_name == 'workflow_dispatch' && inputs.publish)
165169
environment:
166170
name: pypi
167171
url: https://pypi.org/p/flexfoil

crates/rustfoil-python/src/lib.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,96 @@ fn analyze_inviscid_batch(
414414
.collect()
415415
}
416416

417+
/// Compute boundary-layer distributions for a viscous operating point.
418+
///
419+
/// Returns a dict with keys: x_upper, x_lower, theta_upper, theta_lower,
420+
/// delta_star_upper, delta_star_lower, h_upper, h_lower, cf_upper, cf_lower,
421+
/// ue_upper, ue_lower, x_tr_upper, x_tr_lower, converged, iterations,
422+
/// residual, success, error.
423+
#[pyfunction]
424+
#[pyo3(signature = (coords, alpha_deg, reynolds=1.0e6, mach=0.0, ncrit=9.0, max_iterations=100))]
425+
fn get_bl_distribution(
426+
py: Python<'_>,
427+
coords: Vec<f64>,
428+
alpha_deg: f64,
429+
reynolds: f64,
430+
mach: f64,
431+
ncrit: f64,
432+
max_iterations: usize,
433+
) -> PyResult<Py<PyDict>> {
434+
use rustfoil_xfoil::oper::{build_state_from_coords, solve_operating_point_from_state, coords_from_body, AlphaSpec};
435+
use rustfoil_xfoil::XfoilOptions;
436+
437+
let d = PyDict::new(py);
438+
439+
if coords.len() < 6 || coords.len() % 2 != 0 {
440+
d.set_item("success", false)?;
441+
d.set_item("error", "Invalid coordinates")?;
442+
return Ok(d.into());
443+
}
444+
445+
let points = points_from_flat(&coords);
446+
let body = match Body::from_points("airfoil", &points) {
447+
Ok(b) => b,
448+
Err(e) => {
449+
d.set_item("success", false)?;
450+
d.set_item("error", format!("Geometry error: {e}"))?;
451+
return Ok(d.into());
452+
}
453+
};
454+
455+
let body_coords = coords_from_body(&body);
456+
let options = XfoilOptions {
457+
reynolds, mach, ncrit, max_iterations,
458+
..Default::default()
459+
};
460+
461+
let (mut state, factorized) = match build_state_from_coords(
462+
"airfoil", &body_coords, AlphaSpec::AlphaDeg(alpha_deg), &options,
463+
) {
464+
Ok(v) => v,
465+
Err(e) => {
466+
d.set_item("success", false)?;
467+
d.set_item("error", format!("{e}"))?;
468+
return Ok(d.into());
469+
}
470+
};
471+
472+
match solve_operating_point_from_state(&mut state, &factorized, &options) {
473+
Ok(result) => {
474+
let iblte_upper = state.iblte_upper.min(state.upper_rows.len().saturating_sub(1));
475+
let iblte_lower = state.iblte_lower.min(state.lower_rows.len().saturating_sub(1));
476+
let upper = &state.upper_rows[..=iblte_upper];
477+
let lower = &state.lower_rows[..=iblte_lower];
478+
479+
d.set_item("x_upper", upper.iter().map(|r| r.x_coord).collect::<Vec<_>>())?;
480+
d.set_item("x_lower", lower.iter().map(|r| r.x_coord).collect::<Vec<_>>())?;
481+
d.set_item("theta_upper", upper.iter().map(|r| r.theta).collect::<Vec<_>>())?;
482+
d.set_item("theta_lower", lower.iter().map(|r| r.theta).collect::<Vec<_>>())?;
483+
d.set_item("delta_star_upper", upper.iter().map(|r| r.dstr).collect::<Vec<_>>())?;
484+
d.set_item("delta_star_lower", lower.iter().map(|r| r.dstr).collect::<Vec<_>>())?;
485+
d.set_item("h_upper", upper.iter().map(|r| r.h).collect::<Vec<_>>())?;
486+
d.set_item("h_lower", lower.iter().map(|r| r.h).collect::<Vec<_>>())?;
487+
d.set_item("cf_upper", upper.iter().map(|r| r.cf).collect::<Vec<_>>())?;
488+
d.set_item("cf_lower", lower.iter().map(|r| r.cf).collect::<Vec<_>>())?;
489+
d.set_item("ue_upper", upper.iter().map(|r| r.uedg).collect::<Vec<_>>())?;
490+
d.set_item("ue_lower", lower.iter().map(|r| r.uedg).collect::<Vec<_>>())?;
491+
d.set_item("x_tr_upper", result.x_tr_upper)?;
492+
d.set_item("x_tr_lower", result.x_tr_lower)?;
493+
d.set_item("converged", result.converged)?;
494+
d.set_item("iterations", result.iterations)?;
495+
d.set_item("residual", result.residual)?;
496+
d.set_item("success", true)?;
497+
d.set_item("error", py.None())?;
498+
}
499+
Err(e) => {
500+
d.set_item("success", false)?;
501+
d.set_item("error", format!("{e}"))?;
502+
}
503+
}
504+
Ok(d.into())
505+
}
506+
417507
#[pymodule]
418508
fn _rustfoil(m: &Bound<'_, PyModule>) -> PyResult<()> {
419509
m.add_function(wrap_pyfunction!(analyze_faithful, m)?)?;
@@ -424,5 +514,6 @@ fn _rustfoil(m: &Bound<'_, PyModule>) -> PyResult<()> {
424514
m.add_function(wrap_pyfunction!(repanel_xfoil, m)?)?;
425515
m.add_function(wrap_pyfunction!(deflect_flap, m)?)?;
426516
m.add_function(wrap_pyfunction!(parse_dat_file, m)?)?;
517+
m.add_function(wrap_pyfunction!(get_bl_distribution, m)?)?;
427518
Ok(())
428519
}

docs-site/docs/python-api.mdx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,19 @@ Polar sweeps are **parallelized by default** using all available CPU cores
5757
```python
5858
polar = foil.polar(alpha=(-5, 15, 0.5), Re=1e6)
5959
print(polar)
60-
# PolarResult('NACA 2412', Re=1e+06, 40/41 converged)
60+
# PolarResult('NACA 2412', Re=1e+06, 40/41 converged, CLmax=1.4523, α_stall=12.5°, L/D_max=78.3)
61+
62+
# Aggregate statistics
63+
polar.cl_max # 1.4523
64+
polar.alpha_stall # 12.5
65+
polar.ld_max # 78.3
66+
polar.cd_min # 0.00523
67+
polar.summary() # dict with all aggregate statistics
68+
69+
# Generic aggregation with optional outlier filtering
70+
polar.column_max('cl', filter_outliers=True)
71+
polar.argmax('ld', 'alpha') # alpha at max L/D
72+
polar.argmin('cd', 'alpha') # alpha at min CD
6173

6274
# Interactive plotly figure (default): CL-α, CD-α, CL-CD, CM-α
6375
polar.plot()
@@ -68,8 +80,8 @@ polar.plot(backend="matplotlib")
6880
# Sequential mode (for debugging or progress output)
6981
polar = foil.polar(alpha=(-5, 15, 0.5), Re=1e6, parallel=False)
7082

71-
# Export to pandas
72-
df = polar.to_dataframe()
83+
# Export to pandas (with summary statistics in df.attrs)
84+
df = polar.to_dataframe(summary=True)
7385
df.to_csv("polar.csv", index=False)
7486
```
7587

@@ -262,8 +274,23 @@ are byte-identical.
262274
| `.ld` | `list[float]` — lift-to-drag ratios |
263275
| `.converged` | `list[SolveResult]` — converged results only |
264276
| `.results` | `list[SolveResult]` — all results |
265-
| `.to_dict()` | Export as `dict` |
266-
| `.to_dataframe()` | Export as `pandas.DataFrame` |
277+
| `.cl_max` | `float \| None` — maximum CL |
278+
| `.cl_min` | `float \| None` — minimum CL |
279+
| `.cd_min` | `float \| None` — minimum CD |
280+
| `.ld_max` | `float \| None` — maximum L/D |
281+
| `.alpha_stall` | `float \| None` — alpha at CL_max |
282+
| `.alpha_at_ld_max` | `float \| None` — alpha at L/D_max |
283+
| `.alpha_at_cd_min` | `float \| None` — alpha at CD_min |
284+
| `.column_max(col, filter_outliers=False)` | Max of any column |
285+
| `.column_min(col, filter_outliers=False)` | Min of any column |
286+
| `.column_mean(col, filter_outliers=False)` | Mean of any column |
287+
| `.column_median(col, filter_outliers=False)` | Median of any column |
288+
| `.column_stdev(col, filter_outliers=False)` | Std deviation of any column |
289+
| `.argmax(target, return_col, filter_outliers=False)` | Value of *return_col* at max of *target* |
290+
| `.argmin(target, return_col, filter_outliers=False)` | Value of *return_col* at min of *target* |
291+
| `.summary()` | Dict of all aggregate statistics |
292+
| `.to_dict(summary=False)` | Export as `dict` (optionally include summary) |
293+
| `.to_dataframe(summary=False)` | Export as `pandas.DataFrame` (summary in `.attrs`) |
267294
| `.plot(show=True, backend="plotly")` | 4-panel figure (plotly default, or `"matplotlib"`) |
268295

269296
### RunDatabase

docs-site/docs/web-app.mdx

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ The UI is organized as a set of dockable, rearrangeable panels.
2929

3030
### Airfoil Library
3131

32-
Browse and load airfoils from the NACA 4-digit generator and the Selig
33-
database (over 1,500 airfoils). Select any airfoil to load it into the canvas.
32+
Browse and load airfoils from the NACA 4-digit generator, import `.dat` files,
33+
or search the UIUC Selig database (~1,600 airfoils). Type at least two
34+
characters in the Selig search box to filter by name or filename, then click an
35+
entry to fetch and load it. The **Random Foil** button picks a random airfoil
36+
from the database and loads it instantly.
3437

3538
### Airfoil Canvas
3639

@@ -57,7 +60,8 @@ Visualize sweep results with configurable axes — CL, CD, CM, L/D, alpha,
5760
Reynolds, Mach, Ncrit, flap deflection, and flap hinge x/c. Overlay
5861
multiple series for direct comparison. Enable the **Outliers** checkbox to
5962
automatically remove statistical outliers (IQR method) from the displayed
60-
data.
63+
data. Right-click any data point to manually flag it as an outlier; flagged
64+
points appear as red X marks and are excluded when the outlier filter is on.
6165

6266
### Visualization
6367

@@ -76,13 +80,44 @@ filter, and search across airfoils, operating conditions, and results. Includes
7680
a SPLOM (scatter plot matrix) correlogram for multivariate exploration. Custom
7781
computed columns let you define algebraic expressions over existing fields. The
7882
correlogram view supports automatic outlier removal via the **Outliers** toggle.
83+
Right-click a scatter point to manually flag it as an outlier; flagged runs are
84+
persisted in the database and excluded from all plots. The table includes a
85+
filterable **Outlier** column.
86+
87+
**Row grouping and aggregation** — drag any column header (e.g. Airfoil, Re) into
88+
the row group panel above the grid. Grouped rows automatically aggregate their
89+
children: CL shows `max`, CD shows `min`, L/D shows `max`, etc. Change the
90+
aggregation function for any column via the sidebar (right-click a column header
91+
→ "Value Aggregation"). Custom aggregation functions include:
92+
93+
- **at max(CL)** / **at min(CD)** / **at max(L/D)** — returns the value of the
94+
current column at the row where the target column reaches its extremum.
95+
For example, applying "at max(CL)" to the alpha column gives **α_stall**.
96+
- **median** — median aggregation (not available by default in AG Grid).
97+
98+
Two predefined aerodynamic summary columns are available (hidden by default —
99+
show them via the sidebar):
100+
- **α_stall** — angle of attack at CL_max
101+
- **α @ L/D_max** — angle of attack at maximum lift-to-drag ratio
102+
103+
Use the **Converged Only** button to quickly filter out non-converged solver
104+
points before grouping, which is particularly useful near stall where the
105+
solver may not converge reliably.
79106

80107
### Plot Builder
81108

82109
Build custom charts from run data: scatter, line, bar, and histogram plots.
83110
Map any database field to axes, color, or size. Useful for parameter studies
84111
and trade-off visualization. Toggle **Outliers** to automatically exclude
85-
statistical outliers using the IQR method.
112+
statistical outliers using the IQR method. Right-click any plotted point to
113+
manually flag it as an outlier; flagged runs are persisted and excluded from
114+
the chart.
115+
116+
The **Aggregated** data source (available when row grouping is active in the
117+
Data Explorer) lets you plot group-level statistics directly. For example, to
118+
plot L/D_max vs Reynolds number for multiple airfoils: group by Airfoil and Re
119+
in the Data Explorer, then select Data → Aggregated, X → Re, Y → L/D in the
120+
Plot Builder.
86121

87122
### Case Logs
88123

docs-site/docusaurus.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const config: Config = {
1414
url: 'https://foil.flexcompute.com',
1515
baseUrl: '/docs/',
1616

17-
organizationName: 'flexfoil',
17+
organizationName: 'flexcompute',
1818
projectName: 'flexfoil',
1919

2020
onBrokenLinks: 'throw',
@@ -37,7 +37,7 @@ const config: Config = {
3737
docs: {
3838
routeBasePath: '/',
3939
sidebarPath: './sidebars.ts',
40-
editUrl: 'https://github.com/flexfoil/flexfoil/tree/main/docs-site/',
40+
editUrl: 'https://github.com/flexcompute/flexfoil/tree/main/docs-site/',
4141
},
4242
blog: false,
4343
theme: {
@@ -69,7 +69,7 @@ const config: Config = {
6969
label: 'Documentation',
7070
},
7171
{
72-
href: 'https://github.com/flexfoil/flexfoil',
72+
href: 'https://github.com/flexcompute/flexfoil',
7373
label: 'GitHub',
7474
position: 'right',
7575
},
@@ -104,7 +104,7 @@ const config: Config = {
104104
},
105105
{
106106
label: 'GitHub',
107-
href: 'https://github.com/flexfoil/flexfoil',
107+
href: 'https://github.com/flexcompute/flexfoil',
108108
},
109109
],
110110
},

0 commit comments

Comments
 (0)