@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, ...).
161 lines (135 loc) • 6.19 kB
JavaScript
//
// MIT No Attribution
//
// Copyright (C) 2010-2023 Joel Andersson, Joris Gillis, Moritz Diehl, KU Leuven.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify,
// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
//
// JS port of docs/examples/python/implicit_runge-kutta.py.
//
// Build a fixed-step implicit Runge-Kutta integrator by hand: a collocation
// system solved with a rootfinder, iterated over finite elements, then
// forward/adjoint directional derivatives via Function.factory.
//
// JS notes (see README.md):
// * The Python example also builds a 'cvodes' reference integrator and runs
// the same sensitivity checks on it. cvodes is not in the wasm build, so
// we keep only the hand-built IRK integrator.
// * The rootfinder uses 'newton' (kinsol is not available in wasm).
async function example(M, log) {
// End time
const tf = 10.0;
// Dimensions
const nx = 3;
const np = 1;
// Declare variables
const x = M.SX.sym("x", nx); // state
const p = M.SX.sym("u", np); // control
const [xa, xb] = M.vertsplit(x); // x[0], x[1]
// ODE right hand side
const ode = M.vertcat(
M.plus(M.minus(M.times(M.minus(1, M.times(xb, xb)), xa), xb), p),
xa,
M.plus(M.plus(M.times(xa, xa), M.times(xb, xb)), M.times(p, p)));
const f = new M.Function("f", [x, p], [ode]);
// Number of finite elements
const n = 100;
// Size of the finite elements
const h = tf / n;
// Degree of interpolating polynomial
const d = 4;
// Collocation points
const tau_root = [0, ...M.collocation_points(d, "legendre")];
// Coefficients of the collocation (C) and continuity (D) equations
const C = Array.from({ length: d + 1 }, () => new Array(d + 1).fill(0));
const D = new Array(d + 1).fill(0);
// Dimensionless time inside one control interval
const tau = M.SX.sym("tau");
for (let j = 0; j <= d; j++) {
// Lagrange polynomial basis at collocation point j
let Lp = M.SX(1);
for (let r = 0; r <= d; r++)
if (r !== j) Lp = M.times(Lp, M.times(M.minus(tau, tau_root[r]), 1 / (tau_root[j] - tau_root[r])));
// Continuity coefficient
const lfcn = new M.Function("lfcn", [tau], [Lp]);
D[j] = lfcn.call([M.DM(1.0)])[0].nonzeros()[0];
// Collocation coefficients
const tfcn = new M.Function("tfcn", [tau], [M.tangent(Lp, tau)]);
for (let r = 0; r <= d; r++) C[j][r] = tfcn.call([M.DM(tau_root[r])])[0].nonzeros()[0];
}
// Variables for one finite element
const X0 = M.MX.sym("X0", nx);
const Pm = M.MX.sym("P", np);
const V = M.MX.sym("V", d * nx);
// State at each collocation point: [X0, V split into d blocks of nx]
const offs = [];
for (let r = 0; r <= d; r++) offs.push(r * nx);
let X = [X0, ...M.vertsplit(V, offs)];
// Collocation equations defining V
const V_eq = [];
for (let j = 1; j <= d; j++) {
let xp_j = M.times(C[0][j], X[0]);
for (let r = 1; r <= d; r++) xp_j = M.plus(xp_j, M.times(C[r][j], X[r]));
const f_j = f.call([X[j], Pm])[0];
V_eq.push(M.minus(M.times(h, f_j), xp_j));
}
const V_eq_cat = M.vcat(V_eq);
// Root-finding function, implicitly defines V as a function of X0 and P
const vfcn = new M.Function("vfcn", [V, X0, Pm], [V_eq_cat]);
const vfcn_sx = vfcn.expand(); // convert to SX to decrease overhead
// Implicit function instance (newton, not kinsol)
const ifcn = M.rootfinder("ifcn", "newton", vfcn_sx);
const Vsol = ifcn.call([new M.MX(), X0, Pm])[0];
// Recover states and form end-of-element state
const Vparts = M.vertsplit(Vsol, offs);
X = [X0, ...Vparts];
let XF = M.times(D[0], X[0]);
for (let r = 1; r <= d; r++) XF = M.plus(XF, M.times(D[r], X[r]));
// Discrete time dynamics for one finite element
const F = new M.Function("F", [X0, Pm], [XF]);
// Iterate over all finite elements
let Xacc = X0;
for (let i = 0; i < n; i++) Xacc = F.call([Xacc, Pm])[0];
// Fixed-step integrator (as a plain Function over x0, p -> xf)
const irk = new M.Function("irk_integrator", [X0, Pm], [Xacc], ["x0", "p"], ["xf"]);
// Test values
const x0_val = M.DM([0, 1, 0]);
const p_val = M.DM(0.2);
log("-------");
log("Testing " + irk.name());
log("-------");
// Forward and reverse directional derivatives
const dF = irk.factory("dF",
["x0", "p", "fwd:x0", "fwd:p", "adj:xf"],
["xf", "fwd:xf", "adj:x0", "adj:p"]);
const res = dF.call({
x0: x0_val, p: p_val,
fwd_x0: M.DM([1, 0, 0]), fwd_p: M.DM(1),
adj_xf: M.DM([0, 0, 1]),
});
log("xf = " + res["xf"].nonzeros().map((v) => v.toFixed(6)).join(" "));
log("d(xf)/d(p)+d(xf)/d(x0[0]) = " + res["fwd_xf"].nonzeros().map((v) => v.toFixed(6)).join(" "));
log("d(xf[2])/d(x0) = " + res["adj_x0"].nonzeros().map((v) => v.toFixed(6)).join(" "));
log("d(xf[2])/d(p) = " + res["adj_p"].nonzeros().map((v) => v.toFixed(6)).join(" "));
}
if (typeof require !== "undefined" && typeof module !== "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, (...a) => console.log(...a)))
.catch((e) => { console.error("FATAL:", e.message || e); process.exit(1); });
}
if (typeof module !== "undefined" && module.exports) module.exports = example;