Skip to content
Merged
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
95 changes: 93 additions & 2 deletions packages/catlog-wasm/src/analyses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use catlog::simulate::ode::PolynomialSystem;
use catlog::stdlib::analyses::ode;
use catlog::zero::QualifiedName;

use crate::latex::{latex_mor_names_linear_ode, latex_mor_names_lotka_volterra};

use super::latex::{LatexEquations, latex_mor_names, latex_mor_names_mass_action, latex_ob_names};
use super::model::DblModel;
use super::result::JsResult;
Expand Down Expand Up @@ -74,8 +76,8 @@ pub(crate) fn polynomial_ode_simulation(
})
}

/// The mass-action analysis is currently implemented for Petri nets and stock-flow
/// diagrams, and we can avoid some code reduplication by making this explicit.
/// Mass-action analysis is currently implemented for Petri nets and stock-flow diagrams
/// and we can avoid some code reduplication by making this explicit.
pub enum MassActionAnalysisLogic {
/// The modal theory of Petri nets.
PetriNet,
Expand Down Expand Up @@ -143,3 +145,92 @@ pub(crate) fn mass_action_simulation(
latex_equations: LatexEquations(latex_equations),
})
}

/// Generates the PolynomialSystem for Lotka-Volterra dynamics.
fn lotka_volterra_system(
model: &DblModel,
) -> Result<PolynomialSystem<QualifiedName, ode::Parameter<ode::LotkaVolterraParameter>, i8>, String>
{
let realised_model = model.discrete()?;
let analysis = ode::CLDLotkaVolterraAnalysis::default();
Ok(analysis.build_system(realised_model))
}

/// The analysis data for polynomial ODE equations.
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct LotkaVolterraEquationsData {
#[serde(rename = "trivialData")]
trivial_data: bool,
}

/// Generates Lotka-Volterra equations for the system.
pub(crate) fn lotka_volterra_equations(model: &DblModel) -> Result<LatexEquations, String> {
let sys = lotka_volterra_system(model);
let equations = sys?
.map_variables(latex_ob_names(model))
.extend_scalars(|param| param.map_variables(latex_mor_names_lotka_volterra(model)))
.to_latex_equations();
Ok(LatexEquations(equations))
}

/// Simulates Lotka-Volterra ODEs.
pub(crate) fn lotka_volterra_simulation(
model: &DblModel,
data: ode::LotkaVolterraProblemData,
) -> Result<ODEResultWithEquations, String> {
let sys = lotka_volterra_system(model);
let sys_extended_scalars = ode::extend_lotka_volterra_scalars(sys?, &data);
let latex_equations =
sys_extended_scalars.map_variables(latex_ob_names(model)).to_latex_equations();
let analysis = ode::into_lotka_volterra_analysis(sys_extended_scalars, data);
let solution = analysis.solve_with_defaults().map_err(|err| format!("{err:?}"));
Ok(ODEResultWithEquations {
solution: ODEResult(solution.into()),
latex_equations: LatexEquations(latex_equations),
})
}

/// Generates the PolynomialSystem for linear ODE dynamics.
fn linear_ode_system(
model: &DblModel,
) -> Result<PolynomialSystem<QualifiedName, ode::Parameter<ode::LinearODEParameter>, i8>, String> {
let realised_model = model.discrete()?;
let analysis = ode::CLDLinearODEAnalysis::default();
Ok(analysis.build_system(realised_model))
}

/// The analysis data for polynomial ODE equations.
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct LinearODEEquationsData {
#[serde(rename = "trivialData")]
trivial_data: bool,
}

/// Generates linear ODE equations for the system.
pub(crate) fn linear_ode_equations(model: &DblModel) -> Result<LatexEquations, String> {
let sys = linear_ode_system(model);
let equations = sys?
.map_variables(latex_ob_names(model))
.extend_scalars(|param| param.map_variables(latex_mor_names_linear_ode(model)))
.to_latex_equations();
Ok(LatexEquations(equations))
}

/// Simulates linear ODE equations.
pub(crate) fn linear_ode_simulation(
model: &DblModel,
data: ode::LinearODEProblemData,
) -> Result<ODEResultWithEquations, String> {
let sys = linear_ode_system(model);
let sys_extended_scalars = ode::extend_linear_ode_scalars(sys?, &data);
let latex_equations =
sys_extended_scalars.map_variables(latex_ob_names(model)).to_latex_equations();
let analysis = ode::into_linear_ode_analysis(sys_extended_scalars, data);
let solution = analysis.solve_with_defaults().map_err(|err| format!("{err:?}"));
Ok(ODEResultWithEquations {
solution: ODEResult(solution.into()),
latex_equations: LatexEquations(latex_equations),
})
}
61 changes: 61 additions & 0 deletions packages/catlog-wasm/src/latex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,67 @@ pub(crate) fn latex_mor_names_mass_action(
}
}

