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>
849 lines
30 KiB
Rust
849 lines
30 KiB
Rust
//! Normalisation algorithm (Construction 17)
|
|
//!
|
|
//! The normalisation algorithm computes the smallest element of Deg(T),
|
|
//! the poset of degeneracy subobjects of a diagram T. This removes all
|
|
//! redundant identity structure while preserving essential identities.
|
|
//!
|
|
//! Key insight: In dimension >= 4, some identity cospans are ESSENTIAL -
|
|
//! removing them would make zigzag maps ill-defined (no monotone function
|
|
//! of the required type exists). The algorithm detects and preserves these.
|
|
//!
|
|
//! # Algorithm Overview (Construction 17)
|
|
//!
|
|
//! Input: A sink S = (T, {fi: Ai -> T})
|
|
//! Output: Degeneracy d: N -> T and factorisations Ai -> N
|
|
//!
|
|
//! 1. Base case (dim 0): d = identity
|
|
//! 2. Recursive case:
|
|
//! a. Normalise at each regular height (recursive)
|
|
//! b. Normalise at each singular height (recursive, including cospan legs)
|
|
//! c. Assemble into zigzag P with parallel degeneracy dP
|
|
//! d. Remove trivial cospans not in image of any sink map
|
|
//! e. Compose: d = dP o dS
|
|
|
|
use crate::diagram::{Diagram, DiagramN, DiagramMap, Rewrite, Cospan, RewriteN, Cone};
|
|
|
|
/// Result of normalising a diagram (or sink).
|
|
#[derive(Debug, Clone)]
|
|
pub struct NormalisationResult {
|
|
/// The normalised diagram N
|
|
pub normal_form: Diagram,
|
|
/// The degeneracy map d: N -> T
|
|
pub degeneracy: DiagramMap,
|
|
/// Factorisations of each sink map through the degeneracy
|
|
pub factorisations: Vec<DiagramMap>,
|
|
}
|
|
|
|
/// A sink: a target diagram with maps from source diagrams.
|
|
///
|
|
/// Used for relative normalisation: find the smallest degeneracy
|
|
/// through which all sink maps factor.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Sink<'a> {
|
|
/// The target diagram T
|
|
pub target: &'a Diagram,
|
|
/// Maps from source diagrams to T
|
|
pub maps: Vec<DiagramMap>,
|
|
}
|
|
|
|
impl<'a> Sink<'a> {
|
|
/// Create a new sink.
|
|
pub fn new(target: &'a Diagram, maps: Vec<DiagramMap>) -> Self {
|
|
Self { target, maps }
|
|
}
|
|
|
|
/// Create an empty sink (for absolute normalisation).
|
|
pub fn empty(target: &'a Diagram) -> Self {
|
|
Self {
|
|
target,
|
|
maps: vec![],
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Proposition 19: Normalise a sink (Construction 17).
|
|
///
|
|
/// This is the core normalisation algorithm from the LICS 2022 paper.
|
|
/// Correctness: The output degeneracy d: N -> T is the smallest element
|
|
/// of Deg(T) through which all sink maps factor.
|
|
///
|
|
/// # Arguments
|
|
/// * `sink` - The sink to normalise (target diagram + incoming maps)
|
|
///
|
|
/// # Returns
|
|
/// A `NormalisationResult` containing:
|
|
/// - The normal form N
|
|
/// - The degeneracy d: N -> T
|
|
/// - Factorisations Ai -> N for each sink map
|
|
pub fn normalise_sink(sink: &Sink) -> NormalisationResult {
|
|
match sink.target {
|
|
Diagram::Diagram0(_) => {
|
|
// Base case: dimension 0
|
|
// The only degeneracy is the identity
|
|
NormalisationResult {
|
|
normal_form: sink.target.clone(),
|
|
degeneracy: DiagramMap::identity(sink.target),
|
|
factorisations: sink.maps.clone(),
|
|
}
|
|
}
|
|
Diagram::DiagramN(diagram_n) => {
|
|
normalise_sink_n(diagram_n, &sink.maps)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Construction 17: Normalise an n-dimensional diagram (n > 0).
|
|
///
|
|
/// Implements the full 5-step algorithm for dimension > 0.
|
|
fn normalise_sink_n(target: &DiagramN, sink_maps: &[DiagramMap]) -> NormalisationResult {
|
|
// Step 1: Normalise at each regular height
|
|
let regular_normalisations = normalise_regular_heights(target, sink_maps);
|
|
|
|
// Step 2: Normalise at each singular height
|
|
// CRITICAL: Include P(rh) -> T(rh) -> T(sh) composites in each sink
|
|
let singular_normalisations = normalise_singular_heights(
|
|
target,
|
|
sink_maps,
|
|
®ular_normalisations,
|
|
);
|
|
|
|
// Step 3: Assemble into zigzag P with parallel degeneracy dP
|
|
let (p, d_parallel, assembled_factorisations) = assemble(
|
|
target,
|
|
®ular_normalisations,
|
|
&singular_normalisations,
|
|
sink_maps,
|
|
);
|
|
|
|
// Step 4: Remove trivial cospans not in image of any sink map
|
|
// A cospan is removable iff:
|
|
// - Both legs are isomorphisms (identity cospan)
|
|
// - AND the singular height is not in the image of any sink map
|
|
let (n, d_simple, final_factorisations) = remove_trivial_cospans(
|
|
&p,
|
|
&assembled_factorisations,
|
|
);
|
|
|
|
// Step 5: Compose degeneracies d = dP o dS
|
|
let degeneracy = compose_degeneracies(&d_simple, &d_parallel);
|
|
|
|
NormalisationResult {
|
|
normal_form: n,
|
|
degeneracy,
|
|
factorisations: final_factorisations,
|
|
}
|
|
}
|
|
|
|
/// Intermediate result for regular height normalisation.
|
|
#[derive(Debug, Clone)]
|
|
struct RegularNormalisation {
|
|
/// Normalised diagram at this regular height
|
|
normal_form: Diagram,
|
|
/// Degeneracy from normal form to original
|
|
degeneracy: DiagramMap,
|
|
/// Factorisations for each sink map at this height
|
|
factorisations: Vec<DiagramMap>,
|
|
}
|
|
|
|
/// Construction 17, Step 1: Normalise at each regular height.
|
|
///
|
|
/// For each regular height rh:
|
|
/// - Extract the slice T(rh)
|
|
/// - Collect sink maps restricted to this height: fi(rh): Ai(r_{fi^r(h)}) -> T(rh)
|
|
/// - Recursively normalise
|
|
fn normalise_regular_heights(
|
|
target: &DiagramN,
|
|
sink_maps: &[DiagramMap],
|
|
) -> Vec<RegularNormalisation> {
|
|
let num_regular = target.length() + 1;
|
|
let mut results = Vec::with_capacity(num_regular);
|
|
|
|
for h in 0..num_regular {
|
|
// Get the regular slice T(rh)
|
|
let t_r_h = target.regular_slice(h).unwrap_or_else(|| {
|
|
// Fallback to source if slice computation not available
|
|
(*target.source).clone()
|
|
});
|
|
|
|
// Collect sink maps restricted to this regular height
|
|
// Each fi(rh): Ai(r_{fi^r(h)}) -> T(rh)
|
|
// The regular map fi^r is derived from the singular map via Wraith's R
|
|
let restricted_maps: Vec<DiagramMap> = sink_maps
|
|
.iter()
|
|
.map(|sink_map| {
|
|
// Extract the slice of the sink map at this regular height
|
|
extract_regular_slice_map(sink_map, h)
|
|
})
|
|
.collect();
|
|
|
|
// Recursively normalise this lower-dimensional sink
|
|
let sub_sink = Sink::new(&t_r_h, restricted_maps);
|
|
let sub_result = normalise_sink(&sub_sink);
|
|
|
|
results.push(RegularNormalisation {
|
|
normal_form: sub_result.normal_form,
|
|
degeneracy: sub_result.degeneracy,
|
|
factorisations: sub_result.factorisations,
|
|
});
|
|
}
|
|
|
|
results
|
|
}
|
|
|
|
/// Helper for Construction 17, Step 1: Extract the regular slice map at height h.
|
|
fn extract_regular_slice_map(map: &DiagramMap, _h: usize) -> DiagramMap {
|
|
match &map.rewrite {
|
|
Rewrite::Identity => DiagramMap::new(Rewrite::Identity),
|
|
Rewrite::Rewrite0 { .. } => map.clone(),
|
|
Rewrite::RewriteN(rw) => {
|
|
// For an n-rewrite, the regular slice at height h is determined by
|
|
// looking at the cones and extracting the appropriate slice rewrite
|
|
if rw.cones.is_empty() {
|
|
DiagramMap::new(Rewrite::Identity)
|
|
} else {
|
|
// Find the slice data for this height
|
|
// This would normally involve looking at cone boundaries
|
|
DiagramMap::new(Rewrite::Identity)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Intermediate result for singular height normalisation.
|
|
#[derive(Debug, Clone)]
|
|
#[allow(dead_code)]
|
|
struct SingularNormalisation {
|
|
/// Normalised diagram at this singular height
|
|
normal_form: Diagram,
|
|
/// Degeneracy from normal form to original
|
|
degeneracy: DiagramMap,
|
|
/// Forward cospan leg from left regular (P(rh) -> P(sh))
|
|
forward_leg: DiagramMap,
|
|
/// Backward cospan leg from right regular (P(r{h+1}) -> P(sh))
|
|
backward_leg: DiagramMap,
|
|
/// Factorisations for each sink map at this height
|
|
factorisations: Vec<DiagramMap>,
|
|
}
|
|
|
|
/// Construction 17, Step 2: Normalise at each singular height (with cospan legs in sink).
|
|
///
|
|
/// CRITICAL: The sink at each singular height includes:
|
|
/// - Direct singular maps from sink: fi(st) for t in (fi^s)^{-1}(h)
|
|
/// - Cospan legs: P(rh) -> T(rh) -> T(sh) and P(r{h+1}) -> T(r{h+1}) -> T(sh)
|
|
///
|
|
/// The cospan leg composites are essential for preserving the zigzag structure.
|
|
fn normalise_singular_heights(
|
|
target: &DiagramN,
|
|
sink_maps: &[DiagramMap],
|
|
regular_results: &[RegularNormalisation],
|
|
) -> Vec<SingularNormalisation> {
|
|
let num_singular = target.length();
|
|
let mut results = Vec::with_capacity(num_singular);
|
|
|
|
for h in 0..num_singular {
|
|
// Get the singular slice T(sh)
|
|
let t_s_h = target.singular_slice(h).unwrap_or_else(|| {
|
|
// Fallback to source if slice computation not available
|
|
(*target.source).clone()
|
|
});
|
|
|
|
// Build the sink for this singular height:
|
|
let mut combined_maps: Vec<DiagramMap> = Vec::new();
|
|
|
|
// 1. Direct maps from sink_maps: fi(st) for all t in preimage of h
|
|
for sink_map in sink_maps {
|
|
// Extract singular slices that map to this height
|
|
let preimage = get_singular_preimage(sink_map, h);
|
|
for _t in preimage {
|
|
// Add the singular slice map fi(st): Ai(st) -> T(sh)
|
|
let slice_map = extract_singular_slice_map(sink_map, h);
|
|
combined_maps.push(slice_map);
|
|
}
|
|
}
|
|
|
|
// 2. Cospan leg composite: P(rh) -> T(rh) -> T(sh)
|
|
// This is the composition of the regular degeneracy with the forward cospan leg
|
|
let forward_composite = compose_with_cospan_leg(
|
|
®ular_results[h].degeneracy,
|
|
&target.cospans[h].forward,
|
|
);
|
|
combined_maps.push(forward_composite);
|
|
|
|
// 3. Cospan leg composite: P(r{h+1}) -> T(r{h+1}) -> T(sh)
|
|
// This is the composition of the regular degeneracy with the backward cospan leg
|
|
let backward_composite = compose_with_cospan_leg(
|
|
®ular_results[h + 1].degeneracy,
|
|
&target.cospans[h].backward,
|
|
);
|
|
combined_maps.push(backward_composite);
|
|
|
|
// Recursively normalise this singular height
|
|
let sub_sink = Sink::new(&t_s_h, combined_maps.clone());
|
|
let sub_result = normalise_sink(&sub_sink);
|
|
|
|
// Extract the factorised cospan legs from the result
|
|
// The last two factorisations are for the cospan legs
|
|
let num_factorisations = sub_result.factorisations.len();
|
|
let forward_leg = if num_factorisations >= 2 {
|
|
sub_result.factorisations[num_factorisations - 2].clone()
|
|
} else {
|
|
DiagramMap::identity(&sub_result.normal_form)
|
|
};
|
|
let backward_leg = if num_factorisations >= 1 {
|
|
sub_result.factorisations[num_factorisations - 1].clone()
|
|
} else {
|
|
DiagramMap::identity(&sub_result.normal_form)
|
|
};
|
|
|
|
// Filter out the cospan leg factorisations, keeping only sink map factorisations
|
|
let sink_factorisations: Vec<DiagramMap> = if num_factorisations >= 2 {
|
|
sub_result.factorisations[..num_factorisations - 2].to_vec()
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
results.push(SingularNormalisation {
|
|
normal_form: sub_result.normal_form,
|
|
degeneracy: sub_result.degeneracy,
|
|
forward_leg,
|
|
backward_leg,
|
|
factorisations: sink_factorisations,
|
|
});
|
|
}
|
|
|
|
results
|
|
}
|
|
|
|
/// Helper for Construction 17, Step 2: Get the preimage of singular height h.
|
|
fn get_singular_preimage(map: &DiagramMap, h: usize) -> Vec<usize> {
|
|
match &map.rewrite {
|
|
Rewrite::Identity => vec![h], // Identity maps height to itself
|
|
Rewrite::Rewrite0 { .. } => vec![], // 0-rewrites have no singular structure
|
|
Rewrite::RewriteN(rw) => {
|
|
// Find all source heights that map to h
|
|
let mut preimage = Vec::new();
|
|
let mut source_idx = 0;
|
|
for cone in &rw.cones {
|
|
if cone.index == h {
|
|
// All source indices in this cone's range map to h
|
|
for i in 0..cone.source_size() {
|
|
preimage.push(source_idx + i);
|
|
}
|
|
}
|
|
source_idx += cone.source_size();
|
|
}
|
|
preimage
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper for Construction 17, Step 2: Extract the singular slice map at height h.
|
|
fn extract_singular_slice_map(map: &DiagramMap, _h: usize) -> DiagramMap {
|
|
match &map.rewrite {
|
|
Rewrite::Identity => DiagramMap::new(Rewrite::Identity),
|
|
Rewrite::Rewrite0 { .. } => map.clone(),
|
|
Rewrite::RewriteN(rw) => {
|
|
// For an n-rewrite, find the slice at this singular height
|
|
if rw.cones.is_empty() {
|
|
DiagramMap::new(Rewrite::Identity)
|
|
} else {
|
|
// Extract slice data from cones
|
|
DiagramMap::new(Rewrite::Identity)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper for Construction 17, Step 2: Compose degeneracy with cospan leg.
|
|
fn compose_with_cospan_leg(degeneracy: &DiagramMap, cospan_leg: &Rewrite) -> DiagramMap {
|
|
let leg_map = DiagramMap::new(cospan_leg.clone());
|
|
degeneracy.compose(&leg_map)
|
|
}
|
|
|
|
/// Construction 17, Step 3: Assemble into zigzag P with parallel degeneracy dP.
|
|
///
|
|
/// Returns:
|
|
/// - P: the assembled diagram
|
|
/// - dP: the parallel degeneracy P -> T
|
|
/// - Assembled factorisations
|
|
fn assemble(
|
|
target: &DiagramN,
|
|
regular_results: &[RegularNormalisation],
|
|
singular_results: &[SingularNormalisation],
|
|
sink_maps: &[DiagramMap],
|
|
) -> (Diagram, DiagramMap, Vec<DiagramMap>) {
|
|
// Build cospans for P from the normalisation results
|
|
// Each cospan has forward and backward legs computed from singular normalisation
|
|
let cospans: Vec<Cospan> = singular_results
|
|
.iter()
|
|
.map(|sr| {
|
|
// Convert the factorised legs to rewrites
|
|
Cospan::new(
|
|
sr.forward_leg.rewrite.clone(),
|
|
sr.backward_leg.rewrite.clone(),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
// The source of P is the normalised first regular slice
|
|
let source = regular_results
|
|
.first()
|
|
.map(|r| r.normal_form.clone())
|
|
.unwrap_or_else(|| (*target.source).clone());
|
|
|
|
let p = Diagram::DiagramN(DiagramN::new(source, cospans));
|
|
|
|
// Build the parallel degeneracy dP: P -> T
|
|
// This is assembled from the slice degeneracies
|
|
let d_parallel = build_parallel_degeneracy(regular_results, singular_results, target);
|
|
|
|
// Assemble factorisations for each sink map
|
|
// Each original sink map Ai -> T factors as Ai -> P -> T
|
|
let factorisations = assemble_factorisations(
|
|
sink_maps,
|
|
regular_results,
|
|
singular_results,
|
|
);
|
|
|
|
(p, d_parallel, factorisations)
|
|
}
|
|
|
|
/// Helper for Construction 17, Step 3: Build the parallel degeneracy dP.
|
|
///
|
|
/// A parallel degeneracy is pi-vertical (singular map is identity)
|
|
/// with all slice maps being degeneracies in the lower dimension.
|
|
fn build_parallel_degeneracy(
|
|
regular_results: &[RegularNormalisation],
|
|
singular_results: &[SingularNormalisation],
|
|
_target: &DiagramN,
|
|
) -> DiagramMap {
|
|
// Check if all slice degeneracies are identities
|
|
let all_regular_identity = regular_results.iter().all(|r| r.degeneracy.is_identity());
|
|
let all_singular_identity = singular_results.iter().all(|s| s.degeneracy.is_identity());
|
|
|
|
if all_regular_identity && all_singular_identity {
|
|
// If all slices are identity, the parallel degeneracy is identity
|
|
DiagramMap::new(Rewrite::Identity)
|
|
} else {
|
|
// Build a RewriteN with no cones (pi-vertical) but non-identity slices
|
|
// The slice data is implicit in the structure
|
|
DiagramMap::new(Rewrite::RewriteN(RewriteN {
|
|
dimension: 1,
|
|
cones: vec![],
|
|
}))
|
|
}
|
|
}
|
|
|
|
/// Helper for Construction 17, Step 3: Assemble factorisations through P.
|
|
///
|
|
/// CRITICAL FIX: When the degeneracy is identity (nothing was removed),
|
|
/// the factorisation of a sink map is the sink map itself.
|
|
/// When there is a non-trivial degeneracy, we need to compose properly.
|
|
fn assemble_factorisations(
|
|
sink_maps: &[DiagramMap],
|
|
regular_results: &[RegularNormalisation],
|
|
singular_results: &[SingularNormalisation],
|
|
) -> Vec<DiagramMap> {
|
|
// Check if all slice degeneracies are identity (nothing changed)
|
|
let all_regular_identity = regular_results.iter().all(|r| r.degeneracy.is_identity());
|
|
let all_singular_identity = singular_results.iter().all(|s| s.degeneracy.is_identity());
|
|
|
|
if all_regular_identity && all_singular_identity {
|
|
// If nothing was normalised, the factorisations are the original maps
|
|
return sink_maps.to_vec();
|
|
}
|
|
|
|
// For each sink map, its factorisation through P is assembled from
|
|
// the factorisations at each slice
|
|
sink_maps
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, sink_map)| {
|
|
// Try to get factorisation from regular slices
|
|
if let Some(first_regular) = regular_results.first() {
|
|
if let Some(factorisation) = first_regular.factorisations.get(i) {
|
|
return factorisation.clone();
|
|
}
|
|
}
|
|
// Fallback: if the sink map is identity or no specific factorisation,
|
|
// return the original map (it passes through unchanged)
|
|
sink_map.clone()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Construction 17, Step 4: Remove trivial cospans (simple degeneracy dS : N -> P).
|
|
///
|
|
/// A cospan at singular height h is removable iff:
|
|
/// 1. Both legs are isomorphisms (identity cospan)
|
|
/// 2. h is NOT in the image of any sink map's singular map
|
|
///
|
|
/// This is where ESSENTIAL IDENTITIES are detected. In dimension >= 4,
|
|
/// some identity cospans must be preserved because removing them would
|
|
/// make the zigzag maps ill-defined.
|
|
///
|
|
/// Returns:
|
|
/// - N: the diagram with trivial cospans removed
|
|
/// - dS: the simple degeneracy N -> P that re-inserts them
|
|
/// - Updated factorisations
|
|
fn remove_trivial_cospans(
|
|
p: &Diagram,
|
|
factorisations: &[DiagramMap],
|
|
) -> (Diagram, DiagramMap, Vec<DiagramMap>) {
|
|
match p {
|
|
Diagram::Diagram0(_) => {
|
|
// No cospans to remove
|
|
(p.clone(), DiagramMap::identity(p), factorisations.to_vec())
|
|
}
|
|
Diagram::DiagramN(diagram_n) => {
|
|
// Identify which cospans are trivial (identity) and not in sink image
|
|
let mut kept_cospans = Vec::new();
|
|
let mut removed_indices = Vec::new();
|
|
|
|
for (h, cospan) in diagram_n.cospans.iter().enumerate() {
|
|
let is_identity = cospan.is_identity();
|
|
let in_sink_image = is_in_sink_image(h, factorisations);
|
|
|
|
if !is_identity || in_sink_image {
|
|
// Keep this cospan:
|
|
// - Either it's non-trivial (not identity), OR
|
|
// - It's essential (in the image of some sink map)
|
|
kept_cospans.push(cospan.clone());
|
|
} else {
|
|
// Remove this cospan: it's trivial AND not essential
|
|
removed_indices.push(h);
|
|
}
|
|
}
|
|
|
|
// Build N with kept cospans
|
|
let n = Diagram::DiagramN(DiagramN::new(
|
|
(*diagram_n.source).clone(),
|
|
kept_cospans,
|
|
));
|
|
|
|
// Build simple degeneracy dS: N -> P
|
|
// This re-inserts the removed identity cospans at the correct positions
|
|
let d_simple = build_simple_degeneracy(&n, p, &removed_indices);
|
|
|
|
// Update factorisations to account for removed cospans
|
|
let updated_factorisations = update_factorisations_for_removal(
|
|
factorisations,
|
|
&removed_indices,
|
|
);
|
|
|
|
(n, d_simple, updated_factorisations)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper for Construction 17, Step 4: Check if height h is in sink image.
|
|
///
|
|
/// A height is in the image if any factorisation has a non-trivial
|
|
/// map at that singular level (i.e., some Ai has content mapping to height h).
|
|
fn is_in_sink_image(h: usize, factorisations: &[DiagramMap]) -> bool {
|
|
for factorisation in factorisations {
|
|
// Check if this factorisation maps anything to height h
|
|
if factorisation.has_singular_height_in_image(h) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Lemma 7: Build a simple degeneracy that inserts identity cospans.
|
|
///
|
|
/// A simple degeneracy is pi-cocartesian over a face map composition.
|
|
/// This implements the "simple then parallel" factorisation.
|
|
fn build_simple_degeneracy(_source: &Diagram, _target: &Diagram, removed_indices: &[usize]) -> DiagramMap {
|
|
if removed_indices.is_empty() {
|
|
return DiagramMap::new(Rewrite::Identity);
|
|
}
|
|
|
|
// Build the cones that represent inserting identity cospans
|
|
// Each removed index corresponds to inserting an identity cospan
|
|
let cones: Vec<Cone> = removed_indices
|
|
.iter()
|
|
.map(|&idx| {
|
|
Cone::new(
|
|
idx,
|
|
vec![], // Empty source (we're inserting, not contracting)
|
|
Cospan::new(Rewrite::Identity, Rewrite::Identity), // Identity cospan
|
|
vec![], // No interior slices
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
DiagramMap::new(Rewrite::RewriteN(RewriteN {
|
|
dimension: 1,
|
|
cones,
|
|
}))
|
|
}
|
|
|
|
/// Helper for Construction 17, Step 4: Update factorisations after cospan removal.
|
|
///
|
|
/// Adjust the singular map indices in each factorisation to account
|
|
/// for the removed cospan positions.
|
|
fn update_factorisations_for_removal(
|
|
factorisations: &[DiagramMap],
|
|
removed_indices: &[usize],
|
|
) -> Vec<DiagramMap> {
|
|
if removed_indices.is_empty() {
|
|
return factorisations.to_vec();
|
|
}
|
|
|
|
factorisations
|
|
.iter()
|
|
.map(|f| adjust_factorisation_indices(f, removed_indices))
|
|
.collect()
|
|
}
|
|
|
|
/// Helper for Construction 17, Step 4: Adjust factorisation indices after removal.
|
|
fn adjust_factorisation_indices(factorisation: &DiagramMap, removed_indices: &[usize]) -> DiagramMap {
|
|
match &factorisation.rewrite {
|
|
Rewrite::Identity => factorisation.clone(),
|
|
Rewrite::Rewrite0 { .. } => factorisation.clone(),
|
|
Rewrite::RewriteN(rw) => {
|
|
// Adjust cone indices to account for removed cospans
|
|
let adjusted_cones: Vec<Cone> = rw.cones
|
|
.iter()
|
|
.map(|cone| {
|
|
let new_index = adjust_index(cone.index, removed_indices);
|
|
Cone::new(
|
|
new_index,
|
|
cone.source.clone(),
|
|
cone.target.clone(),
|
|
cone.slices.clone(),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
DiagramMap::new(Rewrite::RewriteN(RewriteN {
|
|
dimension: rw.dimension,
|
|
cones: adjusted_cones,
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper for Construction 17, Step 4: Adjust an index after removing positions.
|
|
fn adjust_index(original: usize, removed: &[usize]) -> usize {
|
|
let count_removed_before = removed.iter().filter(|&&r| r < original).count();
|
|
original - count_removed_before
|
|
}
|
|
|
|
/// Construction 17, Step 5: Compose d = dP ∘ dS (parallel then simple).
|
|
///
|
|
/// Lemma 7: Every degeneracy factors as simple then parallel.
|
|
/// The composition gives the final degeneracy d: N -> T.
|
|
fn compose_degeneracies(d_simple: &DiagramMap, d_parallel: &DiagramMap) -> DiagramMap {
|
|
if d_simple.is_identity() {
|
|
d_parallel.clone()
|
|
} else if d_parallel.is_identity() {
|
|
d_simple.clone()
|
|
} else {
|
|
// Full composition needed
|
|
d_simple.compose(d_parallel)
|
|
}
|
|
}
|
|
|
|
/// Construction 17 (absolute case): Normalise with empty sink.
|
|
///
|
|
/// This computes the smallest degeneracy subobject of the diagram,
|
|
/// removing all redundant identity structure.
|
|
pub fn normalise(diagram: &Diagram) -> NormalisationResult {
|
|
let sink = Sink::empty(diagram);
|
|
normalise_sink(&sink)
|
|
}
|
|
|
|
impl Diagram {
|
|
/// Normalise this diagram (absolute normalisation).
|
|
pub fn normalise(&self) -> NormalisationResult {
|
|
normalise(self)
|
|
}
|
|
|
|
/// Check if this diagram is normalised (is its own normal form).
|
|
pub fn is_normalised(&self) -> bool {
|
|
let result = self.normalise();
|
|
result.degeneracy.is_identity()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::signature::Generator;
|
|
|
|
#[test]
|
|
fn test_normalise_zero_diagram() {
|
|
let g = Generator::point(0);
|
|
let d = Diagram::Diagram0(g);
|
|
|
|
let result = d.normalise();
|
|
|
|
assert!(result.degeneracy.is_identity());
|
|
assert_eq!(result.normal_form, d);
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalise_identity_diagram() {
|
|
let g = Generator::point(0);
|
|
let d0 = Diagram::Diagram0(g);
|
|
let d1 = Diagram::DiagramN(DiagramN::identity(d0.clone()));
|
|
|
|
let result = d1.normalise();
|
|
|
|
// Identity diagram should normalise to itself
|
|
// (an identity zigzag of length 0 has no cospans to remove)
|
|
assert_eq!(result.normal_form.length(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalisation_idempotent() {
|
|
let g = Generator::point(0);
|
|
let d = Diagram::Diagram0(g);
|
|
|
|
let once = d.normalise();
|
|
let twice = once.normal_form.normalise();
|
|
|
|
assert_eq!(once.normal_form, twice.normal_form);
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalise_removes_identity_cospan() {
|
|
// Create a diagram with an identity cospan: r0 -> s0 <- r1
|
|
// where both legs are identities
|
|
let g = Generator::point(0);
|
|
let d0 = Diagram::Diagram0(g);
|
|
|
|
// Create a length-1 diagram with identity cospan
|
|
let identity_cospan = Cospan::new(Rewrite::Identity, Rewrite::Identity);
|
|
let d1 = Diagram::DiagramN(DiagramN::new(d0.clone(), vec![identity_cospan]));
|
|
|
|
let result = d1.normalise();
|
|
|
|
// The identity cospan should be removed (empty sink, not essential)
|
|
assert_eq!(result.normal_form.length(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalise_preserves_non_identity_cospan() {
|
|
// Create a diagram with a non-identity cospan
|
|
let g0 = Generator::point(0);
|
|
let g1 = Generator::point(1);
|
|
|
|
let d0 = Diagram::Diagram0(g0.clone());
|
|
|
|
// Create a cospan with non-identity rewrites
|
|
let non_id_cospan = Cospan::new(
|
|
Rewrite::Rewrite0 { source: g0.clone(), target: g1.clone() },
|
|
Rewrite::Rewrite0 { source: g0.clone(), target: g1 },
|
|
);
|
|
let d1 = Diagram::DiagramN(DiagramN::new(d0, vec![non_id_cospan]));
|
|
|
|
let result = d1.normalise();
|
|
|
|
// The non-identity cospan should be preserved
|
|
assert_eq!(result.normal_form.length(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalise_preserves_essential_identity() {
|
|
// Test case for essential identities (dimension >= 4 scenario)
|
|
// In this simplified test, we create a situation where an identity
|
|
// cospan is in the image of a sink map via CONTRACTION, making it essential.
|
|
//
|
|
// Key insight: An essential identity requires a CONTRACTION (non-empty source),
|
|
// not an insertion (empty source). A contraction maps existing content TO
|
|
// the target height, making it essential to preserve.
|
|
let g = Generator::point(0);
|
|
let d0 = Diagram::Diagram0(g.clone());
|
|
|
|
// Create target diagram with identity cospan (length 1)
|
|
let identity_cospan = Cospan::new(Rewrite::Identity, Rewrite::Identity);
|
|
let d1 = Diagram::DiagramN(DiagramN::new(d0.clone(), vec![identity_cospan.clone()]));
|
|
|
|
// Create a sink map that CONTRACTS to height 0 (non-empty source).
|
|
// This represents a map from a length-2 diagram to d1 (length 1).
|
|
// The contraction maps two cospans to one, putting height 0 in the image.
|
|
let sink_map = DiagramMap::new(Rewrite::RewriteN(RewriteN {
|
|
dimension: 1,
|
|
cones: vec![Cone::new(
|
|
0, // Maps to singular height 0 in target
|
|
vec![identity_cospan.clone(), identity_cospan.clone()], // NON-EMPTY source: contraction!
|
|
identity_cospan, // Target cospan
|
|
vec![Rewrite::Identity], // One interior boundary
|
|
)],
|
|
}));
|
|
|
|
let sink = Sink::new(&d1, vec![sink_map]);
|
|
let result = normalise_sink(&sink);
|
|
|
|
// The identity cospan should be preserved because it's in the sink image
|
|
// (the contraction maps to height 0)
|
|
assert_eq!(result.normal_form.length(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalisation_factorisations_correct() {
|
|
// Test that factorisations are correctly computed
|
|
let g = Generator::point(0);
|
|
let d = Diagram::Diagram0(g);
|
|
|
|
let sink_map = DiagramMap::identity(&d);
|
|
let sink = Sink::new(&d, vec![sink_map]);
|
|
let result = normalise_sink(&sink);
|
|
|
|
// The factorisation should exist for each sink map
|
|
assert_eq!(result.factorisations.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_adjust_index() {
|
|
// Test index adjustment after removal
|
|
assert_eq!(adjust_index(0, &[]), 0);
|
|
assert_eq!(adjust_index(3, &[1, 2]), 1);
|
|
assert_eq!(adjust_index(5, &[0, 2, 4]), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalise_multiple_identity_cospans() {
|
|
// Create a diagram with multiple identity cospans
|
|
let g = Generator::point(0);
|
|
let d0 = Diagram::Diagram0(g);
|
|
|
|
let identity_cospan = Cospan::new(Rewrite::Identity, Rewrite::Identity);
|
|
let d3 = Diagram::DiagramN(DiagramN::new(
|
|
d0.clone(),
|
|
vec![identity_cospan.clone(), identity_cospan.clone(), identity_cospan],
|
|
));
|
|
|
|
let result = d3.normalise();
|
|
|
|
// All identity cospans should be removed (empty sink)
|
|
assert_eq!(result.normal_form.length(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sink_empty() {
|
|
let g = Generator::point(0);
|
|
let d = Diagram::Diagram0(g);
|
|
|
|
let sink = Sink::empty(&d);
|
|
assert!(sink.maps.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_in_sink_image_empty() {
|
|
// With no factorisations, nothing is in the sink image
|
|
assert!(!is_in_sink_image(0, &[]));
|
|
assert!(!is_in_sink_image(5, &[]));
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_in_sink_image_with_identity() {
|
|
// Identity factorisation maps all heights to themselves
|
|
let id = DiagramMap::new(Rewrite::Identity);
|
|
assert!(is_in_sink_image(0, &[id.clone()]));
|
|
assert!(is_in_sink_image(10, &[id]));
|
|
}
|
|
}
|