diff --git a/package.json b/package.json index f0853878b..4e2388100 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build:deps": "pnpm --filter ./packages/frontend run build:deps", - "dev": "pnpm --filter ./packages/frontend run dev" + "dev": "pnpm --filter ./packages/frontend run dev", + "check": "cargo +nightly fmt && cargo clippy && pnpm --filter ./packages/frontend run check" }, "devDependencies": { "depcheck": "^1.4.7", diff --git a/packages/catlog-wasm/src/analyses.rs b/packages/catlog-wasm/src/analyses.rs index 2b9513433..19877f51b 100644 --- a/packages/catlog-wasm/src/analyses.rs +++ b/packages/catlog-wasm/src/analyses.rs @@ -1,15 +1,14 @@ //! Auxiliary structs and glue code for data passed to/from analyses. +use catlog::latex::LatexEquations; use serde::{Deserialize, Serialize}; use tsify::Tsify; use catlog::simulate::ode::PolynomialSystem; -use catlog::stdlib::analyses::ode::{self, ODESemanticsAnalysis, ODESemanticsProblemData}; +use catlog::stdlib::analyses::ode::{self, ODESemantics, ODESemanticsProblemData, Parameter}; 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::latex::latex_names; use super::model::DblModel; use super::result::JsResult; @@ -29,214 +28,464 @@ pub struct ODEResultWithEquations { pub latex_equations: LatexEquations, } -/// The analysis data for polynomial ODE equations. -#[derive(Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct PolynomialODEEquationsData { - #[serde(rename = "trivialData")] - trivial_data: bool, -} - -/// Generates the PolynomialSystem for the systems of polynomial ODEs. -fn polynomial_ode_system( +/// Simulate specific ODE semantics on a model, for use in a simulation analysis. +pub(crate) fn ode_semantics_simulation( model: &DblModel, -) -> Result, i8>, String> { - let realised_model = model.modal_nonunital()?; - let analysis = ode::PolynomialODEAnalysis::default(); - Ok(analysis.build_system(realised_model)) -} - -/// Generates equations for the system of polynomial ODEs. -pub(crate) fn polynomial_ode_equations( - model: &DblModel, - _data: PolynomialODEEquationsData, -) -> Result { - let sys = polynomial_ode_system(model); - let equations = sys? - .map_variables(latex_ob_names(model)) - .extend_scalars(|param| param.map_variables(latex_mor_names(model))) - .to_latex_equations(); - Ok(LatexEquations(equations)) -} - -/// Simulates mass-action ODEs. -pub(crate) fn polynomial_ode_simulation( - model: &DblModel, - data: ode::PolynomialODEProblemData, + problem_data: S::ProblemDataType, + system: PolynomialSystem, i8>, ) -> Result { - let sys = polynomial_ode_system(model); - let sys_extended_scalars = ode::extend_polynomial_ode_scalars(sys?, &data); + let sys_extended_scalars = problem_data.extend_scalars(system); let latex_equations = - sys_extended_scalars.map_variables(latex_ob_names(model)).to_latex_equations(); - let analysis = ode::polynomial_ode_analysis(sys_extended_scalars, data); + sys_extended_scalars.map_variables(latex_names(model)).to_latex_equations(); + let analysis = problem_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), + latex_equations, }) } -/// 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, - /// The discrete tabulator theory of stock-flow diagrams. - StockFlow, +/// Generate the equations of specific ODE semantics on a model, for use in an equations analysis. +pub(crate) fn ode_semantics_equations( + model: &DblModel, + system: PolynomialSystem, i8>, +) -> Result { + Ok(system.to_latex_equations_with_map(|param| latex_names(model)(param))) } -/// Generates the PolynomialSystem for mass-action dynamics. -fn mass_action_system( - model: &DblModel, - mass_conservation_type: ode::MassConservationType, - logic: MassActionAnalysisLogic, -) -> Result, i8>, String> { - match logic { - MassActionAnalysisLogic::PetriNet => { - let realised_model = model.modal_unital()?; - let analysis = ode::PetriNetMassActionAnalysis { - mass_conservation_type, - ..ode::PetriNetMassActionAnalysis::default() - }; - Ok(analysis.build_system(realised_model)) +#[cfg(test)] +mod tests { + use super::*; + + use crate::model::{DblModel, tests::backward_link}; + use crate::theories::ThSignedCategory; + use catcolab_document_types::v2::{MorDecl, MorType, Ob, ObDecl, ObType}; + use catlog::dbl::modal::{List, ModalMorType, ModalOb, ModalObType, ModeApp}; + use catlog::dbl::model::{ModalDblModel, MutDblModel}; + use catlog::latex::{Latex, LatexEquation, LatexEquations}; + use catlog::stdlib::{ + analyses::ode::{self, MassConservationType, ODESemanticsAnalysis}, + theories, + }; + use catlog::zero::{LabelSegment, Namespace, QualifiedName}; + use std::rc::Rc; + use uuid::Uuid; + + #[test] + fn signed_polynomial_ode_latex_equations() { + // The signed multicategory with objects `x`, `yum`, and `z`, (unnamed) positive morphisms + // `[x,y] -+-> z` and `q : z -+-> y`, and a negative morphism `negative : [x,x,y,z] ---> x`. + let model = example_signed_multicategory("x", "yum", "z", "", "", "negative"); + let system = + ode::PolynomialODEAnalysis::default().build_system(model.modal_nonunital().unwrap()); + let equations = + ode_semantics_equations::(&model, system).unwrap(); + + let expected = LatexEquations(vec![ + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} x".to_string()), + rhs: Latex( + "-\\lambda_{\\text{negative}} \\cdot x^2 \\cdot \\text{yum} \\cdot z" + .to_string(), + ), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{yum}".to_string()), + rhs: Latex("\\lambda_{z \\to \\text{yum}} \\cdot z".to_string()), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} z".to_string()), + rhs: Latex( + "\\lambda_{[x, \\text{yum}] \\to z} \\cdot x \\cdot \\text{yum}".to_string(), + ), + }, + ]); + + assert_eq!(equations, expected); + } + + #[test] + fn cld_lotka_volterra_latex_equations() { + let model = parallel_negative_cld("x", "yellow", "f", ""); + let system = ode::LotkaVolterraAnalysis::default().build_system(model.discrete().unwrap()); + let equations = + ode_semantics_equations::(&model, system).unwrap(); + + let expected = LatexEquations(vec![ + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} x".to_string()), + rhs: Latex( + "g_{x} \\cdot x" + .to_string(), + ), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{yellow}".to_string()), + rhs: Latex( + "(-k_{f} - k_{x \\to \\text{yellow}}) \\cdot x \\cdot \\text{yellow} + g_{\\text{yellow}} \\cdot \\text{yellow}" + .to_string(), + ), + }, + ]); + + assert_eq!(equations, expected); + } + + #[test] + fn cld_lcc_latex_equations() { + let model = parallel_negative_cld("x", "yellow", "f", ""); + let system = ode::LinearODEAnalysis::default().build_system(model.discrete().unwrap()); + let equations = ode_semantics_equations::(&model, system).unwrap(); + + let expected = LatexEquations(vec![ + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} x".to_string()), + rhs: Latex("0".to_string()), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{yellow}".to_string()), + rhs: Latex( + "(-\\lambda_{f} - \\lambda_{x \\to \\text{yellow}}) \\cdot x".to_string(), + ), + }, + ]); + + assert_eq!(equations, expected); + } + + #[test] + fn stock_flow_balanced_mass_action_latex_equations() { + let model = backward_link("xylophone", "y", "fff"); + let system = ode::StockFlowMassActionAnalysis { + mass_conservation_type: MassConservationType::Balanced, + ..ode::StockFlowMassActionAnalysis::default() } - MassActionAnalysisLogic::StockFlow => { - let realised_model = model.discrete_tab()?; - let analysis = ode::StockFlowMassActionAnalysis { - mass_conservation_type, - ..ode::StockFlowMassActionAnalysis::default() - }; - Ok(analysis.build_system(realised_model)) + .build_system(model.discrete_tab().unwrap()); + let equations = + ode_semantics_equations::(&model, system).unwrap(); + + let expected = LatexEquations(vec![ + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{xylophone}".to_string()), + rhs: Latex("-r_{\\text{fff}} \\cdot \\text{xylophone} \\cdot y".to_string()), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} y".to_string()), + rhs: Latex("r_{\\text{fff}} \\cdot \\text{xylophone} \\cdot y".to_string()), + }, + ]); + assert_eq!(equations, expected); + } + + #[test] + fn stock_flow_unbalanced_mass_action_latex_equations() { + let model = backward_link("xylophone", "y", "fff"); + let system = ode::StockFlowMassActionAnalysis { + mass_conservation_type: MassConservationType::Unbalanced( + ode::RateGranularity::PerTransition, + ), + ..ode::StockFlowMassActionAnalysis::default() } + .build_system(model.discrete_tab().unwrap()); + let equations = + ode_semantics_equations::(&model, system).unwrap(); + + let expected = LatexEquations(vec![ + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{xylophone}".to_string()), + rhs: Latex("-\\kappa_{\\text{fff}} \\cdot \\text{xylophone} \\cdot y".to_string()), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} y".to_string()), + rhs: Latex("\\rho_{\\text{fff}} \\cdot \\text{xylophone} \\cdot y".to_string()), + }, + ]); + assert_eq!(equations, expected); } -} -/// The analysis data for mass-action equations. -#[derive(Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct MassActionEquationsData { - /// The mass-conservation type. - #[serde(rename = "massConservationType")] - pub mass_conservation_type: ode::MassConservationType, -} + #[test] + fn petri_net_balanced_mass_action_latex_equations() { + // The Petri net with places `liquid`, `solid`, and `c`, and one (unnamed) transition `[liquid, c] -> [solid, c]`. + let model = catalytic_petri_net("liquid", "solid", "c", ""); + let system = ode::PetriNetMassActionAnalysis { + mass_conservation_type: MassConservationType::Balanced, + ..ode::PetriNetMassActionAnalysis::default() + } + .build_system(model.modal_unital().unwrap()); + let equations = + ode_semantics_equations::(&model, system).unwrap(); -/// Generates mass-action equations for the system. -pub(crate) fn mass_action_equations( - model: &DblModel, - data: MassActionEquationsData, - logic: MassActionAnalysisLogic, -) -> Result { - let sys = mass_action_system(model, data.mass_conservation_type, logic); - let equations = sys? - .map_variables(latex_ob_names(model)) - .extend_scalars(|param| param.map_variables(latex_mor_names_mass_action(model))) - .to_latex_equations(); - Ok(LatexEquations(equations)) -} + let expected = LatexEquations(vec![ + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{liquid}".to_string()), + rhs: Latex( + "-r_{[\\text{liquid}, c] \\to [\\text{solid}, c]} \\cdot \\text{liquid} \\cdot c" + .to_string(), + ), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{solid}".to_string()), + rhs: Latex( + "r_{[\\text{liquid}, c] \\to [\\text{solid}, c]} \\cdot \\text{liquid} \\cdot c" + .to_string(), + ), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} c".to_string()), + rhs: Latex("0".to_string()), + }, + ]); + assert_eq!(equations, expected); + } -/// Simulates mass-action ODEs. -pub(crate) fn mass_action_simulation( - model: &DblModel, - data: ode::MassActionProblemData, - logic: MassActionAnalysisLogic, -) -> Result { - let sys = mass_action_system(model, data.mass_conservation_type, logic); - 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), - }) -} + #[test] + fn petri_net_unbalanced_pt_mass_action_latex_equations() { + // The Petri net with places "liquid", "solid", and "c", and one transition + // `transition : [liquid, c] -> [solid, c]`. + let model = catalytic_petri_net("liquid", "solid", "c", "transition"); + let system = ode::PetriNetMassActionAnalysis { + mass_conservation_type: MassConservationType::Unbalanced( + ode::RateGranularity::PerTransition, + ), + ..ode::PetriNetMassActionAnalysis::default() + } + .build_system(model.modal_unital().unwrap()); + let equations = + ode_semantics_equations::(&model, system).unwrap(); -/// Generates the PolynomialSystem for Lotka-Volterra dynamics. -fn lotka_volterra_system( - model: &DblModel, -) -> Result, i8>, String> -{ - let realised_model = model.discrete()?; - let analysis = ode::LotkaVolterraAnalysis::default(); - Ok(analysis.build_system(realised_model)) -} + let expected = LatexEquations(vec![ + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{liquid}".to_string()), + rhs: Latex("-\\kappa_{\\text{transition}} \\cdot \\text{liquid} \\cdot c".to_string()), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{solid}".to_string()), + rhs: Latex("\\rho_{\\text{transition}} \\cdot \\text{liquid} \\cdot c".to_string()), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} c".to_string()), + rhs: Latex("(\\rho_{\\text{transition}} - \\kappa_{\\text{transition}}) \\cdot \\text{liquid} \\cdot c".to_string()), + }, + ]); + assert_eq!(equations, expected); + } -/// 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, -} + #[test] + fn petri_net_unbalanced_pp_mass_action_latex_equations() { + // The Petri net with places "liquid", "solid", and "c", and one (unnamed) transition [liquid, c] -> [solid, c]. + let model = catalytic_petri_net("liquid", "solid", "c", ""); + let system = ode::PetriNetMassActionAnalysis { + mass_conservation_type: MassConservationType::Unbalanced( + ode::RateGranularity::PerPlace, + ), + ..ode::PetriNetMassActionAnalysis::default() + } + .build_system(model.modal_unital().unwrap()); + let equations = + ode_semantics_equations::(&model, system).unwrap(); -/// Generates Lotka-Volterra equations for the system. -pub(crate) fn lotka_volterra_equations(model: &DblModel) -> Result { - 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)) -} + let expected = LatexEquations(vec![ + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{liquid}".to_string()), + rhs: Latex("-\\kappa_{[\\text{liquid}, c] \\to [\\text{solid}, c]}^{\\text{liquid}} \\cdot \\text{liquid} \\cdot c".to_string()), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{solid}".to_string()), + rhs: Latex("\\rho_{[\\text{liquid}, c] \\to [\\text{solid}, c]}^{\\text{solid}} \\cdot \\text{liquid} \\cdot c".to_string()), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} c".to_string()), + rhs: Latex("(\\rho_{[\\text{liquid}, c] \\to [\\text{solid}, c]}^{c} - \\kappa_{[\\text{liquid}, c] \\to [\\text{solid}, c]}^{c}) \\cdot \\text{liquid} \\cdot c".to_string()), + }, + ]); + assert_eq!(equations, expected); + } -/// Simulates Lotka-Volterra ODEs. -pub(crate) fn lotka_volterra_simulation( - model: &DblModel, - data: ode::LotkaVolterraProblemData, -) -> Result { - 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), - }) -} + /// Construct a causal loop diagram with objects x, y and negative links f, g : x -> y. + fn parallel_negative_cld( + source_name: &str, + target_name: &str, + first_link_name: &str, + second_link_name: &str, + ) -> DblModel { + let th = ThSignedCategory::new().theory(); + let mut model = DblModel::new(&th); + let [x, y, f, g] = [Uuid::now_v7(), Uuid::now_v7(), Uuid::now_v7(), Uuid::now_v7()]; -/// Generates the PolynomialSystem for linear ODE dynamics. -fn linear_ode_system( - model: &DblModel, -) -> Result, i8>, String> { - let realised_model = model.discrete()?; - let analysis = ode::LinearODEAnalysis::default(); - Ok(analysis.build_system(realised_model)) -} + assert!( + model + .add_ob(&ObDecl { + name: source_name.into(), + id: x, + ob_type: ObType::Basic("Object".into()) + }) + .is_ok() + ); + assert!( + model + .add_ob(&ObDecl { + name: target_name.into(), + id: y, + ob_type: ObType::Basic("Object".into()) + }) + .is_ok() + ); + assert!( + model + .add_mor(&MorDecl { + name: first_link_name.into(), + id: f, + mor_type: MorType::Basic("Negative".into()), + dom: Some(Ob::Basic(x.to_string())), + cod: Some(Ob::Basic(y.to_string())), + }) + .is_ok() + ); + assert!( + model + .add_mor(&MorDecl { + name: second_link_name.into(), + id: g, + mor_type: MorType::Basic("Negative".into()), + dom: Some(Ob::Basic(x.to_string())), + cod: Some(Ob::Basic(y.to_string())), + }) + .is_ok() + ); -/// 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, -} + model + } -/// Generates linear ODE equations for the system. -pub(crate) fn linear_ode_equations(model: &DblModel) -> Result { - 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)) -} + /// Construct a signed multicategory with objects `x, y, z`, positive morphisms `p : [x,y] -+-> z` + /// and `q : z -+-> y`, and negative morphism `n : [x,x,y,z] ---> x`. + fn example_signed_multicategory( + x_name: &str, + y_name: &str, + z_name: &str, + p_name: &str, + q_name: &str, + n_name: &str, + ) -> DblModel { + let th = Rc::new(theories::th_signed_polynomial_ode_system()); + let ob_type = ModalObType::new(("State").into()); + let pos_mor_type: ModalMorType = ModeApp::new(("Contribution").into()).into(); + let neg_mor_type: ModalMorType = ModeApp::new(("NegativeContribution").into()).into(); -/// Simulates linear ODE equations. -pub(crate) fn linear_ode_simulation( - model: &DblModel, - data: ode::LinearODEProblemData, -) -> Result { - 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 = 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), - }) + let mut inner = ModalDblModel::new(th); + + let [x, y, z, p, q, n] = [ + Uuid::now_v7(), + Uuid::now_v7(), + Uuid::now_v7(), + Uuid::now_v7(), + Uuid::now_v7(), + Uuid::now_v7(), + ]; + + inner.add_ob(x.into(), ob_type.clone()); + inner.add_ob(y.into(), ob_type.clone()); + inner.add_ob(z.into(), ob_type.clone()); + + inner.add_mor( + p.into(), + ModalOb::List( + List::Symmetric, + vec![ModalOb::Generator(x.into()), ModalOb::Generator(y.into())], + ), + ModalOb::Generator(z.into()), + pos_mor_type.clone(), + ); + inner.add_mor( + q.into(), + ModalOb::List(List::Symmetric, vec![ModalOb::Generator(z.into())]), + ModalOb::Generator(y.into()), + pos_mor_type.clone(), + ); + inner.add_mor( + n.into(), + ModalOb::List( + List::Symmetric, + vec![ + ModalOb::Generator(x.into()), + ModalOb::Generator(x.into()), + ModalOb::Generator(y.into()), + ModalOb::Generator(z.into()), + ], + ), + ModalOb::Generator(x.into()), + neg_mor_type.clone(), + ); + + let mut ob_namespace = Namespace::new_for_uuid(); + ob_namespace.set_label(x, LabelSegment::Text(x_name.into())); + ob_namespace.set_label(y, LabelSegment::Text(y_name.into())); + ob_namespace.set_label(z, LabelSegment::Text(z_name.into())); + + let mut mor_namespace = Namespace::new_for_uuid(); + mor_namespace.set_label(p, LabelSegment::Text(p_name.into())); + mor_namespace.set_label(q, LabelSegment::Text(q_name.into())); + mor_namespace.set_label(n, LabelSegment::Text(n_name.into())); + + DblModel { + model: inner.into(), + ty: None, + ob_namespace, + mor_namespace, + } + } + + /// Construct a Petri net representing a catalytic transition [x,c] -> [y,c]. + fn catalytic_petri_net( + source_name: &str, + target_name: &str, + catalyst_name: &str, + transition_name: &str, + ) -> DblModel { + let th = Rc::new(theories::th_sym_monoidal_category()); + let ob_type = ModalObType::new(QualifiedName::from("Object")); + let op = QualifiedName::from("tensor"); + + let [x, y, c, t] = [Uuid::now_v7(), Uuid::now_v7(), Uuid::now_v7(), Uuid::now_v7()]; + + let mut inner = ModalDblModel::new(th); + inner.add_ob(x.into(), ob_type.clone()); + inner.add_ob(y.into(), ob_type.clone()); + inner.add_ob(c.into(), ob_type.clone()); + + inner.add_mor( + t.into(), + ModalOb::App( + ModalOb::List( + List::Symmetric, + vec![ModalOb::Generator(x.into()), ModalOb::Generator(c.into())], + ) + .into(), + op.clone(), + ), + ModalOb::App( + ModalOb::List( + List::Symmetric, + vec![ModalOb::Generator(y.into()), ModalOb::Generator(c.into())], + ) + .into(), + op.clone(), + ), + ModalMorType::Zero(ob_type.clone()), + ); + + let mut ob_namespace = Namespace::new_for_uuid(); + ob_namespace.set_label(x, LabelSegment::Text(source_name.into())); + ob_namespace.set_label(y, LabelSegment::Text(target_name.into())); + ob_namespace.set_label(c, LabelSegment::Text(catalyst_name.into())); + + let mut mor_namespace = Namespace::new_for_uuid(); + mor_namespace.set_label(t, LabelSegment::Text(transition_name.into())); + + DblModel { + model: inner.into(), + ty: None, + ob_namespace, + mor_namespace, + } + } } diff --git a/packages/catlog-wasm/src/latex.rs b/packages/catlog-wasm/src/latex.rs index 772df0785..cb7db4b80 100644 --- a/packages/catlog-wasm/src/latex.rs +++ b/packages/catlog-wasm/src/latex.rs @@ -1,321 +1,42 @@ //! Auxiliary structs and glue code for any LaTeX code being passed through analyses. -use serde::{Deserialize, Serialize}; -use tsify::Tsify; - -use catlog::simulate::ode::LatexEquation; -use catlog::stdlib::analyses::ode; -use catlog::zero::QualifiedName; +use catlog::{ + latex::{list_object_as_latex, wrap_with_backslash_text}, + zero::QualifiedName, +}; use super::model::DblModel; -/// Symbolic equations in LaTeX format. -#[derive(Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct LatexEquations(pub Vec); - -/// Creates a closure that formats object names for LaTeX output. -pub(crate) fn latex_ob_names(model: &DblModel) -> impl Fn(&QualifiedName) -> String { +/// Creates a closure that formats object and morphism names for LaTeX output. When a morphism has a +/// name (and thus label), it is used directly; when unnamed, the label falls back to the format +/// `domain→codomain` (e.g., `X \to Y`). +pub(crate) fn latex_names(model: &DblModel) -> impl Fn(&QualifiedName) -> String { |id: &QualifiedName| { - let name = model.ob_namespace.label_string(id); - if name.chars().count() > 1 { - format!("\\text{{{name}}}") - } else { - name - } - } -} - -/// 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(model: &DblModel) -> impl Fn(&QualifiedName) -> String { - // Returns a LaTeX fragment for a morphism, 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 morphism_subscript = |morphism: &QualifiedName| -> String { - if let Some(label) = model.mor_namespace.label(morphism) { - format!("\\text{{{label}}}") - } else { - let (dom, cod) = model - .mor_generator_dom_cod_label_strings(morphism) - .expect("Morphism in equation system should have domain and codomain"); - format!("\\text{{{dom}}} \\to \\text{{{cod}}}") - } - }; - - move |id: &QualifiedName| { - let sub = morphism_subscript(id); - format!("\\lambda_{{{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_mass_action( - model: &DblModel, -) -> 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. - let transition_subscript = |transition: &QualifiedName| -> String { - if let Some(label) = model.mor_namespace.label(transition) { - format!("\\text{{{label}}}") + if let Some(ob_label) = model.ob_namespace.label(id) { + wrap_with_backslash_text(ob_label.to_string()) + } else if let Some(mor_label) = model.mor_namespace.label(id) { + wrap_with_backslash_text(mor_label.to_string()) } 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}}}") + .mor_generator_dom_cod(id) + .expect("Morphism in equation system should have domain and codomain."); + let dom_labels: Vec = model + .get_ob_label(&dom) + .expect("Object in equation system should have a label.") + .into_iter() + .map(|label| wrap_with_backslash_text(label.to_string())) + .collect(); + let cod_labels: Vec = model + .get_ob_label(&cod) + .expect("Object in equation system should have a label.") + .into_iter() + .map(|label| wrap_with_backslash_text(label.to_string())) + .collect(); + format!( + "{} \\to {}", + list_object_as_latex(dom_labels), + list_object_as_latex(cod_labels) + ) } - }; - - move |id: &ode::MassActionParameter| match id { - ode::MassActionParameter::Balanced { flow: transition } => { - let sub = transition_subscript(transition); - format!("r_{{{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}}}}}") - } - } - } - } -} - -/// 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}; - 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; - use uuid::Uuid; - - use super::*; - use crate::model::{DblModel, tests::backward_link}; - - #[test] - fn unbalanced_mass_action_latex_equations() { - let model = backward_link("xxx", "yyy", "fff"); - let tab_model = model.discrete_tab().unwrap(); - 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))) - .to_latex_equations(); - - let expected = vec![ - LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{xxx}".to_string(), - rhs: "-\\kappa_{\\text{fff}} \\cdot \\text{xxx} \\cdot \\text{yyy}".to_string(), - }, - LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{yyy}".to_string(), - rhs: "\\rho_{\\text{fff}} \\cdot \\text{xxx} \\cdot \\text{yyy}".to_string(), - }, - ]; - assert_eq!(equations, expected); - } - - #[test] - fn unnamed_mor_uses_dom_cod_in_equations() { - let model = backward_link("xxx", "yyy", ""); - let tab_model = model.discrete_tab().unwrap(); - 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))) - .to_latex_equations(); - - let expected = vec![ - LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{xxx}".to_string(), - rhs: - "-\\kappa_{\\text{xxx} \\to \\text{yyy}} \\cdot \\text{xxx} \\cdot \\text{yyy}" - .to_string(), - }, - LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{yyy}".to_string(), - rhs: "\\rho_{\\text{xxx} \\to \\text{yyy}} \\cdot \\text{xxx} \\cdot \\text{yyy}" - .to_string(), - }, - ]; - assert_eq!(equations, expected); - } - - #[test] - fn modal_mor_dom_cod_labels() { - let th = Rc::new(theories::th_sym_monoidal_category()); - let ob_type = ModalObType::new(QualifiedName::from("Object")); - let op = QualifiedName::from("tensor"); - - let [s_id, i_id, r_id] = [Uuid::now_v7(), Uuid::now_v7(), Uuid::now_v7()]; - let [infect_id, recover_id] = [Uuid::now_v7(), Uuid::now_v7()]; - - let mut inner = ModalDblModel::new(th); - inner.add_ob(s_id.into(), ob_type.clone()); - inner.add_ob(i_id.into(), ob_type.clone()); - inner.add_ob(r_id.into(), ob_type.clone()); - - // infect: tensor(S, I) -> tensor(I, I) — product-typed dom and cod. - inner.add_mor( - infect_id.into(), - ModalOb::App( - ModalOb::List( - List::Symmetric, - vec![ModalOb::Generator(s_id.into()), ModalOb::Generator(i_id.into())], - ) - .into(), - op.clone(), - ), - ModalOb::App( - ModalOb::List( - List::Symmetric, - vec![ModalOb::Generator(i_id.into()), ModalOb::Generator(i_id.into())], - ) - .into(), - op.clone(), - ), - ModalMorType::Zero(ob_type.clone()), - ); - - // recover: I -> R — simple generator dom and cod. - inner.add_mor( - recover_id.into(), - ModalOb::Generator(i_id.into()), - ModalOb::Generator(r_id.into()), - ModalMorType::Zero(ob_type), - ); - - let mut ob_namespace = Namespace::new_for_uuid(); - ob_namespace.set_label(s_id, LabelSegment::Text("S".into())); - ob_namespace.set_label(i_id, LabelSegment::Text("I".into())); - ob_namespace.set_label(r_id, LabelSegment::Text("R".into())); - - let model = DblModel { - model: inner.into(), - ty: None, - ob_namespace, - mor_namespace: Namespace::new_for_uuid(), - }; - - // Morphism with basic generator dom/cod resolves labels. - assert_eq!( - model.mor_generator_dom_cod_label_strings(&recover_id.into()), - Some(("I".to_string(), "R".to_string())) - ); - - // Morphism with product-typed dom/cod resolves to bracketed labels. - assert_eq!( - model.mor_generator_dom_cod_label_strings(&infect_id.into()), - Some(("[S, I]".to_string(), "[I, I]".to_string())) - ); } } diff --git a/packages/catlog-wasm/src/model.rs b/packages/catlog-wasm/src/model.rs index 7dae0666c..a56120d32 100644 --- a/packages/catlog-wasm/src/model.rs +++ b/packages/catlog-wasm/src/model.rs @@ -427,46 +427,42 @@ impl DblModel { Ok(()) } - /// Gets label strings for the domain and codomain of a morphism generator. + /// Gets the domain and codomain of a morphism generator. /// - /// Returns `Some((dom_label, cod_label))` when the morphism has a domain - /// and codomain whose labels can be resolved from the namespace. - pub fn mor_generator_dom_cod_label_strings( - &self, - id: &QualifiedName, - ) -> Option<(String, String)> { + /// Returns `Some((dom, cod))`. + pub fn mor_generator_dom_cod(&self, id: &QualifiedName) -> Option<(Ob, Ob)> { let (dom, cod) = all_the_same!(match &self.model { DblModelBox::[Discrete, DiscreteTab, ModalUnital, ModalNonUnital](model) => { (Quoter.quote(model.get_dom(id)?), Quoter.quote(model.get_cod(id)?)) } }); - Some((self.ob_label_string(&dom)?, self.ob_label_string(&cod)?)) + Some((dom, cod)) } - /// Gets a label string for an object. + /// Gets the list of labels for an object. /// - /// For a single object returns its label (e.g. `"S"`). For a list of - /// objects returns bracketed labels (e.g. `"[S, I]"`). - fn ob_label_string(&self, ob: &Ob) -> Option { + /// This works for both basic objects and list objects (e.g. `[x,y]` in a Petri net). + pub fn get_ob_label(&self, ob: &Ob) -> Option> { match ob { Ob::Basic(s) => { let name = QualifiedName::deserialize_str(s).ok()?; - Some(self.ob_namespace.label_string(&name)) + self.ob_namespace.label(&name).map(|var| vec![var]) } Ob::App { ob, .. } => { // FIXME: This is incorrect in general. The design issue is that // this pretty printer claims to handles all models, but is // customized to Petri nets as free SMCs where we prefer to omit // the tensor application. - self.ob_label_string(ob) + self.get_ob_label(ob) } Ob::List { objects, .. } => { - let labels: Option> = objects + let labels: Vec<_> = objects .iter() - .map(|ob| ob.as_ref().and_then(|ob| self.ob_label_string(ob))) + .filter_map(|ob| ob.as_ref().and_then(|ob| self.get_ob_label(ob))) + .flatten() .collect(); - Some(format!("[{}]", labels?.join(", "))) + Some(labels) } _ => None, } @@ -864,10 +860,12 @@ pub(crate) mod tests { assert_eq!(Result::from(model.validate().0).map_err(|errs| errs.len()), Err(2)); } + /// Construct a stock-flow diagram with a backwards link. pub(crate) fn backward_link(src_name: &str, tgt_name: &str, flow_name: &str) -> DblModel { let th = ThCategoryLinks::new().theory(); let mut model = DblModel::new(&th); let [f, x, y, link] = [Uuid::now_v7(), Uuid::now_v7(), Uuid::now_v7(), Uuid::now_v7()]; + assert!( model .add_ob(&ObDecl { diff --git a/packages/catlog-wasm/src/theories.rs b/packages/catlog-wasm/src/theories.rs index a2b0c1ad0..7dc548d7f 100644 --- a/packages/catlog-wasm/src/theories.rs +++ b/packages/catlog-wasm/src/theories.rs @@ -7,14 +7,14 @@ use std::rc::Rc; use wasm_bindgen::prelude::*; use catlog::dbl::theory::{self as theory, NonUnital, Unital}; +use catlog::latex::LatexEquations; use catlog::one::Path; +use catlog::stdlib::analyses::ode::ODESemanticsAnalysis; use catlog::stdlib::{analyses, models, theories, theory_morphisms}; use catlog::zero::name; -use super::latex::LatexEquations; use super::model_morphism::{MotifOccurrence, MotifsOptions, motifs}; use super::result::JsResult; -use super::theories::MassActionAnalysisLogic; use super::{analyses::*, model::DblModel, theory::DblTheory}; /// The empty or initial theory. @@ -151,13 +151,17 @@ impl ThSignedCategory { model: &DblModel, data: analyses::ode::LotkaVolterraProblemData, ) -> Result { - lotka_volterra_simulation(model, data) + let system = + analyses::ode::LotkaVolterraAnalysis::default().build_system(model.discrete()?); + ode_semantics_simulation::(model, data, system) } /// 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 { - lotka_volterra_equations(model) + let system = + analyses::ode::LotkaVolterraAnalysis::default().build_system(model.discrete()?); + ode_semantics_equations::(model, system) } /// Simulate the linear ODE system derived from a model. @@ -167,13 +171,15 @@ impl ThSignedCategory { model: &DblModel, data: analyses::ode::LinearODEProblemData, ) -> Result { - linear_ode_simulation(model, data) + let system = analyses::ode::LinearODEAnalysis::default().build_system(model.discrete()?); + ode_semantics_simulation::(model, data, system) } /// 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 { - linear_ode_equations(model) + let system = analyses::ode::LinearODEAnalysis::default().build_system(model.discrete()?); + ode_semantics_equations::(model, system) } } @@ -307,7 +313,12 @@ impl ThCategoryLinks { model: &DblModel, data: analyses::ode::MassActionProblemData, ) -> Result { - mass_action_simulation(model, data, MassActionAnalysisLogic::StockFlow) + let system = analyses::ode::StockFlowMassActionAnalysis { + mass_conservation_type: data.equations_data.mass_conservation_type, + ..analyses::ode::StockFlowMassActionAnalysis::default() + } + .build_system(model.discrete_tab()?); + ode_semantics_simulation::(model, data, system) } /// Returns the symbolic mass-action equations in LaTeX format. @@ -315,9 +326,14 @@ impl ThCategoryLinks { pub fn mass_action_equations( &self, model: &DblModel, - data: MassActionEquationsData, + data: analyses::ode::MassActionEquationsData, ) -> Result { - mass_action_equations(model, data, MassActionAnalysisLogic::StockFlow) + let system = analyses::ode::StockFlowMassActionAnalysis { + mass_conservation_type: data.mass_conservation_type, + ..analyses::ode::StockFlowMassActionAnalysis::default() + } + .build_system(model.discrete_tab()?); + ode_semantics_equations::(model, system) } } @@ -361,7 +377,12 @@ impl ThSymMonoidalCategory { model: &DblModel, data: analyses::ode::MassActionProblemData, ) -> Result { - mass_action_simulation(model, data, MassActionAnalysisLogic::PetriNet) + let system = analyses::ode::PetriNetMassActionAnalysis { + mass_conservation_type: data.equations_data.mass_conservation_type, + ..analyses::ode::PetriNetMassActionAnalysis::default() + } + .build_system(model.modal_unital()?); + ode_semantics_simulation::(model, data, system) } /// Returns the symbolic mass-action equations in LaTeX format. @@ -369,9 +390,14 @@ impl ThSymMonoidalCategory { pub fn mass_action_equations( &self, model: &DblModel, - data: MassActionEquationsData, + data: analyses::ode::MassActionEquationsData, ) -> Result { - mass_action_equations(model, data, MassActionAnalysisLogic::PetriNet) + let system = analyses::ode::PetriNetMassActionAnalysis { + mass_conservation_type: data.mass_conservation_type, + ..analyses::ode::PetriNetMassActionAnalysis::default() + } + .build_system(model.modal_unital()?); + ode_semantics_equations::(model, system) } /// Simulates the stochastic mass-action system derived from a model. @@ -423,17 +449,17 @@ impl ThPolynomialODE { model: &DblModel, data: analyses::ode::PolynomialODEProblemData, ) -> Result { - polynomial_ode_simulation(model, data) + let system = + analyses::ode::PolynomialODEAnalysis::default().build_system(model.modal_nonunital()?); + ode_semantics_simulation::(model, data, system) } /// Returns the symbolic equations in LaTeX format. #[wasm_bindgen(js_name = "polynomialODEEquations")] - pub fn polynomial_ode_equations( - &self, - model: &DblModel, - data: PolynomialODEEquationsData, - ) -> Result { - polynomial_ode_equations(model, data) + pub fn polynomial_ode_equations(&self, model: &DblModel) -> Result { + let system = + analyses::ode::PolynomialODEAnalysis::default().build_system(model.modal_nonunital()?); + ode_semantics_equations::(model, system) } } @@ -460,17 +486,17 @@ impl ThSignedPolynomialODE { model: &DblModel, data: analyses::ode::PolynomialODEProblemData, ) -> Result { - polynomial_ode_simulation(model, data) + let system = + analyses::ode::PolynomialODEAnalysis::default().build_system(model.modal_nonunital()?); + ode_semantics_simulation::(model, data, system) } /// Returns the symbolic equations in LaTeX format. #[wasm_bindgen(js_name = "polynomialODEEquations")] - pub fn polynomial_ode_equations( - &self, - model: &DblModel, - data: PolynomialODEEquationsData, - ) -> Result { - polynomial_ode_equations(model, data) + pub fn polynomial_ode_equations(&self, model: &DblModel) -> Result { + let system = + analyses::ode::PolynomialODEAnalysis::default().build_system(model.modal_nonunital()?); + ode_semantics_equations::(model, system) } } diff --git a/packages/catlog/src/latex.rs b/packages/catlog/src/latex.rs new file mode 100644 index 000000000..3e316cc3f --- /dev/null +++ b/packages/catlog/src/latex.rs @@ -0,0 +1,122 @@ +//! Code for passing around LaTeX representations of data. +//! +//! We reserve the `std::Display` trait for unicode-style display of mathematical +//! objects, so here we provide structure for passing around LaTeX code for such. +//! +//! N.B. Although the software is called "LaTeX" we will consistently ignore the +//! correct capitalisation and simply write latex or Latex in our code. + +use duplicate::duplicate_item; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "serde-wasm")] +use tsify::Tsify; + +use crate::zero::QualifiedName; + +/// We should mark which strings are to be parsed as Latex. +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Latex(pub String); + +/// Implement `Display` for Latex by simply printing out the string it contains. +impl fmt::Display for Latex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// An equation in Latex format with a left-hand side and a right-hand side. +#[derive(PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-wasm", derive(Tsify))] +#[cfg_attr(feature = "serde-wasm", tsify(into_wasm_abi, from_wasm_abi))] +#[derive(Clone)] +pub struct LatexEquation { + /// The left-hand side of the equation. + pub lhs: Latex, + /// The right-hand side of the equation. + pub rhs: Latex, +} + +impl fmt::Display for LatexEquation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} = {}", self.lhs, self.rhs) + } +} + +/// Symbolic equations in Latex format. +#[derive(PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-wasm", derive(Tsify))] +#[cfg_attr(feature = "serde-wasm", tsify(into_wasm_abi, from_wasm_abi))] +pub struct LatexEquations(pub Vec); + +impl fmt::Debug for LatexEquations { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let eqns: Vec = self.0.clone().into_iter().map(|eqn| format!("{}", eqn)).collect(); + write!(f, "\n{}", eqns.join("\n")) + } +} + +/// An object that can be rendered to Latex. +pub trait ToLatex { + /// Convert the object to its Latex representation. + fn to_latex(&self) -> Latex; +} + +/// An object that can be rendered to Latex, with some function that can be applied to selected +/// appearances of a `QualifiedName` within the object. The main purpose of this trait is for rendering +/// the equations derived from an ODE semantics analysis, where we do not want to show UUIDs directly +/// to the frontend. For an example implementation see e.g. `catlog::src::stdlib::analyses::ode::mass_action` +/// where this is implemented for `MassActionParameter`. +pub trait ToLatexWithMap { + /// Convert the object to its Latex representation, after applying the provided function `f` to + /// selected `QualifiedName`. See `PolynomialSystem::to_latex_equations_with_map` for the main + /// use of this function. + fn to_latex_with_map String>(&self, f: F) -> Latex; +} + +/// We can recover the intended behaviour of `to_latex` by simply passing the "identity function" +/// to `to_latex_with_map`. +impl ToLatex for T +where + T: ToLatexWithMap, +{ + fn to_latex(&self) -> Latex { + let name = |id: &QualifiedName| id.to_string(); + self.to_latex_with_map(name) + } +} + +/// We only want to apply the `f : &QualifiedName -> String` to something of type `QualifiedName`; +/// we leave any numerical or string-literal values unchanged. +#[duplicate_item(T; [f32]; [f64]; [i8]; [i32]; [i64]; [u32]; [u64]; [usize]; [char]; [String])] +impl ToLatexWithMap for T { + fn to_latex_with_map String>(&self, _f: F) -> Latex { + Latex(self.to_string()) + } +} + +/// Wrap a string with a Latex text literal if it is longer than a single character. +// FIXME: This is built on the assumption that any single letter should be rendered as a variable +// name, and any longer name should be a text literal. A more correct solution should allow +// us to write e.g. `$\pi_1$` as a name directly. +pub fn wrap_with_backslash_text(name: String) -> String { + if name.chars().count() > 1 { + format!("\\text{{{name}}}") + } else { + name.to_string() + } +} + +/// Display a single-object list `[x]` directly as `x`, but display any longer list as `[x, y ,z]`. +pub fn list_object_as_latex(vec: Vec) -> String { + if vec.len() > 1 { + format!("[{}]", vec.join(", ")) + } else { + vec[0].to_string() + } +} diff --git a/packages/catlog/src/lib.rs b/packages/catlog/src/lib.rs index a7fcbd387..da82a958a 100644 --- a/packages/catlog/src/lib.rs +++ b/packages/catlog/src/lib.rs @@ -20,6 +20,7 @@ pub mod refs; pub mod egglog_util; +pub mod latex; pub mod validate; pub mod dbl; diff --git a/packages/catlog/src/simulate/ode/polynomial.rs b/packages/catlog/src/simulate/ode/polynomial.rs index 5e162ffed..281e7a2fa 100644 --- a/packages/catlog/src/simulate/ode/polynomial.rs +++ b/packages/catlog/src/simulate/ode/polynomial.rs @@ -9,14 +9,11 @@ use indexmap::IndexMap; use nalgebra::DVector; use num_traits::{One, Pow, Zero}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -#[cfg(feature = "serde-wasm")] -use tsify::Tsify; - #[cfg(test)] use super::ODEProblem; use super::ODESystem; +use crate::latex::{Latex, LatexEquation, LatexEquations, ToLatex, ToLatexWithMap}; +use crate::zero::QualifiedName; use crate::zero::{alg::Polynomial, rig::DisplayCoef}; /// A system of polynomial differential equations. @@ -94,33 +91,48 @@ where PolynomialSystem { components } } - /// Converts to equations as LaTeX strings. - pub fn to_latex_equations(&self) -> Vec + /// Converts to equations as Latex strings. + pub fn to_latex_equations(&self) -> LatexEquations where - Var: Display, - Coef: Display + DisplayCoef + Clone + PartialEq + One + Neg, - Exp: Display + PartialEq + One, + Var: Display + ToLatexWithMap, + Coef: Display + ToLatexWithMap + DisplayCoef + Clone + PartialEq + One + Neg, + Exp: Display + ToLatex + PartialEq + One, { - self.components - .iter() - .map(|(var, poly)| LatexEquation { - lhs: format!("\\frac{{\\mathrm{{d}}}}{{\\mathrm{{d}}t}} {var}"), - rhs: poly.to_latex(), - }) - .collect() + let name = |id: &QualifiedName| id.to_string(); + self.to_latex_equations_with_map(name) } -} -#[derive(Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-wasm", derive(Tsify))] -#[cfg_attr(feature = "serde-wasm", tsify(into_wasm_abi, from_wasm_abi))] -/// An equation in LaTeX format with a left-hand side and a right-hand side. -pub struct LatexEquation { - /// The left-hand side of the equation. - pub lhs: String, - /// The right-hand side of the equation. - pub rhs: String, + // REQUEST | It might be much cleaner to only implement `to_latex_equations_with_map` in the + // FOR | case where `Var = QualifiedName` and `Coef = Parameter`, but I + // FEEDBACK | cannot figure out how to convince Rust to let me do this. + //__________/ + /// Converts to equations as Latex string, after applying the function `f : &QualifiedName -> String` + /// to each of the variables and coefficients. This is intended for frontend functionality, where we + /// do not want to display UUIDs directly but instead look them up in the model namespace. For more + /// details, see `catlog-wasm::src::latex` where we use `to_latex_equations_with_map` and pass in + /// the function `catlog-wasm::src::latex_names`. + pub fn to_latex_equations_with_map String>( + &self, + f: F, + ) -> LatexEquations + where + Var: Display + ToLatexWithMap, + Coef: Display + ToLatexWithMap + DisplayCoef + Clone + PartialEq + One + Neg, + Exp: Display + ToLatex + PartialEq + One, + { + LatexEquations( + self.components + .iter() + .map(|(var, poly)| LatexEquation { + lhs: Latex(format!( + "\\frac{{\\mathrm{{d}}}}{{\\mathrm{{d}}t}} {}", + var.to_latex_with_map(|var| f(var)) + )), + rhs: poly.to_latex_with_map(|term| f(term)), + }) + .collect(), + ) + } } impl PolynomialSystem diff --git a/packages/catlog/src/stdlib/analyses/ode/linear_ode.rs b/packages/catlog/src/stdlib/analyses/ode/linear_ode.rs index 2b6a8f212..02b06d839 100644 --- a/packages/catlog/src/stdlib/analyses/ode/linear_ode.rs +++ b/packages/catlog/src/stdlib/analyses/ode/linear_ode.rs @@ -15,6 +15,7 @@ use tsify::Tsify; use super::Parameter; use crate::dbl::model::{FpDblModel, MutDblModel}; +use crate::latex::{Latex, ToLatexWithMap}; use crate::one::Path; use crate::simulate::ode::PolynomialSystem; use crate::stdlib::analyses::ode::ode_semantics::{ @@ -31,6 +32,7 @@ impl ODESemantics for LinearODESemantics { type ModelType = DiscreteDblModel; type ParameterType = LinearODEParameter; type AnalysisType = LinearODEAnalysis; + type EquationsDataType = (); type ProblemDataType = LinearODEProblemData; } @@ -54,6 +56,14 @@ impl fmt::Display for LinearODEParameter { } } +impl ToLatexWithMap for LinearODEParameter { + fn to_latex_with_map String>(&self, f: T) -> Latex { + match self { + Self::Parameter { morphism } => Latex(format!("\\lambda_{{{}}}", f(morphism))), + } + } +} + impl ODEParameterType for LinearODEParameter {} /// Linear ODE analysis for causal loop diagrams (CLDs). @@ -88,8 +98,8 @@ impl /// A system of ODEs for building arbitrary LinearODE ODEs from CLDs. fn build_system_builder( &self, - model: &::ModelType, - ) -> PolynomialODESystemBuilder<::ParameterType> { + model: &DiscreteDblModel, + ) -> PolynomialODESystemBuilder { let mut builder = PolynomialODESystemBuilder::new(); for var in model.ob_generators_with_type(&self.var_ob_type) { @@ -170,11 +180,7 @@ impl ODESemanticsProblemData<::ParameterType fn extend_scalars( &self, - sys: PolynomialSystem< - QualifiedName, - Parameter<::ParameterType>, - i8, - >, + sys: PolynomialSystem, i8>, ) -> PolynomialSystem { let sys = sys.extend_scalars(|poly| { poly.eval(|param| match param { @@ -196,7 +202,7 @@ mod test { use super::*; use crate::{ dbl::model::MutDblModel, - simulate::ode::LatexEquation, + latex::{LatexEquation, LatexEquations, wrap_with_backslash_text}, stdlib::{models::*, theories::*}, }; @@ -244,18 +250,20 @@ mod test { fn to_latex() { let th = Rc::new(th_signed_category()); let model = negative_feedback(th); - let sys = LinearODEAnalysis::default().build_system(&model); - let expected = vec![ + let system = LinearODEAnalysis::default().build_system(&model); + let equations = + system.to_latex_equations_with_map(|name| wrap_with_backslash_text(name.to_string())); + let expected = LatexEquations(vec![ LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} x".to_string(), - rhs: "-Parameter(negative) \\cdot y".to_string(), + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} x".to_string()), + rhs: Latex("-\\lambda_{\\text{negative}} \\cdot y".to_string()), }, LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} y".to_string(), - rhs: "Parameter(positive) \\cdot x".to_string(), + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} y".to_string()), + rhs: Latex("\\lambda_{\\text{positive}} \\cdot x".to_string()), }, - ]; - assert_eq!(expected, sys.to_latex_equations()); + ]); + assert_eq!(expected, equations); } // Numerical test. diff --git a/packages/catlog/src/stdlib/analyses/ode/lotka_volterra.rs b/packages/catlog/src/stdlib/analyses/ode/lotka_volterra.rs index f335ca12d..9b2057205 100644 --- a/packages/catlog/src/stdlib/analyses/ode/lotka_volterra.rs +++ b/packages/catlog/src/stdlib/analyses/ode/lotka_volterra.rs @@ -15,6 +15,7 @@ use tsify::Tsify; use super::Parameter; use crate::dbl::model::{FpDblModel, MutDblModel}; +use crate::latex::{Latex, ToLatexWithMap}; use crate::one::Path; use crate::simulate::ode::PolynomialSystem; use crate::stdlib::analyses::ode::ode_semantics::{ @@ -31,6 +32,7 @@ impl ODESemantics for LotkaVolterraSemantics { type ModelType = DiscreteDblModel; type ParameterType = LotkaVolterraParameter; type AnalysisType = LotkaVolterraAnalysis; + type EquationsDataType = (); type ProblemDataType = LotkaVolterraProblemData; } @@ -63,6 +65,15 @@ impl fmt::Display for LotkaVolterraParameter { } } +impl ToLatexWithMap for LotkaVolterraParameter { + fn to_latex_with_map String>(&self, f: T) -> Latex { + match self { + Self::Growth { variable } => Latex(format!("g_{{{}}}", f(variable))), + Self::Interaction { link } => Latex(format!("k_{{{}}}", f(link))), + } + } +} + impl ODEParameterType for LotkaVolterraParameter {} /// This Lotka-Volterra ODE analysis is intended for application to CLDs. @@ -100,8 +111,8 @@ impl /// and [our paper on regulatory networks](crate::refs::RegNets). fn build_system_builder( &self, - model: &::ModelType, - ) -> PolynomialODESystemBuilder<::ParameterType> { + model: &DiscreteDblModel, + ) -> PolynomialODESystemBuilder { let mut builder = PolynomialODESystemBuilder::new(); for var in model.ob_generators_with_type(&self.var_ob_type) { @@ -198,16 +209,11 @@ impl ODESemanticsProblemData<::Parameter fn extend_scalars( &self, - sys: PolynomialSystem< - QualifiedName, - Parameter<::ParameterType>, - i8, - >, + sys: PolynomialSystem, i8>, ) -> PolynomialSystem { let sys = sys.extend_scalars(|poly| { poly.eval(|param| match param { LotkaVolterraParameter::Growth { variable } => { - // FIXME: this won't work, because `variable` will now be `Growth.variable` self.growth_rates.get(variable).cloned().unwrap_or_default() } LotkaVolterraParameter::Interaction { link } => { @@ -228,7 +234,7 @@ mod test { use super::*; use crate::{ dbl::model::MutDblModel, - simulate::ode::LatexEquation, + latex::{LatexEquation, LatexEquations, wrap_with_backslash_text}, stdlib::{models::*, theories::*}, }; @@ -276,18 +282,20 @@ mod test { fn to_latex() { let th = Rc::new(th_signed_category()); let model = negative_feedback(th); - let sys = LotkaVolterraAnalysis::default().build_system(&model); - let expected = vec![ + let system = LotkaVolterraAnalysis::default().build_system(&model); + let equations = + system.to_latex_equations_with_map(|name| wrap_with_backslash_text(name.to_string())); + let expected = LatexEquations(vec![ LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} x".to_string(), - rhs: "Growth(x) \\cdot x - Interaction(negative) \\cdot x \\cdot y".to_string(), + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} x".to_string()), + rhs: Latex("g_{x} \\cdot x - k_{\\text{negative}} \\cdot x \\cdot y".to_string()), }, LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} y".to_string(), - rhs: "Interaction(positive) \\cdot x \\cdot y + Growth(y) \\cdot y".to_string(), + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} y".to_string()), + rhs: Latex("k_{\\text{positive}} \\cdot x \\cdot y + g_{y} \\cdot y".to_string()), }, - ]; - assert_eq!(expected, sys.to_latex_equations()); + ]); + assert_eq!(expected, equations); } // Numerical test. diff --git a/packages/catlog/src/stdlib/analyses/ode/mass_action.rs b/packages/catlog/src/stdlib/analyses/ode/mass_action.rs index b70761dc0..c70eed04e 100644 --- a/packages/catlog/src/stdlib/analyses/ode/mass_action.rs +++ b/packages/catlog/src/stdlib/analyses/ode/mass_action.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; use tsify::Tsify; use super::Parameter; +use crate::latex::{Latex, ToLatexWithMap}; use crate::simulate::ode::PolynomialSystem; use crate::stdlib::analyses::ode::ode_semantics::*; use crate::stdlib::analyses::petri::transition_interface; @@ -35,6 +36,7 @@ impl ODESemantics for PetriNetMassActionSemantics { type ModelType = ModalDblModel; type ParameterType = MassActionParameter; type AnalysisType = PetriNetMassActionAnalysis; + type EquationsDataType = MassActionEquationsData; type ProblemDataType = MassActionProblemData; } @@ -42,6 +44,7 @@ impl ODESemantics for StockFlowMassActionSemantics { type ModelType = DiscreteTabModel; type ParameterType = MassActionParameter; type AnalysisType = StockFlowMassActionAnalysis; + type EquationsDataType = MassActionEquationsData; type ProblemDataType = MassActionProblemData; } @@ -68,11 +71,11 @@ pub enum MassConservationType { #[cfg_attr(feature = "serde-wasm", derive(Tsify))] #[cfg_attr(feature = "serde-wasm", tsify(into_wasm_abi, from_wasm_abi))] pub enum RateGranularity { - /// Each flow gets assigned a single consumption and single production rate. + /// Each flow (transition) gets assigned a single consumption and single production rate. PerTransition, - /// Each flow gets assigned a consumption rate for each input stock and - /// a production rate for each output stock. + /// Each flow (transition) gets assigned a consumption rate for each input stock (place) and + /// a production rate for each output stock (place). PerPlace, } @@ -160,6 +163,30 @@ impl fmt::Display for MassActionParameter { } } +impl ToLatexWithMap for MassActionParameter { + fn to_latex_with_map String>(&self, f: T) -> Latex { + match self { + MassActionParameter::Balanced { flow } => Latex(format!("r_{{{}}}", f(flow))), + MassActionParameter::Unbalanced { direction, parameter } => { + match (direction, parameter) { + (Direction::IncomingFlow, RateParameter::PerTransition { flow }) => { + Latex(format!("\\rho_{{{}}}", f(flow))) + } + (Direction::OutgoingFlow, RateParameter::PerTransition { flow }) => { + Latex(format!("\\kappa_{{{}}}", f(flow))) + } + (Direction::IncomingFlow, RateParameter::PerPlace { flow, stock }) => { + Latex(format!("\\rho_{{{}}}^{{{}}}", f(flow), f(stock))) + } + (Direction::OutgoingFlow, RateParameter::PerPlace { flow, stock }) => { + Latex(format!("\\kappa_{{{}}}^{{{}}}", f(flow), f(stock))) + } + } + } + } + } +} + impl ODEParameterType for MassActionParameter {} /// Mass-action ODE analysis for Petri nets. @@ -194,9 +221,8 @@ impl { fn build_system_builder( &self, - model: &::ModelType, - ) -> PolynomialODESystemBuilder<::ParameterType> - { + model: &ModalDblModel, + ) -> PolynomialODESystemBuilder { let mut builder = PolynomialODESystemBuilder::new(); for place in model.ob_generators_with_type(&self.place_ob_type) { @@ -331,9 +357,8 @@ impl { fn build_system_builder( &self, - model: &::ModelType, - ) -> PolynomialODESystemBuilder<::ParameterType> - { + model: &DiscreteTabModel, + ) -> PolynomialODESystemBuilder { let mut builder = PolynomialODESystemBuilder::new(); for stock in model.ob_generators_with_type(&self.stock_ob_type) { @@ -404,7 +429,28 @@ impl } } -/// Data defining an unbalanced mass-action ODE problem for a model. +/// Data defining mass-action ODE equations for a model. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-wasm", derive(Tsify))] +#[cfg_attr(feature = "serde-wasm", tsify(into_wasm_abi, from_wasm_abi))] +#[derive(Clone)] +pub struct MassActionEquationsData { + /// Whether or not mass is conserved. + #[cfg_attr(feature = "serde", serde(rename = "massConservationType"))] + pub mass_conservation_type: MassConservationType, +} + +impl Default for MassActionEquationsData { + fn default() -> Self { + Self { + mass_conservation_type: MassConservationType::Balanced, + } + } +} + +impl ODESemanticsEquationsData for MassActionEquationsData {} + +/// Data defining a mass-action ODE problem for a model. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde-wasm", derive(Tsify))] #[cfg_attr( @@ -412,9 +458,9 @@ impl tsify(into_wasm_abi, from_wasm_abi, hashmap_as_object) )] pub struct MassActionProblemData { - /// Whether or not mass is conserved. - #[cfg_attr(feature = "serde", serde(rename = "massConservationType"))] - pub mass_conservation_type: MassConservationType, + /// Data used for generating the equations (namely, whether or not mass is conserved). + #[cfg_attr(feature = "serde", serde(rename = "equationsData"))] + pub equations_data: MassActionEquationsData, /// Map from morphism IDs to consumption rate coefficients (nonnegative reals), /// for the balanced per transition case. @@ -451,6 +497,10 @@ pub struct MassActionProblemData { } impl ODESemanticsProblemData for MassActionProblemData { + fn equations_data(&self) -> impl ODESemanticsEquationsData { + self.equations_data.clone() + } + fn initial_values(&self) -> HashMap { self.initial_values.clone() } @@ -519,8 +569,10 @@ mod tests { use std::rc::Rc; use super::*; - use crate::simulate::ode::LatexEquation; - use crate::stdlib::{analyses, models::*, theories::*}; + use crate::{ + latex::{LatexEquation, LatexEquations}, + stdlib::{analyses, models::*, theories::*}, + }; // Tests for stock-flow diagrams. These all use the backward_link() model, // which has a single flow x==f==>y and a single link y->f. @@ -622,16 +674,16 @@ mod tests { ..StockFlowMassActionAnalysis::default() } .build_system(&model); - let expected = vec![ + let expected = LatexEquations(vec![ LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} x".to_string(), - rhs: "-Outgoing(f) \\cdot x \\cdot y".to_string(), + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} x".to_string()), + rhs: Latex("-\\kappa_{f} \\cdot x \\cdot y".to_string()), }, LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} y".to_string(), - rhs: "Incoming(f) \\cdot x \\cdot y".to_string(), + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} y".to_string()), + rhs: Latex("\\rho_{f} \\cdot x \\cdot y".to_string()), }, - ]; + ]); assert_eq!(expected, sys.to_latex_equations()); } } diff --git a/packages/catlog/src/stdlib/analyses/ode/ode_semantics.rs b/packages/catlog/src/stdlib/analyses/ode/ode_semantics.rs index b23a4a67a..747d09b3d 100644 --- a/packages/catlog/src/stdlib/analyses/ode/ode_semantics.rs +++ b/packages/catlog/src/stdlib/analyses/ode/ode_semantics.rs @@ -1,24 +1,22 @@ //! Analyses for different ODE semantics on models. //! //! Inspired by schema migration, we define the data of an ODE semantics on models in a theory to -//! consist of (in particular) a `PolynomialODESystemBuilder`, which contains all the data needed -//! for [`ode::polynomial_ode::PolynomialODEAnalysis`] to do the following: -//! -//! 1. Build the system as a model of the theory of polynomial ODE systems (i.e. multicategories) -//! with abstract coefficients, using `build_system_custom_parameters()`. -//! 2. Substitute in numerical coefficients, using `extend_polynomial_ode_scalars()`. -//! 3. Build an `ODEAnalysis>` that can be fed into an ODE solver, -//! using `polynomial_ode_analysis()`. -//! +//! consist of (in particular) a `PolynomialODESystemBuilder`, which constructs a model of the +//! theory of multicategories (viewed as polynomial ODE systems with abstract coefficients). This +//! is then passed to [`ode::polynomial_ode::PolynomialODEAnalysis`] which constructs from this a +//! `PolynomialSystem`, using `build_system_custom_parameters()`. + //! In short, this module constructs multicategories from models, and [`ode::polynomial_ode`] then //! constructs `PolynomialSystem` from multicategories. //! //! To implement a new ODE semantics for models in some theory, one essentially needs to create an //! empty struct and implement `ODESemantics`, and then follow the compiler. For more documentation, -//! see [`ode::polynomial_ode`]; for an example implementation, see [`ode::mass_action`]. +//! see [`ode::polynomial_ode`]; for a simple example see [`ode::lotka_volterra`], and for a more +//! complicated example see [`ode::mass_action`]. //! //! [`ode::polynomial_ode`]: crate::stdlib::analyses::ode::polynomial_ode //! [`ode::polynomial_ode::PolynomialODEAnalysis`]: crate::stdlib::analyses::ode::polynomial_ode::PolynomialODEAnalysis +//! [`ode::lotka_volterra`]: crate::stdlib::analyses::ode::lotka_volterra //! [`ode::mass_action`]: crate::stdlib::analyses::ode::mass_action use indexmap::IndexMap; @@ -28,9 +26,12 @@ use std::{collections::HashMap, fmt}; use crate::{ dbl::{ modal::{List, ModeApp}, - model::{DiscreteDblModel, DiscreteTabModel, ModalDblModel, ModalOb, MutDblModel}, + model::{ + DblModel, DiscreteDblModel, DiscreteTabModel, ModalDblModel, ModalOb, MutDblModel, + }, theory::{NonUnital, Unital}, }, + latex::{Latex, ToLatexWithMap}, one::FgCategory, simulate::ode::{NumericalPolynomialSystem, ODEProblem, PolynomialSystem}, stdlib::{ @@ -50,19 +51,23 @@ pub trait ODESemantics { /// identified with one another, or to be rendered differently in debug/LaTeX output. For an /// instructive example, see `MassActionParameter` in `ode::mass_action`. type ParameterType: ODEParameterType; - /// The data describing the things that the ODE semantics "cares about". (See the documentation - /// for `ODESemanticsAnalysis`). + /// The data describing the things that the ODE semantics "cares about". See the documentation + /// for `ODESemanticsAnalysis` for more details. type AnalysisType: ODESemanticsAnalysis; - /// The data describing how to turn the algebraic system of equations into a simulation, - /// including e.g. which values that appear in the front-end analysis correspond to which - /// parameters within the equations. + /// The data necessary for displaying the system of equations, to be provided at run-time by the + /// front-end. + type EquationsDataType: ODESemanticsEquationsData; + /// The data necessary for simulating the system of equations, to be provided at run-time by the + /// front-end. For example, which values appear in the front-end analysis widget, and to which + /// which parameters within the algebraic equations they correspond. Note that this is forced to + /// contain a value of type `EquationsDataType` by the definition of `ODESemanticsProblemData`. type ProblemDataType: ODESemanticsProblemData; } /// The models for which we support ODE semantics need to be sufficiently nice, though /// these bounds are not particularly restrictive. pub trait DblModelForODESemantics: - FgCategory + MutDblModel + Clone + FgCategory + DblModel + MutDblModel + Clone { } @@ -72,10 +77,19 @@ impl DblModelForODESemantics for ModalDblModel {} impl DblModelForODESemantics for ModalDblModel {} /// The type of the parameters in the ODE system need to be sufficiently nice, though -/// (again) these bounds are not particularly restrictive. -pub trait ODEParameterType: Eq + Ord + Clone + fmt::Display {} +/// (again) these bounds are not particularly restrictive. The two that will need the most +/// manual effort for implementation are `Display` and `ToLatex`, which govern how these +/// coefficients should be rendered. The `Display` trait is used for debugging whereas the +/// `ToLatex` trait is used for user-facing display. +pub trait ODEParameterType: Eq + Ord + Clone + fmt::Display + ToLatexWithMap {} /// The simplest type for parameters is `QualifiedName`. +impl ToLatexWithMap for QualifiedName { + fn to_latex_with_map String>(&self, f: T) -> Latex { + Latex(f(self)) + } +} + impl ODEParameterType for QualifiedName {} /// Builder for polynomial ODE systems. @@ -107,6 +121,15 @@ impl PolynomialODESystemBuilder