/// Creates a closure that formats morphism names for Lotka-Volterra LaTeX output.
///
/// When a morphism has a label, it is used directly. When unnamed, the label
/// falls back to the domain→codomain format (e.g., `X \to Y`).
pub(crate) fn latex_mor_names_lotka_volterra(
model: &DblModel,
) -> impl Fn(&ode::LotkaVolterraParameter) -> String {
// Returns a LaTeX fragment for a transition, suitable for use as a subscript.
// Named morphisms produce `\text{name}`, unnamed ones produce
// `\text{dom} \to \text{cod}` so that `\to` is in math mode.
let transition_subscript = |transition: &QualifiedName| -> String {
if let Some(label) = model.mor_namespace.label(transition) {
format!("\\text{{{label}}}")
} else {
let (dom, cod) = model
.mor_generator_dom_cod_label_strings(transition)
.expect("Morphism in equation system should have domain and codomain");
format!("\\text{{{dom}}} \\to \\text{{{cod}}}")
}
};

move |id: &ode::LotkaVolterraParameter| match id {
ode::LotkaVolterraParameter::Growth { variable } => {
format!("g_{{{variable}}}")
}
ode::LotkaVolterraParameter::Interaction { link } => {
let sub = transition_subscript(link);
format!("k_{{{sub}}}")
}
}
}

/// Creates a closure that formats morphism names for mass-action LaTeX output.
///
/// When a morphism has a label, it is used directly. When unnamed, the label
/// falls back to the domain→codomain format (e.g., `X \to Y`).
pub(crate) fn latex_mor_names_linear_ode(
model: &DblModel,
) -> impl Fn(&ode::LinearODEParameter) -> String {
// Returns a LaTeX fragment for a transition, suitable for use as a subscript.
// Named morphisms produce `\text{name}`, unnamed ones produce
// `\text{dom} \to \text{cod}` so that `\to` is in math mode.
let transition_subscript = |transition: &QualifiedName| -> String {
if let Some(label) = model.mor_namespace.label(transition) {
format!("\\text{{{label}}}")
} else {
let (dom, cod) = model
.mor_generator_dom_cod_label_strings(transition)
.expect("Morphism in equation system should have domain and codomain");
format!("\\text{{{dom}}} \\to \\text{{{cod}}}")
}
};

move |id: &ode::LinearODEParameter| match id {
ode::LinearODEParameter::Parameter { morphism } => {
let sub = transition_subscript(morphism);
format!("\\lambda_{{{sub}}}")
}
}
}

#[cfg(test)]
mod tests {
use catlog::dbl::modal::{List, ModalMorType, ModalOb, ModalObType};
Expand Down
36 changes: 16 additions & 20 deletions packages/catlog-wasm/src/theories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,14 @@ impl ThSignedCategory {
&self,
model: &DblModel,
data: analyses::ode::LotkaVolterraProblemData,
) -> Result<ODEResult, String> {
Ok(ODEResult(
analyses::ode::SignedCoefficientBuilder::new(name("Object"))
.add_positive(Path::Id(name("Object")))
.add_negative(name("Negative").into())
.lotka_volterra_analysis(model.discrete()?, data)
.solve_with_defaults()
.map_err(|err| format!("{err:?}"))
.into(),
))
) -> Result<ODEResultWithEquations, String> {
lotka_volterra_simulation(model, data)
}

/// Show the equations of the Lotka-Volterra system derived from a model.
#[wasm_bindgen(js_name = "lotkaVolterraEquations")]
pub fn lotka_volterra_equations(&self, model: &DblModel) -> Result<LatexEquations, String> {
lotka_volterra_equations(model)
}

/// Simulate the linear ODE system derived from a model.
Expand All @@ -174,16 +172,14 @@ impl ThSignedCategory {
&self,
model: &DblModel,
data: analyses::ode::LinearODEProblemData,
) -> Result<ODEResult, String> {
Ok(ODEResult(
analyses::ode::SignedCoefficientBuilder::new(name("Object"))
.add_positive(Path::Id(name("Object")))
.add_negative(name("Negative").into())
.linear_ode_analysis(model.discrete()?, data)
.solve_with_defaults()
.map_err(|err| format!("{err:?}"))
.into(),
))
) -> Result<ODEResultWithEquations, String> {
linear_ode_simulation(model, data)
}

/// Show the equations of the linear ODE system derived from a model.
#[wasm_bindgen(js_name = "linearODEEquations")]
pub fn linear_ode_equations(&self, model: &DblModel) -> Result<LatexEquations, String> {
linear_ode_equations(model)
}
}

Expand Down
Loading
Loading