Skip to content
Draft
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
1 change: 1 addition & 0 deletions .rustfmt.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
array_width = 80
chain_width = 80
fn_call_width = 80
max_width = 100
19 changes: 19 additions & 0 deletions math-docs/trees/dct-000C.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
\title{Degree-delay signed categories}
\taxon{doctrine}
\import{macros}

\p{Degree-delay signed categories are categories graded in the monoid #{\mathbb{N} \times \mathbb{N} \times \mathbb{Z}_2}. The two copies of #{\mathbb{N}} label edges by the \em{differential degree} and \em{order of delay}. The order of the differential degree is meant to indicate which derivative of the target is affected (linearly) by the source. For example, an edge #{A \to B} with degree 2 indicates a contribution of the form #{\ddot{B} \mathrel{+}= kA} for some #{k \in \mathbb{R}}. The \em{order of delay} only has meaning with the assumption that the delays are modelled stochastically by Erlang distributions with the same time-scale parameter (see, for example, [here](https://en.wikipedia.org/wiki/Erlang_distribution)). The labelling in #{\mathbb{Z}_2} indicates the "sign" of the influence, as described in \ref{dct-0002}.}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps worth explaining that the reason you're talking about the Erlang distribution is that it's a sum of i.i.d. Gammas, where the number of summands is the "order of delay" in the ECLD.


\subtree{
\title{Degree-delay signed category}
\taxon{definition}

\p{A \define{degree-delay signed category} is a category sliced over the product
monoid #{\mathbb{N} \times \mathbb{N} \times \mathbb{Z}_2} with the monoidal product given by addition.}

}

\transclude{thy-000C}

\p{A free model of this double theory a causal loop diagram in which edges can
are additionally marked with two natural numbers indicating the \em{degree} and \em{delay} of the influence, called an \em{extended causal loop diagram}.}
1 change: 1 addition & 0 deletions math-docs/trees/mat-0001.tree
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ systems interpretation:}
\tr{\td{[[dct-0002]]} \td{Signed graphs} \td{Regulatory networks/causal loop diagrams}}
\tr{\td{[[dct-000B]]} \td{Signed graphs with delays} \td{Variant of causal loop diagram}}
\tr{\td{[[dct-0007]]} \td{Nullable signed graphs} \td{Variant of causal loop diagram}}
\tr{\td{[[dct-000C]]} \td{Delay-degree signed graphs} \td{Extension of causal loop diagram}}
\tr{\td{[[dct-0003]]} \td{Graphs with links} \td{Stock-flow diagrams}}
\tr{\td{Symmetric monoidal categories} \td{Petri nets} \td{Reaction networks}}
\tr{\td{[[dct-0004]]} \td{Sets} \td{Labels}}
Expand Down
7 changes: 7 additions & 0 deletions math-docs/trees/thy-000C.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
\title{Theory of degree-delay signed categories}
\taxon{theory}
\import{macros}

\p{The \define{theory of degree-delay signed categories} is the discrete double
theory with a single object and with proarrows given by the elements of the
monoid #{\mathbb{N} \times \mathbb{N} \times \mathbb{Z}_2}.}
17 changes: 17 additions & 0 deletions packages/catlog-wasm/src/theories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,23 @@ impl ThNullableSignedCategory {
}
}

/// The theory of degree delay signed categories (for ECLDs).
#[wasm_bindgen]
pub struct ThDegDelSignedCategory(Rc<theory::DiscreteDblTheory>);

#[wasm_bindgen]
impl ThDegDelSignedCategory {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self(Rc::new(theories::th_deg_del_signed_category()))
}

#[wasm_bindgen]
pub fn theory(&self) -> DblTheory {
DblTheory(self.0.clone().into())
}
}

