Compare commits

..

No commits in common. "main" and "feat/explosion" have entirely different histories.

36 changed files with 197 additions and 143786 deletions

4
.gitignore vendored
View file

@ -1,3 +1 @@
target/ /target
*.swp
*.swo

3
Cargo.lock generated
View file

@ -346,7 +346,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_derive",
] ]
[[package]] [[package]]
@ -633,8 +632,6 @@ name = "zigzag-engine"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"proptest", "proptest",
"serde",
"serde_json",
"thiserror", "thiserror",
] ]

View file

@ -7,8 +7,6 @@ license = "BSD-3-Clause"
[dependencies] [dependencies]
thiserror = "1" thiserror = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dev-dependencies] [dev-dependencies]
proptest = "1" proptest = "1"

View file

@ -1,94 +0,0 @@
//! Inspect the categorical structure of half_braid.json
//!
//! Run with: cargo run --example inspect_half_braid
use std::fs;
use zigzag_engine::diagram::Diagram;
use zigzag_engine::import::load_homotopy_diagram_n;
fn describe_diagram(d: &Diagram, indent: usize) -> String {
let prefix = " ".repeat(indent);
match d {
Diagram::Diagram0(g) => {
format!("{}0-diagram: generator id={}, dim={}", prefix, g.id, g.dimension)
}
Diagram::DiagramN(dn) => {
let dim = d.dimension();
let mut lines = vec![format!(
"{}{}-diagram with {} cospans:",
prefix,
dim,
dn.cospans.len()
)];
lines.push(format!("{} source:", prefix));
lines.push(describe_diagram(dn.source(), indent + 2));
if !dn.cospans.is_empty() {
lines.push(format!("{} target:", prefix));
lines.push(describe_diagram(&dn.target(), indent + 2));
}
lines.join("\n")
}
}
}
fn main() {
let json = fs::read_to_string("fixtures/half_braid.json")
.expect("Failed to read half_braid.json");
let half_braid = load_homotopy_diagram_n(&json)
.expect("Failed to parse");
let half_braid_d = Diagram::DiagramN(half_braid.clone());
println!("=== HALF_BRAID CATEGORICAL STRUCTURE ===\n");
// The half_braid itself is a 3-diagram
println!("half_braid is a {}-diagram with {} cospans\n",
half_braid_d.dimension(), half_braid.cospans.len());
// Its source (a 2-diagram)
let source_2d = half_braid.source();
println!("SOURCE of half_braid (the 2-diagram it transforms FROM):");
println!("{}\n", describe_diagram(source_2d, 1));
// Its target (a 2-diagram)
let target_2d = half_braid.target();
println!("TARGET of half_braid (the 2-diagram it transforms TO):");
println!("{}\n", describe_diagram(&target_2d, 1));
// Are source and target the same?
println!("Are source and target equal? {}\n", source_2d == &target_2d);
// Look at the source 2-diagram structure
if let Diagram::DiagramN(src) = source_2d {
println!("=== SOURCE 2-DIAGRAM SLICES ===");
println!("This 2-diagram has {} cospans (singular heights)\n", src.cospans.len());
// Regular slices
for i in 0..=src.cospans.len() {
if let Some(slice) = src.regular_slice(i) {
println!("Regular slice r{}: {}", i, describe_diagram(&slice, 0));
}
}
println!();
// Singular slices
for i in 0..src.cospans.len() {
if let Some(slice) = src.singular_slice(i) {
println!("Singular slice s{}: {}", i, describe_diagram(&slice, 0));
}
}
}
println!("\n=== INTERPRETATION ===");
println!("Generator 0 (dim=0): The base object x");
println!("Generator 1 (dim=2): The scalar s (a 2-cell: id_x → id_x)");
println!();
println!("The SOURCE 2-diagram is 'two scalars stacked':");
println!(" - 2 cospans means 2 singular heights (s0, s1)");
println!(" - Each singular height is where a scalar (2-cell) lives");
println!();
println!("The half_braid 3-diagram is the Eckmann-Hilton homotopy:");
println!(" - It shows the two scalars 'sliding past' each other");
println!(" - Source = target (as 2-diagrams, they're the same configuration)");
println!(" - But the 3-diagram is NON-trivial: it's the braiding coherence");
}

View file

@ -1,287 +0,0 @@
//! Generate geometry JSON from explosion for Three.js rendering.
//!
//! Run with: cargo run --example render_braiding
//!
//! Outputs fixtures/half_braid_geometry.json with VISIBLE elements only.
//! Visibility follows homotopy.io's rule: a point at geom_dim d is visible
//! iff coords[d..] are all singular.
use std::collections::{HashMap, HashSet, VecDeque};
use std::fs;
use zigzag_engine::diagram::Diagram;
use zigzag_engine::explosion::{HeightLabel, Point};
use zigzag_engine::import::load_homotopy_diagram_n;
use serde::Serialize;
#[derive(Serialize)]
struct Geometry {
metadata: Metadata,
vertices: Vec<Vertex>,
wires: Vec<Wire>,
surfaces: Vec<Surface>,
}
#[derive(Serialize)]
struct Metadata {
source: String,
dimension: usize,
total_points: usize,
visible_points: usize,
total_covers: usize,
}
#[derive(Serialize)]
struct Vertex {
id: usize,
label: String,
point: String,
coords: [f64; 3],
}
#[derive(Serialize)]
struct Wire {
id: usize,
label: String,
point: String,
coords: [f64; 3],
endpoints: [usize; 2],
endpoint_coords: [[f64; 3]; 2],
}
#[derive(Serialize)]
struct Surface {
id: usize,
label: String,
point: String,
coords: [f64; 3],
boundary_wires: Vec<usize>,
}
/// Format a point as a string like "s0,s1,r0"
fn format_point(p: &Point) -> String {
p.0.iter()
.map(|h| match h {
HeightLabel::Regular(j) => format!("r{}", j),
HeightLabel::Singular(j) => format!("s{}", j),
})
.collect::<Vec<_>>()
.join(",")
}
/// Compute layout coordinates for rendering.
///
/// For half_braid visible elements:
/// - coord[0] (depth/strand): r0→-1, r1→0, r2→1 for wires; s0→-0.5, s1→0.5 for vertices
/// - coord[1] (height): r0→-1, s0→0, r1→1 for layout y
/// - coord[2] (time): s0→0 for visible elements (all at crossing time)
///
/// Output: [x, y, z] where:
/// - x = time (all 0 for visible crossing slice)
/// - y = height
/// - z = depth (spread wires/vertices along this axis)
fn layout_coords(p: &Point) -> [f64; 3] {
// For the visible crossing slice, all elements have coord[2] = s0 (time = singular)
// So x (time) = 0 for all visible elements
// z = depth axis (coord[0])
let z = match &p.0[0] {
HeightLabel::Regular(j) => (*j as f64) - 1.0, // r0→-1, r1→0, r2→1
HeightLabel::Singular(j) => (*j as f64) - 0.5, // s0→-0.5, s1→0.5
};
// y = height axis (coord[1])
let y = match &p.0[1] {
HeightLabel::Regular(j) => (*j as f64) - 1.0, // r0→-1, r1→1
HeightLabel::Singular(j) => *j as f64, // s0→0
};
// x = time axis (coord[2]) - all visible elements are at s0
let x = 0.0;
[x, y, z]
}
/// Count singular labels in a point
fn singular_count(p: &Point) -> usize {
p.0.iter().filter(|h| h.is_singular()).count()
}
/// Compute geometric dimension from singular count
fn geom_dim(p: &Point, n: usize) -> usize {
n - singular_count(p)
}
fn main() {
// Load diagram
let json = fs::read_to_string("fixtures/half_braid.json")
.expect("Failed to read fixtures/half_braid.json");
let diagram_n = load_homotopy_diagram_n(&json)
.expect("Failed to parse half_braid.json");
let diagram = Diagram::DiagramN(diagram_n);
let n = diagram.dimension();
let pts = diagram.full_points();
eprintln!("Loaded half_braid.json: dim={}, {} points, {} covers",
n, pts.len(), pts.covers().len());
// Filter to VISIBLE points only
let visible_indices: Vec<usize> = pts.elements()
.iter()
.enumerate()
.filter(|(_, point)| point.is_visible(n))
.map(|(idx, _)| idx)
.collect();
eprintln!("Visible points: {}", visible_indices.len());
// Group visible points by geometric dimension
let mut by_geom_dim: HashMap<usize, Vec<usize>> = HashMap::new();
for &idx in &visible_indices {
let point = &pts.elements()[idx];
let gd = geom_dim(point, n);
by_geom_dim.entry(gd).or_default().push(idx);
}
// Build adjacency for reachability (on full poset)
let mut successors: Vec<Vec<usize>> = vec![vec![]; pts.len()];
let mut predecessors: Vec<Vec<usize>> = vec![vec![]; pts.len()];
for &(lower, upper) in pts.covers() {
successors[lower].push(upper);
predecessors[upper].push(lower);
}
// Helper: find all transitively reachable points
let reachable_from = |start: usize, adj: &[Vec<usize>]| -> HashSet<usize> {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
visited.insert(start);
while let Some(curr) = queue.pop_front() {
for &next in &adj[curr] {
if visited.insert(next) {
queue.push_back(next);
}
}
}
visited
};
// Get visible vertex indices
let vertex_indices = by_geom_dim.get(&0).map(|v| v.as_slice()).unwrap_or(&[]);
let vertex_set: HashSet<usize> = vertex_indices.iter().copied().collect();
// Get visible wire indices
let wire_indices = by_geom_dim.get(&1).map(|v| v.as_slice()).unwrap_or(&[]);
let wire_set: HashSet<usize> = wire_indices.iter().copied().collect();
// Build vertices
let mut vertices: Vec<Vertex> = Vec::new();
for (i, &idx) in vertex_indices.iter().enumerate() {
let point = &pts.elements()[idx];
vertices.push(Vertex {
id: idx,
label: format!("vertex_{}", i),
point: format_point(point),
coords: layout_coords(point),
});
}
// Build wires with endpoint connections
let mut wires: Vec<Wire> = Vec::new();
for (i, &idx) in wire_indices.iter().enumerate() {
let point = &pts.elements()[idx];
// Find connected VISIBLE vertices via transitive reachability
let reachable_up = reachable_from(idx, &successors);
let reachable_down = reachable_from(idx, &predecessors);
let mut connected: Vec<usize> = reachable_up
.union(&reachable_down)
.filter(|v| vertex_set.contains(v))
.copied()
.collect();
connected.sort();
connected.dedup();
let endpoints = if connected.len() >= 2 {
[connected[0], connected[1]]
} else if connected.len() == 1 {
[connected[0], connected[0]]
} else {
[vertex_indices[0], vertex_indices.get(1).copied().unwrap_or(vertex_indices[0])]
};
let endpoint_coords = [
layout_coords(&pts.elements()[endpoints[0]]),
layout_coords(&pts.elements()[endpoints[1]]),
];
wires.push(Wire {
id: idx,
label: format!("wire_{}", i),
point: format_point(point),
coords: layout_coords(point),
endpoints,
endpoint_coords,
});
}
// Build visible surfaces (geom_dim=2)
let surface_indices = by_geom_dim.get(&2).map(|v| v.as_slice()).unwrap_or(&[]);
let mut surfaces: Vec<Surface> = Vec::new();
for (i, &idx) in surface_indices.iter().enumerate() {
let point = &pts.elements()[idx];
// Find boundary wires via DIRECT covering relations
// Filter to only VISIBLE wires
let mut boundary_wires: Vec<usize> = successors[idx]
.iter()
.chain(predecessors[idx].iter())
.filter(|v| wire_set.contains(v))
.copied()
.collect();
boundary_wires.sort();
boundary_wires.dedup();
surfaces.push(Surface {
id: idx,
label: format!("surface_{}", i),
point: format_point(point),
coords: layout_coords(point),
boundary_wires,
});
}
// Build output (no volumes - they're not rendered)
let geometry = Geometry {
metadata: Metadata {
source: "half_braid.json".to_string(),
dimension: n,
total_points: pts.len(),
visible_points: visible_indices.len(),
total_covers: pts.covers().len(),
},
vertices,
wires,
surfaces,
};
// Output JSON
let json_output = serde_json::to_string_pretty(&geometry).expect("Failed to serialize");
// Write to file
fs::write("fixtures/half_braid_geometry.json", &json_output)
.expect("Failed to write fixtures/half_braid_geometry.json");
eprintln!("\nWrote fixtures/half_braid_geometry.json (VISIBLE ONLY)");
eprintln!(" {} vertices", geometry.vertices.len());
eprintln!(" {} wires", geometry.wires.len());
eprintln!(" {} surfaces", geometry.surfaces.len());
// Also print to stdout for piping
println!("{}", json_output);
}

View file