{ Self::default() } + /// Constructs an ODE system for an existing model of an ODE system. (Essentially trivial, but + /// useful to reduce boilerplate). + pub fn identity(model: ModalDblModel) -> Self { + Self { + model, + associated_parameters: HashMap::new(), + } + } + /// Returns a model of the theory of polynomial ODE systems. pub fn model(self) -> ModalDblModel { self.model @@ -203,10 +226,19 @@ pub enum ContributionSign { Negative, } +/// For some ODE semantics, it might be the case there extra information can be given to determine +/// the equations. For example, a boolean describing whether or not mass should be conserved, or +/// something more complicated. This is generally data that will be exposed to the frontend in the +/// corresponding analysis. For an example, see `mass_action::MassActionEquationsData`. +pub trait ODESemanticsEquationsData {} +impl ODESemanticsEquationsData for () {} + /// The trait describing how to turn the formal system of ODEs into a numerical problem, to be /// solved by an ODE solver and presented to the front-end. At minimum, such data must contain /// initial values for variables and the intended duration of simulation, as well as the method for -/// converting the parameters (which are of type `ODEParameterType`) into floats. +/// converting the parameters (which are of type `ODEParameterType`) into floats. Note that it must +/// also contain `ODESemanticsEquationsData`, since we need to know how to build the equations +/// before we are able to solve them numerically. // REQUEST | If you look at a struct that implements this trait (such as `LotkaVolterraProblemData`), // FOR | there are a lot of serde statements going on. Should I be able to just move them // FEEDBACK | (that is, those that come *before* the struct) here and have things all work? I'm still @@ -219,10 +251,14 @@ pub enum ContributionSign { // tsify(into_wasm_abi, from_wasm_abi, hashmap_as_object) // )] pub trait ODESemanticsProblemData { - // REQUEST | The two getters (`initial_values()` and `duration()`) are annoying boilerplate to - // FOR | ask to be implemented. Is there a nice way to get rid of them here? Without them, + // REQUEST | These getters (`equations_data`, `initial_values`, and `duration`) are annoying + // FOR | boilerplate to ask for. Is there a nice way to get rid of them here? Without them, // FEEDBACK | the call to `self.initial_values` in `build_analysis()` fails because there is no // _________/ way of knowing whether a struct implementing this trait actually has those fields. + // In short: + // is there a better way to ensure that any struct implementing a trait has specific fields? + /// Further data needed to specify the ODE equations. + fn equations_data(&self) -> impl ODESemanticsEquationsData {} /// Map from object IDs to initial values (nonnegative reals). fn initial_values(&self) -> HashMap; /// Duration of simulation. diff --git a/packages/catlog/src/stdlib/analyses/ode/polynomial_ode.rs b/packages/catlog/src/stdlib/analyses/ode/polynomial_ode.rs index d1fcfa08e..8f764f01d 100644 --- a/packages/catlog/src/stdlib/analyses/ode/polynomial_ode.rs +++ b/packages/catlog/src/stdlib/analyses/ode/polynomial_ode.rs @@ -13,8 +13,6 @@ use std::{collections::HashMap, fmt}; -use indexmap::IndexMap; -use nalgebra::DVector; use num_traits::Zero; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -27,31 +25,52 @@ use crate::{ model::{FpDblModel, ModalDblModel, ModalOb, MutDblModel}, theory::NonUnital, }, - simulate::ode::{NumericalPolynomialSystem, ODEProblem, PolynomialSystem}, + latex::{Latex, ToLatexWithMap}, + simulate::ode::PolynomialSystem, + stdlib::analyses::ode::{ + ODEParameterType, ODESemantics, ODESemanticsAnalysis, ODESemanticsProblemData, Parameter, + PolynomialODESystemBuilder, + }, zero::{QualifiedName, alg::Polynomial, name, rig::Monomial}, }; -use super::{ODEAnalysis, Parameter}; +/// Implementing Lotka-Volterra as an ODE semantics for models of type `DiscreteDblModel`. +pub struct PolynomialODESemantics; -/// Data defining an unbalanced mass-action ODE problem for a model. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-wasm", derive(Tsify))] -#[cfg_attr( - feature = "serde-wasm", - tsify(into_wasm_abi, from_wasm_abi, hashmap_as_object) -)] -pub struct PolynomialODEProblemData { - /// Map from morphism IDs to coefficients (nonnegative reals). - coefficients: HashMap, +impl ODESemantics for PolynomialODESemantics { + type ModelType = ModalDblModel; + type ParameterType = PolynomialODEParameter; + type AnalysisType = PolynomialODEAnalysis; + type EquationsDataType = (); + type ProblemDataType = PolynomialODEProblemData; +} - /// Map from object IDs to initial values (nonnegative reals). - #[cfg_attr(feature = "serde", serde(rename = "initialValues"))] - pub initial_values: HashMap, +/// Parameters come precisely from contributions. +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] +pub enum PolynomialODEParameter { + /// The parameter associated to a contribution. + Coefficient { + /// The contribution. + contribution: QualifiedName, + }, +} - /// Duration of simulation. - pub duration: f32, +impl fmt::Display for PolynomialODEParameter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self::Coefficient { contribution } = &self; + write!(f, "Coefficient({})", contribution) + } } +impl ToLatexWithMap for PolynomialODEParameter { + fn to_latex_with_map String>(&self, f: T) -> Latex { + let Self::Coefficient { contribution } = self; + Latex(format!("\\lambda_{{{}}}", f(contribution))) + } +} + +impl ODEParameterType for PolynomialODEParameter {} + /// Polynomial ODE analysis. /// /// The "canonical" analysis for system of polynomial ODEs, namely interpreting @@ -75,25 +94,54 @@ impl Default for PolynomialODEAnalysis { } } +// We give a trivial implementation of `ODESemanticsAnalysis` using the helper method +// `PolynomialODESystemBuilder::identity`. This is nice from a conceptual point of view (in that all +// polynomial ODE semantics are unified under one trait), but also concretely helpful in reducing +// boilerplate since we can then use `catlog-wasm::src::analyses::ode_semantics_simulation` and +// `catlog-wasm::src::analyses::ode_semantics_equations`. +impl + ODESemanticsAnalysis< + ::ModelType, + ::ParameterType, + > for PolynomialODEAnalysis +{ + fn build_system_builder( + &self, + model: &ModalDblModel, + ) -> PolynomialODESystemBuilder { + PolynomialODESystemBuilder::identity(model.clone()) + } +} + impl PolynomialODEAnalysis { - /// Creates a `PolynomialSystem` with symbolic coefficients of type `QualifiedName`. + /// Creates a `PolynomialSystem` with symbolic coefficients of type `PolynomialODEParameter`. pub fn build_system( &self, model: &ModalDblModel, - ) -> PolynomialSystem, i8> { + ) -> PolynomialSystem< + QualifiedName, + Parameter<::ParameterType>, + i8, + > { // The default is to build a system whose parameters are in bijective correspondence // with morphisms, given by using the `QualifiedName` of the morphism as the parameter - // generator. We thus build the graph of the identity function to pass as the HashMap - // of associated parameters. - let mut associated_parameters: HashMap = HashMap::new(); + // generator. + let mut associated_parameters: HashMap = + HashMap::new(); for mor in model.mor_generators_with_type(&self.positive_contribution_mor_type) { - associated_parameters.insert(mor.clone(), mor.clone()); + associated_parameters.insert( + mor.clone(), + PolynomialODEParameter::Coefficient { contribution: mor.clone() }, + ); } for mor in model.mor_generators_with_type(&self.negative_contribution_mor_type) { - associated_parameters.insert(mor.clone(), mor.clone()); + associated_parameters.insert( + mor.clone(), + PolynomialODEParameter::Coefficient { contribution: mor.clone() }, + ); } - self.build_system_custom_parameters::(model, associated_parameters) + self.build_system_custom_parameters::(model, associated_parameters) } /// Creates a `PolynomialSystem` with symbolic coefficients of some generic type. @@ -160,36 +208,54 @@ impl PolynomialODEAnalysis { } } -/// Substitutes numerical rate coefficients into a symbolic mass-action system. -pub fn extend_polynomial_ode_scalars( - sys: PolynomialSystem, i8>, - data: &PolynomialODEProblemData, -) -> PolynomialSystem { - let sys = sys.extend_scalars(|poly| { - poly.eval(|mor| data.coefficients.get(mor).cloned().unwrap_or_default()) - }); +/// Data defining an unbalanced mass-action ODE problem for a model. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-wasm", derive(Tsify))] +#[cfg_attr( + feature = "serde-wasm", + tsify(into_wasm_abi, from_wasm_abi, hashmap_as_object) +)] +pub struct PolynomialODEProblemData { + /// Map from morphism IDs to coefficients (nonnegative reals). + coefficients: HashMap, - sys.normalize() + /// Map from object IDs to initial values (nonnegative reals). + #[cfg_attr(feature = "serde", serde(rename = "initialValues"))] + pub initial_values: HashMap, + + /// Duration of simulation. + pub duration: f32, } -/// Builds the numerical ODE analysis for a system of polynomial ODEs whose scalars have been substituted. -pub fn polynomial_ode_analysis( - sys: PolynomialSystem, - data: PolynomialODEProblemData, -) -> ODEAnalysis> { - let ob_index: IndexMap<_, _> = - sys.components.keys().cloned().enumerate().map(|(i, x)| (x, i)).collect(); - let n = ob_index.len(); +impl ODESemanticsProblemData<::ParameterType> + for PolynomialODEProblemData +{ + fn initial_values(&self) -> HashMap { + self.initial_values.clone() + } - let initial_values = ob_index - .keys() - .map(|ob| data.initial_values.get(ob).copied().unwrap_or_default()); - let x0 = DVector::from_iterator(n, initial_values); + fn duration(&self) -> f32 { + self.duration + } - let num_sys = sys.to_numerical(); - let problem = ODEProblem::new(num_sys, x0).end_time(data.duration); + fn extend_scalars( + &self, + sys: PolynomialSystem< + QualifiedName, + Parameter<::ParameterType>, + i8, + >, + ) -> PolynomialSystem { + let sys = sys.extend_scalars(|poly| { + poly.eval(|mor| match mor { + PolynomialODEParameter::Coefficient { contribution } => { + self.coefficients.get(contribution).cloned().unwrap_or_default() + } + }) + }); - ODEAnalysis::new(problem, ob_index) + sys.normalize() + } } #[cfg(test)] @@ -199,42 +265,44 @@ mod tests { use super::*; use crate::{ - simulate::ode::LatexEquation, + latex::{Latex, LatexEquation, LatexEquations, wrap_with_backslash_text}, stdlib::{models::*, theories::*}, tt, }; /// (Unsigned) Lotka-Volterra dynamics on a two-level model. #[test] - fn unsigned_lotka_volterra_equations() { + fn polynomial_ode_unsigned_lotka_volterra_equations() { let th = Rc::new(th_polynomial_ode_system()); let model = unsigned_lotka_volterra_dynamics(th); let sys = PolynomialODEAnalysis::default().build_system(&model); let expected = expect!([r#" - dA = A_growth A + BA_interaction A B - dB = AB_interaction A B + B_growth B + CB_interaction B C - dC = BC_interaction B C + C_growth C + dA = Coefficient(A_growth) A + Coefficient(BA_interaction) A B + dB = Coefficient(AB_interaction) A B + Coefficient(B_growth) B + Coefficient(CB_interaction) B C + dC = Coefficient(BC_interaction) B C + Coefficient(C_growth) C "#]); expected.assert_eq(&sys.to_string()); } /// Lotka-Volterra dynamics on a two-level model with LaTeX. #[test] - fn lotka_volterra_equations_latex() { + fn polynomial_ode_lotka_volterra_equations_latex() { let th = Rc::new(th_signed_polynomial_ode_system()); let model = signed_lotka_volterra_dynamics(th); - let sys = PolynomialODEAnalysis::default().build_system(&model); - let expected = vec![ + let system = PolynomialODEAnalysis::default().build_system(&model); + let equations = + system.to_latex_equations_with_map(|name| wrap_with_backslash_text(name.to_string())); + let expected = LatexEquations(vec![ LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} A".to_string(), - rhs: "A_growth \\cdot A - BA_interaction \\cdot A \\cdot B".to_string(), + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} A".to_string()), + rhs: Latex("\\lambda_{\\text{A_growth}} \\cdot A - \\lambda_{\\text{BA_interaction}} \\cdot A \\cdot B".to_string()), }, LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} B".to_string(), - rhs: "AB_interaction \\cdot A \\cdot B + B_growth \\cdot B".to_string(), + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} B".to_string()), + rhs: Latex("\\lambda_{\\text{AB_interaction}} \\cdot A \\cdot B + \\lambda_{\\text{B_growth}} \\cdot B".to_string()), }, - ]; - assert_eq!(expected, sys.to_latex_equations()); + ]); + assert_eq!(expected, equations); } /// DoubleTT elaboration from text. @@ -255,9 +323,9 @@ mod tests { let model = model.unwrap().as_modal_non_unital().unwrap(); let sys = PolynomialODEAnalysis::default().build_system(&model); let expected = expect!([r#" - dX = h A - dY = g X^2 - dA = f X Y^2 + dX = Coefficient(h) A + dY = Coefficient(g) X^2 + dA = Coefficient(f) X Y^2 "#]); expected.assert_eq(&sys.to_string()); } diff --git a/packages/catlog/src/zero/alg.rs b/packages/catlog/src/zero/alg.rs index 6899222f9..b17257629 100644 --- a/packages/catlog/src/zero/alg.rs +++ b/packages/catlog/src/zero/alg.rs @@ -9,6 +9,9 @@ use std::ops::{Add, AddAssign, Mul, Neg}; use derivative::Derivative; +use crate::latex::{Latex, ToLatex, ToLatexWithMap}; +use crate::zero::QualifiedName; + use super::rig::*; /// A commutative algebra over a commutative ring. @@ -147,30 +150,31 @@ where } } -impl Polynomial +impl ToLatexWithMap for Polynomial where - Var: Display, - Coef: Display + DisplayCoef + Clone + PartialEq + One + Neg, - Exp: Display + PartialEq + One, + Var: Display + ToLatexWithMap, + Coef: Display + ToLatexWithMap + DisplayCoef + Clone + PartialEq + One + Neg, + Exp: Display + ToLatex + PartialEq + One, { - /// Convert to a LaTeX string, formatting each monomial via [`Monomial::to_latex`]. - pub fn to_latex(&self) -> String { + /// Convert to a LaTeX string, formatting each monomial via [`Monomial::to_latex_with_map`]. + fn to_latex_with_map String>(&self, f: F) -> Latex { let fmt_term = |coef: &Coef, monomial: &Monomial| -> String { - let monomial = monomial.to_latex(); + let Latex(monomial_latex) = monomial.to_latex_with_map(|mon| f(mon)); + let Latex(coef_latex) = coef.to_latex_with_map(|param| f(param)); if coef.is_one() { - monomial + monomial_latex } else if *coef == Coef::one().neg() { - format!("-{monomial}") + format!("-{monomial_latex}") } else if coef.needs_parentheses() { - format!("({coef}) \\cdot {monomial}") + format!("({coef_latex}) \\cdot {monomial_latex}") } else { - format!("{coef} \\cdot {monomial}") + format!("{coef_latex} \\cdot {monomial_latex}") } }; let mut terms = (&self.0).into_iter(); let Some((coef, monomial)) = terms.next() else { - return "0".to_string(); + return Latex("0".to_string()); }; let mut output = fmt_term(coef, monomial); for (coef, monomial) in terms { @@ -182,7 +186,7 @@ where output.push_str(&fmt_term(coef, monomial)); } } - output + Latex(output) } } diff --git a/packages/catlog/src/zero/rig.rs b/packages/catlog/src/zero/rig.rs index 800330b34..0a77012ca 100644 --- a/packages/catlog/src/zero/rig.rs +++ b/packages/catlog/src/zero/rig.rs @@ -22,6 +22,9 @@ use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use derivative::Derivative; use duplicate::duplicate_item; +use crate::latex::{Latex, ToLatex, ToLatexWithMap}; +use crate::zero::QualifiedName; + /// A commutative monoid, written additively. pub trait AdditiveMonoid: Add + Zero {} @@ -586,35 +589,36 @@ where } } -impl Monomial +impl ToLatexWithMap for Monomial where - Var: Display, - Exp: Display + PartialEq + One, + Var: Display + ToLatexWithMap, + Exp: Display + ToLatex + PartialEq + One, { /// Convert to a LaTeX string, separating variables with `\cdot`. - pub fn to_latex(&self) -> String { + fn to_latex_with_map String>(&self, f: F) -> Latex { let fmt_power = |var: &Var, exp: &Exp| { + let Latex(var_latex) = var.to_latex_with_map(|variable| f(variable)); if exp.is_one() { - format!("{var}") + var_latex.to_string() } else { let exp = exp.to_string(); if exp.len() == 1 { - format!("{var}^{exp}") + format!("{var_latex}^{exp}") } else { - format!("{var}^{{{exp}}}") + format!("{var_latex}^{{{exp}}}") } } }; let mut pairs = self.0.iter(); let Some((var, exp)) = pairs.next() else { - return "1".to_string(); + return Latex("1".to_string()); }; let mut output = fmt_power(var, exp); for (var, exp) in pairs { output.push_str(" \\cdot "); output.push_str(&fmt_power(var, exp)); } - output + Latex(output) } } @@ -743,7 +747,7 @@ mod tests { let monomial: Monomial = Monomial::one(); assert_eq!(monomial.to_string(), "1"); - assert_eq!(monomial.to_latex(), "1"); + assert_eq!(monomial.to_latex(), Latex("1".to_string())); let monomial: Monomial<_, u32> = [('x', 1), ('y', 0), ('x', 2)].into_iter().collect(); assert_eq!(monomial.normalize().to_string(), "x^3"); @@ -752,6 +756,6 @@ mod tests { assert_eq!(monomial.normalize().to_string(), "x y^{-2}"); let monomial: Monomial<_, i32> = [('x', 1), ('y', 2), ('z', -1)].into_iter().collect(); - assert_eq!(monomial.to_latex(), "x \\cdot y^2 \\cdot z^{-1}"); + assert_eq!(monomial.to_latex(), Latex("x \\cdot y^2 \\cdot z^{-1}".to_string())); } } diff --git a/packages/frontend/src/stdlib/analyses.tsx b/packages/frontend/src/stdlib/analyses.tsx index 8d4d4d0ab..d10fccd7f 100644 --- a/packages/frontend/src/stdlib/analyses.tsx +++ b/packages/frontend/src/stdlib/analyses.tsx @@ -1,12 +1,9 @@ import { lazy } from "solid-js"; import type { - LinearODEEquationsData, - LotkaVolterraEquationsData, MassActionEquationsData, MorType, ObType, - PolynomialODEEquationsData, StochasticMassActionProblemData, } from "catlog-wasm"; import type { DiagramAnalysisMeta, ModelAnalysisMeta } from "../theory"; @@ -24,6 +21,8 @@ type AnalysisOptions = { help?: string; }; +const ODESemanticsEquationsDisplay = lazy(() => import("./analyses/ode_semantics_equations")); + export const decapodes = ( options: AnalysisOptions, ): DiagramAnalysisMeta => ({ @@ -139,7 +138,7 @@ export function linearODEEquations( options: Partial & { getEquations: Simulators.LinearODEEquations; }, -): ModelAnalysisMeta { +): ModelAnalysisMeta { const { id = "linear-ode-equations", name = "Linear ODE equations", @@ -153,14 +152,11 @@ export function linearODEEquations( description, help, component: (props) => ( - + ), - initialContent: () => ({ - trivialData: true, - }), + initialContent: () => null, }; } -const LinearODEEquationsDisplay = lazy(() => import("./analyses/linear_ode_equations")); export function lotkaVolterra( options: Partial & { @@ -195,7 +191,7 @@ export function lotkaVolterraEquations( options: Partial & { getEquations: Simulators.LotkaVolterraEquations; }, -): ModelAnalysisMeta { +): ModelAnalysisMeta { const { id = "lotka-volterra-equations", name = "Lotka–Volterra equations", @@ -209,14 +205,11 @@ export function lotkaVolterraEquations( description, help, component: (props) => ( - + ), - initialContent: () => ({ - trivialData: true, - }), + initialContent: () => null, }; } -const LotkaVolterraEquationsDisplay = lazy(() => import("./analyses/lotka_volterra_equations")); export function massAction( options: Partial & { @@ -240,7 +233,7 @@ export function massAction( help, component: (props) => , initialContent: () => ({ - massConservationType: { type: "Balanced" }, + equationsData: { massConservationType: { type: "Balanced" } }, rates: {}, transitionProductionRates: {}, transitionConsumptionRates: {}, @@ -426,7 +419,7 @@ export function polynomialODEEquations( options: Partial & { getEquations: Simulators.PolynomialODEEquations; }, -): ModelAnalysisMeta { +): ModelAnalysisMeta { const { id = "polynomial-ode-equations", name = "Polynomial ODE equations", @@ -440,14 +433,11 @@ export function polynomialODEEquations( description, help, component: (props) => ( - + ), - initialContent: () => ({ - trivialData: true, - }), + initialContent: () => null, }; } -const PolynomialODEEquationsDisplay = lazy(() => import("./analyses/polynomial_ode_equations")); export function polynomialODESimulation( options: Partial & { diff --git a/packages/frontend/src/stdlib/analyses/lotka_volterra_equations.tsx b/packages/frontend/src/stdlib/analyses/lotka_volterra_equations.tsx deleted file mode 100644 index dcb6271d4..000000000 --- a/packages/frontend/src/stdlib/analyses/lotka_volterra_equations.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { BlockTitle, ExpandableTable, KatexDisplay } from "catcolab-ui-components"; -import { LotkaVolterraEquationsData } from "catlog-wasm"; -import type { ModelAnalysisProps } from "../../analysis"; -import { createModelODELatex } from "./model_ode_plot"; -import type { LotkaVolterraEquations } from "./simulator_types"; - -import "./simulation.css"; - -/** Display the symbolic mass-action dynamics equations for a model. */ -export default function LotkaVolterraEquationsDisplay( - props: ModelAnalysisProps & { - content: LotkaVolterraEquationsData; - getEquations: LotkaVolterraEquations; - title?: string; - }, -) { - const latexEquations = createModelODELatex( - () => props.liveModel.validatedModel(), - (model) => props.getEquations(model, props.content), - ); - - return ( -

