Skip to content

Running a benchmark

pp.benchmark compares physics-AI methods (PINNs, neural operators, graph networks, MLPs, your own) on physics problems (built-in or your own) under the same compute budget, and tells you whether the comparison can be trusted. One call does everything; each stage can also be run alone.

prepare  ->  validate  ->  run (gates, search, final, equal FLOPs)  ->  report (tables, figures, diagnostics)

In one call

import pinneapple as pp

res = pp.benchmark.run(
    problems=["burgers", "darcy", "navier_stokes"],   # built-in ids, or Problem objects (yours)
    methods=["pinn", "fno", "deeponet"],               # built-in ids, or Method objects (yours)
    budget=pp.ComputeBudget(wall_s=300),               # per training run; or flops=..., steps=...
    budget_levels=[1/9, 1/3, 1.0],                     # error-versus-budget curves
    seeds=3, n_trials=4, search_fraction=1/3,          # the same search effort for every method
    equal_flops=True,                                  # an extra arm at equal FLOPs
    n_workers=4, threads_per_run=1,                    # every run gets the same hardware share
    out_dir="results/my_benchmark",                    # re-running resumes from here
)
res.table()            # pandas DataFrame
res.to_markdown()      # the report as Markdown; res.to_csv()
res.report()           # (re)writes table.md, table.csv, summary.json and figures/ in out_dir
res.diagnostics        # the advisor's findings (below)

From the shell, with a YAML file holding the same fields (example):

pinneapple benchmark prepare --config bench.yaml --out results/x     # artifacts + validation only
pinneapple benchmark run     --config bench.yaml --out results/x     # train and report
pinneapple benchmark report  results/x                               # report again from the cache
pinneapple benchmark run --config bench.yaml --out results/x --smoke # tiny sizes: a pipeline check in minutes

