1use 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#[derive(Serialize, Deserialize, Tsify)]
14#[tsify(into_wasm_abi, from_wasm_abi)]
15pub struct LatexEquations(pub Vec<LatexEquation>);
16
17pub(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
29pub(crate) fn latex_mor_names_mass_action(
34 model: &DblModel,
35) -> impl Fn(&ode::FlowParameter) -> String {
36 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 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 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 assert_eq!(
204 model.mor_generator_dom_cod_label_strings(&recover_id.into()),
205 Some(("I".to_string(), "R".to_string()))
206 );
207
208 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}