Merge feat/explosion: k-points, poset structure

This commit is contained in:
Maximus Gorog 2026-04-07 03:12:27 -06:00
commit c352384e92
2 changed files with 883 additions and 28 deletions

View file

@ -6,8 +6,27 @@
//!
//! This module provides the interface for the layout algorithm without
//! implementing the full spring-constraint solver.
//!
//! # Ordering on Points
//!
//! Points are ordered by the product ordering on height labels, where
//! the ordering within each dimension follows the zigzag structure:
//!
//! ```text
//! r₀ < s₀ < r₁ < s₁ < r₂ < ... < sₙ₋₁ < rₙ
//! ```
//!
//! That is: regular height rⱼ < singular height sⱼ < regular height rⱼ₊₁
//!
//! # Covering Relations
//!
//! A covering relation p ⋖ q means p < q and there is no z with p < z < q.
//! In the zigzag structure, covers occur between adjacent heights:
//! - rⱼ ⋖ sⱼ (regular covers singular)
//! - sⱼ ⋖ rⱼ₊₁ (singular covers regular)
use std::cmp::Ordering;
use std::collections::{HashSet, HashMap, VecDeque};
use crate::diagram::Diagram;
/// A height label in a diagram: either regular or singular.
@ -37,6 +56,34 @@ impl HeightLabel {
HeightLabel::Singular(i) => *i,
}
}
/// Convert to a linear index for ordering purposes.
///
/// The zigzag ordering r₀ < s₀ < r₁ < s₁ < ... maps to:
/// - r₀ → 0, s₀ → 1, r₁ → 2, s₁ → 3, ...
/// - rⱼ → 2j, sⱼ → 2j + 1
pub fn to_linear_index(&self) -> usize {
match self {
HeightLabel::Regular(j) => 2 * j,
HeightLabel::Singular(j) => 2 * j + 1,
}
}
/// Check if this height label covers another in the zigzag ordering.
///
/// Returns true if `other ⋖ self` (other is covered by self).
/// Covers occur between adjacent heights in the zigzag:
/// - sⱼ covers rⱼ (regular to singular)
/// - rⱼ₊₁ covers sⱼ (singular to regular)
pub fn covers(&self, other: &HeightLabel) -> bool {
match (other, self) {
// rⱼ ⋖ sⱼ
(HeightLabel::Regular(r), HeightLabel::Singular(s)) => *r == *s,
// sⱼ ⋖ rⱼ₊₁
(HeightLabel::Singular(s), HeightLabel::Regular(r)) => *r == *s + 1,
_ => false,
}
}
}
/// A point in the explosion of a diagram.
@ -137,6 +184,121 @@ impl<T: Clone + Eq> Poset<T> {
pub fn find(&self, element: &T) -> Option<usize> {
self.elements.iter().position(|e| e == element)
}
/// Get the minimal elements (elements with no predecessors).
///
/// These are elements x such that there is no y with y < x.
pub fn minimal_elements(&self) -> Vec<usize> {
let mut has_predecessor = vec![false; self.elements.len()];
for &(_, upper) in &self.covers {
has_predecessor[upper] = true;
}
(0..self.elements.len())
.filter(|&i| !has_predecessor[i])
.collect()
}
/// Get the maximal elements (elements with no successors).
///
/// These are elements x such that there is no y with x < y.
pub fn maximal_elements(&self) -> Vec<usize> {
let mut has_successor = vec![false; self.elements.len()];
for &(lower, _) in &self.covers {
has_successor[lower] = true;
}
(0..self.elements.len())
.filter(|&i| !has_successor[i])
.collect()
}
/// Compute the transitive closure of the covering relations.
///
/// Returns a set of pairs (i, j) where elements[i] < elements[j].
fn transitive_closure(&self) -> HashSet<(usize, usize)> {
let n = self.elements.len();
if n == 0 {
return HashSet::new();
}
// Build adjacency list for BFS
let mut successors: Vec<Vec<usize>> = vec![vec![]; n];
for &(lower, upper) in &self.covers {
successors[lower].push(upper);
}
// For each element, BFS to find all elements greater than it
let mut result = HashSet::new();
for start in 0..n {
let mut visited = vec![false; n];
let mut queue = VecDeque::new();
for &succ in &successors[start] {
if !visited[succ] {
visited[succ] = true;
queue.push_back(succ);
result.insert((start, succ));
}
}
while let Some(current) = queue.pop_front() {
for &succ in &successors[current] {
if !visited[succ] {
visited[succ] = true;
queue.push_back(succ);
result.insert((start, succ));
}
}
}
}
result
}
/// Check if element at index `lower` is less than element at index `upper`.
///
/// This computes reachability in the cover graph.
pub fn is_less_than(&self, lower: usize, upper: usize) -> bool {
if lower >= self.elements.len() || upper >= self.elements.len() {
return false;
}
if lower == upper {
return false;
}
self.transitive_closure().contains(&(lower, upper))
}
/// Check if element at index `lower` is less than or equal to element at index `upper`.
pub fn is_leq(&self, lower: usize, upper: usize) -> bool {
lower == upper || self.is_less_than(lower, upper)
}
/// Compare two elements by their indices.
///
/// Returns Some(Ordering) if comparable, None if incomparable.
pub fn compare(&self, a: usize, b: usize) -> Option<Ordering> {
if a == b {
Some(Ordering::Equal)
} else if self.is_less_than(a, b) {
Some(Ordering::Less)
} else if self.is_less_than(b, a) {
Some(Ordering::Greater)
} else {
None // Incomparable
}
}
/// Get immediate successors (elements covered by the given element).
pub fn immediate_successors(&self, idx: usize) -> Vec<usize> {
self.covers
.iter()
.filter_map(|&(l, u)| if l == idx { Some(u) } else { None })
.collect()
}
/// Get immediate predecessors (elements that cover the given element).
pub fn immediate_predecessors(&self, idx: usize) -> Vec<usize> {
self.covers
.iter()
.filter_map(|&(l, u)| if u == idx { Some(l) } else { None })
.collect()
}
}
/// Compute the k-points of a diagram.
@ -145,6 +307,10 @@ impl<T: Clone + Eq> Poset<T> {
/// - Pt₀(X) = {()} (single empty point)
/// - Ptₖ₊₁(X) = {(sᵢ, x) | i ∈ Xˢ, x ∈ Ptₖ(X(sᵢ))}
/// {(rⱼ, x) | j ∈ Xʳ, x ∈ Ptₖ(X(rⱼ))}
///
/// The covering relations come from the zigzag structure:
/// - Adjacent heights rⱼ ⋖ sⱼ and sⱼ ⋖ rⱼ₊₁
/// - Combined with the covering relations in the sub-posets
pub fn k_points(diagram: &Diagram, k: usize) -> Poset<Point> {
if k == 0 {
// Base case: single empty point
@ -159,29 +325,141 @@ pub fn k_points(diagram: &Diagram, k: usize) -> Poset<Point> {
Diagram::DiagramN(d) => {
let mut poset = Poset::empty();
// Track which indices belong to which height and sub-point
// Map from (HeightLabel, sub_point_index_in_sub_poset) -> index in poset
let mut point_map: HashMap<(HeightLabel, usize), usize> = HashMap::new();
// Store the sub-posets for each height for later reference
let mut sub_posets: HashMap<HeightLabel, Poset<Point>> = HashMap::new();
// Add points from regular heights
for j in 0..=d.length() {
if let Some(slice) = d.regular_slice(j) {
let sub_points = k_points(&slice, k - 1);
for sub_point in sub_points.elements() {
let point = sub_point.extend(HeightLabel::Regular(j));
poset.add_element(point);
}
let label = HeightLabel::Regular(j);
// TODO: Proper slice computation - currently regular_slice returns None for j > 0
// 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 sub_points = k_points(&slice, k - 1);
for (sub_idx, sub_point) in sub_points.elements().iter().enumerate() {
let point = sub_point.extend(label);
let idx = poset.add_element(point);
point_map.insert((label, sub_idx), idx);
}
sub_posets.insert(label, sub_points);
}
// Add points from singular heights
for i in 0..d.length() {
if let Some(slice) = d.singular_slice(i) {
let sub_points = k_points(&slice, k - 1);
for sub_point in sub_points.elements() {
let point = sub_point.extend(HeightLabel::Singular(i));
poset.add_element(point);
let label = HeightLabel::Singular(i);
// TODO: Proper slice computation - currently singular_slice returns None
// For now, use source as a structural placeholder
let slice = d.singular_slice(i).unwrap_or_else(|| (*d.source).clone());
let sub_points = k_points(&slice, k - 1);
for (sub_idx, sub_point) in sub_points.elements().iter().enumerate() {
let point = sub_point.extend(label);
let idx = poset.add_element(point);
point_map.insert((label, sub_idx), idx);
}
sub_posets.insert(label, sub_points);
}
// Compute covering relations
//
// A point p = (h, x) covers q = (h', x') iff:
// 1. h = h' and x covers x' in the sub-poset, OR
// 2. h covers h' and x, x' are related via the cospan structure
//
// For case 2, when h covers h' in the zigzag ordering:
// - If h = sⱼ and h' = rⱼ: points are related via the forward cospan map
// - If h = rⱼ₊₁ and h' = sⱼ: points are related via the backward cospan map
//
// Since we don't have full cospan data, we use a simplified model:
// Points at adjacent heights are covers if they share the same sub-point structure.
// TODO: Use actual cospan data to determine which sub-points are related.
// Case 1: Covers within the same height (vertical covers in sub-poset)
for (&label, sub_poset) in &sub_posets {
for &(lower_sub, upper_sub) in sub_poset.covers() {
if let (Some(&lower_idx), Some(&upper_idx)) =
(point_map.get(&(label, lower_sub)), point_map.get(&(label, upper_sub)))
{
poset.add_cover(lower_idx, upper_idx);
}
}
}
// TODO: Compute covering relations from cospan structure
// Case 2: Covers between adjacent heights (horizontal covers)
// For each pair of adjacent heights h < h' where h' covers h,
// we need to determine which points are related via the cospan maps.
// Helper to get all height labels in order
let mut all_labels: Vec<HeightLabel> = Vec::new();
for j in 0..=d.length() {
all_labels.push(HeightLabel::Regular(j));
if j < d.length() {
all_labels.push(HeightLabel::Singular(j));
}
}
// Check consecutive pairs
for window in all_labels.windows(2) {
let lower_label = window[0];
let upper_label = window[1];
// upper_label should cover lower_label in the zigzag ordering
debug_assert!(upper_label.covers(&lower_label));
if let (Some(lower_sub_poset), Some(upper_sub_poset)) =
(sub_posets.get(&lower_label), sub_posets.get(&upper_label))
{
// Determine which points are related via the cospan map
//
// The cospan structure gives us maps:
// - forward: rⱼ → sⱼ
// - backward: rⱼ₊₁ → sⱼ
//
// These induce maps on sub-points. For a covering relation,
// 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.
// Simplified case: when sub-posets have matching structure,
// connect corresponding points
if lower_sub_poset.len() == upper_sub_poset.len() {
for sub_idx in 0..lower_sub_poset.len() {
if let (Some(&lower_idx), Some(&upper_idx)) =
(point_map.get(&(lower_label, sub_idx)),
point_map.get(&(upper_label, sub_idx)))
{
poset.add_cover(lower_idx, upper_idx);
}
}
} else {
// When sub-posets have different sizes, the cospan maps
// contract or expand the structure. Without actual cospan data,
// we can't determine the exact correspondence.
//
// For structural completeness, connect all points at the
// lower height to all points at the upper height.
// This is an over-approximation of the covering relation.
// TODO: Refine this when cospan data is available.
for lower_sub_idx in 0..lower_sub_poset.len() {
for upper_sub_idx in 0..upper_sub_poset.len() {
if let (Some(&lower_idx), Some(&upper_idx)) =
(point_map.get(&(lower_label, lower_sub_idx)),
point_map.get(&(upper_label, upper_sub_idx)))
{
poset.add_cover(lower_idx, upper_idx);
}
}
}
}
}
}
poset
}
@ -259,11 +537,203 @@ fn compare_labels(a: &HeightLabel, b: &HeightLabel) -> Option<Ordering> {
}
}
// =============================================================================
// Injectification (Section 5.2)
// =============================================================================
/// Result of the injectification construction.
///
/// Given a poset P, injectification produces a new poset where all
/// covering relations become injective in a sense that prevents
/// degenerate layouts.
#[derive(Debug, Clone)]
pub struct InjectificationResult<T> {
/// The injectified poset
pub poset: Poset<T>,
/// Map from original element indices to injectified indices
/// (may be many-to-one if elements were merged)
pub projection: HashMap<usize, usize>,
}
/// Perform injectification on a poset.
///
/// The injectification construction (Section 5.2 of the spec) ensures that
/// all morphisms in the layout constraint system are injective, preventing
/// degenerate layouts where distinct points collapse to the same position.
///
/// Given a poset-shaped diagram X: J -> Pos, injectification produces
/// X_hat: J -> Pos_inj with a natural transformation whose components are
/// epimorphisms.
///
/// The construction works by:
/// 1. Taking colimits of downward-closed subsets
/// 2. Factoring through (mono, epi) factorization
/// 3. Composing with colimit inclusions
///
/// For the poset of k-points, this means identifying points that would
/// necessarily have the same position in any valid layout.
pub fn injectify<T: Clone + Eq + std::hash::Hash>(poset: &Poset<T>) -> InjectificationResult<T> {
if poset.is_empty() {
return InjectificationResult {
poset: Poset::empty(),
projection: HashMap::new(),
};
}
// The injectification identifies elements that have the same
// "downward closure" - i.e., the same set of elements below them.
//
// Two elements x, y are identified if:
// { z | z <= x } = { z | z <= y }
//
// This is computed using the transitive closure.
let n = poset.len();
let closure = poset.transitive_closure();
// Compute the downward closure for each element
let mut down_closures: Vec<HashSet<usize>> = vec![HashSet::new(); n];
for i in 0..n {
down_closures[i].insert(i); // reflexive
for j in 0..n {
if closure.contains(&(j, i)) {
down_closures[i].insert(j);
}
}
}
// Find equivalence classes: elements with the same down closure
let mut class_map: HashMap<Vec<usize>, usize> = HashMap::new();
let mut projection: HashMap<usize, usize> = HashMap::new();
let mut representatives: Vec<usize> = Vec::new();
for i in 0..n {
// Convert HashSet to sorted Vec for use as map key
let mut dc: Vec<usize> = down_closures[i].iter().copied().collect();
dc.sort();
if let Some(&class_idx) = class_map.get(&dc) {
projection.insert(i, class_idx);
} else {
let class_idx = representatives.len();
class_map.insert(dc, class_idx);
projection.insert(i, class_idx);
representatives.push(i);
}
}
// Build the injectified poset using representatives
let mut new_poset = Poset::empty();
for &rep in &representatives {
new_poset.add_element(poset.elements[rep].clone());
}
// Add covering relations between equivalence classes
let mut new_covers: HashSet<(usize, usize)> = HashSet::new();
for &(lower, upper) in poset.covers() {
let new_lower = projection[&lower];
let new_upper = projection[&upper];
if new_lower != new_upper {
new_covers.insert((new_lower, new_upper));
}
}
for (lower, upper) in new_covers {
new_poset.add_cover(lower, upper);
}
// Reduce to minimal covering relations (remove transitive edges)
let reduced_poset = reduce_to_covers(&new_poset);
InjectificationResult {
poset: reduced_poset,
projection,
}
}
/// Reduce a poset to its minimal covering relations.
///
/// Removes transitive edges: if a < b and a < c < b, remove the edge a -> b.
fn reduce_to_covers<T: Clone + Eq>(poset: &Poset<T>) -> Poset<T> {
let n = poset.len();
if n == 0 {
return Poset::empty();
}
let closure = poset.transitive_closure();
// Build adjacency for direct edges
let direct_edges: HashSet<(usize, usize)> = poset.covers().iter().copied().collect();
// A direct edge (a, b) is a cover iff there's no c with a < c < b
let mut covers: Vec<(usize, usize)> = Vec::new();
for &(a, b) in &direct_edges {
let mut is_cover = true;
for c in 0..n {
if c != a && c != b {
// Check if a < c < b via transitive closure
let a_lt_c = closure.contains(&(a, c));
let c_lt_b = closure.contains(&(c, b));
if a_lt_c && c_lt_b {
is_cover = false;
break;
}
}
}
if is_cover {
covers.push((a, b));
}
}
let mut result = Poset::empty();
for elem in poset.elements() {
result.add_element(elem.clone());
}
for (lower, upper) in covers {
result.add_cover(lower, upper);
}
result
}
/// Check if two points would have the same position in any valid layout.
///
/// This occurs when points have the same downward closure in the poset.
pub fn are_layout_equivalent(poset: &Poset<Point>, a: usize, b: usize) -> bool {
if a == b {
return true;
}
let closure = poset.transitive_closure();
// Compute downward closures
let mut down_a: HashSet<usize> = HashSet::new();
let mut down_b: HashSet<usize> = HashSet::new();
down_a.insert(a);
down_b.insert(b);
for i in 0..poset.len() {
if closure.contains(&(i, a)) {
down_a.insert(i);
}
if closure.contains(&(i, b)) {
down_b.insert(i);
}
}
down_a == down_b
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signature::Generator;
use crate::diagram::DiagramN;
use crate::diagram::{DiagramN, Cospan, Rewrite};
// =========================================================================
// Point and HeightLabel tests
// =========================================================================
#[test]
fn test_point_creation() {
@ -276,34 +746,419 @@ mod tests {
}
#[test]
fn test_zero_points() {
let g = Generator::point(0);
let d = Diagram::Diagram0(g);
let pts = d.points(0);
assert_eq!(pts.len(), 1);
fn test_point_empty() {
let p = Point::empty();
assert_eq!(p.depth(), 0);
assert_eq!(p.at(0), None);
}
#[test]
fn test_identity_points() {
let g = Generator::point(0);
let d0 = Diagram::Diagram0(g);
let d1 = Diagram::DiagramN(DiagramN::identity(d0));
// 1-points of an identity should have one point (the regular height)
let pts = d1.points(1);
assert!(pts.len() >= 1);
fn test_point_extend() {
let p = Point::empty();
let p2 = p.extend(HeightLabel::Regular(0));
assert_eq!(p2.depth(), 1);
let p3 = p2.extend(HeightLabel::Singular(0));
assert_eq!(p3.depth(), 2);
}
#[test]
fn test_height_label_linear_index() {
// r₀ -> 0, s₀ -> 1, r₁ -> 2, s₁ -> 3, ...
assert_eq!(HeightLabel::Regular(0).to_linear_index(), 0);
assert_eq!(HeightLabel::Singular(0).to_linear_index(), 1);
assert_eq!(HeightLabel::Regular(1).to_linear_index(), 2);
assert_eq!(HeightLabel::Singular(1).to_linear_index(), 3);
assert_eq!(HeightLabel::Regular(2).to_linear_index(), 4);
}
#[test]
fn test_height_label_covers() {
// sⱼ covers rⱼ
assert!(HeightLabel::Singular(0).covers(&HeightLabel::Regular(0)));
assert!(HeightLabel::Singular(1).covers(&HeightLabel::Regular(1)));
// rⱼ₊₁ covers sⱼ
assert!(HeightLabel::Regular(1).covers(&HeightLabel::Singular(0)));
assert!(HeightLabel::Regular(2).covers(&HeightLabel::Singular(1)));
// Non-adjacent don't cover
assert!(!HeightLabel::Regular(2).covers(&HeightLabel::Regular(0)));
assert!(!HeightLabel::Singular(1).covers(&HeightLabel::Regular(0)));
assert!(!HeightLabel::Regular(0).covers(&HeightLabel::Singular(0)));
}
// =========================================================================
// Label comparison tests
// =========================================================================
#[test]
fn test_compare_labels() {
// Regular comparisons
assert_eq!(
compare_labels(&HeightLabel::Regular(0), &HeightLabel::Regular(1)),
Some(Ordering::Less)
);
assert_eq!(
compare_labels(&HeightLabel::Regular(2), &HeightLabel::Regular(1)),
Some(Ordering::Greater)
);
assert_eq!(
compare_labels(&HeightLabel::Regular(1), &HeightLabel::Regular(1)),
Some(Ordering::Equal)
);
// Singular comparisons
assert_eq!(
compare_labels(&HeightLabel::Singular(0), &HeightLabel::Singular(1)),
Some(Ordering::Less)
);
// Mixed comparisons: r < s if r <= s_index
assert_eq!(
compare_labels(&HeightLabel::Regular(0), &HeightLabel::Singular(0)),
Some(Ordering::Less)
);
assert_eq!(
compare_labels(&HeightLabel::Regular(0), &HeightLabel::Singular(1)),
Some(Ordering::Less)
);
assert_eq!(
compare_labels(&HeightLabel::Regular(1), &HeightLabel::Singular(0)),
Some(Ordering::Greater)
);
// Mixed comparisons: s < r if s_index < r
assert_eq!(
compare_labels(&HeightLabel::Singular(0), &HeightLabel::Regular(1)),
Some(Ordering::Less)
);
assert_eq!(
compare_labels(&HeightLabel::Singular(0), &HeightLabel::Regular(0)),
Some(Ordering::Greater)
);
}
// =========================================================================
// Point comparison tests
// =========================================================================
#[test]
fn test_compare_points_equal() {
let p1 = Point::new(vec![HeightLabel::Regular(0), HeightLabel::Singular(0)]);
let p2 = Point::new(vec![HeightLabel::Regular(0), HeightLabel::Singular(0)]);
assert_eq!(compare_points(&p1, &p2), Some(Ordering::Equal));
}
#[test]
fn test_compare_points_less() {
// Both coordinates less
let p1 = Point::new(vec![HeightLabel::Regular(0), HeightLabel::Regular(0)]);
let p2 = Point::new(vec![HeightLabel::Regular(1), HeightLabel::Regular(1)]);
assert_eq!(compare_points(&p1, &p2), Some(Ordering::Less));
}
#[test]
fn test_compare_points_incomparable() {
// One coord less, one greater => incomparable
let p1 = Point::new(vec![HeightLabel::Regular(0), HeightLabel::Regular(1)]);
let p2 = Point::new(vec![HeightLabel::Regular(1), HeightLabel::Regular(0)]);
assert_eq!(compare_points(&p1, &p2), None);
}
#[test]
fn test_compare_points_different_depths() {
let p1 = Point::new(vec![HeightLabel::Regular(0)]);
let p2 = Point::new(vec![HeightLabel::Regular(0), HeightLabel::Singular(0)]);
assert_eq!(compare_points(&p1, &p2), None);
}
// =========================================================================
// Poset method tests
// =========================================================================
#[test]
fn test_poset_minimal_maximal_elements() {
// Create a diamond poset: bottom < left, bottom < right, left < top, right < top
let mut poset: Poset<&str> = Poset::empty();
let bottom = poset.add_element("bottom");
let left = poset.add_element("left");
let right = poset.add_element("right");
let top = poset.add_element("top");
poset.add_cover(bottom, left);
poset.add_cover(bottom, right);
poset.add_cover(left, top);
poset.add_cover(right, top);
let minimals = poset.minimal_elements();
let maximals = poset.maximal_elements();
assert_eq!(minimals, vec![bottom]);
assert_eq!(maximals, vec![top]);
}
#[test]
fn test_poset_is_less_than() {
// Linear chain: a < b < c
let mut poset: Poset<i32> = Poset::empty();
let a = poset.add_element(1);
let b = poset.add_element(2);
let c = poset.add_element(3);
poset.add_cover(a, b);
poset.add_cover(b, c);
// Direct covers
assert!(poset.is_less_than(a, b));
assert!(poset.is_less_than(b, c));
// Transitive
assert!(poset.is_less_than(a, c));
// Not less than
assert!(!poset.is_less_than(c, a));
assert!(!poset.is_less_than(b, a));
// Self comparison
assert!(!poset.is_less_than(a, a));
}
#[test]
fn test_poset_is_leq() {
let mut poset: Poset<i32> = Poset::empty();
let a = poset.add_element(1);
let b = poset.add_element(2);
poset.add_cover(a, b);
assert!(poset.is_leq(a, a));
assert!(poset.is_leq(a, b));
assert!(!poset.is_leq(b, a));
}
#[test]
fn test_poset_compare() {
// Diamond poset
let mut poset: Poset<i32> = Poset::empty();
let bottom = poset.add_element(0);
let left = poset.add_element(1);
let right = poset.add_element(2);
let top = poset.add_element(3);
poset.add_cover(bottom, left);
poset.add_cover(bottom, right);
poset.add_cover(left, top);
poset.add_cover(right, top);
assert_eq!(poset.compare(bottom, top), Some(Ordering::Less));
assert_eq!(poset.compare(top, bottom), Some(Ordering::Greater));
assert_eq!(poset.compare(left, left), Some(Ordering::Equal));
// left and right are incomparable
assert_eq!(poset.compare(left, right), None);
}
#[test]
fn test_poset_immediate_successors_predecessors() {
let mut poset: Poset<i32> = Poset::empty();
let a = poset.add_element(1);
let b = poset.add_element(2);
let c = poset.add_element(3);
let d = poset.add_element(4);
poset.add_cover(a, b);
poset.add_cover(a, c);
poset.add_cover(b, d);
poset.add_cover(c, d);
let succs = poset.immediate_successors(a);
assert!(succs.contains(&b));
assert!(succs.contains(&c));
assert_eq!(succs.len(), 2);
let preds = poset.immediate_predecessors(d);
assert!(preds.contains(&b));
assert!(preds.contains(&c));
assert_eq!(preds.len(), 2);
}
// =========================================================================
// k-points and covering relation tests
// =========================================================================
#[test]
fn test_zero_points() {
let g = Generator::point(0);
let d = Diagram::Diagram0(g);
let pts = d.points(0);
assert_eq!(pts.len(), 1);
assert!(pts.covers().is_empty()); // No covers for a single point
}
#[test]
fn test_identity_1_points() {
let g = Generator::point(0);
let d0 = Diagram::Diagram0(g);
let d1 = Diagram::DiagramN(DiagramN::identity(d0));
// 1-points of an identity (length 0 zigzag) should have one regular height r₀
let pts = d1.points(1);
assert_eq!(pts.len(), 1);
assert_eq!(pts.elements()[0], Point::new(vec![HeightLabel::Regular(0)]));
}
#[test]
fn test_single_cospan_1_points() {
// Create a 1-diagram with one cospan (length 1 zigzag)
let g = Generator::point(0);
let d0 = Diagram::Diagram0(g);
let cospan = Cospan::new(Rewrite::Identity, Rewrite::Identity);
let d1 = Diagram::DiagramN(DiagramN::new(d0, vec![cospan]));
// 1-points should have: r₀, s₀, r₁
let pts = d1.points(1);
assert_eq!(pts.len(), 3);
// Check that we have the right points
let elements: HashSet<_> = pts.elements().iter().collect();
assert!(elements.contains(&Point::new(vec![HeightLabel::Regular(0)])));
assert!(elements.contains(&Point::new(vec![HeightLabel::Singular(0)])));
assert!(elements.contains(&Point::new(vec![HeightLabel::Regular(1)])));
// Check covering relations: r₀ < s₀ < r₁
assert_eq!(pts.covers().len(), 2);
}
#[test]
fn test_covering_relations_chain() {
// Build a simple chain poset manually
let mut poset: Poset<Point> = Poset::empty();
let p0 = poset.add_element(Point::new(vec![HeightLabel::Regular(0)]));
let p1 = poset.add_element(Point::new(vec![HeightLabel::Singular(0)]));
let p2 = poset.add_element(Point::new(vec![HeightLabel::Regular(1)]));
poset.add_cover(p0, p1);
poset.add_cover(p1, p2);
// Check transitive ordering
assert!(poset.is_less_than(p0, p1));
assert!(poset.is_less_than(p1, p2));
assert!(poset.is_less_than(p0, p2)); // Transitive
// Check minimal/maximal
assert_eq!(poset.minimal_elements(), vec![p0]);
assert_eq!(poset.maximal_elements(), vec![p2]);
}
#[test]
fn test_two_cospan_1_points() {
// Create a 1-diagram with two cospans (length 2 zigzag)
let g = Generator::point(0);
let d0 = Diagram::Diagram0(g);
let cospan = Cospan::new(Rewrite::Identity, Rewrite::Identity);
let d1 = Diagram::DiagramN(DiagramN::new(d0, vec![cospan.clone(), cospan]));
// 1-points should have: r₀, s₀, r₁, s₁, r₂
let pts = d1.points(1);
assert_eq!(pts.len(), 5);
// Covering relations: r₀ < s₀ < r₁ < s₁ < r₂
assert_eq!(pts.covers().len(), 4);
}
// =========================================================================
// Injectification tests
// =========================================================================
#[test]
fn test_injectify_empty() {
let poset: Poset<i32> = Poset::empty();
let result = injectify(&poset);
assert!(result.poset.is_empty());
}
#[test]
fn test_injectify_singleton() {
let mut poset: Poset<i32> = Poset::empty();
poset.add_element(42);
let result = injectify(&poset);
assert_eq!(result.poset.len(), 1);
assert_eq!(result.poset.elements()[0], 42);
}
#[test]
fn test_injectify_chain() {
// Chain a < b < c should remain unchanged
let mut poset: Poset<i32> = Poset::empty();
let a = poset.add_element(1);
let b = poset.add_element(2);
let c = poset.add_element(3);
poset.add_cover(a, b);
poset.add_cover(b, c);
let result = injectify(&poset);
assert_eq!(result.poset.len(), 3);
assert_eq!(result.poset.covers().len(), 2);
}
#[test]
fn test_injectify_parallel() {
// Two parallel elements (incomparable with same predecessors/successors)
// should be identified
let mut poset: Poset<i32> = Poset::empty();
let bottom = poset.add_element(0);
let left = poset.add_element(1);
let right = poset.add_element(2);
// bottom < left and bottom < right, but left and right are unrelated
// They have the same downward closure {bottom, self}... but actually
// left's closure is {bottom, left} and right's is {bottom, right}
// which are different, so they won't be merged.
poset.add_cover(bottom, left);
poset.add_cover(bottom, right);
let result = injectify(&poset);
// All three should remain since they have different down-closures
assert_eq!(result.poset.len(), 3);
}
#[test]
fn test_reduce_to_covers() {
// Create a poset with transitive edges
let mut poset: Poset<i32> = Poset::empty();
let a = poset.add_element(1);
let b = poset.add_element(2);
let c = poset.add_element(3);
poset.add_cover(a, b);
poset.add_cover(b, c);
poset.add_cover(a, c); // This is transitive, should be removed
let reduced = reduce_to_covers(&poset);
assert_eq!(reduced.covers().len(), 2);
// Check that a->c is not in the covers
let covers_set: HashSet<_> = reduced.covers().iter().collect();
assert!(covers_set.contains(&(a, b)));
assert!(covers_set.contains(&(b, c)));
assert!(!covers_set.contains(&(a, c)));
}
#[test]
fn test_are_layout_equivalent() {
let mut poset: Poset<Point> = Poset::empty();
let p0 = poset.add_element(Point::new(vec![HeightLabel::Regular(0)]));
let p1 = poset.add_element(Point::new(vec![HeightLabel::Singular(0)]));
poset.add_cover(p0, p1);
// p0 and p1 are not equivalent (different downward closures)
assert!(!are_layout_equivalent(&poset, p0, p1));
// Self is equivalent
assert!(are_layout_equivalent(&poset, p0, p0));
}
}

View file

@ -44,5 +44,5 @@ pub use diagram::{Diagram, DiagramN, Cospan, Rewrite, Cone};
pub use signature::{Generator, Signature};
pub use normalise::{NormalisationResult, normalise, normalise_sink};
pub use typecheck::{TypeError, type_check};
pub use explosion::{Point, HeightLabel};
pub use explosion::{Point, HeightLabel, Poset, InjectificationResult, injectify};
pub use layout::{Layout, LayoutConstraints, SpringConstraint};