diff --git a/src/diagram.rs b/src/diagram.rs index 363d4af..11a9e01 100644 --- a/src/diagram.rs +++ b/src/diagram.rs @@ -74,40 +74,77 @@ impl DiagramN { /// /// For an identity (length 0), this is the same as source. /// Otherwise, we traverse the rewrites to find the final regular slice. + /// + /// The target is computed by starting with the source and applying + /// each cospan's rewrites in sequence: for each cospan, apply the + /// forward rewrite (to reach the apex), then apply the backward + /// rewrite in reverse (to reach the next regular slice). pub fn target(&self) -> Diagram { - // TODO: Implement proper slice computation through rewrites - // For now, return source for identity diagrams - if self.cospans.is_empty() { - (*self.source).clone() - } else { - // Placeholder: proper implementation requires traversing cospan structure - (*self.source).clone() - } + self.regular_slice(self.cospans.len()) + .expect("target should always be computable") } /// Get the regular slice at height h. /// - /// - h = 0: source - /// - h > 0: computed by applying rewrites + /// Regular slices r₀, r₁, ..., rₙ where n = number of cospans: + /// - r₀ = source + /// - rᵢ₊₁ is computed by traversing cospan i + /// + /// To traverse a cospan (forward: rᵢ → sᵢ, backward: rᵢ₊₁ → sᵢ): + /// 1. Apply forward rewrite to rᵢ to get sᵢ + /// 2. Apply backward rewrite in reverse to sᵢ to get rᵢ₊₁ pub fn regular_slice(&self, h: usize) -> Option { - if h == 0 { - Some((*self.source).clone()) - } else if h <= self.cospans.len() { - // TODO: Compute via rewrite application - None - } else { - None + if h > self.cospans.len() { + return None; } + + let mut slice = (*self.source).clone(); + + // Traverse cospans 0..h to reach regular slice h + for cospan in &self.cospans[..h] { + // Apply forward rewrite to get to the apex (singular slice) + slice = cospan.forward.apply_forward(&slice)?; + // Apply backward rewrite in reverse to get to the next regular slice + slice = cospan.backward.apply_backward(&slice)?; + } + + Some(slice) } /// Get the singular slice at height h. + /// + /// The singular slice sₕ is the apex of cospan h. + /// It is computed by: + /// 1. Getting regular slice h (rₕ) + /// 2. Applying the forward rewrite of cospan h pub fn singular_slice(&self, h: usize) -> Option { - if h < self.cospans.len() { - // TODO: Compute via cospan apex - None - } else { - None + if h >= self.cospans.len() { + return None; } + + // Get the regular slice at height h + let regular = self.regular_slice(h)?; + + // Apply the forward rewrite to get the apex + self.cospans[h].forward.apply_forward(®ular) + } + + /// Iterator over all slices (interleaved regular and singular). + /// + /// Returns slices in order: r₀, s₀, r₁, s₁, ..., sₙ₋₁, rₙ + /// Total of 2n + 1 slices for a diagram of length n. + pub fn slices(&self) -> Slices<'_> { + Slices::new(self) + } + + /// Iterator over regular slices only. + pub fn regular_slices(&self) -> impl Iterator + '_ { + (0..=self.cospans.len()).filter_map(|h| self.regular_slice(h)) + } + + /// Iterator over singular slices only. + pub fn singular_slices(&self) -> impl Iterator + '_ { + (0..self.cospans.len()).filter_map(|h| self.singular_slice(h)) } } @@ -165,6 +202,64 @@ impl Rewrite { Rewrite::RewriteN(r) => r.dimension, } } + + /// Apply this rewrite in the forward direction. + /// + /// Given a rewrite f: A → B and a diagram matching A, returns B. + /// For 0-dimensional rewrites, this replaces the generator. + /// For n-dimensional rewrites, this modifies the cospan structure. + pub fn apply_forward(&self, diagram: &Diagram) -> Option { + match (self, diagram) { + // Identity rewrite: return the diagram unchanged + (Rewrite::Identity, d) => Some(d.clone()), + + // 0-dimensional rewrite: source must match + (Rewrite::Rewrite0 { source, target }, Diagram::Diagram0(g)) => { + if g == source { + Some(Diagram::Diagram0(target.clone())) + } else { + // If source doesn't match, the rewrite doesn't apply + None + } + } + + // n-dimensional rewrite on an n-diagram + (Rewrite::RewriteN(r), Diagram::DiagramN(d)) => { + r.apply_forward(d).map(Diagram::DiagramN) + } + + // Dimension mismatch + _ => None, + } + } + + /// Apply this rewrite in the backward direction. + /// + /// Given a rewrite f: A → B and a diagram matching B, returns A. + /// This is the inverse direction of apply_forward. + pub fn apply_backward(&self, diagram: &Diagram) -> Option { + match (self, diagram) { + // Identity rewrite: return the diagram unchanged + (Rewrite::Identity, d) => Some(d.clone()), + + // 0-dimensional rewrite: target must match + (Rewrite::Rewrite0 { source, target }, Diagram::Diagram0(g)) => { + if g == target { + Some(Diagram::Diagram0(source.clone())) + } else { + None + } + } + + // n-dimensional rewrite on an n-diagram + (Rewrite::RewriteN(r), Diagram::DiagramN(d)) => { + r.apply_backward(d).map(Diagram::DiagramN) + } + + // Dimension mismatch + _ => None, + } + } } /// An n-dimensional rewrite (n > 0). @@ -193,6 +288,59 @@ impl RewriteN { cones: vec![], } } + + /// Apply this rewrite in the forward direction. + /// + /// A forward rewrite transforms the cospan structure by: + /// - For each cone, replacing source[cone.index..cone.index+cone.source.len()] + /// with the single target cospan + /// + /// The source of the diagram is unchanged; only cospans are modified. + pub fn apply_forward(&self, diagram: &DiagramN) -> Option { + let mut cospans = diagram.cospans.clone(); + let mut offset: isize = 0; + + for cone in &self.cones { + let start = (cone.index as isize + offset) as usize; + let end = start + cone.source.len(); + + // Verify the source cospans match + if cospans.get(start..end) != Some(&cone.source[..]) { + return None; + } + + // Replace source cospans with target cospan + cospans.splice(start..end, std::iter::once(cone.target.clone())); + + // Update offset: we removed cone.source.len() cospans and added 1 + offset -= cone.source.len() as isize - 1; + } + + Some(DiagramN::new((*diagram.source).clone(), cospans)) + } + + /// Apply this rewrite in the backward direction. + /// + /// A backward rewrite is the inverse: for each cone, we replace + /// the single target cospan with the source cospans. + pub fn apply_backward(&self, diagram: &DiagramN) -> Option { + let mut cospans = diagram.cospans.clone(); + + for cone in &self.cones { + let start = cone.index; + let end = start + 1; + + // Verify the target cospan matches + if cospans.get(start) != Some(&cone.target) { + return None; + } + + // Replace target cospan with source cospans + cospans.splice(start..end, cone.source.iter().cloned()); + } + + Some(DiagramN::new((*diagram.source).clone(), cospans)) + } } /// A cone: atomic rewrite data. @@ -340,6 +488,87 @@ impl DiagramMap { } } +/// Direction for slice iteration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SliceDirection { + Forward, + Backward, +} + +/// Iterator over all slices of a diagram (interleaved regular and singular). +/// +/// Returns slices in order: r₀, s₀, r₁, s₁, ..., sₙ₋₁, rₙ +pub struct Slices<'a> { + diagram: &'a DiagramN, + current: Option, + direction: SliceDirection, + cospan_index: usize, +} + +impl<'a> Slices<'a> { + fn new(diagram: &'a DiagramN) -> Self { + Self { + diagram, + current: Some((*diagram.source).clone()), + direction: SliceDirection::Forward, + cospan_index: 0, + } + } +} + +impl<'a> Iterator for Slices<'a> { + type Item = Diagram; + + fn next(&mut self) -> Option { + // If we've exhausted all cospans, return the final slice + if self.cospan_index >= self.diagram.cospans.len() { + return self.current.take(); + } + + let current = self.current.as_ref()?; + let cospan = &self.diagram.cospans[self.cospan_index]; + + let next = match self.direction { + SliceDirection::Forward => { + // Apply forward rewrite to get singular slice + self.direction = SliceDirection::Backward; + cospan.forward.apply_forward(current)? + } + SliceDirection::Backward => { + // Apply backward rewrite in reverse to get next regular slice + self.direction = SliceDirection::Forward; + self.cospan_index += 1; + cospan.backward.apply_backward(current)? + } + }; + + std::mem::replace(&mut self.current, Some(next)) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = if self.current.is_none() { + 0 + } else { + let cospans_left = self.diagram.cospans.len() - self.cospan_index; + let slices_from_cospans = cospans_left * 2; + let extra = match self.direction { + SliceDirection::Forward => 1, // Still need to emit current regular + traverse remaining + SliceDirection::Backward => 0, // Already emitted current, just need backward + remaining + }; + slices_from_cospans + extra + }; + (remaining, Some(remaining)) + } +} + +impl<'a> ExactSizeIterator for Slices<'a> { + fn len(&self) -> usize { + self.size_hint().0 + } +} + +impl<'a> std::iter::FusedIterator for Slices<'a> {} + #[cfg(test)] mod tests { use super::*; @@ -348,6 +577,14 @@ mod tests { Generator::new(0, 0, false) } + fn gen(id: usize) -> Generator { + Generator::new(id, 0, false) + } + + fn diagram0(id: usize) -> Diagram { + Diagram::Diagram0(gen(id)) + } + #[test] fn test_diagram_0() { let g = test_generator(); @@ -372,4 +609,340 @@ mod tests { let c = Cospan::new(Rewrite::Identity, Rewrite::Identity); assert!(c.is_identity()); } + + // ========== Slice computation tests ========== + + #[test] + fn test_identity_diagram_slices() { + // An identity diagram (length 0) has source = target + let g = test_generator(); + let d0 = Diagram::Diagram0(g.clone()); + let id = DiagramN::identity(d0.clone()); + + // Regular slice 0 is the source + assert_eq!(id.regular_slice(0), Some(d0.clone())); + + // Target should equal source for identity + assert_eq!(id.target(), d0); + + // No singular slices for identity diagram + assert_eq!(id.singular_slice(0), None); + + // Out of bounds + assert_eq!(id.regular_slice(1), None); + } + + #[test] + fn test_identity_rewrite_application() { + // Identity rewrite should not change the diagram + let d = diagram0(0); + assert_eq!(Rewrite::Identity.apply_forward(&d), Some(d.clone())); + assert_eq!(Rewrite::Identity.apply_backward(&d), Some(d.clone())); + } + + #[test] + fn test_rewrite0_application() { + let src = gen(0); + let tgt = gen(1); + let rewrite = Rewrite::Rewrite0 { + source: src.clone(), + target: tgt.clone(), + }; + + // Forward: source -> target + let d_src = Diagram::Diagram0(src.clone()); + let d_tgt = Diagram::Diagram0(tgt.clone()); + assert_eq!(rewrite.apply_forward(&d_src), Some(d_tgt.clone())); + + // Backward: target -> source + assert_eq!(rewrite.apply_backward(&d_tgt), Some(d_src.clone())); + + // Mismatched source should fail + let d_other = diagram0(2); + assert_eq!(rewrite.apply_forward(&d_other), None); + assert_eq!(rewrite.apply_backward(&d_other), None); + } + + #[test] + fn test_simple_cospan_slices() { + // A diagram with one cospan: A -> X <- B + // Where forward: A -> X and backward: B -> X + let a = gen(0); + let b = gen(1); + let x = gen(2); + + let forward = Rewrite::Rewrite0 { + source: a.clone(), + target: x.clone(), + }; + let backward = Rewrite::Rewrite0 { + source: b.clone(), + target: x.clone(), + }; + let cospan = Cospan::new(forward, backward); + + // Create the 1-diagram with source A + let source = Diagram::Diagram0(a.clone()); + let diag = DiagramN::new(source.clone(), vec![cospan]); + + // Regular slices + assert_eq!(diag.regular_slice(0), Some(Diagram::Diagram0(a.clone()))); + assert_eq!(diag.regular_slice(1), Some(Diagram::Diagram0(b.clone()))); + assert_eq!(diag.regular_slice(2), None); + + // Singular slices + assert_eq!(diag.singular_slice(0), Some(Diagram::Diagram0(x.clone()))); + assert_eq!(diag.singular_slice(1), None); + + // Target should be the last regular slice + assert_eq!(diag.target(), Diagram::Diagram0(b.clone())); + } + + #[test] + fn test_identity_cospan_slices() { + // A diagram with identity cospans: A -> A <- A (weak identity) + let a = gen(0); + + let cospan = Cospan::new(Rewrite::Identity, Rewrite::Identity); + + let source = Diagram::Diagram0(a.clone()); + let diag = DiagramN::new(source.clone(), vec![cospan]); + + // All slices should be A + assert_eq!(diag.regular_slice(0), Some(Diagram::Diagram0(a.clone()))); + assert_eq!(diag.regular_slice(1), Some(Diagram::Diagram0(a.clone()))); + assert_eq!(diag.singular_slice(0), Some(Diagram::Diagram0(a.clone()))); + assert_eq!(diag.target(), Diagram::Diagram0(a.clone())); + } + + #[test] + fn test_multiple_cospans() { + // A diagram with two cospans: A -> X <- B -> Y <- C + let a = gen(0); + let b = gen(1); + let c = gen(2); + let x = gen(3); + let y = gen(4); + + let cospan1 = Cospan::new( + Rewrite::Rewrite0 { source: a.clone(), target: x.clone() }, + Rewrite::Rewrite0 { source: b.clone(), target: x.clone() }, + ); + let cospan2 = Cospan::new( + Rewrite::Rewrite0 { source: b.clone(), target: y.clone() }, + Rewrite::Rewrite0 { source: c.clone(), target: y.clone() }, + ); + + let source = Diagram::Diagram0(a.clone()); + let diag = DiagramN::new(source, vec![cospan1, cospan2]); + + // Regular slices: A, B, C + assert_eq!(diag.regular_slice(0), Some(Diagram::Diagram0(a.clone()))); + assert_eq!(diag.regular_slice(1), Some(Diagram::Diagram0(b.clone()))); + assert_eq!(diag.regular_slice(2), Some(Diagram::Diagram0(c.clone()))); + assert_eq!(diag.regular_slice(3), None); + + // Singular slices: X, Y + assert_eq!(diag.singular_slice(0), Some(Diagram::Diagram0(x.clone()))); + assert_eq!(diag.singular_slice(1), Some(Diagram::Diagram0(y.clone()))); + assert_eq!(diag.singular_slice(2), None); + + // Target is C + assert_eq!(diag.target(), Diagram::Diagram0(c.clone())); + } + + #[test] + fn test_slices_iterator() { + // A diagram with one cospan: A -> X <- B + let a = gen(0); + let b = gen(1); + let x = gen(2); + + let cospan = Cospan::new( + Rewrite::Rewrite0 { source: a.clone(), target: x.clone() }, + Rewrite::Rewrite0 { source: b.clone(), target: x.clone() }, + ); + + let source = Diagram::Diagram0(a.clone()); + let diag = DiagramN::new(source, vec![cospan]); + + // slices() should yield: r0, s0, r1 = A, X, B + let slices: Vec<_> = diag.slices().collect(); + assert_eq!(slices.len(), 3); + assert_eq!(slices[0], Diagram::Diagram0(a.clone())); + assert_eq!(slices[1], Diagram::Diagram0(x.clone())); + assert_eq!(slices[2], Diagram::Diagram0(b.clone())); + } + + #[test] + fn test_slices_iterator_two_cospans() { + // A diagram with two cospans: A -> X <- B -> Y <- C + let a = gen(0); + let b = gen(1); + let c = gen(2); + let x = gen(3); + let y = gen(4); + + let cospan1 = Cospan::new( + Rewrite::Rewrite0 { source: a.clone(), target: x.clone() }, + Rewrite::Rewrite0 { source: b.clone(), target: x.clone() }, + ); + let cospan2 = Cospan::new( + Rewrite::Rewrite0 { source: b.clone(), target: y.clone() }, + Rewrite::Rewrite0 { source: c.clone(), target: y.clone() }, + ); + + let source = Diagram::Diagram0(a.clone()); + let diag = DiagramN::new(source, vec![cospan1, cospan2]); + + // slices() should yield: r0, s0, r1, s1, r2 = A, X, B, Y, C + let slices: Vec<_> = diag.slices().collect(); + assert_eq!(slices.len(), 5); + assert_eq!(slices[0], Diagram::Diagram0(a.clone())); + assert_eq!(slices[1], Diagram::Diagram0(x.clone())); + assert_eq!(slices[2], Diagram::Diagram0(b.clone())); + assert_eq!(slices[3], Diagram::Diagram0(y.clone())); + assert_eq!(slices[4], Diagram::Diagram0(c.clone())); + } + + #[test] + fn test_slices_iterator_identity() { + // Identity diagram has just one slice + let a = gen(0); + let source = Diagram::Diagram0(a.clone()); + let diag = DiagramN::identity(source); + + let slices: Vec<_> = diag.slices().collect(); + assert_eq!(slices.len(), 1); + assert_eq!(slices[0], Diagram::Diagram0(a.clone())); + } + + #[test] + fn test_regular_slices_iterator() { + // A diagram with two cospans: A -> X <- B -> Y <- C + let a = gen(0); + let b = gen(1); + let c = gen(2); + let x = gen(3); + let y = gen(4); + + let cospan1 = Cospan::new( + Rewrite::Rewrite0 { source: a.clone(), target: x.clone() }, + Rewrite::Rewrite0 { source: b.clone(), target: x.clone() }, + ); + let cospan2 = Cospan::new( + Rewrite::Rewrite0 { source: b.clone(), target: y.clone() }, + Rewrite::Rewrite0 { source: c.clone(), target: y.clone() }, + ); + + let source = Diagram::Diagram0(a.clone()); + let diag = DiagramN::new(source, vec![cospan1, cospan2]); + + // regular_slices() should yield: A, B, C + let regular: Vec<_> = diag.regular_slices().collect(); + assert_eq!(regular.len(), 3); + assert_eq!(regular[0], Diagram::Diagram0(a.clone())); + assert_eq!(regular[1], Diagram::Diagram0(b.clone())); + assert_eq!(regular[2], Diagram::Diagram0(c.clone())); + } + + #[test] + fn test_singular_slices_iterator() { + // A diagram with two cospans: A -> X <- B -> Y <- C + let a = gen(0); + let b = gen(1); + let c = gen(2); + let x = gen(3); + let y = gen(4); + + let cospan1 = Cospan::new( + Rewrite::Rewrite0 { source: a.clone(), target: x.clone() }, + Rewrite::Rewrite0 { source: b.clone(), target: x.clone() }, + ); + let cospan2 = Cospan::new( + Rewrite::Rewrite0 { source: b.clone(), target: y.clone() }, + Rewrite::Rewrite0 { source: c.clone(), target: y.clone() }, + ); + + let source = Diagram::Diagram0(a.clone()); + let diag = DiagramN::new(source, vec![cospan1, cospan2]); + + // singular_slices() should yield: X, Y + let singular: Vec<_> = diag.singular_slices().collect(); + assert_eq!(singular.len(), 2); + assert_eq!(singular[0], Diagram::Diagram0(x.clone())); + assert_eq!(singular[1], Diagram::Diagram0(y.clone())); + } + + #[test] + fn test_slices_iterator_len() { + let a = gen(0); + let b = gen(1); + let x = gen(2); + + let cospan = Cospan::new( + Rewrite::Rewrite0 { source: a.clone(), target: x.clone() }, + Rewrite::Rewrite0 { source: b.clone(), target: x.clone() }, + ); + + let source = Diagram::Diagram0(a.clone()); + let diag = DiagramN::new(source, vec![cospan]); + + let mut iter = diag.slices(); + assert_eq!(iter.len(), 3); + iter.next(); + assert_eq!(iter.len(), 2); + iter.next(); + assert_eq!(iter.len(), 1); + iter.next(); + assert_eq!(iter.len(), 0); + } + + #[test] + fn test_globular_identity() { + // An identity diagram over a point is globular + let g = test_generator(); + let d0 = Diagram::Diagram0(g); + let d1 = DiagramN::identity(d0.clone()); + + assert!(Diagram::DiagramN(d1).is_globular()); + } + + #[test] + fn test_globular_non_identity() { + // A non-identity diagram with source != target is not globular + let a = gen(0); + let b = gen(1); + let x = gen(2); + + let cospan = Cospan::new( + Rewrite::Rewrite0 { source: a.clone(), target: x.clone() }, + Rewrite::Rewrite0 { source: b.clone(), target: x.clone() }, + ); + + let source = Diagram::Diagram0(a.clone()); + let diag = DiagramN::new(source, vec![cospan]); + + // Source is A, target is B, so not globular + assert!(!Diagram::DiagramN(diag).is_globular()); + } + + #[test] + fn test_globular_loop() { + // A diagram with source = target is globular (a loop) + let a = gen(0); + let x = gen(1); + + // Cospan: A -> X <- A + let cospan = Cospan::new( + Rewrite::Rewrite0 { source: a.clone(), target: x.clone() }, + Rewrite::Rewrite0 { source: a.clone(), target: x.clone() }, + ); + + let source = Diagram::Diagram0(a.clone()); + let diag = DiagramN::new(source, vec![cospan]); + + // Source is A, target is A, so globular + assert!(Diagram::DiagramN(diag).is_globular()); + } }