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 typeOdeSolverProblem
that we'll create when the component is mounted, and keep wrapped in ause_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 ause_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(¶ms);
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