@casadi/casadi-wasm
Version:
CasADi — symbolic framework for algorithmic differentiation and numerical optimization, compiled to WebAssembly. Runs in Node.js with on-demand solver plugins (ipopt, fatrop, sundials, ...).
206 lines (167 loc) • 9.18 kB
Markdown
# CasADi examples in JavaScript / WebAssembly
These are JavaScript ports of [`docs/examples/python`](../python), running
against the **wasm-js** build of CasADi: the C++ library compiled to
WebAssembly with a JavaScript API generated by SWIG. The same examples
run two ways:
- **Node** — `node rosenbrock.js` (headless, scriptable, CI-friendly)
- **Browser** — open `rosenbrock.html`, no install, CasADi runs client-side
Every example is **one file shared by both**: a small `example(M, log)`
function containing nothing but CasADi calls. A Node entry point at the
bottom runs it from the command line; the matching `.html` page runs the
*identical* function in the browser. Read any `.js` to learn the syntax;
read this file first to learn the handful of ways JS differs from Python.
---
## Running under Node
```bash
node rosenbrock.js
```
The script finds the wasm build at `../../../build-wasm/swig/wasm-js/casadi.js`.
Point elsewhere with an env var:
```bash
CASADI_JS=/path/to/wasm-js/casadi.js node rosenbrock.js
```
## Running in a browser
The page must be **served over HTTP** — not opened as a `file://` URL.
This is the only "CORS-ish" gotcha, and it is *not* a cross-origin
problem: browsers simply refuse to `fetch()` a sibling `.wasm` over
`file://`. Serve from a real (even local) web server and everything is
same-origin. The browser also needs the build artifacts reachable next
to the page. From this directory:
```bash
# 1. make the four wasm artifacts available here (symlink or copy)
ln -sf ../../../build-wasm/swig/wasm-js/casadi.js \
../../../build-wasm/swig/wasm-js/casadi_wasm.js \
../../../build-wasm/swig/wasm-js/casadi_wasm.wasm \
../../../build-wasm/swig/wasm-js/casadi_wasm.data .
# 2. serve and open
python3 -m http.server 8000
# -> http://localhost:8000/rosenbrock.html
```
(The symlinks are `.gitignore`d.) If the artifacts live elsewhere, skip
the symlinks and pass their URL: `rosenbrock.html?casadi=/some/path/`.
How the browser loads a Node-targeted module: the generated `casadi.js`
wrapper is CommonJS, but only its first ~10 lines use Node APIs
(`require`, `__dirname`); the other ~25k lines are plain ECMAScript.
[`_casadi_browser.js`](./_casadi_browser.js) evaluates it inside a tiny
sandbox that supplies just those few symbols — so the unmodified file
runs in the browser. You never call it directly; the HTML does.
---
## CasADi in JavaScript: what differs from Python
The C++ API is the same. Only the *language surface* changes.
| Concept | Python | JavaScript |
|---|---|---|
| Load the library | `import casadi as ca` (sync) | `const M = await loadCasadi(...)` (async) |
| Construct symbol | `ca.SX.sym("x")` | `M.SX.sym("x")` |
| Construct, no `new` | `ca.SX(2)` | `M.SX(2)` (classes are callable; `new` also works) |
| Add / multiply | `a + b`, `a * b` | `M.plus(a, b)`, `M.times(a, b)` — **no operator overloading** |
| Power | `a ** 2` | `M.power(a, 2)` |
| Number literal in op | `2 * x` | `M.times(2, x)` — bare numbers/arrays coerce automatically |
| Matrix product | `A @ b` | `M.mtimes(A, b)` |
| Element index | `x[k]`, `x[k] = v` | `x[k]`, `x[k] = v` (same!) |
| 2-D index | `A[i, j]` | `A[[i, j]]` — note the inner array (`A[i,j]` is the comma operator in JS) |
| Slice | `x[1:3]`, `A[:, 1]` | `x["1:3"]`, `A[[":", 1]]` (string slices) |
| Nonzeros | `x.nz[k]` | `x.nz[k]`, `x.nz[k] = v` (same!) |
| Concatenate (variadic) | `ca.vertcat(x, y, z)` | `M.vertcat(x, y, z)` |
| Concatenate (list) | `ca.vertcat(*xs)` | `M.vcat(xs)` |
| Solver IO | dict `{"x0": ...}` | object `{ x0: ... }` |
| Read DM values | `np.array(dm)` / `dm.full()` | `dm.nonzeros()` → `number[]` |
| Integer args | `f.size1()` → `int` | `f.size1()` → `bigint` (note the `n` suffix) |
| Function repr | `str(f)` / `print(f)` | `String(f)`, `` `${f}` ``, `console.log(f)`, or `f.str()` |
| Const MX → value | `mx.to_DM()` | `M.evalf(mx)` → DM (MX has no `to_DM`) |
| Opcode enums | `ca.OP_ADD` | `M.Operation.OP_ADD` (`bigint`, matches `instruction_id()` — compare with `===`) |
The single biggest adjustment is **no operator overloading**: write
`x*x` as `M.times(x, x)`. Bare number/array literals *are* coerced into
the matching matrix type, so `M.plus(2, x)`, `M.times(2.5, y)`, and
`M.plus(M.DM(1), 4)` all work — no need to wrap `2` as `M.SX(2)`.
### The example shape
```js
// foo.js — the body is pure CasADi and environment-agnostic
async function example(M, log) {
const x = M.SX.sym("x");
const f = new M.Function("f", [x], [M.sin(x)]);
log("f(0.5) = " + f.call([M.DM(0.5)])[0].nonzeros()[0]);
}
// Node entry point (skipped in the browser)
if (typeof require !== "undefined" && require.main === module) {
const path = require("path");
const casadiPath = process.env.CASADI_JS
|| path.resolve(__dirname, "../../../build-wasm/swig/wasm-js/casadi.js");
require(casadiPath)().then((M) => example(M, console.log));
}
if (typeof module !== "undefined" && module.exports) module.exports = example;
```
`log` is the output sink: `console.log` under Node, a DOM writer in the
browser. Keep `example()` free of `require`, `process`, and DOM access
so both runners can share it.
### Editor autocomplete
`jsconfig.json` wires up the generated `casadi.d.ts` (symlinked as part
of the browser setup above; or symlink it directly). Annotate the
module parameter to get full completion:
```js
/** @param {typeof import("casadi")} M */
async function example(M, log) { /* M. <-- autocompletes the whole API */ }
```
---
## Adding a new example
1. Copy the corresponding `docs/examples/python/<name>.py` mentally and
translate it into `<name>.js` using the example shape above.
2. Make the page: `sed 's/__NAME__/<name>/g' _template.html > <name>.html`.
3. Test: `node <name>.js`, then serve and open `<name>.html`.
## Coverage
Ported from `docs/examples/python`, alphabetically — **25 of 45**. Each
ported `.js` runs under Node (and in the browser via its `.html`).
Examples needing capabilities not present in this wasm build are skipped
with the reason; see also the [capability matrix](#plugin-availability).
### Ported (25)
| Example | Notes |
|---|---|
| `accessing_mx_algorithm` | MX algorithm introspection |
| `accessing_sx_algorithm` | SX algorithm introspection |
| `biegler_10_1` | collocation NLP (ipopt) |
| `c_code_generation` | `CodeGenerator.dump()` C text only (no compile in wasm) |
| `chain_qp` | hanging-chain QP (qpoases) |
| `dae_collocation` | hand collocation OCP (ipopt) |
| `dae_multiple_shooting` | integrator `idas`→`collocation` |
| `dae_single_shooting` | integrator `idas`→`collocation` |
| `direct_collocation` | flagship; hand collocation OCP |
| `direct_multiple_shooting` | RK4 multiple shooting |
| `direct_single_shooting` | RK4 single shooting |
| `implicit_runge-kutta` | IRK via `newton` rootfinder |
| `multipoint_simulation` | integrator `cvodes`→`rk` |
| `nlp_codegen` | ipopt solve + `CodeGenerator.dump()` (no compile) |
| `nlp_sensitivities` | sqpmethod + qrqp; fwd/rev/FD sensitivities |
| `parallel_map` | serial `Function.map` (no threads in wasm) |
| `race_car` | Opti min-time OCP |
| `rocket` | NLP OCP |
| `rosenbrock` | NLP (ipopt) |
| `sensitivity_analysis` | integrator `cvodes`→`rk`/`collocation` |
| `simple_lp` | LP via conic (qpoases) |
| `simple_nlp` | NLP (ipopt) |
| `vdp_collocation` | hand Radau collocation NLP |
| `vdp_dynamic_programming` | numeric DP over a grid |
| `vdp_indirect_multiple_shooting` | indirect method, `cvodes`→`rk` |
### Skipped (20) — capability not in the wasm build
| Example | Reason |
|---|---|
| `bouncing_ball` | cvodes event/zero-crossing detection (no rk/collocation equivalent) |
| `bouncing_ball_daebuilder` | FMU export + compile |
| `breaking_spring` | DaeBuilder + cvodes events |
| `callback` | Callback director numeric `eval` aborts (`DM: arg-type mismatch`) |
| `daebuilder` | `DaeBuilder.der()` const overload throws in the binding |
| `dae_reduced_index` | `dae_reduce_index` aborts (by-ref dict marshalling) |
| `debug_fatrop` | matplotlib sparsity-spy of `.mtx` files; no numeric output |
| `fmu_collocation` `fmu_demo` `fmu_demo2` `fmu_export` `modelica_fmu_import` | FMU / DaeBuilder-FMI runtime |
| `ipopt_nl` | reads an AMPL `.nl` from disk (no JS-accessible FS) |
| `lotka_volterra_minlp` | MINLP (bonmin) |
| `lqr_control` `mhe_spring_damper` `simulation` `vdp_collocation2` | `casadi.tools.struct` / scipy (Python-only) |
| `sysid` | ipopt fatal on NaN line-search trial (no backtracking recovery in this build) |
| `vdp_indirect_single_shooting` | unstable augmented ODE needs a stiff integrator (idas/cvodes) |
### Plugin availability
Works: `nlpsol` (ipopt, fatrop, sqpmethod), `conic` (qrqp, qpoases, osqp,
highs, ipqp), `integrator` (rk, collocation), `rootfinder` (newton,
fast_newton), `CodeGenerator.dump()`. Not available: cvodes/idas/kinsol
(sundials), FMU/DaeBuilder-FMI, compiling generated C, `casadi.tools`,
MINLP solvers, threads.
> Known binding rough edges (candidates for a future pass): `expand=true`
> can crash teardown; Callback director numeric eval; ipopt aborting on
> NaN trial points.