diff --git a/packages/backend/schema.svg b/packages/backend/schema.svg index 5ac6f2949..47dcd9fcc 100644 --- a/packages/backend/schema.svg +++ b/packages/backend/schema.svg @@ -4,147 +4,135 @@ - - + + 019b2816-7c3a-7048-8f0f-21c3c6ef817d - - -       permissions     - -level     - -permission_level        - -object     - -→ refs     - -subject     - -→ users     + + +       permissions     + +level     + +permission_level        + +object     + +→ refs     + +subject     + +→ users     019b281f-bedf-74d3-b933-15d4af5fa061 - - -    refs   - -id   - -uuid    - -created     - -timestamp      - -deleted_at      - -timestamp | null        - -head    - -→ snapshots       + + +    refs   + +created     + +timestamp      + +deleted_at      + +timestamp      + +head    + +→ snapshots       019b2816-7c3a-7048-8f0f-21c3c6ef817d:w->019b281f-bedf-74d3-b933-15d4af5fa061 - - + + 019b2829-bef7-7017-83c3-19f7ea771f00 - - -    users   - -id   - -uuid    - -created     - -timestamp      - -signed_in      - -timestamp | null        - -username      - -text | null       - -display_name       - -text | null       - -state_doc_id       - -text | null       + + +    users   + +created     + +timestamp      + +signed_in      + +timestamp      + +username      + +text    + +display_name       + +text    + +state_doc_id       + +text    019b2816-7c3a-7048-8f0f-21c3c6ef817d:w->019b2829-bef7-7017-83c3-19f7ea771f00 - - + + 019b282d-ec81-718f-af71-5e3d0f841924 - - -      snapshots    - -id   - -int    - -content     - -jsonb     - -last_updated       - -timestamp      - -doc_id     - -text    - -for_ref     - -→ refs     + + +      snapshots    + +content     + +jsonb     + +last_updated       + +timestamp      + +doc_id     + +text    + +for_ref     + +→ refs     019b281f-bedf-74d3-b933-15d4af5fa061:w->019b282d-ec81-718f-af71-5e3d0f841924 - - + + 019b282d-ec81-718f-af71-5e3d0f841924:w->019b281f-bedf-74d3-b933-15d4af5fa061 - - + + 019b283f-3445-72bf-8102-98cb3c2617e8 - - -     storage    - -key    - -text[]     - -data    - -bytea     + + +     storage    + +key    + +text[]     + +data    + +bytea     diff --git a/packages/catlog-wasm/src/analyses.rs b/packages/catlog-wasm/src/analyses.rs index 012a4f838..18b89d3ee 100644 --- a/packages/catlog-wasm/src/analyses.rs +++ b/packages/catlog-wasm/src/analyses.rs @@ -3,307 +3,156 @@ use serde::{Deserialize, Serialize}; use tsify::Tsify; -use super::result::JsResult; -use catlog::simulate::ode::LatexEquation; -use catlog::stdlib::analyses; -use catlog::stdlib::analyses::ode::{Direction, FlowParameter, ODESolution, RateParameter}; +use catlog::latex::{Latex, LatexEquations, ToLatexEquations}; +use catlog::simulate::ode::PolynomialSystem; +use catlog::stdlib::analyses::ode::{self, Parameter}; use catlog::zero::QualifiedName; +use crate::latex::RenderPolynomial; + +use super::latex::{latex_mor_names_mass_action, latex_ob_names_mass_action}; use super::model::DblModel; +use super::result::JsResult; /// The result of an ODE analysis, containing the solution when successful. #[derive(Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] -pub struct ODEResult(pub JsResult); +pub struct ODEResult(pub JsResult); -/// The result of an ODE analysis including equations in LaTex with substitutions. +/// The result of an ODE analysis including equations in LaTeX with substitutions. #[derive(Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct ODEResultWithEquations { /// The result of the simulation. - pub solution: JsResult, + pub solution: ODEResult, /// The equations in LaTeX format with parameters substituted. #[serde(rename = "latexEquations")] - pub latex_equations: Vec, + pub latex_equations: LatexEquations, +} + +/// The mass-action analysis is currently implemented for Petri nets and stock-flow +/// diagrams, and we can avoid some code reduplication by making this explicit. +pub enum MassActionAnalysisLogic { + /// The modal theory of Petri nets. + PetriNet, + /// The discrete tabulator theory of stock-flow diagrams. + StockFlow, } -/// Symbolic equations in LaTeX format. +/// The analysis data for mass-action equations. #[derive(Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] -pub struct ODELatex(pub Vec); - -/// Creates a closure that formats object names for LaTeX output. -pub(crate) fn latex_ob_names_mass_action(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 - } - } +pub struct MassActionEquationsData { + /// The mass-conservation type. + #[serde(rename = "massConservationType")] + pub mass_conservation_type: ode::MassConservationType, } -/// 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(&FlowParameter) -> 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: &FlowParameter| match id { - FlowParameter::Balanced { transition } => { - let sub = transition_subscript(transition); - format!("r_{{{sub}}}") - } - FlowParameter::Unbalanced { direction, parameter } => match (direction, parameter) { - (Direction::IncomingFlow, RateParameter::PerTransition { transition }) => { - let sub = transition_subscript(transition); - format!("\\rho_{{{sub}}}") - } - (Direction::OutgoingFlow, RateParameter::PerTransition { transition }) => { - let sub = transition_subscript(transition); - format!("\\kappa_{{{sub}}}") +impl RenderPolynomial for MassActionEquationsData { + fn render_variable(&self, model: DblModel) -> impl Fn(&QualifiedName) -> Latex { + move |id: &QualifiedName| { + let name = model.ob_namespace.label_string(id); + if name.chars().count() > 1 { + Latex("\\text{{{name}}}".to_string()) + } else { + Latex(name) } - (Direction::IncomingFlow, RateParameter::PerPlace { transition, place }) => { - let sub = transition_subscript(transition); - let output_place_label = model.ob_namespace.label_string(place); - format!("\\rho_{{{sub}}}^{{\\text{{{output_place_label}}}}}") + } + } + + fn render_coefficient(&self, model: DblModel) -> impl Fn(Coef) -> Latex { + // 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}}}") } - (Direction::OutgoingFlow, RateParameter::PerPlace { transition, place }) => { + }; + + move |id: &ode::FlowParameter| match id { + ode::FlowParameter::Balanced { transition } => { let sub = transition_subscript(transition); - let input_place_label = model.ob_namespace.label_string(place); - format!("\\kappa_{{{sub}}}^{{\\text{{{input_place_label}}}}}") + format!("r_{{{sub}}}") } - }, + ode::FlowParameter::Unbalanced { direction, parameter } => match (direction, parameter) { + (ode::Direction::IncomingFlow, ode::RateParameter::PerTransition { transition }) => { + let sub = transition_subscript(transition); + format!("\\rho_{{{sub}}}") + } + (ode::Direction::OutgoingFlow, ode::RateParameter::PerTransition { transition }) => { + let sub = transition_subscript(transition); + format!("\\kappa_{{{sub}}}") + } + (ode::Direction::IncomingFlow, ode::RateParameter::PerPlace { transition, place }) => { + let sub = transition_subscript(transition); + let output_place_label = model.ob_namespace.label_string(place); + format!("\\rho_{{{sub}}}^{{\\text{{{output_place_label}}}}}") + } + (ode::Direction::OutgoingFlow, ode::RateParameter::PerPlace { transition, place }) => { + let sub = transition_subscript(transition); + let input_place_label = model.ob_namespace.label_string(place); + format!("\\kappa_{{{sub}}}^{{\\text{{{input_place_label}}}}}") + } + }, + } } } -/// Simulates mass-action ODE on tabulated models. -pub(crate) fn mass_action_tab( +/// Generates the PolynomialSystem for mass-action dynamics. +fn mass_action_system( model: &DblModel, - data: analyses::ode::MassActionProblemData, -) -> Result { - let realised_model = model.discrete_tab()?; - let analysis = analyses::ode::StockFlowMassActionAnalysis::default(); - let sys = analysis.build_system(realised_model, data.mass_conservation_type); - let sys_extended_scalars = analyses::ode::extend_mass_action_scalars(sys, &data); - let latex_equations = sys_extended_scalars - .map_variables(latex_ob_names_mass_action(model)) - .to_latex_equations(); - let analysis = analyses::ode::into_mass_action_analysis(sys_extended_scalars, data); - let solution = analysis.solve_with_defaults().map_err(|err| format!("{err:?}")); - Ok(ODEResultWithEquations { - solution: solution.into(), - latex_equations, - }) + mass_conservation_type: ode::MassConservationType, + logic: MassActionAnalysisLogic, +) -> Result, i8>, String> { + match logic { + MassActionAnalysisLogic::PetriNet => { + let realised_model = model.modal()?; + let analysis = ode::PetriNetMassActionAnalysis::default(); + Ok(analysis.build_system(realised_model, mass_conservation_type)) + } + MassActionAnalysisLogic::StockFlow => { + let realised_model = model.discrete_tab()?; + let analysis = ode::StockFlowMassActionAnalysis::default(); + Ok(analysis.build_system(realised_model, mass_conservation_type)) + } + } } -/// Simulates mass-action ODE on modal models. -pub(crate) fn mass_action_modal( +/// Simulates mass-action ODEs. +pub(crate) fn mass_action_simulation( model: &DblModel, - data: analyses::ode::MassActionProblemData, + data: ode::MassActionProblemData, + logic: MassActionAnalysisLogic, ) -> Result { - let realised_model = model.modal()?; - let analysis = analyses::ode::PetriNetMassActionAnalysis::default(); - let sys = analysis.build_system(realised_model, data.mass_conservation_type); - let sys_extended_scalars = analyses::ode::extend_mass_action_scalars(sys, &data); + let sys = mass_action_system(model, data.mass_conservation_type, logic); + let sys_extended_scalars = ode::extend_mass_action_scalars(sys?, &data); let latex_equations = sys_extended_scalars .map_variables(latex_ob_names_mass_action(model)) .to_latex_equations(); - let analysis = analyses::ode::into_mass_action_analysis(sys_extended_scalars, data); + let analysis = ode::into_mass_action_analysis(sys_extended_scalars, data); let solution = analysis.solve_with_defaults().map_err(|err| format!("{err:?}")); Ok(ODEResultWithEquations { - solution: solution.into(), - latex_equations, + solution: ODEResult(solution.into()), + latex_equations: latex_equations, }) } -/// 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: analyses::ode::MassConservationType, -} - -/// Generates mass-action equations for tabulated models. -pub(crate) fn mass_action_equations_tab( +/// Generates mass-action equations for the equations. +pub(crate) fn mass_action_equations( model: &DblModel, data: MassActionEquationsData, -) -> Result { - let realised_model = model.discrete_tab()?; - let analysis = analyses::ode::StockFlowMassActionAnalysis::default(); - let sys = analysis.build_system(realised_model, data.mass_conservation_type); - let equations = sys - .map_variables(latex_ob_names_mass_action(model)) - .extend_scalars(|param| param.map_variables(latex_mor_names_mass_action(model))) + logic: MassActionAnalysisLogic, +) -> Result { + let sys = mass_action_system(model, data.mass_conservation_type, logic); + let equations = sys? + .map_variables(data.render_variable(model)) + .extend_scalars(|param| param.map_variables(data.render_coefficient(model))) .to_latex_equations(); - Ok(ODELatex(equations)) -} - -/// Generates mass-action equations for modal models. -pub(crate) fn mass_action_equations_modal( - model: &DblModel, - data: MassActionEquationsData, -) -> Result { - let realised_model = model.modal()?; - let analysis = analyses::ode::PetriNetMassActionAnalysis::default(); - let sys = analysis.build_system(realised_model, data.mass_conservation_type); - let equations = sys - .map_variables(latex_ob_names_mass_action(model)) - .extend_scalars(|param| param.map_variables(latex_mor_names_mass_action(model))) - .to_latex_equations(); - Ok(ODELatex(equations)) -} - -#[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, 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 = ode::StockFlowMassActionAnalysis::default(); - let sys = analysis.build_system( - tab_model, - ode::MassConservationType::Unbalanced(ode::RateGranularity::PerTransition), - ); - let equations = sys - .map_variables(latex_ob_names_mass_action(&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}}) \\text{xxx} \\text{yyy}".to_string(), - }, - LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{yyy}".to_string(), - rhs: "(\\rho_{\\text{fff}}) \\text{xxx} \\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 = ode::StockFlowMassActionAnalysis::default(); - let sys = analysis.build_system( - tab_model, - ode::MassConservationType::Unbalanced(ode::RateGranularity::PerTransition), - ); - let equations = sys - .map_variables(latex_ob_names_mass_action(&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}}) \\text{xxx} \\text{yyy}" - .to_string(), - }, - LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{yyy}".to_string(), - rhs: "(\\rho_{\\text{xxx} \\to \\text{yyy}}) \\text{xxx} \\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())) - ); - } + Ok(equations) } diff --git a/packages/catlog-wasm/src/latex.rs b/packages/catlog-wasm/src/latex.rs new file mode 100644 index 000000000..66862ce38 --- /dev/null +++ b/packages/catlog-wasm/src/latex.rs @@ -0,0 +1,218 @@ +//! Auxiliary structs and glue code for any LaTeX code being passed through analyses. + +use crate::latex::ode::Parameter; +use catlog::latex::Latex; +use catlog::stdlib::analyses::ode; +use catlog::zero::QualifiedName; + +use super::model::DblModel; + +/// Certain analyses will have an opinion on how to (re)name the variables and +/// coefficients of a polynomial when rendering it as a LaTeX expression. For example, +/// the interaction coefficients in Lotka–Volterra tend to be called `k_{AB}`, whereas +/// in mass-action they are `r_{AB}`. Note that this requires the model itself as input. +pub trait RenderPolynomial { + /// How to render the variable names. + fn render_variable(&self, model: DblModel) -> impl Fn(&QualifiedName) -> Latex; + /// How to render the coefficient names. + fn render_coefficient(&self, model: DblModel) -> impl Fn(&Coef) -> Latex; +} + +/// Creates a closure that formats object names for LaTeX output. +pub(crate) fn latex_ob_names_mass_action(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_mass_action( + model: &DblModel, +) -> impl Fn(&ode::FlowParameter) -> 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::FlowParameter| match id { + ode::FlowParameter::Balanced { transition } => { + let sub = transition_subscript(transition); + format!("r_{{{sub}}}") + } + ode::FlowParameter::Unbalanced { direction, parameter } => match (direction, parameter) { + (ode::Direction::IncomingFlow, ode::RateParameter::PerTransition { transition }) => { + let sub = transition_subscript(transition); + format!("\\rho_{{{sub}}}") + } + (ode::Direction::OutgoingFlow, ode::RateParameter::PerTransition { transition }) => { + let sub = transition_subscript(transition); + format!("\\kappa_{{{sub}}}") + } + (ode::Direction::IncomingFlow, ode::RateParameter::PerPlace { transition, place }) => { + let sub = transition_subscript(transition); + let output_place_label = model.ob_namespace.label_string(place); + format!("\\rho_{{{sub}}}^{{\\text{{{output_place_label}}}}}") + } + (ode::Direction::OutgoingFlow, ode::RateParameter::PerPlace { transition, place }) => { + let sub = transition_subscript(transition); + let input_place_label = model.ob_namespace.label_string(place); + format!("\\kappa_{{{sub}}}^{{\\text{{{input_place_label}}}}}") + } + }, + } +} + +#[cfg(test)] +mod tests { + use catlog::dbl::modal::{List, ModalMorType, ModalOb, ModalObType}; + use catlog::dbl::model::{ModalDblModel, MutDblModel}; + use catlog::latex::{Latex, LatexEquation, LatexEquations, ToLatexEquations}; + 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 = ode::StockFlowMassActionAnalysis::default(); + let sys = analysis.build_system( + tab_model, + ode::MassConservationType::Unbalanced(ode::RateGranularity::PerTransition), + ); + let equations = sys + .map_variables(latex_ob_names_mass_action(&model)) + .extend_scalars(|param| param.map_variables(latex_mor_names_mass_action(&model))) + .to_latex_equations(); + + let expected = vec![ + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{xxx}".to_string()), + rhs: Latex("(-\\kappa_{\\text{fff}}) \\text{xxx} \\text{yyy}".to_string()), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{yyy}".to_string()), + rhs: Latex("(\\rho_{\\text{fff}}) \\text{xxx} \\text{yyy}".to_string()), + }, + ]; + assert_eq!(equations, LatexEquations(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 = ode::StockFlowMassActionAnalysis::default(); + let sys = analysis.build_system( + tab_model, + ode::MassConservationType::Unbalanced(ode::RateGranularity::PerTransition), + ); + let equations = sys + .map_variables(latex_ob_names_mass_action(&model)) + .extend_scalars(|param| param.map_variables(latex_mor_names_mass_action(&model))) + .to_latex_equations(); + + let expected = vec![ + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{xxx}".to_string()), + rhs: Latex("(-\\kappa_{\\text{xxx} \\to \\text{yyy}}) \\text{xxx} \\text{yyy}" + .to_string()), + }, + LatexEquation { + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{yyy}".to_string()), + rhs: Latex("(\\rho_{\\text{xxx} \\to \\text{yyy}}) \\text{xxx} \\text{yyy}".to_string()), + }, + ]; + assert_eq!(equations, LatexEquations(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/lib.rs b/packages/catlog-wasm/src/lib.rs index 42c71818d..020f6552a 100644 --- a/packages/catlog-wasm/src/lib.rs +++ b/packages/catlog-wasm/src/lib.rs @@ -21,6 +21,7 @@ pub mod theory; pub mod wd; pub mod analyses; +pub mod latex; #[allow(clippy::new_without_default)] #[allow(missing_docs)] pub mod theories; diff --git a/packages/catlog-wasm/src/theories.rs b/packages/catlog-wasm/src/theories.rs index 188cdf6ff..f35e40600 100644 --- a/packages/catlog-wasm/src/theories.rs +++ b/packages/catlog-wasm/src/theories.rs @@ -10,9 +10,11 @@ use catlog::dbl::theory::{self as theory, Unital}; use catlog::one::Path; use catlog::stdlib::{analyses, models, theories, theory_morphisms}; use catlog::zero::{QualifiedLabel, name}; +use catlog::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. @@ -315,7 +317,7 @@ impl ThCategoryLinks { model: &DblModel, data: analyses::ode::MassActionProblemData, ) -> Result { - mass_action_tab(model, data) + mass_action_simulation(model, data, MassActionAnalysisLogic::StockFlow) } /// Returns the symbolic mass-action equations in LaTeX format. @@ -324,8 +326,8 @@ impl ThCategoryLinks { &self, model: &DblModel, data: MassActionEquationsData, - ) -> Result { - mass_action_equations_tab(model, data) + ) -> Result { + mass_action_equations(model, data, MassActionAnalysisLogic::StockFlow) } } @@ -352,7 +354,7 @@ impl ThCategorySignedLinks { model: &DblModel, data: analyses::ode::MassActionProblemData, ) -> Result { - mass_action_tab(model, data) + mass_action_simulation(model, data, MassActionAnalysisLogic::StockFlow) } /// Returns the symbolic mass-action equations in LaTeX format. @@ -361,8 +363,8 @@ impl ThCategorySignedLinks { &self, model: &DblModel, data: MassActionEquationsData, - ) -> Result { - mass_action_equations_tab(model, data) + ) -> Result { + mass_action_equations(model, data, MassActionAnalysisLogic::StockFlow) } } @@ -389,7 +391,7 @@ impl ThSymMonoidalCategory { model: &DblModel, data: analyses::ode::MassActionProblemData, ) -> Result { - mass_action_modal(model, data) + mass_action_simulation(model, data, MassActionAnalysisLogic::PetriNet) } /// Returns the symbolic mass-action equations in LaTeX format. @@ -398,8 +400,8 @@ impl ThSymMonoidalCategory { &self, model: &DblModel, data: MassActionEquationsData, - ) -> Result { - mass_action_equations_modal(model, data) + ) -> Result { + mass_action_equations(model, data, MassActionAnalysisLogic::PetriNet) } /// Simulates the stochastic mass-action system derived from a model. diff --git a/packages/catlog/src/latex.rs b/packages/catlog/src/latex.rs new file mode 100644 index 000000000..43dd7b227 --- /dev/null +++ b/packages/catlog/src/latex.rs @@ -0,0 +1,52 @@ +//! 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 std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// We should mark which strings are to be parsed as LaTeX. +#[derive(Debug, PartialEq, Eq)] +#[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 object that can be rendered to LaTeX. +pub trait ToLatex { + /// Convert the object to its LaTeX representation. + fn to_latex(&self) -> Latex; +} + +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// 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: Latex, + /// The right-hand side of the equation. + pub rhs: Latex, +} + +/// Symbolic equations in LaTeX format. +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct LatexEquations(pub Vec); + +/// An object that can be rendered to a collection of LaTeX equations (of the form +/// `lhs = rhs`). +pub trait ToLatexEquations { + /// Convert the object to the LaTeX equations. + fn to_latex_equations(&self) -> LatexEquations; +} 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 462402ccd..45b7b0308 100644 --- a/packages/catlog/src/simulate/ode/polynomial.rs +++ b/packages/catlog/src/simulate/ode/polynomial.rs @@ -9,14 +9,10 @@ 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, ToLatexEquations}; use crate::zero::alg::Polynomial; /// A system of polynomial differential equations. @@ -93,34 +89,6 @@ where let components = self.components.into_iter().map(|(var, poly)| (var, f(poly))).collect(); PolynomialSystem { components } } - - /// Converts to equations as LaTeX strings. - pub fn to_latex_equations(&self) -> Vec - where - Var: Display, - Coef: Display + PartialEq + One + Neg, - Exp: Display + PartialEq + One, - { - self.components - .iter() - .map(|(var, poly)| LatexEquation { - lhs: format!("\\frac{{\\mathrm{{d}}}}{{\\mathrm{{d}}t}} {var}"), - rhs: poly.to_latex(), - }) - .collect() - } -} - -#[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, } impl PolynomialSystem @@ -144,6 +112,32 @@ where } } +impl ToLatexEquations for PolynomialSystem +where + Var: Display, + Coef: Display + PartialEq + One + Neg, + Exp: Display + PartialEq + One, +{ + + /// Converts to equations as LaTeX strings. + fn to_latex_equations(&self) -> LatexEquations + where + Var: Display, + Coef: Display + PartialEq + One + Neg, + Exp: Display + PartialEq + One, + { + LatexEquations( + self.components + .iter() + .map(|(var, poly)| LatexEquation { + lhs: Latex(format!("\\frac{{\\mathrm{{d}}}}{{\\mathrm{{d}}t}} {var}")), + rhs: poly.to_latex(), + }) + .collect() + ) + } +} + impl Display for PolynomialSystem where Var: Display, @@ -217,7 +211,7 @@ mod tests { use super::super::textplot_ode_result; use super::*; - type Parameter = Polynomial; + type Parameter = Polynomial; #[test] fn sir() { diff --git a/packages/catlog/src/stdlib/analyses/ode/mass_action.rs b/packages/catlog/src/stdlib/analyses/ode/mass_action.rs index 0b506c1b2..2b5fd11d0 100644 --- a/packages/catlog/src/stdlib/analyses/ode/mass_action.rs +++ b/packages/catlog/src/stdlib/analyses/ode/mass_action.rs @@ -187,7 +187,7 @@ pub struct MassActionProblemData { } /// Symbolic parameter in mass-action polynomial system. -type Parameter = Polynomial; +pub type Parameter = Polynomial; /// Mass-action ODE analysis for Petri nets. /// @@ -475,7 +475,7 @@ mod tests { use std::rc::Rc; use super::*; - use crate::simulate::ode::LatexEquation; + use crate::latex::{Latex, LatexEquation, LatexEquations, ToLatexEquations}; use crate::stdlib::{analyses, models::*, theories::*}; // Tests for stock-flow diagrams. These all use the backward_link() model, @@ -609,16 +609,16 @@ mod tests { analyses::ode::RateGranularity::PerTransition, ), ); - let expected = vec![ + let expected = LatexEquations(vec![ LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} x".to_string(), - rhs: "(-Outgoing(f)) x y".to_string(), + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} x".to_string()), + rhs: Latex("(-Outgoing(f)) x y".to_string()), }, LatexEquation { - lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} y".to_string(), - rhs: "(Incoming(f)) x y".to_string(), + lhs: Latex("\\frac{\\mathrm{d}}{\\mathrm{d}t} y".to_string()), + rhs: Latex("(Incoming(f)) x y".to_string()), }, - ]; + ]); assert_eq!(expected, sys.to_latex_equations()); } } diff --git a/packages/catlog/src/stdlib/analyses/ode/mod.rs b/packages/catlog/src/stdlib/analyses/ode/mod.rs index 756e06f79..741452795 100644 --- a/packages/catlog/src/stdlib/analyses/ode/mod.rs +++ b/packages/catlog/src/stdlib/analyses/ode/mod.rs @@ -16,7 +16,7 @@ use crate::simulate::ode::{ODEProblem, ODESystem}; use crate::zero::{QualifiedName, alg::Polynomial}; /// Symbolic parameter in polynomial system. -pub type Parameter = Polynomial; +pub type Parameter = Polynomial; /// Solution to an ODE problem. #[derive(Clone, Derivative)] diff --git a/packages/catlog/src/zero/alg.rs b/packages/catlog/src/zero/alg.rs index c98958ec2..f6f160d77 100644 --- a/packages/catlog/src/zero/alg.rs +++ b/packages/catlog/src/zero/alg.rs @@ -8,6 +8,8 @@ use std::ops::{Add, AddAssign, Mul, Neg}; use derivative::Derivative; +use crate::latex::{Latex, ToLatex}; + use super::rig::*; /// A commutative algebra over a commutative ring. @@ -146,18 +148,6 @@ where } } -impl Polynomial -where - Var: Display, - Coef: Display + PartialEq + One + Neg, - Exp: Display + PartialEq + One, -{ - /// Convert to a LaTeX string. - pub fn to_latex(&self) -> String { - self.0.to_latex() - } -} - impl FromIterator<(Coef, Monomial)> for Polynomial where Var: Ord, @@ -169,6 +159,22 @@ where } } +/// Print the combination using LaTeX. +impl ToLatex for Polynomial +where + Var: Display, + Coef: Display + PartialEq + One + Neg, + Exp: Display + PartialEq + One, +{ + fn to_latex(&self) -> Latex { + self.0.to_latex() + } +} + +/// Print the combination using ASCII. +/// +/// Intended for debugging/testing. It currently just uses the LaTeX format since we are not yet +/// using any LaTeX-specific characters there. impl Display for Polynomial where Var: Display, @@ -176,7 +182,7 @@ where Exp: Display + PartialEq + One, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.to_latex()) + write!(f, "{:?}", self.to_latex()) } } diff --git a/packages/catlog/src/zero/rig.rs b/packages/catlog/src/zero/rig.rs index 0ed7bf84c..1ced6d5d2 100644 --- a/packages/catlog/src/zero/rig.rs +++ b/packages/catlog/src/zero/rig.rs @@ -22,6 +22,8 @@ use std::ops::{Add, AddAssign, Mul, MulAssign, Neg}; use derivative::Derivative; use duplicate::duplicate_item; +use crate::latex::{Latex, ToLatex}; + /// A commutative monoid, written additively. pub trait AdditiveMonoid: Add + Zero {} @@ -168,13 +170,51 @@ where } } -impl Combination +/// Constructs a combination from a list of terms (coefficient-variable pairs). +impl FromIterator<(Coef, Var)> for Combination +where + Var: Ord, + Coef: Add, +{ + fn from_iter>(iter: T) -> Self { + let mut combination = Combination::default(); + for rhs in iter { + combination += rhs; + } + combination + } +} + +/// Iterates over the terms (coefficient-variable pairs) of the combination. +impl IntoIterator for Combination { + type Item = (Coef, Var); + type IntoIter = std::iter::Map, fn((Var, Coef)) -> (Coef, Var)>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter().map(|(var, coef)| (coef, var)) + } +} + +impl<'a, Var, Coef> IntoIterator for &'a Combination { + type Item = (&'a Coef, &'a Var); + type IntoIter = std::iter::Map< + btree_map::Iter<'a, Var, Coef>, + fn((&'a Var, &'a Coef)) -> (&'a Coef, &'a Var), + >; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter().map(|(var, coef)| (coef, var)) + } +} + +/// Print the combination using LaTeX. +impl ToLatex for Combination where Var: Display, Coef: Display + PartialEq + One + Neg, { /// Convert to a LaTeX string. - pub fn to_latex(&self) -> String { + fn to_latex(&self) -> Latex { let is_simple = |s: &str| { // Numeric: digits and dots if s.chars().all(|c| c.is_ascii_digit() || c == '.') { @@ -220,44 +260,7 @@ where output.push_str(&fmt_scalar_mul(coef, var)); } - output - } -} - -/// Constructs a combination from a list of terms (coefficient-variable pairs). -impl FromIterator<(Coef, Var)> for Combination -where - Var: Ord, - Coef: Add, -{ - fn from_iter>(iter: T) -> Self { - let mut combination = Combination::default(); - for rhs in iter { - combination += rhs; - } - combination - } -} - -/// Iterates over the terms (coefficient-variable pairs) of the combination. -impl IntoIterator for Combination { - type Item = (Coef, Var); - type IntoIter = std::iter::Map, fn((Var, Coef)) -> (Coef, Var)>; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter().map(|(var, coef)| (coef, var)) - } -} - -impl<'a, Var, Coef> IntoIterator for &'a Combination { - type Item = (&'a Coef, &'a Var); - type IntoIter = std::iter::Map< - btree_map::Iter<'a, Var, Coef>, - fn((&'a Var, &'a Coef)) -> (&'a Coef, &'a Var), - >; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter().map(|(var, coef)| (coef, var)) + Latex(output) } } @@ -271,7 +274,7 @@ where Coef: Display + PartialEq + One + Neg, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.to_latex()) + write!(f, "{:?}", self.to_latex()) } } diff --git a/packages/frontend/src/stdlib/analyses/model_ode_plot.ts b/packages/frontend/src/stdlib/analyses/model_ode_plot.ts index 703a30d08..bf3a8b2d0 100644 --- a/packages/frontend/src/stdlib/analyses/model_ode_plot.ts +++ b/packages/frontend/src/stdlib/analyses/model_ode_plot.ts @@ -3,8 +3,7 @@ import { type Accessor, createMemo } from "solid-js"; import type { DblModel, JsResult, - LatexEquation, - ODELatex, + LatexEquations, ODEResult, ODEResultWithEquations, } from "catlog-wasm"; @@ -14,7 +13,7 @@ import type { ODEPlotData, StateVarData } from "../../visualization"; /** Result of simulating an ODE with equations, containing both plot data and LaTeX equations. */ export type ODEPlotDataWithEquations = { plotData: JsResult; - latexEquations: LatexEquation[]; + latexEquations: LatexEquations; }; /** Convert an ODE solution result to plot data for a model. */ @@ -92,9 +91,9 @@ export function createModelODEPlotWithEquations( */ export function createModelODELatex( validatedModel: Accessor, - getEquations: (model: DblModel) => ODELatex, + getEquations: (model: DblModel) => LatexEquations, ) { - return createMemo( + return createMemo( () => { const validated = validatedModel(); if (validated?.tag !== "Valid") { diff --git a/packages/frontend/src/stdlib/analyses/simulator_types.ts b/packages/frontend/src/stdlib/analyses/simulator_types.ts index 6dc5640b3..699ccaf81 100644 --- a/packages/frontend/src/stdlib/analyses/simulator_types.ts +++ b/packages/frontend/src/stdlib/analyses/simulator_types.ts @@ -1,11 +1,11 @@ import type { DblModel, KuramotoProblemData, + LatexEquations, LinearODEProblemData, LotkaVolterraProblemData, MassActionEquationsData, MassActionProblemData, - ODELatex, ODEResult, ODEResultWithEquations, StochasticMassActionProblemData, @@ -29,7 +29,10 @@ export type StochasticMassActionSimulator = ( model: DblModel, data: StochasticMassActionProblemData, ) => ODEResult; -export type MassActionEquations = (model: DblModel, data: MassActionEquationsData) => ODELatex; +export type MassActionEquations = ( + model: DblModel, + data: MassActionEquationsData, +) => LatexEquations; /** Configuration for a Decapodes analysis of a diagram. */ export type DecapodesAnalysisContent = {