- - }, - { cell: () => }, - { cell: (row) => }, - ]} - /> -
- ); -} diff --git a/packages/frontend/src/stdlib/analyses/mass_action.tsx b/packages/frontend/src/stdlib/analyses/mass_action.tsx index 6cfa1fe43..107a5f4bb 100644 --- a/packages/frontend/src/stdlib/analyses/mass_action.tsx +++ b/packages/frontend/src/stdlib/analyses/mass_action.tsx @@ -253,13 +253,13 @@ export default function MassAction( // Now we can generate the parameter tables that will actually be rendered. const ParameterTables = () => ( - + @@ -267,8 +267,8 @@ export default function MassAction( diff --git a/packages/frontend/src/stdlib/analyses/mass_action_config_form.tsx b/packages/frontend/src/stdlib/analyses/mass_action_config_form.tsx index 84332f3be..ef8feb2c0 100644 --- a/packages/frontend/src/stdlib/analyses/mass_action_config_form.tsx +++ b/packages/frontend/src/stdlib/analyses/mass_action_config_form.tsx @@ -4,7 +4,11 @@ import { CheckboxField, FormGroup, SelectField } from "catcolab-ui-components"; import type { MassActionEquationsData, MassActionProblemData, RateGranularity } from "catlog-wasm"; /** Configuration of a mass-action analysis. */ -export type Config = MassActionProblemData | MassActionEquationsData; +export type Config = MassActionEquationsData | MassActionProblemData; + +function isMassActionProblemData(config: Config): config is MassActionProblemData { + return (config as MassActionProblemData).equationsData !== undefined; +} /** Form to configure a mass-action analysis. */ export function MassActionConfigForm(props: { @@ -12,11 +16,21 @@ export function MassActionConfigForm(props: { changeConfig: (f: (config: Config) => void) => void; enableGranularity: boolean; }) { - const massConservation = () => props.config.massConservationType; - const massConservationGranularity = () => - props.config.massConservationType.type === "Unbalanced" - ? props.config.massConservationType.granularity + function massActionEquationsData(): MassActionEquationsData { + if (isMassActionProblemData(props.config)) { + return props.config.equationsData; + } else { + return props.config; + } + } + + const massConservation = () => massActionEquationsData().massConservationType; + const massConservationGranularity = () => { + const massConversarvation = massActionEquationsData().massConservationType; + return massConversarvation.type === "Unbalanced" + ? massConversarvation.granularity : undefined; + }; return ( @@ -25,14 +39,20 @@ export function MassActionConfigForm(props: { checked={massConservation().type === "Balanced"} onChange={(evt) => { props.changeConfig((content) => { + let massActionEquationsData: MassActionEquationsData; + if (isMassActionProblemData(content)) { + massActionEquationsData = content.equationsData; + } else { + massActionEquationsData = content; + } if (evt.currentTarget.checked) { - content.massConservationType = { + massActionEquationsData.massConservationType = { type: "Balanced", }; } else { - content.massConservationType = { + massActionEquationsData.massConservationType = { type: "Unbalanced", - granularity: "PerTransition", + granularity: "PerPlace", }; } }); @@ -41,12 +61,20 @@ export function MassActionConfigForm(props: { { props.changeConfig((content) => { - if (content.massConservationType.type === "Unbalanced") { - content.massConservationType.granularity = evt.currentTarget - .value as RateGranularity; + let massActionEquationsData: MassActionEquationsData; + if (isMassActionProblemData(content)) { + massActionEquationsData = content.equationsData; + } else { + massActionEquationsData = content; + } + if ( + massActionEquationsData.massConservationType.type === "Unbalanced" + ) { + massActionEquationsData.massConservationType.granularity = evt + .currentTarget.value as RateGranularity; } }); }} diff --git a/packages/frontend/src/stdlib/analyses/linear_ode_equations.tsx b/packages/frontend/src/stdlib/analyses/ode_semantics_equations.tsx similarity index 71% rename from packages/frontend/src/stdlib/analyses/linear_ode_equations.tsx rename to packages/frontend/src/stdlib/analyses/ode_semantics_equations.tsx index d0e65d2db..774a37d85 100644 --- a/packages/frontend/src/stdlib/analyses/linear_ode_equations.tsx +++ b/packages/frontend/src/stdlib/analyses/ode_semantics_equations.tsx @@ -1,22 +1,21 @@ import { BlockTitle, ExpandableTable, KatexDisplay } from "catcolab-ui-components"; -import { LinearODEEquationsData } from "catlog-wasm"; +import { DblModel, LatexEquations } from "catlog-wasm"; import type { ModelAnalysisProps } from "../../analysis"; import { createModelODELatex } from "./model_ode_plot"; -import type { LinearODEEquations } from "./simulator_types"; import "./simulation.css"; /** Display the symbolic mass-action dynamics equations for a model. */ -export default function LinearODEEquationsDisplay( - props: ModelAnalysisProps & { - content: LinearODEEquationsData; - getEquations: LinearODEEquations; +export default function ODESemanticsEquationsDisplay( + props: ModelAnalysisProps & { + content: null; + getEquations: (model: DblModel) => LatexEquations; title?: string; }, ) { const latexEquations = createModelODELatex( () => props.liveModel.validatedModel(), - (model) => props.getEquations(model, props.content), + (model) => props.getEquations(model), ); return ( diff --git a/packages/frontend/src/stdlib/analyses/polynomial_ode_equations.tsx b/packages/frontend/src/stdlib/analyses/polynomial_ode_equations.tsx deleted file mode 100644 index 33486ff9e..000000000 --- a/packages/frontend/src/stdlib/analyses/polynomial_ode_equations.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { BlockTitle, ExpandableTable, KatexDisplay } from "catcolab-ui-components"; -import { PolynomialODEEquationsData } from "catlog-wasm"; -import type { ModelAnalysisProps } from "../../analysis"; -import { createModelODELatex } from "./model_ode_plot"; -import type { PolynomialODEEquations } from "./simulator_types"; - -import "./simulation.css"; - -/** Display the symbolic mass-action dynamics equations for a model. */ -export default function PolynomialODEEquationsDisplay( - props: ModelAnalysisProps & { - content: PolynomialODEEquationsData; - getEquations: PolynomialODEEquations; - title?: string; - }, -) { - const latexEquations = createModelODELatex( - () => props.liveModel.validatedModel(), - (model) => props.getEquations(model, props.content), - ); - - return ( -
- - }, - { cell: () => }, - { cell: (row) => }, - ]} - /> -
- ); -} diff --git a/packages/frontend/src/stdlib/analyses/simulator_types.ts b/packages/frontend/src/stdlib/analyses/simulator_types.ts index 335b5e7ed..cdd3c0f35 100644 --- a/packages/frontend/src/stdlib/analyses/simulator_types.ts +++ b/packages/frontend/src/stdlib/analyses/simulator_types.ts @@ -3,14 +3,11 @@ import type { KuramotoProblemData, LatexEquations, LinearODEProblemData, - LinearODEEquationsData, LotkaVolterraProblemData, - LotkaVolterraEquationsData, MassActionEquationsData, MassActionProblemData, ODEResult, ODEResultWithEquations, - PolynomialODEEquationsData, PolynomialODEProblemData, StochasticMassActionProblemData, } from "catlog-wasm"; @@ -28,15 +25,12 @@ export type LinearODESimulator = ( model: DblModel, data: LinearODEProblemData, ) => ODEResultWithEquations; -export type LinearODEEquations = (model: DblModel, data: LinearODEEquationsData) => LatexEquations; +export type LinearODEEquations = (model: DblModel) => LatexEquations; export type LotkaVolterraSimulator = ( model: DblModel, data: LotkaVolterraProblemData, ) => ODEResultWithEquations; -export type LotkaVolterraEquations = ( - model: DblModel, - data: LotkaVolterraEquationsData, -) => LatexEquations; +export type LotkaVolterraEquations = (model: DblModel) => LatexEquations; export type MassActionSimulator = ( model: DblModel, data: MassActionProblemData, @@ -53,10 +47,7 @@ export type PolynomialODESimulator = ( model: DblModel, data: PolynomialODEProblemData, ) => ODEResultWithEquations; -export type PolynomialODEEquations = ( - model: DblModel, - data: PolynomialODEEquationsData, -) => LatexEquations; +export type PolynomialODEEquations = (model: DblModel) => LatexEquations; /** Configuration for a Decapodes analysis of a diagram. */ export type DecapodesAnalysisContent = { diff --git a/packages/frontend/src/stdlib/theories/polynomial-ode.ts b/packages/frontend/src/stdlib/theories/polynomial-ode.ts index be3e3b03b..30b7b0995 100644 --- a/packages/frontend/src/stdlib/theories/polynomial-ode.ts +++ b/packages/frontend/src/stdlib/theories/polynomial-ode.ts @@ -34,8 +34,8 @@ export default function createPolynomialODETheory(theoryMeta: TheoryMeta): Theor ], modelAnalyses: [ analyses.polynomialODEEquations({ - getEquations(model, data) { - return thPolynomialODE.polynomialODEEquations(model, data); + getEquations(model) { + return thPolynomialODE.polynomialODEEquations(model); }, }), analyses.polynomialODESimulation({ diff --git a/packages/frontend/src/stdlib/theories/signed-polynomial-ode.ts b/packages/frontend/src/stdlib/theories/signed-polynomial-ode.ts index 63fb66988..45c02d810 100644 --- a/packages/frontend/src/stdlib/theories/signed-polynomial-ode.ts +++ b/packages/frontend/src/stdlib/theories/signed-polynomial-ode.ts @@ -51,8 +51,8 @@ export default function createSignedPolynomialODETheory(theoryMeta: TheoryMeta): ], modelAnalyses: [ analyses.polynomialODEEquations({ - getEquations(model, data) { - return thSignedPolynomialODE.polynomialODEEquations(model, data); + getEquations(model) { + return thSignedPolynomialODE.polynomialODEEquations(model); }, }), analyses.polynomialODESimulation({