Skip to content

Pre-trained models and datasets (pp.zoo, pp.datasets)

PINNeAPPle ships a small set of pre-trained surrogates and ready-made datasets. You can load each one in one line, then test, evaluate, validate, benchmark and fine-tune it.

Only items that passed a quality gate are included:

  • Models: the held-out error against an independent reference is documented and reproducible, the physics checks of the trust report pass, and the validity range is recorded.
  • Datasets: they were generated by a verified solver with convergence evidence, or come from a public source with a compatible licence; their schema, splits and provenance are documented.

The zoo catalogue lists every model and dataset with its metrics, size and Hugging Face link. The candidates that did not pass, and why, are listed in docs/dev/model_dataset_inventory.md.

Model What it predicts Held-out error (reference) Valid for
airfoil_cst_rans_mlp Cl, Cd, Cm of a CST airfoil, with ensemble uncertainty Cd 0.54 % and Cl ±0.003 on 15 unseen shapes (OpenFOAM RANS); Cd 2.8 % vs XFOIL α −2…14°, Re 4·10⁶, CST bounds
heatsink_forced_mlp Plate-fin heat-sink resistance (K/W), pressure drop (Pa), 95 % upper bound R 0.71 %, ΔP 1.0 % on 900 designs (verified physics engine) Ducted air, 0.5–6 m/s, 10–400 W
heatsink_natural_mlp Plate-fin heat-sink resistance (K/W) in natural convection, 95 % upper bound; trained with monotonicity constraints R 0.47 % mean, 1.5 % p95 on 750 designs (verified physics engine) Free air, vertical fins, 1–80 W
rover_contact_grc1_viper (v1.1.0) Drawbar pull, normal load, torque of a rover wheel on lunar regolith Fx 0.22 N, Fz 0.47 N RMSE (ranges 1.4 and 1.4 kN), Wong–Reece with slip-sinkage Slip −0.4…0.9, sinkage 2–60 mm
Dataset Content
airfoil_rans_cst_re4e6 436 OpenFOAM RANS runs, the grid study and an XFOIL polar
heatsink_forced_convection, heatsink_natural_convection 6000 and 5000 heat-sink designs solved by the physics engine
rover_contact_grc1_viper Wong–Reece contact forces over slip × sinkage
single_wheel_tests Published single-wheel testbed points (validation data)
burgers_1d_li2021 1-D viscous Burgers, ν = 0.1: 1200 trajectories on 1024 points, t = 0…1 (Li et al. 2021 input distribution); Hub-only, 55 MB
darcy_2d_li2021 2-D Darcy flow: 1200 piecewise-constant permeability / pressure pairs on 128 × 128 (Li et al. 2021); Hub-only, 71 MB
navier_stokes_2d_li2021_nu1e-3 2-D Navier–Stokes vorticity, ν = 10⁻³: 1200 runs on 64 × 64, t = 0…50 (Li et al. 2021); Hub-only, 12 shards, 919 MB
thermal_channel_cfd Actuated heated channel (2-D incompressible flow + heat, Re 100): 82 trajectories of u, v, p, T on 128 × 32 × 101 snapshots, with two unseen actuation families for testing; Hub-only, 8 shards, 360 MB
airfoil_rans_cst_re4e6_fields The cell fields (p, Ux, Uy, nut on the 20064-cell O-grid) of the 436 OpenFOAM runs of airfoil_rans_cst_re4e6, same splits; Hub-only, 5 files, 186 MB
cylinder_wake_lbm Confined cylinder wake (lattice Boltzmann, blockage 0.2): 720 vorticity snapshots of 9 saturated runs at Re 20–100, labelled by regime, Strouhal number and wake amplitude; Hub-only, 3 files, 64 MB

Quickstart

The complete script is examples/model_zoo/quickstart.py.

1. Browse

import pinneapple as pp

pp.zoo.list()        # ['airfoil_cst_rans_mlp', 'heatsink_forced_mlp', 'heatsink_natural_mlp', ...]
pp.datasets.list()   # ['airfoil_rans_cst_re4e6', 'heatsink_forced_convection', ...]

