diff --git a/packages/catlog-wasm/src/theories.rs b/packages/catlog-wasm/src/theories.rs index e55a19346..066bc4221 100644 --- a/packages/catlog-wasm/src/theories.rs +++ b/packages/catlog-wasm/src/theories.rs @@ -304,6 +304,39 @@ impl ThCategoryLinks { } } +/// The theory of categories with signed links. +#[wasm_bindgen] +pub struct ThCategorySignedLinks(Rc); + +#[wasm_bindgen] +impl ThCategorySignedLinks { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self(Rc::new(theories::th_category_signed_links())) + } + + #[wasm_bindgen] + pub fn theory(&self) -> DblTheory { + DblTheory(self.0.clone().into()) + } + + /// Simulates the mass-action ODE system derived from a model. + #[wasm_bindgen(js_name = "massAction")] + pub fn mass_action( + &self, + model: &DblModel, + data: analyses::ode::MassActionProblemData, + ) -> Result { + Ok(ODEResult( + analyses::ode::StockFlowMassActionAnalysis::default() + .build_numerical_system(model.discrete_tab()?, data) + .solve_with_defaults() + .map_err(|err| format!("{err:?}")) + .into(), + )) + } +} + /// The theory of strict symmetric monoidal categories. #[wasm_bindgen] pub struct ThSymMonoidalCategory(Rc); diff --git a/packages/catlog/src/stdlib/analyses/ode/mass_action.rs b/packages/catlog/src/stdlib/analyses/ode/mass_action.rs index 9cecd25a3..b609c20b5 100644 --- a/packages/catlog/src/stdlib/analyses/ode/mass_action.rs +++ b/packages/catlog/src/stdlib/analyses/ode/mass_action.rs @@ -82,7 +82,7 @@ impl StochasticMassActionAnalysis { } /// Symbolic parameter in mass-action polynomial system. -type Parameter = Polynomial; +type Parameter = Polynomial; /// Mass-action ODE analysis for Petri nets. /// @@ -110,7 +110,7 @@ impl PetriNetMassActionAnalysis { pub fn build_system( &self, model: &ModalDblModel, - ) -> PolynomialSystem, u8> { + ) -> PolynomialSystem, i8> { let mut sys = PolynomialSystem::new(); for ob in model.ob_generators_with_type(&self.place_ob_type) { sys.add_term(ob, Polynomial::zero()); @@ -146,7 +146,7 @@ impl PetriNetMassActionAnalysis { &self, model: &ModalDblModel, data: MassActionProblemData, - ) -> ODEAnalysis> { + ) -> ODEAnalysis> { into_numerical_system(self.build_system(model), data) } @@ -215,10 +215,12 @@ impl PetriNetMassActionAnalysis { pub struct StockFlowMassActionAnalysis { /// Object type for stocks. pub stock_ob_type: TabObType, - /// Morphism types for flows between stocks. + /// Morphism type for flows between stocks. pub flow_mor_type: TabMorType, - /// Morphism types for links for stocks to flows. - pub link_mor_type: TabMorType, + /// Morphism type for positive links from stocks to flows. + pub pos_link_mor_type: TabMorType, + /// Morphism type for negative links from stocks to flows. + pub neg_link_mor_type: TabMorType, } impl Default for StockFlowMassActionAnalysis { @@ -228,7 +230,8 @@ impl Default for StockFlowMassActionAnalysis { Self { stock_ob_type, flow_mor_type, - link_mor_type: TabMorType::Basic(name("Link")), + pos_link_mor_type: TabMorType::Basic(name("Link")), + neg_link_mor_type: TabMorType::Basic(name("NegativeLink")), } } } @@ -238,8 +241,8 @@ impl StockFlowMassActionAnalysis { pub fn build_system( &self, model: &DiscreteTabModel, - ) -> PolynomialSystem, u8> { - let mut terms: HashMap> = model + ) -> PolynomialSystem, i8> { + let mut terms: HashMap> = model .mor_generators_with_type(&self.flow_mor_type) .map(|flow| { let dom = model.mor_generator_dom(&flow).unwrap_basic(); @@ -247,17 +250,25 @@ impl StockFlowMassActionAnalysis { }) .collect(); - for link in model.mor_generators_with_type(&self.link_mor_type) { + let mut multiply_for_link = |link: QualifiedName, exponent: i8| { let dom = model.mor_generator_dom(&link).unwrap_basic(); let path = model.mor_generator_cod(&link).unwrap_tabulated(); let Some(TabEdge::Basic(cod)) = path.only() else { panic!("Codomain of link should be basic morphism"); }; if let Some(term) = terms.get_mut(&cod) { - *term = std::mem::take(term) * Monomial::generator(dom); + let mon: Monomial<_, i8> = [(dom, exponent)].into_iter().collect(); + *term = std::mem::take(term) * mon; } else { panic!("Codomain of link does not belong to model"); }; + }; + + for link in model.mor_generators_with_type(&self.pos_link_mor_type) { + multiply_for_link(link, 1); + } + for link in model.mor_generators_with_type(&self.neg_link_mor_type) { + multiply_for_link(link, -1); } let terms: Vec<_> = terms @@ -289,15 +300,15 @@ impl StockFlowMassActionAnalysis { &self, model: &DiscreteTabModel, data: MassActionProblemData, - ) -> ODEAnalysis> { + ) -> ODEAnalysis> { into_numerical_system(self.build_system(model), data) } } fn into_numerical_system( - sys: PolynomialSystem, u8>, + sys: PolynomialSystem, i8>, data: MassActionProblemData, -) -> ODEAnalysis> { +) -> ODEAnalysis> { let ob_index: IndexMap<_, _> = sys.components.keys().cloned().enumerate().map(|(i, x)| (x, i)).collect(); let n = ob_index.len(); @@ -335,6 +346,30 @@ mod tests { expected.assert_eq(&sys.to_string()); } + #[test] + fn positive_backward_link_dynamics() { + let th = Rc::new(th_category_signed_links()); + let model = positive_backward_link(th); + let sys = StockFlowMassActionAnalysis::default().build_system(&model); + let expected = expect!([r#" + dx = ((-1) f) x y + dy = f x y + "#]); + expected.assert_eq(&sys.to_string()); + } + + #[test] + fn negative_backward_link_dynamics() { + let th = Rc::new(th_category_signed_links()); + let model = negative_backward_link(th); + let sys = StockFlowMassActionAnalysis::default().build_system(&model); + let expected = expect!([r#" + dx = ((-1) f) x y^{-1} + dy = f x y^{-1} + "#]); + expected.assert_eq(&sys.to_string()); + } + #[test] fn catalysis_dynamics() { let th = Rc::new(th_sym_monoidal_category()); diff --git a/packages/catlog/src/stdlib/models.rs b/packages/catlog/src/stdlib/models.rs index 927a5b891..245633f1b 100644 --- a/packages/catlog/src/stdlib/models.rs +++ b/packages/catlog/src/stdlib/models.rs @@ -89,22 +89,38 @@ pub fn walking_attr(th: Rc) -> DiscreteDblModel { /// morphism back to the morphism itself. /// /// In system dynamics jargon, a backward link defines a "reinforcing loop," -/// assuming the link has a positive effect on the flow. An example is an infection -/// flow in a model of an infectious disease, where increasing the number of -/// infectives increases the rate of infection of the remaining susceptibles (other -/// things equal). +/// assuming the link has a positive effect on the flow. An example is an +/// infection flow an infectious disease model, where increasing the number of +/// infectives increases the rate of infection of the remaining susceptibles +/// (other things equal). pub fn backward_link(th: Rc) -> DiscreteTabModel { + backward_link_of_type(th, TabMorType::Basic(name("Link"))) +} + +/// The "walking" backward positive link. +/// +/// This is the free category with signed links that has a positive link from +/// the codomain of a morphism back to the morphism itself. +pub fn positive_backward_link(th: Rc) -> DiscreteTabModel { + // The type for positive links is just `Link`. + backward_link_of_type(th, TabMorType::Basic(name("Link"))) +} + +/// The "walking" backward negative link. +/// +/// This is the free category with signed links that has a negative link from +/// the codomain of a morphism back to the morphism itself. +pub fn negative_backward_link(th: Rc) -> DiscreteTabModel { + backward_link_of_type(th, TabMorType::Basic(name("NegativeLink"))) +} + +fn backward_link_of_type(th: Rc, link_type: TabMorType) -> DiscreteTabModel { let ob_type = TabObType::Basic(name("Object")); let mut model = DiscreteTabModel::new(th.clone()); model.add_ob(name("x"), ob_type.clone()); model.add_ob(name("y"), ob_type.clone()); model.add_mor(name("f"), name("x").into(), name("y").into(), th.hom_type(ob_type)); - model.add_mor( - name("link"), - name("y").into(), - model.tabulated_gen(name("f")), - TabMorType::Basic(name("Link")), - ); + model.add_mor(name("link"), name("y").into(), model.tabulated_gen(name("f")), link_type); model } @@ -192,6 +208,13 @@ mod tests { assert!(backward_link(th).validate().is_ok()); } + #[test] + fn categories_with_signed_links() { + let th = Rc::new(th_category_signed_links()); + assert!(positive_backward_link(th.clone()).validate().is_ok()); + assert!(negative_backward_link(th.clone()).validate().is_ok()); + } + #[test] fn sym_monoidal_categories() { let th = Rc::new(th_sym_monoidal_category()); diff --git a/packages/catlog/src/stdlib/theories.rs b/packages/catlog/src/stdlib/theories.rs index 973fa3ee4..732f43be8 100644 --- a/packages/catlog/src/stdlib/theories.rs +++ b/packages/catlog/src/stdlib/theories.rs @@ -119,6 +119,23 @@ pub fn th_category_links() -> DiscreteTabTheory { th } +/// The theory of categories with signed links. +/// +/// It can be useful to consider a version of stock and flow diagrams where the +/// links are labelled with a sign: positive or negative +pub fn th_category_signed_links() -> DiscreteTabTheory { + let mut th = DiscreteTabTheory::new(); + th.add_ob_type(name("Object")); + let ob_type = TabObType::Basic(name("Object")); + th.add_mor_type(name("Link"), ob_type.clone(), th.tabulator(th.hom_type(ob_type.clone()))); + th.add_mor_type( + name("NegativeLink"), + ob_type.clone(), + th.tabulator(th.hom_type(ob_type.clone())), + ); + th +} + /// The theory of strict monoidal categories. pub fn th_monoidal_category() -> ModalDblTheory { th_list_algebra(List::Plain) diff --git a/packages/catlog/src/zero/rig.rs b/packages/catlog/src/zero/rig.rs index c289b496a..6721a2c95 100644 --- a/packages/catlog/src/zero/rig.rs +++ b/packages/catlog/src/zero/rig.rs @@ -633,5 +633,8 @@ mod tests { let monomial: Monomial<_, u32> = [('x', 1), ('y', 0), ('x', 2)].into_iter().collect(); assert_eq!(monomial.normalize().to_string(), "x^3"); + + let monomial: Monomial<_, i32> = [('x', -1), ('y', -2), ('x', 2)].into_iter().collect(); + assert_eq!(monomial.normalize().to_string(), "x y^{-2}"); } } diff --git a/packages/frontend/src/stdlib/theories.ts b/packages/frontend/src/stdlib/theories.ts index a83ce0fde..77e7ad0fb 100644 --- a/packages/frontend/src/stdlib/theories.ts +++ b/packages/frontend/src/stdlib/theories.ts @@ -85,13 +85,24 @@ stdTheories.add( { id: "primitive-stock-flow", name: "Stock and flow", - description: "Model accumulation (stocks) and change (flows)", - iconLetters: ["S", "f"], + description: "Accumulation (stocks) and change (flows)", + iconLetters: ["S", "F"], group: "System Dynamics", }, async () => (await import("./theories/primitive-stock-flow")).default, ); +stdTheories.add( + { + id: "primitive-signed-stock-flow", + name: "Stock and flow with signed links", + description: "Accumulation (stocks) and change (flows), with signed links", + iconLetters: ["S", "F"], + group: "System Dynamics", + }, + async () => (await import("./theories/primitive-signed-stock-flow")).default, +); + stdTheories.add( { id: "reg-net", diff --git a/packages/frontend/src/stdlib/theories/primitive-signed-stock-flow.ts b/packages/frontend/src/stdlib/theories/primitive-signed-stock-flow.ts new file mode 100644 index 000000000..8d002694d --- /dev/null +++ b/packages/frontend/src/stdlib/theories/primitive-signed-stock-flow.ts @@ -0,0 +1,72 @@ +import { ThCategorySignedLinks } from "catlog-wasm"; +import { Theory, type TheoryMeta } from "../../theory"; +import * as analyses from "../analyses"; +import styles from "../styles.module.css"; +import svgStyles from "../svg_styles.module.css"; + +export default function createPrimitiveSignedStockFlowTheory(theoryMeta: TheoryMeta): Theory { + const thCategorySignedLinks = new ThCategorySignedLinks(); + + return new Theory({ + ...theoryMeta, + theory: thCategorySignedLinks.theory(), + onlyFreeModels: true, + modelTypes: [ + { + tag: "ObType", + obType: { tag: "Basic", content: "Object" }, + name: "Stock", + description: "Thing with an amount", + shortcut: ["S"], + cssClasses: [styles.box], + svgClasses: [svgStyles.box], + }, + { + tag: "MorType", + morType: { + tag: "Hom", + content: { tag: "Basic", content: "Object" }, + }, + name: "Flow", + description: "Flow from one stock to another", + shortcut: ["F"], + arrowStyle: "double", + }, + { + tag: "MorType", + morType: { tag: "Basic", content: "Link" }, + name: "Positive link", + description: "Positive influence of a stock on a flow", + arrowStyle: "plus", + preferUnnamed: true, + shortcut: ["P"], + }, + { + tag: "MorType", + morType: { tag: "Basic", content: "NegativeLink" }, + name: "Negative link", + description: "Negative influence of a stock on a flow", + arrowStyle: "minus", + preferUnnamed: true, + shortcut: ["N"], + }, + ], + modelAnalyses: [ + analyses.stockFlowDiagram({ + id: "diagram", + name: "Visualization", + description: "Visualize the stock and flow diagram", + help: "visualization", + }), + analyses.massAction({ + simulate(model, data) { + return thCategorySignedLinks.massAction(model, data); + }, + transitionType: { + tag: "Hom", + content: { tag: "Basic", content: "Object" }, + }, + }), + ], + }); +}