Options of run: --force (reuse an out_dir made with another configuration), --skip-checks, --allow-failed-validation, --apply-suggestions (writes config.suggested.yaml with the advisor's edits), --no-prepare (refuse to run when the artifacts are not prepared yet).

What ends up in out_dir

Path Content
config.yaml the resolved configuration; its hash guards the cache (a different configuration is refused unless force)
hardware.json CPU, cores, memory, GPU, library versions
prepared/<problem>/ solver configuration, data, geometry (for the airfoil: shapes, a complete OpenFOAM case), data-generation cost, manifest.json with the SHA-256 of every file and the validation status
validation/ per problem: solver verification, data checks, the validation stack; architecture.json, determinism.json
runs/ one JSON per training job (spec, result, hardware, wall time): the cache
table.md, table.csv, summary.json the report
figures/ summary_table.png (shareable), error_vs_budget.png, pareto_error_vs_inference.png

Stage 1: prepare

pp.benchmark.prepare(config, out_dir) creates the artifacts before any training and saves them for you to inspect: the solver configuration, the geometry, the generated data, what generating it cost (core-seconds, measured) and the hashes. run consumes only prepared artifacts: it prepares what is missing, and refuses to train when they are stale (a hash no longer matches the configuration or a file changed) or failed validation, unless you pass allow_failed_validation=True, which the table then shows on every page.

Stage 2: validation, with PINNeAPPle's own validation stack

Run on the prepared artifacts, stored in out_dir/validation/ and in the report's Validation section:

Check Library entry point Applies to
Reference solver against an exact or manufactured solution, observed order the problem's verify_solver (Cole-Hopf, Taylor-Green, a manufactured Darcy solution, NACA 0012 vs the NASA TMR value) built-in problems
Grid convergence (GCI, observed order, asymptotic range) pinneapple_design.fitmesh.gci, pinneapple_analysis.verification.richardson_extrapolate problems with grid levels
Simulation preflight of the solver case pinneapple_data.preflight OpenFOAM / CalculiX problems
Mesh quality pinneapple_data.cae.mesh_report problems with a mesh
First-cell y+ verification.mesh_intelligence.estimate_required_first_cell_height wall-bounded flows
Physics audit of the solver's assumptions pinneapple_physics.physics_audit (ModelProfile, audit_assumptions) every built-in problem
Applicability domain: test inside, OOD outside the training envelope physics_audit.Envelope every problem
Data sanity: finite, split sizes, no duplicates or leakage between splits (hashes), OOD really out of distribution, normalisation from the training split FieldOperatorProblem.data_checks every problem (including your arrays)
Six-stage trust card pinneapple_lab.curation.trust_stages the report's Trust card section

A failed blocking item stops the run. For a problem you bring (an ArrayProblem or your own Problem), the same checks run against what you provide, and the report says which ones did not apply and why.

Stage 3: run, with correctness gates

The gates run by default, are recorded with their measured values, and are never skipped silently:

  • Architecture (before training): forward shapes and dtype, parameter count, FNO modes at most the Nyquist limit and output invariant to the input resolution, DeepONet branch and trunk shapes (and its basis against the POD rank of the outputs), MeshGraphNet permutation equivariance, PINN derivatives against finite differences and the PDE residual of an exact solution through the same compiled residual (a wrong equation or a sign error fails here), and every method overfitting a tiny batch (to 10 % of its initial loss; a model that cannot even halve it is a broken setup, one that learns but more slowly is a warning). A pair that fails an error-level gate is not trained and is INVALID in the table.
  • Training (from every run): loss finite and decreasing, divergence, "did not learn" (judged on the full-budget final runs; the search runs and the short runs of the error-versus-budget curve are expected to learn less), the budget enforced, identical accounting across methods (hardware, threads, timer, FLOP counter), evaluation off the clock, the search touching validation data only, the seed spread (veriphysics tolerance), and a determinism spot-check (same seed, same steps, same loss).

Then the search (n_trials configurations per method, each for search_fraction of the budget, on validation data), the final runs (seeds x budget_levels, test sets) and the equal-FLOPs arm. A row whose error is no better than a trivial prediction is labelled did not converge, not shown as a plain number. skip_checks=True puts a "checks skipped" banner on the table and the summary figure.

Stage 4: report, and the advisor

table.md has one section per problem with n in every column header, integral quantities and fields in separate tables (a method that predicts only integrals shows "integrals only" in the field columns), the simulation-data cost, the time per new instance, the break-even number of queries against a per-instance method, a checks badge per row, and, with equal_flops, an Equal FLOPs table (every method trained to the same FLOP count, with the error on the instances all methods share, the FLOPs and wall time used, and what stopped each run).

Diagnostics and suggestions lists what in the setup is likely to hurt the result, each with a severity, the measured evidence, the suggested change and, where possible, the exact configuration edit (res.apply_suggestions() writes them to a new configuration for you to review). It looks for: a budget too small (the error still falling between the last two budget levels), a model that did not learn or converge (with the likely cause for a PINN: loss-term imbalance, nondimensionalisation, collocation density), too few seeds, an OOD set that is not out of distribution or has a single case, leakage, normalisation from all data, FNO modes against the output spectrum, a DeepONet basis smaller than the field's rank, a learning rate that spikes or makes no progress early, and solver or mesh problems (GCI, y+, unconverged runs, CFL).

Budget study and setup audit

Two optional parts answer "would this method do better with more compute, or with another setting?":

pp.benchmark.run(...,
    budget_study={"airfoil": {"pinn_rans": [3600, 14400], "pinn": [3600]}},       # wall-clock seconds, seed 0
    ablations={"airfoil": {"pinn": {"lr1e-3": {"lr": 1e-3}, "lbfgs": {"lbfgs": 0.3}}}})   # changes to the best config

budget_study trains the best configuration again at each listed budget (seed 0, on the problem's study instances) and adds those points to the seed-0 runs of budget_levels: a table per instance and figures/budget_study.png. ablations trains the best configuration with each change at the full budget on the problem's audit instance and puts it next to the unchanged run (the Setup audit table). Both only add jobs: adding budgets, problems, methods or variants to them reuses an existing out_dir; changing or removing an entry is refused like any other configuration change (it would reinterpret cached runs).

Example: what the gates and the advisor said on PINNeAPPle-Benchmark experiment 09

Experiment 09 is five problems and seven methods through pp.benchmark. From its report:

  • error [airfoil/pinn_rans] Did not converge: the error is no better than a trivial prediction. Evidence: mean velocity rel. L2 on the held-out runs = 0.96 (>= 0.9). Suggestion: check the nondimensionalisation, the loss weights and the collocation density near walls and leading edges; consider more budget or a formulation with fewer stiff terms. With the training gate "no divergence" failing in 2 of its 29 runs, the row is INVALID. The budget study (budget_study: 1 h, 4 h) and the setup audit (ablations: learning rate, Adam -> L-BFGS) show neither more compute nor those settings help: the PINN collapsed to the zero-flow solution of its own equations.
  • warning [airfoil/mgn] Still improving at the full budget: velocity rel. L2 0.368 at 1/3 of the budget, 0.169 at the full one (54 % lower for 3x). Edit: problem_budgets.airfoil.wall_s: 900 -> 2700.
  • warning [darcy] The OOD set is not clearly outside the training data: 9 % of the OOD inputs outside the training distribution (threshold 50 %), 1 of 100 outside the applicability envelope. Suggestion: move the OOD inputs further out. (Its OOD column is read as a second test set.)
  • The setup audit of the potential-flow PINN found that wall and Kutta weights of 1 instead of 10 lower its velocity error from 0.81 to 0.51: a wider search would have helped it.

The gates also caught problems in the benchmark itself before it was published (a preflight that did not know freestreamPressure, a y+ estimate fed the wrong argument, a convergence metric a potential-flow model fails by construction); each was fixed with a test.

Your own problem or method

Wrap your arrays (inputs and outputs on a grid) and FNO and DeepONet apply at once; pass pinn_setup to add PINNs:

prob = pp.benchmark.ArrayProblem("my_flow", train=(X_tr, Y_tr), val=(X_va, Y_va), test=(X_te, Y_te),
                                 ood=(X_ood, Y_ood), data_cost_s=3600.0)
pp.benchmark.run(problems=[prob], methods=["fno", "deeponet"], budget=pp.ComputeBudget(wall_s=120), out_dir="r")

Or register new ones (they are then usable by id, in YAML and in parallel workers):

@pp.benchmark.register_method("my_net")
class MyNet(pp.benchmark.Method):
    title = "My net"
    def supports(self, problem): return isinstance(problem, pp.benchmark.FieldOperatorProblem)
    def space(self, problem): return {"lr": [1e-3, 3e-3], "width": [64, 128]}
    def train(self, problem, prepared, cfg, seed, budget, final=True, **job):
        ...  # train under `budget` with pinneapple_analysis.cost.train_within_budget; return a result dict
    def architecture_checks(self, problem, prepared, cfg): ...  # your gates (shapes, overfit, ...)

@pp.benchmark.register_problem("my_problem")
class MyProblem(pp.benchmark.FieldOperatorProblem):
    def generate(self, split, n, n_workers): ...   # (inputs, outputs, solver core-seconds)
    def verify_solver(self): ...                   # checks of your solver against a known answer

The built-in problems: burgers, darcy, navier_stokes (the operator-learning standards), airfoil (the aero app's RANS airfoil; needs OpenFOAM), inverse_heat (h from sensors). Methods: pinn, fno, deeponet, mgn, mlp, mlp_fields, pinn_rans. See Choosing a method for what the published benchmark found.