Implement slice computation methods in diagram.rs

Add the core slice computation functionality for n-diagrams:

- Rewrite::apply_forward/apply_backward: Apply rewrites to transform
  diagrams in either direction
- RewriteN::apply_forward/apply_backward: Handle n-dimensional rewrites
  by modifying cospan structure via cone contraction/expansion
- DiagramN::regular_slice(h): Compute regular slice at height h by
  traversing cospans, applying forward then backward rewrites
- DiagramN::singular_slice(h): Compute singular slice (cospan apex)
  by applying forward rewrite to the corresponding regular slice
- DiagramN::target(): Now properly computes the last regular slice
- DiagramN::slices(): Iterator yielding all slices in order r0,s0,r1,...
- DiagramN::regular_slices()/singular_slices(): Filtered iterators

All 48 tests pass.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maximus Gorog 2026-04-07 02:56:31 -06:00
parent cd4b951f78
commit 951f86a74f

View file

@ -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<Diagram> {
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<Diagram> {
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(&regular)
}
/// 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<Item = Diagram> + '_ {
(0..=self.cospans.len()).filter_map(|h| self.regular_slice(h))
}
/// Iterator over singular slices only.
pub fn singular_slices(&self) -> impl Iterator<Item = Diagram> + '_ {
(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<Diagram> {
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<Diagram> {
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<DiagramN> {
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<DiagramN> {
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<Diagram>,
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<Self::Item> {
// 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<usize>) {
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());
}
}