Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
597
vendor/ruvector/crates/prime-radiant/src/cohomology/simplex.rs
vendored
Normal file
597
vendor/ruvector/crates/prime-radiant/src/cohomology/simplex.rs
vendored
Normal file
@@ -0,0 +1,597 @@
|
||||
//! Simplicial Complex and Chain Complex Types
|
||||
//!
|
||||
//! This module provides the foundational types for simplicial complexes
|
||||
//! and chain complexes used in cohomology computations.
|
||||
|
||||
use crate::substrate::NodeId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// Unique identifier for a simplex
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct SimplexId(pub u64);
|
||||
|
||||
impl SimplexId {
|
||||
/// Create a new simplex ID
|
||||
pub fn new(id: u64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
/// Compute ID from vertex set (deterministic)
|
||||
pub fn from_vertices(vertices: &BTreeSet<NodeId>) -> Self {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
let mut hasher = DefaultHasher::new();
|
||||
for v in vertices {
|
||||
v.hash(&mut hasher);
|
||||
}
|
||||
Self(hasher.finish())
|
||||
}
|
||||
}
|
||||
|
||||
/// A simplex in a simplicial complex
|
||||
///
|
||||
/// An n-simplex is a set of n+1 vertices. For example:
|
||||
/// - 0-simplex: a single vertex (node)
|
||||
/// - 1-simplex: an edge (pair of nodes)
|
||||
/// - 2-simplex: a triangle (triple of nodes)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Simplex {
|
||||
/// Unique identifier
|
||||
pub id: SimplexId,
|
||||
/// Ordered set of vertices (using BTreeSet for canonical ordering)
|
||||
pub vertices: BTreeSet<NodeId>,
|
||||
/// Dimension of the simplex (number of vertices - 1)
|
||||
pub dimension: usize,
|
||||
/// Optional weight for weighted computations
|
||||
pub weight: f64,
|
||||
}
|
||||
|
||||
impl Simplex {
|
||||
/// Create a new simplex from vertices
|
||||
pub fn new(vertices: impl IntoIterator<Item = NodeId>) -> Self {
|
||||
let vertices: BTreeSet<NodeId> = vertices.into_iter().collect();
|
||||
let dimension = if vertices.is_empty() {
|
||||
0
|
||||
} else {
|
||||
vertices.len() - 1
|
||||
};
|
||||
let id = SimplexId::from_vertices(&vertices);
|
||||
Self {
|
||||
id,
|
||||
vertices,
|
||||
dimension,
|
||||
weight: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a simplex with a specific weight
|
||||
pub fn with_weight(mut self, weight: f64) -> Self {
|
||||
self.weight = weight;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the boundary of this simplex (faces of dimension n-1)
|
||||
///
|
||||
/// The boundary of an n-simplex [v0, v1, ..., vn] is the alternating sum:
|
||||
/// sum_{i=0}^n (-1)^i [v0, ..., v_{i-1}, v_{i+1}, ..., vn]
|
||||
pub fn boundary(&self) -> Vec<(Simplex, i8)> {
|
||||
if self.dimension == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let vertices: Vec<NodeId> = self.vertices.iter().copied().collect();
|
||||
let mut faces = Vec::with_capacity(vertices.len());
|
||||
|
||||
for (i, _) in vertices.iter().enumerate() {
|
||||
let mut face_vertices = BTreeSet::new();
|
||||
for (j, &v) in vertices.iter().enumerate() {
|
||||
if i != j {
|
||||
face_vertices.insert(v);
|
||||
}
|
||||
}
|
||||
let face = Simplex {
|
||||
id: SimplexId::from_vertices(&face_vertices),
|
||||
vertices: face_vertices,
|
||||
dimension: self.dimension - 1,
|
||||
weight: self.weight,
|
||||
};
|
||||
let sign = if i % 2 == 0 { 1i8 } else { -1i8 };
|
||||
faces.push((face, sign));
|
||||
}
|
||||
|
||||
faces
|
||||
}
|
||||
|
||||
/// Check if this simplex contains a given vertex
|
||||
pub fn contains_vertex(&self, vertex: NodeId) -> bool {
|
||||
self.vertices.contains(&vertex)
|
||||
}
|
||||
|
||||
/// Check if this simplex is a face of another simplex
|
||||
pub fn is_face_of(&self, other: &Simplex) -> bool {
|
||||
self.dimension < other.dimension && self.vertices.is_subset(&other.vertices)
|
||||
}
|
||||
|
||||
/// Get the coboundary (simplices that have this as a face)
|
||||
/// Note: This requires the containing simplicial complex to compute
|
||||
pub fn vertices_as_vec(&self) -> Vec<NodeId> {
|
||||
self.vertices.iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Simplex {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.vertices == other.vertices
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Simplex {}
|
||||
|
||||
impl Hash for Simplex {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
// Use ordered iteration for consistent hashing
|
||||
for v in &self.vertices {
|
||||
v.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simplicial complex built from a graph
|
||||
///
|
||||
/// Contains simplices of various dimensions and tracks the incidence relations.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SimplicialComplex {
|
||||
/// Simplices organized by dimension
|
||||
pub simplices: HashMap<usize, HashMap<SimplexId, Simplex>>,
|
||||
/// Maximum dimension
|
||||
pub max_dimension: usize,
|
||||
/// Face relations: simplex -> its faces
|
||||
face_map: HashMap<SimplexId, Vec<SimplexId>>,
|
||||
/// Coface relations: simplex -> simplices it is a face of
|
||||
coface_map: HashMap<SimplexId, Vec<SimplexId>>,
|
||||
}
|
||||
|
||||
impl SimplicialComplex {
|
||||
/// Create a new empty simplicial complex
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
simplices: HashMap::new(),
|
||||
max_dimension: 0,
|
||||
face_map: HashMap::new(),
|
||||
coface_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a simplicial complex from a graph (flag complex / clique complex)
|
||||
///
|
||||
/// The flag complex has an n-simplex for every clique of n+1 vertices
|
||||
pub fn from_graph_cliques(
|
||||
nodes: &[NodeId],
|
||||
edges: &[(NodeId, NodeId)],
|
||||
max_dim: usize,
|
||||
) -> Self {
|
||||
let mut complex = Self::new();
|
||||
|
||||
// Build adjacency for clique detection
|
||||
let mut adjacency: HashMap<NodeId, HashSet<NodeId>> = HashMap::new();
|
||||
for &node in nodes {
|
||||
adjacency.insert(node, HashSet::new());
|
||||
}
|
||||
for &(u, v) in edges {
|
||||
adjacency.entry(u).or_default().insert(v);
|
||||
adjacency.entry(v).or_default().insert(u);
|
||||
}
|
||||
|
||||
// Add 0-simplices (vertices)
|
||||
for &node in nodes {
|
||||
let simplex = Simplex::new([node]);
|
||||
complex.add_simplex(simplex);
|
||||
}
|
||||
|
||||
// Add 1-simplices (edges)
|
||||
for &(u, v) in edges {
|
||||
let simplex = Simplex::new([u, v]);
|
||||
complex.add_simplex(simplex);
|
||||
}
|
||||
|
||||
// Find higher-dimensional cliques using Bron-Kerbosch algorithm
|
||||
if max_dim >= 2 {
|
||||
let all_nodes: HashSet<NodeId> = nodes.iter().copied().collect();
|
||||
Self::find_cliques_recursive(
|
||||
&mut complex,
|
||||
&adjacency,
|
||||
BTreeSet::new(),
|
||||
all_nodes,
|
||||
HashSet::new(),
|
||||
max_dim,
|
||||
);
|
||||
}
|
||||
|
||||
complex.build_incidence_maps();
|
||||
complex
|
||||
}
|
||||
|
||||
/// Bron-Kerbosch algorithm for finding cliques
|
||||
fn find_cliques_recursive(
|
||||
complex: &mut SimplicialComplex,
|
||||
adjacency: &HashMap<NodeId, HashSet<NodeId>>,
|
||||
r: BTreeSet<NodeId>,
|
||||
mut p: HashSet<NodeId>,
|
||||
mut x: HashSet<NodeId>,
|
||||
max_dim: usize,
|
||||
) {
|
||||
if p.is_empty() && x.is_empty() {
|
||||
if r.len() >= 3 && r.len() <= max_dim + 1 {
|
||||
let simplex = Simplex::new(r.iter().copied());
|
||||
complex.add_simplex(simplex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let pivot = p.iter().chain(x.iter()).next().copied();
|
||||
if let Some(pivot_node) = pivot {
|
||||
let pivot_neighbors = adjacency.get(&pivot_node).cloned().unwrap_or_default();
|
||||
let candidates: Vec<NodeId> = p.difference(&pivot_neighbors).copied().collect();
|
||||
|
||||
for v in candidates {
|
||||
let v_neighbors = adjacency.get(&v).cloned().unwrap_or_default();
|
||||
let mut new_r = r.clone();
|
||||
new_r.insert(v);
|
||||
let new_p: HashSet<NodeId> = p.intersection(&v_neighbors).copied().collect();
|
||||
let new_x: HashSet<NodeId> = x.intersection(&v_neighbors).copied().collect();
|
||||
|
||||
Self::find_cliques_recursive(complex, adjacency, new_r, new_p, new_x, max_dim);
|
||||
|
||||
p.remove(&v);
|
||||
x.insert(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a simplex to the complex
|
||||
pub fn add_simplex(&mut self, simplex: Simplex) {
|
||||
let dim = simplex.dimension;
|
||||
self.max_dimension = self.max_dimension.max(dim);
|
||||
self.simplices
|
||||
.entry(dim)
|
||||
.or_default()
|
||||
.insert(simplex.id, simplex);
|
||||
}
|
||||
|
||||
/// Build the face and coface incidence maps
|
||||
fn build_incidence_maps(&mut self) {
|
||||
self.face_map.clear();
|
||||
self.coface_map.clear();
|
||||
|
||||
// For each simplex, compute its faces
|
||||
for dim in 1..=self.max_dimension {
|
||||
if let Some(simplices) = self.simplices.get(&dim) {
|
||||
for (id, simplex) in simplices {
|
||||
let faces = simplex.boundary();
|
||||
let face_ids: Vec<SimplexId> = faces.iter().map(|(f, _)| f.id).collect();
|
||||
self.face_map.insert(*id, face_ids.clone());
|
||||
|
||||
// Update coface map
|
||||
for face_id in face_ids {
|
||||
self.coface_map.entry(face_id).or_default().push(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get simplices of a specific dimension
|
||||
pub fn simplices_of_dim(&self, dim: usize) -> impl Iterator<Item = &Simplex> {
|
||||
self.simplices
|
||||
.get(&dim)
|
||||
.into_iter()
|
||||
.flat_map(|s| s.values())
|
||||
}
|
||||
|
||||
/// Get a simplex by ID
|
||||
pub fn get_simplex(&self, id: SimplexId) -> Option<&Simplex> {
|
||||
for simplices in self.simplices.values() {
|
||||
if let Some(s) = simplices.get(&id) {
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the faces of a simplex
|
||||
pub fn faces(&self, id: SimplexId) -> Option<&[SimplexId]> {
|
||||
self.face_map.get(&id).map(|v| v.as_slice())
|
||||
}
|
||||
|
||||
/// Get the cofaces (simplices that have this as a face)
|
||||
pub fn cofaces(&self, id: SimplexId) -> Option<&[SimplexId]> {
|
||||
self.coface_map.get(&id).map(|v| v.as_slice())
|
||||
}
|
||||
|
||||
/// Count simplices of each dimension
|
||||
pub fn simplex_counts(&self) -> Vec<usize> {
|
||||
(0..=self.max_dimension)
|
||||
.map(|d| self.simplices.get(&d).map(|s| s.len()).unwrap_or(0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Total number of simplices
|
||||
pub fn total_simplices(&self) -> usize {
|
||||
self.simplices.values().map(|s| s.len()).sum()
|
||||
}
|
||||
|
||||
/// Euler characteristic: sum(-1)^n * |K_n|
|
||||
pub fn euler_characteristic(&self) -> i64 {
|
||||
let mut chi = 0i64;
|
||||
for (dim, simplices) in &self.simplices {
|
||||
let count = simplices.len() as i64;
|
||||
if dim % 2 == 0 {
|
||||
chi += count;
|
||||
} else {
|
||||
chi -= count;
|
||||
}
|
||||
}
|
||||
chi
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SimplicialComplex {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// A chain in the chain complex C_n(K)
|
||||
///
|
||||
/// Represents a formal sum of n-simplices with coefficients
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Chain {
|
||||
/// Dimension of the chain
|
||||
pub dimension: usize,
|
||||
/// Simplex coefficients (simplex ID -> coefficient)
|
||||
pub coefficients: HashMap<SimplexId, f64>,
|
||||
}
|
||||
|
||||
impl Chain {
|
||||
/// Create a zero chain of given dimension
|
||||
pub fn zero(dimension: usize) -> Self {
|
||||
Self {
|
||||
dimension,
|
||||
coefficients: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a chain from a single simplex
|
||||
pub fn from_simplex(simplex: &Simplex, coefficient: f64) -> Self {
|
||||
let mut coefficients = HashMap::new();
|
||||
if coefficient.abs() > 1e-10 {
|
||||
coefficients.insert(simplex.id, coefficient);
|
||||
}
|
||||
Self {
|
||||
dimension: simplex.dimension,
|
||||
coefficients,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a simplex to the chain
|
||||
pub fn add_simplex(&mut self, id: SimplexId, coefficient: f64) {
|
||||
if coefficient.abs() > 1e-10 {
|
||||
*self.coefficients.entry(id).or_insert(0.0) += coefficient;
|
||||
// Remove if coefficient is now essentially zero
|
||||
if self
|
||||
.coefficients
|
||||
.get(&id)
|
||||
.map(|c| c.abs() < 1e-10)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.coefficients.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale the chain by a constant
|
||||
pub fn scale(&mut self, factor: f64) {
|
||||
for coeff in self.coefficients.values_mut() {
|
||||
*coeff *= factor;
|
||||
}
|
||||
}
|
||||
|
||||
/// Add another chain to this one
|
||||
pub fn add(&mut self, other: &Chain) {
|
||||
assert_eq!(
|
||||
self.dimension, other.dimension,
|
||||
"Chain dimensions must match"
|
||||
);
|
||||
for (&id, &coeff) in &other.coefficients {
|
||||
self.add_simplex(id, coeff);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if chain is zero
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.coefficients.is_empty()
|
||||
}
|
||||
|
||||
/// L2 norm of the chain
|
||||
pub fn norm(&self) -> f64 {
|
||||
self.coefficients
|
||||
.values()
|
||||
.map(|c| c * c)
|
||||
.sum::<f64>()
|
||||
.sqrt()
|
||||
}
|
||||
}
|
||||
|
||||
/// A cochain in the cochain complex C^n(K)
|
||||
///
|
||||
/// Represents a function from n-simplices to R
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Cochain {
|
||||
/// Dimension of the cochain
|
||||
pub dimension: usize,
|
||||
/// Values on simplices (simplex ID -> value)
|
||||
pub values: HashMap<SimplexId, f64>,
|
||||
}
|
||||
|
||||
impl Cochain {
|
||||
/// Create a zero cochain of given dimension
|
||||
pub fn zero(dimension: usize) -> Self {
|
||||
Self {
|
||||
dimension,
|
||||
values: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a cochain from values
|
||||
pub fn from_values(dimension: usize, values: HashMap<SimplexId, f64>) -> Self {
|
||||
Self { dimension, values }
|
||||
}
|
||||
|
||||
/// Set the value on a simplex
|
||||
pub fn set(&mut self, id: SimplexId, value: f64) {
|
||||
if value.abs() > 1e-10 {
|
||||
self.values.insert(id, value);
|
||||
} else {
|
||||
self.values.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the value on a simplex
|
||||
pub fn get(&self, id: SimplexId) -> f64 {
|
||||
self.values.get(&id).copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Evaluate the cochain on a chain (inner product)
|
||||
pub fn evaluate(&self, chain: &Chain) -> f64 {
|
||||
assert_eq!(self.dimension, chain.dimension, "Dimensions must match");
|
||||
let mut sum = 0.0;
|
||||
for (&id, &coeff) in &chain.coefficients {
|
||||
sum += coeff * self.get(id);
|
||||
}
|
||||
sum
|
||||
}
|
||||
|
||||
/// Add another cochain to this one
|
||||
pub fn add(&mut self, other: &Cochain) {
|
||||
assert_eq!(
|
||||
self.dimension, other.dimension,
|
||||
"Cochain dimensions must match"
|
||||
);
|
||||
for (&id, &value) in &other.values {
|
||||
let new_val = self.get(id) + value;
|
||||
self.set(id, new_val);
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale the cochain
|
||||
pub fn scale(&mut self, factor: f64) {
|
||||
for value in self.values.values_mut() {
|
||||
*value *= factor;
|
||||
}
|
||||
}
|
||||
|
||||
/// L2 norm of the cochain
|
||||
pub fn norm(&self) -> f64 {
|
||||
self.values.values().map(|v| v * v).sum::<f64>().sqrt()
|
||||
}
|
||||
|
||||
/// Check if cochain is zero
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.values.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn make_node_id() -> NodeId {
|
||||
Uuid::new_v4()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simplex_creation() {
|
||||
let v0 = make_node_id();
|
||||
let v1 = make_node_id();
|
||||
let v2 = make_node_id();
|
||||
|
||||
let vertex = Simplex::new([v0]);
|
||||
assert_eq!(vertex.dimension, 0);
|
||||
|
||||
let edge = Simplex::new([v0, v1]);
|
||||
assert_eq!(edge.dimension, 1);
|
||||
|
||||
let triangle = Simplex::new([v0, v1, v2]);
|
||||
assert_eq!(triangle.dimension, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simplex_boundary() {
|
||||
let v0 = make_node_id();
|
||||
let v1 = make_node_id();
|
||||
let v2 = make_node_id();
|
||||
|
||||
// Boundary of edge [v0, v1] = v1 - v0
|
||||
let edge = Simplex::new([v0, v1]);
|
||||
let boundary = edge.boundary();
|
||||
assert_eq!(boundary.len(), 2);
|
||||
|
||||
// Boundary of triangle [v0, v1, v2] = [v1,v2] - [v0,v2] + [v0,v1]
|
||||
let triangle = Simplex::new([v0, v1, v2]);
|
||||
let boundary = triangle.boundary();
|
||||
assert_eq!(boundary.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simplicial_complex() {
|
||||
let v0 = make_node_id();
|
||||
let v1 = make_node_id();
|
||||
let v2 = make_node_id();
|
||||
|
||||
let nodes = vec![v0, v1, v2];
|
||||
let edges = vec![(v0, v1), (v1, v2), (v0, v2)];
|
||||
|
||||
let complex = SimplicialComplex::from_graph_cliques(&nodes, &edges, 2);
|
||||
|
||||
// Should have 3 vertices, 3 edges, 1 triangle
|
||||
let counts = complex.simplex_counts();
|
||||
assert_eq!(counts[0], 3);
|
||||
assert_eq!(counts[1], 3);
|
||||
assert_eq!(counts[2], 1);
|
||||
|
||||
// Euler characteristic: 3 - 3 + 1 = 1
|
||||
assert_eq!(complex.euler_characteristic(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_operations() {
|
||||
let v0 = make_node_id();
|
||||
let v1 = make_node_id();
|
||||
|
||||
let simplex = Simplex::new([v0, v1]);
|
||||
let mut chain = Chain::from_simplex(&simplex, 2.0);
|
||||
|
||||
assert_eq!(chain.dimension, 1);
|
||||
assert!(!chain.is_zero());
|
||||
|
||||
chain.scale(0.5);
|
||||
assert_eq!(chain.coefficients.get(&simplex.id), Some(&1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cochain_evaluation() {
|
||||
let v0 = make_node_id();
|
||||
let v1 = make_node_id();
|
||||
|
||||
let simplex = Simplex::new([v0, v1]);
|
||||
let chain = Chain::from_simplex(&simplex, 3.0);
|
||||
|
||||
let mut cochain = Cochain::zero(1);
|
||||
cochain.set(simplex.id, 2.0);
|
||||
|
||||
// Inner product: 3.0 * 2.0 = 6.0
|
||||
assert!((cochain.evaluate(&chain) - 6.0).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user