Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,26 +115,54 @@ The DSL is compiled to a WASM module using the `compileModel` function:
import { compileModel } from 'diffeq-js';

const code = `...`;
compileModel(code).then(() => {
// create Solver, Vector etc here
});
// Compile with an optional ID to support multiple models
await compileModel(code, 'model1');
```

The `compileModel` function requires an active internet connection as it sends
the model code to a remote server for compilation to WASM. All the classes in
the library are wrappers around corresponding classes in the WASM module, and so
the `compileModel` function must be called successfully before any of the other
classes can be used.
classes can be used.

### Multiple Models

Version 0.2.0 introduces support for compiling and solving multiple models simultaneously. Each model can be given a unique identifier when compiled:

```javascript
// Compile multiple models
await compileModel(code1, 'model1');
await compileModel(code2, 'model2');

// Create solvers for each model
const options1 = new Options({}, 'model1');
const solver1 = new Solver(options1, 'model1');

const options2 = new Options({}, 'model2');
const solver2 = new Solver(options2, 'model2');
```

### Options

The `compileModel` function takes an `Options` object as its argument. The contructor for the `Options` class takes the following arguments:
The `Options` class constructor takes an options object and an optional model ID:

* `print_stats` - statistics about each solve are printed to the console after each successful call to `solve`. Default: false
* `fixed_times` - if false (the default), the solver will consider the first element
of `times` to be the starting time point, and the second element to be the final time point,
between these two times the solver will choose the time points to output (these are returned in the `times` vector).
If true, the solver will only return solutions at the times specified in the input `times` vector. Default: false
```javascript
const options = new Options({
print_stats: false, // Print statistics after each solve
fixed_times: false, // Use fixed time points or let solver choose
mxsteps: 500, // Maximum number of steps
min_step: 0.0, // Minimum step size
max_step: Infinity, // Maximum step size
atol: 1e-6, // Absolute tolerance
rtol: 1e-6, // Relative tolerance
debug: false, // Enable debug output
fwd_sens: false, // Enable forward sensitivity analysis
linear_solver: OptionsLinearSolver.DENSE, // Linear solver type
preconditioner: OptionsPreconditioner.NONE, // Preconditioner type
jacobian: OptionsJacobian.DENSE_JACOBIAN, // Jacobian type
linsol_max_iterations: 100 // Maximum iterations for linear solver
}, 'model1');
```

### Compilation errors

Expand Down
36 changes: 16 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@martinjrobins/diffeq-js",
"version": "0.1.6",
"version": "0.2.0",
"homepage": "https://github.com/martinjrobins/diffeq-js",
"description": "A library for solving differential equations in JavaScript",
"author": "Martin Robinson <[email protected]> (https://github.com/martinjrobins)",
Expand All @@ -13,41 +13,37 @@
"url": "https://github.com/martinjrobins/diffeq-js"
},
"source": "src/index.ts",
"main": "dist/main.js",
"types": "dist/types.d.ts",
"main": "./dist/diffeq-js.umd.js",
"module": "./dist/diffeq-js.es.js",
"types": "./dist/types.d.ts",
"license": "MIT",
"browser": "dist/browser.js",
"browser": "./dist/diffeq-js.es.js",
"type": "module",
"targets": {
"browser": {
"context": "browser",
"optimize": true,
"includeNodeModules": true,
"sourceMap": true,
"outputFormat": "esmodule"
"exports": {
".": {
"import": "./dist/diffeq-js.es.js",
"require": "./dist/diffeq-js.umd.js",
"types": "./dist/types.d.ts"
}
},
"@parcel/resolver-default": {
"packageExports": true
},
"scripts": {
"watch": "parcel watch",
"build": "parcel build",
"dev": "vite",
"build": "vite build",
"test": "mocha -r ts-node/register"
},
"devDependencies": {
"@parcel/packager-ts": "2.12.0",
"@parcel/transformer-typescript-types": "2.12.0",
"@types/chai": "^4.3.5",
"@types/mocha": "^10.0.1",
"@types/node": "^20.5.9",
"@vitejs/plugin-vue": "^5.0.0",
"chai": "^4.3.8",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"parcel": "^2.12.0",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0"
},
"dependencies": {
"@bjorn3/browser_wasi_shim": "^0.2.14"
Expand Down
124 changes: 77 additions & 47 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import { WASI, File, OpenFile, PreopenDirectory } from "@bjorn3/browser_wasi_shim";
import { extract_vector_functions } from "./vector";
import { extract_options_functions } from "./options";
import { extract_solver_functions } from "./solver";
import base from "/node_modules/base-x/src/index";
import {File, OpenFile, WASI} from "@bjorn3/browser_wasi_shim";
import {extract_vector_functions} from "./vector";
import {extract_options_functions} from "./options";
import {extract_solver_functions} from "./solver";

export { default as Vector } from "./vector";
export { default as Options, OptionsJacobian, OptionsLinearSolver, OptionsPreconditioner } from "./options";
export { default as Solver } from "./solver";


let args: string[] = [];
let env: string[] = [];
let fds = [
new OpenFile(new File([])), // stdin
new OpenFile(new File([])), // stdout
new OpenFile(new File([])), // stderr
];
let wasi = new WASI(args, env, fds);
let inst: WebAssembly.WebAssemblyInstantiatedSource | undefined = undefined;

class SimpleOpenFile {
file: File;
file_pos: number;
Expand All @@ -32,29 +20,83 @@ class SimpleOpenFile {
const end = data.byteLength;
const slice = data.slice(start, end);
this.file_pos = end;
const string = new TextDecoder().decode(slice);
return string;
return new TextDecoder().decode(slice);
}
}

// @ts-expect-error
let stderr = new SimpleOpenFile(wasi.fds[2].file);
const defaultBaseUrl = "https://compbio.fhs.um.edu.mo/diffeq";
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if you meant to do this, but I'm happy if you want to run the default server :)


// @ts-expect-error
let stdout = new SimpleOpenFile(wasi.fds[1].file);
function createWasi() {
let args: string[] = [];
let env: string[] = [];
let fds = [
new OpenFile(new File([])), // stdin
new OpenFile(new File([])), // stdout
new OpenFile(new File([])), // stderr
];
return new WASI(args, env, fds);
}

function getWasmMemory() {
if (inst === undefined) {
throw new Error("WASM module not loaded");
class ModelRegistry {
private static models: Map<string, any> = new Map();
private static defaultModel: any = null;

static register(id: string | undefined, model: any) {
if (id) {
this.models.set(id, model);
} else {
this.defaultModel = model;
}
}

static get(id?: string) {
if (id) {
return this.models.get(id);
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if an id is supplied but not in the registry, you might want to throw an error here. I can imagine that it would be easy to just have a typo in your id name and you would want to know about that rather than just get the default model

return this.defaultModel;
}
return inst.instance.exports.memory as WebAssembly.Memory;
}

const defaultBaseUrl = "https://diffeq-backend.fly.dev";
function compileResponse(response: Promise<Response>) {
const wasi = createWasi();
const importObject = {
"wasi_snapshot_preview1": wasi.wasiImport,
};
return WebAssembly.instantiateStreaming(response, importObject).then(
(obj) => {
wasi.initialize({
exports: {
memory: obj.instance.exports.memory as WebAssembly.Memory,
},
});
const stderr = new SimpleOpenFile((wasi.fds[2] as OpenFile).file);
const stdout = new SimpleOpenFile((wasi.fds[1] as OpenFile).file);
const vectorFunctions = extract_vector_functions(obj);
const optionsFunctions = extract_options_functions(obj);
const solverFunctions = extract_solver_functions(obj);

// Set global functions
global.vectorFunctions = vectorFunctions;
global.optionsFunctions = optionsFunctions;
global.solverFunctions = solverFunctions;
global.stderr = stderr;
global.stdout = stdout;

return {
instance: obj,
stderr,
stdout,
vectorFunctions,
optionsFunctions,
solverFunctions
};
},
);
}

function compileModel(text: string, baseUrl: string = defaultBaseUrl) {
export async function compileModel(code: string, id?: string) {
const data = {
text,
text: code,
name: "unknown",
};
const options: RequestInit = {
Expand All @@ -65,35 +107,23 @@ function compileModel(text: string, baseUrl: string = defaultBaseUrl) {
},
body: JSON.stringify(data),
};
const response = fetch(`${baseUrl}/compile`, options).then((response) => {
const response = fetch(`${defaultBaseUrl}/compile`, options).then((response) => {
if (!response.ok) {
return response.text().then((text) => {
throw text;
});
}
return response;
});
return compileResponse(response);
const model = await compileResponse(response);
ModelRegistry.register(id, model);
return model;
}

function compileResponse(response: Promise<Response>) {
const importObject = {
"wasi_snapshot_preview1": wasi.wasiImport,
};
return WebAssembly.instantiateStreaming(response, importObject).then(
(obj) => {
extract_vector_functions(obj);
extract_options_functions(obj);
extract_solver_functions(obj);
inst = obj
// @ts-expect-error
wasi.initialize(inst.instance);
},
);
export function getModel(id?: string) {
return ModelRegistry.get(id);
}

export { compileModel, compileResponse, getWasmMemory, stderr, stdout }




Expand Down
Loading