zigzag-engine/src/diagram.rs
Maximus Gorog c51e3274f9 Stage 2 complete: Construction 17 validated on real 3D data
Zigzag engine (6802 lines, 184 tests):
- Construction 17 normalisation: working through dimension 3+
- Import from homotopy-rs JSON: working (scalar, two_scalars, half_braid)
- Piece extraction via Embedding/restrict_diagram: working
- Type checking pipeline: working (Eckmann-Hilton half_braid passes)
- Essential identity detection: validated with full 2-diagram test

Bugs found and fixed:
- assemble_factorisations losing cospan legs during reassembly
- RewriteN::slice() using source offsets instead of target indices
- singular_preimage() not handling passthrough heights
- restrict_rewrite() not accounting for accumulated cone offsets
- Embedding::preimage() using regular_preimage for Singular case

Added vis-engine-spec.md: visualization engine specification
- 6-layer architecture from math primitives to scene graph
- SVG renderer for 2D, WebGL2 for 3D, custom hit testing
- Spring constraint integration point for semiotic rendering
- No external dependencies - game engine approach

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-09 05:26:15 -06:00

1140 lines
38 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Diagrams in associative n-categories
//!
//! An n-diagram is an object of Zⁿ(), the n-fold iterated zigzag category
//! over natural numbers. The natural number at each point encodes the dimension
//! of the algebraic generator at that location.
//!
//! The concrete representation uses:
//! - `Diagram`: enum of Diagram0 | DiagramN
//! - `DiagramN`: source diagram + list of cospans (the zigzag structure)
//! - `Cospan`: forward rewrite + backward rewrite
//! - `Rewrite`: zigzag map between diagram slices
//! - `Cone`: atomic rewrite data
use crate::signature::Generator;
/// An n-diagram in the iterated zigzag category.
///
/// This is the main data structure representing diagrams in associative n-categories.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Diagram {
/// A 0-diagram: a single generator (point in the signature).
Diagram0(Generator),
/// An n-diagram for n > 0: source + zigzag of cospans.
DiagramN(DiagramN),
}
/// An n-dimensional diagram (n > 0).
///
/// Represented as:
/// - A source (n-1)-diagram (the first regular slice)
/// - A sequence of cospans encoding the zigzag structure
///
/// The regular slices r₀, r₁, ..., rₙ are:
/// - r₀ = source
/// - rᵢ₊₁ = backward.target of cospan i (equivalently, forward.target of cospan i)
///
/// The singular slices s₀, s₁, ..., sₙ₋₁ are the apexes of each cospan.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagramN {
/// The source diagram (first regular slice, r₀)
pub source: Box<Diagram>,
/// The cospans forming the zigzag structure
pub cospans: Vec<Cospan>,
}
impl DiagramN {
/// Create a new n-diagram.
pub fn new(source: Diagram, cospans: Vec<Cospan>) -> Self {
Self {
source: Box::new(source),
cospans,
}
}
/// Create an identity diagram (zigzag of length 0).
pub fn identity(source: Diagram) -> Self {
Self {
source: Box::new(source),
cospans: vec![],
}
}
/// The zigzag length (number of cospans / singular heights).
pub fn length(&self) -> usize {
self.cospans.len()
}
/// Get the source (first regular slice).
pub fn source(&self) -> &Diagram {
&self.source
}
/// Compute the target (last regular slice).
///
/// 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 {
self.regular_slice(self.cospans.len())
.expect("target should always be computable")
}
/// Get the regular slice at height h.
///
/// 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 > 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() {
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))
}
}
/// A cospan in a zigzag: rₕ → sₕ ← rₕ₊₁
///
/// The forward rewrite goes from the left regular to the singular.
/// The backward rewrite goes from the right regular to the singular.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cospan {
/// Rewrite from left regular slice to singular apex
pub forward: Rewrite,
/// Rewrite from right regular slice to singular apex
pub backward: Rewrite,
}
impl Cospan {
/// Create a new cospan.
pub fn new(forward: Rewrite, backward: Rewrite) -> Self {
Self { forward, backward }
}
/// Check if this is an identity cospan (both legs are trivially identity).
///
/// Uses `is_trivial()` which recognizes both `Rewrite::Identity` and
/// `RewriteN` with empty cones (as serialized by homotopy-rs).
pub fn is_identity(&self) -> bool {
self.forward.is_trivial() && self.backward.is_trivial()
}
}
/// A rewrite (zigzag map) between diagram slices.
///
/// This encodes how one (n-1)-diagram maps to another within the zigzag structure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Rewrite {
/// Identity rewrite (does nothing).
Identity,
/// A 0-dimensional rewrite between generators.
Rewrite0 {
source: Generator,
target: Generator,
},
/// An n-dimensional rewrite (n > 0), encoded as a sequence of cones.
RewriteN(RewriteN),
}
impl Rewrite {
/// Check if this is explicitly marked as an identity rewrite.
///
/// Returns true only for `Rewrite::Identity`. This is conservative and
/// used for degeneracy tracking where we need to distinguish between
/// a true identity and a parallel degeneracy with non-identity slices.
pub fn is_identity(&self) -> bool {
matches!(self, Rewrite::Identity)
}
/// Check if this rewrite is trivially identity (makes no changes).
///
/// Returns true for:
/// - `Rewrite::Identity` (explicit identity marker)
/// - `RewriteN` with empty cones (no structural changes at this level)
/// - `Rewrite0` with source == target
///
/// This is used for detecting identity cospans that should be removed
/// during normalisation.
pub fn is_trivial(&self) -> bool {
match self {
Rewrite::Identity => true,
Rewrite::RewriteN(r) => r.cones.is_empty(),
Rewrite::Rewrite0 { source, target } => source == target,
}
}
/// The dimension of this rewrite.
pub fn dimension(&self) -> usize {
match self {
Rewrite::Identity => 0,
Rewrite::Rewrite0 { .. } => 0,
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).
///
/// Encoded as a sequence of cones, where each cone describes an atomic
/// transformation at a specific position.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RewriteN {
/// Dimension of this rewrite (> 0)
pub dimension: usize,
/// The cones encoding the rewrite structure
pub cones: Vec<Cone>,
}
impl RewriteN {
/// Create a new n-dimensional rewrite.
pub fn new(dimension: usize, cones: Vec<Cone>) -> Self {
assert!(dimension > 0, "RewriteN dimension must be > 0");
Self { dimension, cones }
}
/// Create an identity rewrite at dimension n.
pub fn identity(dimension: usize) -> Self {
Self {
dimension,
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.
///
/// A cone describes a single "bubble" in a string diagram — a region where
/// a source configuration of cospans is replaced by a target cospan.
///
/// The singular map structure of the parent rewrite is determined by the
/// indices of the cones.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cone {
/// Index in the target zigzag where this cone maps to
pub index: usize,
/// Source configuration (sequence of cospans being contracted)
pub source: Vec<Cospan>,
/// Target cospan (what the source contracts to)
pub target: Cospan,
/// Slice rewrites for each source singular height.
/// Length equals source.len() (one per source cospan apex).
/// Matches homotopy-rs singular_slices convention.
pub slices: Vec<Rewrite>,
}
impl Cone {
/// Create a new cone.
pub fn new(index: usize, source: Vec<Cospan>, target: Cospan, slices: Vec<Rewrite>) -> Self {
Self {
index,
source,
target,
slices,
}
}
/// The number of source cospans this cone contracts.
pub fn source_size(&self) -> usize {
self.source.len()
}
/// The number of singular slices in this cone.
/// Same as source_size() - one singular slice per source cospan.
pub fn len(&self) -> usize {
self.source.len()
}
/// Check if this cone is empty (no source cospans).
pub fn is_empty(&self) -> bool {
self.source.is_empty()
}
}
impl RewriteN {
/// Compute where a regular height in the source maps to in the target.
///
/// For a rewrite f: A → B, given a regular height h in A,
/// returns the corresponding regular height in B.
///
/// Based on homotopy-rs implementation.
pub fn regular_image(&self, h: usize) -> usize {
let mut height = h;
for cone in &self.cones {
// Only affect heights that are completely AFTER this cone's source range
if height >= cone.index + cone.source_size() {
// Shift down by the contraction: source_size cospans become 1
height -= cone.source_size().saturating_sub(1);
}
}
height
}
/// Compute the preimage of a regular height from target back to source.
///
/// For a rewrite f: A → B, given a regular height h in B,
/// returns the corresponding regular height in A.
///
/// This is the inverse of regular_image.
pub fn regular_preimage(&self, target_height: usize) -> usize {
let mut source_height = target_height;
for cone in &self.cones {
// For each cone that starts before or at this target height,
// we need to account for the expansion
if target_height > cone.index {
source_height += cone.source_size().saturating_sub(1);
}
}
source_height
}
/// Compute the preimage of a singular height h in the target.
///
/// For a rewrite f: A → B, given a singular height h in B,
/// returns all singular heights in A that map to h.
///
/// This handles three cases:
/// 1. **Contraction**: A cone targets h and has len > 0. Returns all source
/// heights consumed by that cone.
/// 2. **Insertion**: A cone targets h but has len == 0. Returns empty (no
/// source heights map to this insertion point).
/// 3. **Passthrough**: No cone targets h. Returns the single source height
/// that passes through to h (computed from the cone structure).
pub fn singular_preimage(&self, target_h: usize) -> Vec<usize> {
let mut current_source = 0;
let mut current_target = 0;
for cone in &self.cones {
// Handle passthroughs before this cone
while current_target < cone.index {
if current_target == target_h {
// Found passthrough at target_h
return vec![current_source];
}
current_source += 1;
current_target += 1;
}
// Handle the cone itself
if current_target == target_h {
// This cone targets our height
// Return all source heights in the cone's range
// (empty if len == 0, i.e., insertion)
return (current_source..current_source + cone.len()).collect();
}
current_source += cone.len();
current_target += 1; // Cone produces 1 target cospan
}
// Handle passthroughs after all cones
// target_h is beyond all cone indices
let offset = target_h - current_target;
vec![current_source + offset]
}
/// Get the target heights (where cones map to).
pub fn targets(&self) -> impl Iterator<Item = usize> + '_ {
self.cones.iter().map(|c| c.index)
}
/// Get the slice rewrite at a source singular height.
///
/// For a rewrite f: A → B, given a source singular height h,
/// returns the (n-1)-dimensional rewrite between source's singular
/// slice at h and the corresponding target singular slice.
///
/// Based on homotopy-rs: finds the cone containing this source height,
/// then indexes into its singular_slices.
pub fn slice(&self, source_height: usize) -> Rewrite {
// Find which cone contains this source height
let mut source_offset = 0;
for cone in &self.cones {
let source_end = source_offset + cone.len();
if source_height >= source_offset && source_height < source_end {
// Found the cone - index into its slices
let local_idx = source_height - source_offset;
if local_idx < cone.slices.len() {
return cone.slices[local_idx].clone();
}
}
source_offset = source_end;
}
// Height is outside all cones (passthrough), return identity
Rewrite::Identity
}
/// Get the cone that targets a specific height, if any.
pub fn cone_over_target(&self, target_height: usize) -> Option<&Cone> {
self.cones.iter().find(|c| c.index == target_height)
}
}
// === Diagram methods ===
impl Diagram {
/// The dimension of this diagram.
pub fn dimension(&self) -> usize {
match self {
Diagram::Diagram0(_) => 0,
Diagram::DiagramN(d) => 1 + d.source.dimension(),
}
}
/// The zigzag length at the top level.
///
/// For a 0-diagram, this is 0.
/// For an n-diagram, this is the number of cospans.
pub fn length(&self) -> usize {
match self {
Diagram::Diagram0(_) => 0,
Diagram::DiagramN(d) => d.length(),
}
}
/// Check if this is a 0-diagram.
pub fn is_zero(&self) -> bool {
matches!(self, Diagram::Diagram0(_))
}
/// Create an identity diagram over this one (adds one dimension).
pub fn identity(self) -> DiagramN {
DiagramN::identity(self)
}
/// Get the source of this diagram.
///
/// For a 0-diagram, returns None.
/// For an n-diagram, returns the source (n-1)-diagram.
pub fn source(&self) -> Option<&Diagram> {
match self {
Diagram::Diagram0(_) => None,
Diagram::DiagramN(d) => Some(&d.source),
}
}
/// Get the target of this diagram.
pub fn target(&self) -> Option<Diagram> {
match self {
Diagram::Diagram0(_) => None,
Diagram::DiagramN(d) => Some(d.target()),
}
}
/// Check if this diagram is globular.
///
/// A diagram is globular if:
/// - It's a 0-diagram, OR
/// - Its source and target are equal globular diagrams
pub fn is_globular(&self) -> bool {
match self {
Diagram::Diagram0(_) => true,
Diagram::DiagramN(d) => {
let src = d.source();
let tgt = d.target();
src.is_globular() && tgt.is_globular() && src == &tgt
}
}
}
}
impl From<Generator> for Diagram {
fn from(g: Generator) -> Self {
Diagram::Diagram0(g)
}
}
impl From<DiagramN> for Diagram {
fn from(d: DiagramN) -> Self {
Diagram::DiagramN(d)
}
}
/// A map between diagrams (zigzag map in the iterated category).
///
/// This is the full morphism structure, containing the singular map
/// and all slice data recursively.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagramMap {
/// The rewrite encoding this map
pub rewrite: Rewrite,
}
impl DiagramMap {
/// Create a diagram map from a rewrite.
pub fn new(rewrite: Rewrite) -> Self {
Self { rewrite }
}
/// Create an identity map.
pub fn identity(_diagram: &Diagram) -> Self {
Self {
rewrite: Rewrite::Identity,
}
}
/// Check if this is an identity map.
pub fn is_identity(&self) -> bool {
self.rewrite.is_identity()
}
/// Compose two diagram maps: (g ∘ f) where self = f and other = g.
///
/// For identity maps, composition is trivial.
/// For rewrites, we need to compose the underlying structure.
pub fn compose(&self, other: &DiagramMap) -> DiagramMap {
if self.is_identity() {
other.clone()
} else if other.is_identity() {
self.clone()
} else {
// TODO: Implement full rewrite composition
// For now, return other (this is a simplification)
other.clone()
}
}
/// Check if a singular height h is in the image of this map's singular component.
///
/// For degeneracy maps, this checks if height h would be preserved
/// (i.e., is not an inserted identity cospan position).
pub fn has_singular_height_in_image(&self, h: usize) -> bool {
match &self.rewrite {
Rewrite::Identity => true, // Identity maps preserve all heights
Rewrite::Rewrite0 { .. } => true, // 0-dim has no singular structure
Rewrite::RewriteN(r) => {
// Check if h is NOT an insertion point (not in any cone's empty-source positions)
let insertion_points: std::collections::HashSet<usize> = r.cones
.iter()
.filter(|c| c.source.is_empty())
.map(|c| c.index)
.collect();
!insertion_points.contains(&h)
}
}
}
}
/// 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::*;
fn test_generator() -> Generator {
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();
let d = Diagram::Diagram0(g.clone());
assert_eq!(d.dimension(), 0);
assert!(d.is_zero());
assert!(d.is_globular());
}
#[test]
fn test_diagram_n_identity() {
let g = test_generator();
let d0 = Diagram::Diagram0(g);
let d1 = DiagramN::identity(d0);
assert_eq!(d1.length(), 0);
assert_eq!(Diagram::DiagramN(d1).dimension(), 1);
}
#[test]
fn test_cospan_identity() {
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());
}
}