PEST++ 5.2.27 compiled to WebAssembly, plus the one adapter it needs to drive a
forward model that is not a child process. pestpp-ies and pestpp-glm run in
Node or in a browser Worker and hand every forward run to a host callback.
This repository publishes an artifact. It contains no application and no user interface. That separation is deliberate, see Licence boundary.
Built and gated. The build emits dist/pestpp.js, dist/pestpp.wasm,
dist/pestpp-glm.js and dist/pestpp-glm.wasm from the pinned PEST++ 5.2.27
sources and a numbered patch set applied at build time.
npm ci
npm run build
npm test # Node, Chromium and Firefox gates
npm run test:parity # the same cases against native PEST++ 5.2.27The parity gate hides the conductivity multipliers [3, 1, 0.3] of the pyEMU
Freyberg MODFLOW 6 case, generates the observations by running the model with
them, and requires the optimiser to find them back. It then repeats both runs
with the pinned official macOS binaries and compares. Last run, 2026-08-11:
| WASM | Native 5.2.27 | |
|---|---|---|
| pestpp-ies recovered | [2.9655, 1, 0.3005] | [2.9568, 1, 0.3005] |
| pestpp-glm recovered | [2.9999, 1, 0.3000] | [3.0000, 1, 0.3000] |
The largest parameter difference is 0.295% against a stated 0.5% tolerance,
and the WASM forward model reproduces native MODFLOW 6.7.0 heads byte for
byte. The gate downloads both native archives and verifies their SHA-256
hashes before use. ROADMAP.md section 8 records the full evidence.
PEST++ already implements the algorithms. They are the thing being ported, not
something to be rewritten. What PEST++ cannot do in a browser is run the model:
its run managers either fork/exec a child process or coordinate workers
over TCP sockets, and a page can do neither. The whole port reduces to that
one seam.
Every PEST++ algorithm talks to one abstract class, and that class has exactly one pure virtual method:
// src/libs/run_managers/abstract_base/RunManagerAbstract.h
virtual void run() = 0;Queueing runs, retrieving results, storage and serialisation are all concrete
and shared. IterEnsembleSmoother and its siblings receive a
RunManagerAbstract * and never learn what is behind it.
This repository adds one subclass whose run() hands the queued parameter
sets to a host callback instead of spawning processes, and compiles the suite
with Emscripten. The callback is documented in
docs/host-contract.md and is the artifact's entire
public interface.
PEST++ is GPL-3.0. Everything produced here is therefore GPL-3.0, and this repository is the corresponding source for the artifact it builds.
The boundary is what keeps a consuming application off that licence, and it is structural rather than a matter of intent:
| Inside this repository, GPL-3.0 | Outside, any licence |
|---|---|
| PEST++ sources and patches | The application and its interface |
| The run manager adapter | The MODFLOW worker that runs a model |
| Build recipe and artifact | Anything that only exchanges messages |
Never compile consumer code into the module. The host implements the narrow, documented callback and nothing more. Native PEST++ already invokes arbitrary model executables without making them derivative works, and the adapter preserves exactly that relationship.
That reasoning is an engineering judgement, not legal advice, and how GPL linking doctrine applies to WebAssembly modules is untested. Take counsel before shipping commercially.
src/holds the browser run manager, shaped likesrc/libs/run_managers/externalso it reads as part of PEST++.build/holds the numbered patches, the Dockerfile and the build recipe. BUILD.md is the patch audit trail with measured evidence.docs/holds the host contract and the blocking bridge decision.fixtures/holds the pinned Freyberg case and the native reference outputs the gates compare against.- ROADMAP.md records the investigation and the implementation evidence, written to be picked up by someone with no prior context.