Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WebAssembly

Most of diffsol can be compiled to WebAssembly, allowing it to be used in web applications in the browser or other environments that support Wasm.

The main limitation is that JIT compilation of the DiffSL DSL is not yet supported in Wasm. This means you must use rust closures or the OdeEquations trait to specify your problems, rather than using the DiffSL DSL.

An example Yew app

To demonstrate using Diffsol in a WebAssembly application, we will create a simple Yew app that simulates a population dynamics model. The app will allow users to move sliders to adjust the parameters of the model and see the results in real-time using a plotly chart.

A demo built from this example is available online.

Getting started

First, you need to add the wasm32-unknown-unknown target to your Rust toolchain:

rustup target add wasm32-unknown-unknown

We'll use trunk to build our Yew app, so you need to install it if you haven't already:

cargo install trunk

Then, create a new Yew app:

cargo new example-yew-diffsol

And add the necessary dependencies to your Cargo.toml:

[dependencies]
yew = { version = "0.21.0", features = ["csr"] }
diffsol = { version = "0.6.2", features = [] }
nalgebra = { workspace = true }
yew-plotly = "0.3.0"
web-sys = "0.3.77"

The Yew app

We'll keep it simple and create a single component that will handle the population dynamics simulation. This will consist of the following:

  • An problem of type OdeSolverProblem that we'll create when the component is mounted, and keep wrapped in a use_mut_ref hook so we can modify it when the parameters change.
  • The initial parameters for the problem held in params, which will be stored in a use_state hook.
  • An onchange callback that will update the parameters of the problem when the user changes the sliders.
  • A Plotly component that will display the results of the simulation.
  • Two sliders for the user to adjust two out of the four parameters of the model.
use diffsol::{MatrixCommon, NalgebraVec, OdeBuilder, OdeEquations, OdeSolverMethod, Op, Vector};
use web_sys::HtmlInputElement;
use yew::prelude::*;
use yew_plotly::{
    plotly::{common::Mode, layout::Axis, Layout, Plot, Scatter},
    Plotly,
};
type M = diffsol::NalgebraMat<f64>;

#[function_component]
fn App() -> Html {
    let problem = use_mut_ref(|| {
        OdeBuilder::<M>::new()
            .rhs(|x, p, _t, y| {
                y[0] = p[0] * x[0] - p[1] * x[0] * x[1];
                y[1] = p[2] * x[0] * x[1] - p[3] * x[1];
            })
            .init(|_p, _t, y| y.fill(1.0), 2)
            .p(vec![2.0 / 3.0, 4.0 / 3.0, 1.0, 1.0])
            .build()
            .unwrap()
    });
    let params = use_state(|| {
        NalgebraVec::from_vec(
            vec![2.0 / 3.0, 4.0 / 3.0, 1.0, 1.0],
            problem.borrow().eqn.context().clone(),
        )
    });

    let onchange = |i: usize| {
        let params = params.clone();
        let problem = problem.clone();
        Callback::from(move |e: InputEvent| {
            let input: HtmlInputElement = e.target_unchecked_into();
            let value = input.value().parse::<f64>().unwrap();
            let mut new_params = NalgebraVec::clone(&params);
            new_params[i] = value;
            let mut problem = problem.borrow_mut();
            problem.eqn.set_params(&new_params);
            params.set(new_params);
        })
    };
    let oninput_a: Callback<InputEvent> = onchange(0);
    let oninput_b: Callback<InputEvent> = onchange(1);

    let (ys, ts) = {
        let problem = problem.borrow();
        let mut solver = problem.tsit45().unwrap();
        solver.solve(40.0).unwrap()
    };

    let prey: Vec<_> = ys.inner().row(0).into_iter().copied().collect();
    let predator: Vec<_> = ys.inner().row(1).into_iter().copied().collect();
    let time: Vec<_> = ts.into_iter().collect();

    let prey = Scatter::new(time.clone(), prey)
        .mode(Mode::Lines)
        .name("Prey");
    let predator = Scatter::new(time, predator)
        .mode(Mode::Lines)
        .name("Predator");

    let mut plot = Plot::new();
    plot.add_trace(prey);
    plot.add_trace(predator);

    let layout = Layout::new()
        .x_axis(Axis::new().title("t".into()))
        .y_axis(Axis::new().title("population".into()));
    plot.set_layout(layout);

    let a_str = format!("{}", params.get_index(0));
    let b_str = format!("{}", params.get_index(1));

    html! {
        <div>
            <h1>{ "Population Dynamics: Prey-Predator Model" }</h1>
            <Plotly plot={plot}/>
            <ul>
                <li>
                    <input oninput={oninput_a} type="range" id="a" name="a" min="0.1" max="3" step="0.1" value={a_str} />
                    <label for="a">{"a"}</label>
                </li>
                <li>
                    <input oninput={oninput_b} type="range" id="b" name="b" min="0.1" max="3" step="0.1" value={b_str} />
                    <label for="b">{"b"}</label>
                </li>
            </ul>
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

Building and running the app

You can build and run the app using trunk:

trunk serve