card = pp.zoo.info("airfoil_cst_rans_mlp")      # a ModelCard
card.task, card.inputs, card.outputs            # names and units
card.validity_range["alpha_deg"]                # [-2.0, 14.0]
card.validation_metrics["test/Cd_mape_pct"]     # 0.545
print(card.to_markdown())                       # the full card: training data, licence, citation, trust report

2. Load and predict

model = pp.zoo.load("airfoil_cst_rans_mlp")     # weights checked against the manifest's SHA-256
naca2412 = dict(T0=0.169591, T1=0.144959, T2=0.131433, M0=0.024922, M1=0.071088, M2=0.058960)
out = model.predict(**naca2412, alpha_deg=[0, 2, 4, 6])
out["Cl"], out["Cd"], out["Cl_std"]             # ensemble mean and spread

predict takes named columns (keyword arguments or a mapping) or an (n, n_inputs) array in model.input_names order. It warns with pp.zoo.OutOfRangeWarning for rows outside the validity range. model.in_range(...) returns the mask.

3. Compare with the reference

ev = pp.zoo.evaluate("airfoil_cst_rans_mlp")
ev["matches_card"]                              # True: the card's metrics are reproduced
ev["metrics"]["xfoil_naca2412/Cd_mape_pct"]     # 2.8 -- against XFOIL, an independent solver

xfoil = pp.datasets.load("airfoil_rans_cst_re4e6", split="xfoil_naca2412")
pred = model.predict({k: xfoil[k] for k in model.input_names})

evaluate(name, dataset) also accepts any of the following, and reports the share of cases outside the validity range:

  • a dataset name with split=;
  • a DatasetSplit;
  • an (inputs, outputs) pair;
  • a mapping of columns.

4. Validate

report = pp.zoo.validate("airfoil_cst_rans_mlp", inputs={**naca2412, "alpha_deg": [4.0, 18.0]})
report.decisao                  # "REVIEW": alpha = 18 deg is outside the validity range (and a warning is raised)
print(report.to_markdown())     # card metrics reproduced, acceptance limits, physics checks

The physics checks are model-specific:

  • Airfoil: lift slope against thin-airfoil theory, Cl monotone below stall, symmetric-airfoil antisymmetry, Cd > 0.
  • Heat sink: monotonic effects of airflow, conductivity and source size; positivity.
  • Rover:
    • load grows with sinkage;
    • pull grows with slip before its peak;
    • the Mohr–Coulomb limit holds;
    • My ≥ R·Fx.

5. Benchmark

res = pp.zoo.benchmark("airfoil_cst_rans_mlp", budget_s=30, seeds=3)
res.table(); print(res.to_markdown())

This runs pp.benchmark (the equal-budget benchmark API) on the model's own dataset. It compares the shipped model with the same architecture trained from scratch on the same training split under the same compute budget, and evaluates both on the same held-out test set. The run includes pp.benchmark's gates, determinism check, report and trust card. It needs pinneapple_analysis.benchmark.

6. Fine-tune, with a held-out guard

new = pp.datasets.load("airfoil_rans_cst_re4e6", split="naca_reference")   # or (X_new, Y_new), or a dict of columns
ft = pp.zoo.finetune("airfoil_cst_rans_mlp", new, epochs=50, lr=1e-5)
print(ft.summary())             # guard metrics on the ORIGINAL test set, before -> after
ft.save("my_airfoil")           # refused (FinetuneRejectedError) if any got worse by > 2 %, unless force=True
pp.zoo.load_dir("my_airfoil")

How the fine-tune works:

  • It trains a copy of the model; the original is never modified.
  • By default (replay=True) the original training split is mixed in, to limit forgetting. NaN targets (e.g. a missing Cm) are ignored.
  • save_to= / registry= (a pinneapple_registry.ArtifactRegistry) save in the same call, and are refused the same way.
  • A forced save records the regression in the new card's data_lineage["guard"].

The guard is not a formality. Fine-tuning this airfoil model on 36 extra NACA runs makes it worse on the 15 held-out shapes, and the guard refuses to save it.

7. Check each query, export to ONNX, run in a browser

validate judges a model as a whole. query_checks judges one input. It answers four questions:

  • Is the input inside the validity range?
  • Are the outputs physical?
  • Does the ensemble agree as well as it did on held-out data?
  • Do the model's physics checks hold at this point?

