Skip to content
200 changes: 94 additions & 106 deletions packages/backend/schema.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
369 changes: 109 additions & 260 deletions packages/catlog-wasm/src/analyses.rs

Large diffs are not rendered by default.

218 changes: 218 additions & 0 deletions packages/catlog-wasm/src/latex.rs
Original file line number Diff line number Diff line change
@@ -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<Coef>(&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()))
);
}
}
1 change: 1 addition & 0 deletions packages/catlog-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 11 additions & 9 deletions packages/catlog-wasm/src/theories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -315,7 +317,7 @@ impl ThCategoryLinks {
model: &DblModel,
data: analyses::ode::MassActionProblemData,
) -> Result<ODEResultWithEquations, String> {
mass_action_tab(model, data)
mass_action_simulation(model, data, MassActionAnalysisLogic::StockFlow)
}

/// Returns the symbolic mass-action equations in LaTeX format.
Expand All @@ -324,8 +326,8 @@ impl ThCategoryLinks {
&self,
model: &DblModel,
data: MassActionEquationsData,
) -> Result<ODELatex, String> {
mass_action_equations_tab(model, data)
) -> Result<LatexEquations, String> {
mass_action_equations(model, data, MassActionAnalysisLogic::StockFlow)
}
}

Expand All @@ -352,7 +354,7 @@ impl ThCategorySignedLinks {
model: &DblModel,
data: analyses::ode::MassActionProblemData,
) -> Result<ODEResultWithEquations, String> {
mass_action_tab(model, data)
mass_action_simulation(model, data, MassActionAnalysisLogic::StockFlow)
}

/// Returns the symbolic mass-action equations in LaTeX format.
Expand All @@ -361,8 +363,8 @@ impl ThCategorySignedLinks {
&self,
model: &DblModel,
data: MassActionEquationsData,
) -> Result<ODELatex, String> {
mass_action_equations_tab(model, data)
) -> Result<LatexEquations, String> {
mass_action_equations(model, data, MassActionAnalysisLogic::StockFlow)
}
}

Expand All @@ -389,7 +391,7 @@ impl ThSymMonoidalCategory {
model: &DblModel,
data: analyses::ode::MassActionProblemData,
) -> Result<ODEResultWithEquations, String> {
mass_action_modal(model, data)
mass_action_simulation(model, data, MassActionAnalysisLogic::PetriNet)
}

/// Returns the symbolic mass-action equations in LaTeX format.
Expand All @@ -398,8 +400,8 @@ impl ThSymMonoidalCategory {
&self,
model: &DblModel,
data: MassActionEquationsData,
) -> Result<ODELatex, String> {
mass_action_equations_modal(model, data)
) -> Result<LatexEquations, String> {
mass_action_equations(model, data, MassActionAnalysisLogic::PetriNet)
}

/// Simulates the stochastic mass-action system derived from a model.
Expand Down
52 changes: 52 additions & 0 deletions packages/catlog/src/latex.rs
Original file line number Diff line number Diff line change
@@ -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<LatexEquation>);

/// 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;
}
1 change: 1 addition & 0 deletions packages/catlog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
pub mod refs;

pub mod egglog_util;
pub mod latex;
pub mod validate;

pub mod dbl;
Expand Down
Loading