Spec: Prandtl — CFD Surrogate Modeling Toolkit¶
Objective¶
What: A Python toolkit that lets simulation engineers train, validate, and export surrogate models for CFD with minimal code. Three lines: sample → fit → export.
Who: Simulation engineers who need fast aerodynamic predictions without running full CFD every time.
Why: Existing solutions (scikit-learn GP, SMT) are generic ML tools. None provide the domain-specific workflow of parameter sampling → surrogate training → validation reports → simulator-ready export in a single package. Every aerospace/robotics company builds this internally; no open-source standard exists.
Success: An engineer can replace a CFD simulation loop with a trained surrogate model that predicts CL/CD/CM at sub-millisecond latency with >95% R² on held-out data.
Tech Stack¶
| Component | Choice | Version |
|---|---|---|
| Language | Python | ≥ 3.10 |
| Gaussian Process | GPyTorch | latest |
| Neural Network | PyTorch | latest |
| Export | ONNX + onnxruntime | latest |
| Sampling | scipy (LHS) | latest |
| Math | numpy | latest |
| Build | setuptools / pyproject.toml | PEP 621 |
| Test | pytest | latest |
| Lint | ruff | latest |
Commands¶
Install: pip install -e .
Test: pytest tests/ -v
Lint: ruff check src/
Format: ruff format src/
Typecheck: mypy src/
Project Structure¶
prandtl/
├── pyproject.toml # PEP 621 build config
├── README.md
├── docs/
│ └── SPEC.md # This file
├── src/
│ └── prandtl/
│ ├── __init__.py # Public API surface
│ ├── _sampling.py # LHS, uniform, Sobol samplers
│ ├── _analytical.py # Analytical truth functions for validation
│ ├── _surrogate.py # Core Surrogate class (unified interface)
│ ├── _gaussian.py # GP backend (GPyTorch)
│ ├── _neural.py # MLP backend (PyTorch)
│ ├── _validate.py # Cross-validation, residual analysis, metrics
│ └── _export.py # ONNX export + simulator interface stubs
└── tests/
├── test_sampling.py
├── test_analytical.py
├── test_surrogate.py
├── test_gaussian.py
├── test_neural.py
├── test_validate.py
└── test_export.py
Underscore-prefixed private modules. Only __init__.py exposes the public API.
Code Style¶
"""One-line module docstring."""
from typing import Optional
import numpy as np
import torch
class Surrogate:
"""CFD surrogate model with a scikit-learn-like interface.
Parameters
----------
params : list of str
Names of input parameters, e.g. ['alpha', 'mach', 'camber'].
outputs : list of str
Names of output quantities, e.g. ['CL', 'CD'].
method : str
Backend: 'gp' (Gaussian Process) or 'mlp' (neural network).
"""
def __init__(
self,
params: list[str],
outputs: list[str],
method: str = "gp",
) -> None:
...
def fit(
self,
X: np.ndarray,
Y: np.ndarray,
*,
n_iter: int = 100,
verbose: bool = True,
) -> "Surrogate":
"""Train the surrogate on (X, Y) data. Returns self for chaining."""
...
def predict(self, X: np.ndarray) -> np.ndarray:
"""Return predicted outputs for given inputs."""
...
def validate(self, X_test: np.ndarray, Y_test: np.ndarray) -> dict:
"""Return dict with R², RMSE, max_error per output."""
...
Key conventions:
- Google-style docstrings (numpy docstring format for scientific audience)
- Type hints on all public methods
- Private modules prefixed with _
- Classes: PascalCase. Functions/variables: snake_case
- 100 char line limit
- Explicit * for keyword-only arguments where appropriate
Testing Strategy¶
- Framework: pytest with
--strict-markers - Location:
tests/mirrorssrc/prandtl/ - Coverage target: >90% on core modules
- Test levels:
- Unit: each module in isolation with small synthetic data
- Integration:
Surrogate.fit() → .predict() → .validate()end-to-end with analytical truth - No GPU required — all tests runnable on CPU with small synthetic data
- CI (future): GitHub Actions on push, CPU only
API Design (MVP)¶
The Three-Line Interface¶
import prandtl as pr
# 1. Sample
X, Y = pr.sample(pr.analytical.cl_flat_plate, bounds=[(-5, 15), (0.01, 0.1)], n=100)
# 2. Fit
surrogate = pr.Surrogate(params=["alpha", "camber"], outputs=["CL"]).fit(X, Y)
# 3. Validate
report = surrogate.validate(*pr.sample(pr.analytical.cl_flat_plate, bounds=[...], n=20))
print(f"R² = {report['CL']['r2']:.4f}") # → R² = 0.9998
Module Breakdown¶
prandtl.sample(func, bounds, n, method='lhs')¶
Sample parameter space and evaluate truth function.
- func: Callable that takes **params and returns dict of outputs
- bounds: List of (low, high) tuples per parameter
- n: Number of design points
- method: 'lhs' | 'uniform' | 'sobol'
- Returns: (X: np.ndarray, Y: np.ndarray)
prandtl.Surrogate(params, outputs, method='gp')¶
Main class. Unified interface over GP and MLP backends.
analytical module¶
Built-in truth functions for framework validation:
- cl_flat_plate(alpha, camber) → CL = 2π(α + 2c) [thin airfoil theory]
- cd_cylinder(reynolds) → empirical drag curve
- thrust_propeller(rpm, diameter, pitch) → T = CT·ρ·n²·D⁴
Success Criteria (MVP)¶
- [x]
pip install -e .succeeds - [x]
pr.sample()returns correct shapes with LHS and uniform methods - [x] GP surrogate fits
cl_flat_platewith R² > 0.99 on 100 train / 20 test points - [x] MLP surrogate fits
cl_flat_platewith R² > 0.99 on 100 train / 20 test points - [x]
surrogate.validate()returns dict with r2, rmse, max_error per output - [x]
surrogate.export('model.onnx')produces valid ONNX file - [x] All tests pass:
pytest tests/ -v - [x] Ruff lint clean:
ruff check src/
Boundaries¶
Always do: - Run tests before declaring a feature done - Write docstrings on all public functions and classes - Use type hints on all public API - Keep imports minimal — no unused deps
Ask first:
- Adding new dependencies beyond numpy, scipy, torch, gpytorch, onnx
- Changing the public API (anything in __init__.py)
- Adding GPU-dependent code paths
Never do:
- Hard-code file paths
- Assume internet access at runtime
- Import heavy deps at module level (lazy import in __init__.py)
Open Questions¶
None — all addressed in assumptions above.