Each check carries the rule ID that pinneapple check uses: PC601 (out of range), PC602 (sparse region), PC402 and PC403 (unphysical or non-finite output), and PC302 (model residual). The verdict is the worst status among them:

  • fail: the query is outside the range, so the answer is an extrapolation.
  • warn: use the answer with caution.
  • pass: every check holds.

Design constraints, such as the airfoil's t/c ≥ 11 % and |Cm| ≤ 0.10, are reported but never change the verdict.

r = pp.zoo.query_checks("airfoil_cst_rans_mlp", T0=.17, T1=.15, T2=.13, M0=.02, M1=.07, M2=.06, alpha_deg=16)[0]
r["verdict"]["status"]                                # 'fail': alpha 16 deg is beyond the trained range
[(c["id"], c["status"], c["detail_text"]) for c in r["checks"] if c["status"] != "pass"]
pp.zoo.check_spec("airfoil_cst_rans_mlp")             # the declarative spec: values, limits, templates, verdicts

export_onnx writes one ONNX graph that takes the card's physical inputs and returns its physical outputs. The graph includes the pre-processing (CST encoding, log features, normalisation) and the post-processing (exp, ensemble mean and spread, conformal bound).

The export is refused if the graph differs from the PyTorch model by more than 1e-5. The comparison runs on every held-out and independent-reference input of the zoo. With checks=True, the graph also takes a query input and returns the values of the query checks, so any ONNX runtime gives the same verdict as Python.

info = pp.zoo.export_onnx("heatsink_forced_mlp", "heatsink.onnx", checks=True)
info["parity"]                                        # 1.6e-06, against PyTorch on the 900 held-out designs
pp.zoo.onnx_parity("heatsink_forced_mlp", "heatsink.onnx")

For the browser, python -m pinneapple_hub.zoo.web data/web/zoo writes each model's graph and a JSON file. The JSON holds the model card, the check spec and the reference data to plot. The same command writes the CST airfoil geometry graphs from pinneapple_design.aero.geometry_onnx.

The JS runtime that reads these files ships with the package in pinneapple_hub/zoo/web/:

  • onnx.js, a dependency-free ONNX interpreter;
  • zoo.js, with loadModel, loadAirfoilGeometry and evaluate. evaluate is the JS twin of pinneapple_hub.zoo.checks.evaluate.

tests/js/zoo_web.test.mjs checks the runtime against Python. The Playground is built on it.

Datasets

info = pp.datasets.info("heatsink_forced_convection")
print(info.to_markdown())                       # splits, columns with units, provenance, verification, licence
test = pp.datasets.load("heatsink_forced_convection", split="test")
test["r_base_k_w"], test.columns, test.n_rows
X, Y = test.xy(["base_width_mm", "power_w"], ["r_base_k_w"])
test.to_pandas()

The zoo datasets are also registered in pinneapple_data.datasets.DatasetRegistry as zoo/<name>.

Operator-learning benchmarks

burgers_1d_li2021, darcy_2d_li2021 and navier_stokes_2d_li2021_nu1e-3 are the standard problems of Li et al. (2021, the Fourier Neural Operator paper), with the input distributions of that paper. They are generated by PINNeAPPle's own verified solvers (pinneapple_simulation.numerical_solvers.operator_datasets). Each dataset card gives:

  • the resolution and time-step study (discretisation error 4e-12 for Burgers and about 8e-4 for Darcy, in relative L2);
  • the solver checks: the Cole–Hopf exact solution for Burgers, a manufactured solution with second-order convergence for Darcy;
  • the seeds and the 1000 / 200 train / test split.
train = pp.datasets.load("darcy_2d_li2021", split="train")      # downloads ~59 MB once
a, u = train["a"], train["u"]                                    # (1000, 128, 128) each

Airfoil flow fields

airfoil_rans_cst_re4e6_fields holds the cell fields of the same 436 OpenFOAM runs as airfoil_rans_cst_re4e6, regenerated with pinneapple_design.aero.dataset (the coefficients match the campaign; only runs flagged unsteady differ, within their oscillation). Each row is one run: CST weights, angle of attack, coefficients, status, and the cell centres and p, Ux, Uy, nut at the 20064 cells (free stream U = 1, chord 1). The grid moves with the shape, so x and y are stored per run. The test split holds the 15 shapes held out by the airfoil surrogates.

