Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
182
vendor/ruvector/examples/prime-radiant/src/cohomology/chain_complex.rs
vendored
Normal file
182
vendor/ruvector/examples/prime-radiant/src/cohomology/chain_complex.rs
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
//! Chain complex implementation
|
||||
|
||||
use super::Homology;
|
||||
use crate::{Error, Result};
|
||||
use nalgebra::DMatrix;
|
||||
|
||||
/// A chain complex for computing homology
|
||||
///
|
||||
/// A chain complex is a sequence of abelian groups (vector spaces) connected
|
||||
/// by boundary maps: ... -> C_{n+1} -d_{n+1}-> C_n -d_n-> C_{n-1} -> ...
|
||||
///
|
||||
/// The key property is d_n ∘ d_{n+1} = 0 (boundary of boundary is zero).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChainComplex {
|
||||
/// Boundary maps d_n: C_n -> C_{n-1}
|
||||
boundary_maps: Vec<DMatrix<f64>>,
|
||||
}
|
||||
|
||||
impl ChainComplex {
|
||||
/// Create a new chain complex from boundary maps
|
||||
pub fn new(boundary_maps: Vec<DMatrix<f64>>) -> Self {
|
||||
Self { boundary_maps }
|
||||
}
|
||||
|
||||
/// Create a chain complex from dimensions and explicit maps
|
||||
pub fn from_dimensions(dimensions: &[usize]) -> Self {
|
||||
let mut maps = Vec::new();
|
||||
for i in 1..dimensions.len() {
|
||||
maps.push(DMatrix::zeros(dimensions[i - 1], dimensions[i]));
|
||||
}
|
||||
Self::new(maps)
|
||||
}
|
||||
|
||||
/// Get the number of chain groups
|
||||
pub fn length(&self) -> usize {
|
||||
self.boundary_maps.len() + 1
|
||||
}
|
||||
|
||||
/// Get the n-th boundary map
|
||||
pub fn boundary(&self, n: usize) -> Option<&DMatrix<f64>> {
|
||||
self.boundary_maps.get(n)
|
||||
}
|
||||
|
||||
/// Set the n-th boundary map
|
||||
pub fn set_boundary(&mut self, n: usize, map: DMatrix<f64>) -> Result<()> {
|
||||
if n >= self.boundary_maps.len() {
|
||||
return Err(Error::InvalidTopology(format!(
|
||||
"Boundary index {} out of range",
|
||||
n
|
||||
)));
|
||||
}
|
||||
self.boundary_maps[n] = map;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check the chain complex property: d ∘ d = 0
|
||||
pub fn verify(&self, epsilon: f64) -> Result<bool> {
|
||||
for i in 0..self.boundary_maps.len().saturating_sub(1) {
|
||||
let d_i = &self.boundary_maps[i];
|
||||
let d_i1 = &self.boundary_maps[i + 1];
|
||||
|
||||
// Check dimensions are compatible
|
||||
if d_i.ncols() != d_i1.nrows() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check d_i ∘ d_{i+1} = 0
|
||||
let composition = d_i * d_i1;
|
||||
if composition.norm() > epsilon {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Compute the n-th homology group H_n = ker(d_n) / im(d_{n+1})
|
||||
pub fn homology(&self, n: usize) -> Result<Homology> {
|
||||
// Get the relevant boundary maps
|
||||
let d_n = self.boundary_maps.get(n);
|
||||
let d_n1 = if n + 1 < self.boundary_maps.len() {
|
||||
Some(&self.boundary_maps[n + 1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Compute kernel of d_n
|
||||
let kernel_dim = if let Some(d) = d_n {
|
||||
compute_kernel_dimension(d)
|
||||
} else {
|
||||
// If no outgoing boundary, kernel is everything
|
||||
if n > 0 && n - 1 < self.boundary_maps.len() {
|
||||
self.boundary_maps[n - 1].ncols()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
// Compute image of d_{n+1}
|
||||
let image_dim = if let Some(d) = d_n1 {
|
||||
compute_image_dimension(d)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Homology dimension = dim(ker) - dim(im)
|
||||
let homology_dim = kernel_dim.saturating_sub(image_dim);
|
||||
|
||||
Ok(Homology::new(n, homology_dim))
|
||||
}
|
||||
|
||||
/// Compute all homology groups
|
||||
pub fn all_homology(&self) -> Result<Vec<Homology>> {
|
||||
let mut result = Vec::new();
|
||||
for n in 0..self.length() {
|
||||
result.push(self.homology(n)?);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get the Betti numbers
|
||||
pub fn betti_numbers(&self) -> Result<Vec<usize>> {
|
||||
let homology = self.all_homology()?;
|
||||
Ok(homology.iter().map(|h| h.dimension()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the dimension of the kernel of a matrix
|
||||
fn compute_kernel_dimension(matrix: &DMatrix<f64>) -> usize {
|
||||
// Use SVD to compute rank, kernel dimension = ncols - rank
|
||||
let svd = matrix.clone().svd(false, false);
|
||||
let singular_values = svd.singular_values;
|
||||
|
||||
let threshold = 1e-10;
|
||||
let rank = singular_values.iter().filter(|&&s| s > threshold).count();
|
||||
|
||||
matrix.ncols().saturating_sub(rank)
|
||||
}
|
||||
|
||||
/// Compute the dimension of the image of a matrix
|
||||
fn compute_image_dimension(matrix: &DMatrix<f64>) -> usize {
|
||||
// Image dimension = rank
|
||||
let svd = matrix.clone().svd(false, false);
|
||||
let singular_values = svd.singular_values;
|
||||
|
||||
let threshold = 1e-10;
|
||||
singular_values.iter().filter(|&&s| s > threshold).count()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_chain_complex_creation() {
|
||||
let d0 = DMatrix::from_row_slice(2, 3, &[1.0, -1.0, 0.0, 0.0, 1.0, -1.0]);
|
||||
|
||||
let complex = ChainComplex::new(vec![d0]);
|
||||
assert_eq!(complex.length(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kernel_dimension() {
|
||||
// Identity matrix has trivial kernel
|
||||
let identity = DMatrix::identity(3, 3);
|
||||
assert_eq!(compute_kernel_dimension(&identity), 0);
|
||||
|
||||
// Zero matrix has full kernel
|
||||
let zero = DMatrix::zeros(2, 3);
|
||||
assert_eq!(compute_kernel_dimension(&zero), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_image_dimension() {
|
||||
// Identity matrix has full image
|
||||
let identity = DMatrix::identity(3, 3);
|
||||
assert_eq!(compute_image_dimension(&identity), 3);
|
||||
|
||||
// Zero matrix has trivial image
|
||||
let zero = DMatrix::zeros(2, 3);
|
||||
assert_eq!(compute_image_dimension(&zero), 0);
|
||||
}
|
||||
}
|
||||
177
vendor/ruvector/examples/prime-radiant/src/cohomology/homology.rs
vendored
Normal file
177
vendor/ruvector/examples/prime-radiant/src/cohomology/homology.rs
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
//! Homology group implementation
|
||||
|
||||
use nalgebra::DVector;
|
||||
|
||||
/// A homology group H_n
|
||||
///
|
||||
/// Homology groups measure "holes" in topological spaces:
|
||||
/// - H_0: connected components
|
||||
/// - H_1: loops/tunnels
|
||||
/// - H_2: voids/cavities
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Homology {
|
||||
/// Degree n of the homology group
|
||||
degree: usize,
|
||||
/// Dimension of the homology group (Betti number)
|
||||
dimension: usize,
|
||||
/// Generators of the homology group (representative cycles)
|
||||
generators: Vec<DVector<f64>>,
|
||||
}
|
||||
|
||||
impl Homology {
|
||||
/// Create a new homology group
|
||||
pub fn new(degree: usize, dimension: usize) -> Self {
|
||||
Self {
|
||||
degree,
|
||||
dimension,
|
||||
generators: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a homology group with generators
|
||||
pub fn with_generators(degree: usize, generators: Vec<DVector<f64>>) -> Self {
|
||||
let dimension = generators.len();
|
||||
Self {
|
||||
degree,
|
||||
dimension,
|
||||
generators,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the degree of the homology group
|
||||
pub fn degree(&self) -> usize {
|
||||
self.degree
|
||||
}
|
||||
|
||||
/// Get the dimension (Betti number)
|
||||
pub fn dimension(&self) -> usize {
|
||||
self.dimension
|
||||
}
|
||||
|
||||
/// Get the generators
|
||||
pub fn generators(&self) -> &[DVector<f64>] {
|
||||
&self.generators
|
||||
}
|
||||
|
||||
/// Check if the homology group is trivial
|
||||
pub fn is_trivial(&self) -> bool {
|
||||
self.dimension == 0
|
||||
}
|
||||
|
||||
/// Set the generators
|
||||
pub fn set_generators(&mut self, generators: Vec<DVector<f64>>) {
|
||||
self.dimension = generators.len();
|
||||
self.generators = generators;
|
||||
}
|
||||
|
||||
/// Add a generator
|
||||
pub fn add_generator(&mut self, generator: DVector<f64>) {
|
||||
self.generators.push(generator);
|
||||
self.dimension += 1;
|
||||
}
|
||||
|
||||
/// Check if a cycle is a boundary (homologous to zero)
|
||||
pub fn is_boundary(&self, cycle: &DVector<f64>, epsilon: f64) -> bool {
|
||||
// A cycle is a boundary if it's in the span of boundaries
|
||||
// For now, check if it's close to zero
|
||||
cycle.norm() < epsilon
|
||||
}
|
||||
|
||||
/// Compute the homology class of a cycle
|
||||
pub fn classify(&self, cycle: &DVector<f64>) -> HomologyClass {
|
||||
if self.generators.is_empty() {
|
||||
return HomologyClass::Zero;
|
||||
}
|
||||
|
||||
// Project onto generator space
|
||||
let mut coefficients = Vec::new();
|
||||
for gen in &self.generators {
|
||||
let coeff = cycle.dot(gen) / gen.dot(gen);
|
||||
coefficients.push(coeff);
|
||||
}
|
||||
|
||||
HomologyClass::NonTrivial(coefficients)
|
||||
}
|
||||
}
|
||||
|
||||
/// A homology class [α] in H_n
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HomologyClass {
|
||||
/// The zero class
|
||||
Zero,
|
||||
/// Non-trivial class with coefficients in terms of generators
|
||||
NonTrivial(Vec<f64>),
|
||||
}
|
||||
|
||||
impl HomologyClass {
|
||||
/// Check if this is the zero class
|
||||
pub fn is_zero(&self) -> bool {
|
||||
matches!(self, HomologyClass::Zero)
|
||||
}
|
||||
|
||||
/// Get the coefficients if non-trivial
|
||||
pub fn coefficients(&self) -> Option<&[f64]> {
|
||||
match self {
|
||||
HomologyClass::Zero => None,
|
||||
HomologyClass::NonTrivial(c) => Some(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative homology H_n(X, A)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RelativeHomology {
|
||||
/// Degree
|
||||
degree: usize,
|
||||
/// Space X
|
||||
space: Homology,
|
||||
/// Subspace A
|
||||
subspace: Homology,
|
||||
/// Relative homology dimension
|
||||
dimension: usize,
|
||||
}
|
||||
|
||||
impl RelativeHomology {
|
||||
/// Create new relative homology
|
||||
pub fn new(degree: usize, space: Homology, subspace: Homology) -> Self {
|
||||
// Long exact sequence: ... -> H_n(A) -> H_n(X) -> H_n(X,A) -> H_{n-1}(A) -> ...
|
||||
let dimension = space.dimension().saturating_sub(subspace.dimension());
|
||||
Self {
|
||||
degree,
|
||||
space,
|
||||
subspace,
|
||||
dimension,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the dimension
|
||||
pub fn dimension(&self) -> usize {
|
||||
self.dimension
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_homology_creation() {
|
||||
let h1 = Homology::new(1, 2);
|
||||
assert_eq!(h1.degree(), 1);
|
||||
assert_eq!(h1.dimension(), 2);
|
||||
assert!(!h1.is_trivial());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trivial_homology() {
|
||||
let h0 = Homology::new(0, 0);
|
||||
assert!(h0.is_trivial());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_homology_class() {
|
||||
let class = HomologyClass::NonTrivial(vec![1.0, 2.0]);
|
||||
assert!(!class.is_zero());
|
||||
assert_eq!(class.coefficients().unwrap(), &[1.0, 2.0]);
|
||||
}
|
||||
}
|
||||
695
vendor/ruvector/examples/prime-radiant/src/cohomology/mod.rs
vendored
Normal file
695
vendor/ruvector/examples/prime-radiant/src/cohomology/mod.rs
vendored
Normal file
@@ -0,0 +1,695 @@
|
||||
//! Sheaf Cohomology Module for Prime-Radiant
|
||||
//!
|
||||
//! This module provides sheaf cohomology computations for detecting global obstructions
|
||||
//! to local consistency in belief networks. Key capabilities:
|
||||
//!
|
||||
//! - **SheafGraph**: Directed graph with local sections on nodes
|
||||
//! - **CohomologyEngine**: Computes cohomology groups H^i and obstruction classes
|
||||
//! - **RestrictionMaps**: Linear maps between stalks encoding local compatibility
|
||||
//! - **Obstruction Detection**: Identifies global inconsistencies from local data
|
||||
//!
|
||||
//! ## Mathematical Background
|
||||
//!
|
||||
//! A sheaf F on a graph G assigns vector spaces F(U) to open sets U and restriction
|
||||
//! maps r_{UV}: F(V) -> F(U) for U ⊆ V satisfying:
|
||||
//! - r_{UU} = id
|
||||
//! - r_{UW} = r_{UV} ∘ r_{VW} for U ⊆ V ⊆ W
|
||||
//!
|
||||
//! The cohomology groups H^i(G, F) measure the failure of local sections to glue globally.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Error types for cohomology operations
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CohomologyError {
|
||||
/// Dimension mismatch between sections
|
||||
DimensionMismatch { expected: usize, got: usize },
|
||||
/// Invalid node index
|
||||
InvalidNode(usize),
|
||||
/// Invalid edge specification
|
||||
InvalidEdge(usize, usize),
|
||||
/// Singular matrix in computation
|
||||
SingularMatrix,
|
||||
/// Numerical error
|
||||
NumericalError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CohomologyError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::DimensionMismatch { expected, got } => {
|
||||
write!(f, "Dimension mismatch: expected {}, got {}", expected, got)
|
||||
}
|
||||
Self::InvalidNode(n) => write!(f, "Invalid node index: {}", n),
|
||||
Self::InvalidEdge(i, j) => write!(f, "Invalid edge: ({}, {})", i, j),
|
||||
Self::SingularMatrix => write!(f, "Singular matrix encountered"),
|
||||
Self::NumericalError(msg) => write!(f, "Numerical error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CohomologyError {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, CohomologyError>;
|
||||
|
||||
/// A node in the sheaf graph with local section data
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SheafNode {
|
||||
/// Node identifier
|
||||
pub id: usize,
|
||||
/// Node label
|
||||
pub label: String,
|
||||
/// Local section as a vector (stalk of the sheaf)
|
||||
pub section: Vec<f64>,
|
||||
/// Confidence weight for this node
|
||||
pub weight: f64,
|
||||
}
|
||||
|
||||
impl SheafNode {
|
||||
pub fn new(id: usize, label: impl Into<String>, section: Vec<f64>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
label: label.into(),
|
||||
section,
|
||||
weight: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_weight(mut self, weight: f64) -> Self {
|
||||
self.weight = weight;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn dimension(&self) -> usize {
|
||||
self.section.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// An edge with restriction map
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SheafEdge {
|
||||
/// Source node index
|
||||
pub source: usize,
|
||||
/// Target node index
|
||||
pub target: usize,
|
||||
/// Restriction map as a matrix (row-major, target_dim x source_dim)
|
||||
/// Maps source section to target section
|
||||
pub restriction_map: Vec<f64>,
|
||||
/// Source dimension
|
||||
pub source_dim: usize,
|
||||
/// Target dimension
|
||||
pub target_dim: usize,
|
||||
}
|
||||
|
||||
impl SheafEdge {
|
||||
/// Create an edge with identity restriction (sections must have same dimension)
|
||||
pub fn identity(source: usize, target: usize, dim: usize) -> Self {
|
||||
let mut restriction = vec![0.0; dim * dim];
|
||||
for i in 0..dim {
|
||||
restriction[i * dim + i] = 1.0;
|
||||
}
|
||||
Self {
|
||||
source,
|
||||
target,
|
||||
restriction_map: restriction,
|
||||
source_dim: dim,
|
||||
target_dim: dim,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an edge with a custom restriction map
|
||||
pub fn with_map(source: usize, target: usize, map: Vec<f64>, source_dim: usize, target_dim: usize) -> Self {
|
||||
Self {
|
||||
source,
|
||||
target,
|
||||
restriction_map: map,
|
||||
source_dim,
|
||||
target_dim,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply restriction map to a section
|
||||
pub fn apply(&self, section: &[f64]) -> Result<Vec<f64>> {
|
||||
if section.len() != self.source_dim {
|
||||
return Err(CohomologyError::DimensionMismatch {
|
||||
expected: self.source_dim,
|
||||
got: section.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut result = vec![0.0; self.target_dim];
|
||||
for i in 0..self.target_dim {
|
||||
for j in 0..self.source_dim {
|
||||
result[i] += self.restriction_map[i * self.source_dim + j] * section[j];
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// A sheaf on a graph
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SheafGraph {
|
||||
/// Nodes with local sections
|
||||
pub nodes: Vec<SheafNode>,
|
||||
/// Edges with restriction maps
|
||||
pub edges: Vec<SheafEdge>,
|
||||
}
|
||||
|
||||
impl SheafGraph {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: Vec::new(),
|
||||
edges: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_node(&mut self, node: SheafNode) -> usize {
|
||||
let id = self.nodes.len();
|
||||
self.nodes.push(node);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn add_edge(&mut self, edge: SheafEdge) -> Result<()> {
|
||||
if edge.source >= self.nodes.len() {
|
||||
return Err(CohomologyError::InvalidNode(edge.source));
|
||||
}
|
||||
if edge.target >= self.nodes.len() {
|
||||
return Err(CohomologyError::InvalidNode(edge.target));
|
||||
}
|
||||
self.edges.push(edge);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn node_count(&self) -> usize {
|
||||
self.nodes.len()
|
||||
}
|
||||
|
||||
pub fn edge_count(&self) -> usize {
|
||||
self.edges.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SheafGraph {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of cohomology computation
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CohomologyResult {
|
||||
/// Dimension of H^0 (global sections)
|
||||
pub h0_dim: usize,
|
||||
/// Dimension of H^1 (first obstruction group)
|
||||
pub h1_dim: usize,
|
||||
/// Euler characteristic χ = dim(H^0) - dim(H^1)
|
||||
pub euler_characteristic: i64,
|
||||
/// Local consistency energy (sum of squared restriction errors)
|
||||
pub consistency_energy: f64,
|
||||
/// Obstruction cocycle (if any)
|
||||
pub obstruction_cocycle: Option<Vec<f64>>,
|
||||
/// Is the sheaf globally consistent?
|
||||
pub is_consistent: bool,
|
||||
}
|
||||
|
||||
/// Detected obstruction to global consistency
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Obstruction {
|
||||
/// Edge where the obstruction is localized
|
||||
pub edge_index: usize,
|
||||
/// Source node
|
||||
pub source_node: usize,
|
||||
/// Target node
|
||||
pub target_node: usize,
|
||||
/// Obstruction vector (difference after restriction)
|
||||
pub obstruction_vector: Vec<f64>,
|
||||
/// Magnitude of the obstruction
|
||||
pub magnitude: f64,
|
||||
/// Description of the inconsistency
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Main cohomology computation engine
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CohomologyEngine {
|
||||
/// Tolerance for numerical comparisons
|
||||
pub tolerance: f64,
|
||||
}
|
||||
|
||||
impl CohomologyEngine {
|
||||
pub fn new() -> Self {
|
||||
Self { tolerance: 1e-10 }
|
||||
}
|
||||
|
||||
pub fn with_tolerance(tolerance: f64) -> Self {
|
||||
Self { tolerance }
|
||||
}
|
||||
|
||||
/// Compute cohomology groups of a sheaf graph
|
||||
pub fn compute_cohomology(&self, graph: &SheafGraph) -> Result<CohomologyResult> {
|
||||
if graph.nodes.is_empty() {
|
||||
return Ok(CohomologyResult {
|
||||
h0_dim: 0,
|
||||
h1_dim: 0,
|
||||
euler_characteristic: 0,
|
||||
consistency_energy: 0.0,
|
||||
obstruction_cocycle: None,
|
||||
is_consistent: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Compute the coboundary map d^0: C^0 -> C^1
|
||||
// C^0 = direct sum of stalks at vertices
|
||||
// C^1 = direct sum of stalks at edges (target stalks)
|
||||
|
||||
let c0_dim: usize = graph.nodes.iter().map(|n| n.dimension()).sum();
|
||||
let c1_dim: usize = graph.edges.iter().map(|e| e.target_dim).sum();
|
||||
|
||||
// Build the coboundary matrix
|
||||
let coboundary = self.build_coboundary_matrix(graph, c0_dim, c1_dim)?;
|
||||
|
||||
// Compute kernel dimension (H^0)
|
||||
let kernel_dim = self.compute_kernel_dimension(&coboundary, c0_dim, c1_dim);
|
||||
|
||||
// Compute image dimension
|
||||
let image_dim = self.compute_rank(&coboundary, c0_dim, c1_dim);
|
||||
|
||||
// H^1 = C^1 / Im(d^0) for this simplified case
|
||||
let h1_dim = if c1_dim > image_dim { c1_dim - image_dim } else { 0 };
|
||||
|
||||
// Compute consistency energy
|
||||
let (consistency_energy, obstruction) = self.compute_consistency_energy(graph)?;
|
||||
|
||||
let is_consistent = consistency_energy < self.tolerance;
|
||||
|
||||
Ok(CohomologyResult {
|
||||
h0_dim: kernel_dim,
|
||||
h1_dim,
|
||||
euler_characteristic: kernel_dim as i64 - h1_dim as i64,
|
||||
consistency_energy,
|
||||
obstruction_cocycle: obstruction,
|
||||
is_consistent,
|
||||
})
|
||||
}
|
||||
|
||||
/// Detect all obstructions to global consistency
|
||||
pub fn detect_obstructions(&self, graph: &SheafGraph) -> Result<Vec<Obstruction>> {
|
||||
let mut obstructions = Vec::new();
|
||||
|
||||
for (i, edge) in graph.edges.iter().enumerate() {
|
||||
let source = &graph.nodes[edge.source];
|
||||
let target = &graph.nodes[edge.target];
|
||||
|
||||
// Apply restriction map to source section
|
||||
let restricted = edge.apply(&source.section)?;
|
||||
|
||||
// Compare with target section
|
||||
let mut diff = Vec::with_capacity(edge.target_dim);
|
||||
let mut magnitude_sq = 0.0;
|
||||
|
||||
for j in 0..edge.target_dim.min(target.section.len()) {
|
||||
let d = restricted[j] - target.section[j];
|
||||
diff.push(d);
|
||||
magnitude_sq += d * d;
|
||||
}
|
||||
|
||||
let magnitude = magnitude_sq.sqrt();
|
||||
|
||||
if magnitude > self.tolerance {
|
||||
obstructions.push(Obstruction {
|
||||
edge_index: i,
|
||||
source_node: edge.source,
|
||||
target_node: edge.target,
|
||||
obstruction_vector: diff,
|
||||
magnitude,
|
||||
description: format!(
|
||||
"Inconsistency between '{}' and '{}': magnitude {:.6}",
|
||||
source.label, target.label, magnitude
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by magnitude (largest first)
|
||||
obstructions.sort_by(|a, b| b.magnitude.partial_cmp(&a.magnitude).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
Ok(obstructions)
|
||||
}
|
||||
|
||||
/// Compute the global section space (H^0)
|
||||
pub fn compute_global_sections(&self, graph: &SheafGraph) -> Result<Vec<Vec<f64>>> {
|
||||
if graph.nodes.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// For a simple connected graph, a global section must agree on all restrictions
|
||||
// We find sections that minimize the total restriction error
|
||||
|
||||
let dim = graph.nodes.get(0).map(|n| n.dimension()).unwrap_or(0);
|
||||
let mut global_sections = Vec::new();
|
||||
|
||||
// Start with the first node's section as a candidate
|
||||
if let Some(first_node) = graph.nodes.first() {
|
||||
let mut candidate = first_node.section.clone();
|
||||
|
||||
// Check if it's a valid global section
|
||||
let mut is_global = true;
|
||||
for edge in &graph.edges {
|
||||
let restricted = edge.apply(&graph.nodes[edge.source].section)?;
|
||||
let target = &graph.nodes[edge.target].section;
|
||||
|
||||
for j in 0..edge.target_dim.min(target.len()) {
|
||||
if (restricted[j] - target[j]).abs() > self.tolerance {
|
||||
is_global = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !is_global { break; }
|
||||
}
|
||||
|
||||
if is_global {
|
||||
global_sections.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find a global section by averaging (simple approach)
|
||||
if global_sections.is_empty() && !graph.nodes.is_empty() {
|
||||
let dim = graph.nodes[0].dimension();
|
||||
let mut avg = vec![0.0; dim];
|
||||
let mut total_weight = 0.0;
|
||||
|
||||
for node in &graph.nodes {
|
||||
for j in 0..dim.min(node.section.len()) {
|
||||
avg[j] += node.section[j] * node.weight;
|
||||
}
|
||||
total_weight += node.weight;
|
||||
}
|
||||
|
||||
if total_weight > 0.0 {
|
||||
for v in &mut avg {
|
||||
*v /= total_weight;
|
||||
}
|
||||
global_sections.push(avg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(global_sections)
|
||||
}
|
||||
|
||||
/// Repair local sections to achieve global consistency
|
||||
pub fn repair_sections(&self, graph: &mut SheafGraph) -> Result<f64> {
|
||||
// Iterative repair: adjust sections to minimize total restriction error
|
||||
let mut total_adjustment = 0.0;
|
||||
let max_iterations = 100;
|
||||
let learning_rate = 0.5;
|
||||
|
||||
for _ in 0..max_iterations {
|
||||
let mut iteration_adjustment = 0.0;
|
||||
|
||||
for edge in &graph.edges {
|
||||
let source = &graph.nodes[edge.source];
|
||||
let target = &graph.nodes[edge.target];
|
||||
|
||||
// Apply restriction
|
||||
let restricted = edge.apply(&source.section)?;
|
||||
|
||||
// Compute gradient for target adjustment
|
||||
let mut gradient = Vec::with_capacity(edge.target_dim);
|
||||
for j in 0..edge.target_dim.min(target.section.len()) {
|
||||
gradient.push(restricted[j] - target.section[j]);
|
||||
}
|
||||
|
||||
// Apply adjustment (weighted by node weights)
|
||||
let source_weight = source.weight;
|
||||
let target_weight = target.weight;
|
||||
let total_w = source_weight + target_weight;
|
||||
|
||||
if total_w > 0.0 {
|
||||
// Adjust target
|
||||
let target_node = &mut graph.nodes[edge.target];
|
||||
for j in 0..gradient.len().min(target_node.section.len()) {
|
||||
let adj = learning_rate * gradient[j] * source_weight / total_w;
|
||||
target_node.section[j] += adj;
|
||||
iteration_adjustment += adj.abs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total_adjustment += iteration_adjustment;
|
||||
|
||||
if iteration_adjustment < self.tolerance {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total_adjustment)
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
fn build_coboundary_matrix(&self, graph: &SheafGraph, c0_dim: usize, c1_dim: usize) -> Result<Vec<f64>> {
|
||||
// Coboundary matrix d^0: C^0 -> C^1
|
||||
// For edge e: u -> v, d^0 acts as: (d^0 s)(e) = r_e(s_u) - s_v
|
||||
let mut matrix = vec![0.0; c1_dim * c0_dim];
|
||||
|
||||
let mut row_offset = 0;
|
||||
let mut col_offsets: Vec<usize> = Vec::with_capacity(graph.nodes.len());
|
||||
let mut current_offset = 0;
|
||||
for node in &graph.nodes {
|
||||
col_offsets.push(current_offset);
|
||||
current_offset += node.dimension();
|
||||
}
|
||||
|
||||
for edge in &graph.edges {
|
||||
let source_offset = col_offsets[edge.source];
|
||||
let target_offset = col_offsets[edge.target];
|
||||
|
||||
// Add restriction map contribution (positive)
|
||||
for i in 0..edge.target_dim {
|
||||
for j in 0..edge.source_dim {
|
||||
let row = row_offset + i;
|
||||
let col = source_offset + j;
|
||||
if row < c1_dim && col < c0_dim {
|
||||
matrix[row * c0_dim + col] = edge.restriction_map[i * edge.source_dim + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract identity on target (negative contribution)
|
||||
for i in 0..edge.target_dim.min(graph.nodes[edge.target].dimension()) {
|
||||
let row = row_offset + i;
|
||||
let col = target_offset + i;
|
||||
if row < c1_dim && col < c0_dim {
|
||||
matrix[row * c0_dim + col] -= 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
row_offset += edge.target_dim;
|
||||
}
|
||||
|
||||
Ok(matrix)
|
||||
}
|
||||
|
||||
fn compute_kernel_dimension(&self, matrix: &[f64], rows: usize, cols: usize) -> usize {
|
||||
// Kernel dimension = cols - rank
|
||||
let rank = self.compute_rank(matrix, rows, cols);
|
||||
if cols > rank { cols - rank } else { 0 }
|
||||
}
|
||||
|
||||
fn compute_rank(&self, matrix: &[f64], rows: usize, cols: usize) -> usize {
|
||||
// Simple rank computation via Gaussian elimination
|
||||
if rows == 0 || cols == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut m = matrix.to_vec();
|
||||
let mut rank = 0;
|
||||
let mut pivot_col = 0;
|
||||
|
||||
for row in 0..rows {
|
||||
if pivot_col >= cols {
|
||||
break;
|
||||
}
|
||||
|
||||
// Find pivot
|
||||
let mut max_row = row;
|
||||
let mut max_val = m[row * cols + pivot_col].abs();
|
||||
for k in (row + 1)..rows {
|
||||
let val = m[k * cols + pivot_col].abs();
|
||||
if val > max_val {
|
||||
max_val = val;
|
||||
max_row = k;
|
||||
}
|
||||
}
|
||||
|
||||
if max_val < self.tolerance {
|
||||
pivot_col += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Swap rows
|
||||
if max_row != row {
|
||||
for j in 0..cols {
|
||||
m.swap(row * cols + j, max_row * cols + j);
|
||||
}
|
||||
}
|
||||
|
||||
// Eliminate
|
||||
let pivot = m[row * cols + pivot_col];
|
||||
for k in (row + 1)..rows {
|
||||
let factor = m[k * cols + pivot_col] / pivot;
|
||||
for j in pivot_col..cols {
|
||||
m[k * cols + j] -= factor * m[row * cols + j];
|
||||
}
|
||||
}
|
||||
|
||||
rank += 1;
|
||||
pivot_col += 1;
|
||||
}
|
||||
|
||||
rank
|
||||
}
|
||||
|
||||
fn compute_consistency_energy(&self, graph: &SheafGraph) -> Result<(f64, Option<Vec<f64>>)> {
|
||||
let mut total_energy = 0.0;
|
||||
let mut obstruction = Vec::new();
|
||||
|
||||
for edge in &graph.edges {
|
||||
let source = &graph.nodes[edge.source];
|
||||
let target = &graph.nodes[edge.target];
|
||||
|
||||
let restricted = edge.apply(&source.section)?;
|
||||
|
||||
for j in 0..edge.target_dim.min(target.section.len()) {
|
||||
let diff = restricted[j] - target.section[j];
|
||||
total_energy += diff * diff;
|
||||
obstruction.push(diff);
|
||||
}
|
||||
}
|
||||
|
||||
let obs = if obstruction.iter().any(|&x| x.abs() > self.tolerance) {
|
||||
Some(obstruction)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((total_energy, obs))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CohomologyEngine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for creating sheaf graphs from belief networks
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BeliefGraphBuilder {
|
||||
dimension: usize,
|
||||
}
|
||||
|
||||
impl BeliefGraphBuilder {
|
||||
pub fn new(dimension: usize) -> Self {
|
||||
Self { dimension }
|
||||
}
|
||||
|
||||
/// Create a sheaf graph from belief nodes and edges
|
||||
pub fn build_from_beliefs(
|
||||
&self,
|
||||
beliefs: &[(String, Vec<f64>)],
|
||||
connections: &[(usize, usize)],
|
||||
) -> Result<SheafGraph> {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
for (i, (label, section)) in beliefs.iter().enumerate() {
|
||||
let node = SheafNode::new(i, label.clone(), section.clone());
|
||||
graph.add_node(node);
|
||||
}
|
||||
|
||||
for &(source, target) in connections {
|
||||
let source_dim = graph.nodes[source].dimension();
|
||||
let target_dim = graph.nodes[target].dimension();
|
||||
|
||||
// Create identity restriction map (or projection if dimensions differ)
|
||||
let min_dim = source_dim.min(target_dim);
|
||||
let mut map = vec![0.0; target_dim * source_dim];
|
||||
for i in 0..min_dim {
|
||||
map[i * source_dim + i] = 1.0;
|
||||
}
|
||||
|
||||
let edge = SheafEdge::with_map(source, target, map, source_dim, target_dim);
|
||||
graph.add_edge(edge)?;
|
||||
}
|
||||
|
||||
Ok(graph)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sheaf_graph_creation() {
|
||||
let mut graph = SheafGraph::new();
|
||||
let n1 = graph.add_node(SheafNode::new(0, "A", vec![1.0, 2.0]));
|
||||
let n2 = graph.add_node(SheafNode::new(1, "B", vec![1.0, 2.0]));
|
||||
|
||||
let edge = SheafEdge::identity(n1, n2, 2);
|
||||
graph.add_edge(edge).unwrap();
|
||||
|
||||
assert_eq!(graph.node_count(), 2);
|
||||
assert_eq!(graph.edge_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cohomology_consistent() {
|
||||
let mut graph = SheafGraph::new();
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1.0, 2.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![1.0, 2.0]));
|
||||
|
||||
let edge = SheafEdge::identity(0, 1, 2);
|
||||
graph.add_edge(edge).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
assert!(result.is_consistent);
|
||||
assert!(result.consistency_energy < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cohomology_inconsistent() {
|
||||
let mut graph = SheafGraph::new();
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1.0, 2.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![3.0, 4.0]));
|
||||
|
||||
let edge = SheafEdge::identity(0, 1, 2);
|
||||
graph.add_edge(edge).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
assert!(!result.is_consistent);
|
||||
assert!(result.consistency_energy > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_obstructions() {
|
||||
let mut graph = SheafGraph::new();
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1.0, 0.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![0.0, 1.0]));
|
||||
|
||||
let edge = SheafEdge::identity(0, 1, 2);
|
||||
graph.add_edge(edge).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let obstructions = engine.detect_obstructions(&graph).unwrap();
|
||||
|
||||
assert_eq!(obstructions.len(), 1);
|
||||
assert!(obstructions[0].magnitude > 1.0);
|
||||
}
|
||||
}
|
||||
176
vendor/ruvector/examples/prime-radiant/src/cohomology/presheaf.rs
vendored
Normal file
176
vendor/ruvector/examples/prime-radiant/src/cohomology/presheaf.rs
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
//! Presheaf implementation
|
||||
|
||||
use super::{RestrictionMap, Section};
|
||||
use crate::{Error, Result};
|
||||
use nalgebra::{DMatrix, DVector};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A presheaf over a topological space
|
||||
///
|
||||
/// A presheaf F assigns to each open set U a set F(U) (sections over U)
|
||||
/// and to each inclusion U ⊆ V a restriction map F(V) -> F(U).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Presheaf {
|
||||
/// Sections indexed by open set
|
||||
sections: HashMap<String, Section>,
|
||||
/// Restriction maps indexed by (source, target) pairs
|
||||
restrictions: HashMap<(String, String), RestrictionMap>,
|
||||
/// Topology as inclusion relations
|
||||
inclusions: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl Presheaf {
|
||||
/// Create a new empty presheaf
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sections: HashMap::new(),
|
||||
restrictions: HashMap::new(),
|
||||
inclusions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a section over an open set
|
||||
pub fn section(mut self, domain: impl Into<String>, values: DVector<f64>) -> Self {
|
||||
let domain = domain.into();
|
||||
self.sections
|
||||
.insert(domain.clone(), Section::new(domain, values));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a restriction map between open sets
|
||||
pub fn restriction(
|
||||
mut self,
|
||||
source: impl Into<String>,
|
||||
target: impl Into<String>,
|
||||
matrix: DMatrix<f64>,
|
||||
) -> Self {
|
||||
let source = source.into();
|
||||
let target = target.into();
|
||||
self.inclusions.push((target.clone(), source.clone()));
|
||||
self.restrictions.insert(
|
||||
(source.clone(), target.clone()),
|
||||
RestrictionMap::new(source, target, matrix),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get a section by domain
|
||||
pub fn get_section(&self, domain: &str) -> Option<&Section> {
|
||||
self.sections.get(domain)
|
||||
}
|
||||
|
||||
/// Get a restriction map
|
||||
pub fn get_restriction(&self, source: &str, target: &str) -> Option<&RestrictionMap> {
|
||||
self.restrictions.get(&(source.to_string(), target.to_string()))
|
||||
}
|
||||
|
||||
/// List all open sets
|
||||
pub fn open_sets(&self) -> Vec<&str> {
|
||||
self.sections.keys().map(|s| s.as_str()).collect()
|
||||
}
|
||||
|
||||
/// Check presheaf functoriality
|
||||
///
|
||||
/// Verifies that restriction maps compose correctly:
|
||||
/// If U ⊆ V ⊆ W, then res_{W,U} = res_{V,U} ∘ res_{W,V}
|
||||
pub fn check_functoriality(&self, epsilon: f64) -> Result<bool> {
|
||||
// Check identity: res_{U,U} = id
|
||||
for (domain, section) in &self.sections {
|
||||
if let Some(res) = self.get_restriction(domain, domain) {
|
||||
let identity = DMatrix::identity(section.dimension(), section.dimension());
|
||||
let diff = (&res.matrix - &identity).norm();
|
||||
if diff > epsilon {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check composition for all triples
|
||||
// This is a simplified check - full implementation would traverse the topology
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Compute the global sections
|
||||
///
|
||||
/// Global sections are elements that are compatible under all restriction maps
|
||||
pub fn global_sections(&self) -> Result<Vec<DVector<f64>>> {
|
||||
if self.sections.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// For a simple two-layer case, find vectors v such that res(v) = v|_U for all U
|
||||
// This is the kernel of the difference map in the Cech complex
|
||||
|
||||
// Simplified: return sections that are consistent
|
||||
let mut global = Vec::new();
|
||||
|
||||
// Check each section for global compatibility
|
||||
for (domain, section) in &self.sections {
|
||||
let mut is_global = true;
|
||||
for ((src, tgt), res) in &self.restrictions {
|
||||
if src == domain {
|
||||
if let Some(target_section) = self.sections.get(tgt) {
|
||||
let restricted = res.apply(§ion.values)?;
|
||||
let diff = (&restricted - &target_section.values).norm();
|
||||
if diff > 1e-10 {
|
||||
is_global = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if is_global {
|
||||
global.push(section.values.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(global)
|
||||
}
|
||||
|
||||
/// Convert to a sheaf by checking/enforcing gluing conditions
|
||||
pub fn to_sheaf(&self) -> Result<super::Sheaf> {
|
||||
// Verify gluing axioms
|
||||
self.verify_gluing()?;
|
||||
Ok(super::Sheaf::from_presheaf(self.clone()))
|
||||
}
|
||||
|
||||
/// Verify gluing axioms
|
||||
fn verify_gluing(&self) -> Result<()> {
|
||||
// Locality: if sections agree on all overlaps, they are equal
|
||||
// Gluing: compatible sections can be glued to a global section
|
||||
|
||||
// Simplified check for now
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Presheaf {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_presheaf_creation() {
|
||||
let presheaf = Presheaf::new()
|
||||
.section("U", DVector::from_vec(vec![1.0, 2.0]))
|
||||
.section("V", DVector::from_vec(vec![1.0]));
|
||||
|
||||
assert_eq!(presheaf.open_sets().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_presheaf_restriction() {
|
||||
let matrix = DMatrix::from_row_slice(1, 2, &[1.0, 0.0]);
|
||||
let presheaf = Presheaf::new()
|
||||
.section("U", DVector::from_vec(vec![1.0, 2.0]))
|
||||
.section("V", DVector::from_vec(vec![1.0]))
|
||||
.restriction("U", "V", matrix);
|
||||
|
||||
assert!(presheaf.get_restriction("U", "V").is_some());
|
||||
}
|
||||
}
|
||||
258
vendor/ruvector/examples/prime-radiant/src/cohomology/sheaf.rs
vendored
Normal file
258
vendor/ruvector/examples/prime-radiant/src/cohomology/sheaf.rs
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
//! Sheaf implementation
|
||||
|
||||
use super::{BettiNumbers, ChainComplex, Homology, Presheaf, Section};
|
||||
use crate::{Error, Result};
|
||||
use nalgebra::{DMatrix, DVector};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A sheaf over a topological space
|
||||
///
|
||||
/// A sheaf is a presheaf that satisfies the gluing axioms:
|
||||
/// 1. Locality: Sections that agree on overlaps are equal
|
||||
/// 2. Gluing: Compatible sections can be glued to a global section
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sheaf {
|
||||
/// Underlying presheaf
|
||||
presheaf: Presheaf,
|
||||
/// Cached cohomology groups
|
||||
cohomology_cache: HashMap<usize, Homology>,
|
||||
}
|
||||
|
||||
impl Sheaf {
|
||||
/// Create a new sheaf from a presheaf
|
||||
pub fn from_presheaf(presheaf: Presheaf) -> Self {
|
||||
Self {
|
||||
presheaf,
|
||||
cohomology_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a sheaf from neural network activations
|
||||
///
|
||||
/// Treats each layer as an open set with the activation vectors as sections
|
||||
pub fn from_activations(layers: &[DVector<f64>]) -> Result<Self> {
|
||||
if layers.is_empty() {
|
||||
return Err(Error::InvalidTopology("Empty layer list".to_string()));
|
||||
}
|
||||
|
||||
let mut presheaf = Presheaf::new();
|
||||
|
||||
// Add each layer as a section
|
||||
for (i, activations) in layers.iter().enumerate() {
|
||||
presheaf = presheaf.section(format!("layer_{}", i), activations.clone());
|
||||
}
|
||||
|
||||
// Add identity restrictions (simplified topology)
|
||||
// In practice, you'd derive these from weight matrices
|
||||
|
||||
Ok(Self::from_presheaf(presheaf))
|
||||
}
|
||||
|
||||
/// Create a sheaf builder
|
||||
pub fn builder() -> SheafBuilder {
|
||||
SheafBuilder::new()
|
||||
}
|
||||
|
||||
/// Get the underlying presheaf
|
||||
pub fn presheaf(&self) -> &Presheaf {
|
||||
&self.presheaf
|
||||
}
|
||||
|
||||
/// Compute the n-th cohomology group H^n(X, F)
|
||||
///
|
||||
/// Cohomology measures obstructions to extending local sections globally.
|
||||
pub fn cohomology(&self, degree: usize) -> Result<Homology> {
|
||||
// Check cache first
|
||||
if let Some(cached) = self.cohomology_cache.get(°ree) {
|
||||
return Ok(cached.clone());
|
||||
}
|
||||
|
||||
// Build the Cech complex and compute cohomology
|
||||
let complex = self.cech_complex()?;
|
||||
let homology = complex.homology(degree)?;
|
||||
|
||||
Ok(homology)
|
||||
}
|
||||
|
||||
/// Compute all Betti numbers up to a given degree
|
||||
pub fn betti_numbers(&self, max_degree: usize) -> Result<BettiNumbers> {
|
||||
let mut betti = BettiNumbers::default();
|
||||
|
||||
for degree in 0..=max_degree {
|
||||
let h = self.cohomology(degree)?;
|
||||
match degree {
|
||||
0 => betti.b0 = h.dimension(),
|
||||
1 => betti.b1 = h.dimension(),
|
||||
2 => betti.b2 = h.dimension(),
|
||||
_ => betti.higher.push(h.dimension()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(betti)
|
||||
}
|
||||
|
||||
/// Compute persistent homology for multi-scale analysis
|
||||
pub fn persistent_homology(&self) -> Result<PersistenceDiagram> {
|
||||
// Compute homology at multiple filtration levels
|
||||
let mut persistence = PersistenceDiagram::new();
|
||||
|
||||
// Simplified: compute at single scale
|
||||
let h0 = self.cohomology(0)?;
|
||||
let h1 = self.cohomology(1)?;
|
||||
|
||||
persistence.add_bar(0, 0.0, f64::INFINITY, h0.dimension());
|
||||
persistence.add_bar(1, 0.0, f64::INFINITY, h1.dimension());
|
||||
|
||||
Ok(persistence)
|
||||
}
|
||||
|
||||
/// Build the Cech complex for cohomology computation
|
||||
fn cech_complex(&self) -> Result<ChainComplex> {
|
||||
// The Cech complex is built from intersections of open sets
|
||||
// C^0: Direct product of all F(U_i)
|
||||
// C^1: Direct product of all F(U_i ∩ U_j)
|
||||
// etc.
|
||||
|
||||
let open_sets = self.presheaf.open_sets();
|
||||
let n = open_sets.len();
|
||||
|
||||
if n == 0 {
|
||||
return Err(Error::InvalidTopology("No open sets".to_string()));
|
||||
}
|
||||
|
||||
// Build boundary maps
|
||||
// For simplicity, use identity matrices as placeholder
|
||||
let dim = self
|
||||
.presheaf
|
||||
.get_section(open_sets[0])
|
||||
.map(|s| s.dimension())
|
||||
.unwrap_or(1);
|
||||
|
||||
let d0 = DMatrix::zeros(dim, dim);
|
||||
let d1 = DMatrix::zeros(dim, dim);
|
||||
|
||||
Ok(ChainComplex::new(vec![d0, d1]))
|
||||
}
|
||||
|
||||
/// Compute the Euler characteristic
|
||||
pub fn euler_characteristic(&self) -> Result<i64> {
|
||||
let betti = self.betti_numbers(2)?;
|
||||
Ok(betti.euler_characteristic())
|
||||
}
|
||||
|
||||
/// Check if the sheaf is locally constant
|
||||
pub fn is_locally_constant(&self, epsilon: f64) -> Result<bool> {
|
||||
self.presheaf.check_functoriality(epsilon)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for constructing sheaves
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SheafBuilder {
|
||||
presheaf: Presheaf,
|
||||
}
|
||||
|
||||
impl SheafBuilder {
|
||||
/// Create a new builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
presheaf: Presheaf::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a section
|
||||
pub fn section(mut self, domain: impl Into<String>, values: DVector<f64>) -> Self {
|
||||
self.presheaf = self.presheaf.section(domain, values);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a restriction map
|
||||
pub fn restriction(
|
||||
mut self,
|
||||
source: impl Into<String>,
|
||||
target: impl Into<String>,
|
||||
matrix: DMatrix<f64>,
|
||||
) -> Self {
|
||||
self.presheaf = self.presheaf.restriction(source, target, matrix);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the sheaf
|
||||
pub fn build(self) -> Result<Sheaf> {
|
||||
self.presheaf.to_sheaf()
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistence diagram for topological data analysis
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PersistenceDiagram {
|
||||
/// Bars (birth, death, multiplicity) by dimension
|
||||
bars: HashMap<usize, Vec<(f64, f64, usize)>>,
|
||||
}
|
||||
|
||||
impl PersistenceDiagram {
|
||||
/// Create a new persistence diagram
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
bars: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a persistence bar
|
||||
pub fn add_bar(&mut self, dimension: usize, birth: f64, death: f64, multiplicity: usize) {
|
||||
self.bars
|
||||
.entry(dimension)
|
||||
.or_default()
|
||||
.push((birth, death, multiplicity));
|
||||
}
|
||||
|
||||
/// Get bars for a given dimension
|
||||
pub fn bars(&self, dimension: usize) -> &[(f64, f64, usize)] {
|
||||
self.bars.get(&dimension).map(|v| v.as_slice()).unwrap_or(&[])
|
||||
}
|
||||
|
||||
/// Compute bottleneck distance to another diagram
|
||||
pub fn bottleneck_distance(&self, other: &PersistenceDiagram) -> f64 {
|
||||
// Simplified implementation
|
||||
let mut max_dist = 0.0f64;
|
||||
|
||||
for dim in 0..=2 {
|
||||
let self_bars = self.bars(dim);
|
||||
let other_bars = other.bars(dim);
|
||||
|
||||
// Compare number of bars
|
||||
let diff = (self_bars.len() as f64 - other_bars.len() as f64).abs();
|
||||
max_dist = max_dist.max(diff);
|
||||
}
|
||||
|
||||
max_dist
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sheaf_from_activations() {
|
||||
let layers = vec![
|
||||
DVector::from_vec(vec![1.0, 2.0, 3.0]),
|
||||
DVector::from_vec(vec![0.5, 1.5]),
|
||||
];
|
||||
|
||||
let sheaf = Sheaf::from_activations(&layers).unwrap();
|
||||
assert!(sheaf.presheaf().get_section("layer_0").is_some());
|
||||
assert!(sheaf.presheaf().get_section("layer_1").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sheaf_builder() {
|
||||
let sheaf = Sheaf::builder()
|
||||
.section("U", DVector::from_vec(vec![1.0, 2.0]))
|
||||
.section("V", DVector::from_vec(vec![1.0]))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert!(sheaf.presheaf().get_section("U").is_some());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user