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

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

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

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

330 lines
15 KiB
Rust

//! 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!("════════════════════════════════════════════════════════════════════════════════");
}