test = pp.datasets.load("airfoil_rans_cst_re4e6_fields", split="test")    # one 26 MB file
steady = test["status"] != "unsteady"
p, x, y = test["p"][steady], test["x"][steady], test["y"][steady]           # (n, 20064) each

Heated-channel digital twin

thermal_channel_cfd is the actuated heated-channel problem of PINNeAPPle-Benchmark experiment 05, generated by the library's 2-D incompressible solver (pinneapple_simulation.numerical_solvers.thermal_channel_datasets). The inlet velocity U(t) and the heater flux Q(t) vary in time; each trajectory stores the fields, the actuators and the quantities of interest (outlet temperature, maximum temperature, pressure drop, heat balance). The card gives the solver checks (Poiseuille, Nusselt number 5.388 vs 5.385), the three-grid GCI of the dataset grid (1–4 %) and the split plan: 48 / 8 / 12 trajectories of random steps and ramps, plus 8 chirped-sinusoid and 6 edge-step trajectories from actuation families never seen in training.

test = pp.datasets.load("thermal_channel_cfd", split="test_unseen_sinusoid")   # one 37 MB file
fields = test["fields"]                                   # (8, 101, 4, 128, 32): u, v, p, T every 0.4 time units

Hosting, cache and offline use

Every file is listed in pinneapple_hub/zoo/manifest.json with its SHA-256, size, version and its Hugging Face Hub location (huggingface.co/PINNeAPPle-Labs/<name>, tag revision: v<version>, or v<version>.postN when the same files were republished with a new card or derived formats; the loader then falls back to v<version> while the new tag does not exist yet, and checks the SHA-256 either way). A file is resolved in this order:

  1. Bundled with the package. This applies only to small items (under about 2 MB each). Larger datasets are Hub-only: one <split>.npz per split, possibly sharded into <split>-NN.npz, and pp.datasets.load(name, split=...) downloads only the files of the requested split.
  2. The cache. $PINNEAPPLE_ZOO_CACHE, by default ~/.cache/pinneapple/zoo.
  3. The Hub. The file is downloaded once with huggingface_hub (or plain HTTPS), checked and cached.

Each step can fail with a specific error:

  • A checksum that does not match raises pp.zoo.ChecksumMismatchError. A corrupt cache entry is fetched again when online.
  • With PINNEAPPLE_OFFLINE=1 (or HF_HUB_OFFLINE=1), nothing is downloaded, and a file that is not available locally raises ArtifactUnavailableError.
  • An item the Hub does not have yet raises ArtifactNotPublishedError, which says how to publish it.

pp.zoo.status(name) and pp.datasets.status(name) show where each file resolves from, without downloading it.

Versions and the physics a model was trained on

A new release of a model keeps the old one: the manifest lists it under superseded, with its files, their SHA-256, its Hub tag and the reason it was superseded. pp.zoo.versions(name) lists every release, and pp.zoo.load(name, version="1.0.0") loads an old one (from the Hub, with a SupersededModelWarning that gives the reason).

A surrogate is only a surrogate of the physics it was trained on. The rover contact model records that physics in its card (data_lineage.physics) and its config.json: the full WongReeceModel.physics_config(), i.e. the soil and all its parameters, the wheel, gravity and the model options. ZooContactSurrogate(pp.zoo.load(...)) .check_physics(model) lists what differs from the model it is used with and raises a PhysicsMismatchWarning; vehicle.dashboard.vehicle_dashboard does this and notes any difference on the PINN chart. A test fails when the library's GRC-1, the VIPER wheel or the Wong-Reece options change without a new zoo release.

rover_contact_grc1_viper v1.0.0 was trained before slip-sinkage was added to GRC-1 (n1 = 0). Against the current Wong-Reece model it errs by up to ~200 N at high driving slip. v1.1.0 is trained on the current physics (n1 = 0.8) with the same held-out protocol (2000 Latin-hypercube states, seed 1000, disjoint from training). With slip-sinkage the pull need not grow with slip at a fixed sinkage, so its gate checks that dFx/ds has the sign of the recorded reference instead of dFx/ds ≥ 0.

On the Hugging Face Hub