/// The theory of categories with scalars.
#[wasm_bindgen]
pub struct ThCategoryWithScalars(Rc<theory::DiscreteDblTheory>);
Expand Down
292 changes: 292 additions & 0 deletions packages/catlog/src/stdlib/analyses/ecld/atomisations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
/*! Atomisations for extended causal loop diagrams (ECLDs)

ECLDs have arrows labelled by two natural numbers, for degree and delay. In the
intended linear ODE semantics, both of these behave additively under composition
of paths. It is useful to have a rewrite rule that "atomises" any arrow, i.e.
replacing an arrow X -> Y of degree n (which corresponds to the equation
(d/dt)^n(Y) += kX) by n-many arrows of degree 1, thus also introducing (n-1)-many
new objects X -> Y_{n-1} -> ... -> Y_2 -> Y_1 -> Y. The idea to keep in mind is
that a degree-n differential equation of the form (d/dt)^n(Y) = X can
equivalently be written as a system of degree-1 differential equations, namely
(d/dt)(Y) = Y_1, (d/dt)(Y_1) = Y_2, ..., (d/dt)(Y_{n-1}) = X. An analogous story
holds for order of delay, though this is formally dual: an arrow X -> Y of
order m corresponds to (something like) a morphism Y += k*E(m)*X, i.e. the
*source* is modified, not the target.

We call the objects Y_i the *formal derivatives* (resp. *formal delays*) of Y,
and the list [Y_0 = Y, Y_1, ..., Y_{n-1}] the *tower* over Y (resp. under Y),
and call Y the *base* of this tower; we call the length n of the list
[Y_0, ..., Y_{n-1}] the *height* of the tower.
*/

use crate::dbl::model::{DiscreteDblModel, FgDblModel, MutDblModel};
use crate::one::{category::FgCategory, Path};
use crate::stdlib::theories;
use crate::zero::{name, QualifiedName};
use std::{collections::HashMap, rc::Rc};

// Some helpful functions
fn deg_of_mor(model: &DiscreteDblModel, f: &QualifiedName) -> usize {
model.mor_generator_type(f).into_iter().filter(|t| *t == name("Degree")).count()
}

