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
117 changes: 107 additions & 10 deletions packages/catlog-wasm/src/analyses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ use serde::{Deserialize, Serialize};
use tsify::Tsify;

use catlog::simulate::ode::PolynomialSystem;
use catlog::stdlib::analyses::ode;
use catlog::stdlib::analyses::ode::{self, ODESemanticsAnalysis, ODESemanticsProblemData};
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 All @@ -88,17 +90,23 @@ fn mass_action_system(
model: &DblModel,
mass_conservation_type: ode::MassConservationType,
logic: MassActionAnalysisLogic,
) -> Result<PolynomialSystem<QualifiedName, ode::Parameter<ode::FlowParameter>, i8>, String> {
) -> Result<PolynomialSystem<QualifiedName, ode::Parameter<ode::MassActionParameter>, i8>, String> {
match logic {
MassActionAnalysisLogic::PetriNet => {
let realised_model = model.modal_unital()?;
let analysis = ode::PetriNetMassActionAnalysis::default();
Ok(analysis.build_system(realised_model, mass_conservation_type))
let analysis = ode::PetriNetMassActionAnalysis {
mass_conservation_type,
..ode::PetriNetMassActionAnalysis::default()
};
Ok(analysis.build_system(realised_model))
}
MassActionAnalysisLogic::StockFlow => {
let realised_model = model.discrete_tab()?;
let analysis = ode::StockFlowMassActionAnalysis::default();
Ok(analysis.build_system(realised_model, mass_conservation_type))
let analysis = ode::StockFlowMassActionAnalysis {
mass_conservation_type,
..ode::StockFlowMassActionAnalysis::default()
};
Ok(analysis.build_system(realised_model))
}
}
}
Expand Down Expand Up @@ -133,10 +141,99 @@ pub(crate) fn mass_action_simulation(
logic: MassActionAnalysisLogic,
) -> Result<ODEResultWithEquations, String> {
let sys = mass_action_system(model, data.mass_conservation_type, logic);
let sys_extended_scalars = ode::extend_mass_action_scalars(sys?, &data);
let sys_extended_scalars = data.extend_scalars(sys?);
let latex_equations =
sys_extended_scalars.map_variables(latex_ob_names(model)).to_latex_equations();
let analysis = data.build_analysis(sys_extended_scalars);
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 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::LotkaVolterraAnalysis::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 = data.extend_scalars(sys?);
let latex_equations =
sys_extended_scalars.map_variables(latex_ob_names(model)).to_latex_equations();
let analysis = data.build_analysis(sys_extended_scalars);
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::LinearODEAnalysis::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 = data.extend_scalars(sys?);
let latex_equations =
sys_extended_scalars.map_variables(latex_ob_names(model)).to_latex_equations();
let analysis = ode::into_mass_action_analysis(sys_extended_scalars, data);
let analysis = data.build_analysis(sys_extended_scalars);
let solution = analysis.solve_with_defaults().map_err(|err| format!("{err:?}"));
Ok(ODEResultWithEquations {
solution: ODEResult(solution.into()),
Expand Down
144 changes: 112 additions & 32 deletions packages/catlog-wasm/src/latex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub(crate) fn latex_mor_names(model: &DblModel) -> impl Fn(&QualifiedName) -> St
/// falls back to the domain→codomain format (e.g., `X \to Y`).
pub(crate) fn latex_mor_names_mass_action(
model: &DblModel,
) -> impl Fn(&ode::FlowParameter) -> String {
) -> impl Fn(&ode::MassActionParameter) -> 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.
Expand All @@ -72,31 +72,106 @@ pub(crate) fn latex_mor_names_mass_action(
}
};

move |id: &ode::FlowParameter| match id {
ode::FlowParameter::Balanced { transition } => {
move |id: &ode::MassActionParameter| match id {
ode::MassActionParameter::Balanced { flow: transition } => {
let sub = transition_subscript(transition);
format!("r_{{{sub}}}")
}
ode::FlowParameter::Unbalanced { direction, parameter } => match (direction, parameter) {
(ode::Direction::IncomingFlow, ode::RateParameter::PerTransition { transition }) => {
let sub = transition_subscript(transition);
format!("\\rho_{{{sub}}}")
ode::MassActionParameter::Unbalanced { direction, parameter } => {
match (direction, parameter) {
(
ode::Direction::IncomingFlow,
ode::RateParameter::PerTransition { flow: transition },
) => {
let sub = transition_subscript(transition);
format!("\\rho_{{{sub}}}")
}
(
ode::Direction::OutgoingFlow,
ode::RateParameter::PerTransition { flow: transition },
) => {
let sub = transition_subscript(transition);
format!("\\kappa_{{{sub}}}")
}
(
ode::Direction::IncomingFlow,
ode::RateParameter::PerPlace { flow: transition, stock: place },
) => {
let sub = transition_subscript(transition);
let output_place_label = model.ob_namespace.label_string(place);
format!("\\rho_{{{sub}}}^{{\\text{{{output_place_label}}}}}")
}
(
ode::Direction::OutgoingFlow,
ode::RateParameter::PerPlace { flow: transition, stock: place },
) => {
let sub = transition_subscript(transition);
let input_place_label = model.ob_namespace.label_string(place);
format!("\\kappa_{{{sub}}}^{{\\text{{{input_place_label}}}}}")
}
}
(ode::Direction::OutgoingFlow, ode::RateParameter::PerTransition { transition }) => {
let sub = transition_subscript(transition);
format!("\\kappa_{{{sub}}}")
}
(ode::Direction::IncomingFlow, ode::RateParameter::PerPlace { transition, place }) => {
let sub = transition_subscript(transition);
let output_place_label = model.ob_namespace.label_string(place);
format!("\\rho_{{{sub}}}^{{\\text{{{output_place_label}}}}}")
}
(ode::Direction::OutgoingFlow, ode::RateParameter::PerPlace { transition, place }) => {
let sub = transition_subscript(transition);
let input_place_label = model.ob_namespace.label_string(place);
format!("\\kappa_{{{sub}}}^{{\\text{{{input_place_label}}}}}")
}
},
}
}
}

/// 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}}}")
}
}
}

Expand All @@ -105,6 +180,7 @@ mod tests {
use catlog::dbl::modal::{List, ModalMorType, ModalOb, ModalObType};
use catlog::dbl::model::{ModalDblModel, MutDblModel};
use catlog::simulate::ode::LatexEquation;
use catlog::stdlib::analyses::ode::{StockFlowMassActionAnalysis, ode_semantics::*};
use catlog::stdlib::{analyses::ode, theories};
use catlog::zero::{LabelSegment, Namespace, QualifiedName};
use std::rc::Rc;
Expand All @@ -117,11 +193,13 @@ mod tests {
fn unbalanced_mass_action_latex_equations() {
let model = backward_link("xxx", "yyy", "fff");
let tab_model = model.discrete_tab().unwrap();
let analysis = ode::StockFlowMassActionAnalysis::default();
let sys = analysis.build_system(
tab_model,
ode::MassConservationType::Unbalanced(ode::RateGranularity::PerTransition),
);
let analysis = StockFlowMassActionAnalysis {
mass_conservation_type: ode::MassConservationType::Unbalanced(
ode::RateGranularity::PerTransition,
),
..StockFlowMassActionAnalysis::default()
};
let sys = analysis.build_system(tab_model);
let equations = sys
.map_variables(latex_ob_names(&model))
.extend_scalars(|param| param.map_variables(latex_mor_names_mass_action(&model)))
Expand All @@ -144,11 +222,13 @@ mod tests {
fn unnamed_mor_uses_dom_cod_in_equations() {
let model = backward_link("xxx", "yyy", "");
let tab_model = model.discrete_tab().unwrap();
let analysis = ode::StockFlowMassActionAnalysis::default();
let sys = analysis.build_system(
tab_model,
ode::MassConservationType::Unbalanced(ode::RateGranularity::PerTransition),
);
let analysis = StockFlowMassActionAnalysis {
mass_conservation_type: ode::MassConservationType::Unbalanced(
ode::RateGranularity::PerTransition,
),
..StockFlowMassActionAnalysis::default()
};
let sys = analysis.build_system(tab_model);
let equations = sys
.map_variables(latex_ob_names(&model))
.extend_scalars(|param| param.map_variables(latex_mor_names_mass_action(&model)))
Expand Down
Loading
Loading