-
Notifications
You must be signed in to change notification settings - Fork 40
Extended causal loop diagrams #664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
233f4e5
b8daa6e
b29a04a
2f4786b
c10dad6
f8337c6
e2dcd0b
c79757e
4253cf8
546c973
926881a
ed837db
8f5a051
2d56311
b55e799
aa091b0
26f15f9
c2a227a
fefa486
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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}.} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}.} | ||
| 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}.} |
| 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 | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
is almost certainly more code, so perhaps not worth the additional complexity.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
tim-at-topos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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 | ||
tim-at-topos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| //! Utilities for analyses of ECLDs | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| pub mod atomisations; | ||
| pub use atomisations::*; | ||
| 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; |
Uh oh!
There was an error while loading. Please reload this page.