fn is_mor_pos(model: &DiscreteDblModel, f: &QualifiedName) -> bool {
// TO-DO: use th_deg_del_signed_category_to_signed_category()
0 == model
.mor_generator_type(f)
.into_iter()
.filter(|t| *t == name("Negative"))
.count()
% 2
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we ought to be doing such reasoning using general machinery rather than ad hoc calculations. However, the procedure that comes to mind, namely

  1. Project onto the monoid of signs
  2. Check whether the result is equal (in the e-graph) to the generator Negative

is almost certainly more code, so perhaps not worth the additional complexity.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Come to think of it, to enable the expected model migrations, we're going to have to define the needed theory map anyway, similar to this one, so maybe it is worth it after all.


/** Atomisiation of an ECLD by degree: replace every degree-n arrow by a path
* of n-many degree-1 arrows, going via (n-1)-many new objects; all the
* degree-0 arrows are kept unchanged. Returns the derivative towers to keep
* track of the relation between the formal derivatives and the original objects
*/
pub fn degree_atomisation(
model: DiscreteDblModel,
) -> (DiscreteDblModel, HashMap<QualifiedName, Vec<QualifiedName>>) {
let mut atomised_model: DiscreteDblModel =
DiscreteDblModel::new(Rc::new(theories::th_deg_del_signed_category()));

// height: the total height of the tower
// in_arrows_pos_deg: all incoming arrows of (strictly) positive degree
// in_arrows_zero_deg: all incoming arrows of zero degree
struct TowerConstructor {
height: usize,
in_arrows_pos_deg: Vec<QualifiedName>,
in_arrows_zero_deg: Vec<QualifiedName>,
}
let mut tower_constructors: HashMap<QualifiedName, TowerConstructor> = HashMap::new();

// Every tower will be of height at least 1, and will have at the very least
// an empty list of positive-degree (resp. zero-degree) incoming arrows
for x in model.ob_generators() {
tower_constructors.insert(
x,
TowerConstructor {
height: 1,
in_arrows_pos_deg: Vec::new(),
in_arrows_zero_deg: Vec::new(),
},
);
}

// -------------------------------------------------------------------------
// 1. For each base, calculate the maximum degree over all incoming
// arrows. Note that this is most easily done by actually iterating
// over the *morphisms* instead.

for f in model.mor_generators() {
let f_cod = model.mor_generator_cod(&f);
let f_degree = deg_of_mor(&model, &f);

if f_degree != 0 {
let tower_cons = tower_constructors.get_mut(&f_cod).unwrap();
let new_degree = std::cmp::max(tower_cons.height, f_degree);
tower_cons.height = new_degree;
tower_cons.in_arrows_pos_deg.push(f.clone());
} else {
let tower_cons = tower_constructors.get_mut(&f_cod).unwrap();
tower_cons.in_arrows_zero_deg.push(f.clone());
}
}

// -------------------------------------------------------------------------
// 2. Iterate over all unchecked bases, starting with (any one of) the
// one(s) with greatest current height. For each one, ensure that all
// the incoming arrows can be lifted so that their target is the
// highest floor of the tower, i.e. that the tower over Y has enough
// floors so that *every* arrow into Y can have the same target Y_max.
// But note that if we lift an arrow of degree n from X to Y then we
// need to know that X_n exists, i.e. we need to (potentially) add
// more floors to the *source* of every arrow whose target is Y.

// We have yet to build any of the towers so, right now, every base is
// "unchecked".
let mut unchecked_bases: Vec<QualifiedName> = model.ob_generators().collect::<Vec<_>>();

while !unchecked_bases.is_empty() {
// Since heights will change as we go, we start by resorting the list
unchecked_bases.sort_by(|x, y| {
let height = |base| tower_constructors.get(base).unwrap().height;
// Sort from smallest to largest so that we can pop from this stack
height(y).cmp(&height(x))
});

// Work on the base of (any one of) the tallest tower(s)
let target = unchecked_bases.pop().unwrap();

// Ensure that every incoming arrow can be lifted high enough in the
// tower over its source
let target_in_arrows = &tower_constructors.get(&target).unwrap().in_arrows_pos_deg.clone();
for f in target_in_arrows {
let source = model.mor_generator_dom(f);
let required_height =
tower_constructors.get(&target).unwrap().height - deg_of_mor(&model, f) + 1;
tower_constructors.entry(source.clone()).and_modify(|tower_cons| {
if tower_cons.height < required_height {
tower_cons.height = required_height
}
});
}
}

// -------------------------------------------------------------------------
// 3. Now that we know the required height of all the towers, we can
// actually build them: create the formal derivatives and the morphisms
// between them, resulting in Y_{n-1} -> ... -> Y_1 -> Y. Once we have
// these, we add them to our final model, resulting in a "discrete"
// copy of our original model, where each object has been extruded out
// to a tower of formal derivatives, but where there are no arrows
// between distinct towers.

// The hash map of towers will be useful when we later come to lifting all
// positive-degree arrows, so we build this at the same time as adding all
// these formal derivatives (and their morphisms) to the final model.
let mut towers: HashMap<QualifiedName, Vec<QualifiedName>> = HashMap::new();

for (base, tower_cons) in tower_constructors.iter_mut() {
// Firstly, add the base object itself
towers.insert(base.clone(), vec![base.clone()]);
atomised_model.add_ob(base.clone(), name("Object"));
// Then add all the formal derivatives Y_i, along with the morphisms
// Y_i -> Y_{i-1}
for i in 1..tower_cons.height {
let suffix = format!("_d[{}]", i);
let formal_der_i = base.clone().append(suffix.as_str().into());
atomised_model.add_ob(formal_der_i.clone(), name("Object"));
let formal_der_i_minus_1 = towers.get(base).unwrap().last().unwrap();
atomised_model.add_mor(
format!("d_({})^({})", base, i).as_str().into(),
formal_der_i.clone(),
formal_der_i_minus_1.clone(),
name("Degree").into(),
);
towers.get_mut(base).unwrap().push(formal_der_i);
}
}

// -------------------------------------------------------------------------
// 4. Finally, we add all the arrows from our original model into the new
// model:
// - we lift all positive-degree morphisms to have their new
// target be the top floor of the tower over their old target (i.e.
// a degree-n arrow X -> Y corresponds to a degree-1 arrow
// X -> Y_{n-1}, which should then be lifted to a (degree-1) arrow
// X_{h - (n-1)} -> Y_h, where h is the height of the tower over Y)
// - we simply copy over all the degree-zero morphisms.

for (base, tower) in towers.iter() {
let tower_cons = tower_constructors.get(base).unwrap();
for f in &tower_cons.in_arrows_pos_deg {
let deg = deg_of_mor(&model, f);
let source = model.mor_generator_dom(f);
let source_tower = towers.get(&source).unwrap();
// Note that we could alternatively take height to be the length of
// towers.get(source), which is equal by construction/definition
let height = tower_cons.height;
let new_source = &source_tower[height - deg];
let new_target = tower.last().unwrap();
if is_mor_pos(&model, f) {
atomised_model.add_mor(
f.clone(),
new_source.clone(),
new_target.clone(),
name("Degree").into(),
)
} else {
atomised_model.add_mor(
f.clone(),
new_source.clone(),
new_target.clone(),
Path::pair(name("Negative"), name("Degree")),
)
}
}

for f in &tower_cons.in_arrows_zero_deg {
atomised_model.add_mor(
f.clone(),
model.mor_generator_dom(f).clone(),
model.mor_generator_cod(f).clone(),
Path::Id(name("Object")),
);
}
}
(atomised_model, towers)
}

#[cfg(test)]
mod tests {
use super::degree_atomisation;

use crate::one::category::FgCategory;
use crate::stdlib::models::sample_ecld;
use crate::zero::{name, QualifiedName};
use std::collections::HashMap;

// Makes a hash map from objects in sample_ecld to tower heights
fn correct_heights() -> HashMap<QualifiedName, usize> {
let mut heights = HashMap::new();
heights.insert(name("a"), 1);
heights.insert(name("b"), 3);
heights.insert(name("c"), 3);
heights.insert(name("d"), 2);
heights
// TO-DO: make the following work
// [(name("a"), 1), (name("b"), 3), (name("c"), 3), (name("d"), 2)]
// .collect::<HashMap<QualifiedName, usize>>()
}

// Makes a hash map from morphisms in sample_ecld to the correct index of
// the domain in the atomised tower
fn correct_domain_indices() -> HashMap<QualifiedName, usize> {
let mut domains = HashMap::new();
domains.insert(name("f"), 0);
domains.insert(name("g"), 2);
domains.insert(name("l"), 0);
domains
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

}

#[test]
fn ecld_atomisation_test_tower_heights() {
let model = &sample_ecld();
let correct_heights = correct_heights();
let (atomised_model, towers) = degree_atomisation(model.clone());
for x in model.ob_generators() {
let height_at_x = towers.get(&x).unwrap().len();
let correct_height = correct_heights.get(&x).unwrap();
assert_eq!(
height_at_x, *correct_height,
"The tower over the object {} has the wrong height",
x
);
}
let correct_domain_indices = correct_domain_indices();
for f in correct_domain_indices.keys() {
let atomised_dom = atomised_model.mor_generator_dom(f);
let atomised_cod = atomised_model.mor_generator_cod(f);
let base_dom = model.mor_generator_dom(f);
let base_cod = model.mor_generator_cod(f);
let dom_index = *correct_domain_indices.get(f).unwrap();
let cod_index = *correct_heights.get(&base_cod).unwrap() - 1;
let correct_dom = &towers.get(&base_dom).unwrap()[dom_index].clone();
let correct_cod = &towers.get(&base_cod).unwrap()[cod_index].clone();
assert_eq!(
atomised_dom,
correct_dom.clone(),
"The morphism {} has the wrong domain",
*f
);
assert_eq!(
atomised_cod,
correct_cod.clone(),
"The morphism {} has the wrong codomain",
*f
);
}
}
}
4 changes: 4 additions & 0 deletions packages/catlog/src/stdlib/analyses/ecld/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Utilities for analyses of ECLDs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you planning to add more submodules here? If not, I'd suggest removing the extra directory level and having just stdlib/analyses/ecld.rs.

pub mod atomisations;
pub use atomisations::*;
1 change: 1 addition & 0 deletions packages/catlog/src/stdlib/analyses/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Various analyses that can be performed on models.

pub mod ecld;
#[cfg(feature = "ode")]
pub mod ode;
Loading