use crate::{
EsmFile, graph::component_graph as rust_component_graph, load_string as rust_load_string,
performance::CompactExpr, stoichiometric_matrix, substitute_in_model,
substitute_in_reaction_system, to_json as rust_to_json, validate as rust_validate,
};
use wasm_bindgen::prelude::*;
fn js_err<E: std::fmt::Display>(context: &str) -> impl Fn(E) -> JsValue + '_ {
move |e| JsValue::from_str(&format!("{context}: {e}"))
}
fn to_js<T: serde::Serialize>(value: &T) -> Result<JsValue, JsValue> {
let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
value
.serialize(&serializer)
.map_err(js_err("Serialization error"))
}
fn render_expressions(esm_file: &EsmFile, render: fn(&crate::Expr) -> String) -> String {
let mut out = String::new();
if let Some(models) = &esm_file.models {
let mut ids: Vec<&String> = models.keys().collect();
ids.sort();
for model_id in ids {
out.push_str(&format!("Model: {model_id}\n"));
for (i, eq) in models[model_id].equations.iter().enumerate() {
out.push_str(&format!(
" Eq {}: {} = {}\n",
i + 1,
render(&eq.lhs),
render(&eq.rhs)
));
}
}
}
if let Some(reaction_systems) = &esm_file.reaction_systems {
let mut ids: Vec<&String> = reaction_systems.keys().collect();
ids.sort();
for rs_id in ids {
out.push_str(&format!("Reaction System: {rs_id}\n"));
for (i, reaction) in reaction_systems[rs_id].reactions.iter().enumerate() {
out.push_str(&format!(
" Reaction {}: rate = {}\n",
i + 1,
render(&reaction.rate)
));
}
}
}
out
}
#[wasm_bindgen]
pub fn load_string(json_str: &str) -> Result<JsValue, JsValue> {
let esm_file = rust_load_string(json_str).map_err(js_err("Load error"))?;
to_js(&esm_file)
}
#[wasm_bindgen]
pub fn to_json(esm_file_js: &JsValue) -> Result<String, JsValue> {
let esm_file: EsmFile = serde_wasm_bindgen::from_value(esm_file_js.clone())
.map_err(js_err("Deserialization error"))?;
rust_to_json(&esm_file).map_err(js_err("Save error"))
}
#[wasm_bindgen]
pub fn validate(json_str: &str) -> Result<JsValue, JsValue> {
let esm_file = rust_load_string(json_str).map_err(js_err("Parse error"))?;
let result = rust_validate(&esm_file);
to_js(&result)
}
#[wasm_bindgen]
pub fn to_unicode(json_str: &str) -> Result<String, JsValue> {
let esm_file = rust_load_string(json_str).map_err(js_err("Parse error"))?;
Ok(render_expressions(&esm_file, crate::display::to_unicode))
}
#[wasm_bindgen]
pub fn to_latex(json_str: &str) -> Result<String, JsValue> {
let esm_file = rust_load_string(json_str).map_err(js_err("Parse error"))?;
Ok(render_expressions(&esm_file, crate::display::to_latex))
}
#[wasm_bindgen]
pub fn to_ascii(json_str: &str) -> Result<String, JsValue> {
let esm_file = rust_load_string(json_str).map_err(js_err("Parse error"))?;
Ok(render_expressions(&esm_file, crate::display::to_ascii))
}
#[wasm_bindgen]
pub fn substitute(json_str: &str, bindings_str: &str) -> Result<String, JsValue> {
use crate::Expr;
let esm_file = rust_load_string(json_str).map_err(js_err("Parse error"))?;
let bindings: serde_json::Value =
serde_json::from_str(bindings_str).map_err(js_err("Bindings parse error"))?;
let mut expr_bindings = std::collections::HashMap::new();
if let serde_json::Value::Object(obj) = bindings {
for (key, value) in obj {
let expr = match value {
serde_json::Value::Number(n) => {
if let Some(f) = n.as_f64() {
Expr::Number(f)
} else {
return Err(JsValue::from_str(&format!(
"Invalid number in bindings: {n}"
)));
}
}
serde_json::Value::String(s) => {
if let Ok(f) = s.parse::<f64>() {
Expr::Number(f)
} else {
Expr::Variable(s)
}
}
_ => {
return Err(JsValue::from_str(&format!(
"Unsupported binding type for key '{key}': {value:?}"
)));
}
};
expr_bindings.insert(key, expr);
}
}
let mut result_file = esm_file.clone();
if let Some(ref mut models) = result_file.models {
for model in models.values_mut() {
*model = substitute_in_model(model, &expr_bindings);
}
}
if let Some(ref mut reactions) = result_file.reaction_systems {
for reaction_system in reactions.values_mut() {
*reaction_system = substitute_in_reaction_system(reaction_system, &expr_bindings);
}
}
rust_to_json(&result_file).map_err(js_err("Save error"))
}
#[wasm_bindgen]
pub fn create_compact_expression(expr_str: &str) -> Result<JsValue, JsValue> {
let expr: crate::Expr = serde_json::from_str(expr_str).map_err(js_err("Parse error"))?;
let compact = CompactExpr::from_expr(&expr);
to_js(&compact)
}
#[wasm_bindgen]
pub fn compute_stoichiometric_matrix(reaction_system_str: &str) -> Result<JsValue, JsValue> {
let reaction_system: crate::ReactionSystem =
serde_json::from_str(reaction_system_str).map_err(js_err("Parse error"))?;
let matrix = stoichiometric_matrix(&reaction_system);
to_js(&matrix)
}
#[wasm_bindgen]
pub fn simulate_inputs(json_str: &str) -> Result<JsValue, JsValue> {
use crate::types::ModelVariable;
use indexmap::IndexMap;
let esm_file = rust_load_string(json_str).map_err(js_err("Parse error"))?;
let flat = crate::flatten(&esm_file).map_err(js_err("Flatten error"))?;
let to_vars = |vars: &IndexMap<String, ModelVariable>| -> Vec<serde_json::Value> {
vars.iter()
.map(|(name, mv)| {
serde_json::json!({ "name": name, "default": mv.default, "units": mv.units })
})
.collect()
};
let algebraic: std::collections::HashSet<String> =
crate::simulate::algebraic_state_names(&flat)
.into_iter()
.collect();
let mut states = to_vars(&flat.state_variables);
for state in &mut states {
let kind = match state.get("name").and_then(|n| n.as_str()) {
Some(name) if algebraic.contains(name) => "algebraic",
_ => "differential",
};
state["kind"] = serde_json::Value::from(kind);
}
let out = serde_json::json!({
"parameters": to_vars(&flat.parameters),
"states": states,
"independentVariables": flat.independent_variables,
});
to_js(&out)
}
#[cfg(feature = "solve")]
#[wasm_bindgen]
pub fn solve(
json_str: &str,
t0: f64,
t_end: f64,
params_str: &str,
ic_str: &str,
opts_str: &str,
progress: Option<js_sys::Function>,
) -> Result<JsValue, JsValue> {
use crate::problem::{ProblemOptions, esm_problem, solve as rust_solve};
let esm_file = rust_load_string(json_str).map_err(js_err("Parse error"))?;
let opts = parse_solve_options(opts_str, t0, t_end, progress)?;
let prob = esm_problem(
&esm_file,
(t0, t_end),
ProblemOptions {
p: parse_binding_map(params_str, "Params")?,
u0: parse_binding_map(ic_str, "Initial-conditions")?,
rhs: crate::problem::Rhs::Always,
..Default::default()
},
)
.map_err(js_err("EsmProblem build error"))?;
let sol = rust_solve(&prob, &opts).map_err(js_err("Solve error"))?;
to_js(&solution_json(&sol))
}
fn parse_binding_map(
s: &str,
what: &str,
) -> Result<std::collections::HashMap<String, f64>, JsValue> {
let s = s.trim();
if s.is_empty() {
return Ok(std::collections::HashMap::new());
}
serde_json::from_str(s).map_err(js_err(&format!("{what} parse error")))
}
#[cfg(feature = "solve")]
fn parse_solve_options(
opts_str: &str,
t0: f64,
t_end: f64,
progress: Option<js_sys::Function>,
) -> Result<crate::simulate::SolveOptions, JsValue> {
use crate::simulate::{Alg, Flow, Progress, ProgressFn, SolveOptions};
let opts_json: serde_json::Value = {
let s = opts_str.trim();
if s.is_empty() {
serde_json::json!({})
} else {
serde_json::from_str(s).map_err(js_err("Options parse error"))?
}
};
let mut opts = SolveOptions::default();
let alg_name = opts_json
.get("alg")
.or_else(|| opts_json.get("solver"))
.and_then(|v| v.as_str());
if let Some(s) = alg_name {
opts.alg =
Alg::from_name(s).ok_or_else(|| JsValue::from_str(&format!("Unknown alg '{s}'")))?;
}
if let Some(v) = opts_json.get("abstol").and_then(|v| v.as_f64()) {
opts.abstol = Some(v);
}
if let Some(v) = opts_json.get("reltol").and_then(|v| v.as_f64()) {
opts.reltol = Some(v);
}
if let Some(v) = opts_json
.get("maxiters")
.or_else(|| opts_json.get("maxSteps"))
.and_then(|v| v.as_u64())
{
opts.maxiters = Some(v as usize);
}
if let Some(n) = opts_json.get("outputPoints").and_then(|v| v.as_u64()) {
opts.sample_evenly(t0, t_end, n as usize);
}
if let Some(cb) = progress {
let observer: ProgressFn = std::sync::Arc::new(move |p: &Progress<'_>| {
match cb.call3(
&JsValue::NULL,
&JsValue::from_f64(p.fraction()),
&JsValue::from_f64(p.t),
&JsValue::from_f64(p.step as f64),
) {
Ok(v) => {
if v == JsValue::FALSE {
Flow::Cancel
} else {
Flow::Continue
}
}
Err(_) => Flow::Cancel,
}
});
opts.progress = Some(observer);
}
Ok(opts)
}
#[cfg(feature = "solve")]
fn solution_json(sol: &crate::simulate::Solution) -> serde_json::Value {
serde_json::json!({
"time": sol.time,
"state": sol.state,
"stateVariableNames": sol.state_variable_names,
"retcode": sol.retcode.name(),
"metadata": metadata_json(sol),
})
}
#[cfg(feature = "solve")]
fn metadata_json(sol: &crate::simulate::Solution) -> serde_json::Value {
serde_json::json!({
"alg": sol.metadata.alg,
"nRhsCalls": sol.metadata.n_rhs_calls,
"nJacobianCalls": sol.metadata.n_jacobian_calls,
"nAcceptedSteps": sol.metadata.n_accepted_steps,
"nRejectedSteps": sol.metadata.n_rejected_steps,
"tapeFallbacks": sol.metadata.tape_fallbacks.iter()
.map(|(rule, reason)| serde_json::json!({"rule": rule, "reason": reason}))
.collect::<Vec<_>>(),
})
}
#[wasm_bindgen]
pub fn observed_fields(
json_str: &str,
t0: f64,
t_end: f64,
params_str: &str,
ic_str: &str,
) -> Result<JsValue, JsValue> {
use crate::problem::{ProblemOptions, esm_problem, observed_field};
let esm_file = rust_load_string(json_str).map_err(js_err("Parse error"))?;
let prob = esm_problem(
&esm_file,
(t0, t_end),
ProblemOptions {
p: parse_binding_map(params_str, "Params")?,
u0: parse_binding_map(ic_str, "Initial-conditions")?,
..Default::default()
},
)
.map_err(js_err("EsmProblem build error"))?;
let names = prob.observed_field_names();
let mut fields = serde_json::Map::with_capacity(names.len());
for name in &names {
let a = observed_field(&prob, name).map_err(js_err(&format!("observed_field({name})")))?;
fields.insert(
name.clone(),
serde_json::json!({
"shape": a.shape(),
"values": a.iter().copied().collect::<Vec<f64>>(),
}),
);
}
to_js(&serde_json::json!({ "names": names, "fields": fields }))
}
#[wasm_bindgen]
pub fn tape_report(json_str: &str) -> Result<JsValue, JsValue> {
let esm_file = rust_load_string(json_str).map_err(js_err("Parse error"))?;
let compiled = match crate::simulate_array::ArrayCompiled::from_file(&esm_file) {
Ok(c) => c,
Err(_) => {
return to_js(&serde_json::json!({
"nRules": 0, "nTaped": 0, "fallbacks": [],
}));
}
};
let report = compiled.debug_build_tape_report();
let out = serde_json::json!({
"nRules": report.n_rules,
"nTaped": report.n_taped,
"fallbacks": report.fallbacks.iter()
.map(|(rule, reason)| serde_json::json!({"rule": rule, "reason": reason}))
.collect::<Vec<_>>(),
});
to_js(&out)
}
#[wasm_bindgen]
pub fn component_graph(json_str: &str) -> Result<JsValue, JsValue> {
let esm_file = rust_load_string(json_str).map_err(js_err("Parse error"))?;
let graph = rust_component_graph(&esm_file);
to_js(&graph)
}
#[wasm_bindgen]
pub fn polygon_area(ring_json: &str, manifold: &str) -> Result<f64, JsValue> {
let ring: Vec<(f64, f64)> =
serde_json::from_str(ring_json).map_err(js_err("ring parse error"))?;
let m = crate::geometry::Manifold::from_flag(manifold)
.ok_or_else(|| JsValue::from_str(&format!("unknown manifold '{manifold}'")))?;
crate::geometry::polygon_area(&ring, m).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen]
pub fn intersect_polygon(a_json: &str, b_json: &str, manifold: &str) -> Result<String, JsValue> {
let a: Vec<(f64, f64)> = serde_json::from_str(a_json).map_err(js_err("a parse error"))?;
let b: Vec<(f64, f64)> = serde_json::from_str(b_json).map_err(js_err("b parse error"))?;
let m = crate::geometry::Manifold::from_flag(manifold)
.ok_or_else(|| JsValue::from_str(&format!("unknown manifold '{manifold}'")))?;
let ring = crate::geometry::intersect_polygon(&a, &b, m)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_json::to_string(&ring).map_err(js_err("serialize error"))
}
#[wasm_bindgen]
pub fn get_version_info() -> JsValue {
let info = serde_json::json!({
"library_version": crate::LIBRARY_VERSION,
"schema_version": crate::SCHEMA_VERSION,
});
to_js(&info).unwrap_or(JsValue::NULL)
}
#[wasm_bindgen]
pub fn benchmark_parsing(json_str: &str, iterations: u32) -> Result<f64, JsValue> {
let start = js_sys::Date::now();
for _ in 0..iterations {
rust_load_string(json_str).map_err(js_err("Parse error"))?;
}
let end = js_sys::Date::now();
let total_time = end - start;
Ok(total_time / iterations as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_wasm_exports_compile() {
let json = r#"
{
"esm": "1.0.0",
"metadata": {
"name": "Test Model",
"description": "A simple test model for WASM exports"
},
"models": {
"SimpleModel": {
"variables": {
"x": {
"type": "unknown",
"units": "m",
"default": 1.0
},
"k": {
"type": "parameter",
"default": 0.5
}
},
"equations": [
{
"lhs": {
"op": "D",
"args": [
"x"
]
},
"rhs": {
"op": "*",
"args": [
"k",
"x"
]
}
}
]
}
}
}
"#;
let esm_file = rust_load_string(json).expect("Should load valid ESM file");
let graph = rust_component_graph(&esm_file);
assert_eq!(graph.nodes.len(), 1, "Should have 1 model node");
assert_eq!(graph.edges.len(), 0, "Should have no edges");
assert_eq!(graph.nodes[0].id, "SimpleModel");
println!("✓ New WASM export functions compile and core functionality works");
}
}
#[cfg(feature = "solve")]
#[wasm_bindgen]
pub struct Problem {
inner: std::rc::Rc<crate::problem::EsmProblem>,
}
#[cfg(feature = "solve")]
#[wasm_bindgen]
impl Problem {
pub fn build(
json_str: &str,
t0: f64,
t_end: f64,
params_str: &str,
ic_str: &str,
) -> Result<Problem, JsValue> {
Problem::build_with(json_str, t0, t_end, params_str, ic_str, None)
}
#[wasm_bindgen(js_name = buildWith)]
pub fn build_with(
json_str: &str,
t0: f64,
t_end: f64,
params_str: &str,
ic_str: &str,
compiler: Option<String>,
) -> Result<Problem, JsValue> {
use crate::problem::{Compiler, ProblemOptions, Rhs, esm_problem};
let compiler = match compiler.as_deref() {
None | Some("") => None,
Some(name) => Some(Compiler::parse_named(name).map_err(|m| JsValue::from_str(&m))?),
};
let esm_file = rust_load_string(json_str).map_err(js_err("Parse error"))?;
let prob = esm_problem(
&esm_file,
(t0, t_end),
ProblemOptions {
p: parse_binding_map(params_str, "Params")?,
u0: parse_binding_map(ic_str, "Initial-conditions")?,
rhs: Rhs::Auto,
compiler,
..Default::default()
},
)
.map_err(js_err("EsmProblem build error"))?;
Ok(Problem {
inner: std::rc::Rc::new(prob),
})
}
pub fn compiler(&self) -> String {
self.inner.compiler().as_str().to_string()
}
#[wasm_bindgen(js_name = compilerReport)]
pub fn compiler_report(&self) -> Result<JsValue, JsValue> {
let report = self.inner.compiler_report();
to_js(&serde_json::json!({
"compiler": report.compiler().as_str(),
"nTaped": report.n_taped(),
"nOracle": report.n_oracle(),
"fusedGroups": report.fused_groups(),
"fusedInstructions": report.fused_instructions(),
"rules": report.rules().iter().map(|r| serde_json::json!({
"rule": r.rule,
"kind": r.kind,
"cadence": r.cadence,
"tier": r.tier,
"reason": r.reason,
})).collect::<Vec<_>>(),
}))
}
pub fn solve(
&self,
opts_str: &str,
progress: Option<js_sys::Function>,
) -> Result<Solution, JsValue> {
let (t0, t_end) = self.inner.tspan();
let opts = parse_solve_options(opts_str, t0, t_end, progress)?;
let sol = crate::problem::solve(&self.inner, &opts).map_err(js_err("Solve error"))?;
Ok(Solution {
inner: sol,
problem: self.inner.clone(),
})
}
pub fn remake(
&self,
params_str: &str,
ic_str: &str,
tspan_str: &str,
) -> Result<Problem, JsValue> {
let tspan = {
let s = tspan_str.trim();
if s.is_empty() {
None
} else {
let v: Vec<f64> = serde_json::from_str(s).map_err(js_err("Tspan parse error"))?;
if v.len() != 2 {
return Err(JsValue::from_str("Tspan must be [t0, t_end]"));
}
Some((v[0], v[1]))
}
};
let next = crate::problem::remake(
&self.inner,
&crate::problem::Remake {
p: parse_binding_map(params_str, "Params")?,
u0: parse_binding_map(ic_str, "Initial-conditions")?,
tspan,
callbacks: None,
},
)
.map_err(js_err("Remake error"))?;
Ok(Problem {
inner: std::rc::Rc::new(next),
})
}
pub fn observed_field(&self, name: &str) -> Result<JsValue, JsValue> {
let a = crate::problem::observed_field(&self.inner, name)
.map_err(js_err(&format!("observed_field({name})")))?;
to_js(&serde_json::json!({
"shape": a.shape(),
"values": a.iter().copied().collect::<Vec<f64>>(),
}))
}
#[wasm_bindgen(js_name = observedFieldNames)]
pub fn observed_field_names(&self) -> Vec<String> {
self.inner.observed_field_names()
}
#[wasm_bindgen(js_name = stateVariableNames)]
pub fn state_variable_names(&self) -> Vec<String> {
self.inner.state_variable_names()
}
#[wasm_bindgen(js_name = parameterNames)]
pub fn parameter_names(&self) -> Vec<String> {
self.inner.parameter_names()
}
#[wasm_bindgen(js_name = isDynamic)]
pub fn is_dynamic(&self) -> bool {
self.inner.is_dynamic()
}
pub fn tspan(&self) -> Vec<f64> {
let (a, b) = self.inner.tspan();
vec![a, b]
}
}
#[cfg(feature = "solve")]
#[wasm_bindgen]
pub struct Solution {
inner: crate::simulate::Solution,
problem: std::rc::Rc<crate::problem::EsmProblem>,
}
#[cfg(feature = "solve")]
#[wasm_bindgen]
impl Solution {
#[wasm_bindgen(js_name = toJSON)]
pub fn to_json(&self) -> Result<JsValue, JsValue> {
to_js(&solution_json(&self.inner))
}
pub fn time(&self) -> Vec<f64> {
self.inner.time.clone()
}
#[wasm_bindgen(js_name = stateVariableNames)]
pub fn state_variable_names(&self) -> Vec<String> {
self.inner.state_variable_names.clone()
}
pub fn get(&self, name: &str) -> Result<Vec<f64>, JsValue> {
self.inner
.get(name)
.map(|v| v.to_vec())
.ok_or_else(|| JsValue::from_str(&format!("no state variable named '{name}'")))
}
pub fn observed(&self, name: &str) -> Result<Vec<f64>, JsValue> {
crate::problem::observed_trajectory(&self.problem, &self.inner, name)
.map_err(js_err(&format!("observed({name})")))
}
#[wasm_bindgen(js_name = observedMany)]
pub fn observed_many(&self, names_str: &str) -> Result<JsValue, JsValue> {
let names: Vec<String> =
serde_json::from_str(names_str).map_err(js_err("Names parse error"))?;
let rows = crate::problem::observed_trajectories(&self.problem, &self.inner, &names)
.map_err(js_err("observedMany"))?;
let out: serde_json::Map<String, serde_json::Value> = rows
.into_iter()
.map(|(n, row)| (n, serde_json::json!(row)))
.collect();
to_js(&out)
}
#[wasm_bindgen(js_name = observedNames)]
pub fn observed_names(&self) -> Vec<String> {
self.problem.observed_variable_names()
}
pub fn retcode(&self) -> String {
self.inner.retcode.name().to_string()
}
pub fn metadata(&self) -> Result<JsValue, JsValue> {
to_js(&metadata_json(&self.inner))
}
}