@ -1,330 +0,0 @@
//! Complete scaffold analysis for half_braid
//!
//! Run with: cargo run --example scaffold_analysis
use std::fs;
use zigzag_engine::diagram::Diagram;
use zigzag_engine::explosion::{HeightLabel, Point};
use zigzag_engine::import::load_homotopy_diagram_n;
/// Format a point as a string like "r0,s0,s1"
fn format_point(p: &Point) -> String {
p.0.iter()
.map(|h| match h {
HeightLabel::Regular(j) => format!("r{}", j),
HeightLabel::Singular(j) => format!("s{}", j),
})
.collect::<Vec<_>>()
.join(",")
}
/// Count singular labels in a point
fn singular_count(p: &Point) -> usize {
p.0.iter().filter(|h| h.is_singular()).count()
}
/// Compute geometric dimension: n - singular_count
fn geom_dim(p: &Point, n: usize) -> usize {
n - singular_count(p)
}
/// Naive layout position: regular -> integer, singular -> half-integer
fn naive_layout(p: &Point) -> Vec<f64> {
p.0.iter()
.map(|h| match h {
HeightLabel::Regular(j) => *j as f64,
HeightLabel::Singular(j) => *j as f64 + 0.5,
})
.collect()
}
/// Describe which coordinate changed between two points
fn describe_change(lower: &Point, upper: &Point) -> String {
for (i, (l, u)) in lower.0.iter().zip(upper.0.iter()).enumerate() {
if l != u {
let l_str = match l {
HeightLabel::Regular(j) => format!("r{}", j),
HeightLabel::Singular(j) => format!("s{}", j),
};
let u_str = match u {
HeightLabel::Regular(j) => format!("r{}", j),
HeightLabel::Singular(j) => format!("s{}", j),
};
return format!("coord[{}]: {}{}", i, l_str, u_str);
}
}
"no change".to_string()
}
/// Get time slice label from coord[2]
fn time_slice(p: &Point) -> String {
match p.0.get(2) {
Some(HeightLabel::Regular(0)) => "r0 (source)".to_string(),
Some(HeightLabel::Singular(0)) => "s0 (merge)".to_string(),
Some(HeightLabel::Regular(1)) => "r1 (target)".to_string(),
Some(h) => format!("{:?}", h),
None => "N/A".to_string(),
}
}
fn main() {
// Load diagram
let json = fs::read_to_string("fixtures/half_braid.json")
.expect("Failed to read fixtures/half_braid.json");
let diagram_n = load_homotopy_diagram_n(&json)
.expect("Failed to parse half_braid.json");
let diagram = Diagram::DiagramN(diagram_n);
let n = diagram.dimension();
let pts = diagram.full_points();
println!("════════════════════════════════════════════════════════════════════════════════");
println!("COMPLETE SCAFFOLD ANALYSIS FOR half_braid.json");
println!("════════════════════════════════════════════════════════════════════════════════");
println!();
println!("Diagram dimension: {}", n);
println!("Total points: {}", pts.len());
println!("Total covering relations: {}", pts.covers().len());
println!();
// =========================================================================
// SECTION 1: All 23 Points
// =========================================================================
println!("════════════════════════════════════════════════════════════════════════════════");
println!("SECTION 1: ALL {} POINTS", pts.len());
println!("════════════════════════════════════════════════════════════════════════════════");
println!();
println!("{:>3} {:>12} {:>4} {:>8} {:>8} {:>20}",
"idx", "coords", "sing", "geom_dim", "visible", "naive_layout");
println!("{}", "-".repeat(80));
for (idx, point) in pts.elements().iter().enumerate() {
let sc = singular_count(point);
let gd = geom_dim(point, n);
let vis = point.is_visible(n);
let layout = naive_layout(point);
let layout_str = format!("({:.1}, {:.1}, {:.1})", layout[0], layout[1], layout[2]);
println!("{:>3} {:>12} {:>4} {:>8} {:>8} {:>20}",
idx,
format_point(point),
sc,
gd,
if vis { "YES" } else { "no" },
layout_str);
}
println!();
// =========================================================================
// SECTION 2: All 35 Covering Relations
// =========================================================================
println!("════════════════════════════════════════════════════════════════════════════════");
println!("SECTION 2: ALL {} COVERING RELATIONS", pts.covers().len());
println!("════════════════════════════════════════════════════════════════════════════════");
println!();
println!("{:>3} {:>12}{:>12} {:>20}",
"#", "lower", "upper", "change");
println!("{}", "-".repeat(60));
for (i, &(lower_idx, upper_idx)) in pts.covers().iter().enumerate() {
let lower = &pts.elements()[lower_idx];
let upper = &pts.elements()[upper_idx];
let change = describe_change(lower, upper);
println!("{:>3} {:>12}{:>12} {:>20}",
i + 1,
format!("{}:{}", lower_idx, format_point(lower)),
format!("{}:{}", upper_idx, format_point(upper)),
change);
}
println!();
// =========================================================================
// SECTION 3: Visible Elements and Their Connections
// =========================================================================
println!("════════════════════════════════════════════════════════════════════════════════");
println!("SECTION 3: VISIBLE ELEMENTS AND THEIR CONNECTIONS");
println!("════════════════════════════════════════════════════════════════════════════════");
println!();
let visible_indices: Vec<usize> = pts.elements()
.iter()
.enumerate()
.filter(|(_, point)| point.is_visible(n))
.map(|(idx, _)| idx)
.collect();
println!("Visible elements: {} total", visible_indices.len());
println!();
// Group by geometric dimension
for gd in 0..=n {
let elements: Vec<usize> = visible_indices.iter()
.filter(|&&idx| geom_dim(&pts.elements()[idx], n) == gd)
.copied()
.collect();
if elements.is_empty() {
continue;
}
let gd_name = match gd {
0 => "VERTICES (0-dim)",
1 => "WIRES (1-dim)",
2 => "SURFACES (2-dim)",
3 => "VOLUMES (3-dim)",
_ => "HIGHER",
};
println!("--- {} ---", gd_name);
println!();
for idx in elements {
let point = &pts.elements()[idx];
let preds = pts.immediate_predecessors(idx);
let succs = pts.immediate_successors(idx);
println!(" [{:>2}] {} = ({:.1}, {:.1}, {:.1})",
idx, format_point(point),
naive_layout(point)[0],
naive_layout(point)[1],
naive_layout(point)[2]);
if !preds.is_empty() {
println!(" predecessors (covered by this):");
for p_idx in &preds {
let p = &pts.elements()[*p_idx];
let vis = if p.is_visible(n) { " [VIS]" } else { "" };
println!(" [{:>2}] {}{}", p_idx, format_point(p), vis);
}
}
if !succs.is_empty() {
println!(" successors (covers this):");
for s_idx in &succs {
let s = &pts.elements()[*s_idx];
let vis = if s.is_visible(n) { " [VIS]" } else { "" };
println!(" [{:>2}] {}{}", s_idx, format_point(s), vis);
}
}
println!();
}
}
// =========================================================================
// SECTION 4: Points Grouped by Time Slice
// =========================================================================
println!("════════════════════════════════════════════════════════════════════════════════");
println!("SECTION 4: POINTS GROUPED BY TIME SLICE (coord[2])");
println!("════════════════════════════════════════════════════════════════════════════════");
println!();
// Group points by time slice
let mut by_time: std::collections::HashMap<String, Vec<usize>> = std::collections::HashMap::new();
for (idx, point) in pts.elements().iter().enumerate() {
let ts = time_slice(point);
by_time.entry(ts).or_default().push(idx);
}
for time_label in &["r0 (source)", "s0 (merge)", "r1 (target)"] {
if let Some(indices) = by_time.get(*time_label) {
println!("--- TIME {} ---", time_label);
println!(" {} points at this time slice:", indices.len());
for &idx in indices {
let point = &pts.elements()[idx];
let vis = if point.is_visible(n) { " [VISIBLE]" } else { "" };
let gd = geom_dim(point, n);
let gd_str = match gd {
0 => "vertex",
1 => "wire",
2 => "surface",
3 => "volume",
_ => "?",
};
println!(" [{:>2}] {:>12} (geom_dim={}, {}){}",
idx, format_point(point), gd, gd_str, vis);
}
println!();
}
}
// =========================================================================
// SECTION 5: Scaffold Node Paths for Visible Wires
// =========================================================================
println!("════════════════════════════════════════════════════════════════════════════════");
println!("SECTION 5: SCAFFOLD NODE PATHS FOR VISIBLE WIRES");
println!("════════════════════════════════════════════════════════════════════════════════");
println!();
let wire_indices: Vec<usize> = visible_indices.iter()
.filter(|&&idx| geom_dim(&pts.elements()[idx], n) == 1)
.copied()
.collect();
println!("Visible wires trace through the scaffold via covering relations.");
println!("Each wire has geom_dim=1 (one regular coordinate).");
println!();
for idx in wire_indices {
let point = &pts.elements()[idx];
let layout = naive_layout(point);
println!("WIRE [{:>2}] {} at ({:.1}, {:.1}, {:.1})",
idx, format_point(point), layout[0], layout[1], layout[2]);
// Find all reachable points in both directions (full path through scaffold)
let preds = pts.immediate_predecessors(idx);
let succs = pts.immediate_successors(idx);
println!(" Direct connections:");
for p_idx in &preds {
let p = &pts.elements()[*p_idx];
let vis = if p.is_visible(n) { " [VIS]" } else { "" };
let p_layout = naive_layout(p);
println!(" ↓ [{:>2}] {} at ({:.1},{:.1},{:.1}){}",
p_idx, format_point(p), p_layout[0], p_layout[1], p_layout[2], vis);
}
println!(" ● [{:>2}] {} (this wire)", idx, format_point(point));
for s_idx in &succs {
let s = &pts.elements()[*s_idx];
let vis = if s.is_visible(n) { " [VIS]" } else { "" };
let s_layout = naive_layout(s);
println!(" ↑ [{:>2}] {} at ({:.1},{:.1},{:.1}){}",
s_idx, format_point(s), s_layout[0], s_layout[1], s_layout[2], vis);
}
println!();
}
// =========================================================================
// SECTION 6: Adjacency Matrix (abbreviated)
// =========================================================================
println!("════════════════════════════════════════════════════════════════════════════════");
println!("SECTION 6: COVER ADJACENCY (which points cover which)");
println!("════════════════════════════════════════════════════════════════════════════════");
println!();
// Build adjacency
let mut successors: Vec<Vec<usize>> = vec![vec![]; pts.len()];
let mut predecessors: Vec<Vec<usize>> = vec![vec![]; pts.len()];
for &(lower, upper) in pts.covers() {
successors[lower].push(upper);
predecessors[upper].push(lower);
}
println!("Point → Immediate Successors (covered by)");
println!("{}", "-".repeat(50));
for (idx, succs) in successors.iter().enumerate() {
if !succs.is_empty() {
let point = &pts.elements()[idx];
let succs_str: Vec<String> = succs.iter()
.map(|&s| format!("{}:{}", s, format_point(&pts.elements()[s])))
.collect();
println!("[{:>2}] {:>12} → [{}]", idx, format_point(point), succs_str.join(", "));
}
}
println!();
println!("════════════════════════════════════════════════════════════════════════════════");
println!("END OF SCAFFOLD ANALYSIS");
println!("════════════════════════════════════════════════════════════════════════════════");
}

View file

@ -1,117 +0,0 @@
//! Trace the merge topology of half_braid
//!
//! Run with: cargo run --example trace_merge
use std::fs;
use zigzag_engine::diagram::Diagram;
use zigzag_engine::import::load_homotopy_diagram_n;
fn main() {
let json = fs::read_to_string("fixtures/half_braid.json")
.expect("Failed to read half_braid.json");
let half_braid = load_homotopy_diagram_n(&json)
.expect("Failed to parse");
println!("=== MERGE TOPOLOGY ANALYSIS ===\n");
// Source 2-diagram structure
if let Diagram::DiagramN(src) = half_braid.source() {
println!("SOURCE 2-diagram ({} cospans):", src.cospans.len());
println!("Heights: r0, s0, r1, s1, r2");
println!();
// Print y-coordinates for each height
println!("Height mappings (using layout_coords logic):");
for i in 0..=src.cospans.len() {
let y = (i as f64) - 1.0;
println!(" r{}: y = {:.1}", i, y);
if i < src.cospans.len() {
let y_sing = i as f64;
println!(" s{}: y = {:.1} ← SCALAR HERE", i, y_sing);
}
}
println!();
}
// Target 2-diagram structure
let target = half_braid.target();
if let Diagram::DiagramN(tgt) = &target {
println!("TARGET 2-diagram ({} cospans):", tgt.cospans.len());
println!("Heights: r0, s0, r1");
println!();
println!("Height mappings:");
for i in 0..=tgt.cospans.len() {
let y = (i as f64) - 1.0;
println!(" r{}: y = {:.1}", i, y);
if i < tgt.cospans.len() {
let y_sing = i as f64;
println!(" s{}: y = {:.1} ← MERGED SCALAR HERE", i, y_sing);
}
}
println!();
}
println!("=== VISIBLE ELEMENT ANALYSIS ===\n");
println!("The 2 VERTICES (geom_dim=0) are the TWO INPUT SCALARS:");
println!(" vertex (s0,s0,s0): z=-0.5, the FIRST scalar from source s0");
println!(" vertex (s1,s0,s0): z=+0.5, the SECOND scalar from source s1");
println!();
println!("The 3 WIRES (geom_dim=1) are the BOUNDARIES between regions:");
println!(" wire (r0,s0,s0): z=-1.0, LEFT boundary (below both scalars)");
println!(" wire (r1,s0,s0): z= 0.0, MIDDLE boundary (between the two scalars)");
println!(" wire (r2,s0,s0): z=+1.0, RIGHT boundary (above both scalars)");
println!();
println!("=== Y-SHAPE TOPOLOGY ===\n");
println!("The MERGE contracts source heights r0,s0,r1,s1,r2 into target heights r0,s0,r1");
println!();
println!("Mapping:");
println!(" Source r0 (y=-1) → Target r0 (y=-1) [PRESERVED]");
println!(" Source s0 (y= 0) → Target s0 (y= 0) [MERGED INTO]");
println!(" Source r1 (y= 0) → Target s0 (y= 0) [ABSORBED]");
println!(" Source s1 (y= 1) → Target s0 (y= 0) [MERGED INTO]");
println!(" Source r2 (y=+1) → Target r1 (y= 0) [CONTRACTED DOWN]");
println!();
println!("For the 3 visible wires:");
println!();
println!("Wire r0 (z=-1, LEFT EDGE):");
println!(" Source endpoint (x=-1): y=-1 (at source height r0)");
println!(" Merge waypoint (x= 0): y= 0 (at merge height s0)");
println!(" Target endpoint (x=+1): y=-1 (at target height r0)");
println!(" → This wire DIPS DOWN to the merge then back up");
println!();
println!("Wire r1 (z=0, MIDDLE/STEM):");
println!(" Source endpoint (x=-1): y= 0 (at source height r1, between s0 and s1)");
println!(" Merge waypoint (x= 0): y= 0 (at merge height s0)");
println!(" Target endpoint (x=+1): y= 0 (at target height s0)");
println!(" → This is the STEM - stays at y=0 throughout");
println!();
println!("Wire r2 (z=+1, RIGHT EDGE):");
println!(" Source endpoint (x=-1): y=+1 (at source height r2)");
println!(" Merge waypoint (x= 0): y= 0 (at merge height s0)");
println!(" Target endpoint (x=+1): y= 0 (at target height r1)");
println!(" → This wire comes DOWN from above into the merge");
println!();
println!("=== THE Y-SHAPE ===\n");
println!("Looking at y-z plane (height vs depth) at different x (time) slices:\n");
println!("At SOURCE (x=-1): At MERGE (x=0): At TARGET (x=+1):");
println!(" ");
println!("y=+1 ──●r2── y=+1 y=+1 ");
println!(" │ ╲ ");
println!(" │ ╲ ");
println!("y= 0 ──●r1── ←s1 scalar y= 0 ●●● (merge) y= 0 ──●r1,r2── ");
println!(" ↑ ↑ ");
println!(" vertices merged ");
println!("y=-1 ──●r0── ←s0 scalar y=-1 y=-1 ──●r0── ");
println!(" ");
println!(" z: -1 0 +1 -1 0 +1 -1 0 +1 ");
}

View file

