Merge feat/explosion: k-points, poset structure
This commit is contained in:
commit
c352384e92
2 changed files with 883 additions and 28 deletions
909
src/explosion.rs
909
src/explosion.rs
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue