diff --git a/packages/catlog/src/dbl/model.rs b/packages/catlog/src/dbl/model.rs index 100c470c7..2ea9b118f 100644 --- a/packages/catlog/src/dbl/model.rs +++ b/packages/catlog/src/dbl/model.rs @@ -35,12 +35,12 @@ In addition, a model has the following operations: whose type is the composite of the corresponding morphism types. */ -use std::hash::Hash; +use std::hash::{BuildHasher, BuildHasherDefault, Hash, RandomState}; use std::iter::Iterator; use std::sync::Arc; use derivative::Derivative; -use ustr::Ustr; +use ustr::{IdentityHasher, Ustr}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -51,7 +51,9 @@ use super::theory::{DblTheory, DiscreteDblTheory}; use crate::one::fin_category::{FpCategory, InvalidFpCategory, UstrFinCategory}; use crate::one::*; use crate::validate::{self, Validate}; -use crate::zero::{Column, IndexedHashColumn, Mapping}; +use crate::zero::{Column, FinSet, HashColumn, HashFinSet, IndexedHashColumn, Mapping, Set}; + +use super::theory::*; /** A model of a double theory. @@ -93,7 +95,7 @@ pub trait DblModel: Category { /// Type of operations on morphisms defined in the theory. type MorOp: Eq; - /// The type of theories that Self could be models of + /// The type of double theory that this is a model of. type Theory: DblTheory< ObType = Self::ObType, MorType = Self::MorType, @@ -160,8 +162,8 @@ pub struct DiscreteDblModel { mor_types: IndexedHashColumn, } -/// A model of a discrete double theory where both the model and theory have -/// keys of type `Ustr`. +/// A model of a discrete double theory where both theoy and model have keys of +/// type `Ustr`. pub type UstrDiscreteDblModel = DiscreteDblModel; // NOTE: We are leaving a small optimization on the table by not using the // `IdentityHasher` but adding that extra type parameter quickly gets annoying @@ -367,10 +369,8 @@ where { type ObType = Cat::Ob; type MorType = Cat::Mor; - type ObOp = Cat::Ob; type MorOp = Cat::Mor; - type Theory = DiscreteDblTheory; fn theory(&self) -> &Self::Theory { @@ -384,15 +384,11 @@ where m } - fn ob_type(&self, x: &Self::Ob) -> Self::ObType { - self.ob_types.apply(x).expect("Object type should be set").clone() + fn ob_type(&self, ob: &Self::Ob) -> Self::ObType { + self.ob_gen_type(ob) } - - fn mor_type(&self, m: &Self::Mor) -> Self::MorType { - let types = m.clone().map( - |x| self.ob_type(&x), - |n| self.mor_types.apply(&n).expect("Morphism type should be set").clone(), - ); + fn mor_type(&self, mor: &Self::Mor) -> Self::MorType { + let types = mor.clone().map(|x| self.ob_gen_type(&x), |m| self.mor_gen_type(&m)); self.theory.compose_types(types) } } @@ -405,17 +401,15 @@ where Cat::Mor: Hash, { fn ob_gen_type(&self, ob: &Self::ObGen) -> Self::ObType { - self.ob_types.apply(ob).unwrap().clone() + self.ob_types.apply(ob).cloned().expect("Object should have type") } - fn mor_gen_type(&self, mor: &Self::MorGen) -> Self::MorType { - self.mor_types.apply(mor).unwrap().clone() + self.mor_types.apply(mor).cloned().expect("Morphism should have type") } fn object_generators_with_type(&self, typ: &Self::ObType) -> impl Iterator { self.ob_types.preimage(typ) } - fn morphism_generators_with_type( &self, typ: &Self::MorType, @@ -480,6 +474,310 @@ pub enum InvalidDiscreteDblModel { EqTgt(Id), } +/// Object in a model of a discrete tabulator theory. +#[derive(Clone, PartialEq, Eq)] +pub enum TabOb { + /// Basic or generating object. + Basic(V), + + /// A morphism viewed as an object of a tabulator. + Tabulated(Box>), +} + +impl From for TabOb { + fn from(value: V) -> Self { + TabOb::Basic(value) + } +} + +/** "Edge" in a model of a discrete tabulator theory. + +Morphisms of these two forms generate all the morphisms in the model. + */ +#[derive(Clone, PartialEq, Eq)] +pub enum TabEdge { + /// Basic morphism between any two objects. + Basic(E), + + /// Generating morphism between tabulated morphisms, a commuting square. + Square { + /// The domain, a tabulated morphism. + dom: Box>, + + /// The codomain, a tabulated morphism. + cod: Box>, + + /// Edge that acts by pre-composition onto codomain. + pre: Box>, + + /// Edge that acts by post-composition onto domain. + post: Box>, + }, +} + +impl From for TabEdge { + fn from(value: E) -> Self { + TabEdge::Basic(value) + } +} + +/// Morphism in a model of a discrete tabulator theory. +pub type TabMor = Path, TabEdge>; + +impl From for TabMor { + fn from(value: E) -> Self { + Path::single(value.into()) + } +} + +#[derive(Clone, Derivative)] +#[derivative(Default(bound = ""))] +#[derivative(PartialEq(bound = "V: Eq + Hash, E: Eq + Hash"))] +#[derivative(Eq(bound = "V: Eq + Hash, E: Eq + Hash"))] +struct DiscreteTabGenerators { + objects: HashFinSet, + morphisms: HashFinSet, + dom: HashColumn>, + cod: HashColumn>, +} + +impl Graph for DiscreteTabGenerators +where + V: Eq + Clone + Hash, + E: Eq + Clone + Hash, +{ + type V = TabOb; + type E = TabEdge; + + fn has_vertex(&self, ob: &Self::V) -> bool { + match ob { + TabOb::Basic(v) => self.objects.contains(v), + TabOb::Tabulated(p) => (*p).contained_in(self), + } + } + + fn has_edge(&self, edge: &Self::E) -> bool { + match edge { + TabEdge::Basic(e) => self.morphisms.contains(e), + TabEdge::Square { + dom, + cod, + pre, + post, + } => { + if !(dom.contained_in(self) && cod.contained_in(self)) { + return false; + } + let path1 = dom.clone().concat_in(self, Path::single(*post.clone())); + let path2 = Path::single(*pre.clone()).concat_in(self, *cod.clone()); + path1.is_some() && path2.is_some() && path1 == path2 + } + } + } + + fn src(&self, edge: &Self::E) -> Self::V { + match edge { + TabEdge::Basic(e) => { + self.dom.apply(e).cloned().expect("Domain of morphism should be defined") + } + TabEdge::Square { dom, .. } => TabOb::Tabulated(dom.clone()), + } + } + + fn tgt(&self, edge: &Self::E) -> Self::V { + match edge { + TabEdge::Basic(e) => { + self.cod.apply(e).cloned().expect("Codomain of morphism should be defined") + } + TabEdge::Square { cod, .. } => TabOb::Tabulated(cod.clone()), + } + } +} + +/** A finitely presented model of a discrete tabulator theory. + +A **model** of a [discrete tabulator theory](super::theory::DiscreteTabTheory) +is a normal lax functor from the theory into the double category of profunctors +that preserves tabulators. For the definition of "preserving tabulators," see +the dev docs. + */ +#[derive(Clone, Derivative)] +#[derivative(PartialEq(bound = "Id: Eq + Hash, ThId: Eq + Hash"))] +#[derivative(Eq(bound = "Id: Eq + Hash, ThId: Eq + Hash"))] +pub struct DiscreteTabModel { + #[derivative(PartialEq(compare_with = "Arc::ptr_eq"))] + theory: Arc>, + generators: DiscreteTabGenerators, + // TODO: Equations + ob_types: IndexedHashColumn>, + mor_types: IndexedHashColumn>, +} + +/// A model of a discrete tabulator theory where both theory and model have keys +/// of type `Ustr`. +pub type UstrDiscreteTabModel = DiscreteTabModel>; + +impl DiscreteTabModel +where + Id: Eq + Clone + Hash, + ThId: Eq + Clone + Hash, + S: BuildHasher, +{ + /// Creates an empty model of the given theory. + pub fn new(theory: Arc>) -> Self { + Self { + theory, + generators: Default::default(), + ob_types: Default::default(), + mor_types: Default::default(), + } + } + + /// Convenience method to turn a morphism into an object. + pub fn tabulated(&self, mor: TabMor) -> TabOb { + TabOb::Tabulated(Box::new(mor)) + } + + /// Convenience method to turn a morphism generator into an object. + pub fn tabulated_gen(&self, f: Id) -> TabOb { + self.tabulated(Path::single(TabEdge::Basic(f))) + } + + /// Adds a basic object to the model. + pub fn add_ob(&mut self, x: Id, typ: TabObType) -> bool { + self.ob_types.set(x.clone(), typ); + self.generators.objects.insert(x) + } + + /// Adds a basic morphism to the model. + pub fn add_mor( + &mut self, + f: Id, + dom: TabOb, + cod: TabOb, + typ: TabMorType, + ) -> bool { + self.mor_types.set(f.clone(), typ); + self.generators.dom.set(f.clone(), dom); + self.generators.cod.set(f.clone(), cod); + self.generators.morphisms.insert(f) + } +} + +impl Category for DiscreteTabModel +where + Id: Eq + Clone + Hash, +{ + type Ob = TabOb; + type Mor = TabMor; + + fn has_ob(&self, x: &Self::Ob) -> bool { + self.generators.has_vertex(x) + } + fn has_mor(&self, path: &Self::Mor) -> bool { + path.contained_in(&self.generators) + } + fn dom(&self, path: &Self::Mor) -> Self::Ob { + path.src(&self.generators) + } + fn cod(&self, path: &Self::Mor) -> Self::Ob { + path.tgt(&self.generators) + } + + fn compose(&self, path: Path) -> Self::Mor { + path.flatten_in(&self.generators).expect("Paths should be composable") + } +} + +impl FgCategory for DiscreteTabModel +where + Id: Eq + Clone + Hash, +{ + type ObGen = Id; + type MorGen = Id; + + fn object_generators(&self) -> impl Iterator { + self.generators.objects.iter() + } + fn morphism_generators(&self) -> impl Iterator { + self.generators.morphisms.iter() + } + + fn morphism_generator_dom(&self, f: &Self::MorGen) -> Self::Ob { + self.generators.dom.apply(f).cloned().expect("Domain should be defined") + } + fn morphism_generator_cod(&self, f: &Self::MorGen) -> Self::Ob { + self.generators.cod.apply(f).cloned().expect("Codomain should be defined") + } +} + +impl DblModel for DiscreteTabModel +where + Id: Eq + Clone + Hash, + ThId: Eq + Clone + Hash, +{ + type ObType = TabObType; + type MorType = TabMorType; + type ObOp = TabObOp; + type MorOp = TabMorOp; + type Theory = DiscreteTabTheory; + + fn theory(&self) -> &Self::Theory { + &self.theory + } + + fn ob_type(&self, ob: &Self::Ob) -> Self::ObType { + match ob { + TabOb::Basic(x) => self.ob_gen_type(x), + TabOb::Tabulated(m) => TabObType::Tabulator(Box::new(self.mor_type(m))), + } + } + + fn mor_type(&self, mor: &Self::Mor) -> Self::MorType { + let types = mor.clone().map( + |x| self.ob_type(&x), + |edge| match edge { + TabEdge::Basic(f) => self.mor_gen_type(&f), + TabEdge::Square { dom, .. } => { + let typ = self.mor_type(&dom); // == self.mor_type(&cod) + TabMorType::Hom(Box::new(TabObType::Tabulator(Box::new(typ)))) + } + }, + ); + self.theory.compose_types(types) + } + + fn ob_act(&self, ob: Self::Ob, op: &Self::ObOp) -> Self::Ob { + // Should we type check more rigorously here and in `mor_act`? + match (ob, op) { + (ob, TabObOp::Id(_)) => ob, + (TabOb::Tabulated(m), TabObOp::ProjSrc(_)) => self.dom(&m), + (TabOb::Tabulated(m), TabObOp::ProjTgt(_)) => self.cod(&m), + _ => panic!("Ill-typed application of object operation"), + } + } + + fn mor_act(&self, mor: Self::Mor, op: &Self::MorOp) -> Self::Mor { + match (mor, op) { + (mor, TabMorOp::Id(_)) => mor, + _ => panic!("Non-identity morphism operations not implemented"), + } + } +} + +impl FgDblModel for DiscreteTabModel +where + Id: Eq + Clone + Hash, + ThId: Eq + Clone + Hash, +{ + fn ob_gen_type(&self, ob: &Self::ObGen) -> Self::ObType { + self.ob_types.apply(ob).cloned().expect("Object should have type") + } + fn mor_gen_type(&self, mor: &Self::MorGen) -> Self::MorType { + self.mor_types.apply(mor).cloned().expect("Morphism should have type") + } +} + #[cfg(test)] mod tests { use ustr::ustr; diff --git a/packages/catlog/src/dbl/theory.rs b/packages/catlog/src/dbl/theory.rs index 8ba827c82..366372579 100644 --- a/packages/catlog/src/dbl/theory.rs +++ b/packages/catlog/src/dbl/theory.rs @@ -278,7 +278,7 @@ impl Validate for DiscreteDblTheory { } /// Object type in a discrete tabulator theory. -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, Hash)] pub enum TabObType { /// Basic or generating object type. Basic(V), @@ -288,7 +288,7 @@ pub enum TabObType { } /// Morphism type in a discrete tabulator theory. -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, Hash)] pub enum TabMorType { /// Basic or generating morphism type. Basic(E), @@ -336,8 +336,8 @@ identities and tabulator projections. #[derive(Clone, Derivative)] #[derivative(Default(bound = "S: Default"))] pub struct DiscreteTabTheory { - ob_types: HashFinSet, - mor_types: HashFinSet, + ob_types: HashFinSet, + mor_types: HashFinSet, src: HashColumn, S>, tgt: HashColumn, S>, compose_map: HashColumn<(E, E), TabMorType>, diff --git a/packages/catlog/src/one/category.rs b/packages/catlog/src/one/category.rs index 95604dd1e..90e7dc9da 100644 --- a/packages/catlog/src/one/category.rs +++ b/packages/catlog/src/one/category.rs @@ -161,7 +161,12 @@ impl Category for FreeCategory { } fn compose(&self, path: Path>) -> Path { - path.flatten() + path.flatten_in(&self.0).expect("Paths should be composable") + } + fn compose2(&self, path1: Path, path2: Path) -> Path { + path1 + .concat_in(&self.0, path2) + .expect("Target of first path should equal source of second path") } } @@ -329,6 +334,8 @@ mod tests { Path::pair(2, 3), Path::Id(4), ]); - assert_eq!(cat.compose(path), Path::Seq(nonempty![0, 1, 2, 3])); + let result = Path::Seq(nonempty![0, 1, 2, 3]); + assert_eq!(cat.compose(path), result); + assert_eq!(cat.compose2(Path::pair(0, 1), Path::pair(2, 3)), result); } } diff --git a/packages/catlog/src/one/fin_category.rs b/packages/catlog/src/one/fin_category.rs index 01e949864..8243324be 100644 --- a/packages/catlog/src/one/fin_category.rs +++ b/packages/catlog/src/one/fin_category.rs @@ -387,7 +387,12 @@ where } fn compose(&self, path: Path>) -> Path { - path.flatten() + path.flatten_in(&self.generators).expect("Paths should be composable") + } + fn compose2(&self, path1: Path, path2: Path) -> Path { + path1 + .concat_in(&self.generators, path2) + .expect("Target of first path should equal source of second path") } } diff --git a/packages/catlog/src/one/path.rs b/packages/catlog/src/one/path.rs index 3100d1220..2cb42445f 100644 --- a/packages/catlog/src/one/path.rs +++ b/packages/catlog/src/one/path.rs @@ -165,6 +165,34 @@ impl Path { } } + /** Concatenates this path with another path in the graph. + + This methods *checks* that the two paths are compatible (the target of this + path equals the source of the other path) and it *assumes* that both paths + are contained in the graph, which should be checked with + [`contained_in`](Self::contained_in) if in doubt. Thus, when returned, the + concatenated path is also a valid path. + */ + pub fn concat_in(self, graph: &G, other: Self) -> Option + where + V: Eq + Clone, + G: Graph, + { + if self.tgt(graph) != other.src(graph) { + return None; + } + let concatenated = match (self, other) { + (path, Path::Id(_)) => path, + (Path::Id(_), path) => path, + (Path::Seq(mut edges), Path::Seq(mut other_edges)) => { + edges.push(other_edges.head); + edges.append(&mut other_edges.tail); + Path::Seq(edges) + } + }; + Some(concatenated) + } + /// Is the path contained in the given graph? pub fn contained_in(&self, graph: &G) -> bool where @@ -174,16 +202,19 @@ impl Path { match self { Path::Id(v) => graph.has_vertex(v), Path::Seq(edges) => { - // All the edges are exist in the graph... + // All the edges exist in the graph... edges.iter().all(|e| graph.has_edge(e)) && - // ...and their sources and target are compatible. Too strict? + // ...and their sources and target are compatible. std::iter::zip(edges.iter(), edges.iter().skip(1)).all( |(e,f)| graph.tgt(e) == graph.src(f)) } } } - /// Returns whether or not there are repeated edges in the path. + /** Returns whether the path is simple. + + On our definition, a path is **simple** if it has no repeated edges. + */ pub fn is_simple(&self) -> bool where E: Eq + Hash, @@ -266,23 +297,51 @@ impl Path { } impl Path> { - /// Flatten a path of paths into a single path. + /** Flattens a path of paths into a single path. + + Unlike [`flatten_in`](Self::flatten_in), this method does not check that the + composite is well typed before computing it. + */ pub fn flatten(self) -> Path { match self { Path::Id(x) => Path::Id(x), - Path::Seq(fs) => { - if fs.iter().any(|p| matches!(p, Path::Seq(_))) { - let seqs = NonEmpty::collect(fs.into_iter().filter_map(|p| match p { - Path::Id(_) => None, - Path::Seq(gs) => Some(gs), - })); - Path::Seq(NonEmpty::flatten(seqs.unwrap())) + Path::Seq(paths) => { + if paths.iter().any(|p| matches!(p, Path::Seq(_))) { + // We either have at least one non-empty sequence... + let edges = paths + .into_iter() + .filter_map(|p| match p { + Path::Id(_) => None, + Path::Seq(edges) => Some(edges), + }) + .flatten(); + Path::Seq(NonEmpty::collect(edges).unwrap()) } else { - fs.head // An identity. + // ...or else every path is an identity. + paths.head } } } } + + /** Flattens a path of paths in a graph into a single path. + + Returns the flattened path just when the original paths have compatible + start and end points. + */ + pub fn flatten_in(self, graph: &G) -> Option> + where + V: Eq + Clone, + G: Graph, + { + if let Path::Seq(paths) = &self { + let mut pairs = std::iter::zip(paths.iter(), paths.iter().skip(1)); + if !pairs.all(|(p1, p2)| p1.tgt(graph) == p2.src(graph)) { + return None; + } + } + Some(self.flatten()) + } } /// A path in a graph with skeletal vertex and edge sets. @@ -387,14 +446,15 @@ mod tests { #[test] fn path_in_graph() { let g = SkelGraph::triangle(); + let path = Path::pair(0, 1); + assert_eq!(path.src(&g), 0); + assert_eq!(path.tgt(&g), 2); + assert_eq!(Path::single(0).concat_in(&g, Path::single(1)), Some(path)); + assert!(Path::Id(2).contained_in(&g)); assert!(!Path::Id(3).contained_in(&g)); assert!(Path::pair(0, 1).contained_in(&g)); assert!(!Path::pair(1, 0).contained_in(&g)); - - let path = Path::pair(0, 1); - assert_eq!(path.src(&g), 0); - assert_eq!(path.tgt(&g), 2); } #[test] diff --git a/packages/catlog/src/stdlib/models.rs b/packages/catlog/src/stdlib/models.rs index 38e1d9f7b..0ca240c2c 100644 --- a/packages/catlog/src/stdlib/models.rs +++ b/packages/catlog/src/stdlib/models.rs @@ -3,8 +3,7 @@ use std::sync::Arc; use ustr::ustr; -use crate::dbl::model::*; -use crate::dbl::theory::UstrDiscreteDblTheory; +use crate::dbl::{model::*, theory::*}; use crate::one::fin_category::FinMor; /** The positive self-loop. @@ -72,6 +71,33 @@ pub fn walking_attr(th: Arc) -> UstrDiscreteDblModel { model } +/** The "walking" backward link. + +The free category with links having a link from the codomain of a morphism back +to the morphism itself. + +In the 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). + */ +pub fn backward_link(th: Arc) -> UstrDiscreteTabModel { + let mut model = UstrDiscreteTabModel::new(th.clone()); + let (x, y, f) = (ustr("x"), ustr("y"), ustr("f")); + let ob_type = TabObType::Basic(ustr("Object")); + model.add_ob(x, ob_type.clone()); + model.add_ob(y, ob_type.clone()); + model.add_mor(f, TabOb::Basic(x), TabOb::Basic(y), th.hom_type(ob_type)); + model.add_mor( + ustr("link"), + TabOb::Basic(y), + model.tabulated_gen(f), + TabMorType::Basic(ustr("Link")), + ); + model +} + #[cfg(test)] mod tests { use super::super::theories::*; @@ -92,4 +118,11 @@ mod tests { let th = Arc::new(th_schema()); assert!(walking_attr(th).validate().is_ok()); } + + #[test] + fn categories_with_links() { + let th = Arc::new(th_category_links()); + // TODO: Implement validation for models of tabulator theories. + backward_link(th); + } }