@ -1,115 +0,0 @@
//! Trace scaffold nodes for visible wires through all time heights
//!
//! Run with: cargo run --example trace_scaffold
use std::fs;
use zigzag_engine::diagram::Diagram;
use zigzag_engine::import::load_homotopy_diagram_n;
fn main() {
let json = fs::read_to_string("fixtures/half_braid.json")
.expect("Failed to read half_braid.json");
let half_braid = load_homotopy_diagram_n(&json)
.expect("Failed to parse");
println!("=== SCAFFOLD NODE TRACING ===\n");
// Time structure of the 3-diagram
println!("TIME STRUCTURE (coord[2]):");
println!(" The half_braid has {} cospan(s)", half_braid.cospans.len());
println!(" Time heights: r0 (source), s0 (merge), r1 (target)");
println!();
// Source 2-diagram heights
if let Diagram::DiagramN(src) = half_braid.source() {
println!("SOURCE 2-DIAGRAM (at time r0):");
println!(" {} cospans → heights: r0, s0, r1, s1, r2", src.cospans.len());
println!(" Y-mapping: r0→-1, s0→0, r1→0, s1→1, r2→+1");
println!();
}
// Target 2-diagram heights
let target = half_braid.target();
if let Diagram::DiagramN(tgt) = &target {
println!("TARGET 2-DIAGRAM (at time r1):");
println!(" {} cospan(s) → heights: r0, s0, r1", tgt.cospans.len());
println!(" Y-mapping: r0→-1, s0→0, r1→0");
println!();
}
println!("=== HEIGHT MAPPING THROUGH MERGE ===");
println!();
println!("The merge contracts source heights to target heights:");
println!(" Source r0 → Target r0 (preserved, y stays at -1)");
println!(" Source s0 → Target s0 (merges, y=0 → y=0)");
println!(" Source r1 → Target s0 (absorbed into merge, y=0 → y=0)");
println!(" Source s1 → Target s0 (merges, y=1 → y=0)");
println!(" Source r2 → Target r1 (contracts down, y=+1 → y=0)");
println!();
println!("=== SCAFFOLD NODE POSITIONS FOR EACH WIRE ===");
println!();
println!("Time positions: r0 at x=-1, s0 at x=0, r1 at x=+1");
println!("Depth positions: r0→z=-1, r1→z=0, r2→z=+1");
println!();
// Wire r0
println!("WIRE r0 (coord[0]=r0, depth z=-1):");
println!(" At time r0 (source): coord=(r0, r0, r0)");
println!(" → coord[1]=r0=Regular(0) → y = -1");
println!(" → position: (-1, -1, -1)");
println!();
println!(" At time s0 (merge): coord=(r0, s0, s0)");
println!(" → coord[1]=s0=Singular(0) → y = 0");
println!(" → position: (0, 0, -1)");
println!();
println!(" At time r1 (target): coord=(r0, r0, r1)");
println!(" → coord[1]=r0=Regular(0) → y = -1");
println!(" → position: (+1, -1, -1)");
println!();
println!(" Wire r0 polyline: [(-1,-1,-1), (0,0,-1), (+1,-1,-1)]");
println!(" Shape: DIPS to merge, returns to original height");
println!();
// Wire r1
println!("WIRE r1 (coord[0]=r1, depth z=0):");
println!(" At time r0 (source): coord=(r1, r1, r0)");
println!(" → coord[1]=r1=Regular(1) → y = 0");
println!(" → position: (-1, 0, 0)");
println!();
println!(" At time s0 (merge): coord=(r1, s0, s0)");
println!(" → coord[1]=s0=Singular(0) → y = 0");
println!(" → position: (0, 0, 0)");
println!();
println!(" At time r1 (target): coord=(r1, s0, r1)");
println!(" → coord[1]=s0=Singular(0) → y = 0 (r1 absorbed into s0)");
println!(" → position: (+1, 0, 0)");
println!();
println!(" Wire r1 polyline: [(-1,0,0), (0,0,0), (+1,0,0)]");
println!(" Shape: FLAT at y=0 throughout - this is the STEM");
println!();
// Wire r2
println!("WIRE r2 (coord[0]=r2, depth z=+1):");
println!(" At time r0 (source): coord=(r2, r2, r0)");
println!(" → coord[1]=r2=Regular(2) → y = +1");
println!(" → position: (-1, +1, +1)");
println!();
println!(" At time s0 (merge): coord=(r2, s0, s0)");
println!(" → coord[1]=s0=Singular(0) → y = 0");
println!(" → position: (0, 0, +1)");
println!();
println!(" At time r1 (target): coord=(r2, r1, r1)");
println!(" → coord[1]=r1=Regular(1) → y = 0 (r2 contracted to r1)");
println!(" → position: (+1, 0, +1)");
println!();
println!(" Wire r2 polyline: [(-1,+1,+1), (0,0,+1), (+1,0,+1)]");
println!(" Shape: DROPS from y=+1 to y=0, stays at y=0");
println!();
println!("=== SUMMARY ===");
println!();
println!("Wire r0: [(-1,-1,-1), (0,0,-1), (1,-1,-1)] // dips and returns");
println!("Wire r1: [(-1, 0, 0), (0,0, 0), (1, 0, 0)] // flat stem");
println!("Wire r2: [(-1,+1,+1), (0,0,+1), (1, 0,+1)] // drops and stays");
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,42 +0,0 @@
warning: unnecessary parentheses around closure body
--> homotopy-core/src/collapse.rs:135:22
|
135 | .filter(|&e| (e.target() != keep))
| ^ ^
|
= note: `#[warn(unused_parens)]` (part of `#[warn(unused)]`) on by default
help: remove these parentheses
|
135 - .filter(|&e| (e.target() != keep))
135 + .filter(|&e| e.target() != keep)
|
warning: unnecessary parentheses around closure body
--> homotopy-core/src/collapse.rs:158:22
|
158 | .filter(|&e| (e.source() != keep))
| ^ ^
|
help: remove these parentheses
|
158 - .filter(|&e| (e.source() != keep))
158 + .filter(|&e| e.source() != keep)
|
warning: `homotopy-core` (lib) generated 2 warnings (run `cargo fix --lib -p homotopy-core` to apply 2 suggestions)
Compiling homotopy-core v0.1.0 (/home/maximus/.env/extern/diagrammatic-semiotics/homotopy-rs/homotopy-core)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/examples/export_dim456`
=== Building dimension 4, 5, 6 test fixtures ===
lips: dim=4, size=1
padded_4: dim=4, size=2
as_5d: dim=5, size=1
source dim=4, size=2
as_6d: dim=6, size=1
source dim=5, size=1
=== Exporting fixtures ===
padded_4: 1076794 bytes
as_5d: 1154944 bytes
as_6d: 1233174 bytes

View file

@ -1,61 +0,0 @@
warning: unnecessary parentheses around closure body
--> homotopy-core/src/collapse.rs:135:22
|
135 | .filter(|&e| (e.target() != keep))
| ^ ^
|
= note: `#[warn(unused_parens)]` (part of `#[warn(unused)]`) on by default
help: remove these parentheses
|
135 - .filter(|&e| (e.target() != keep))
135 + .filter(|&e| e.target() != keep)
|
warning: unnecessary parentheses around closure body
--> homotopy-core/src/collapse.rs:158:22
|
158 | .filter(|&e| (e.source() != keep))
| ^ ^
|
help: remove these parentheses
|
158 - .filter(|&e| (e.source() != keep))
158 + .filter(|&e| e.source() != keep)
|
warning: `homotopy-core` (lib) generated 2 warnings (run `cargo fix --lib -p homotopy-core` to apply 2 suggestions)
Compiling homotopy-core v0.1.0 (/home/maximus/.env/extern/diagrammatic-semiotics/homotopy-rs/homotopy-core)
warning: unused import: `homotopy_core::common::Boundary`
--> homotopy-core/examples/export_dim4.rs:4:5
|
4 | use homotopy_core::common::Boundary;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default
warning: `homotopy-core` (example "export_dim4") generated 1 warning (run `cargo fix --example "export_dim4" -p homotopy-core` to apply 1 suggestion)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/examples/export_dim4`
=== Searching for dimension 4 examples ===
--- lips() ---
lips: dim=4, size=1
source dim=3
--- pants_unit() ---
pants_unit: dim=4, size=1
source dim=3
--- algebraic_snake() ---
algebraic_snake: dim=3, size=1
=== Approach 1: Padded half_braid lifted to dim 4 ===
half_braid: dim=3, size=1
padded_3d: dim=3, size=2
padded_4d: dim=4, size=0
=== Approach 2: Pad lips() at dim 4 ===
padded_lips: dim=4, size=2
(original lips size was 1, now 2)
=== Exporting padded_lips (dim 4 with identity cospan) ===

File diff suppressed because it is too large Load diff

View file

@ -1,41 +0,0 @@
warning: unnecessary parentheses around closure body
--> homotopy-core/src/collapse.rs:135:22
|
135 | .filter(|&e| (e.target() != keep))
| ^ ^
|
= note: `#[warn(unused_parens)]` (part of `#[warn(unused)]`) on by default
help: remove these parentheses
|
135 - .filter(|&e| (e.target() != keep))
135 + .filter(|&e| e.target() != keep)
|
warning: unnecessary parentheses around closure body
--> homotopy-core/src/collapse.rs:158:22
|
158 | .filter(|&e| (e.source() != keep))
| ^ ^
|
help: remove these parentheses
|
158 - .filter(|&e| (e.source() != keep))
158 + .filter(|&e| e.source() != keep)
|
warning: `homotopy-core` (lib) generated 2 warnings (run `cargo fix --lib -p homotopy-core` to apply 2 suggestions)
Compiling homotopy-core v0.1.0 (/home/maximus/.env/extern/diagrammatic-semiotics/homotopy-rs/homotopy-core)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
Running `target/debug/examples/export_essential`
=== Building essential identity scenario ===
two_scalars: dim=2, size=2
two_scalars_3d: dim=3, size=0
Attempting contraction...
Contraction failed: OutOfBounds
Fallback: using half_braid
half_braid: dim=3, size=1
padded_3d: dim=3, size=2
wrapped_4d: dim=4, size=1

File diff suppressed because it is too large Load diff

View file

@ -1,139 +0,0 @@
{
"metadata": {
"source": "half_braid.json",
"dimension": 3,
"total_points": 23,
"visible_points": 12,
"total_covers": 35
},
"vertices": [
{
"id": 21,
"label": "vertex_0",
"point": "s0,s0,s0",
"coords": [
0.0,
0.0,
-0.5
]
},
{
"id": 22,
"label": "vertex_1",
"point": "s1,s0,s0",
"coords": [
0.0,
0.0,
0.5
]
}
],
"wires": [
{
"id": 18,
"label": "wire_0",
"point": "r0,s0,s0",
"coords": [
0.0,
0.0,
-1.0
],
"endpoints": [
21,
22
],
"endpoint_coords": [
[
0.0,
0.0,
-0.5
],
[
0.0,
0.0,
0.5
]
]
},
{
"id": 19,
"label": "wire_1",
"point": "r1,s0,s0",
"coords": [
0.0,
0.0,
0.0
],
"endpoints": [
21,
22
],
"endpoint_coords": [
[
0.0,
0.0,
-0.5
],
[
0.0,
0.0,
0.5
]
]
},
{
"id": 20,
"label": "wire_2",
"point": "r2,s0,s0",
"coords": [
0.0,
0.0,
1.0
],
"endpoints": [
21,
22
],
"endpoint_coords": [
[
0.0,
0.0,
-0.5
],
[
0.0,
0.0,
0.5
]
]
}
],
"surfaces": [
{
"id": 16,
"label": "surface_0",
"point": "r0,r0,s0",
"coords": [
0.0,
-1.0,
-1.0
],
"boundary_wires": [
18
]
},
{
"id": 17,
"label": "surface_1",
"point": "r0,r1,s0",
"coords": [
0.0,
0.0,
-1.0
],
"boundary_wires": [
18
]
}
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,464 +0,0 @@
{
"padded_3d": {
"source": {
"DiagramN": {
"source": {
"DiagramN": {
"source": {
"Diagram0": {
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
}
},
"cospans": []
}
},
"cospans": [
{
"forward": {
"RewriteN": {
"dimension": 1,
"cones": [
{
"index": 0,
"internal": {
"source": [],
"target": {
"forward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
1
],
[
[]
]
]
]
},
"backward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
1
],
[
[]
]
]
]
}
},
"regular_slices": [
{
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
0
],
[
[
{
"Regular": 0
}
]
]
]
]
}
],
"singular_slices": []
}
}
]
}
},
"backward": {
"RewriteN": {
"dimension": 1,
"cones": [
{
"index": 0,
"internal": {
"source": [],
"target": {
"forward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
1
],
[
[]
]
]
]
},
"backward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
1
],
[
[]
]
]
]
}
},
"regular_slices": [
{
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
0
],
[
[
{
"Regular": 0
}
]
]
]
]
}
],
"singular_slices": []
}
}
]
}
}
}
]
}
},
"cospans": []
},
"scalar_3d": {
"source": {
"DiagramN": {
"source": {
"DiagramN": {
"source": {
"Diagram0": {
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
}
},
"cospans": []
}
},
"cospans": [
{
"forward": {
"RewriteN": {
"dimension": 1,
"cones": [
{
"index": 0,
"internal": {
"source": [],
"target": {
"forward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
1
],
[
[]
]
]
]
},
"backward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
1
],
[
[]
]
]
]
}
},
"regular_slices": [
{
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
0
],
[
[
{
"Regular": 0
}
]
]
]
]
}
],
"singular_slices": []
}
}
]
}
},
"backward": {
"RewriteN": {
"dimension": 1,
"cones": [
{
"index": 0,
"internal": {
"source": [],
"target": {
"forward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
1
],
[
[]
]
]
]
},
"backward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
1
],
[
[]
]
]
]
}
},
"regular_slices": [
{
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
0
],
[
[
{
"Regular": 0
}
]
]
]
]
}
],
"singular_slices": []
}
}
]
}
}
}
]
}
},
"cospans": []
}
}

View file

@ -1,42 +0,0 @@
warning: unnecessary parentheses around closure body
--> homotopy-core/src/collapse.rs:135:22
|
135 | .filter(|&e| (e.target() != keep))
| ^ ^
|
= note: `#[warn(unused_parens)]` (part of `#[warn(unused)]`) on by default
help: remove these parentheses
|
135 - .filter(|&e| (e.target() != keep))
135 + .filter(|&e| e.target() != keep)
|
warning: unnecessary parentheses around closure body
--> homotopy-core/src/collapse.rs:158:22
|
158 | .filter(|&e| (e.source() != keep))
| ^ ^
|
help: remove these parentheses
|
158 - .filter(|&e| (e.source() != keep))
158 + .filter(|&e| e.source() != keep)
|
warning: `homotopy-core` (lib) generated 2 warnings (run `cargo fix --lib -p homotopy-core` to apply 2 suggestions)
Compiling homotopy-core v0.1.0 (/home/maximus/.env/extern/diagrammatic-semiotics/homotopy-rs/homotopy-core)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.74s
Running `target/debug/examples/export_padded`
=== Original half_braid ===
dimension: 3
size: 1
=== Identity cospan ===
forward is_identity: true
backward is_identity: true
cospan is_identity: true
=== Padded diagram ===
dimension: 3
size: 2
last cospan is_identity: true

File diff suppressed because it is too large Load diff

View file

@ -1,226 +0,0 @@
{
"source": {
"DiagramN": {
"source": {
"Diagram0": {
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
}
},
"cospans": []
}
},
"cospans": [
{
"forward": {
"RewriteN": {
"dimension": 1,
"cones": [
{
"index": 0,
"internal": {
"source": [],
"target": {
"forward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
1
],
[
[]
]
]
]
},
"backward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
1
],
[
[]
]
]
]
}
},
"regular_slices": [
{
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
0
],
[
[
{
"Regular": 0
}
]
]
]
]
}
],
"singular_slices": []
}
}
]
}
},
"backward": {
"RewriteN": {
"dimension": 1,
"cones": [
{
"index": 0,
"internal": {
"source": [],
"target": {
"forward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
1
],
[
[]
]
]
]
},
"backward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
1
],
[
[]
]
]
]
}
},
"regular_slices": [
{
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
0
],
[
[
{
"Regular": 0
}
]
]
]
]
}
],
"singular_slices": []
}
}
]
}
}
}
]
}

View file

@ -1,68 +0,0 @@
warning: unnecessary parentheses around closure body
--> homotopy-core/src/collapse.rs:135:22
|
135 | .filter(|&e| (e.target() != keep))
| ^ ^
|
= note: `#[warn(unused_parens)]` (part of `#[warn(unused)]`) on by default
help: remove these parentheses
|
135 - .filter(|&e| (e.target() != keep))
135 + .filter(|&e| e.target() != keep)
|
warning: unnecessary parentheses around closure body
--> homotopy-core/src/collapse.rs:158:22
|
158 | .filter(|&e| (e.source() != keep))
| ^ ^
|
help: remove these parentheses
|
158 - .filter(|&e| (e.source() != keep))
158 + .filter(|&e| e.source() != keep)
|
warning: `homotopy-core` (lib) generated 2 warnings (run `cargo fix --lib -p homotopy-core` to apply 2 suggestions)
Compiling homotopy-core v0.1.0 (/home/maximus/.env/extern/diagrammatic-semiotics/homotopy-rs/homotopy-core)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.55s
Running `target/debug/examples/scan_identities`
=== Scanning homotopy-rs examples for identity cospans ===
--- Scanning lips() ---
lips: dim=4, size=1
--- Scanning pants_unit() ---
pants_unit: dim=4, size=1
--- Scanning touching() ---
touching: dim=3, size=2
--- Scanning crossing() ---
crossing: dim=3, size=2
--- Scanning algebraic_snake() ---
algebraic_snake: dim=3, size=1
--- Scanning bubble() ---
bubble: dim=2, size=2
--- Scanning snake() ---
snake: dim=2, size=2
=== Constructing potential essential identity scenario ===
half_braid: dim=3, size=1
half_braid.source: dim=2
size=2
dim3_with_id: dim=3, size=2
cospan[0] is_identity: false
cospan[1] is_identity: true
Attempting contraction on dim3_with_id...
Contraction succeeded!
result: dim=4, size=1
FOUND: source at depth=1, dim=3, size=2, identity_cospans=[1]/2, path=["source"]
=== Exporting contracted diagram ===