What each repo holds besides the manifest's files, so the Hub's own tools work:

  • Datasets: Parquet for the dataset viewer and datasets.load_dataset. The npz files stay the source of truth (the library reads them, SHA-256 checked). pinneapple_hub.zoo.parquet.write_dataset derives Parquet files from them, deterministically, under data/<config>/<split>-NNNNN.parquet. Scalar columns are plain columns; array columns are fixed-size list columns of their shape (List(List(float32, length=128), length=128); use with_format("numpy")). A sample larger than 512 KiB whose arrays share a leading axis (Navier-Stokes vorticity in time, the heated-channel fields in time) is stored as one row per step, with sample and step columns, so the viewer can show every row. Shards stay below 256 MiB uncompressed and row groups below 16 MiB. Splits with other columns (a grid, a convergence study, an XFOIL polar) are separate configs. The README's YAML (configs with the data_files of each split, dataset_info features and split sizes, licence, task category, size category, tags) is written by pinneapple_hub.zoo.hub_cards.dataset_card, with the columns, units, provenance and both ways of loading. pinneapple_hub.zoo.parquet.read_parquet puts a split back together; a test checks the round trip npz -> Parquet -> arrays bit for bit.
  • Models: safetensors, ONNX and an honest card. Next to weights.pt the repo holds model.safetensors (the same tensors; pinneapple_hub.zoo.export.load_safetensors), model.onnx (pre- and post-processing in the graph, parity with PyTorch within 1e-5) and example.json (a held-out case, the model's outputs for it and the verdicts). The card says library_name: pinneapple, pipeline_tag: tabular-regression and inference: false: no Hub inference provider can run these models with their encoding, ensembles and validity checks, so the widget is off. Its "How to use" code prints the numbers of example.json, and the publishing workflow runs it.
  • The demo Space PINNeAPPle-Labs/zoo-demo, linked from every model card, replaces the widget. It is a static Space (no server, free for the organisation): pinneapple_hub.zoo.demo.export_space computes, from the models published on the Hub, every model's prediction along a sweep of each input with its ensemble spread or conformal bound and the verdict of its query checks at every point, and writes them with each model's ONNX graph (prediction and query checks in one graph) and the browser runtime (pinneapple_hub/zoo/web). The page (apps/zoo_space, UI only, with the Playground's charts) plots a sweep, shades where the checks warn (yellow) or fail (red), and shows the checks at the chosen input and the model's trust decision. Evaluate in your browser runs the graph on the visitor's own inputs. tests/js/zoo_space.test.mjs runs every precomputed sweep again through the browser runtime and requires the same numbers and the same verdicts as Python.

To publish (maintainers):

  1. Add a repository secret HF_TOKEN with write access to the PINNeAPPle-Labs organisation.
  2. Run the Publish zoo to the Hugging Face Hub workflow. It also runs on every push to main that changes the zoo.

Hub-only items are not in the repository. To upload them, put their files on a branch as <name>/<file> (for example the output of scripts/zoo/operator_data.py generate) and run the workflow by hand with staging_ref set to that branch. Every staged file must match the SHA-256 in the manifest. Runs without staging_ref convert the Hub-only datasets from the copy already on the Hub (downloaded, SHA-256 checked) and upload only their Parquet files and card.

After the upload the workflow checks, anonymously, what the Hub serves, and fails when something is wrong:

  1. every manifest file at the tag (SHA-256);
  2. the dataset viewer of every dataset: datasets-server is-valid (preview and viewer, retried while the Hub processes the push), splits (every split of the manifest is listed) and first-rows of every split;
  3. every model: loaded from the Hub with the bundled copy ignored, its card metrics reproduced on the stored reference, the card's example reproduced by the PyTorch, safetensors and ONNX copies, the card's front matter, and its "How to use" code run;
  4. the Space reaches RUNNING and serves its page, index.json and every model's sweeps.

To republish files that did not change (a new card, new derived formats), bump the item's revision to the next v<version>.postN: the old tags stay as they are, so code pinned to them keeps working. Inputs of the manual run: only (some items), tags=false (upload to main without creating the tags, e.g. to try a branch; the model checks then wait for the tagged run) and space.

Without the secret, the workflow checks the manifest and skips the upload with a warning. python scripts/zoo/publish_to_hub.py --dry-run builds the derived files and cards and lists the uploads; --verify-viewer, --verify-models and --verify-space run the checks from any machine that reaches the Hub.