Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions packages/catlog-wasm/src/theories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,39 @@ impl ThCategoryLinks {
}
}

/// The theory of categories with signed links.
#[wasm_bindgen]
pub struct ThCategorySignedLinks(Rc<theory::DiscreteTabTheory>);

#[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<ODEResult, String> {
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<theory::ModalDblTheory>);
Expand Down
63 changes: 49 additions & 14 deletions packages/catlog/src/stdlib/analyses/ode/mass_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ impl StochasticMassActionAnalysis {
}

/// Symbolic parameter in mass-action polynomial system.
type Parameter<Id> = Polynomial<Id, f32, u8>;
type Parameter<Id> = Polynomial<Id, f32, i8>;

/// Mass-action ODE analysis for Petri nets.
///
Expand Down Expand Up @@ -110,7 +110,7 @@ impl PetriNetMassActionAnalysis {
pub fn build_system(
&self,
model: &ModalDblModel,
) -> PolynomialSystem<QualifiedName, Parameter<QualifiedName>, u8> {
) -> PolynomialSystem<QualifiedName, Parameter<QualifiedName>, i8> {
let mut sys = PolynomialSystem::new();
for ob in model.ob_generators_with_type(&self.place_ob_type) {
sys.add_term(ob, Polynomial::zero());
Expand Down Expand Up @@ -146,7 +146,7 @@ impl PetriNetMassActionAnalysis {
&self,
model: &ModalDblModel,
data: MassActionProblemData,
) -> ODEAnalysis<NumericalPolynomialSystem<u8>> {
) -> ODEAnalysis<NumericalPolynomialSystem<i8>> {
into_numerical_system(self.build_system(model), data)
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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")),
}
}
}
Expand All @@ -238,26 +241,34 @@ impl StockFlowMassActionAnalysis {
pub fn build_system(
&self,
model: &DiscreteTabModel,
) -> PolynomialSystem<QualifiedName, Parameter<QualifiedName>, u8> {
let mut terms: HashMap<QualifiedName, Monomial<QualifiedName, u8>> = model
) -> PolynomialSystem<QualifiedName, Parameter<QualifiedName>, i8> {
let mut terms: HashMap<QualifiedName, Monomial<QualifiedName, i8>> = model
.mor_generators_with_type(&self.flow_mor_type)
.map(|flow| {
let dom = model.mor_generator_dom(&flow).unwrap_basic();
(flow, Monomial::generator(dom))
})
.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
Expand Down Expand Up @@ -289,15 +300,15 @@ impl StockFlowMassActionAnalysis {
&self,
model: &DiscreteTabModel,
data: MassActionProblemData,
) -> ODEAnalysis<NumericalPolynomialSystem<u8>> {
) -> ODEAnalysis<NumericalPolynomialSystem<i8>> {
into_numerical_system(self.build_system(model), data)
}
}

fn into_numerical_system(
sys: PolynomialSystem<QualifiedName, Parameter<QualifiedName>, u8>,
sys: PolynomialSystem<QualifiedName, Parameter<QualifiedName>, i8>,
data: MassActionProblemData,
) -> ODEAnalysis<NumericalPolynomialSystem<u8>> {
) -> ODEAnalysis<NumericalPolynomialSystem<i8>> {
let ob_index: IndexMap<_, _> =
sys.components.keys().cloned().enumerate().map(|(i, x)| (x, i)).collect();
let n = ob_index.len();
Expand Down Expand Up @@ -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());
Expand Down
43 changes: 33 additions & 10 deletions packages/catlog/src/stdlib/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,22 +89,38 @@ pub fn walking_attr(th: Rc<DiscreteDblTheory>) -> 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<DiscreteTabTheory>) -> 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<DiscreteTabTheory>) -> 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<DiscreteTabTheory>) -> DiscreteTabModel {
backward_link_of_type(th, TabMorType::Basic(name("NegativeLink")))
}

fn backward_link_of_type(th: Rc<DiscreteTabTheory>, 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
}

Expand Down Expand Up @@ -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());
Expand Down
17 changes: 17 additions & 0 deletions packages/catlog/src/stdlib/theories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions packages/catlog/src/zero/rig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
}
15 changes: 13 additions & 2 deletions packages/frontend/src/stdlib/theories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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" },
},
}),
],
});
}