View file

@ -1,434 +0,0 @@
{
"source": {
"DiagramN": {
"source": {
"Diagram0": {
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
}
},
"cospans": []
}
},
"cospans": [
{
"forward": {
"RewriteN": {
"dimension": 1,
"cones": [
{
"index": 0,
"internal": {
"source": [],
"target": {
"forward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
1
],
[
[]
]
]
]
},
"backward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
1
],
[
[]
]
]
]
}
},
"regular_slices": [
{
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
0
],
[
[
{
"Regular": 0
}
]
]
]
]
}
],
"singular_slices": []
}
}
]
}
},
"backward": {
"RewriteN": {
"dimension": 1,
"cones": [
{
"index": 0,
"internal": {
"source": [],
"target": {
"forward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
1
],
[
[]
]
]
]
},
"backward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
1
],
[
[]
]
]
]
}
},
"regular_slices": [
{
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 1,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
0
],
[
[
{
"Regular": 0
}
]
]
]
]
}
],
"singular_slices": []
}
}
]
}
}
},
{
"forward": {
"RewriteN": {
"dimension": 1,
"cones": [
{
"index": 0,
"internal": {
"source": [],
"target": {
"forward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 2,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
1
],
[
[]
]
]
]
},
"backward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 2,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
1
],
[
[]
]
]
]
}
},
"regular_slices": [
{
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 2,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
0
],
[
[
{
"Regular": 0
}
]
]
]
]
}
],
"singular_slices": []
}
}
]
}
},
"backward": {
"RewriteN": {
"dimension": 1,
"cones": [
{
"index": 0,
"internal": {
"source": [],
"target": {
"forward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 2,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Source",
1
],
[
[]
]
]
]
},
"backward": {
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 2,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
1
],
[
[]
]
]
]
}
},
"regular_slices": [
{
"Rewrite0": [
{
"generator": {
"id": 0,
"dimension": 0
},
"orientation": "Positive"
},
{
"generator": {
"id": 2,
"dimension": 2
},
"orientation": "Positive"
},
[
[
"Target",
0
],
[
[
{
"Regular": 0
}
]
]
]
]
}
],
"singular_slices": []
}
}
]
}
}
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -74,77 +74,40 @@ impl DiagramN {
/// ///
/// For an identity (length 0), this is the same as source. /// For an identity (length 0), this is the same as source.
/// Otherwise, we traverse the rewrites to find the final regular slice. /// 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 { pub fn target(&self) -> Diagram {
self.regular_slice(self.cospans.len()) // TODO: Implement proper slice computation through rewrites
.expect("target should always be computable") // 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()
}
} }
/// Get the regular slice at height h. /// Get the regular slice at height h.
/// ///
/// Regular slices r₀, r₁, ..., rₙ where n = number of cospans: /// - h = 0: source
/// - r₀ = source /// - h > 0: computed by applying rewrites
/// - 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> { pub fn regular_slice(&self, h: usize) -> Option<Diagram> {
if h > self.cospans.len() { if h == 0 {
return None; Some((*self.source).clone())
} else if h <= self.cospans.len() {
// TODO: Compute via rewrite application
None
} else {
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. /// 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> { pub fn singular_slice(&self, h: usize) -> Option<Diagram> {
if h >= self.cospans.len() { if h < self.cospans.len() {
return None; // TODO: Compute via cospan apex
None
} else {
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))
} }
} }
@ -166,12 +129,9 @@ impl Cospan {
Self { forward, backward } Self { forward, backward }
} }
/// Check if this is an identity cospan (both legs are trivially identity). /// Check if this is an identity cospan (both legs are isomorphisms).
///
/// Uses `is_trivial()` which recognizes both `Rewrite::Identity` and
/// `RewriteN` with empty cones (as serialized by homotopy-rs).
pub fn is_identity(&self) -> bool { pub fn is_identity(&self) -> bool {
self.forward.is_trivial() && self.backward.is_trivial() self.forward.is_identity() && self.backward.is_identity()
} }
} }
@ -192,32 +152,11 @@ pub enum Rewrite {
} }
impl Rewrite { impl Rewrite {
/// Check if this is explicitly marked as an identity rewrite. /// Check if this is 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 { pub fn is_identity(&self) -> bool {
matches!(self, Rewrite::Identity) 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. /// The dimension of this rewrite.
pub fn dimension(&self) -> usize { pub fn dimension(&self) -> usize {
match self { match self {
@ -226,64 +165,6 @@ impl Rewrite {
Rewrite::RewriteN(r) => r.dimension, 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). /// An n-dimensional rewrite (n > 0).
@ -312,59 +193,6 @@ impl RewriteN {
cones: vec![], 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: atomic rewrite data.
@ -382,9 +210,7 @@ pub struct Cone {
pub source: Vec<Cospan>, pub source: Vec<Cospan>,
/// Target cospan (what the source contracts to) /// Target cospan (what the source contracts to)
pub target: Cospan, pub target: Cospan,
/// Slice rewrites for each source singular height. /// Slice rewrites for each interior boundary
/// Length equals source.len() (one per source cospan apex).
/// Matches homotopy-rs singular_slices convention.
pub slices: Vec<Rewrite>, pub slices: Vec<Rewrite>,
} }
@ -403,136 +229,6 @@ impl Cone {
pub fn source_size(&self) -> usize { pub fn source_size(&self) -> usize {
self.source.len() 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 === // === Diagram methods ===
@ -642,125 +338,8 @@ impl DiagramMap {
pub fn is_identity(&self) -> bool { pub fn is_identity(&self) -> bool {
self.rewrite.is_identity() 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -769,14 +348,6 @@ mod tests {
Generator::new(0, 0, false) 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] #[test]
fn test_diagram_0() { fn test_diagram_0() {
let g = test_generator(); let g = test_generator();
@ -801,340 +372,4 @@ mod tests {
let c = Cospan::new(Rewrite::Identity, Rewrite::Identity); let c = Cospan::new(Rewrite::Identity, Rewrite::Identity);
assert!(c.is_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());
}
} }

View file

@ -27,7 +27,7 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::{HashSet, HashMap, VecDeque}; use std::collections::{HashSet, HashMap, VecDeque};
use crate::diagram::{Cone, Diagram, Rewrite, RewriteN}; use crate::diagram::Diagram;
/// A height label in a diagram: either regular or singular. /// A height label in a diagram: either regular or singular.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@ -120,19 +120,6 @@ impl Point {
labels.push(label); labels.push(label);
Self(labels) Self(labels)
} }
/// Check if this point is visible for rendering.
///
/// A point at geometric dimension d is visible iff coords[d..] are all singular.
/// This matches homotopy.io's visibility filter (mesh.rs:111-115).
///
/// Geometric dimension = total_dim - singular_count, where singular_count
/// is the number of singular labels in the point.
pub fn is_visible(&self, total_dim: usize) -> bool {
let singular_count = self.0.iter().filter(|h| h.is_singular()).count();
let geom_dim = total_dim - singular_count;
self.0.iter().skip(geom_dim).all(|h| h.is_singular())
}
} }
/// A poset: a set with a partial order. /// A poset: a set with a partial order.
@ -345,16 +332,12 @@ pub fn k_points(diagram: &Diagram, k: usize) -> Poset<Point> {
// Store the sub-posets for each height for later reference // Store the sub-posets for each height for later reference
let mut sub_posets: HashMap<HeightLabel, Poset<Point>> = HashMap::new(); let mut sub_posets: HashMap<HeightLabel, Poset<Point>> = HashMap::new();
// Store slice lengths (number of cospans) for computing correspondences
let mut slice_lengths: HashMap<HeightLabel, usize> = HashMap::new();
// Add points from regular heights // Add points from regular heights
for j in 0..=d.length() { for j in 0..=d.length() {
let label = HeightLabel::Regular(j); let label = HeightLabel::Regular(j);
// TODO: Proper slice computation - currently regular_slice returns None for j > 0 // TODO: Proper slice computation - currently regular_slice returns None for j > 0
// For now, use source for all regular slices as a structural placeholder // For now, use source for all regular slices as a structural placeholder
let slice = d.regular_slice(j).unwrap_or_else(|| (*d.source).clone()); let slice = d.regular_slice(j).unwrap_or_else(|| (*d.source).clone());
slice_lengths.insert(label, slice.length());
let sub_points = k_points(&slice, k - 1); let sub_points = k_points(&slice, k - 1);
for (sub_idx, sub_point) in sub_points.elements().iter().enumerate() { for (sub_idx, sub_point) in sub_points.elements().iter().enumerate() {
@ -371,7 +354,6 @@ pub fn k_points(diagram: &Diagram, k: usize) -> Poset<Point> {
// TODO: Proper slice computation - currently singular_slice returns None // TODO: Proper slice computation - currently singular_slice returns None
// For now, use source as a structural placeholder // For now, use source as a structural placeholder
let slice = d.singular_slice(i).unwrap_or_else(|| (*d.source).clone()); let slice = d.singular_slice(i).unwrap_or_else(|| (*d.source).clone());
slice_lengths.insert(label, slice.length());
let sub_points = k_points(&slice, k - 1); let sub_points = k_points(&slice, k - 1);
for (sub_idx, sub_point) in sub_points.elements().iter().enumerate() { for (sub_idx, sub_point) in sub_points.elements().iter().enumerate() {
@ -434,43 +416,39 @@ pub fn k_points(diagram: &Diagram, k: usize) -> Poset<Point> {
// Determine which points are related via the cospan map // Determine which points are related via the cospan map
// //
// The cospan structure gives us maps: // The cospan structure gives us maps:
// - forward: rⱼ → sⱼ (rewrite maps lower → upper) // - forward: rⱼ → sⱼ
// - backward: rⱼ₊₁ → sⱼ (rewrite maps upper → lower) // - backward: rⱼ₊₁ → sⱼ
// //
// These induce maps on sub-points. For a covering relation, // These induce maps on sub-points. For a covering relation,
// we need to identify which sub-points map to each other. // we need to identify which sub-points map to each other.
//
// TODO: When proper cospan data is available, use it here.
// For now, we use a structural heuristic: if both sub-posets
// have the same structure (same number of points), we assume
// corresponding points are related.
// Get the rewrite, direction, and source cospan count // Simplified case: when sub-posets have matching structure,
let (rewrite, forward_direction, source_cospan_count) = match (lower_label, upper_label) { // connect corresponding points
// r_j → s_j: forward rewrite of cospan j if lower_sub_poset.len() == upper_sub_poset.len() {
// Forward direction: rewrite maps lower (source) → upper (target) for sub_idx in 0..lower_sub_poset.len() {
(HeightLabel::Regular(j), HeightLabel::Singular(sj)) if j == sj => { if let (Some(&lower_idx), Some(&upper_idx)) =
let src_len = slice_lengths.get(&lower_label).copied().unwrap_or(0); (point_map.get(&(lower_label, sub_idx)),
(&d.cospans[j].forward, true, src_len) point_map.get(&(upper_label, sub_idx)))
{
poset.add_cover(lower_idx, upper_idx);
} }
// s_j → r_{j+1}: backward rewrite of cospan j
// Backward direction: rewrite maps upper (r_{j+1}) → lower (s_j)
(HeightLabel::Singular(j), HeightLabel::Regular(rj)) if rj == j + 1 => {
let src_len = slice_lengths.get(&upper_label).copied().unwrap_or(0);
(&d.cospans[j].backward, false, src_len)
} }
_ => { } else {
// Shouldn't happen for valid adjacent heights // When sub-posets have different sizes, the cospan maps
continue; // contract or expand the structure. Without actual cospan data,
} // we can't determine the exact correspondence.
}; //
// For structural completeness, connect all points at the
// Compute the correspondence between sub-points based on the rewrite // lower height to all points at the upper height.
let correspondences = compute_subpoint_correspondence( // This is an over-approximation of the covering relation.
rewrite, // TODO: Refine this when cospan data is available.
lower_sub_poset, for lower_sub_idx in 0..lower_sub_poset.len() {
upper_sub_poset, for upper_sub_idx in 0..upper_sub_poset.len() {
forward_direction,
source_cospan_count,
);
// Add covers for corresponding points
for (lower_sub_idx, upper_sub_idx) in correspondences {
if let (Some(&lower_idx), Some(&upper_idx)) = if let (Some(&lower_idx), Some(&upper_idx)) =
(point_map.get(&(lower_label, lower_sub_idx)), (point_map.get(&(lower_label, lower_sub_idx)),
point_map.get(&(upper_label, upper_sub_idx))) point_map.get(&(upper_label, upper_sub_idx)))
@ -480,6 +458,8 @@ pub fn k_points(diagram: &Diagram, k: usize) -> Poset<Point> {
} }
} }
} }
}
}
poset poset
} }
@ -487,236 +467,6 @@ pub fn k_points(diagram: &Diagram, k: usize) -> Poset<Point> {
} }
} }
/// Compute correspondences between points in adjacent sub-posets based on the rewrite.
///
/// # Arguments
/// * `rewrite` - The rewrite between slices
/// * `lower_sub_poset` - Points at the lower height
/// * `upper_sub_poset` - Points at the upper height
/// * `forward_direction` - true if rewrite maps lower→upper (forward rewrite),
/// false if rewrite maps upper→lower (backward rewrite)
/// * `source_cospan_count` - Number of cospans in the rewrite's source sub-diagram
///
/// Returns pairs (lower_sub_idx, upper_sub_idx) indicating which points should be connected.
fn compute_subpoint_correspondence(
rewrite: &Rewrite,
lower_sub_poset: &Poset<Point>,
upper_sub_poset: &Poset<Point>,
forward_direction: bool,
source_cospan_count: usize,
) -> Vec<(usize, usize)> {
// CASE 1: Identity or trivial rewrite (empty cones)
// Points correspond 1-to-1 by index
if rewrite.is_trivial() {
let min_len = std::cmp::min(lower_sub_poset.len(), upper_sub_poset.len());
return (0..min_len).map(|i| (i, i)).collect();
}
// CASE 2: RewriteN with cones
if let Rewrite::RewriteN(r) = rewrite {
return build_cone_correspondence(
r,
lower_sub_poset,
upper_sub_poset,
forward_direction,
source_cospan_count,
);
}
// CASE 3: Rewrite0 or other - 1-to-1 correspondence
let min_len = std::cmp::min(lower_sub_poset.len(), upper_sub_poset.len());
(0..min_len).map(|i| (i, i)).collect()
}
/// Build correspondence based on cone structure.
fn build_cone_correspondence(
rewrite: &RewriteN,
lower_sub_poset: &Poset<Point>,
upper_sub_poset: &Poset<Point>,
forward_direction: bool,
source_cospan_count: usize,
) -> Vec<(usize, usize)> {
// Build both singular and regular height maps
// Source has source_cospan_count singular heights (0..source_cospan_count)
// and source_cospan_count + 1 regular heights (0..=source_cospan_count)
let (singular_map, regular_map) = compute_height_maps(&rewrite.cones, source_cospan_count);
let mut correspondences = Vec::new();
for (lower_idx, lower_point) in lower_sub_poset.elements().iter().enumerate() {
for (upper_idx, upper_point) in upper_sub_poset.elements().iter().enumerate() {
let corresponds = if forward_direction {
// Forward: lower is source, upper is target
points_correspond_forward(lower_point, upper_point, &singular_map, &regular_map)
} else {
// Backward: rewrite maps upper→lower, but cover goes lower→upper
// So we check if upper (source) maps to lower (target)
points_correspond_forward(upper_point, lower_point, &singular_map, &regular_map)
};
if corresponds {
correspondences.push((lower_idx, upper_idx));
}
}
}
correspondences
}
/// Compute how heights map from source to target based on cones.
///
/// Returns (singular_map, regular_map) where:
/// - singular_map[src] = Some(tgt) if source singular src maps to target singular tgt
/// - regular_map[src] = Some(tgt) if source regular src maps to target regular tgt
/// - None means the height is absorbed/removed by a contraction
fn compute_height_maps(
cones: &[Cone],
source_cospan_count: usize,
) -> (Vec<Option<usize>>, Vec<Option<usize>>) {
// Source has:
// - source_cospan_count singular heights (indices 0..source_cospan_count)
// - source_cospan_count + 1 regular heights (indices 0..=source_cospan_count)
let mut singular_map: Vec<Option<usize>> = (0..source_cospan_count).map(Some).collect();
let mut regular_map: Vec<Option<usize>> = (0..=source_cospan_count).map(Some).collect();
if cones.is_empty() {
return (singular_map, regular_map);
}
// Sort cones by target index
let mut sorted_cones: Vec<&Cone> = cones.iter().collect();
sorted_cones.sort_by_key(|c| c.index);
// Process cones to build the mapping
// Each cone describes: source cospans at [src_start, src_start + cone.source.len())
// collapse to target cospan at cone.index
//
// We need to track the cumulative offset to convert between source and target indices
let mut cumulative_source_offset = 0usize; // How many extra source cospans we've seen
for cone in &sorted_cones {
if cone.source.is_empty() {
// INSERTION: a new cospan appears at cone.index in target
// This doesn't consume source cospans but shifts target indices
// All source heights at >= cone.index need their target shifted by +1
for src in 0..source_cospan_count {
if let Some(tgt) = singular_map[src] {
if tgt >= cone.index {
singular_map[src] = Some(tgt + 1);
}
}
}
for src in 0..=source_cospan_count {
if let Some(tgt) = regular_map[src] {
// Regular heights shift when target index > cone.index
if tgt > cone.index {
regular_map[src] = Some(tgt + 1);
}
}
}
} else {
// CONTRACTION: source cospans [src_start, src_end) collapse to target cone.index
let src_start = cone.index + cumulative_source_offset;
let src_end = src_start + cone.source.len();
// Singular heights in [src_start, src_end) all map to cone.index
for src in src_start..src_end {
if src < source_cospan_count {
singular_map[src] = Some(cone.index);
}
}
// Singular heights after src_end shift down by (cone.source.len() - 1)
let shift = cone.source.len() - 1;
for src in src_end..source_cospan_count {
if let Some(tgt) = singular_map[src] {
singular_map[src] = Some(tgt - shift);
}
}
// Regular heights:
// - r_src_start maps to r_cone.index (left boundary)
// - r_{src_start+1} through r_{src_end-1} are ABSORBED (interior boundaries)
// - r_src_end maps to r_{cone.index+1} (right boundary)
// - r_{src_end+1}.. shift down
// Interior regular heights are absorbed
for src in (src_start + 1)..src_end {
if src <= source_cospan_count {
regular_map[src] = None;
}
}
// r_src_end maps to r_{cone.index + 1}
if src_end <= source_cospan_count {
regular_map[src_end] = Some(cone.index + 1);
}
// Regular heights after src_end shift down
for src in (src_end + 1)..=source_cospan_count {
if let Some(tgt) = regular_map[src] {
regular_map[src] = Some(tgt - shift);
}
}
cumulative_source_offset += cone.source.len() - 1;
}
}
(singular_map, regular_map)
}
/// Check if a source point maps to a target point under the height mappings.
///
/// Points have labels ordered [inner, ..., outer]. The rewrite affects the OUTERMOST
/// dimension (last label). Inner dimensions must match exactly.
fn points_correspond_forward(
source: &Point,
target: &Point,
singular_map: &[Option<usize>],
regular_map: &[Option<usize>],
) -> bool {
let depth = source.depth();
// Must have same depth
if depth != target.depth() {
return false;
}
// Empty points always correspond
if depth == 0 {
return true;
}
// Inner dimensions (all but last) must match exactly
for i in 0..(depth - 1) {
if source.0[i] != target.0[i] {
return false;
}
}
// Outermost dimension (last label) must align under the mapping
let src_outer = &source.0[depth - 1];
let tgt_outer = &target.0[depth - 1];
match (src_outer, tgt_outer) {
(HeightLabel::Regular(s), HeightLabel::Regular(t)) => {
// Check regular height mapping
regular_map.get(*s).and_then(|&m| m).map(|mapped| mapped == *t).unwrap_or(false)
}
(HeightLabel::Singular(s), HeightLabel::Singular(t)) => {
// Check singular height mapping
singular_map.get(*s).and_then(|&m| m).map(|mapped| mapped == *t).unwrap_or(false)
}
_ => {
// Mixed regular/singular don't correspond
false
}
}
}
/// Compute the full explosion of a diagram. /// Compute the full explosion of a diagram.
/// ///
/// Returns Ptₙ(X) where n = dimension(X). /// Returns Ptₙ(X) where n = dimension(X).

File diff suppressed because it is too large Load diff

View file

@ -36,7 +36,6 @@ pub mod normalise;
pub mod typecheck; pub mod typecheck;
pub mod explosion; pub mod explosion;
pub mod layout; pub mod layout;
pub mod import;
// Re-exports for convenience // Re-exports for convenience
pub use monotone::MonotoneMap; pub use monotone::MonotoneMap;

View file

@ -4,14 +4,14 @@
//! the poset of degeneracy subobjects of a diagram T. This removes all //! the poset of degeneracy subobjects of a diagram T. This removes all
//! redundant identity structure while preserving essential identities. //! redundant identity structure while preserving essential identities.
//! //!
//! Key insight: In dimension >= 4, some identity cospans are ESSENTIAL - //! Key insight: In dimension ≥ 4, some identity cospans are ESSENTIAL —
//! removing them would make zigzag maps ill-defined (no monotone function //! removing them would make zigzag maps ill-defined (no monotone function
//! of the required type exists). The algorithm detects and preserves these. //! of the required type exists). The algorithm detects and preserves these.
//! //!
//! # Algorithm Overview (Construction 17) //! # Algorithm Overview (Construction 17)
//! //!
//! Input: A sink S = (T, {fi: Ai -> T}) //! Input: A sink S = (T, {fᵢ: Aᵢ → T})
//! Output: Degeneracy d: N -> T and factorisations Ai -> N //! Output: Degeneracy d: N → T and factorisations Aᵢ → N
//! //!
//! 1. Base case (dim 0): d = identity //! 1. Base case (dim 0): d = identity
//! 2. Recursive case: //! 2. Recursive case:
@ -19,16 +19,16 @@
//! b. Normalise at each singular height (recursive, including cospan legs) //! b. Normalise at each singular height (recursive, including cospan legs)
//! c. Assemble into zigzag P with parallel degeneracy dP //! c. Assemble into zigzag P with parallel degeneracy dP
//! d. Remove trivial cospans not in image of any sink map //! d. Remove trivial cospans not in image of any sink map
//! e. Compose: d = dP o dS //! e. Compose: d = dP dS
use crate::diagram::{Diagram, DiagramN, DiagramMap, Rewrite, Cospan, RewriteN, Cone}; use crate::diagram::{Diagram, DiagramN, DiagramMap, Rewrite};
/// Result of normalising a diagram (or sink). /// Result of normalising a diagram (or sink).
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct NormalisationResult { pub struct NormalisationResult {
/// The normalised diagram N /// The normalised diagram N
pub normal_form: Diagram, pub normal_form: Diagram,
/// The degeneracy map d: N -> T /// The degeneracy map d: N T
pub degeneracy: DiagramMap, pub degeneracy: DiagramMap,
/// Factorisations of each sink map through the degeneracy /// Factorisations of each sink map through the degeneracy
pub factorisations: Vec<DiagramMap>, pub factorisations: Vec<DiagramMap>,
@ -61,11 +61,9 @@ impl<'a> Sink<'a> {
} }
} }
/// Proposition 19: Normalise a sink (Construction 17). /// Normalise a sink (Construction 17).
/// ///
/// This is the core normalisation algorithm from the LICS 2022 paper. /// 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 /// # Arguments
/// * `sink` - The sink to normalise (target diagram + incoming maps) /// * `sink` - The sink to normalise (target diagram + incoming maps)
@ -73,8 +71,8 @@ impl<'a> Sink<'a> {
/// # Returns /// # Returns
/// A `NormalisationResult` containing: /// A `NormalisationResult` containing:
/// - The normal form N /// - The normal form N
/// - The degeneracy d: N -> T /// - The degeneracy d: N T
/// - Factorisations Ai -> N for each sink map /// - Factorisations Aᵢ → N for each sink map
pub fn normalise_sink(sink: &Sink) -> NormalisationResult { pub fn normalise_sink(sink: &Sink) -> NormalisationResult {
match sink.target { match sink.target {
Diagram::Diagram0(_) => { Diagram::Diagram0(_) => {
@ -92,15 +90,13 @@ pub fn normalise_sink(sink: &Sink) -> NormalisationResult {
} }
} }
/// Construction 17: Normalise an n-dimensional diagram (n > 0). /// 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 { fn normalise_sink_n(target: &DiagramN, sink_maps: &[DiagramMap]) -> NormalisationResult {
// Step 1: Normalise at each regular height // Step 1: Normalise at each regular height
let regular_normalisations = normalise_regular_heights(target, sink_maps); let regular_normalisations = normalise_regular_heights(target, sink_maps);
// Step 2: Normalise at each singular height // Step 2: Normalise at each singular height
// CRITICAL: Include P(rh) -> T(rh) -> T(sh) composites in each sink // CRITICAL: Include P(rₕ) → T(rₕ) → T(sₕ) composites in each sink
let singular_normalisations = normalise_singular_heights( let singular_normalisations = normalise_singular_heights(
target, target,
sink_maps, sink_maps,
@ -112,7 +108,6 @@ fn normalise_sink_n(target: &DiagramN, sink_maps: &[DiagramMap]) -> Normalisatio
target, target,
&regular_normalisations, &regular_normalisations,
&singular_normalisations, &singular_normalisations,
sink_maps,
); );
// Step 4: Remove trivial cospans not in image of any sink map // Step 4: Remove trivial cospans not in image of any sink map
@ -124,8 +119,8 @@ fn normalise_sink_n(target: &DiagramN, sink_maps: &[DiagramMap]) -> Normalisatio
&assembled_factorisations, &assembled_factorisations,
); );
// Step 5: Compose degeneracies d = dP o dS // Step 5: Compose degeneracies d = dP dS
let degeneracy = compose_degeneracies(&d_simple, &d_parallel); let degeneracy = compose_degeneracies(&d_parallel, &d_simple);
NormalisationResult { NormalisationResult {
normal_form: n, normal_form: n,
@ -135,7 +130,6 @@ fn normalise_sink_n(target: &DiagramN, sink_maps: &[DiagramMap]) -> Normalisatio
} }
/// Intermediate result for regular height normalisation. /// Intermediate result for regular height normalisation.
#[derive(Debug, Clone)]
struct RegularNormalisation { struct RegularNormalisation {
/// Normalised diagram at this regular height /// Normalised diagram at this regular height
normal_form: Diagram, normal_form: Diagram,
@ -145,12 +139,7 @@ struct RegularNormalisation {
factorisations: Vec<DiagramMap>, factorisations: Vec<DiagramMap>,
} }
/// Construction 17, Step 1: Normalise at each regular height. /// Normalise at each regular height of the diagram.
///
/// 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( fn normalise_regular_heights(
target: &DiagramN, target: &DiagramN,
sink_maps: &[DiagramMap], sink_maps: &[DiagramMap],
@ -159,24 +148,20 @@ fn normalise_regular_heights(
let mut results = Vec::with_capacity(num_regular); let mut results = Vec::with_capacity(num_regular);
for h in 0..num_regular { for h in 0..num_regular {
// Get the regular slice T(rh) // Get the regular slice T(r)
let t_r_h = target.regular_slice(h).unwrap_or_else(|| { let t_r_h = target.regular_slice(h).unwrap_or_else(|| {
// Fallback to source if slice computation not available // Fallback to source if slice computation not implemented
(*target.source).clone() (*target.source).clone()
}); });
// Collect sink maps restricted to this regular height // Collect sink maps restricted to this regular height
// Each fi(rh): Ai(r_{fi^r(h)}) -> T(rh) // Each fᵢ(rₕ): Aᵢ(r_{fᵢʳ(h)}) → T(rₕ)
// The regular map fi^r is derived from the singular map via Wraith's R
let restricted_maps: Vec<DiagramMap> = sink_maps let restricted_maps: Vec<DiagramMap> = sink_maps
.iter() .iter()
.map(|sink_map| { .map(|_| DiagramMap::identity(&t_r_h))
// Extract the slice of the sink map at this regular height
extract_regular_slice_map(sink_map, h)
})
.collect(); .collect();
// Recursively normalise this lower-dimensional sink // Recursively normalise
let sub_sink = Sink::new(&t_r_h, restricted_maps); let sub_sink = Sink::new(&t_r_h, restricted_maps);
let sub_result = normalise_sink(&sub_sink); let sub_result = normalise_sink(&sub_sink);
@ -190,48 +175,25 @@ fn normalise_regular_heights(
results 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. /// Intermediate result for singular height normalisation.
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct SingularNormalisation { struct SingularNormalisation {
/// Normalised diagram at this singular height /// Normalised diagram at this singular height
normal_form: Diagram, normal_form: Diagram,
/// Degeneracy from normal form to original /// Degeneracy from normal form to original
degeneracy: DiagramMap, degeneracy: DiagramMap,
/// Forward cospan leg from left regular (P(rh) -> P(sh)) /// Forward cospan leg from left regular
forward_leg: DiagramMap, forward_leg: DiagramMap,
/// Backward cospan leg from right regular (P(r{h+1}) -> P(sh)) /// Backward cospan leg from right regular
backward_leg: DiagramMap, backward_leg: DiagramMap,
/// Factorisations for each sink map at this height /// Factorisations for each sink map at this height
factorisations: Vec<DiagramMap>, factorisations: Vec<DiagramMap>,
} }
/// Construction 17, Step 2: Normalise at each singular height (with cospan legs in sink). /// Normalise at each singular height of the diagram.
/// ///
/// CRITICAL: The sink at each singular height includes: /// CRITICAL: The sink at each singular height includes:
/// - Direct singular maps from sink: fi(st) for t in (fi^s)^{-1}(h) /// - Direct singular maps from sink: fᵢ(sₜ) for t ∈ (fᵢˢ)⁻¹(h)
/// - Cospan legs: P(rh) -> T(rh) -> T(sh) and P(r{h+1}) -> T(r{h+1}) -> T(sh) /// - Cospan legs: P(rₕ) → T(rₕ) → T(sₕ) and P(rₕ₊₁) → T(rₕ₊₁) → T(sₕ)
///
/// The cospan leg composites are essential for preserving the zigzag structure.
fn normalise_singular_heights( fn normalise_singular_heights(
target: &DiagramN, target: &DiagramN,
sink_maps: &[DiagramMap], sink_maps: &[DiagramMap],
@ -241,151 +203,71 @@ fn normalise_singular_heights(
let mut results = Vec::with_capacity(num_singular); let mut results = Vec::with_capacity(num_singular);
for h in 0..num_singular { for h in 0..num_singular {
// Get the singular slice T(sh) // Get the singular slice T(s)
let t_s_h = target.singular_slice(h).unwrap_or_else(|| { let t_s_h = target.singular_slice(h).unwrap_or_else(|| {
// Fallback to source if slice computation not available // Fallback if slice computation not implemented
(*target.source).clone() (*target.source).clone()
}); });
// Build the sink for this singular height: // Build the sink for this singular height:
// 1. Direct maps from sink_maps
// 2. Composites P(rₕ) → T(rₕ) → T(sₕ)
// 3. Composites P(rₕ₊₁) → T(rₕ₊₁) → T(sₕ)
let mut combined_maps: Vec<DiagramMap> = Vec::new(); let mut combined_maps: Vec<DiagramMap> = Vec::new();
// 1. Direct maps from sink_maps: fi(st) for all t in preimage of h // Add direct singular maps from sink
for sink_map in sink_maps { for _sink_map in sink_maps {
// Extract singular slices that map to this height // TODO: Extract and add fᵢ(sₜ) for t in preimage of h
let preimage = get_singular_preimage(sink_map, h); combined_maps.push(DiagramMap::identity(&t_s_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) // Add cospan leg composites
// This is the composition of the regular degeneracy with the forward cospan leg // TODO: Compose regular normalisations with cospan structure
let forward_composite = compose_with_cospan_leg( combined_maps.push(regular_results[h].degeneracy.clone());
&regular_results[h].degeneracy, combined_maps.push(regular_results[h + 1].degeneracy.clone());
&target.cospans[h].forward,
);
combined_maps.push(forward_composite);
// 3. Cospan leg composite: P(r{h+1}) -> T(r{h+1}) -> T(sh) // Recursively normalise
// This is the composition of the regular degeneracy with the backward cospan leg let sub_sink = Sink::new(&t_s_h, combined_maps);
let backward_composite = compose_with_cospan_leg(
&regular_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); let sub_result = normalise_sink(&sub_sink);
// Extract the factorised cospan legs from the result let forward_leg = DiagramMap::identity(&sub_result.normal_form);
// The last two factorisations are for the cospan legs let backward_leg = DiagramMap::identity(&sub_result.normal_form);
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 { results.push(SingularNormalisation {
normal_form: sub_result.normal_form, normal_form: sub_result.normal_form,
degeneracy: sub_result.degeneracy, degeneracy: sub_result.degeneracy,
forward_leg, forward_leg,
backward_leg, backward_leg,
factorisations: sink_factorisations, factorisations: sub_result.factorisations,
}); });
} }
results results
} }
/// Helper for Construction 17, Step 2: Get the preimage of singular height h. /// Assemble regular and singular normalisations into a zigzag P.
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: /// Returns:
/// - P: the assembled diagram /// - P: the assembled diagram
/// - dP: the parallel degeneracy P -> T /// - dP: the parallel degeneracy P → T
/// - Assembled factorisations /// - Assembled factorisations
fn assemble( fn assemble(
target: &DiagramN, target: &DiagramN,
regular_results: &[RegularNormalisation], regular_results: &[RegularNormalisation],
singular_results: &[SingularNormalisation], singular_results: &[SingularNormalisation],
sink_maps: &[DiagramMap],
) -> (Diagram, DiagramMap, Vec<DiagramMap>) { ) -> (Diagram, DiagramMap, Vec<DiagramMap>) {
// Build cospans for P from the normalisation results // Build cospans from the normalisation results
// Each cospan has forward and backward legs computed from singular normalisation let cospans: Vec<crate::diagram::Cospan> = singular_results
let cospans: Vec<Cospan> = singular_results
.iter() .iter()
.map(|sr| { .map(|sr| {
// Convert the factorised legs to rewrites crate::diagram::Cospan::new(
Cospan::new(
sr.forward_leg.rewrite.clone(), sr.forward_leg.rewrite.clone(),
sr.backward_leg.rewrite.clone(), sr.backward_leg.rewrite.clone(),
) )
}) })
.collect(); .collect();
// The source of P is the normalised first regular slice // The source of P is the first regular normalisation
let source = regular_results let source = regular_results
.first() .first()
.map(|r| r.normal_form.clone()) .map(|r| r.normal_form.clone())
@ -393,98 +275,32 @@ fn assemble(
let p = Diagram::DiagramN(DiagramN::new(source, cospans)); let p = Diagram::DiagramN(DiagramN::new(source, cospans));
// Build the parallel degeneracy dP: P -> T // The parallel degeneracy is assembled from slice degeneracies
// This is assembled from the slice degeneracies // Since all slice maps are degeneracies, the assembled map is parallel
let d_parallel = build_parallel_degeneracy(regular_results, singular_results, target); let d_parallel = DiagramMap::new(Rewrite::Identity); // TODO: Proper assembly
// Assemble factorisations for each sink map // Assemble factorisations
// Each original sink map Ai -> T factors as Ai -> P -> T let factorisations = regular_results
let factorisations = assemble_factorisations( .first()
sink_maps, .map(|r| r.factorisations.clone())
regular_results, .unwrap_or_default();
singular_results,
);
(p, d_parallel, factorisations) (p, d_parallel, factorisations)
} }
/// Helper for Construction 17, Step 3: Build the parallel degeneracy dP. /// Remove trivial cospans from the assembled diagram P.
///
/// 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: /// A cospan at singular height h is removable iff:
/// 1. Both legs are isomorphisms (identity cospan) /// 1. Both legs are isomorphisms (identity cospan)
/// 2. h is NOT in the image of any sink map's singular map /// 2. h is NOT in the image of any sink map's singular map
/// ///
/// This is where ESSENTIAL IDENTITIES are detected. In dimension >= 4, /// This is where ESSENTIAL IDENTITIES are detected. In dimension ≥ 4,
/// some identity cospans must be preserved because removing them would /// some identity cospans must be preserved because removing them would
/// make the zigzag maps ill-defined. /// make the zigzag maps ill-defined.
/// ///
/// Returns: /// Returns:
/// - N: the diagram with trivial cospans removed /// - N: the diagram with trivial cospans removed
/// - dS: the simple degeneracy N -> P that re-inserts them /// - dS: the simple degeneracy N → P that re-inserts them
/// - Updated factorisations /// - Updated factorisations
fn remove_trivial_cospans( fn remove_trivial_cospans(
p: &Diagram, p: &Diagram,
@ -498,21 +314,17 @@ fn remove_trivial_cospans(
Diagram::DiagramN(diagram_n) => { Diagram::DiagramN(diagram_n) => {
// Identify which cospans are trivial (identity) and not in sink image // Identify which cospans are trivial (identity) and not in sink image
let mut kept_cospans = Vec::new(); let mut kept_cospans = Vec::new();
let mut removed_indices = Vec::new(); let _removed_indices = Vec::<usize>::new();
for (h, cospan) in diagram_n.cospans.iter().enumerate() { for (h, cospan) in diagram_n.cospans.iter().enumerate() {
let is_identity = cospan.is_identity(); let is_identity = cospan.is_identity();
let in_sink_image = is_in_sink_image(h, factorisations); let in_sink_image = is_in_sink_image(h, factorisations);
if !is_identity || in_sink_image { if !is_identity || in_sink_image {
// Keep this cospan: // Keep this cospan (either non-trivial or essential)
// - Either it's non-trivial (not identity), OR
// - It's essential (in the image of some sink map)
kept_cospans.push(cospan.clone()); kept_cospans.push(cospan.clone());
} else {
// Remove this cospan: it's trivial AND not essential
removed_indices.push(h);
} }
// If trivial AND not in sink image, it's removed
} }
// Build N with kept cospans // Build N with kept cospans
@ -521,132 +333,38 @@ fn remove_trivial_cospans(
kept_cospans, kept_cospans,
)); ));
// Build simple degeneracy dS: N -> P // Build simple degeneracy dS that re-inserts removed cospans
// This re-inserts the removed identity cospans at the correct positions let d_simple = DiagramMap::identity(&n); // TODO: Proper construction
let d_simple = build_simple_degeneracy(&n, p, &removed_indices);
// Update factorisations to account for removed cospans // Update factorisations to go through dS
let updated_factorisations = update_factorisations_for_removal( let updated_factorisations = factorisations.to_vec();
factorisations,
&removed_indices,
);
(n, d_simple, updated_factorisations) (n, d_simple, updated_factorisations)
} }
} }
} }
/// Helper for Construction 17, Step 4: Check if height h is in sink image. /// Check if singular height h is in the image of any sink map.
/// fn is_in_sink_image(_h: usize, _factorisations: &[DiagramMap]) -> bool {
/// A height is in the image if any factorisation has a non-trivial // TODO: Extract singular maps from factorisations and check if h is in image
/// map at that singular level (i.e., some Ai has content mapping to height h). // For now, conservatively return true (don't remove anything)
fn is_in_sink_image(h: usize, factorisations: &[DiagramMap]) -> bool { true
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. /// Compose two degeneracy maps.
/// fn compose_degeneracies(d_parallel: &DiagramMap, d_simple: &DiagramMap) -> DiagramMap {
/// A simple degeneracy is pi-cocartesian over a face map composition. // TODO: Proper composition
/// This implements the "simple then parallel" factorisation. if d_parallel.is_identity() {
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() d_simple.clone()
} else if d_simple.is_identity() {
d_parallel.clone()
} else { } else {
// Full composition needed // Full composition needed
d_simple.compose(d_parallel) d_parallel.clone()
} }
} }
/// Construction 17 (absolute case): Normalise with empty sink. /// Absolute normalisation: normalise with empty sink.
/// ///
/// This computes the smallest degeneracy subobject of the diagram, /// This computes the smallest degeneracy subobject of the diagram,
/// removing all redundant identity structure. /// removing all redundant identity structure.
@ -707,143 +425,4 @@ mod tests {
assert_eq!(once.normal_form, twice.normal_form); 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]));
}
} }

View file

@ -6,19 +6,8 @@
//! 2. Break the diagram into pieces (one per singular content element) //! 2. Break the diagram into pieces (one per singular content element)
//! 3. Normalise each piece //! 3. Normalise each piece
//! 4. Check that each normalised piece matches a signature element //! 4. Check that each normalised piece matches a signature element
//!
//! ## Piece extraction (Section 7 of the paper)
//!
//! For an n-diagram D, the "pieces" are sub-n-diagrams of the SAME DIMENSION as D,
//! each corresponding to one generator in the singular content. The algorithm:
//!
//! 1. Find all generators and their "paths" (sequence of singular heights to reach them)
//! 2. For each (path, generator), construct an Embedding from the path
//! 3. Use restrict_diagram to extract the sub-diagram for that embedding
//!
//! This is based on the homotopy-rs implementation in typecheck.rs.
use crate::diagram::{Diagram, DiagramN, Cospan, Rewrite, RewriteN, Cone}; use crate::diagram::Diagram;
use crate::signature::{Signature, Generator}; use crate::signature::{Signature, Generator};
use thiserror::Error; use thiserror::Error;
@ -53,313 +42,12 @@ pub enum TypeError {
/// A piece of singular content from a diagram. /// A piece of singular content from a diagram.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SingularPiece { pub struct SingularPiece {
/// The piece as a sub-diagram (SAME dimension as original) /// The piece as a sub-diagram
pub diagram: Diagram, pub diagram: Diagram,
/// Path to this piece in the original diagram (sequence of singular indices) /// Path to this piece in the original diagram (sequence of singular indices)
pub path: Vec<usize>, pub path: Vec<usize>,
} }
// =============================================================================
// Embedding: tracks how a generator sits inside a diagram
// =============================================================================
/// An embedding describes how a point (generator) is embedded in a diagram.
///
/// This is a tree structure that tracks the path through the cospan structure
/// to reach a particular generator.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Embedding {
/// Base case: at dimension 0, the embedding contains the point itself
Zero,
/// At a regular height: the embedding goes through a regular slice
/// and wraps in an identity cospan
Regular(usize, Box<Embedding>),
/// At a singular height: the embedding goes through singular slices
/// Each inner embedding corresponds to a slice in the range
Singular(usize, Vec<Embedding>),
}
impl Embedding {
/// Construct an embedding from a path of singular heights.
///
/// The path [h0, h1, h2] means: at the outermost level, go to singular
/// height h0; within that slice, go to singular height h1; etc.
pub fn from_point(point: &[usize]) -> Self {
let mut embedding = Self::Zero;
for &height in point.iter().rev() {
embedding = Self::Singular(height, vec![embedding]);
}
embedding
}
/// Compute the preimage of this embedding under a rewrite.
///
/// Given a rewrite f: A → B and an embedding into B, compute
/// the corresponding embedding into A.
pub fn preimage(&self, rewrite: &Rewrite) -> Self {
match self {
Self::Zero => Self::Zero,
Self::Regular(height, inner) => {
match rewrite {
Rewrite::Identity => self.clone(),
Rewrite::Rewrite0 { .. } => self.clone(),
Rewrite::RewriteN(rw) => {
// Map target regular height back to source regular height
let preimage_height = rw.regular_preimage(*height);
Self::Regular(preimage_height, inner.clone())
}
}
}
Self::Singular(height, slices) => {
match rewrite {
Rewrite::Identity => self.clone(),
Rewrite::Rewrite0 { .. } => self.clone(),
Rewrite::RewriteN(rw) => {
// Collect source heights and preimage slices from all singular
// heights in our range
let mut min_source_height: Option<usize> = None;
let preimage_slices: Vec<Embedding> = slices
.iter()
.enumerate()
.flat_map(|(target_offset, slice)| {
let target_height = height + target_offset;
rw.singular_preimage(target_height)
.into_iter()
.map(|source_height| {
// Track minimum source height for the result
min_source_height = Some(
min_source_height.map_or(source_height, |m| m.min(source_height))
);
slice.preimage(&rw.slice(source_height))
})
.collect::<Vec<_>>()
})
.collect();
if preimage_slices.is_empty() {
// This is an insertion: the cone has no source cospans
// Fall back to Regular embedding
let regular_preimage_height = rw.regular_preimage(*height);
if let Some(cone) = rw.cone_over_target(*height) {
// Use the forward leg of the target cospan
Self::Regular(
regular_preimage_height,
Box::new(slices[0].preimage(&cone.target.forward)),
)
} else {
Self::Regular(regular_preimage_height, Box::new(slices[0].clone()))
}
} else {
// Use the minimum source height as the preimage height
Self::Singular(min_source_height.unwrap_or(*height), preimage_slices)
}
}
}
}
}
}
}
// =============================================================================
// restrict_diagram: extract sub-diagram for an embedding
// =============================================================================
/// Restrict a diagram to the sub-diagram corresponding to an embedding.
///
/// The resulting diagram has the SAME dimension as the input, but only
/// contains the structure relevant to the embedded point.
pub fn restrict_diagram(diagram: &Diagram, embedding: &Embedding) -> Diagram {
match embedding {
Embedding::Zero => {
// Base case: return the 0-diagram as-is
debug_assert_eq!(diagram.dimension(), 0);
diagram.clone()
}
Embedding::Regular(height, inner) => {
// Take the regular slice at height, restrict recursively,
// then wrap in an identity cospan
match diagram {
Diagram::Diagram0(_) => diagram.clone(),
Diagram::DiagramN(d) => {
if let Some(slice) = d.regular_slice(*height) {
let restricted = restrict_diagram(&slice, inner);
Diagram::DiagramN(DiagramN::identity(restricted))
} else {
diagram.clone()
}
}
}
}
Embedding::Singular(height, slices) => {
match diagram {
Diagram::Diagram0(_) => diagram.clone(),
Diagram::DiagramN(d) => {
if d.cospans.is_empty() || *height + slices.len() > d.cospans.len() {
// Not enough cospans, return identity
return diagram.clone();
}
// Get the source for the restricted diagram
let regular_slice = d.regular_slice(*height)
.unwrap_or_else(|| (*d.source).clone());
// Compute the embedding for the source via preimage through forward
let source_embedding = slices[0].preimage(&d.cospans[*height].forward);
let restricted_source = restrict_diagram(&regular_slice, &source_embedding);
// Restrict each cospan in the range
let restricted_cospans: Vec<Cospan> = d.cospans[*height..*height + slices.len()]
.iter()
.enumerate()
.map(|(i, cospan)| {
let slice_embedding = &slices[i.min(slices.len() - 1)];
Cospan {
forward: restrict_rewrite(&cospan.forward, slice_embedding),
backward: restrict_rewrite(&cospan.backward, slice_embedding),
}
})
.collect();
Diagram::DiagramN(DiagramN::new(restricted_source, restricted_cospans))
}
}
}
}
}
// =============================================================================
// restrict_rewrite: restrict a rewrite to the preimage over a sub-diagram
// =============================================================================
/// Restrict a rewrite to the preimage over a sub-diagram of the target.
///
/// For a rewrite f: A → B and an embedding E into B, this produces
/// a rewrite f': A' → B' where A' and B' are the restricted diagrams.
pub fn restrict_rewrite(rewrite: &Rewrite, embedding: &Embedding) -> Rewrite {
if rewrite.is_trivial() {
return Rewrite::Identity;
}
match embedding {
Embedding::Zero => {
// At dimension 0, return the rewrite as-is
rewrite.clone()
}
Embedding::Regular(_, _) => {
// Regular embedding: the rewrite becomes identity
// (we're restricting to a passthrough region)
Rewrite::identity(rewrite.dimension())
}
Embedding::Singular(height, slices) => {
match rewrite {
Rewrite::Identity => Rewrite::Identity,
Rewrite::Rewrite0 { .. } => rewrite.clone(),
Rewrite::RewriteN(rw) => {
let mut restricted_cones: Vec<Cone> = Vec::new();
// Track cumulative offset to compute actual target positions.
// Cone indices in homotopy-rs are pre-offset; the actual target
// position is cone.index + offset, where offset accumulates as:
// offset += (1 - cone.len) for each cone (insertions add 1,
// contractions subtract (len-1)).
let mut offset: isize = 0;
// Also track the offset for the restricted output
let mut restricted_offset: isize = 0;
for cone in &rw.cones {
// Compute actual target position after accounting for previous cones
let actual_target = (cone.index as isize + offset) as usize;
// Update offset for this cone (even if we skip it)
offset += 1 - cone.len() as isize;
// Only include cones that target heights in our range
if actual_target < *height || actual_target >= height + slices.len() {
continue;
}
let slice_idx = actual_target - *height;
let slice_embedding = &slices[slice_idx.min(slices.len() - 1)];
// Restrict the singular slices
let restricted_singular_slices: Vec<Rewrite> = cone
.slices
.iter()
.map(|s| restrict_rewrite(s, slice_embedding))
.collect();
// Restrict source cospans
let restricted_source: Vec<Cospan> = cone
.source
.iter()
.enumerate()
.map(|(i, cospan)| {
let cone_slice = if i < cone.slices.len() {
&cone.slices[i]
} else {
&Rewrite::Identity
};
let inner_embedding = slice_embedding.preimage(cone_slice);
Cospan {
forward: restrict_rewrite(&cospan.forward, &inner_embedding),
backward: restrict_rewrite(&cospan.backward, &inner_embedding),
}
})
.collect();
// Restrict target cospan
let restricted_target = Cospan {
forward: restrict_rewrite(&cone.target.forward, slice_embedding),
backward: restrict_rewrite(&cone.target.backward, slice_embedding),
};
// Compute adjusted index for the restricted rewrite.
// The index is relative to the restricted output's current position.
let adjusted_index = (slice_idx as isize - restricted_offset) as usize;
// Capture length before moving
let restricted_source_len = restricted_source.len();
restricted_cones.push(Cone::new(
adjusted_index,
restricted_source,
restricted_target,
restricted_singular_slices,
));
// Update restricted offset
restricted_offset += 1 - restricted_source_len as isize;
}
if restricted_cones.is_empty() {
Rewrite::Identity
} else {
Rewrite::RewriteN(RewriteN::new(rw.dimension, restricted_cones))
}
}
}
}
}
}
impl Rewrite {
/// Create an identity rewrite at a given dimension.
pub fn identity(dimension: usize) -> Self {
if dimension == 0 {
Rewrite::Identity
} else {
Rewrite::RewriteN(RewriteN::identity(dimension))
}
}
}
/// Type check a diagram against a signature. /// Type check a diagram against a signature.
/// ///
/// # Arguments /// # Arguments
@ -432,32 +120,11 @@ fn extract_singular_content_recursive(
/// Extract pieces from a diagram. /// Extract pieces from a diagram.
/// ///
/// Each piece corresponds to one element of singular content, /// Each piece corresponds to one element of singular content,
/// extracted as a sub-n-diagram of the SAME DIMENSION as the original. /// extracted as a sub-diagram by taking preimages.
///
/// The algorithm:
/// 1. Find all generators and their paths via singular_content
/// 2. For each (path, generator), create an Embedding from the path
/// 3. Use restrict_diagram to extract the sub-diagram
pub fn extract_pieces(diagram: &Diagram) -> Vec<SingularPiece> { pub fn extract_pieces(diagram: &Diagram) -> Vec<SingularPiece> {
// Get the singular content with paths to each generator // For now, this is the same as singular content extraction
let content = extract_singular_content(diagram); // A full implementation would construct the actual sub-diagrams
extract_singular_content(diagram)
// For each piece of singular content, extract the restricted sub-diagram
content
.into_iter()
.map(|piece| {
// Build an embedding from the path
let embedding = Embedding::from_point(&piece.path);
// Restrict the diagram to this embedding
let restricted = restrict_diagram(diagram, &embedding);
SingularPiece {
diagram: restricted,
path: piece.path,
}
})
.collect()
} }
/// Check a single piece against the signature. /// Check a single piece against the signature.
@ -507,36 +174,6 @@ fn check_piece(piece: &SingularPiece, signature: &Signature, index: usize) -> Re
Ok(()) Ok(())
} }
/// Type check a piece against a slice of signature diagrams.
///
/// This normalises the piece and extracts the source at the generator's
/// dimension by stripping identity wrappings. The source is then compared
/// against the signature elements.
///
/// Returns true if the piece's core matches any signature element.
pub fn type_check_piece(piece: &Diagram, signature: &[Diagram]) -> bool {
use crate::normalise::normalise;
let result = normalise(piece);
let normalised = &result.normal_form;
// Extract the source at the generator's dimension
// by stripping identity wrappings until we hit non-trivial content
let mut d = normalised;
while let Diagram::DiagramN(dn) = d {
if dn.cospans.is_empty() {
// This is an identity diagram - descend to source
d = &dn.source;
} else {
// Non-trivial content at this level
// Check if source matches any signature element
return signature.iter().any(|sig_elem| dn.source.as_ref() == sig_elem);
}
}
// Dimension 0: check generator directly
signature.iter().any(|s| s == d)
}
impl Diagram { impl Diagram {
/// Type check this diagram against a signature. /// Type check this diagram against a signature.
pub fn type_check(&self, signature: &Signature) -> Result<(), TypeError> { pub fn type_check(&self, signature: &Signature) -> Result<(), TypeError> {
@ -552,11 +189,6 @@ impl Diagram {
pub fn pieces(&self) -> Vec<SingularPiece> { pub fn pieces(&self) -> Vec<SingularPiece> {
extract_pieces(self) extract_pieces(self)
} }
/// Type check a piece against signature diagrams.
pub fn type_check_piece(&self, signature: &[Diagram]) -> bool {
type_check_piece(self, signature)
}
} }
#[cfg(test)] #[cfg(test)]

File diff suppressed because it is too large Load diff

View file

@ -1,56 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zigzag Renderer - Half Braid Geometry</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #0a0a0f;
}
#root {
width: 100vw;
height: 100vh;
}
#loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f0c040;
font-family: monospace;
font-size: 18px;
}
</style>
</head>
<body>
<div id="root">
<div id="loading">Loading Three.js...</div>
</div>
<!-- React from CDN -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- Three.js from CDN -->
<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
<!-- Babel for JSX transformation (development only) -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- Our renderer component -->
<script type="text/babel" src="zigzag-renderer.jsx"></script>
<!-- Mount the app -->
<script type="text/babel">
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(ZigzagRenderer));
</script>
</body>
</html>

