catlog_wasm/
latex.rs

1//! Auxiliary structs and glue code for any LaTeX code being passed through analyses.
2
3use serde::{Deserialize, Serialize};
4use tsify::Tsify;
5
6use catlog::simulate::ode::LatexEquation;
7use catlog::stdlib::analyses::ode;
8use catlog::zero::QualifiedName;
9
10use super::model::DblModel;
11
12/// Symbolic equations in LaTeX format.
13#[derive(Serialize, Deserialize, Tsify)]
14#[tsify(into_wasm_abi, from_wasm_abi)]
15pub struct LatexEquations(pub Vec<LatexEquation>);
16
17/// Creates a closure that formats object names for LaTeX output.
18pub(crate) fn latex_ob_names_mass_action(model: &DblModel) -> impl Fn(&QualifiedName) -> String {
19    |id: &QualifiedName| {
20        let name = model.ob_namespace.label_string(id);
21        if name.chars().count() > 1 {
22            format!("\\text{{{name}}}")
23        } else {
24            name
25        }
26    }
27}
28
29/// Creates a closure that formats morphism names for mass-action LaTeX output.
30///
31/// When a morphism has a label, it is used directly. When unnamed, the label
32/// falls back to the domain→codomain format (e.g., `X \to Y`).
33pub(crate) fn latex_mor_names_mass_action(
34    model: &DblModel,
35) -> impl Fn(&ode::FlowParameter) -> String {
36    // Returns a LaTeX fragment for a transition, suitable for use as a subscript.
37    // Named morphisms produce `\text{name}`, unnamed ones produce
38    // `\text{dom} \to \text{cod}` so that `\to` is in math mode.
39    let transition_subscript = |transition: &QualifiedName| -> String {
40        if let Some(label) = model.mor_namespace.label(transition) {
41            format!("\\text{{{label}}}")
42        } else {
43            let (dom, cod) = model
44                .mor_generator_dom_cod_label_strings(transition)
45                .expect("Morphism in equation system should have domain and codomain");
46            format!("\\text{{{dom}}} \\to \\text{{{cod}}}")
47        }
48    };
49
50    move |id: &ode::FlowParameter| match id {
51        ode::FlowParameter::Balanced { transition } => {
52            let sub = transition_subscript(transition);
53            format!("r_{{{sub}}}")
54        }
55        ode::FlowParameter::Unbalanced { direction, parameter } => match (direction, parameter) {
56            (ode::Direction::IncomingFlow, ode::RateParameter::PerTransition { transition }) => {
57                let sub = transition_subscript(transition);
58                format!("\\rho_{{{sub}}}")
59            }
60            (ode::Direction::OutgoingFlow, ode::RateParameter::PerTransition { transition }) => {
61                let sub = transition_subscript(transition);
62                format!("\\kappa_{{{sub}}}")
63            }
64            (ode::Direction::IncomingFlow, ode::RateParameter::PerPlace { transition, place }) => {
65                let sub = transition_subscript(transition);
66                let output_place_label = model.ob_namespace.label_string(place);
67                format!("\\rho_{{{sub}}}^{{\\text{{{output_place_label}}}}}")
68            }
69            (ode::Direction::OutgoingFlow, ode::RateParameter::PerPlace { transition, place }) => {
70                let sub = transition_subscript(transition);
71                let input_place_label = model.ob_namespace.label_string(place);
72                format!("\\kappa_{{{sub}}}^{{\\text{{{input_place_label}}}}}")
73            }
74        },
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use catlog::dbl::modal::{List, ModalMorType, ModalOb, ModalObType};
81    use catlog::dbl::model::{ModalDblModel, MutDblModel};
82    use catlog::simulate::ode::LatexEquation;
83    use catlog::stdlib::{analyses::ode, theories};
84    use catlog::zero::{LabelSegment, Namespace, QualifiedName};
85    use std::rc::Rc;
86    use uuid::Uuid;
87
88    use super::*;
89    use crate::model::{DblModel, tests::backward_link};
90
91    #[test]
92    fn unbalanced_mass_action_latex_equations() {
93        let model = backward_link("xxx", "yyy", "fff");
94        let tab_model = model.discrete_tab().unwrap();
95        let analysis = ode::StockFlowMassActionAnalysis::default();
96        let sys = analysis.build_system(
97            tab_model,
98            ode::MassConservationType::Unbalanced(ode::RateGranularity::PerTransition),
99        );
100        let equations = sys
101            .map_variables(latex_ob_names_mass_action(&model))
102            .extend_scalars(|param| param.map_variables(latex_mor_names_mass_action(&model)))
103            .to_latex_equations();
104
105        let expected = vec![
106            LatexEquation {
107                lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{xxx}".to_string(),
108                rhs: "(-\\kappa_{\\text{fff}}) \\text{xxx} \\text{yyy}".to_string(),
109            },
110            LatexEquation {
111                lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{yyy}".to_string(),
112                rhs: "(\\rho_{\\text{fff}}) \\text{xxx} \\text{yyy}".to_string(),
113            },
114        ];
115        assert_eq!(equations, expected);
116    }
117
118    #[test]
119    fn unnamed_mor_uses_dom_cod_in_equations() {
120        let model = backward_link("xxx", "yyy", "");
121        let tab_model = model.discrete_tab().unwrap();
122        let analysis = ode::StockFlowMassActionAnalysis::default();
123        let sys = analysis.build_system(
124            tab_model,
125            ode::MassConservationType::Unbalanced(ode::RateGranularity::PerTransition),
126        );
127        let equations = sys
128            .map_variables(latex_ob_names_mass_action(&model))
129            .extend_scalars(|param| param.map_variables(latex_mor_names_mass_action(&model)))
130            .to_latex_equations();
131
132        let expected = vec![
133            LatexEquation {
134                lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{xxx}".to_string(),
135                rhs: "(-\\kappa_{\\text{xxx} \\to \\text{yyy}}) \\text{xxx} \\text{yyy}"
136                    .to_string(),
137            },
138            LatexEquation {
139                lhs: "\\frac{\\mathrm{d}}{\\mathrm{d}t} \\text{yyy}".to_string(),
140                rhs: "(\\rho_{\\text{xxx} \\to \\text{yyy}}) \\text{xxx} \\text{yyy}".to_string(),
141            },
142        ];
143        assert_eq!(equations, expected);
144    }
145
146    #[test]
147    fn modal_mor_dom_cod_labels() {
148        let th = Rc::new(theories::th_sym_monoidal_category());
149        let ob_type = ModalObType::new(QualifiedName::from("Object"));
150        let op = QualifiedName::from("tensor");
151
152        let [s_id, i_id, r_id] = [Uuid::now_v7(), Uuid::now_v7(), Uuid::now_v7()];
153        let [infect_id, recover_id] = [Uuid::now_v7(), Uuid::now_v7()];
154
155        let mut inner = ModalDblModel::new(th);
156        inner.add_ob(s_id.into(), ob_type.clone());
157        inner.add_ob(i_id.into(), ob_type.clone());
158        inner.add_ob(r_id.into(), ob_type.clone());
159
160        // infect: tensor(S, I) -> tensor(I, I) — product-typed dom and cod.
161        inner.add_mor(
162            infect_id.into(),
163            ModalOb::App(
164                ModalOb::List(
165                    List::Symmetric,
166                    vec![ModalOb::Generator(s_id.into()), ModalOb::Generator(i_id.into())],
167                )
168                .into(),
169                op.clone(),
170            ),
171            ModalOb::App(
172                ModalOb::List(
173                    List::Symmetric,
174                    vec![ModalOb::Generator(i_id.into()), ModalOb::Generator(i_id.into())],
175                )
176                .into(),
177                op.clone(),
178            ),
179            ModalMorType::Zero(ob_type.clone()),
180        );
181
182        // recover: I -> R — simple generator dom and cod.
183        inner.add_mor(
184            recover_id.into(),
185            ModalOb::Generator(i_id.into()),
186            ModalOb::Generator(r_id.into()),
187            ModalMorType::Zero(ob_type),
188        );
189
190        let mut ob_namespace = Namespace::new_for_uuid();
191        ob_namespace.set_label(s_id, LabelSegment::Text("S".into()));
192        ob_namespace.set_label(i_id, LabelSegment::Text("I".into()));
193        ob_namespace.set_label(r_id, LabelSegment::Text("R".into()));
194
195        let model = DblModel {
196            model: inner.into(),
197            ty: None,
198            ob_namespace,
199            mor_namespace: Namespace::new_for_uuid(),
200        };
201
202        // Morphism with basic generator dom/cod resolves labels.
203        assert_eq!(
204            model.mor_generator_dom_cod_label_strings(&recover_id.into()),
205            Some(("I".to_string(), "R".to_string()))
206        );
207
208        // Morphism with product-typed dom/cod resolves to bracketed labels.
209        assert_eq!(
210            model.mor_generator_dom_cod_label_strings(&infect_id.into()),
211            Some(("[S, I]".to_string(), "[I, I]".to_string()))
212        );
213    }
214}