View file

@ -1,689 +0,0 @@
// Three.js renderer for zigzag-engine half_braid geometry
// Renders VISIBLE elements only from explosion poset
// Visibility: point at geom_dim d is visible iff coords[d..] are all singular
const GEOMETRY_DATA = {
"metadata": {
"source": "half_braid.json",
"dimension": 3,
"total_points": 23,
"visible_points": 7,
"total_covers": 35
},
"vertices": [
{ "id": 21, "label": "vertex_0", "point": "s0,s0,s0", "coords": [0.0, 0.0, -0.5] },
{ "id": 22, "label": "vertex_1", "point": "s1,s0,s0", "coords": [0.0, 0.0, 0.5] }
],
"wires": [
{ "id": 18, "label": "wire_0", "point": "r0,s0,s0", "coords": [0.0, 0.0, -1.0],
"endpoints": [21, 22], "endpoint_coords": [[0.0, 0.0, -0.5], [0.0, 0.0, 0.5]] },
{ "id": 19, "label": "wire_1", "point": "r1,s0,s0", "coords": [0.0, 0.0, 0.0],
"endpoints": [21, 22], "endpoint_coords": [[0.0, 0.0, -0.5], [0.0, 0.0, 0.5]] },
{ "id": 20, "label": "wire_2", "point": "r2,s0,s0", "coords": [0.0, 0.0, 1.0],
"endpoints": [21, 22], "endpoint_coords": [[0.0, 0.0, -0.5], [0.0, 0.0, 0.5]] }
],
"surfaces": [
{ "id": 16, "label": "surface_0", "point": "r0,r0,s0", "coords": [0.0, -1.0, -1.0], "boundary_wires": [18] },
{ "id": 17, "label": "surface_1", "point": "r0,r1,s0", "coords": [0.0, 0.0, -1.0], "boundary_wires": [18] }
]
};
// Coordinate mapping: coords are already [x, y, z] layout coordinates
// Scale for better visualization
const SCALE = 2.0;
function mapCoords(coords) {
return [coords[0] * SCALE, coords[1] * SCALE, coords[2] * SCALE];
}
// Wire colors by group
const WIRE_COLORS = {
input: 0x4fc3f7, // blue - r0 in third coord
crossing: 0xe040fb, // purple - s0 in third coord
output: 0x66bb6a, // green - r1 in third coord
selfloop: 0xff7043 // orange - self-loops
};
function getWireGroup(wire) {
if (wire.endpoints[0] === wire.endpoints[1]) return 'selfloop';
const point = wire.point;
const thirdCoord = point.split(',')[2];
if (thirdCoord === 'r0') return 'input';
if (thirdCoord === 's0') return 'crossing';
if (thirdCoord === 'r1') return 'output';
return 'crossing';
}
// Variational elastic curve solver
// Minimizes E = τ·Σ|Δp|² + β·Σ|Δ²p|² + κ·|p_mid - waypoint|²
function solveElasticCurve(p0, p1, waypoint, tau, beta, kappa, resolution) {
const n = resolution; // interior points
const total = n + 2; // including endpoints
// Build matrix A = τ·DD + β·D²D² + κ·(waypoint spring at midpoint)
// DD tridiagonal: main=2, off=-1
// D²D² pentadiagonal: [1, -4, 6, -4, 1]
// We solve for interior points only (indices 1..n)
// Matrix is n×n
const A = [];
const bx = new Array(n).fill(0);
const by = new Array(n).fill(0);
const bz = new Array(n).fill(0);
for (let i = 0; i < n; i++) {
A[i] = new Array(n).fill(0);
}
// Build DD (tension term)
for (let i = 0; i < n; i++) {
A[i][i] += 2 * tau;
if (i > 0) A[i][i-1] += -tau;
if (i < n-1) A[i][i+1] += -tau;
}
// Boundary contributions to RHS for tension term
bx[0] += tau * p0[0];
by[0] += tau * p0[1];
bz[0] += tau * p0[2];
bx[n-1] += tau * p1[0];
by[n-1] += tau * p1[1];
bz[n-1] += tau * p1[2];
// Build D²D² (bending term) - pentadiagonal
// For interior point i, D²p[i] = p[i-2] - 4p[i-1] + 6p[i] - 4p[i+1] + p[i+2]
for (let i = 0; i < n; i++) {
A[i][i] += 6 * beta;
if (i > 0) A[i][i-1] += -4 * beta;
if (i > 1) A[i][i-2] += beta;
if (i < n-1) A[i][i+1] += -4 * beta;
if (i < n-2) A[i][i+2] += beta;
}
// Boundary contributions for bending term
// At i=0: needs p[-1]=p0, p[-2] (extrapolate as p0)
// At i=1: needs p[-1]=p0
// At i=n-2: needs p[n]=p1
// At i=n-1: needs p[n]=p1, p[n+1] (extrapolate as p1)
// i=0 contributions from p0 (at index -1) and extrapolated p0 (at index -2)
bx[0] += beta * (4 * p0[0] - p0[0]); // -4*p[-1] + p[-2], but these go to RHS with sign flip
by[0] += beta * (4 * p0[1] - p0[1]);
bz[0] += beta * (4 * p0[2] - p0[2]);
if (n > 1) {
bx[1] += beta * p0[0];
by[1] += beta * p0[1];
bz[1] += beta * p0[2];
}
// i=n-1 contributions from p1 (at index n) and extrapolated p1 (at index n+1)
bx[n-1] += beta * (4 * p1[0] - p1[0]);
by[n-1] += beta * (4 * p1[1] - p1[1]);
bz[n-1] += beta * (4 * p1[2] - p1[2]);
if (n > 1) {
bx[n-2] += beta * p1[0];
by[n-2] += beta * p1[1];
bz[n-2] += beta * p1[2];
}
// Waypoint spring at midpoint
const midIdx = Math.floor(n / 2);
A[midIdx][midIdx] += kappa;
bx[midIdx] += kappa * waypoint[0];
by[midIdx] += kappa * waypoint[1];
bz[midIdx] += kappa * waypoint[2];
// Solve using Gaussian elimination with partial pivoting
function solveTridiagonal(A, b) {
const n = b.length;
const x = new Array(n);
const Ac = A.map(row => [...row]);
const bc = [...b];
// Forward elimination
for (let k = 0; k < n-1; k++) {
// Find pivot
let maxIdx = k;
for (let i = k+1; i < n; i++) {
if (Math.abs(Ac[i][k]) > Math.abs(Ac[maxIdx][k])) maxIdx = i;
}
// Swap rows
[Ac[k], Ac[maxIdx]] = [Ac[maxIdx], Ac[k]];
[bc[k], bc[maxIdx]] = [bc[maxIdx], bc[k]];
if (Math.abs(Ac[k][k]) < 1e-10) continue;
for (let i = k+1; i < n; i++) {
const factor = Ac[i][k] / Ac[k][k];
for (let j = k; j < n; j++) {
Ac[i][j] -= factor * Ac[k][j];
}
bc[i] -= factor * bc[k];
}
}
// Back substitution
for (let i = n-1; i >= 0; i--) {
let sum = bc[i];
for (let j = i+1; j < n; j++) {
sum -= Ac[i][j] * x[j];
}
x[i] = Math.abs(Ac[i][i]) > 1e-10 ? sum / Ac[i][i] : 0;
}
return x;
}
const solX = solveTridiagonal(A, bx);
const solY = solveTridiagonal(A, by);
const solZ = solveTridiagonal(A, bz);
// Build full curve with endpoints
const curve = [p0];
for (let i = 0; i < n; i++) {
curve.push([solX[i], solY[i], solZ[i]]);
}
curve.push(p1);
return curve;
}
// Generate self-loop curve (parametric loop offset toward waypoint)
function generateSelfLoop(center, waypoint, resolution) {
const points = [];
const offset = [
waypoint[0] - center[0],
waypoint[1] - center[1],
waypoint[2] - center[2]
];
const offsetMag = Math.sqrt(offset[0]**2 + offset[1]**2 + offset[2]**2);
const norm = offsetMag > 0.01 ? offset.map(x => x / offsetMag) : [0, 1, 0];
// Create perpendicular vectors for the loop plane
let perp1, perp2;
if (Math.abs(norm[1]) < 0.9) {
perp1 = [norm[2], 0, -norm[0]];
} else {
perp1 = [0, norm[2], -norm[1]];
}
const mag1 = Math.sqrt(perp1[0]**2 + perp1[1]**2 + perp1[2]**2);
perp1 = perp1.map(x => x / mag1);
perp2 = [
norm[1]*perp1[2] - norm[2]*perp1[1],
norm[2]*perp1[0] - norm[0]*perp1[2],
norm[0]*perp1[1] - norm[1]*perp1[0]
];
const loopRadius = Math.min(offsetMag * 0.6, 0.8);
const loopCenter = [
center[0] + norm[0] * offsetMag * 0.4,
center[1] + norm[1] * offsetMag * 0.4,
center[2] + norm[2] * offsetMag * 0.4
];
for (let i = 0; i <= resolution; i++) {
const t = (i / resolution) * 2 * Math.PI;
const x = loopCenter[0] + loopRadius * (Math.cos(t) * perp1[0] + Math.sin(t) * perp2[0]);
const y = loopCenter[1] + loopRadius * (Math.cos(t) * perp1[1] + Math.sin(t) * perp2[1]);
const z = loopCenter[2] + loopRadius * (Math.cos(t) * perp1[2] + Math.sin(t) * perp2[2]);
points.push([x, y, z]);
}
return points;
}
function ZigzagRenderer() {
const containerRef = React.useRef(null);
const sceneRef = React.useRef(null);
const cameraRef = React.useRef(null);
const rendererRef = React.useRef(null);
const wireObjectsRef = React.useRef([]);
const waypointObjectsRef = React.useRef([]);
const surfaceObjectsRef = React.useRef([]);
const labelObjectsRef = React.useRef([]);
const [tension, setTension] = React.useState(1.0);
const [bending, setBending] = React.useState(0.5);
const [kappa, setKappa] = React.useState(2.0);
const [resolution, setResolution] = React.useState(20);
const [showWaypoints, setShowWaypoints] = React.useState(true);
const [showSurfaces, setShowSurfaces] = React.useState(true);
const [showLabels, setShowLabels] = React.useState(true);
// Orbit controls state
const orbitRef = React.useRef({
theta: Math.PI / 4,
phi: Math.PI / 4,
radius: 12,
target: [2, 2, 2],
isDragging: false,
lastX: 0,
lastY: 0
});
// Initialize Three.js scene
React.useEffect(() => {
if (!containerRef.current || !window.THREE) return;
const THREE = window.THREE;
const width = containerRef.current.clientWidth;
const height = containerRef.current.clientHeight;
// Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
sceneRef.current = scene;
// Camera
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 100);
cameraRef.current = camera;
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
containerRef.current.appendChild(renderer.domElement);
rendererRef.current = renderer;
// Lights
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);
const directional = new THREE.DirectionalLight(0xffffff, 0.8);
directional.position.set(5, 10, 5);
scene.add(directional);
// Add vertices as glowing spheres
const vertexGeom = new THREE.SphereGeometry(0.15, 32, 32);
const vertexMat = new THREE.MeshStandardMaterial({
color: 0xf0c040,
emissive: 0xf0c040,
emissiveIntensity: 0.5
});
GEOMETRY_DATA.vertices.forEach(v => {
const pos = mapCoords(v.coords);
const mesh = new THREE.Mesh(vertexGeom, vertexMat);
mesh.position.set(pos[0], pos[1], pos[2]);
scene.add(mesh);
});
// Grid helper
const gridHelper = new THREE.GridHelper(10, 10, 0x333344, 0x222233);
gridHelper.position.y = -0.5;
scene.add(gridHelper);
// Axes helper
const axesHelper = new THREE.AxesHelper(3);
scene.add(axesHelper);
// Mouse controls
const canvas = renderer.domElement;
canvas.addEventListener('mousedown', (e) => {
orbitRef.current.isDragging = true;
orbitRef.current.lastX = e.clientX;
orbitRef.current.lastY = e.clientY;
});
canvas.addEventListener('mousemove', (e) => {
if (!orbitRef.current.isDragging) return;
const dx = e.clientX - orbitRef.current.lastX;
const dy = e.clientY - orbitRef.current.lastY;
orbitRef.current.theta -= dx * 0.01;
orbitRef.current.phi = Math.max(0.1, Math.min(Math.PI - 0.1, orbitRef.current.phi + dy * 0.01));
orbitRef.current.lastX = e.clientX;
orbitRef.current.lastY = e.clientY;
});
canvas.addEventListener('mouseup', () => {
orbitRef.current.isDragging = false;
});
canvas.addEventListener('mouseleave', () => {
orbitRef.current.isDragging = false;
});
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
orbitRef.current.radius = Math.max(3, Math.min(30, orbitRef.current.radius + e.deltaY * 0.01));
});
// Animation loop
function animate() {
requestAnimationFrame(animate);
const orbit = orbitRef.current;
const x = orbit.target[0] + orbit.radius * Math.sin(orbit.phi) * Math.cos(orbit.theta);
const y = orbit.target[1] + orbit.radius * Math.cos(orbit.phi);
const z = orbit.target[2] + orbit.radius * Math.sin(orbit.phi) * Math.sin(orbit.theta);
camera.position.set(x, y, z);
camera.lookAt(orbit.target[0], orbit.target[1], orbit.target[2]);
renderer.render(scene, camera);
}
animate();
// Resize handler
const handleResize = () => {
const w = containerRef.current.clientWidth;
const h = containerRef.current.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
renderer.dispose();
if (containerRef.current && renderer.domElement) {
containerRef.current.removeChild(renderer.domElement);
}
};
}, []);
// Update wires when parameters change
React.useEffect(() => {
if (!sceneRef.current || !window.THREE) return;
const THREE = window.THREE;
const scene = sceneRef.current;
// Remove old wire objects
wireObjectsRef.current.forEach(obj => scene.remove(obj));
wireObjectsRef.current = [];
// Create wire lookup for surfaces
const wireById = {};
// Add wires
GEOMETRY_DATA.wires.forEach(wire => {
const p0 = mapCoords(wire.endpoint_coords[0]);
const p1 = mapCoords(wire.endpoint_coords[1]);
const waypoint = mapCoords(wire.coords);
const group = getWireGroup(wire);
const color = WIRE_COLORS[group];
let curvePoints;
if (wire.endpoints[0] === wire.endpoints[1]) {
// Self-loop
curvePoints = generateSelfLoop(p0, waypoint, resolution);
} else {
// Elastic curve
curvePoints = solveElasticCurve(p0, p1, waypoint, tension, bending, kappa, resolution);
}
wireById[wire.id] = curvePoints;
// Create tube geometry
const points = curvePoints.map(p => new THREE.Vector3(p[0], p[1], p[2]));
const curve = new THREE.CatmullRomCurve3(points);
const tubeGeom = new THREE.TubeGeometry(curve, resolution * 2, 0.04, 8, false);
const tubeMat = new THREE.MeshStandardMaterial({
color: color,
emissive: color,
emissiveIntensity: 0.3
});
const tube = new THREE.Mesh(tubeGeom, tubeMat);
scene.add(tube);
wireObjectsRef.current.push(tube);
});
// Store wire paths for surface rendering
sceneRef.current.userData.wireById = wireById;
}, [tension, bending, kappa, resolution]);
// Update waypoint visibility
React.useEffect(() => {
if (!sceneRef.current || !window.THREE) return;
const THREE = window.THREE;
const scene = sceneRef.current;
// Remove old waypoint objects
waypointObjectsRef.current.forEach(obj => scene.remove(obj));
waypointObjectsRef.current = [];
if (showWaypoints) {
const waypointGeom = new THREE.SphereGeometry(0.08, 16, 16);
const waypointMat = new THREE.MeshStandardMaterial({
color: 0xf0c040,
emissive: 0xf0c040,
emissiveIntensity: 0.6,
transparent: true,
opacity: 0.8
});
GEOMETRY_DATA.wires.forEach(wire => {
const pos = mapCoords(wire.coords);
const mesh = new THREE.Mesh(waypointGeom, waypointMat);
mesh.position.set(pos[0], pos[1], pos[2]);
scene.add(mesh);
waypointObjectsRef.current.push(mesh);
});
}
}, [showWaypoints]);
// Update surface visibility
React.useEffect(() => {
if (!sceneRef.current || !window.THREE) return;
const THREE = window.THREE;
const scene = sceneRef.current;
// Remove old surface objects
surfaceObjectsRef.current.forEach(obj => scene.remove(obj));
surfaceObjectsRef.current = [];
if (showSurfaces) {
const surfaceMat = new THREE.MeshStandardMaterial({
color: 0xff6b6b,
emissive: 0xff6b6b,
emissiveIntensity: 0.4,
transparent: true,
opacity: 0.6
});
const lineMat = new THREE.LineBasicMaterial({
color: 0xff6b6b,
transparent: true,
opacity: 0.4
});
GEOMETRY_DATA.surfaces.forEach(surface => {
const pos = mapCoords(surface.coords);
// Surface center point
const sphereGeom = new THREE.SphereGeometry(0.06, 12, 12);
const sphere = new THREE.Mesh(sphereGeom, surfaceMat);
sphere.position.set(pos[0], pos[1], pos[2]);
scene.add(sphere);
surfaceObjectsRef.current.push(sphere);
// Lines to boundary wire midpoints
const wireById = scene.userData.wireById || {};
surface.boundary_wires.forEach(wireId => {
const wire = GEOMETRY_DATA.wires.find(w => w.id === wireId);
if (wire) {
const wirePos = mapCoords(wire.coords);
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(pos[0], pos[1], pos[2]),
new THREE.Vector3(wirePos[0], wirePos[1], wirePos[2])
]);
const line = new THREE.Line(geometry, lineMat);
scene.add(line);
surfaceObjectsRef.current.push(line);
}
});
});
}
}, [showSurfaces, tension, bending, kappa, resolution]);
// Update label visibility
React.useEffect(() => {
if (!sceneRef.current || !window.THREE) return;
const THREE = window.THREE;
const scene = sceneRef.current;
// Remove old label objects
labelObjectsRef.current.forEach(obj => scene.remove(obj));
labelObjectsRef.current = [];
if (showLabels) {
// Create canvas-based sprite labels
GEOMETRY_DATA.vertices.forEach(v => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 128;
canvas.height = 64;
ctx.fillStyle = 'rgba(10, 10, 15, 0.8)';
ctx.fillRect(0, 0, 128, 64);
ctx.strokeStyle = '#f0c040';
ctx.lineWidth = 2;
ctx.strokeRect(2, 2, 124, 60);
ctx.font = 'bold 20px monospace';
ctx.fillStyle = '#f0c040';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(v.label, 64, 32);
const texture = new THREE.CanvasTexture(canvas);
const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true });
const sprite = new THREE.Sprite(spriteMat);
const pos = mapCoords(v.coords);
sprite.position.set(pos[0], pos[1] + 0.4, pos[2]);
sprite.scale.set(1, 0.5, 1);
scene.add(sprite);
labelObjectsRef.current.push(sprite);
});
}
}, [showLabels]);
const panelStyle = {
position: 'absolute',
top: '20px',
left: '20px',
background: 'rgba(10, 10, 20, 0.85)',
backdropFilter: 'blur(10px)',
padding: '20px',
borderRadius: '12px',
color: '#e0e0e0',
fontFamily: 'monospace',
fontSize: '14px',
border: '1px solid rgba(240, 192, 64, 0.3)',
minWidth: '220px'
};
const sliderStyle = {
width: '100%',
marginTop: '4px',
accentColor: '#f0c040'
};
const checkboxStyle = {
accentColor: '#f0c040',
marginRight: '8px'
};
return React.createElement('div', { style: { width: '100vw', height: '100vh', position: 'relative' } },
React.createElement('div', { ref: containerRef, style: { width: '100%', height: '100%' } }),
React.createElement('div', { style: panelStyle },
React.createElement('h3', { style: { margin: '0 0 15px 0', color: '#f0c040' } }, 'Zigzag Renderer'),
React.createElement('div', { style: { marginBottom: '12px' } },
React.createElement('label', null, `Tension τ: ${tension.toFixed(2)}`),
React.createElement('input', {
type: 'range', min: '0.1', max: '5', step: '0.1', value: tension,
onChange: (e) => setTension(parseFloat(e.target.value)),
style: sliderStyle
})
),
React.createElement('div', { style: { marginBottom: '12px' } },
React.createElement('label', null, `Bending β: ${bending.toFixed(2)}`),
React.createElement('input', {
type: 'range', min: '0', max: '2', step: '0.05', value: bending,
onChange: (e) => setBending(parseFloat(e.target.value)),
style: sliderStyle
})
),
React.createElement('div', { style: { marginBottom: '12px' } },
React.createElement('label', null, `Waypoint κ: ${kappa.toFixed(2)}`),
React.createElement('input', {
type: 'range', min: '0', max: '10', step: '0.1', value: kappa,
onChange: (e) => setKappa(parseFloat(e.target.value)),
style: sliderStyle
})
),
React.createElement('div', { style: { marginBottom: '12px' } },
React.createElement('label', null, `Resolution: ${resolution}`),
React.createElement('input', {
type: 'range', min: '5', max: '50', step: '1', value: resolution,
onChange: (e) => setResolution(parseInt(e.target.value)),
style: sliderStyle
})
),
React.createElement('hr', { style: { border: 'none', borderTop: '1px solid rgba(240, 192, 64, 0.3)', margin: '15px 0' } }),
React.createElement('div', { style: { marginBottom: '8px' } },
React.createElement('label', null,
React.createElement('input', {
type: 'checkbox', checked: showWaypoints,
onChange: (e) => setShowWaypoints(e.target.checked),
style: checkboxStyle
}),
'Waypoints'
)
),
React.createElement('div', { style: { marginBottom: '8px' } },
React.createElement('label', null,
React.createElement('input', {
type: 'checkbox', checked: showSurfaces,
onChange: (e) => setShowSurfaces(e.target.checked),
style: checkboxStyle
}),
'Surfaces'
)
),
React.createElement('div', { style: { marginBottom: '8px' } },
React.createElement('label', null,
React.createElement('input', {
type: 'checkbox', checked: showLabels,
onChange: (e) => setShowLabels(e.target.checked),
style: checkboxStyle
}),
'Labels'
)
),
React.createElement('hr', { style: { border: 'none', borderTop: '1px solid rgba(240, 192, 64, 0.3)', margin: '15px 0' } }),
React.createElement('div', { style: { fontSize: '11px', color: '#888' } },
React.createElement('div', null, '🔵 Input (r0)'),
React.createElement('div', null, '🟣 Crossing (s0)'),
React.createElement('div', null, '🟢 Output (r1)'),
React.createElement('div', null, '🟠 Self-loop')
)
)
);
}
// Export for use
window.ZigzagRenderer = ZigzagRenderer;