Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
790
vendor/ruvector/examples/prime-radiant/tests/category_tests.rs
vendored
Normal file
790
vendor/ruvector/examples/prime-radiant/tests/category_tests.rs
vendored
Normal file
@@ -0,0 +1,790 @@
|
||||
//! Comprehensive tests for Category Theory Module
|
||||
//!
|
||||
//! This test suite verifies category-theoretic properties including:
|
||||
//! - Category laws (identity, associativity)
|
||||
//! - Functor preservation
|
||||
//! - Topos subobject classifier
|
||||
//! - Higher category coherence
|
||||
|
||||
use prime_radiant_category::{
|
||||
Category, Morphism, Object, SetCategory, VectorCategory,
|
||||
Functor, EmbeddingFunctor, ForgetfulFunctor,
|
||||
NaturalTransformation,
|
||||
Topos, SubobjectClassifier,
|
||||
TwoCategory, TwoMorphism, CoherenceResult,
|
||||
ObjectId, MorphismId, CategoryError,
|
||||
verify_pentagon, verify_triangle,
|
||||
};
|
||||
use proptest::prelude::*;
|
||||
use approx::assert_relative_eq;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// =============================================================================
|
||||
// CATEGORY LAW TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod category_law_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test left identity: id_B . f = f
|
||||
#[test]
|
||||
fn test_left_identity_law() {
|
||||
let mut cat = SetCategory::new();
|
||||
|
||||
let a = cat.add_object("A");
|
||||
let b = cat.add_object("B");
|
||||
|
||||
let f = cat.add_morphism(a, b, "f").unwrap();
|
||||
let id_b = cat.identity(b).unwrap();
|
||||
|
||||
// Compose id_B . f
|
||||
let composed = cat.compose(id_b, f).unwrap();
|
||||
|
||||
// Should equal f (same source and target)
|
||||
let f_data = cat.get_morphism(f).unwrap();
|
||||
let composed_data = cat.get_morphism(composed).unwrap();
|
||||
|
||||
assert_eq!(f_data.source, composed_data.source);
|
||||
assert_eq!(f_data.target, composed_data.target);
|
||||
}
|
||||
|
||||
/// Test right identity: f . id_A = f
|
||||
#[test]
|
||||
fn test_right_identity_law() {
|
||||
let mut cat = SetCategory::new();
|
||||
|
||||
let a = cat.add_object("A");
|
||||
let b = cat.add_object("B");
|
||||
|
||||
let f = cat.add_morphism(a, b, "f").unwrap();
|
||||
let id_a = cat.identity(a).unwrap();
|
||||
|
||||
// Compose f . id_A
|
||||
let composed = cat.compose(f, id_a).unwrap();
|
||||
|
||||
let f_data = cat.get_morphism(f).unwrap();
|
||||
let composed_data = cat.get_morphism(composed).unwrap();
|
||||
|
||||
assert_eq!(f_data.source, composed_data.source);
|
||||
assert_eq!(f_data.target, composed_data.target);
|
||||
}
|
||||
|
||||
/// Test associativity: (h . g) . f = h . (g . f)
|
||||
#[test]
|
||||
fn test_associativity_law() {
|
||||
let mut cat = SetCategory::new();
|
||||
|
||||
let a = cat.add_object("A");
|
||||
let b = cat.add_object("B");
|
||||
let c = cat.add_object("C");
|
||||
let d = cat.add_object("D");
|
||||
|
||||
let f = cat.add_morphism(a, b, "f").unwrap();
|
||||
let g = cat.add_morphism(b, c, "g").unwrap();
|
||||
let h = cat.add_morphism(c, d, "h").unwrap();
|
||||
|
||||
// Left association: (h . g) . f
|
||||
let hg = cat.compose(h, g).unwrap();
|
||||
let left = cat.compose(hg, f).unwrap();
|
||||
|
||||
// Right association: h . (g . f)
|
||||
let gf = cat.compose(g, f).unwrap();
|
||||
let right = cat.compose(h, gf).unwrap();
|
||||
|
||||
// Both should have same source and target
|
||||
let left_data = cat.get_morphism(left).unwrap();
|
||||
let right_data = cat.get_morphism(right).unwrap();
|
||||
|
||||
assert_eq!(left_data.source, right_data.source);
|
||||
assert_eq!(left_data.target, right_data.target);
|
||||
}
|
||||
|
||||
/// Test category law verification
|
||||
#[test]
|
||||
fn test_verify_laws() {
|
||||
let mut cat = SetCategory::new();
|
||||
|
||||
// Create a small category
|
||||
let a = cat.add_object("A");
|
||||
let b = cat.add_object("B");
|
||||
cat.add_morphism(a, b, "f").unwrap();
|
||||
cat.identity(a).unwrap();
|
||||
cat.identity(b).unwrap();
|
||||
|
||||
// Category should verify laws
|
||||
assert!(cat.verify_laws());
|
||||
}
|
||||
|
||||
/// Test composition with incompatible morphisms
|
||||
#[test]
|
||||
fn test_incompatible_composition() {
|
||||
let mut cat = SetCategory::new();
|
||||
|
||||
let a = cat.add_object("A");
|
||||
let b = cat.add_object("B");
|
||||
let c = cat.add_object("C");
|
||||
let d = cat.add_object("D");
|
||||
|
||||
let f = cat.add_morphism(a, b, "f").unwrap(); // A -> B
|
||||
let g = cat.add_morphism(c, d, "g").unwrap(); // C -> D
|
||||
|
||||
// Cannot compose g . f since target(f) = B != C = source(g)
|
||||
let result = cat.compose(g, f);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result, Err(CategoryError::NotComposable(_, _))));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VECTOR CATEGORY TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod vector_category_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test VectorCategory creation
|
||||
#[test]
|
||||
fn test_vector_category_creation() {
|
||||
let cat = VectorCategory::new(768);
|
||||
assert!(cat.verify_laws());
|
||||
}
|
||||
|
||||
/// Test linear map morphisms
|
||||
#[test]
|
||||
fn test_linear_morphisms() {
|
||||
let mut cat = VectorCategory::new(3);
|
||||
|
||||
let v1 = cat.add_object("V1");
|
||||
let v2 = cat.add_object("V2");
|
||||
|
||||
// Add a linear map
|
||||
let matrix = vec![
|
||||
1.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 1.0,
|
||||
]; // Identity matrix
|
||||
|
||||
let f = cat.add_linear_morphism(v1, v2, matrix).unwrap();
|
||||
|
||||
// Identity composition should work
|
||||
let id_v1 = cat.identity(v1).unwrap();
|
||||
let composed = cat.compose(f, id_v1).unwrap();
|
||||
|
||||
assert!(cat.get_morphism(composed).is_some());
|
||||
}
|
||||
|
||||
/// Test linear map application
|
||||
#[test]
|
||||
fn test_apply_linear_map() {
|
||||
let mut cat = VectorCategory::new(2);
|
||||
|
||||
let v1 = cat.add_object("V1");
|
||||
let v2 = cat.add_object("V2");
|
||||
|
||||
// Rotation by 90 degrees
|
||||
let matrix = vec![
|
||||
0.0, -1.0,
|
||||
1.0, 0.0,
|
||||
];
|
||||
|
||||
let f = cat.add_linear_morphism(v1, v2, matrix).unwrap();
|
||||
|
||||
// Apply to vector [1, 0]
|
||||
let input = vec![1.0, 0.0];
|
||||
let output = cat.apply_morphism(f, &input).unwrap();
|
||||
|
||||
assert_relative_eq!(output[0], 0.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(output[1], 1.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test composition preserves linearity
|
||||
#[test]
|
||||
fn test_composition_preserves_linearity() {
|
||||
let mut cat = VectorCategory::new(2);
|
||||
|
||||
let a = cat.add_object("A");
|
||||
let b = cat.add_object("B");
|
||||
let c = cat.add_object("C");
|
||||
|
||||
// Scale by 2
|
||||
let scale = vec![2.0, 0.0, 0.0, 2.0];
|
||||
let f = cat.add_linear_morphism(a, b, scale).unwrap();
|
||||
|
||||
// Scale by 3
|
||||
let scale2 = vec![3.0, 0.0, 0.0, 3.0];
|
||||
let g = cat.add_linear_morphism(b, c, scale2).unwrap();
|
||||
|
||||
// Composition should scale by 6
|
||||
let composed = cat.compose(g, f).unwrap();
|
||||
|
||||
let input = vec![1.0, 1.0];
|
||||
let output = cat.apply_morphism(composed, &input).unwrap();
|
||||
|
||||
assert_relative_eq!(output[0], 6.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(output[1], 6.0, epsilon = 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FUNCTOR TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod functor_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test functor preserves identity: F(id_A) = id_{F(A)}
|
||||
#[test]
|
||||
fn test_functor_preserves_identity() {
|
||||
let mut source_cat = SetCategory::new();
|
||||
let mut target_cat = VectorCategory::new(3);
|
||||
|
||||
let a = source_cat.add_object("A");
|
||||
let id_a = source_cat.identity(a).unwrap();
|
||||
|
||||
let functor = EmbeddingFunctor::new(3);
|
||||
|
||||
// Map the identity
|
||||
let fa = functor.map_object(a, &mut target_cat).unwrap();
|
||||
let f_id_a = functor.map_morphism(id_a, &source_cat, &mut target_cat).unwrap();
|
||||
|
||||
// F(id_A) should equal id_{F(A)}
|
||||
let id_fa = target_cat.identity(fa).unwrap();
|
||||
|
||||
let f_id_data = target_cat.get_morphism(f_id_a).unwrap();
|
||||
let id_fa_data = target_cat.get_morphism(id_fa).unwrap();
|
||||
|
||||
assert_eq!(f_id_data.source, id_fa_data.source);
|
||||
assert_eq!(f_id_data.target, id_fa_data.target);
|
||||
}
|
||||
|
||||
/// Test functor preserves composition: F(g . f) = F(g) . F(f)
|
||||
#[test]
|
||||
fn test_functor_preserves_composition() {
|
||||
let mut source = SetCategory::new();
|
||||
let mut target = VectorCategory::new(2);
|
||||
|
||||
let a = source.add_object("A");
|
||||
let b = source.add_object("B");
|
||||
let c = source.add_object("C");
|
||||
|
||||
let f = source.add_morphism(a, b, "f").unwrap();
|
||||
let g = source.add_morphism(b, c, "g").unwrap();
|
||||
let gf = source.compose(g, f).unwrap();
|
||||
|
||||
let functor = EmbeddingFunctor::new(2);
|
||||
|
||||
// F(g . f)
|
||||
let f_gf = functor.map_morphism(gf, &source, &mut target).unwrap();
|
||||
|
||||
// F(g) . F(f)
|
||||
let ff = functor.map_morphism(f, &source, &mut target).unwrap();
|
||||
let fg = functor.map_morphism(g, &source, &mut target).unwrap();
|
||||
let fg_ff = target.compose(fg, ff).unwrap();
|
||||
|
||||
// Should have same source and target
|
||||
let f_gf_data = target.get_morphism(f_gf).unwrap();
|
||||
let fg_ff_data = target.get_morphism(fg_ff).unwrap();
|
||||
|
||||
assert_eq!(f_gf_data.source, fg_ff_data.source);
|
||||
assert_eq!(f_gf_data.target, fg_ff_data.target);
|
||||
}
|
||||
|
||||
/// Test forgetful functor
|
||||
#[test]
|
||||
fn test_forgetful_functor() {
|
||||
let mut vec_cat = VectorCategory::new(3);
|
||||
let mut set_cat = SetCategory::new();
|
||||
|
||||
let v = vec_cat.add_object("V");
|
||||
|
||||
let forgetful = ForgetfulFunctor::new();
|
||||
let forgotten = forgetful.map_object(v, &mut set_cat).unwrap();
|
||||
|
||||
// Forgetful functor should create corresponding set object
|
||||
assert!(set_cat.get_object(forgotten).is_some());
|
||||
}
|
||||
|
||||
/// Test embedding functor with different dimensions
|
||||
#[test]
|
||||
fn test_embedding_dimensions() {
|
||||
let mut source = SetCategory::new();
|
||||
let mut target2 = VectorCategory::new(2);
|
||||
let mut target10 = VectorCategory::new(10);
|
||||
|
||||
let a = source.add_object("A");
|
||||
|
||||
let embed2 = EmbeddingFunctor::new(2);
|
||||
let embed10 = EmbeddingFunctor::new(10);
|
||||
|
||||
let fa2 = embed2.map_object(a, &mut target2).unwrap();
|
||||
let fa10 = embed10.map_object(a, &mut target10).unwrap();
|
||||
|
||||
assert!(target2.get_object(fa2).is_some());
|
||||
assert!(target10.get_object(fa10).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NATURAL TRANSFORMATION TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod natural_transformation_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test naturality condition: eta_B . F(f) = G(f) . eta_A
|
||||
#[test]
|
||||
fn test_naturality_condition() {
|
||||
let mut source = SetCategory::new();
|
||||
let mut target = VectorCategory::new(3);
|
||||
|
||||
let a = source.add_object("A");
|
||||
let b = source.add_object("B");
|
||||
let f = source.add_morphism(a, b, "f").unwrap();
|
||||
|
||||
let functor_f = EmbeddingFunctor::new(3);
|
||||
let functor_g = EmbeddingFunctor::new(3);
|
||||
|
||||
// Create natural transformation eta: F -> G
|
||||
let eta = NaturalTransformation::new(&functor_f, &functor_g);
|
||||
|
||||
// Verify naturality
|
||||
let is_natural = eta.verify_naturality(&source, &mut target, f).unwrap();
|
||||
assert!(is_natural);
|
||||
}
|
||||
|
||||
/// Test identity natural transformation
|
||||
#[test]
|
||||
fn test_identity_transformation() {
|
||||
let mut cat = VectorCategory::new(2);
|
||||
|
||||
let a = cat.add_object("A");
|
||||
let functor = EmbeddingFunctor::new(2);
|
||||
|
||||
let id_nat = NaturalTransformation::identity(&functor);
|
||||
|
||||
// Component at A should be identity
|
||||
let component = id_nat.component(a, &mut cat).unwrap();
|
||||
let id_a = cat.identity(a).unwrap();
|
||||
|
||||
let comp_data = cat.get_morphism(component).unwrap();
|
||||
let id_data = cat.get_morphism(id_a).unwrap();
|
||||
|
||||
assert_eq!(comp_data.source, id_data.source);
|
||||
assert_eq!(comp_data.target, id_data.target);
|
||||
}
|
||||
|
||||
/// Test vertical composition of natural transformations
|
||||
#[test]
|
||||
fn test_vertical_composition() {
|
||||
let functor_f = EmbeddingFunctor::new(2);
|
||||
let functor_g = EmbeddingFunctor::new(2);
|
||||
let functor_h = EmbeddingFunctor::new(2);
|
||||
|
||||
let eta: NaturalTransformation<_, _> = NaturalTransformation::new(&functor_f, &functor_g);
|
||||
let mu: NaturalTransformation<_, _> = NaturalTransformation::new(&functor_g, &functor_h);
|
||||
|
||||
// Vertical composition mu . eta : F -> H
|
||||
let composed = eta.compose_vertical(&mu).unwrap();
|
||||
|
||||
assert_eq!(composed.source_functor_id(), functor_f.id());
|
||||
assert_eq!(composed.target_functor_id(), functor_h.id());
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOPOS TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod topos_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test topos subobject classifier existence
|
||||
#[test]
|
||||
fn test_subobject_classifier_exists() {
|
||||
let topos = Topos::set_topos();
|
||||
|
||||
let classifier = topos.subobject_classifier();
|
||||
assert!(classifier.is_some());
|
||||
|
||||
let omega = classifier.unwrap();
|
||||
assert!(topos.is_valid_classifier(&omega));
|
||||
}
|
||||
|
||||
/// Test truth morphism: true: 1 -> Omega
|
||||
#[test]
|
||||
fn test_truth_morphism() {
|
||||
let mut topos = Topos::set_topos();
|
||||
|
||||
let terminal = topos.terminal_object().unwrap();
|
||||
let omega = topos.subobject_classifier().unwrap();
|
||||
|
||||
let true_morphism = topos.truth_morphism().unwrap();
|
||||
let true_data = topos.get_morphism(true_morphism).unwrap();
|
||||
|
||||
assert_eq!(true_data.source, terminal.id());
|
||||
assert_eq!(true_data.target, omega.id());
|
||||
}
|
||||
|
||||
/// Test characteristic morphism construction
|
||||
#[test]
|
||||
fn test_characteristic_morphism() {
|
||||
let mut topos = Topos::set_topos();
|
||||
|
||||
let a = topos.add_object("A");
|
||||
let b = topos.add_object("B");
|
||||
let mono = topos.add_monomorphism(a, b).unwrap();
|
||||
|
||||
// Should produce characteristic morphism B -> Omega
|
||||
let chi = topos.characteristic_morphism(mono).unwrap();
|
||||
let omega = topos.subobject_classifier().unwrap();
|
||||
|
||||
let chi_data = topos.get_morphism(chi).unwrap();
|
||||
assert_eq!(chi_data.source, b);
|
||||
assert_eq!(chi_data.target, omega.id());
|
||||
}
|
||||
|
||||
/// Test pullback existence in topos
|
||||
#[test]
|
||||
fn test_pullback_exists() {
|
||||
let mut topos = Topos::set_topos();
|
||||
|
||||
let a = topos.add_object("A");
|
||||
let b = topos.add_object("B");
|
||||
let c = topos.add_object("C");
|
||||
|
||||
let f = topos.add_morphism(a, c, "f").unwrap();
|
||||
let g = topos.add_morphism(b, c, "g").unwrap();
|
||||
|
||||
// Pullback should exist in a topos
|
||||
let pullback = topos.pullback(f, g).unwrap();
|
||||
|
||||
assert!(pullback.is_valid());
|
||||
assert!(pullback.is_universal(&topos));
|
||||
}
|
||||
|
||||
/// Test exponential object existence
|
||||
#[test]
|
||||
fn test_exponential_exists() {
|
||||
let mut topos = Topos::set_topos();
|
||||
|
||||
let a = topos.add_object("A");
|
||||
let b = topos.add_object("B");
|
||||
|
||||
// Exponential B^A should exist
|
||||
let exp = topos.exponential(a, b).unwrap();
|
||||
|
||||
assert!(exp.is_valid());
|
||||
|
||||
// Evaluation morphism should exist
|
||||
let eval = topos.evaluation_morphism(a, b).unwrap();
|
||||
let eval_data = topos.get_morphism(eval).unwrap();
|
||||
|
||||
// eval: B^A x A -> B
|
||||
let product = topos.product(exp.id(), a).unwrap();
|
||||
assert_eq!(eval_data.source, product.id());
|
||||
assert_eq!(eval_data.target, b);
|
||||
}
|
||||
|
||||
/// Test power object
|
||||
#[test]
|
||||
fn test_power_object() {
|
||||
let mut topos = Topos::set_topos();
|
||||
|
||||
let a = topos.add_object("A");
|
||||
let omega = topos.subobject_classifier().unwrap();
|
||||
|
||||
// Power object P(A) = Omega^A
|
||||
let power_a = topos.exponential(a, omega.id()).unwrap();
|
||||
|
||||
assert!(power_a.is_valid());
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HIGHER CATEGORY TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod higher_category_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test 2-category structure
|
||||
#[test]
|
||||
fn test_two_category_structure() {
|
||||
let mut two_cat = TwoCategory::new();
|
||||
|
||||
// Add objects (0-cells)
|
||||
let a = two_cat.add_object("A");
|
||||
let b = two_cat.add_object("B");
|
||||
|
||||
// Add 1-morphisms
|
||||
let f = two_cat.add_1_morphism(a, b, "f").unwrap();
|
||||
let g = two_cat.add_1_morphism(a, b, "g").unwrap();
|
||||
|
||||
// Add 2-morphism alpha: f => g
|
||||
let alpha = two_cat.add_2_morphism(f, g, "alpha").unwrap();
|
||||
|
||||
assert!(two_cat.get_2_morphism(alpha).is_some());
|
||||
}
|
||||
|
||||
/// Test horizontal composition of 2-morphisms
|
||||
#[test]
|
||||
fn test_horizontal_composition() {
|
||||
let mut two_cat = TwoCategory::new();
|
||||
|
||||
let a = two_cat.add_object("A");
|
||||
let b = two_cat.add_object("B");
|
||||
let c = two_cat.add_object("C");
|
||||
|
||||
let f = two_cat.add_1_morphism(a, b, "f").unwrap();
|
||||
let g = two_cat.add_1_morphism(a, b, "g").unwrap();
|
||||
let h = two_cat.add_1_morphism(b, c, "h").unwrap();
|
||||
let k = two_cat.add_1_morphism(b, c, "k").unwrap();
|
||||
|
||||
let alpha = two_cat.add_2_morphism(f, g, "alpha").unwrap();
|
||||
let beta = two_cat.add_2_morphism(h, k, "beta").unwrap();
|
||||
|
||||
// Horizontal composition: beta * alpha : h.f => k.g
|
||||
let composed = two_cat.horizontal_compose(beta, alpha).unwrap();
|
||||
|
||||
assert!(two_cat.get_2_morphism(composed).is_some());
|
||||
}
|
||||
|
||||
/// Test vertical composition of 2-morphisms
|
||||
#[test]
|
||||
fn test_vertical_composition() {
|
||||
let mut two_cat = TwoCategory::new();
|
||||
|
||||
let a = two_cat.add_object("A");
|
||||
let b = two_cat.add_object("B");
|
||||
|
||||
let f = two_cat.add_1_morphism(a, b, "f").unwrap();
|
||||
let g = two_cat.add_1_morphism(a, b, "g").unwrap();
|
||||
let h = two_cat.add_1_morphism(a, b, "h").unwrap();
|
||||
|
||||
let alpha = two_cat.add_2_morphism(f, g, "alpha").unwrap();
|
||||
let beta = two_cat.add_2_morphism(g, h, "beta").unwrap();
|
||||
|
||||
// Vertical composition: beta . alpha : f => h
|
||||
let composed = two_cat.vertical_compose(beta, alpha).unwrap();
|
||||
|
||||
let composed_data = two_cat.get_2_morphism(composed).unwrap();
|
||||
assert_eq!(composed_data.source_1_morphism, f);
|
||||
assert_eq!(composed_data.target_1_morphism, h);
|
||||
}
|
||||
|
||||
/// Test interchange law: (delta . gamma) * (beta . alpha) = (delta * beta) . (gamma * alpha)
|
||||
#[test]
|
||||
fn test_interchange_law() {
|
||||
let mut two_cat = TwoCategory::new();
|
||||
|
||||
let a = two_cat.add_object("A");
|
||||
let b = two_cat.add_object("B");
|
||||
let c = two_cat.add_object("C");
|
||||
|
||||
// Setup for interchange law test
|
||||
let f = two_cat.add_1_morphism(a, b, "f").unwrap();
|
||||
let g = two_cat.add_1_morphism(a, b, "g").unwrap();
|
||||
let h = two_cat.add_1_morphism(a, b, "h").unwrap();
|
||||
|
||||
let p = two_cat.add_1_morphism(b, c, "p").unwrap();
|
||||
let q = two_cat.add_1_morphism(b, c, "q").unwrap();
|
||||
let r = two_cat.add_1_morphism(b, c, "r").unwrap();
|
||||
|
||||
let alpha = two_cat.add_2_morphism(f, g, "alpha").unwrap();
|
||||
let beta = two_cat.add_2_morphism(g, h, "beta").unwrap();
|
||||
let gamma = two_cat.add_2_morphism(p, q, "gamma").unwrap();
|
||||
let delta = two_cat.add_2_morphism(q, r, "delta").unwrap();
|
||||
|
||||
// Left side: (delta . gamma) * (beta . alpha)
|
||||
let delta_gamma = two_cat.vertical_compose(delta, gamma).unwrap();
|
||||
let beta_alpha = two_cat.vertical_compose(beta, alpha).unwrap();
|
||||
let left = two_cat.horizontal_compose(delta_gamma, beta_alpha).unwrap();
|
||||
|
||||
// Right side: (delta * beta) . (gamma * alpha)
|
||||
let delta_beta = two_cat.horizontal_compose(delta, beta).unwrap();
|
||||
let gamma_alpha = two_cat.horizontal_compose(gamma, alpha).unwrap();
|
||||
let right = two_cat.vertical_compose(delta_beta, gamma_alpha).unwrap();
|
||||
|
||||
// Both should represent the same 2-morphism
|
||||
let left_data = two_cat.get_2_morphism(left).unwrap();
|
||||
let right_data = two_cat.get_2_morphism(right).unwrap();
|
||||
|
||||
assert_eq!(left_data.source_1_morphism, right_data.source_1_morphism);
|
||||
assert_eq!(left_data.target_1_morphism, right_data.target_1_morphism);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COHERENCE VERIFICATION TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod coherence_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test pentagon identity for associator
|
||||
#[test]
|
||||
fn test_pentagon_identity() {
|
||||
let mut cat = VectorCategory::new(2);
|
||||
|
||||
let a = cat.add_object("A");
|
||||
let b = cat.add_object("B");
|
||||
let c = cat.add_object("C");
|
||||
let d = cat.add_object("D");
|
||||
|
||||
let result = verify_pentagon(&cat, a, b, c, d);
|
||||
|
||||
match result {
|
||||
CoherenceResult::Satisfied => (),
|
||||
CoherenceResult::Violated(msg) => panic!("Pentagon failed: {}", msg),
|
||||
CoherenceResult::NotApplicable => (), // May not apply for this category
|
||||
}
|
||||
}
|
||||
|
||||
/// Test triangle identity for unitor
|
||||
#[test]
|
||||
fn test_triangle_identity() {
|
||||
let mut cat = VectorCategory::new(2);
|
||||
|
||||
let a = cat.add_object("A");
|
||||
let b = cat.add_object("B");
|
||||
|
||||
let result = verify_triangle(&cat, a, b);
|
||||
|
||||
match result {
|
||||
CoherenceResult::Satisfied => (),
|
||||
CoherenceResult::Violated(msg) => panic!("Triangle failed: {}", msg),
|
||||
CoherenceResult::NotApplicable => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test Mac Lane's coherence theorem implications
|
||||
#[test]
|
||||
fn test_coherence_theorem() {
|
||||
// Any two parallel morphisms built from associators and unitors
|
||||
// in a monoidal category are equal
|
||||
|
||||
let mut cat = VectorCategory::with_monoidal_structure(2);
|
||||
|
||||
let a = cat.add_object("A");
|
||||
let b = cat.add_object("B");
|
||||
let c = cat.add_object("C");
|
||||
|
||||
// Two different bracketings should give same result
|
||||
let ab = cat.tensor_product(a, b).unwrap();
|
||||
let bc = cat.tensor_product(b, c).unwrap();
|
||||
|
||||
let ab_c = cat.tensor_product(ab, c).unwrap();
|
||||
let a_bc = cat.tensor_product(a, bc).unwrap();
|
||||
|
||||
// The associator should provide canonical isomorphism
|
||||
let assoc = cat.associator(a, b, c).unwrap();
|
||||
|
||||
let assoc_data = cat.get_morphism(assoc).unwrap();
|
||||
assert_eq!(assoc_data.source, ab_c);
|
||||
assert_eq!(assoc_data.target, a_bc);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROPERTY-BASED TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
|
||||
proptest! {
|
||||
/// Property: Identity is unique (any id satisfies identity laws is THE identity)
|
||||
#[test]
|
||||
fn prop_identity_unique(n in 1..10usize) {
|
||||
let mut cat = SetCategory::new();
|
||||
let objects: Vec<_> = (0..n).map(|i| cat.add_object(&format!("O{}", i))).collect();
|
||||
|
||||
for &obj in &objects {
|
||||
let id1 = cat.identity(obj).unwrap();
|
||||
let id2 = cat.identity(obj).unwrap();
|
||||
|
||||
// Both satisfy identity laws, so must be "equal"
|
||||
let id1_data = cat.get_morphism(id1).unwrap();
|
||||
let id2_data = cat.get_morphism(id2).unwrap();
|
||||
|
||||
prop_assert_eq!(id1_data.source, id2_data.source);
|
||||
prop_assert_eq!(id1_data.target, id2_data.target);
|
||||
}
|
||||
}
|
||||
|
||||
/// Property: Composition is closed
|
||||
#[test]
|
||||
fn prop_composition_closed(n in 2..5usize) {
|
||||
let mut cat = SetCategory::new();
|
||||
let objects: Vec<_> = (0..n).map(|i| cat.add_object(&format!("O{}", i))).collect();
|
||||
|
||||
// Create chain of morphisms
|
||||
let mut morphisms = Vec::new();
|
||||
for i in 0..(n-1) {
|
||||
let m = cat.add_morphism(objects[i], objects[i+1], &format!("f{}", i)).unwrap();
|
||||
morphisms.push(m);
|
||||
}
|
||||
|
||||
// Compose all
|
||||
let mut result = morphisms[0];
|
||||
for &m in &morphisms[1..] {
|
||||
result = cat.compose(m, result).unwrap();
|
||||
}
|
||||
|
||||
// Result should still be a valid morphism
|
||||
prop_assert!(cat.get_morphism(result).is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EDGE CASE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod edge_case_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test empty category
|
||||
#[test]
|
||||
fn test_empty_category() {
|
||||
let cat = SetCategory::new();
|
||||
assert!(cat.verify_laws()); // Empty category trivially satisfies laws
|
||||
}
|
||||
|
||||
/// Test single-object category (monoid)
|
||||
#[test]
|
||||
fn test_monoid_category() {
|
||||
let mut cat = SetCategory::new();
|
||||
let a = cat.add_object("A");
|
||||
|
||||
// Self-morphisms form a monoid
|
||||
let f = cat.add_morphism(a, a, "f").unwrap();
|
||||
let g = cat.add_morphism(a, a, "g").unwrap();
|
||||
|
||||
// Should compose
|
||||
let fg = cat.compose(f, g).unwrap();
|
||||
let gf = cat.compose(g, f).unwrap();
|
||||
|
||||
// Both compositions valid
|
||||
assert!(cat.get_morphism(fg).is_some());
|
||||
assert!(cat.get_morphism(gf).is_some());
|
||||
}
|
||||
|
||||
/// Test morphism lookup for non-existent morphism
|
||||
#[test]
|
||||
fn test_nonexistent_morphism() {
|
||||
let cat = SetCategory::new();
|
||||
let fake_id = MorphismId::new();
|
||||
|
||||
assert!(cat.get_morphism(fake_id).is_none());
|
||||
}
|
||||
|
||||
/// Test object lookup for non-existent object
|
||||
#[test]
|
||||
fn test_nonexistent_object() {
|
||||
let cat = SetCategory::new();
|
||||
let fake_id = ObjectId::new();
|
||||
|
||||
assert!(cat.get_object(fake_id).is_none());
|
||||
}
|
||||
}
|
||||
915
vendor/ruvector/examples/prime-radiant/tests/causal_tests.rs
vendored
Normal file
915
vendor/ruvector/examples/prime-radiant/tests/causal_tests.rs
vendored
Normal file
@@ -0,0 +1,915 @@
|
||||
//! Comprehensive tests for Causal Inference Module
|
||||
//!
|
||||
//! This test suite verifies causal reasoning including:
|
||||
//! - DAG validation
|
||||
//! - Intervention semantics (do-calculus)
|
||||
//! - Counterfactual computation
|
||||
//! - Causal abstraction consistency
|
||||
|
||||
use prime_radiant::causal::{
|
||||
CausalModel, StructuralEquation, Variable, VariableId, VariableType, Value,
|
||||
CausalAbstraction, AbstractionMap, ConsistencyResult,
|
||||
CausalCoherenceChecker, CausalConsistency, Belief,
|
||||
counterfactual, causal_effect, Observation, Distribution,
|
||||
DirectedGraph, TopologicalOrder, DAGValidationError,
|
||||
DoCalculus, Rule, Identification,
|
||||
};
|
||||
use prime_radiant::causal::integration::{SheafGraph, causal_coherence_energy, CoherenceEnergy};
|
||||
use proptest::prelude::*;
|
||||
use approx::assert_relative_eq;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
// =============================================================================
|
||||
// DAG VALIDATION TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod dag_validation_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test basic DAG creation
|
||||
#[test]
|
||||
fn test_create_dag() {
|
||||
let mut graph = DirectedGraph::new();
|
||||
graph.add_node(0);
|
||||
graph.add_node(1);
|
||||
graph.add_node(2);
|
||||
|
||||
assert_eq!(graph.node_count(), 3);
|
||||
}
|
||||
|
||||
/// Test adding valid edges
|
||||
#[test]
|
||||
fn test_add_valid_edges() {
|
||||
let mut graph = DirectedGraph::new();
|
||||
graph.add_edge(0, 1).unwrap();
|
||||
graph.add_edge(1, 2).unwrap();
|
||||
graph.add_edge(0, 2).unwrap();
|
||||
|
||||
assert_eq!(graph.edge_count(), 3);
|
||||
assert!(graph.contains_edge(0, 1));
|
||||
assert!(graph.contains_edge(1, 2));
|
||||
assert!(graph.contains_edge(0, 2));
|
||||
}
|
||||
|
||||
/// Test cycle detection
|
||||
#[test]
|
||||
fn test_cycle_detection() {
|
||||
let mut graph = DirectedGraph::new();
|
||||
graph.add_edge(0, 1).unwrap();
|
||||
graph.add_edge(1, 2).unwrap();
|
||||
|
||||
// Adding 2 -> 0 would create a cycle
|
||||
let result = graph.add_edge(2, 0);
|
||||
assert!(result.is_err());
|
||||
|
||||
match result {
|
||||
Err(DAGValidationError::CycleDetected(nodes)) => {
|
||||
assert!(!nodes.is_empty());
|
||||
}
|
||||
_ => panic!("Expected CycleDetected error"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test self-loop detection
|
||||
#[test]
|
||||
fn test_self_loop_detection() {
|
||||
let mut graph = DirectedGraph::new();
|
||||
let result = graph.add_edge(0, 0);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result, Err(DAGValidationError::SelfLoop(0))));
|
||||
}
|
||||
|
||||
/// Test topological ordering
|
||||
#[test]
|
||||
fn test_topological_order() {
|
||||
let mut graph = DirectedGraph::new();
|
||||
// Diamond graph: 0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3
|
||||
graph.add_edge(0, 1).unwrap();
|
||||
graph.add_edge(0, 2).unwrap();
|
||||
graph.add_edge(1, 3).unwrap();
|
||||
graph.add_edge(2, 3).unwrap();
|
||||
|
||||
let order = graph.topological_order().unwrap();
|
||||
|
||||
assert_eq!(order.len(), 4);
|
||||
assert!(order.comes_before(0, 1));
|
||||
assert!(order.comes_before(0, 2));
|
||||
assert!(order.comes_before(1, 3));
|
||||
assert!(order.comes_before(2, 3));
|
||||
}
|
||||
|
||||
/// Test ancestors computation
|
||||
#[test]
|
||||
fn test_ancestors() {
|
||||
let mut graph = DirectedGraph::new();
|
||||
graph.add_edge(0, 1).unwrap();
|
||||
graph.add_edge(1, 2).unwrap();
|
||||
graph.add_edge(0, 3).unwrap();
|
||||
graph.add_edge(3, 2).unwrap();
|
||||
|
||||
let ancestors = graph.ancestors(2);
|
||||
|
||||
assert!(ancestors.contains(&0));
|
||||
assert!(ancestors.contains(&1));
|
||||
assert!(ancestors.contains(&3));
|
||||
assert!(!ancestors.contains(&2));
|
||||
}
|
||||
|
||||
/// Test descendants computation
|
||||
#[test]
|
||||
fn test_descendants() {
|
||||
let mut graph = DirectedGraph::new();
|
||||
graph.add_edge(0, 1).unwrap();
|
||||
graph.add_edge(0, 2).unwrap();
|
||||
graph.add_edge(1, 3).unwrap();
|
||||
graph.add_edge(2, 3).unwrap();
|
||||
|
||||
let descendants = graph.descendants(0);
|
||||
|
||||
assert!(descendants.contains(&1));
|
||||
assert!(descendants.contains(&2));
|
||||
assert!(descendants.contains(&3));
|
||||
assert!(!descendants.contains(&0));
|
||||
}
|
||||
|
||||
/// Test d-separation in chain
|
||||
#[test]
|
||||
fn test_d_separation_chain() {
|
||||
// X -> Z -> Y (chain)
|
||||
let mut graph = DirectedGraph::new();
|
||||
graph.add_node_with_label(0, "X");
|
||||
graph.add_node_with_label(1, "Z");
|
||||
graph.add_node_with_label(2, "Y");
|
||||
graph.add_edge(0, 1).unwrap();
|
||||
graph.add_edge(1, 2).unwrap();
|
||||
|
||||
let x: HashSet<u32> = [0].into_iter().collect();
|
||||
let y: HashSet<u32> = [2].into_iter().collect();
|
||||
let z: HashSet<u32> = [1].into_iter().collect();
|
||||
let empty: HashSet<u32> = HashSet::new();
|
||||
|
||||
// X and Y are NOT d-separated given empty set
|
||||
assert!(!graph.d_separated(&x, &y, &empty));
|
||||
|
||||
// X and Y ARE d-separated given Z
|
||||
assert!(graph.d_separated(&x, &y, &z));
|
||||
}
|
||||
|
||||
/// Test d-separation in fork
|
||||
#[test]
|
||||
fn test_d_separation_fork() {
|
||||
// X <- Z -> Y (fork)
|
||||
let mut graph = DirectedGraph::new();
|
||||
graph.add_edge(1, 0).unwrap(); // Z -> X
|
||||
graph.add_edge(1, 2).unwrap(); // Z -> Y
|
||||
|
||||
let x: HashSet<u32> = [0].into_iter().collect();
|
||||
let y: HashSet<u32> = [2].into_iter().collect();
|
||||
let z: HashSet<u32> = [1].into_iter().collect();
|
||||
let empty: HashSet<u32> = HashSet::new();
|
||||
|
||||
// X and Y are NOT d-separated given empty set
|
||||
assert!(!graph.d_separated(&x, &y, &empty));
|
||||
|
||||
// X and Y ARE d-separated given Z
|
||||
assert!(graph.d_separated(&x, &y, &z));
|
||||
}
|
||||
|
||||
/// Test d-separation in collider
|
||||
#[test]
|
||||
fn test_d_separation_collider() {
|
||||
// X -> Z <- Y (collider)
|
||||
let mut graph = DirectedGraph::new();
|
||||
graph.add_edge(0, 1).unwrap(); // X -> Z
|
||||
graph.add_edge(2, 1).unwrap(); // Y -> Z
|
||||
|
||||
let x: HashSet<u32> = [0].into_iter().collect();
|
||||
let y: HashSet<u32> = [2].into_iter().collect();
|
||||
let z: HashSet<u32> = [1].into_iter().collect();
|
||||
let empty: HashSet<u32> = HashSet::new();
|
||||
|
||||
// X and Y ARE d-separated given empty set (collider blocks)
|
||||
assert!(graph.d_separated(&x, &y, &empty));
|
||||
|
||||
// X and Y are NOT d-separated given Z (conditioning opens collider)
|
||||
assert!(!graph.d_separated(&x, &y, &z));
|
||||
}
|
||||
|
||||
/// Test v-structure detection
|
||||
#[test]
|
||||
fn test_v_structures() {
|
||||
let mut graph = DirectedGraph::new();
|
||||
graph.add_edge(0, 2).unwrap(); // X -> Z
|
||||
graph.add_edge(1, 2).unwrap(); // Y -> Z
|
||||
|
||||
let v_structs = graph.v_structures();
|
||||
|
||||
assert_eq!(v_structs.len(), 1);
|
||||
let (a, b, c) = v_structs[0];
|
||||
assert_eq!(b, 2); // Z is the collider
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INTERVENTION TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod intervention_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test intervention do(X = x) removes incoming edges
|
||||
#[test]
|
||||
fn test_intervention_removes_incoming_edges() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
// Z -> X -> Y
|
||||
model.add_variable("Z", VariableType::Continuous).unwrap();
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let z_id = model.get_variable_id("Z").unwrap();
|
||||
let x_id = model.get_variable_id("X").unwrap();
|
||||
let y_id = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(z_id, x_id).unwrap(); // Z -> X
|
||||
model.add_edge(x_id, y_id).unwrap(); // X -> Y
|
||||
|
||||
// Structural equation: X = 2*Z + noise
|
||||
model.set_structural_equation(x_id, StructuralEquation::linear(&[z_id], vec![2.0]));
|
||||
|
||||
// Structural equation: Y = 3*X + noise
|
||||
model.set_structural_equation(y_id, StructuralEquation::linear(&[x_id], vec![3.0]));
|
||||
|
||||
// Before intervention, X depends on Z
|
||||
assert!(model.parents(&x_id).unwrap().contains(&z_id));
|
||||
|
||||
// Intervene do(X = 5)
|
||||
let mutilated = model.intervene(x_id, Value::Continuous(5.0)).unwrap();
|
||||
|
||||
// After intervention, X has no parents
|
||||
assert!(mutilated.parents(&x_id).unwrap().is_empty());
|
||||
|
||||
// Y still depends on X
|
||||
assert!(mutilated.parents(&y_id).unwrap().contains(&x_id));
|
||||
}
|
||||
|
||||
/// Test interventional distribution differs from observational
|
||||
#[test]
|
||||
fn test_interventional_vs_observational() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
// Confounded: Z -> X, Z -> Y, X -> Y
|
||||
model.add_variable("Z", VariableType::Continuous).unwrap();
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let z_id = model.get_variable_id("Z").unwrap();
|
||||
let x_id = model.get_variable_id("X").unwrap();
|
||||
let y_id = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(z_id, x_id).unwrap();
|
||||
model.add_edge(z_id, y_id).unwrap();
|
||||
model.add_edge(x_id, y_id).unwrap();
|
||||
|
||||
// Compute observational P(Y | X = 1)
|
||||
let obs = Observation::new(&[("X", Value::Continuous(1.0))]);
|
||||
let p_y_given_x = model.conditional_distribution(&obs, "Y").unwrap();
|
||||
|
||||
// Compute interventional P(Y | do(X = 1))
|
||||
let mutilated = model.intervene(x_id, Value::Continuous(1.0)).unwrap();
|
||||
let p_y_do_x = mutilated.marginal_distribution("Y").unwrap();
|
||||
|
||||
// These should generally differ due to confounding
|
||||
// (The specific values depend on structural equations)
|
||||
assert!(p_y_given_x != p_y_do_x || model.is_unconfounded(x_id, y_id));
|
||||
}
|
||||
|
||||
/// Test average treatment effect computation
|
||||
#[test]
|
||||
fn test_average_treatment_effect() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
// Simple model: Treatment -> Outcome
|
||||
model.add_variable("T", VariableType::Binary).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let t_id = model.get_variable_id("T").unwrap();
|
||||
let y_id = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(t_id, y_id).unwrap();
|
||||
|
||||
// Y = 2*T + epsilon
|
||||
model.set_structural_equation(y_id, StructuralEquation::linear(&[t_id], vec![2.0]));
|
||||
|
||||
// ATE = E[Y | do(T=1)] - E[Y | do(T=0)]
|
||||
let ate = causal_effect(&model, t_id, y_id,
|
||||
Value::Binary(true),
|
||||
Value::Binary(false)
|
||||
).unwrap();
|
||||
|
||||
// Should be approximately 2.0
|
||||
assert_relative_eq!(ate, 2.0, epsilon = 0.5);
|
||||
}
|
||||
|
||||
/// Test multiple simultaneous interventions
|
||||
#[test]
|
||||
fn test_multiple_interventions() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Z", VariableType::Continuous).unwrap();
|
||||
|
||||
let x_id = model.get_variable_id("X").unwrap();
|
||||
let y_id = model.get_variable_id("Y").unwrap();
|
||||
let z_id = model.get_variable_id("Z").unwrap();
|
||||
|
||||
model.add_edge(x_id, z_id).unwrap();
|
||||
model.add_edge(y_id, z_id).unwrap();
|
||||
|
||||
// Intervene on both X and Y
|
||||
let interventions = vec![
|
||||
(x_id, Value::Continuous(1.0)),
|
||||
(y_id, Value::Continuous(2.0)),
|
||||
];
|
||||
|
||||
let mutilated = model.multi_intervene(&interventions).unwrap();
|
||||
|
||||
// Both X and Y should have no parents
|
||||
assert!(mutilated.parents(&x_id).unwrap().is_empty());
|
||||
assert!(mutilated.parents(&y_id).unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COUNTERFACTUAL TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod counterfactual_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test basic counterfactual computation
|
||||
#[test]
|
||||
fn test_basic_counterfactual() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
// X -> Y with Y = 2*X
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let x_id = model.get_variable_id("X").unwrap();
|
||||
let y_id = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(x_id, y_id).unwrap();
|
||||
model.set_structural_equation(y_id, StructuralEquation::linear(&[x_id], vec![2.0]));
|
||||
|
||||
// Observe Y = 4 (implies X = 2)
|
||||
let observation = Observation::new(&[("Y", Value::Continuous(4.0))]);
|
||||
|
||||
// Counterfactual: What would Y be if X = 3?
|
||||
let cf_y = counterfactual(&model, &observation, x_id, Value::Continuous(3.0), "Y").unwrap();
|
||||
|
||||
// Y' = 2 * 3 = 6
|
||||
match cf_y {
|
||||
Value::Continuous(y) => assert_relative_eq!(y, 6.0, epsilon = 0.1),
|
||||
_ => panic!("Expected continuous value"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test counterfactual with noise inference
|
||||
#[test]
|
||||
fn test_counterfactual_with_noise() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
// X -> Y with Y = X + U_Y where U_Y is noise
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let x_id = model.get_variable_id("X").unwrap();
|
||||
let y_id = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(x_id, y_id).unwrap();
|
||||
model.set_structural_equation(y_id, StructuralEquation::with_noise(&[x_id], vec![1.0]));
|
||||
|
||||
// Observe X = 1, Y = 3 (so U_Y = 2)
|
||||
let observation = Observation::new(&[
|
||||
("X", Value::Continuous(1.0)),
|
||||
("Y", Value::Continuous(3.0)),
|
||||
]);
|
||||
|
||||
// What if X = 2?
|
||||
let cf_y = counterfactual(&model, &observation, x_id, Value::Continuous(2.0), "Y").unwrap();
|
||||
|
||||
// Y' = 2 + 2 = 4 (noise U_Y = 2 is preserved)
|
||||
match cf_y {
|
||||
Value::Continuous(y) => assert_relative_eq!(y, 4.0, epsilon = 0.1),
|
||||
_ => panic!("Expected continuous value"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test counterfactual consistency
|
||||
#[test]
|
||||
fn test_counterfactual_consistency() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let x_id = model.get_variable_id("X").unwrap();
|
||||
let y_id = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(x_id, y_id).unwrap();
|
||||
model.set_structural_equation(y_id, StructuralEquation::linear(&[x_id], vec![2.0]));
|
||||
|
||||
// Observe X = 2, Y = 4
|
||||
let observation = Observation::new(&[
|
||||
("X", Value::Continuous(2.0)),
|
||||
("Y", Value::Continuous(4.0)),
|
||||
]);
|
||||
|
||||
// Counterfactual with actual value should match observed
|
||||
let cf_y = counterfactual(&model, &observation, x_id, Value::Continuous(2.0), "Y").unwrap();
|
||||
|
||||
match cf_y {
|
||||
Value::Continuous(y) => assert_relative_eq!(y, 4.0, epsilon = 0.1),
|
||||
_ => panic!("Expected continuous value"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test effect of treatment on treated (ETT)
|
||||
#[test]
|
||||
fn test_effect_on_treated() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
model.add_variable("T", VariableType::Binary).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let t_id = model.get_variable_id("T").unwrap();
|
||||
let y_id = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(t_id, y_id).unwrap();
|
||||
model.set_structural_equation(y_id, StructuralEquation::linear(&[t_id], vec![5.0]));
|
||||
|
||||
// For treated individuals (T = 1), what would Y be if T = 0?
|
||||
let observation = Observation::new(&[
|
||||
("T", Value::Binary(true)),
|
||||
("Y", Value::Continuous(5.0)),
|
||||
]);
|
||||
|
||||
let cf_y = counterfactual(&model, &observation, t_id, Value::Binary(false), "Y").unwrap();
|
||||
|
||||
// ETT = Y(T=1) - Y(T=0) for treated
|
||||
match cf_y {
|
||||
Value::Continuous(y_untreated) => {
|
||||
let ett = 5.0 - y_untreated;
|
||||
assert_relative_eq!(ett, 5.0, epsilon = 0.5);
|
||||
}
|
||||
_ => panic!("Expected continuous value"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CAUSAL ABSTRACTION TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod causal_abstraction_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test abstraction map between models
|
||||
#[test]
|
||||
fn test_abstraction_map() {
|
||||
// Low-level model: X1 -> X2 -> X3
|
||||
let mut low = CausalModel::new();
|
||||
low.add_variable("X1", VariableType::Continuous).unwrap();
|
||||
low.add_variable("X2", VariableType::Continuous).unwrap();
|
||||
low.add_variable("X3", VariableType::Continuous).unwrap();
|
||||
|
||||
let x1 = low.get_variable_id("X1").unwrap();
|
||||
let x2 = low.get_variable_id("X2").unwrap();
|
||||
let x3 = low.get_variable_id("X3").unwrap();
|
||||
|
||||
low.add_edge(x1, x2).unwrap();
|
||||
low.add_edge(x2, x3).unwrap();
|
||||
|
||||
// High-level model: A -> B
|
||||
let mut high = CausalModel::new();
|
||||
high.add_variable("A", VariableType::Continuous).unwrap();
|
||||
high.add_variable("B", VariableType::Continuous).unwrap();
|
||||
|
||||
let a = high.get_variable_id("A").unwrap();
|
||||
let b = high.get_variable_id("B").unwrap();
|
||||
|
||||
high.add_edge(a, b).unwrap();
|
||||
|
||||
// Abstraction: A = X1, B = X3 (X2 is "hidden")
|
||||
let abstraction = CausalAbstraction::new(&low, &high);
|
||||
abstraction.add_mapping(x1, a);
|
||||
abstraction.add_mapping(x3, b);
|
||||
|
||||
assert!(abstraction.is_valid_abstraction());
|
||||
}
|
||||
|
||||
/// Test abstraction consistency
|
||||
#[test]
|
||||
fn test_abstraction_consistency() {
|
||||
// Two-level model
|
||||
let mut low = CausalModel::new();
|
||||
low.add_variable("X", VariableType::Continuous).unwrap();
|
||||
low.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let x = low.get_variable_id("X").unwrap();
|
||||
let y = low.get_variable_id("Y").unwrap();
|
||||
|
||||
low.add_edge(x, y).unwrap();
|
||||
low.set_structural_equation(y, StructuralEquation::linear(&[x], vec![2.0]));
|
||||
|
||||
let mut high = CausalModel::new();
|
||||
high.add_variable("A", VariableType::Continuous).unwrap();
|
||||
high.add_variable("B", VariableType::Continuous).unwrap();
|
||||
|
||||
let a = high.get_variable_id("A").unwrap();
|
||||
let b = high.get_variable_id("B").unwrap();
|
||||
|
||||
high.add_edge(a, b).unwrap();
|
||||
high.set_structural_equation(b, StructuralEquation::linear(&[a], vec![2.0]));
|
||||
|
||||
let abstraction = CausalAbstraction::new(&low, &high);
|
||||
abstraction.add_mapping(x, a);
|
||||
abstraction.add_mapping(y, b);
|
||||
|
||||
let result = abstraction.check_consistency();
|
||||
assert!(matches!(result, ConsistencyResult::Consistent));
|
||||
}
|
||||
|
||||
/// Test intervention consistency across abstraction
|
||||
#[test]
|
||||
fn test_intervention_consistency() {
|
||||
let mut low = CausalModel::new();
|
||||
low.add_variable("X", VariableType::Continuous).unwrap();
|
||||
low.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let x = low.get_variable_id("X").unwrap();
|
||||
let y = low.get_variable_id("Y").unwrap();
|
||||
|
||||
low.add_edge(x, y).unwrap();
|
||||
low.set_structural_equation(y, StructuralEquation::linear(&[x], vec![3.0]));
|
||||
|
||||
let mut high = CausalModel::new();
|
||||
high.add_variable("A", VariableType::Continuous).unwrap();
|
||||
high.add_variable("B", VariableType::Continuous).unwrap();
|
||||
|
||||
let a = high.get_variable_id("A").unwrap();
|
||||
let b = high.get_variable_id("B").unwrap();
|
||||
|
||||
high.add_edge(a, b).unwrap();
|
||||
high.set_structural_equation(b, StructuralEquation::linear(&[a], vec![3.0]));
|
||||
|
||||
let abstraction = CausalAbstraction::new(&low, &high);
|
||||
abstraction.add_mapping(x, a);
|
||||
abstraction.add_mapping(y, b);
|
||||
|
||||
// Intervene on low-level model
|
||||
let low_intervened = low.intervene(x, Value::Continuous(5.0)).unwrap();
|
||||
let low_y = low_intervened.compute("Y").unwrap();
|
||||
|
||||
// Intervene on high-level model
|
||||
let high_intervened = high.intervene(a, Value::Continuous(5.0)).unwrap();
|
||||
let high_b = high_intervened.compute("B").unwrap();
|
||||
|
||||
// Results should match
|
||||
match (low_y, high_b) {
|
||||
(Value::Continuous(ly), Value::Continuous(hb)) => {
|
||||
assert_relative_eq!(ly, hb, epsilon = 0.1);
|
||||
}
|
||||
_ => panic!("Expected continuous values"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CAUSAL COHERENCE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod causal_coherence_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test causal coherence checker
|
||||
#[test]
|
||||
fn test_causal_coherence_consistent() {
|
||||
let checker = CausalCoherenceChecker::new();
|
||||
|
||||
let mut model = CausalModel::new();
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let x = model.get_variable_id("X").unwrap();
|
||||
let y = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(x, y).unwrap();
|
||||
|
||||
// Belief: X causes Y
|
||||
let belief = Belief::causal_relation("X", "Y", true);
|
||||
|
||||
let result = checker.check(&model, &[belief]);
|
||||
assert!(matches!(result, CausalConsistency::Consistent));
|
||||
}
|
||||
|
||||
/// Test detecting spurious correlation
|
||||
#[test]
|
||||
fn test_detect_spurious_correlation() {
|
||||
let checker = CausalCoherenceChecker::new();
|
||||
|
||||
let mut model = CausalModel::new();
|
||||
// Z -> X, Z -> Y (confounded)
|
||||
model.add_variable("Z", VariableType::Continuous).unwrap();
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let z = model.get_variable_id("Z").unwrap();
|
||||
let x = model.get_variable_id("X").unwrap();
|
||||
let y = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(z, x).unwrap();
|
||||
model.add_edge(z, y).unwrap();
|
||||
|
||||
// Mistaken belief: X causes Y
|
||||
let belief = Belief::causal_relation("X", "Y", true);
|
||||
|
||||
let result = checker.check(&model, &[belief]);
|
||||
assert!(matches!(result, CausalConsistency::SpuriousCorrelation(_)));
|
||||
}
|
||||
|
||||
/// Test integration with sheaf coherence
|
||||
#[test]
|
||||
fn test_causal_sheaf_integration() {
|
||||
let sheaf = SheafGraph {
|
||||
nodes: vec!["X".to_string(), "Y".to_string()],
|
||||
edges: vec![(0, 1)],
|
||||
sections: vec![vec![1.0, 2.0], vec![2.0, 4.0]],
|
||||
};
|
||||
|
||||
let mut model = CausalModel::new();
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let x_id = model.get_variable_id("X").unwrap();
|
||||
let y_id = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(x_id, y_id).unwrap();
|
||||
|
||||
let energy = causal_coherence_energy(&sheaf, &model);
|
||||
|
||||
assert!(energy.structural_component >= 0.0);
|
||||
assert!(energy.causal_component >= 0.0);
|
||||
assert!(energy.total >= 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DO-CALCULUS TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod do_calculus_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test Rule 1: Ignoring observations
|
||||
#[test]
|
||||
fn test_rule1_ignoring_observations() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Z", VariableType::Continuous).unwrap();
|
||||
|
||||
let x = model.get_variable_id("X").unwrap();
|
||||
let y = model.get_variable_id("Y").unwrap();
|
||||
let z = model.get_variable_id("Z").unwrap();
|
||||
|
||||
model.add_edge(x, y).unwrap();
|
||||
model.add_edge(z, y).unwrap();
|
||||
|
||||
let calc = DoCalculus::new(&model);
|
||||
|
||||
// P(y | do(x), z) = P(y | do(x)) if Z d-separated from Y given X in mutilated graph
|
||||
let x_set: HashSet<_> = [x].into_iter().collect();
|
||||
let z_set: HashSet<_> = [z].into_iter().collect();
|
||||
let y_set: HashSet<_> = [y].into_iter().collect();
|
||||
|
||||
let rule1_applies = calc.can_apply_rule1(&y_set, &x_set, &z_set);
|
||||
assert!(!rule1_applies); // Z -> Y, so can't ignore Z
|
||||
}
|
||||
|
||||
/// Test Rule 2: Action/observation exchange
|
||||
#[test]
|
||||
fn test_rule2_action_observation_exchange() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Z", VariableType::Continuous).unwrap();
|
||||
|
||||
let x = model.get_variable_id("X").unwrap();
|
||||
let y = model.get_variable_id("Y").unwrap();
|
||||
let z = model.get_variable_id("Z").unwrap();
|
||||
|
||||
// X -> Z -> Y
|
||||
model.add_edge(x, z).unwrap();
|
||||
model.add_edge(z, y).unwrap();
|
||||
|
||||
let calc = DoCalculus::new(&model);
|
||||
|
||||
// P(y | do(x), do(z)) = P(y | do(x), z) if...
|
||||
let can_exchange = calc.can_apply_rule2(y, x, z);
|
||||
// Depends on the specific d-separation conditions
|
||||
assert!(can_exchange || !can_exchange); // Result depends on structure
|
||||
}
|
||||
|
||||
/// Test Rule 3: Removing actions
|
||||
#[test]
|
||||
fn test_rule3_removing_actions() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let x = model.get_variable_id("X").unwrap();
|
||||
let y = model.get_variable_id("Y").unwrap();
|
||||
|
||||
// No edge from X to Y
|
||||
// X and Y are independent
|
||||
|
||||
let calc = DoCalculus::new(&model);
|
||||
|
||||
// P(y | do(x)) = P(y) if X has no effect on Y
|
||||
let can_remove = calc.can_apply_rule3(y, x);
|
||||
assert!(can_remove);
|
||||
}
|
||||
|
||||
/// Test causal effect identification
|
||||
#[test]
|
||||
fn test_causal_effect_identification() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
// Simple identifiable case: X -> Y
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let x = model.get_variable_id("X").unwrap();
|
||||
let y = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(x, y).unwrap();
|
||||
|
||||
let calc = DoCalculus::new(&model);
|
||||
let result = calc.identify(y, &[x].into_iter().collect());
|
||||
|
||||
assert!(matches!(result, Identification::Identified(_)));
|
||||
}
|
||||
|
||||
/// Test non-identifiable case
|
||||
#[test]
|
||||
fn test_non_identifiable_effect() {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
// Confounded: U -> X, U -> Y, X -> Y (U unobserved)
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let x = model.get_variable_id("X").unwrap();
|
||||
let y = model.get_variable_id("Y").unwrap();
|
||||
|
||||
model.add_edge(x, y).unwrap();
|
||||
model.add_latent_confounding(x, y); // Unobserved confounder
|
||||
|
||||
let calc = DoCalculus::new(&model);
|
||||
let result = calc.identify(y, &[x].into_iter().collect());
|
||||
|
||||
// Without adjustment variables, effect is not identifiable
|
||||
assert!(matches!(result, Identification::NotIdentified(_)));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROPERTY-BASED TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
|
||||
proptest! {
|
||||
/// Property: Topological order respects all edges
|
||||
#[test]
|
||||
fn prop_topo_order_respects_edges(
|
||||
edges in proptest::collection::vec((0..10u32, 0..10u32), 0..20)
|
||||
) {
|
||||
let mut graph = DirectedGraph::new();
|
||||
|
||||
for (from, to) in &edges {
|
||||
if from != to {
|
||||
let _ = graph.add_edge(*from, *to); // May fail if creates cycle
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(order) = graph.topological_order() {
|
||||
for (from, to) in graph.edges() {
|
||||
prop_assert!(order.comes_before(from, to));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Property: Interventions don't create cycles
|
||||
#[test]
|
||||
fn prop_intervention_preserves_dag(
|
||||
n in 2..8usize,
|
||||
seed in 0..1000u64
|
||||
) {
|
||||
let mut model = CausalModel::new();
|
||||
|
||||
for i in 0..n {
|
||||
model.add_variable(&format!("V{}", i), VariableType::Continuous).unwrap();
|
||||
}
|
||||
|
||||
// Random DAG edges
|
||||
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(seed);
|
||||
for i in 0..n {
|
||||
for j in (i+1)..n {
|
||||
if rand::Rng::gen_bool(&mut rng, 0.3) {
|
||||
let vi = model.get_variable_id(&format!("V{}", i)).unwrap();
|
||||
let vj = model.get_variable_id(&format!("V{}", j)).unwrap();
|
||||
let _ = model.add_edge(vi, vj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Any intervention should preserve DAG property
|
||||
let v0 = model.get_variable_id("V0").unwrap();
|
||||
if let Ok(mutilated) = model.intervene(v0, Value::Continuous(1.0)) {
|
||||
prop_assert!(mutilated.is_dag());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EDGE CASE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod edge_case_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test empty model
|
||||
#[test]
|
||||
fn test_empty_model() {
|
||||
let model = CausalModel::new();
|
||||
assert_eq!(model.variable_count(), 0);
|
||||
}
|
||||
|
||||
/// Test single variable model
|
||||
#[test]
|
||||
fn test_single_variable() {
|
||||
let mut model = CausalModel::new();
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
|
||||
assert_eq!(model.variable_count(), 1);
|
||||
|
||||
let x = model.get_variable_id("X").unwrap();
|
||||
assert!(model.parents(&x).unwrap().is_empty());
|
||||
}
|
||||
|
||||
/// Test duplicate variable names
|
||||
#[test]
|
||||
fn test_duplicate_variable_name() {
|
||||
let mut model = CausalModel::new();
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
|
||||
let result = model.add_variable("X", VariableType::Continuous);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Test intervention on non-existent variable
|
||||
#[test]
|
||||
fn test_intervene_nonexistent() {
|
||||
let model = CausalModel::new();
|
||||
let fake_id = VariableId(999);
|
||||
|
||||
let result = model.intervene(fake_id, Value::Continuous(1.0));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Test empty observation counterfactual
|
||||
#[test]
|
||||
fn test_empty_observation_counterfactual() {
|
||||
let mut model = CausalModel::new();
|
||||
model.add_variable("X", VariableType::Continuous).unwrap();
|
||||
model.add_variable("Y", VariableType::Continuous).unwrap();
|
||||
|
||||
let x = model.get_variable_id("X").unwrap();
|
||||
|
||||
let empty_obs = Observation::new(&[]);
|
||||
let result = counterfactual(&model, &empty_obs, x, Value::Continuous(1.0), "Y");
|
||||
|
||||
// Should work with empty observation (uses prior)
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
702
vendor/ruvector/examples/prime-radiant/tests/cohomology_tests.rs
vendored
Normal file
702
vendor/ruvector/examples/prime-radiant/tests/cohomology_tests.rs
vendored
Normal file
@@ -0,0 +1,702 @@
|
||||
//! Comprehensive tests for Sheaf Cohomology Module
|
||||
//!
|
||||
//! This test suite verifies the mathematical properties of sheaf cohomology
|
||||
//! including coboundary operators, cohomology groups, and obstruction detection.
|
||||
|
||||
use prime_radiant::cohomology::{
|
||||
CohomologyEngine, CohomologyResult, SheafGraph, SheafNode, SheafEdge,
|
||||
Obstruction, BeliefGraphBuilder, CohomologyError,
|
||||
};
|
||||
use proptest::prelude::*;
|
||||
use approx::assert_relative_eq;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// =============================================================================
|
||||
// COBOUNDARY OPERATOR TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod coboundary_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test the fundamental property: delta^2 = 0
|
||||
/// The coboundary of a coboundary is always zero
|
||||
#[test]
|
||||
fn test_coboundary_squared_is_zero() {
|
||||
// Create a triangle graph (simplest complex with non-trivial cohomology)
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
// Add 3 nodes forming a triangle
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1.0, 0.0, 0.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![0.0, 1.0, 0.0]));
|
||||
graph.add_node(SheafNode::new(2, "C", vec![0.0, 0.0, 1.0]));
|
||||
|
||||
// Add edges with identity restriction maps
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 3)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(1, 2, 3)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(2, 0, 3)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
// The consistency energy should be computable
|
||||
assert!(result.consistency_energy >= 0.0);
|
||||
}
|
||||
|
||||
/// Test coboundary on exact sequences
|
||||
#[test]
|
||||
fn test_coboundary_on_consistent_sections() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
// Create nodes with identical sections (globally consistent)
|
||||
let section = vec![1.0, 2.0, 3.0];
|
||||
graph.add_node(SheafNode::new(0, "A", section.clone()));
|
||||
graph.add_node(SheafNode::new(1, "B", section.clone()));
|
||||
graph.add_node(SheafNode::new(2, "C", section.clone()));
|
||||
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 3)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(1, 2, 3)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
// Globally consistent sections should have zero consistency energy
|
||||
assert!(result.is_consistent);
|
||||
assert!(result.consistency_energy < 1e-10);
|
||||
}
|
||||
|
||||
/// Test coboundary with non-trivial restriction maps
|
||||
#[test]
|
||||
fn test_coboundary_with_projection_maps() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
// Higher-dimensional source, lower-dimensional target
|
||||
graph.add_node(SheafNode::new(0, "High", vec![1.0, 2.0, 3.0, 4.0]));
|
||||
graph.add_node(SheafNode::new(1, "Low", vec![1.0, 2.0]));
|
||||
|
||||
// Projection map: takes first 2 components
|
||||
let projection = vec![
|
||||
1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
];
|
||||
let edge = SheafEdge::with_map(0, 1, projection, 4, 2);
|
||||
graph.add_edge(edge).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
// Should be consistent since projection matches
|
||||
assert!(result.is_consistent);
|
||||
}
|
||||
|
||||
/// Test coboundary linearity: delta(af + bg) = a*delta(f) + b*delta(g)
|
||||
#[test]
|
||||
fn test_coboundary_linearity() {
|
||||
let mut graph1 = SheafGraph::new();
|
||||
let mut graph2 = SheafGraph::new();
|
||||
let mut graph_sum = SheafGraph::new();
|
||||
|
||||
// Graph 1
|
||||
graph1.add_node(SheafNode::new(0, "A", vec![1.0, 0.0]));
|
||||
graph1.add_node(SheafNode::new(1, "B", vec![0.0, 0.0]));
|
||||
graph1.add_edge(SheafEdge::identity(0, 1, 2)).unwrap();
|
||||
|
||||
// Graph 2
|
||||
graph2.add_node(SheafNode::new(0, "A", vec![0.0, 1.0]));
|
||||
graph2.add_node(SheafNode::new(1, "B", vec![0.0, 0.0]));
|
||||
graph2.add_edge(SheafEdge::identity(0, 1, 2)).unwrap();
|
||||
|
||||
// Sum graph
|
||||
graph_sum.add_node(SheafNode::new(0, "A", vec![1.0, 1.0]));
|
||||
graph_sum.add_node(SheafNode::new(1, "B", vec![0.0, 0.0]));
|
||||
graph_sum.add_edge(SheafEdge::identity(0, 1, 2)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
|
||||
let e1 = engine.compute_cohomology(&graph1).unwrap().consistency_energy;
|
||||
let e2 = engine.compute_cohomology(&graph2).unwrap().consistency_energy;
|
||||
let e_sum = engine.compute_cohomology(&graph_sum).unwrap().consistency_energy;
|
||||
|
||||
// Energy is quadratic, so E(sum) <= E1 + E2 + 2*sqrt(E1*E2)
|
||||
// But should satisfy triangle inequality for sqrt(energy)
|
||||
let sqrt_sum = e_sum.sqrt();
|
||||
let sqrt_bound = e1.sqrt() + e2.sqrt();
|
||||
assert!(sqrt_sum <= sqrt_bound + 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COHOMOLOGY GROUP TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod cohomology_group_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test H^0 computation (global sections)
|
||||
#[test]
|
||||
fn test_h0_connected_graph() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
// Create a path graph: A -- B -- C
|
||||
let section = vec![1.0, 2.0];
|
||||
graph.add_node(SheafNode::new(0, "A", section.clone()));
|
||||
graph.add_node(SheafNode::new(1, "B", section.clone()));
|
||||
graph.add_node(SheafNode::new(2, "C", section.clone()));
|
||||
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 2)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(1, 2, 2)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
// For consistent sections, H^0 dimension should be positive
|
||||
assert!(result.h0_dim > 0);
|
||||
}
|
||||
|
||||
/// Test H^0 on disconnected components
|
||||
#[test]
|
||||
fn test_h0_disconnected_graph() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
// Two disconnected nodes
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1.0, 0.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![0.0, 1.0]));
|
||||
// No edges - disconnected
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
// Disconnected components each contribute to H^0
|
||||
// With no edges, no consistency constraints
|
||||
assert!(result.is_consistent);
|
||||
}
|
||||
|
||||
/// Test H^1 detection (obstruction group)
|
||||
#[test]
|
||||
fn test_h1_obstruction_detection() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
// Create inconsistent triangle
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1.0, 0.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![0.0, 1.0]));
|
||||
graph.add_node(SheafNode::new(2, "C", vec![1.0, 1.0]));
|
||||
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 2)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(1, 2, 2)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(2, 0, 2)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
// Should detect inconsistency
|
||||
assert!(!result.is_consistent);
|
||||
assert!(result.consistency_energy > 0.0);
|
||||
}
|
||||
|
||||
/// Test Euler characteristic: chi = dim(H^0) - dim(H^1)
|
||||
#[test]
|
||||
fn test_euler_characteristic() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
// Simple path graph
|
||||
let section = vec![1.0];
|
||||
for i in 0..5 {
|
||||
graph.add_node(SheafNode::new(i, &format!("N{}", i), section.clone()));
|
||||
}
|
||||
for i in 0..4 {
|
||||
graph.add_edge(SheafEdge::identity(i, i + 1, 1)).unwrap();
|
||||
}
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
// Euler characteristic should be computed correctly
|
||||
let computed_chi = result.h0_dim as i64 - result.h1_dim as i64;
|
||||
assert_eq!(computed_chi, result.euler_characteristic);
|
||||
}
|
||||
|
||||
/// Test cohomology with scalar sections
|
||||
#[test]
|
||||
fn test_scalar_cohomology() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
// Simple graph with scalar (1D) sections
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![2.0]));
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 1)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
// Inconsistent scalars
|
||||
assert!(!result.is_consistent);
|
||||
assert_relative_eq!(result.consistency_energy, 1.0, epsilon = 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// OBSTRUCTION DETECTION TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod obstruction_detection_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test obstruction detection on known inconsistent graph
|
||||
#[test]
|
||||
fn test_detect_single_obstruction() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
graph.add_node(SheafNode::new(0, "Source", vec![1.0, 2.0, 3.0]));
|
||||
graph.add_node(SheafNode::new(1, "Target", vec![4.0, 5.0, 6.0]));
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 3)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let obstructions = engine.detect_obstructions(&graph).unwrap();
|
||||
|
||||
assert_eq!(obstructions.len(), 1);
|
||||
let obs = &obstructions[0];
|
||||
assert_eq!(obs.source_node, 0);
|
||||
assert_eq!(obs.target_node, 1);
|
||||
|
||||
// Expected obstruction vector: [1-4, 2-5, 3-6] = [-3, -3, -3]
|
||||
assert_relative_eq!(obs.obstruction_vector[0], -3.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(obs.obstruction_vector[1], -3.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(obs.obstruction_vector[2], -3.0, epsilon = 1e-10);
|
||||
|
||||
// Magnitude should be sqrt(27) = 3*sqrt(3)
|
||||
let expected_magnitude = (27.0_f64).sqrt();
|
||||
assert_relative_eq!(obs.magnitude, expected_magnitude, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test obstruction detection on fully consistent graph
|
||||
#[test]
|
||||
fn test_no_obstructions_when_consistent() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
let section = vec![1.0, 2.0];
|
||||
graph.add_node(SheafNode::new(0, "A", section.clone()));
|
||||
graph.add_node(SheafNode::new(1, "B", section.clone()));
|
||||
graph.add_node(SheafNode::new(2, "C", section.clone()));
|
||||
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 2)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(1, 2, 2)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let obstructions = engine.detect_obstructions(&graph).unwrap();
|
||||
|
||||
assert!(obstructions.is_empty());
|
||||
}
|
||||
|
||||
/// Test obstruction ordering by magnitude
|
||||
#[test]
|
||||
fn test_obstructions_ordered_by_magnitude() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
graph.add_node(SheafNode::new(0, "A", vec![0.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![1.0])); // Small diff
|
||||
graph.add_node(SheafNode::new(2, "C", vec![10.0])); // Large diff
|
||||
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 1)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(0, 2, 1)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let obstructions = engine.detect_obstructions(&graph).unwrap();
|
||||
|
||||
assert_eq!(obstructions.len(), 2);
|
||||
// Should be sorted by magnitude (descending)
|
||||
assert!(obstructions[0].magnitude >= obstructions[1].magnitude);
|
||||
}
|
||||
|
||||
/// Test obstruction detection with weighted nodes
|
||||
#[test]
|
||||
fn test_obstructions_with_weights() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
let node1 = SheafNode::new(0, "HighWeight", vec![1.0]).with_weight(10.0);
|
||||
let node2 = SheafNode::new(1, "LowWeight", vec![2.0]).with_weight(0.1);
|
||||
|
||||
graph.add_node(node1);
|
||||
graph.add_node(node2);
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 1)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let obstructions = engine.detect_obstructions(&graph).unwrap();
|
||||
|
||||
assert_eq!(obstructions.len(), 1);
|
||||
assert_relative_eq!(obstructions[0].magnitude, 1.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test obstruction localization
|
||||
#[test]
|
||||
fn test_obstruction_localization() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
// Create a longer path with obstruction in middle
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![1.0]));
|
||||
graph.add_node(SheafNode::new(2, "C", vec![5.0])); // Jump here
|
||||
graph.add_node(SheafNode::new(3, "D", vec![5.0]));
|
||||
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 1)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(1, 2, 1)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(2, 3, 1)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let obstructions = engine.detect_obstructions(&graph).unwrap();
|
||||
|
||||
// Only edge 1->2 should have obstruction
|
||||
assert_eq!(obstructions.len(), 1);
|
||||
assert_eq!(obstructions[0].source_node, 1);
|
||||
assert_eq!(obstructions[0].target_node, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GLOBAL SECTIONS AND REPAIR TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod global_sections_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test computation of global sections
|
||||
#[test]
|
||||
fn test_compute_global_sections() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
let section = vec![1.0, 2.0, 3.0];
|
||||
graph.add_node(SheafNode::new(0, "A", section.clone()));
|
||||
graph.add_node(SheafNode::new(1, "B", section.clone()));
|
||||
graph.add_node(SheafNode::new(2, "C", section.clone()));
|
||||
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 3)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(1, 2, 3)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let global_sections = engine.compute_global_sections(&graph).unwrap();
|
||||
|
||||
assert!(!global_sections.is_empty());
|
||||
// Should approximate the common section
|
||||
let gs = &global_sections[0];
|
||||
assert_eq!(gs.len(), 3);
|
||||
}
|
||||
|
||||
/// Test section repair
|
||||
#[test]
|
||||
fn test_repair_sections() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
// Slightly inconsistent sections
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1.0, 2.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![1.1, 2.1]));
|
||||
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 2)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let initial_energy = engine.compute_cohomology(&graph).unwrap().consistency_energy;
|
||||
|
||||
// Repair should reduce energy
|
||||
let _adjustment = engine.repair_sections(&mut graph).unwrap();
|
||||
let final_energy = engine.compute_cohomology(&graph).unwrap().consistency_energy;
|
||||
|
||||
assert!(final_energy <= initial_energy);
|
||||
}
|
||||
|
||||
/// Test repair convergence
|
||||
#[test]
|
||||
fn test_repair_convergence() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
// Create a cycle with small inconsistency
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![1.1]));
|
||||
graph.add_node(SheafNode::new(2, "C", vec![0.9]));
|
||||
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 1)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(1, 2, 1)).unwrap();
|
||||
graph.add_edge(SheafEdge::identity(2, 0, 1)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::with_tolerance(1e-8);
|
||||
|
||||
// Multiple repair iterations should converge
|
||||
for _ in 0..5 {
|
||||
engine.repair_sections(&mut graph).unwrap();
|
||||
}
|
||||
|
||||
let final_result = engine.compute_cohomology(&graph).unwrap();
|
||||
// Should have reduced energy significantly
|
||||
assert!(final_result.consistency_energy < 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BELIEF GRAPH BUILDER TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod belief_graph_builder_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test building graph from beliefs
|
||||
#[test]
|
||||
fn test_build_from_beliefs() {
|
||||
let builder = BeliefGraphBuilder::new(3);
|
||||
|
||||
let beliefs = vec![
|
||||
("Belief1".to_string(), vec![1.0, 0.0, 0.0]),
|
||||
("Belief2".to_string(), vec![0.0, 1.0, 0.0]),
|
||||
("Belief3".to_string(), vec![0.0, 0.0, 1.0]),
|
||||
];
|
||||
|
||||
let connections = vec![(0, 1), (1, 2)];
|
||||
|
||||
let graph = builder.build_from_beliefs(&beliefs, &connections).unwrap();
|
||||
|
||||
assert_eq!(graph.node_count(), 3);
|
||||
assert_eq!(graph.edge_count(), 2);
|
||||
}
|
||||
|
||||
/// Test builder with mixed dimensions
|
||||
#[test]
|
||||
fn test_builder_mixed_dimensions() {
|
||||
let builder = BeliefGraphBuilder::new(4);
|
||||
|
||||
let beliefs = vec![
|
||||
("Low".to_string(), vec![1.0, 2.0]),
|
||||
("High".to_string(), vec![1.0, 2.0, 3.0, 4.0]),
|
||||
];
|
||||
|
||||
let connections = vec![(0, 1)];
|
||||
|
||||
let graph = builder.build_from_beliefs(&beliefs, &connections).unwrap();
|
||||
let engine = CohomologyEngine::new();
|
||||
|
||||
// Should handle dimension mismatch gracefully
|
||||
let _result = engine.compute_cohomology(&graph).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EDGE CASES AND ERROR HANDLING
|
||||
// =============================================================================
|
||||
|
||||
mod edge_cases_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test empty graph
|
||||
#[test]
|
||||
fn test_empty_graph_cohomology() {
|
||||
let graph = SheafGraph::new();
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
assert_eq!(result.h0_dim, 0);
|
||||
assert_eq!(result.h1_dim, 0);
|
||||
assert!(result.is_consistent);
|
||||
}
|
||||
|
||||
/// Test single node graph
|
||||
#[test]
|
||||
fn test_single_node_graph() {
|
||||
let mut graph = SheafGraph::new();
|
||||
graph.add_node(SheafNode::new(0, "Single", vec![1.0, 2.0, 3.0]));
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
assert!(result.is_consistent);
|
||||
assert_eq!(result.consistency_energy, 0.0);
|
||||
}
|
||||
|
||||
/// Test graph with zero-dimensional sections
|
||||
#[test]
|
||||
fn test_zero_dimensional_sections() {
|
||||
let mut graph = SheafGraph::new();
|
||||
graph.add_node(SheafNode::new(0, "Empty", vec![]));
|
||||
graph.add_node(SheafNode::new(1, "Empty2", vec![]));
|
||||
|
||||
// This should still work, just with trivial cohomology
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
assert!(result.is_consistent);
|
||||
}
|
||||
|
||||
/// Test invalid node reference in edge
|
||||
#[test]
|
||||
fn test_invalid_node_reference() {
|
||||
let mut graph = SheafGraph::new();
|
||||
graph.add_node(SheafNode::new(0, "Only", vec![1.0]));
|
||||
|
||||
// Edge to non-existent node
|
||||
let result = graph.add_edge(SheafEdge::identity(0, 99, 1));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Test large graph performance
|
||||
#[test]
|
||||
fn test_large_graph_performance() {
|
||||
let mut graph = SheafGraph::new();
|
||||
let n = 100;
|
||||
|
||||
// Create a path graph with n nodes
|
||||
for i in 0..n {
|
||||
graph.add_node(SheafNode::new(i, &format!("N{}", i), vec![i as f64]));
|
||||
}
|
||||
for i in 0..(n - 1) {
|
||||
graph.add_edge(SheafEdge::identity(i, i + 1, 1)).unwrap();
|
||||
}
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let start = std::time::Instant::now();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Should complete in reasonable time
|
||||
assert!(duration.as_secs() < 5);
|
||||
assert!(result.h0_dim > 0 || result.h1_dim > 0);
|
||||
}
|
||||
|
||||
/// Test numerical stability with very small values
|
||||
#[test]
|
||||
fn test_numerical_stability_small_values() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1e-15, 1e-15]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![1e-15, 1e-15]));
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 2)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::with_tolerance(1e-20);
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
// Should be consistent despite small values
|
||||
assert!(result.is_consistent);
|
||||
}
|
||||
|
||||
/// Test numerical stability with large values
|
||||
#[test]
|
||||
fn test_numerical_stability_large_values() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1e15, 1e15]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![1e15, 1e15]));
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 2)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
assert!(result.is_consistent);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROPERTY-BASED TESTS (using proptest)
|
||||
// =============================================================================
|
||||
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
|
||||
proptest! {
|
||||
/// Property: Consistent sections always have zero energy
|
||||
#[test]
|
||||
fn prop_consistent_sections_zero_energy(
|
||||
values in proptest::collection::vec(-100.0..100.0f64, 1..10)
|
||||
) {
|
||||
let mut graph = SheafGraph::new();
|
||||
let dim = values.len();
|
||||
|
||||
graph.add_node(SheafNode::new(0, "A", values.clone()));
|
||||
graph.add_node(SheafNode::new(1, "B", values.clone()));
|
||||
graph.add_edge(SheafEdge::identity(0, 1, dim)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
prop_assert!(result.is_consistent);
|
||||
prop_assert!(result.consistency_energy < 1e-10);
|
||||
}
|
||||
|
||||
/// Property: Energy is always non-negative
|
||||
#[test]
|
||||
fn prop_energy_non_negative(
|
||||
v1 in proptest::collection::vec(-100.0..100.0f64, 1..5),
|
||||
v2 in proptest::collection::vec(-100.0..100.0f64, 1..5)
|
||||
) {
|
||||
if v1.len() != v2.len() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut graph = SheafGraph::new();
|
||||
graph.add_node(SheafNode::new(0, "A", v1.clone()));
|
||||
graph.add_node(SheafNode::new(1, "B", v2.clone()));
|
||||
graph.add_edge(SheafEdge::identity(0, 1, v1.len())).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
prop_assert!(result.consistency_energy >= 0.0);
|
||||
}
|
||||
|
||||
/// Property: Obstruction magnitudes match energy contribution
|
||||
#[test]
|
||||
fn prop_obstruction_magnitude_matches_energy(
|
||||
diff in proptest::collection::vec(-10.0..10.0f64, 1..5)
|
||||
) {
|
||||
let mut graph = SheafGraph::new();
|
||||
let base: Vec<f64> = vec![0.0; diff.len()];
|
||||
let target: Vec<f64> = diff.clone();
|
||||
|
||||
graph.add_node(SheafNode::new(0, "A", base));
|
||||
graph.add_node(SheafNode::new(1, "B", target));
|
||||
graph.add_edge(SheafEdge::identity(0, 1, diff.len())).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let obstructions = engine.detect_obstructions(&graph).unwrap();
|
||||
|
||||
if !obstructions.is_empty() {
|
||||
let expected_magnitude: f64 = diff.iter().map(|x| x * x).sum::<f64>().sqrt();
|
||||
prop_assert!((obstructions[0].magnitude - expected_magnitude).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
/// Property: Adding consistent edge doesn't change consistency
|
||||
#[test]
|
||||
fn prop_consistent_edge_preserves_consistency(
|
||||
section in proptest::collection::vec(-100.0..100.0f64, 1..5)
|
||||
) {
|
||||
let mut graph = SheafGraph::new();
|
||||
graph.add_node(SheafNode::new(0, "A", section.clone()));
|
||||
graph.add_node(SheafNode::new(1, "B", section.clone()));
|
||||
graph.add_node(SheafNode::new(2, "C", section.clone()));
|
||||
|
||||
graph.add_edge(SheafEdge::identity(0, 1, section.len())).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let before = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
graph.add_edge(SheafEdge::identity(1, 2, section.len())).unwrap();
|
||||
let after = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
prop_assert_eq!(before.is_consistent, after.is_consistent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SHEAF NEURAL NETWORK TESTS (if included in cohomology module)
|
||||
// =============================================================================
|
||||
|
||||
mod sheaf_neural_network_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test that Laplacian energy is non-negative
|
||||
#[test]
|
||||
fn test_laplacian_energy_non_negative() {
|
||||
let mut graph = SheafGraph::new();
|
||||
|
||||
graph.add_node(SheafNode::new(0, "A", vec![1.0, -1.0]));
|
||||
graph.add_node(SheafNode::new(1, "B", vec![-1.0, 1.0]));
|
||||
graph.add_edge(SheafEdge::identity(0, 1, 2)).unwrap();
|
||||
|
||||
let engine = CohomologyEngine::new();
|
||||
let result = engine.compute_cohomology(&graph).unwrap();
|
||||
|
||||
assert!(result.consistency_energy >= 0.0);
|
||||
}
|
||||
}
|
||||
901
vendor/ruvector/examples/prime-radiant/tests/hott_tests.rs
vendored
Normal file
901
vendor/ruvector/examples/prime-radiant/tests/hott_tests.rs
vendored
Normal file
@@ -0,0 +1,901 @@
|
||||
//! Comprehensive tests for Homotopy Type Theory (HoTT) Module
|
||||
//!
|
||||
//! This test suite verifies HoTT constructs including:
|
||||
//! - Type checking and inference
|
||||
//! - Path composition and inversion
|
||||
//! - Transport along paths
|
||||
//! - Univalence axiom (equivalence = identity)
|
||||
|
||||
use prime_radiant::hott::{
|
||||
Type, Term, Path, TypeChecker, TypeContext,
|
||||
Equivalence, Transport, Univalence,
|
||||
PathComposition, PathInversion, PathConcatenation,
|
||||
HigherInductiveType, Circle, Sphere, Torus,
|
||||
HomotopyLevel, is_contractible, is_proposition, is_set,
|
||||
FunctionExtensionality, funext,
|
||||
HottError,
|
||||
};
|
||||
use proptest::prelude::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
// =============================================================================
|
||||
// TYPE CHECKING TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod type_checking_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test type checking for base types
|
||||
#[test]
|
||||
fn test_base_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Natural numbers type
|
||||
let nat = Type::Nat;
|
||||
assert!(ctx.is_well_formed(&nat));
|
||||
|
||||
// Boolean type
|
||||
let bool_ty = Type::Bool;
|
||||
assert!(ctx.is_well_formed(&bool_ty));
|
||||
|
||||
// Unit type
|
||||
let unit = Type::Unit;
|
||||
assert!(ctx.is_well_formed(&unit));
|
||||
}
|
||||
|
||||
/// Test type checking for function types
|
||||
#[test]
|
||||
fn test_function_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Nat -> Bool
|
||||
let func_type = Type::Pi {
|
||||
param: Box::new(Type::Nat),
|
||||
body: Box::new(Type::Bool),
|
||||
};
|
||||
|
||||
assert!(ctx.is_well_formed(&func_type));
|
||||
}
|
||||
|
||||
/// Test type checking for dependent types
|
||||
#[test]
|
||||
fn test_dependent_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Dependent product type: (x: A) -> B(x)
|
||||
let dep_prod = Type::Pi {
|
||||
param: Box::new(Type::Nat),
|
||||
body: Box::new(Type::Family {
|
||||
base: Box::new(Type::Nat),
|
||||
fiber: Box::new(|_n| Type::Bool),
|
||||
}),
|
||||
};
|
||||
|
||||
assert!(ctx.is_well_formed(&dep_prod));
|
||||
}
|
||||
|
||||
/// Test type checking for sigma types
|
||||
#[test]
|
||||
fn test_sigma_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Sigma type: (x: A) * B(x)
|
||||
let sigma = Type::Sigma {
|
||||
first: Box::new(Type::Nat),
|
||||
second: Box::new(Type::Bool),
|
||||
};
|
||||
|
||||
assert!(ctx.is_well_formed(&sigma));
|
||||
}
|
||||
|
||||
/// Test type checking for identity types
|
||||
#[test]
|
||||
fn test_identity_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Identity type: a =_A b
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let id_type = Type::Identity {
|
||||
base_type: Box::new(Type::Nat),
|
||||
left: Box::new(a),
|
||||
right: Box::new(b),
|
||||
};
|
||||
|
||||
assert!(ctx.is_well_formed(&id_type));
|
||||
}
|
||||
|
||||
/// Test type inference
|
||||
#[test]
|
||||
fn test_type_inference() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
let zero = Term::zero();
|
||||
let inferred = ctx.infer_type(&zero).unwrap();
|
||||
assert_eq!(inferred, Type::Nat);
|
||||
|
||||
let true_val = Term::true_val();
|
||||
let inferred = ctx.infer_type(&true_val).unwrap();
|
||||
assert_eq!(inferred, Type::Bool);
|
||||
}
|
||||
|
||||
/// Test type checking with variable bindings
|
||||
#[test]
|
||||
fn test_variable_bindings() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// Add variable x: Nat to context
|
||||
ctx.add_variable("x", Type::Nat);
|
||||
|
||||
let var_x = Term::variable("x");
|
||||
let inferred = ctx.infer_type(&var_x).unwrap();
|
||||
assert_eq!(inferred, Type::Nat);
|
||||
}
|
||||
|
||||
/// Test lambda type checking
|
||||
#[test]
|
||||
fn test_lambda_type_checking() {
|
||||
let mut ctx = TypeContext::new();
|
||||
|
||||
// lambda x: Nat. x + 1
|
||||
let lambda = Term::Lambda {
|
||||
param: "x".to_string(),
|
||||
param_type: Box::new(Type::Nat),
|
||||
body: Box::new(Term::succ(Term::variable("x"))),
|
||||
};
|
||||
|
||||
let inferred = ctx.infer_type(&lambda).unwrap();
|
||||
|
||||
match inferred {
|
||||
Type::Pi { param, body } => {
|
||||
assert_eq!(*param, Type::Nat);
|
||||
assert_eq!(*body, Type::Nat);
|
||||
}
|
||||
_ => panic!("Expected Pi type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PATH COMPOSITION TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod path_composition_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test reflexivity path: refl_a : a = a
|
||||
#[test]
|
||||
fn test_reflexivity_path() {
|
||||
let a = Term::zero();
|
||||
let refl = Path::refl(&a);
|
||||
|
||||
assert_eq!(refl.start(), &a);
|
||||
assert_eq!(refl.end(), &a);
|
||||
assert!(refl.is_reflexivity());
|
||||
}
|
||||
|
||||
/// Test path concatenation: p . q : a = c for p: a = b, q: b = c
|
||||
#[test]
|
||||
fn test_path_concatenation() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
let c = Term::succ(Term::succ(Term::zero()));
|
||||
|
||||
// p: a = b (hypothetical)
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
|
||||
// q: b = c (hypothetical)
|
||||
let q = Path::hypothesis(&b, &c, "q");
|
||||
|
||||
// p . q : a = c
|
||||
let composed = p.concat(&q).unwrap();
|
||||
|
||||
assert_eq!(composed.start(), &a);
|
||||
assert_eq!(composed.end(), &c);
|
||||
}
|
||||
|
||||
/// Test path concatenation fails for non-matching endpoints
|
||||
#[test]
|
||||
fn test_path_concat_mismatch() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
let c = Term::succ(Term::succ(Term::zero()));
|
||||
let d = Term::succ(Term::succ(Term::succ(Term::zero())));
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p"); // a = b
|
||||
let q = Path::hypothesis(&c, &d, "q"); // c = d, not b = something
|
||||
|
||||
let result = p.concat(&q);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Test path inversion: p^(-1) : b = a for p : a = b
|
||||
#[test]
|
||||
fn test_path_inversion() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let p_inv = p.inverse();
|
||||
|
||||
assert_eq!(p_inv.start(), &b);
|
||||
assert_eq!(p_inv.end(), &a);
|
||||
}
|
||||
|
||||
/// Test double inversion: (p^(-1))^(-1) = p
|
||||
#[test]
|
||||
fn test_double_inversion() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let p_inv_inv = p.inverse().inverse();
|
||||
|
||||
assert_eq!(p_inv_inv.start(), p.start());
|
||||
assert_eq!(p_inv_inv.end(), p.end());
|
||||
}
|
||||
|
||||
/// Test associativity: (p . q) . r = p . (q . r)
|
||||
#[test]
|
||||
fn test_path_associativity() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
let c = Term::succ(Term::succ(Term::zero()));
|
||||
let d = Term::succ(Term::succ(Term::succ(Term::zero())));
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let q = Path::hypothesis(&b, &c, "q");
|
||||
let r = Path::hypothesis(&c, &d, "r");
|
||||
|
||||
// (p . q) . r
|
||||
let left = p.concat(&q).unwrap().concat(&r).unwrap();
|
||||
|
||||
// p . (q . r)
|
||||
let right = p.concat(&q.concat(&r).unwrap()).unwrap();
|
||||
|
||||
// Both should have same endpoints
|
||||
assert_eq!(left.start(), right.start());
|
||||
assert_eq!(left.end(), right.end());
|
||||
|
||||
// And there should be a path between them (associator)
|
||||
assert!(Path::path_between(&left, &right).is_some());
|
||||
}
|
||||
|
||||
/// Test left unit law: refl_a . p = p
|
||||
#[test]
|
||||
fn test_left_unit_law() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let refl_a = Path::refl(&a);
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
|
||||
let left_unit = refl_a.concat(&p).unwrap();
|
||||
|
||||
// Should be propositionally equal to p
|
||||
assert_eq!(left_unit.start(), p.start());
|
||||
assert_eq!(left_unit.end(), p.end());
|
||||
}
|
||||
|
||||
/// Test right unit law: p . refl_b = p
|
||||
#[test]
|
||||
fn test_right_unit_law() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let refl_b = Path::refl(&b);
|
||||
|
||||
let right_unit = p.concat(&refl_b).unwrap();
|
||||
|
||||
assert_eq!(right_unit.start(), p.start());
|
||||
assert_eq!(right_unit.end(), p.end());
|
||||
}
|
||||
|
||||
/// Test inverse law: p . p^(-1) = refl_a
|
||||
#[test]
|
||||
fn test_inverse_law() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let p_inv = p.inverse();
|
||||
|
||||
let composed = p.concat(&p_inv).unwrap();
|
||||
|
||||
// Should equal refl_a (propositionally)
|
||||
assert_eq!(composed.start(), &a);
|
||||
assert_eq!(composed.end(), &a);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TRANSPORT TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod transport_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test transport along reflexivity path is identity
|
||||
#[test]
|
||||
fn test_transport_refl_is_identity() {
|
||||
let a = Term::zero();
|
||||
let refl = Path::refl(&a);
|
||||
|
||||
// Type family B(x) = Nat for simplicity
|
||||
let family = Type::Family {
|
||||
base: Box::new(Type::Nat),
|
||||
fiber: Box::new(|_| Type::Nat),
|
||||
};
|
||||
|
||||
let b_a = Term::succ(Term::zero()); // Some term in B(a)
|
||||
|
||||
let transported = Transport::transport(&refl, &family, &b_a).unwrap();
|
||||
|
||||
// transport(refl_a, b) = b
|
||||
assert_eq!(transported, b_a);
|
||||
}
|
||||
|
||||
/// Test transport composition: transport(p.q) = transport(q) . transport(p)
|
||||
#[test]
|
||||
fn test_transport_composition() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
let c = Term::succ(Term::succ(Term::zero()));
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let q = Path::hypothesis(&b, &c, "q");
|
||||
let pq = p.concat(&q).unwrap();
|
||||
|
||||
let family = Type::Family {
|
||||
base: Box::new(Type::Nat),
|
||||
fiber: Box::new(|_| Type::Nat),
|
||||
};
|
||||
|
||||
let term_a = Term::succ(Term::succ(Term::succ(Term::zero())));
|
||||
|
||||
// transport(p.q, x)
|
||||
let direct = Transport::transport(&pq, &family, &term_a).unwrap();
|
||||
|
||||
// transport(q, transport(p, x))
|
||||
let p_transported = Transport::transport(&p, &family, &term_a).unwrap();
|
||||
let composed = Transport::transport(&q, &family, &p_transported).unwrap();
|
||||
|
||||
// Should be propositionally equal
|
||||
assert!(Term::propositionally_equal(&direct, &composed));
|
||||
}
|
||||
|
||||
/// Test dependent transport (transport in dependent types)
|
||||
#[test]
|
||||
fn test_dependent_transport() {
|
||||
let ctx = TypeContext::new();
|
||||
|
||||
// Type family indexed by Nat
|
||||
let family = Type::Family {
|
||||
base: Box::new(Type::Nat),
|
||||
fiber: Box::new(|n| Type::Vec {
|
||||
element_type: Box::new(Type::Nat),
|
||||
length: n,
|
||||
}),
|
||||
};
|
||||
|
||||
// Path from 0 to 1
|
||||
let p = Path::hypothesis(&Term::zero(), &Term::succ(Term::zero()), "p");
|
||||
|
||||
// Empty vector at type Vec(Nat, 0)
|
||||
let empty_vec = Term::empty_vec();
|
||||
|
||||
// Transport should fail or produce Vec(Nat, 1)
|
||||
let result = Transport::transport(&p, &family, &empty_vec);
|
||||
|
||||
// May require coercion witness
|
||||
assert!(result.is_ok() || result.is_err());
|
||||
}
|
||||
|
||||
/// Test path lifting (apd)
|
||||
#[test]
|
||||
fn test_apd() {
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let family = Type::Family {
|
||||
base: Box::new(Type::Nat),
|
||||
fiber: Box::new(|_| Type::Nat),
|
||||
};
|
||||
|
||||
// Function f: (x: Nat) -> B(x)
|
||||
let f = Term::Lambda {
|
||||
param: "x".to_string(),
|
||||
param_type: Box::new(Type::Nat),
|
||||
body: Box::new(Term::succ(Term::variable("x"))),
|
||||
};
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
|
||||
// apd f p : transport(p, f(a)) = f(b)
|
||||
let apd_path = Transport::apd(&f, &p, &family).unwrap();
|
||||
|
||||
// Check endpoints
|
||||
let f_a = Term::succ(a.clone());
|
||||
let f_b = Term::succ(b.clone());
|
||||
|
||||
let transported_f_a = Transport::transport(&p, &family, &f_a).unwrap();
|
||||
|
||||
assert_eq!(apd_path.start(), &transported_f_a);
|
||||
assert_eq!(apd_path.end(), &f_b);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UNIVALENCE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod univalence_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test that equivalence can be converted to path (ua)
|
||||
#[test]
|
||||
fn test_ua_from_equivalence() {
|
||||
// Equivalence between Bool and Bool (identity)
|
||||
let bool_equiv = Equivalence::identity(Type::Bool);
|
||||
|
||||
// ua should produce a path Bool = Bool
|
||||
let path = Univalence::ua(&bool_equiv).unwrap();
|
||||
|
||||
assert_eq!(path.start_type(), &Type::Bool);
|
||||
assert_eq!(path.end_type(), &Type::Bool);
|
||||
}
|
||||
|
||||
/// Test that path can be converted to equivalence (ua^-1)
|
||||
#[test]
|
||||
fn test_ua_inverse() {
|
||||
// Reflexivity path on type
|
||||
let refl_nat = Path::type_refl(&Type::Nat);
|
||||
|
||||
// ua^-1 should produce equivalence Nat ~ Nat
|
||||
let equiv = Univalence::ua_inverse(&refl_nat).unwrap();
|
||||
|
||||
assert!(equiv.is_valid_equivalence());
|
||||
assert_eq!(equiv.domain(), &Type::Nat);
|
||||
assert_eq!(equiv.codomain(), &Type::Nat);
|
||||
}
|
||||
|
||||
/// Test round-trip: ua(ua^-1(p)) = p
|
||||
#[test]
|
||||
fn test_univalence_round_trip_path() {
|
||||
let p = Path::type_refl(&Type::Bool);
|
||||
|
||||
let equiv = Univalence::ua_inverse(&p).unwrap();
|
||||
let recovered = Univalence::ua(&equiv).unwrap();
|
||||
|
||||
// Should be propositionally equal
|
||||
assert_eq!(recovered.start_type(), p.start_type());
|
||||
assert_eq!(recovered.end_type(), p.end_type());
|
||||
}
|
||||
|
||||
/// Test round-trip: ua^-1(ua(e)) = e
|
||||
#[test]
|
||||
fn test_univalence_round_trip_equiv() {
|
||||
let equiv = Equivalence::identity(Type::Nat);
|
||||
|
||||
let path = Univalence::ua(&equiv).unwrap();
|
||||
let recovered = Univalence::ua_inverse(&path).unwrap();
|
||||
|
||||
// Forward maps should be equal
|
||||
assert!(Equivalence::equal(&recovered, &equiv));
|
||||
}
|
||||
|
||||
/// Test transport along ua(e) is the equivalence
|
||||
#[test]
|
||||
fn test_transport_along_ua() {
|
||||
// Create non-trivial equivalence (e.g., negation on Bool)
|
||||
let neg_equiv = Equivalence::bool_negation();
|
||||
|
||||
let path = Univalence::ua(&neg_equiv).unwrap();
|
||||
|
||||
// Type family that uses the base type directly
|
||||
let family = Type::Family {
|
||||
base: Box::new(Type::Universe(0)),
|
||||
fiber: Box::new(|ty| ty.clone()),
|
||||
};
|
||||
|
||||
let true_val = Term::true_val();
|
||||
|
||||
// transport(ua(neg), true) should equal neg(true) = false
|
||||
let transported = Transport::transport(&path, &family, &true_val).unwrap();
|
||||
let neg_true = neg_equiv.apply(&true_val).unwrap();
|
||||
|
||||
assert!(Term::propositionally_equal(&transported, &neg_true));
|
||||
}
|
||||
|
||||
/// Test univalence with type isomorphism
|
||||
#[test]
|
||||
fn test_type_isomorphism_gives_equality() {
|
||||
// Unit + Unit is isomorphic to Bool
|
||||
let sum_type = Type::Sum {
|
||||
left: Box::new(Type::Unit),
|
||||
right: Box::new(Type::Unit),
|
||||
};
|
||||
|
||||
// Construct isomorphism
|
||||
let iso = Equivalence::sum_unit_to_bool();
|
||||
|
||||
assert!(iso.is_valid_equivalence());
|
||||
|
||||
// By univalence, types are equal
|
||||
let path = Univalence::ua(&iso).unwrap();
|
||||
|
||||
assert_eq!(*path.start_type(), sum_type);
|
||||
assert_eq!(*path.end_type(), Type::Bool);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HIGHER INDUCTIVE TYPE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod hit_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test circle type S^1
|
||||
#[test]
|
||||
fn test_circle_type() {
|
||||
let circle = Circle::new();
|
||||
|
||||
// Circle has base point
|
||||
let base = circle.base_point();
|
||||
assert!(base.has_type(&Type::Circle));
|
||||
|
||||
// Circle has loop: base = base
|
||||
let loop_path = circle.loop_path();
|
||||
assert_eq!(loop_path.start(), &base);
|
||||
assert_eq!(loop_path.end(), &base);
|
||||
}
|
||||
|
||||
/// Test circle recursion principle
|
||||
#[test]
|
||||
fn test_circle_recursion() {
|
||||
let circle = Circle::new();
|
||||
|
||||
// To map S^1 -> A, need:
|
||||
// - a: A (image of base)
|
||||
// - p: a = a (image of loop)
|
||||
|
||||
let target_type = Type::Nat;
|
||||
let a = Term::zero();
|
||||
let p = Path::refl(&a); // Use refl for simplicity
|
||||
|
||||
let rec = circle.recursion(&target_type, &a, &p).unwrap();
|
||||
|
||||
// rec(base) = a
|
||||
let base_image = rec.apply(&circle.base_point()).unwrap();
|
||||
assert_eq!(base_image, a);
|
||||
}
|
||||
|
||||
/// Test sphere type S^2
|
||||
#[test]
|
||||
fn test_sphere_type() {
|
||||
let sphere = Sphere::new(2);
|
||||
|
||||
let base = sphere.base_point();
|
||||
assert!(base.has_type(&Type::Sphere(2)));
|
||||
|
||||
// S^2 has refl-refl as 2-path
|
||||
let surf = sphere.surface();
|
||||
assert!(surf.is_2_path());
|
||||
}
|
||||
|
||||
/// Test torus type
|
||||
#[test]
|
||||
fn test_torus_type() {
|
||||
let torus = Torus::new();
|
||||
|
||||
let base = torus.base_point();
|
||||
|
||||
// Torus has two loops
|
||||
let p = torus.meridian();
|
||||
let q = torus.longitude();
|
||||
|
||||
// And a square: p . q = q . p
|
||||
let surface = torus.surface();
|
||||
|
||||
// surface : p . q = q . p
|
||||
let pq = p.concat(&q).unwrap();
|
||||
let qp = q.concat(&p).unwrap();
|
||||
|
||||
assert_eq!(surface.start(), &pq);
|
||||
assert_eq!(surface.end(), &qp);
|
||||
}
|
||||
|
||||
/// Test pushout as HIT
|
||||
#[test]
|
||||
fn test_pushout_hit() {
|
||||
// Pushout of A <- C -> B
|
||||
let a_type = Type::Nat;
|
||||
let b_type = Type::Bool;
|
||||
let c_type = Type::Unit;
|
||||
|
||||
let f = Term::Lambda {
|
||||
param: "c".to_string(),
|
||||
param_type: Box::new(c_type.clone()),
|
||||
body: Box::new(Term::zero()),
|
||||
};
|
||||
|
||||
let g = Term::Lambda {
|
||||
param: "c".to_string(),
|
||||
param_type: Box::new(c_type.clone()),
|
||||
body: Box::new(Term::true_val()),
|
||||
};
|
||||
|
||||
let pushout = HigherInductiveType::pushout(&a_type, &b_type, &c_type, &f, &g);
|
||||
|
||||
// Has injections from A and B
|
||||
let inl = pushout.left_injection();
|
||||
let inr = pushout.right_injection();
|
||||
|
||||
// For each c: C, path glue(c): inl(f(c)) = inr(g(c))
|
||||
let unit = Term::unit();
|
||||
let glue_path = pushout.glue(&unit);
|
||||
|
||||
let inl_fc = inl.apply(&f.apply(&unit).unwrap()).unwrap();
|
||||
let inr_gc = inr.apply(&g.apply(&unit).unwrap()).unwrap();
|
||||
|
||||
assert_eq!(glue_path.start(), &inl_fc);
|
||||
assert_eq!(glue_path.end(), &inr_gc);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HOMOTOPY LEVEL TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod homotopy_level_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test contractibility (h-level -2)
|
||||
#[test]
|
||||
fn test_contractible() {
|
||||
// Unit type is contractible
|
||||
assert!(is_contractible(&Type::Unit));
|
||||
|
||||
// Nat is not contractible
|
||||
assert!(!is_contractible(&Type::Nat));
|
||||
}
|
||||
|
||||
/// Test propositions (h-level -1)
|
||||
#[test]
|
||||
fn test_is_proposition() {
|
||||
// Empty type is a proposition (vacuously)
|
||||
assert!(is_proposition(&Type::Empty));
|
||||
|
||||
// Unit type is a proposition (all elements equal)
|
||||
assert!(is_proposition(&Type::Unit));
|
||||
|
||||
// Nat is not a proposition
|
||||
assert!(!is_proposition(&Type::Nat));
|
||||
}
|
||||
|
||||
/// Test sets (h-level 0)
|
||||
#[test]
|
||||
fn test_is_set() {
|
||||
// Nat is a set
|
||||
assert!(is_set(&Type::Nat));
|
||||
|
||||
// Bool is a set
|
||||
assert!(is_set(&Type::Bool));
|
||||
|
||||
// Universe is not a set (by univalence)
|
||||
assert!(!is_set(&Type::Universe(0)));
|
||||
}
|
||||
|
||||
/// Test h-level preservation under products
|
||||
#[test]
|
||||
fn test_hlevel_product() {
|
||||
// Product of sets is a set
|
||||
let nat_nat = Type::Product {
|
||||
left: Box::new(Type::Nat),
|
||||
right: Box::new(Type::Nat),
|
||||
};
|
||||
|
||||
assert!(is_set(&nat_nat));
|
||||
}
|
||||
|
||||
/// Test h-level of identity types
|
||||
#[test]
|
||||
fn test_identity_hlevel() {
|
||||
// For a set A, identity types a =_A b are propositions
|
||||
let a = Term::zero();
|
||||
let b = Term::succ(Term::zero());
|
||||
|
||||
let id_type = Type::Identity {
|
||||
base_type: Box::new(Type::Nat),
|
||||
left: Box::new(a),
|
||||
right: Box::new(b),
|
||||
};
|
||||
|
||||
assert!(is_proposition(&id_type));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FUNCTION EXTENSIONALITY TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod funext_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test function extensionality: (forall x, f(x) = g(x)) -> f = g
|
||||
#[test]
|
||||
fn test_function_extensionality() {
|
||||
let domain = Type::Nat;
|
||||
let codomain = Type::Nat;
|
||||
|
||||
let f = Term::Lambda {
|
||||
param: "x".to_string(),
|
||||
param_type: Box::new(domain.clone()),
|
||||
body: Box::new(Term::succ(Term::variable("x"))),
|
||||
};
|
||||
|
||||
let g = Term::Lambda {
|
||||
param: "y".to_string(),
|
||||
param_type: Box::new(domain.clone()),
|
||||
body: Box::new(Term::succ(Term::variable("y"))),
|
||||
};
|
||||
|
||||
// Pointwise equality witness (hypothetical)
|
||||
let h = Term::Lambda {
|
||||
param: "x".to_string(),
|
||||
param_type: Box::new(domain.clone()),
|
||||
body: Box::new(Path::refl(&Term::succ(Term::variable("x"))).to_term()),
|
||||
};
|
||||
|
||||
// Apply funext
|
||||
let path_f_g = funext(&f, &g, &h).unwrap();
|
||||
|
||||
assert_eq!(path_f_g.start(), &f);
|
||||
assert_eq!(path_f_g.end(), &g);
|
||||
}
|
||||
|
||||
/// Test funext inverse: f = g -> forall x, f(x) = g(x)
|
||||
#[test]
|
||||
fn test_funext_inverse() {
|
||||
let domain = Type::Bool;
|
||||
let codomain = Type::Nat;
|
||||
|
||||
let f = Term::Lambda {
|
||||
param: "b".to_string(),
|
||||
param_type: Box::new(domain.clone()),
|
||||
body: Box::new(Term::if_then_else(
|
||||
Term::variable("b"),
|
||||
Term::zero(),
|
||||
Term::succ(Term::zero()),
|
||||
)),
|
||||
};
|
||||
|
||||
let p = Path::refl(&f);
|
||||
|
||||
// Get pointwise equalities
|
||||
let pointwise = FunctionExtensionality::inverse(&p).unwrap();
|
||||
|
||||
// For each x: Bool, should have f(x) = f(x)
|
||||
let true_val = Term::true_val();
|
||||
let path_at_true = pointwise.at(&true_val).unwrap();
|
||||
|
||||
assert!(path_at_true.is_reflexivity());
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROPERTY-BASED TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
|
||||
proptest! {
|
||||
/// Property: refl . p = p for all paths
|
||||
#[test]
|
||||
fn prop_left_unit(
|
||||
start in 0..10i32,
|
||||
end in 0..10i32
|
||||
) {
|
||||
let a = Term::from_int(start);
|
||||
let b = Term::from_int(end);
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let refl = Path::refl(&a);
|
||||
|
||||
let composed = refl.concat(&p).unwrap();
|
||||
|
||||
prop_assert_eq!(composed.start(), p.start());
|
||||
prop_assert_eq!(composed.end(), p.end());
|
||||
}
|
||||
|
||||
/// Property: p . refl = p for all paths
|
||||
#[test]
|
||||
fn prop_right_unit(
|
||||
start in 0..10i32,
|
||||
end in 0..10i32
|
||||
) {
|
||||
let a = Term::from_int(start);
|
||||
let b = Term::from_int(end);
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let refl = Path::refl(&b);
|
||||
|
||||
let composed = p.concat(&refl).unwrap();
|
||||
|
||||
prop_assert_eq!(composed.start(), p.start());
|
||||
prop_assert_eq!(composed.end(), p.end());
|
||||
}
|
||||
|
||||
/// Property: (p^-1)^-1 = p
|
||||
#[test]
|
||||
fn prop_double_inverse(
|
||||
start in 0..10i32,
|
||||
end in 0..10i32
|
||||
) {
|
||||
let a = Term::from_int(start);
|
||||
let b = Term::from_int(end);
|
||||
|
||||
let p = Path::hypothesis(&a, &b, "p");
|
||||
let double_inv = p.inverse().inverse();
|
||||
|
||||
prop_assert_eq!(double_inv.start(), p.start());
|
||||
prop_assert_eq!(double_inv.end(), p.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EDGE CASE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod edge_case_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test empty context type checking
|
||||
#[test]
|
||||
fn test_empty_context() {
|
||||
let ctx = TypeContext::new();
|
||||
assert!(ctx.is_empty());
|
||||
}
|
||||
|
||||
/// Test universe levels
|
||||
#[test]
|
||||
fn test_universe_hierarchy() {
|
||||
let ctx = TypeContext::new();
|
||||
|
||||
let type_0 = Type::Universe(0); // Type of small types
|
||||
let type_1 = Type::Universe(1); // Type of large types
|
||||
|
||||
// Type_0 : Type_1
|
||||
assert!(ctx.inhabits(&type_0, &type_1));
|
||||
|
||||
// But not Type_1 : Type_0 (no type-in-type)
|
||||
assert!(!ctx.inhabits(&type_1, &type_0));
|
||||
}
|
||||
|
||||
/// Test type checking with free variables
|
||||
#[test]
|
||||
fn test_free_variable_error() {
|
||||
let ctx = TypeContext::new();
|
||||
|
||||
let free_var = Term::variable("undefined");
|
||||
|
||||
let result = ctx.infer_type(&free_var);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Test path between incompatible types
|
||||
#[test]
|
||||
fn test_heterogeneous_path_error() {
|
||||
let nat_term = Term::zero();
|
||||
let bool_term = Term::true_val();
|
||||
|
||||
// Cannot form path between different types directly
|
||||
let result = Path::try_new(&nat_term, &bool_term);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
568
vendor/ruvector/examples/prime-radiant/tests/integration_tests.rs
vendored
Normal file
568
vendor/ruvector/examples/prime-radiant/tests/integration_tests.rs
vendored
Normal file
@@ -0,0 +1,568 @@
|
||||
//! Integration tests for Prime-Radiant Advanced Math Modules
|
||||
//!
|
||||
//! Tests cross-module interactions and end-to-end workflows including:
|
||||
//! - Category theory operations
|
||||
//! - HoTT path algebra
|
||||
//! - Cross-module coherence
|
||||
|
||||
use prime_radiant_category::category::{
|
||||
Category, SetCategory, VectorCategory,
|
||||
};
|
||||
use prime_radiant_category::hott::{
|
||||
Term, Path, PathOps,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// CATEGORY THEORY INTEGRATION TESTS
|
||||
// ============================================================================
|
||||
|
||||
mod category_integration {
|
||||
use super::*;
|
||||
|
||||
/// Test SetCategory creation and basic operations
|
||||
#[test]
|
||||
fn test_set_category_basics() {
|
||||
let cat = SetCategory::new();
|
||||
assert_eq!(cat.objects().len(), 0);
|
||||
assert!(cat.verify_laws());
|
||||
}
|
||||
|
||||
/// Test VectorCategory creation and dimension
|
||||
#[test]
|
||||
fn test_vector_category_basics() {
|
||||
let cat = VectorCategory::new(768);
|
||||
assert_eq!(cat.dimension(), 768);
|
||||
assert!(cat.verify_laws());
|
||||
}
|
||||
|
||||
/// Test VectorCategory with different dimensions
|
||||
#[test]
|
||||
fn test_vector_category_dimensions() {
|
||||
// Common embedding dimensions
|
||||
let dims = [64, 128, 256, 384, 512, 768, 1024, 1536];
|
||||
|
||||
for dim in dims {
|
||||
let cat = VectorCategory::new(dim);
|
||||
assert_eq!(cat.dimension(), dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HOTT PATH ALGEBRA TESTS
|
||||
// ============================================================================
|
||||
|
||||
mod hott_integration {
|
||||
use super::*;
|
||||
|
||||
/// Test that path composition corresponds to morphism composition
|
||||
#[test]
|
||||
fn test_path_composition() {
|
||||
let a = Term::var("a");
|
||||
let b = Term::var("b");
|
||||
let c = Term::var("c");
|
||||
|
||||
let p = Path::new(a.clone(), b.clone(), Term::var("p"));
|
||||
let q = Path::new(b.clone(), c.clone(), Term::var("q"));
|
||||
|
||||
// Path composition should work like morphism composition
|
||||
let composed = p.compose(&q);
|
||||
assert!(composed.is_some(), "Composable paths should compose");
|
||||
|
||||
let pq = composed.unwrap();
|
||||
assert_eq!(pq.source(), &a);
|
||||
assert_eq!(pq.target(), &c);
|
||||
}
|
||||
|
||||
/// Test that reflexivity paths act as identity morphisms
|
||||
#[test]
|
||||
fn test_reflexivity_as_identity() {
|
||||
let x = Term::var("x");
|
||||
let refl_x = Path::refl(x.clone());
|
||||
|
||||
// Reflexivity is the identity path
|
||||
assert!(refl_x.is_refl());
|
||||
assert_eq!(refl_x.source(), refl_x.target());
|
||||
}
|
||||
|
||||
/// Test categorical unit laws through HoTT path algebra
|
||||
#[test]
|
||||
fn test_unit_laws() {
|
||||
let a = Term::var("a");
|
||||
let b = Term::var("b");
|
||||
|
||||
// Path p : a = b
|
||||
let p = Path::new(a.clone(), b.clone(), Term::var("p"));
|
||||
|
||||
// Reflexivity paths
|
||||
let refl_a = Path::refl(a.clone());
|
||||
let refl_b = Path::refl(b.clone());
|
||||
|
||||
// refl_a . p should give path from a to b (like p)
|
||||
let left_unit = refl_a.compose(&p);
|
||||
assert!(left_unit.is_some());
|
||||
let lu = left_unit.unwrap();
|
||||
assert_eq!(lu.source(), &a);
|
||||
assert_eq!(lu.target(), &b);
|
||||
|
||||
// p . refl_b should give path from a to b (like p)
|
||||
let right_unit = p.compose(&refl_b);
|
||||
assert!(right_unit.is_some());
|
||||
let ru = right_unit.unwrap();
|
||||
assert_eq!(ru.source(), &a);
|
||||
assert_eq!(ru.target(), &b);
|
||||
}
|
||||
|
||||
/// Test path inverse (symmetry)
|
||||
#[test]
|
||||
fn test_path_inverse() {
|
||||
let a = Term::var("a");
|
||||
let b = Term::var("b");
|
||||
|
||||
let p = Path::new(a.clone(), b.clone(), Term::var("p"));
|
||||
let p_inv = p.inverse();
|
||||
|
||||
// Inverse reverses endpoints
|
||||
assert_eq!(p_inv.source(), &b);
|
||||
assert_eq!(p_inv.target(), &a);
|
||||
|
||||
// Composing with inverse should give loop
|
||||
let round_trip = p.compose(&p_inv);
|
||||
assert!(round_trip.is_some());
|
||||
|
||||
let rt = round_trip.unwrap();
|
||||
assert_eq!(rt.source(), &a);
|
||||
assert_eq!(rt.target(), &a);
|
||||
}
|
||||
|
||||
/// Test associativity of path composition
|
||||
#[test]
|
||||
fn test_path_associativity() {
|
||||
let a = Term::var("a");
|
||||
let b = Term::var("b");
|
||||
let c = Term::var("c");
|
||||
let d = Term::var("d");
|
||||
|
||||
let p = Path::new(a.clone(), b.clone(), Term::var("p"));
|
||||
let q = Path::new(b.clone(), c.clone(), Term::var("q"));
|
||||
let r = Path::new(c.clone(), d.clone(), Term::var("r"));
|
||||
|
||||
// (p . q) . r
|
||||
let pq = p.compose(&q).unwrap();
|
||||
let left = pq.compose(&r);
|
||||
assert!(left.is_some());
|
||||
|
||||
// p . (q . r)
|
||||
let qr = q.compose(&r).unwrap();
|
||||
let right = p.compose(&qr);
|
||||
assert!(right.is_some());
|
||||
|
||||
// Both should have same endpoints
|
||||
let left = left.unwrap();
|
||||
let right = right.unwrap();
|
||||
assert_eq!(left.source(), right.source());
|
||||
assert_eq!(left.target(), right.target());
|
||||
}
|
||||
|
||||
/// Test functoriality via ap
|
||||
#[test]
|
||||
fn test_ap_functoriality() {
|
||||
let a = Term::var("a");
|
||||
let b = Term::var("b");
|
||||
let f = Term::var("f");
|
||||
|
||||
let p = Path::new(a.clone(), b.clone(), Term::var("p"));
|
||||
let ap_p = p.ap(&f);
|
||||
|
||||
// ap f p : f(a) = f(b)
|
||||
// The endpoints should be function applications
|
||||
assert!(!ap_p.is_refl() || a.structural_eq(&b));
|
||||
}
|
||||
|
||||
/// Test path composition fails on mismatch
|
||||
#[test]
|
||||
fn test_composition_mismatch() {
|
||||
let a = Term::var("a");
|
||||
let b = Term::var("b");
|
||||
let c = Term::var("c");
|
||||
|
||||
let p = Path::new(a.clone(), b.clone(), Term::var("p"));
|
||||
let q = Path::new(c.clone(), a.clone(), Term::var("q")); // c != b
|
||||
|
||||
// Should fail - endpoints don't match
|
||||
assert!(p.compose(&q).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CROSS-MODULE INTEGRATION TESTS
|
||||
// ============================================================================
|
||||
|
||||
mod cross_module_integration {
|
||||
use super::*;
|
||||
|
||||
/// Test that HoTT paths correspond to category morphisms
|
||||
#[test]
|
||||
fn test_hott_category_correspondence() {
|
||||
// In HoTT, a category is a type with:
|
||||
// - Objects as terms
|
||||
// - Morphisms as paths
|
||||
// - Composition as path composition
|
||||
|
||||
let a = Term::var("a");
|
||||
let b = Term::var("b");
|
||||
let c = Term::var("c");
|
||||
|
||||
// Morphisms are paths
|
||||
let f = Path::new(a.clone(), b.clone(), Term::var("f"));
|
||||
let g = Path::new(b.clone(), c.clone(), Term::var("g"));
|
||||
|
||||
// Composition is path composition
|
||||
let gf = f.compose(&g);
|
||||
assert!(gf.is_some());
|
||||
|
||||
// Identity is reflexivity
|
||||
let id_a = Path::refl(a.clone());
|
||||
assert!(id_a.is_refl());
|
||||
|
||||
// Identity laws hold via path algebra
|
||||
let f_id = f.compose(&Path::refl(b.clone()));
|
||||
assert!(f_id.is_some());
|
||||
}
|
||||
|
||||
/// Test belief modeling with paths
|
||||
#[test]
|
||||
fn test_belief_path_integration() {
|
||||
// Model belief transitions as paths
|
||||
let belief_a = Term::var("belief_a");
|
||||
let belief_b = Term::var("belief_b");
|
||||
|
||||
// Evidence for transition
|
||||
let evidence = Path::new(
|
||||
belief_a.clone(),
|
||||
belief_b.clone(),
|
||||
Term::var("evidence"),
|
||||
);
|
||||
|
||||
// Can compose evidence chains
|
||||
let belief_c = Term::var("belief_c");
|
||||
let more_evidence = Path::new(
|
||||
belief_b.clone(),
|
||||
belief_c.clone(),
|
||||
Term::var("more_evidence"),
|
||||
);
|
||||
|
||||
let full_path = evidence.compose(&more_evidence);
|
||||
assert!(full_path.is_some());
|
||||
}
|
||||
|
||||
/// Test category-path interaction
|
||||
#[test]
|
||||
fn test_category_path_interaction() {
|
||||
// Create a category
|
||||
let cat = VectorCategory::new(768);
|
||||
assert!(cat.verify_laws());
|
||||
|
||||
// Model categorical morphism composition with paths
|
||||
let obj_a = Term::var("vec_a");
|
||||
let obj_b = Term::var("vec_b");
|
||||
let obj_c = Term::var("vec_c");
|
||||
|
||||
// Linear maps as paths
|
||||
let linear_f = Path::new(obj_a.clone(), obj_b.clone(), Term::var("f"));
|
||||
let linear_g = Path::new(obj_b.clone(), obj_c.clone(), Term::var("g"));
|
||||
|
||||
// Composition
|
||||
let gf = linear_f.compose(&linear_g);
|
||||
assert!(gf.is_some());
|
||||
|
||||
let composed = gf.unwrap();
|
||||
assert_eq!(composed.source(), &obj_a);
|
||||
assert_eq!(composed.target(), &obj_c);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EDGE CASES AND ROBUSTNESS
|
||||
// ============================================================================
|
||||
|
||||
mod edge_cases {
|
||||
use super::*;
|
||||
|
||||
/// Test path composition with identity
|
||||
#[test]
|
||||
fn test_path_identity_composition() {
|
||||
let a = Term::var("a");
|
||||
|
||||
// Identity path
|
||||
let refl_a = Path::refl(a.clone());
|
||||
|
||||
// Composing identity with itself should give identity
|
||||
let composed = refl_a.compose(&refl_a);
|
||||
assert!(composed.is_some());
|
||||
|
||||
let c = composed.unwrap();
|
||||
assert_eq!(c.source(), &a);
|
||||
assert_eq!(c.target(), &a);
|
||||
}
|
||||
|
||||
/// Test multiple path inversions
|
||||
#[test]
|
||||
fn test_double_inverse() {
|
||||
let a = Term::var("a");
|
||||
let b = Term::var("b");
|
||||
|
||||
let p = Path::new(a.clone(), b.clone(), Term::var("p"));
|
||||
let p_inv = p.inverse();
|
||||
let p_inv_inv = p_inv.inverse();
|
||||
|
||||
// Double inverse should return to original endpoints
|
||||
assert_eq!(p_inv_inv.source(), &a);
|
||||
assert_eq!(p_inv_inv.target(), &b);
|
||||
}
|
||||
|
||||
/// Test long path chains
|
||||
#[test]
|
||||
fn test_long_path_chain() {
|
||||
// Create a chain of 10 paths
|
||||
let points: Vec<Term> = (0..11)
|
||||
.map(|i| Term::var(&format!("p{}", i)))
|
||||
.collect();
|
||||
|
||||
let paths: Vec<Path> = (0..10)
|
||||
.map(|i| Path::new(
|
||||
points[i].clone(),
|
||||
points[i + 1].clone(),
|
||||
Term::var(&format!("path{}", i)),
|
||||
))
|
||||
.collect();
|
||||
|
||||
// Compose all paths
|
||||
let mut composed = paths[0].clone();
|
||||
for path in paths.iter().skip(1) {
|
||||
composed = composed.compose(path).expect("Composition should succeed");
|
||||
}
|
||||
|
||||
// Result should go from first to last point
|
||||
assert_eq!(composed.source(), &points[0]);
|
||||
assert_eq!(composed.target(), &points[10]);
|
||||
}
|
||||
|
||||
/// Test category with many objects
|
||||
#[test]
|
||||
fn test_large_category() {
|
||||
let cat = VectorCategory::new(768);
|
||||
|
||||
// Creating many vector spaces should work
|
||||
for _ in 0..100 {
|
||||
// VectorCategory should handle multiple dimensions
|
||||
assert!(cat.verify_laws());
|
||||
}
|
||||
}
|
||||
|
||||
/// Test paths with numeric variable names
|
||||
#[test]
|
||||
fn test_numeric_variable_paths() {
|
||||
let vars: Vec<Term> = (0..5)
|
||||
.map(|i| Term::var(&i.to_string()))
|
||||
.collect();
|
||||
|
||||
// Create paths between sequential points
|
||||
for i in 0..4 {
|
||||
let p = Path::new(
|
||||
vars[i].clone(),
|
||||
vars[i + 1].clone(),
|
||||
Term::var(&format!("p{}", i)),
|
||||
);
|
||||
assert_eq!(p.source(), &vars[i]);
|
||||
assert_eq!(p.target(), &vars[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test reflexivity on complex terms
|
||||
#[test]
|
||||
fn test_complex_term_reflexivity() {
|
||||
// Create a lambda term
|
||||
let body = Term::var("x");
|
||||
let lambda = Term::lambda("x", body);
|
||||
|
||||
// Reflexivity should work on any term
|
||||
let refl = Path::refl(lambda.clone());
|
||||
assert!(refl.is_refl());
|
||||
assert_eq!(refl.source(), &lambda);
|
||||
assert_eq!(refl.target(), &lambda);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PERFORMANCE TESTS
|
||||
// ============================================================================
|
||||
|
||||
mod performance_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test path composition performance
|
||||
#[test]
|
||||
fn test_path_composition_performance() {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
// Create and compose many paths
|
||||
for i in 0..1000 {
|
||||
let a = Term::var(&format!("a{}", i));
|
||||
let b = Term::var(&format!("b{}", i));
|
||||
let c = Term::var(&format!("c{}", i));
|
||||
|
||||
let p = Path::new(a.clone(), b.clone(), Term::var("p"));
|
||||
let q = Path::new(b.clone(), c.clone(), Term::var("q"));
|
||||
|
||||
let _ = p.compose(&q);
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Should complete quickly
|
||||
assert!(duration.as_secs() < 5,
|
||||
"Path composition should be fast: {:?}", duration);
|
||||
}
|
||||
|
||||
/// Test category operations performance
|
||||
#[test]
|
||||
fn test_category_operations_performance() {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
for _ in 0..100 {
|
||||
let cat = VectorCategory::new(768);
|
||||
let _ = cat.verify_laws();
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
assert!(duration.as_secs() < 10,
|
||||
"Category operations should be fast: {:?}", duration);
|
||||
}
|
||||
|
||||
/// Test path inverse performance
|
||||
#[test]
|
||||
fn test_path_inverse_performance() {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
for i in 0..1000 {
|
||||
let a = Term::var(&format!("a{}", i));
|
||||
let b = Term::var(&format!("b{}", i));
|
||||
|
||||
let p = Path::new(a, b, Term::var("p"));
|
||||
let _ = p.inverse();
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
assert!(duration.as_secs() < 5,
|
||||
"Path inverse should be fast: {:?}", duration);
|
||||
}
|
||||
|
||||
/// Test long composition chain performance
|
||||
#[test]
|
||||
fn test_long_chain_performance() {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
// Create chain of 100 paths
|
||||
let points: Vec<Term> = (0..101)
|
||||
.map(|i| Term::var(&format!("p{}", i)))
|
||||
.collect();
|
||||
|
||||
let paths: Vec<Path> = (0..100)
|
||||
.map(|i| Path::new(
|
||||
points[i].clone(),
|
||||
points[i + 1].clone(),
|
||||
Term::var(&format!("path{}", i)),
|
||||
))
|
||||
.collect();
|
||||
|
||||
// Compose all
|
||||
let mut composed = paths[0].clone();
|
||||
for path in paths.iter().skip(1) {
|
||||
composed = composed.compose(path).expect("Should compose");
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
assert!(duration.as_secs() < 5,
|
||||
"Long chain composition should be fast: {:?}", duration);
|
||||
assert_eq!(composed.source(), &points[0]);
|
||||
assert_eq!(composed.target(), &points[100]);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GROUPOID STRUCTURE TESTS
|
||||
// ============================================================================
|
||||
|
||||
mod groupoid_structure {
|
||||
use super::*;
|
||||
|
||||
/// Test that paths form a groupoid (category where every morphism is invertible)
|
||||
#[test]
|
||||
fn test_groupoid_structure() {
|
||||
let a = Term::var("a");
|
||||
let b = Term::var("b");
|
||||
|
||||
let p = Path::new(a.clone(), b.clone(), Term::var("p"));
|
||||
|
||||
// Every path has an inverse
|
||||
let p_inv = p.inverse();
|
||||
assert_eq!(p_inv.source(), &b);
|
||||
assert_eq!(p_inv.target(), &a);
|
||||
|
||||
// p . p^(-1) gives identity (loop at source)
|
||||
let loop_a = p.compose(&p_inv);
|
||||
assert!(loop_a.is_some());
|
||||
let loop_a = loop_a.unwrap();
|
||||
assert_eq!(loop_a.source(), &a);
|
||||
assert_eq!(loop_a.target(), &a);
|
||||
|
||||
// p^(-1) . p gives identity (loop at target)
|
||||
let loop_b = p_inv.compose(&p);
|
||||
assert!(loop_b.is_some());
|
||||
let loop_b = loop_b.unwrap();
|
||||
assert_eq!(loop_b.source(), &b);
|
||||
assert_eq!(loop_b.target(), &b);
|
||||
}
|
||||
|
||||
/// Test inverse properties
|
||||
#[test]
|
||||
fn test_inverse_properties() {
|
||||
let a = Term::var("a");
|
||||
let b = Term::var("b");
|
||||
let c = Term::var("c");
|
||||
|
||||
let p = Path::new(a.clone(), b.clone(), Term::var("p"));
|
||||
let q = Path::new(b.clone(), c.clone(), Term::var("q"));
|
||||
|
||||
// (p . q)^(-1) should have endpoints reversed
|
||||
let pq = p.compose(&q).unwrap();
|
||||
let pq_inv = pq.inverse();
|
||||
|
||||
assert_eq!(pq_inv.source(), &c);
|
||||
assert_eq!(pq_inv.target(), &a);
|
||||
|
||||
// Compare with q^(-1) . p^(-1)
|
||||
let q_inv = q.inverse();
|
||||
let p_inv = p.inverse();
|
||||
let reversed = q_inv.compose(&p_inv).unwrap();
|
||||
|
||||
assert_eq!(reversed.source(), &c);
|
||||
assert_eq!(reversed.target(), &a);
|
||||
}
|
||||
|
||||
/// Test reflexivity inverse is itself
|
||||
#[test]
|
||||
fn test_refl_inverse() {
|
||||
let a = Term::var("a");
|
||||
let refl_a = Path::refl(a.clone());
|
||||
let refl_a_inv = refl_a.inverse();
|
||||
|
||||
// Inverse of refl should still be a loop at a
|
||||
assert_eq!(refl_a_inv.source(), &a);
|
||||
assert_eq!(refl_a_inv.target(), &a);
|
||||
}
|
||||
}
|
||||
871
vendor/ruvector/examples/prime-radiant/tests/quantum_tests.rs
vendored
Normal file
871
vendor/ruvector/examples/prime-radiant/tests/quantum_tests.rs
vendored
Normal file
@@ -0,0 +1,871 @@
|
||||
//! Comprehensive tests for Quantum/Algebraic Topology Module
|
||||
//!
|
||||
//! This test suite verifies quantum computing and topology constructs including:
|
||||
//! - Quantum state normalization and operations
|
||||
//! - Topological invariant computation (Betti numbers)
|
||||
//! - Persistent homology
|
||||
//! - Structure-preserving encoding
|
||||
|
||||
use prime_radiant::quantum::{
|
||||
ComplexMatrix, ComplexVector, Complex64,
|
||||
QuantumState, QuantumBasis, Qubit,
|
||||
DensityMatrix, MixedState,
|
||||
QuantumChannel, KrausOperator, PauliOperator, PauliType,
|
||||
TopologicalInvariant, HomologyGroup, CohomologyGroup, Cocycle,
|
||||
PersistenceDiagram, BirthDeathPair, PersistentHomologyComputer,
|
||||
Simplex, SimplicialComplex, SparseMatrix, BoundaryMatrix,
|
||||
TopologicalCode, StabilizerCode, GraphState, StructurePreservingEncoder,
|
||||
TopologicalEnergy, TopologicalCoherenceAnalyzer, QuantumCoherenceMetric,
|
||||
QuantumTopologyError, constants,
|
||||
};
|
||||
use prime_radiant::quantum::complex_matrix::gates;
|
||||
use proptest::prelude::*;
|
||||
use approx::assert_relative_eq;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
// =============================================================================
|
||||
// COMPLEX VECTOR AND MATRIX TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod complex_math_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test complex vector creation and normalization
|
||||
#[test]
|
||||
fn test_vector_normalization() {
|
||||
let mut v = ComplexVector::new(vec![
|
||||
Complex64::new(3.0, 0.0),
|
||||
Complex64::new(0.0, 4.0),
|
||||
]);
|
||||
|
||||
assert_relative_eq!(v.norm(), 5.0, epsilon = 1e-10);
|
||||
|
||||
v.normalize();
|
||||
assert_relative_eq!(v.norm(), 1.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test inner product
|
||||
#[test]
|
||||
fn test_inner_product() {
|
||||
let v1 = ComplexVector::new(vec![
|
||||
Complex64::new(1.0, 0.0),
|
||||
Complex64::new(0.0, 0.0),
|
||||
]);
|
||||
let v2 = ComplexVector::new(vec![
|
||||
Complex64::new(0.0, 0.0),
|
||||
Complex64::new(1.0, 0.0),
|
||||
]);
|
||||
|
||||
// Orthogonal vectors
|
||||
let inner = v1.inner(&v2);
|
||||
assert_relative_eq!(inner.norm(), 0.0, epsilon = 1e-10);
|
||||
|
||||
// Self inner product
|
||||
let self_inner = v1.inner(&v1);
|
||||
assert_relative_eq!(self_inner.re, 1.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(self_inner.im, 0.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test tensor product
|
||||
#[test]
|
||||
fn test_tensor_product() {
|
||||
// |0> tensor |1> = |01>
|
||||
let v0 = ComplexVector::basis_state(2, 0); // |0>
|
||||
let v1 = ComplexVector::basis_state(2, 1); // |1>
|
||||
|
||||
let tensor = v0.tensor(&v1);
|
||||
|
||||
assert_eq!(tensor.dim(), 4);
|
||||
// |01> = [0, 1, 0, 0]
|
||||
assert_relative_eq!(tensor.data[0].norm(), 0.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(tensor.data[1].norm(), 1.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(tensor.data[2].norm(), 0.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(tensor.data[3].norm(), 0.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test matrix properties
|
||||
#[test]
|
||||
fn test_matrix_properties() {
|
||||
let identity = ComplexMatrix::identity(3);
|
||||
|
||||
assert!(identity.is_square());
|
||||
assert!(identity.is_hermitian(1e-10));
|
||||
assert!(identity.is_unitary(1e-10));
|
||||
|
||||
let trace = identity.trace();
|
||||
assert_relative_eq!(trace.re, 3.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test Pauli matrices
|
||||
#[test]
|
||||
fn test_pauli_matrices() {
|
||||
let x = gates::pauli_x();
|
||||
let y = gates::pauli_y();
|
||||
let z = gates::pauli_z();
|
||||
|
||||
// All Pauli matrices are Hermitian
|
||||
assert!(x.is_hermitian(1e-10));
|
||||
assert!(y.is_hermitian(1e-10));
|
||||
assert!(z.is_hermitian(1e-10));
|
||||
|
||||
// X^2 = Y^2 = Z^2 = I
|
||||
let x2 = x.matmul(&x);
|
||||
let y2 = y.matmul(&y);
|
||||
let z2 = z.matmul(&z);
|
||||
|
||||
let i = ComplexMatrix::identity(2);
|
||||
|
||||
for row in 0..2 {
|
||||
for col in 0..2 {
|
||||
assert_relative_eq!(x2.get(row, col).norm(), i.get(row, col).norm(), epsilon = 1e-10);
|
||||
assert_relative_eq!(y2.get(row, col).norm(), i.get(row, col).norm(), epsilon = 1e-10);
|
||||
assert_relative_eq!(z2.get(row, col).norm(), i.get(row, col).norm(), epsilon = 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test Hadamard gate unitarity
|
||||
#[test]
|
||||
fn test_hadamard_gate() {
|
||||
let h = gates::hadamard();
|
||||
|
||||
assert!(h.is_unitary(1e-10));
|
||||
|
||||
// H|0> = |+> = (|0> + |1>)/sqrt(2)
|
||||
let zero = ComplexVector::basis_state(2, 0);
|
||||
let result = h.matvec(&zero);
|
||||
|
||||
let expected = 1.0 / 2.0_f64.sqrt();
|
||||
assert_relative_eq!(result.data[0].re, expected, epsilon = 1e-10);
|
||||
assert_relative_eq!(result.data[1].re, expected, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test rotation gates
|
||||
#[test]
|
||||
fn test_rotation_gates() {
|
||||
// Rx(pi) should be -iX
|
||||
let rx_pi = gates::rx(PI);
|
||||
|
||||
let zero = ComplexVector::basis_state(2, 0);
|
||||
let result = rx_pi.matvec(&zero);
|
||||
|
||||
// Rx(pi)|0> = -i|1>
|
||||
assert_relative_eq!(result.data[0].norm(), 0.0, epsilon = 1e-8);
|
||||
assert_relative_eq!(result.data[1].norm(), 1.0, epsilon = 1e-8);
|
||||
}
|
||||
|
||||
/// Test CNOT gate
|
||||
#[test]
|
||||
fn test_cnot_gate() {
|
||||
let cnot = gates::cnot();
|
||||
|
||||
assert!(cnot.is_unitary(1e-10));
|
||||
|
||||
// CNOT|10> = |11>
|
||||
let v10 = ComplexVector::basis_state(4, 2); // |10>
|
||||
let result = cnot.matvec(&v10);
|
||||
|
||||
// |11> is basis state 3
|
||||
assert_relative_eq!(result.data[3].norm(), 1.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(result.data[0].norm(), 0.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(result.data[1].norm(), 0.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(result.data[2].norm(), 0.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test partial trace
|
||||
#[test]
|
||||
fn test_partial_trace() {
|
||||
// Create maximally entangled state |00> + |11>
|
||||
let mut state = ComplexVector::zeros(4);
|
||||
state.data[0] = Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0);
|
||||
state.data[3] = Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0);
|
||||
|
||||
let density = state.outer(&state);
|
||||
|
||||
// Partial trace over second qubit
|
||||
let reduced = density.partial_trace_b(2, 2);
|
||||
|
||||
// Should give maximally mixed state: I/2
|
||||
assert_relative_eq!(reduced.get(0, 0).re, 0.5, epsilon = 1e-10);
|
||||
assert_relative_eq!(reduced.get(1, 1).re, 0.5, epsilon = 1e-10);
|
||||
assert_relative_eq!(reduced.get(0, 1).norm(), 0.0, epsilon = 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// QUANTUM STATE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod quantum_state_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test quantum state creation is normalized
|
||||
#[test]
|
||||
fn test_state_normalization() {
|
||||
let state = QuantumState::from_amplitudes(vec![
|
||||
Complex64::new(1.0, 0.0),
|
||||
Complex64::new(1.0, 0.0),
|
||||
]).unwrap();
|
||||
|
||||
assert_relative_eq!(state.norm(), 1.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test Bell state creation
|
||||
#[test]
|
||||
fn test_bell_states() {
|
||||
// |Phi+> = (|00> + |11>)/sqrt(2)
|
||||
let bell_phi_plus = QuantumState::bell_state_phi_plus();
|
||||
|
||||
assert_eq!(bell_phi_plus.dimension(), 4);
|
||||
assert_relative_eq!(bell_phi_plus.norm(), 1.0, epsilon = 1e-10);
|
||||
|
||||
// Check entanglement
|
||||
let density = bell_phi_plus.density_matrix();
|
||||
let reduced = density.partial_trace_b(2, 2);
|
||||
|
||||
// Von Neumann entropy of reduced state should be log(2)
|
||||
let entropy = bell_phi_plus.entanglement_entropy(2, 2);
|
||||
assert_relative_eq!(entropy, 2.0_f64.ln(), epsilon = 0.1);
|
||||
}
|
||||
|
||||
/// Test measurement probabilities
|
||||
#[test]
|
||||
fn test_measurement_probabilities() {
|
||||
let state = QuantumState::from_amplitudes(vec![
|
||||
Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0),
|
||||
Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0),
|
||||
]).unwrap();
|
||||
|
||||
let probs = state.measurement_probabilities();
|
||||
|
||||
assert_eq!(probs.len(), 2);
|
||||
assert_relative_eq!(probs[0], 0.5, epsilon = 1e-10);
|
||||
assert_relative_eq!(probs[1], 0.5, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test state evolution under unitary
|
||||
#[test]
|
||||
fn test_unitary_evolution() {
|
||||
let state = QuantumState::zero();
|
||||
let h = gates::hadamard();
|
||||
|
||||
let evolved = state.evolve(&h).unwrap();
|
||||
|
||||
// H|0> = |+>
|
||||
let probs = evolved.measurement_probabilities();
|
||||
assert_relative_eq!(probs[0], 0.5, epsilon = 1e-10);
|
||||
assert_relative_eq!(probs[1], 0.5, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test state fidelity
|
||||
#[test]
|
||||
fn test_state_fidelity() {
|
||||
let state1 = QuantumState::zero();
|
||||
let state2 = QuantumState::zero();
|
||||
|
||||
let fidelity = state1.fidelity(&state2);
|
||||
assert_relative_eq!(fidelity, 1.0, epsilon = 1e-10);
|
||||
|
||||
let state3 = QuantumState::one();
|
||||
let fidelity_orth = state1.fidelity(&state3);
|
||||
assert_relative_eq!(fidelity_orth, 0.0, epsilon = 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DENSITY MATRIX TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod density_matrix_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test pure state density matrix
|
||||
#[test]
|
||||
fn test_pure_state_density() {
|
||||
let state = QuantumState::zero();
|
||||
let density = DensityMatrix::from_pure_state(&state);
|
||||
|
||||
assert!(density.is_valid(1e-10));
|
||||
assert_relative_eq!(density.purity(), 1.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test mixed state
|
||||
#[test]
|
||||
fn test_mixed_state() {
|
||||
// Maximally mixed state: I/2
|
||||
let mixed = DensityMatrix::maximally_mixed(2);
|
||||
|
||||
assert!(mixed.is_valid(1e-10));
|
||||
assert_relative_eq!(mixed.purity(), 0.5, epsilon = 1e-10);
|
||||
assert_relative_eq!(mixed.trace().re, 1.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test von Neumann entropy
|
||||
#[test]
|
||||
fn test_von_neumann_entropy() {
|
||||
// Pure state has zero entropy
|
||||
let pure = DensityMatrix::from_pure_state(&QuantumState::zero());
|
||||
assert_relative_eq!(pure.von_neumann_entropy(), 0.0, epsilon = 1e-10);
|
||||
|
||||
// Maximally mixed has max entropy
|
||||
let mixed = DensityMatrix::maximally_mixed(2);
|
||||
assert_relative_eq!(mixed.von_neumann_entropy(), 2.0_f64.ln(), epsilon = 0.1);
|
||||
}
|
||||
|
||||
/// Test density matrix trace preservation under channels
|
||||
#[test]
|
||||
fn test_trace_preservation() {
|
||||
let density = DensityMatrix::from_pure_state(&QuantumState::zero());
|
||||
|
||||
// Apply depolarizing channel
|
||||
let channel = QuantumChannel::depolarizing(0.1);
|
||||
let evolved = density.apply_channel(&channel).unwrap();
|
||||
|
||||
assert_relative_eq!(evolved.trace().re, 1.0, epsilon = 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// QUANTUM CHANNEL TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod quantum_channel_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test identity channel
|
||||
#[test]
|
||||
fn test_identity_channel() {
|
||||
let channel = QuantumChannel::identity(2);
|
||||
|
||||
assert!(channel.is_valid());
|
||||
|
||||
let state = DensityMatrix::from_pure_state(&QuantumState::zero());
|
||||
let evolved = state.apply_channel(&channel).unwrap();
|
||||
|
||||
// Should be unchanged
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
assert_relative_eq!(
|
||||
evolved.matrix().get(i, j).norm(),
|
||||
state.matrix().get(i, j).norm(),
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test depolarizing channel
|
||||
#[test]
|
||||
fn test_depolarizing_channel() {
|
||||
let p = 0.5;
|
||||
let channel = QuantumChannel::depolarizing(p);
|
||||
|
||||
assert!(channel.is_valid());
|
||||
|
||||
// Full depolarization (p=1) gives maximally mixed state
|
||||
let full_depol = QuantumChannel::depolarizing(1.0);
|
||||
let state = DensityMatrix::from_pure_state(&QuantumState::zero());
|
||||
let evolved = state.apply_channel(&full_depol).unwrap();
|
||||
|
||||
// Should be maximally mixed
|
||||
assert_relative_eq!(evolved.purity(), 0.5, epsilon = 0.01);
|
||||
}
|
||||
|
||||
/// Test amplitude damping channel
|
||||
#[test]
|
||||
fn test_amplitude_damping() {
|
||||
let gamma = 0.5;
|
||||
let channel = QuantumChannel::amplitude_damping(gamma);
|
||||
|
||||
assert!(channel.is_valid());
|
||||
|
||||
// Should drive excited state toward ground state
|
||||
let excited = DensityMatrix::from_pure_state(&QuantumState::one());
|
||||
let evolved = excited.apply_channel(&channel).unwrap();
|
||||
|
||||
// Population in |0> should increase
|
||||
let p0 = evolved.matrix().get(0, 0).re;
|
||||
assert!(p0 > 0.0);
|
||||
}
|
||||
|
||||
/// Test Kraus operators sum to identity
|
||||
#[test]
|
||||
fn test_kraus_completeness() {
|
||||
let channel = QuantumChannel::depolarizing(0.3);
|
||||
|
||||
// Sum of K_i^dagger K_i should be identity
|
||||
let sum = channel.kraus_sum();
|
||||
|
||||
let identity = ComplexMatrix::identity(2);
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
assert_relative_eq!(
|
||||
sum.get(i, j).norm(),
|
||||
identity.get(i, j).norm(),
|
||||
epsilon = 1e-8
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOPOLOGICAL INVARIANT TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod topological_invariant_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test Betti numbers for sphere
|
||||
#[test]
|
||||
fn test_sphere_betti_numbers() {
|
||||
// S^2: b_0 = 1, b_1 = 0, b_2 = 1
|
||||
let sphere = SimplicialComplex::triangulated_sphere();
|
||||
let invariant = TopologicalInvariant::compute(&sphere);
|
||||
|
||||
assert_eq!(invariant.betti_number(0), 1);
|
||||
assert_eq!(invariant.betti_number(1), 0);
|
||||
assert_eq!(invariant.betti_number(2), 1);
|
||||
}
|
||||
|
||||
/// Test Betti numbers for torus
|
||||
#[test]
|
||||
fn test_torus_betti_numbers() {
|
||||
// T^2: b_0 = 1, b_1 = 2, b_2 = 1
|
||||
let torus = SimplicialComplex::triangulated_torus();
|
||||
let invariant = TopologicalInvariant::compute(&torus);
|
||||
|
||||
assert_eq!(invariant.betti_number(0), 1);
|
||||
assert_eq!(invariant.betti_number(1), 2);
|
||||
assert_eq!(invariant.betti_number(2), 1);
|
||||
}
|
||||
|
||||
/// Test Euler characteristic
|
||||
#[test]
|
||||
fn test_euler_characteristic() {
|
||||
// Sphere: chi = 2
|
||||
let sphere = SimplicialComplex::triangulated_sphere();
|
||||
let invariant = TopologicalInvariant::compute(&sphere);
|
||||
|
||||
let chi = invariant.euler_characteristic();
|
||||
assert_eq!(chi, 2);
|
||||
|
||||
// Torus: chi = 0
|
||||
let torus = SimplicialComplex::triangulated_torus();
|
||||
let invariant_torus = TopologicalInvariant::compute(&torus);
|
||||
|
||||
let chi_torus = invariant_torus.euler_characteristic();
|
||||
assert_eq!(chi_torus, 0);
|
||||
}
|
||||
|
||||
/// Test boundary operator
|
||||
#[test]
|
||||
fn test_boundary_operator() {
|
||||
// Triangle: boundary of face is the three edges
|
||||
let triangle = SimplicialComplex::from_simplices(vec![
|
||||
Simplex::new(vec![0, 1, 2]), // Face
|
||||
]);
|
||||
|
||||
let boundary_2 = triangle.boundary_matrix(2);
|
||||
|
||||
// Each edge appears with coefficient +/- 1
|
||||
assert!(boundary_2.num_nonzeros() > 0);
|
||||
}
|
||||
|
||||
/// Test boundary squared is zero
|
||||
#[test]
|
||||
fn test_boundary_squared_zero() {
|
||||
let complex = SimplicialComplex::triangulated_sphere();
|
||||
|
||||
let d2 = complex.boundary_matrix(2);
|
||||
let d1 = complex.boundary_matrix(1);
|
||||
|
||||
// d1 . d2 should be zero
|
||||
let composed = d1.matmul(&d2);
|
||||
|
||||
// All entries should be zero
|
||||
for val in composed.values() {
|
||||
assert_relative_eq!(*val, 0.0, epsilon = 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PERSISTENT HOMOLOGY TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod persistent_homology_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test persistence diagram for point cloud
|
||||
#[test]
|
||||
fn test_persistence_diagram_basic() {
|
||||
// Simple point cloud: 3 points forming a triangle
|
||||
let points = vec![
|
||||
vec![0.0, 0.0],
|
||||
vec![1.0, 0.0],
|
||||
vec![0.5, 0.866], // Equilateral triangle
|
||||
];
|
||||
|
||||
let computer = PersistentHomologyComputer::from_point_cloud(&points, 1.5);
|
||||
let diagram = computer.compute(1); // H_1
|
||||
|
||||
// Should detect one loop that persists for some range
|
||||
assert!(!diagram.pairs.is_empty() || diagram.pairs.is_empty());
|
||||
}
|
||||
|
||||
/// Test persistence pairing
|
||||
#[test]
|
||||
fn test_birth_death_pairs() {
|
||||
// 4 points forming a square
|
||||
let points = vec![
|
||||
vec![0.0, 0.0],
|
||||
vec![1.0, 0.0],
|
||||
vec![1.0, 1.0],
|
||||
vec![0.0, 1.0],
|
||||
];
|
||||
|
||||
let computer = PersistentHomologyComputer::from_point_cloud(&points, 2.0);
|
||||
let diagram = computer.compute(1);
|
||||
|
||||
// Check all pairs have birth < death
|
||||
for pair in &diagram.pairs {
|
||||
assert!(pair.birth < pair.death);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test persistence of connected components
|
||||
#[test]
|
||||
fn test_h0_persistence() {
|
||||
// Two clusters
|
||||
let points = vec![
|
||||
// Cluster 1
|
||||
vec![0.0, 0.0],
|
||||
vec![0.1, 0.1],
|
||||
// Cluster 2 (far away)
|
||||
vec![10.0, 10.0],
|
||||
vec![10.1, 10.1],
|
||||
];
|
||||
|
||||
let computer = PersistentHomologyComputer::from_point_cloud(&points, 5.0);
|
||||
let diagram = computer.compute(0); // H_0
|
||||
|
||||
// At scale 0, 4 components; they merge as scale increases
|
||||
// Should see some long-persisting component
|
||||
let long_lived: Vec<_> = diagram.pairs.iter()
|
||||
.filter(|p| p.persistence() > 1.0)
|
||||
.collect();
|
||||
|
||||
assert!(!long_lived.is_empty());
|
||||
}
|
||||
|
||||
/// Test bottleneck distance between diagrams
|
||||
#[test]
|
||||
fn test_bottleneck_distance() {
|
||||
let diag1 = PersistenceDiagram {
|
||||
dimension: 1,
|
||||
pairs: vec![
|
||||
BirthDeathPair { birth: 0.0, death: 1.0 },
|
||||
],
|
||||
};
|
||||
|
||||
let diag2 = PersistenceDiagram {
|
||||
dimension: 1,
|
||||
pairs: vec![
|
||||
BirthDeathPair { birth: 0.0, death: 1.5 },
|
||||
],
|
||||
};
|
||||
|
||||
let distance = diag1.bottleneck_distance(&diag2);
|
||||
|
||||
// Should be 0.5 (difference in death times)
|
||||
assert!(distance >= 0.0);
|
||||
assert!(distance <= 0.5 + 1e-6);
|
||||
}
|
||||
|
||||
/// Test Wasserstein distance
|
||||
#[test]
|
||||
fn test_wasserstein_distance() {
|
||||
let diag1 = PersistenceDiagram {
|
||||
dimension: 0,
|
||||
pairs: vec![
|
||||
BirthDeathPair { birth: 0.0, death: 1.0 },
|
||||
BirthDeathPair { birth: 0.5, death: 1.5 },
|
||||
],
|
||||
};
|
||||
|
||||
let diag2 = diag1.clone();
|
||||
|
||||
let distance = diag1.wasserstein_distance(&diag2, 2);
|
||||
assert_relative_eq!(distance, 0.0, epsilon = 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SIMPLICIAL COMPLEX TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod simplicial_complex_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test simplex creation
|
||||
#[test]
|
||||
fn test_simplex_creation() {
|
||||
let simplex = Simplex::new(vec![0, 1, 2]);
|
||||
|
||||
assert_eq!(simplex.dimension(), 2);
|
||||
assert_eq!(simplex.num_vertices(), 3);
|
||||
}
|
||||
|
||||
/// Test simplex faces
|
||||
#[test]
|
||||
fn test_simplex_faces() {
|
||||
let triangle = Simplex::new(vec![0, 1, 2]);
|
||||
let faces = triangle.faces();
|
||||
|
||||
assert_eq!(faces.len(), 3);
|
||||
for face in &faces {
|
||||
assert_eq!(face.dimension(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test simplicial complex construction
|
||||
#[test]
|
||||
fn test_complex_construction() {
|
||||
let complex = SimplicialComplex::from_simplices(vec![
|
||||
Simplex::new(vec![0, 1, 2]),
|
||||
Simplex::new(vec![0, 1, 3]),
|
||||
]);
|
||||
|
||||
assert!(complex.num_simplices(0) >= 4); // At least 4 vertices
|
||||
assert!(complex.num_simplices(1) >= 5); // At least 5 edges
|
||||
assert_eq!(complex.num_simplices(2), 2); // 2 triangles
|
||||
}
|
||||
|
||||
/// Test f-vector
|
||||
#[test]
|
||||
fn test_f_vector() {
|
||||
let tetrahedron = SimplicialComplex::from_simplices(vec![
|
||||
Simplex::new(vec![0, 1, 2, 3]),
|
||||
]);
|
||||
|
||||
let f_vec = tetrahedron.f_vector();
|
||||
|
||||
// Tetrahedron: 4 vertices, 6 edges, 4 triangles, 1 tetrahedron
|
||||
assert_eq!(f_vec[0], 4);
|
||||
assert_eq!(f_vec[1], 6);
|
||||
assert_eq!(f_vec[2], 4);
|
||||
assert_eq!(f_vec[3], 1);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOPOLOGICAL CODE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod topological_code_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test structure-preserving encoder
|
||||
#[test]
|
||||
fn test_structure_preserving_encoding() {
|
||||
let encoder = StructurePreservingEncoder::new(4); // 4 logical qubits
|
||||
|
||||
let data = vec![1.0, 0.0, 1.0, 0.0]; // Classical data
|
||||
let encoded = encoder.encode(&data).unwrap();
|
||||
|
||||
// Encoded state should be valid quantum state
|
||||
assert_relative_eq!(encoded.norm(), 1.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test stabilizer code
|
||||
#[test]
|
||||
fn test_stabilizer_code() {
|
||||
// Simple 3-qubit repetition code
|
||||
let code = StabilizerCode::repetition_code(3);
|
||||
|
||||
assert!(code.is_valid());
|
||||
assert_eq!(code.num_physical_qubits(), 3);
|
||||
assert_eq!(code.num_logical_qubits(), 1);
|
||||
}
|
||||
|
||||
/// Test error correction capability
|
||||
#[test]
|
||||
fn test_error_correction() {
|
||||
let code = StabilizerCode::repetition_code(3);
|
||||
|
||||
// Single bit flip should be correctable
|
||||
let error = PauliOperator::single_qubit(PauliType::X, 0, 3);
|
||||
|
||||
assert!(code.can_correct(&error));
|
||||
}
|
||||
|
||||
/// Test graph state creation
|
||||
#[test]
|
||||
fn test_graph_state() {
|
||||
// Linear graph: 0 - 1 - 2
|
||||
let edges = vec![(0, 1), (1, 2)];
|
||||
let graph_state = GraphState::from_edges(3, &edges);
|
||||
|
||||
let state = graph_state.state();
|
||||
assert_relative_eq!(state.norm(), 1.0, epsilon = 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOPOLOGICAL COHERENCE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod topological_coherence_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test topological energy computation
|
||||
#[test]
|
||||
fn test_topological_energy() {
|
||||
let complex = SimplicialComplex::triangulated_sphere();
|
||||
let energy = TopologicalEnergy::compute(&complex);
|
||||
|
||||
assert!(energy.total >= 0.0);
|
||||
assert!(energy.betti_contribution >= 0.0);
|
||||
}
|
||||
|
||||
/// Test coherence analyzer
|
||||
#[test]
|
||||
fn test_coherence_analyzer() {
|
||||
let analyzer = TopologicalCoherenceAnalyzer::new();
|
||||
|
||||
// Simple point cloud
|
||||
let points = vec![
|
||||
vec![0.0, 0.0],
|
||||
vec![1.0, 0.0],
|
||||
vec![0.5, 0.866],
|
||||
];
|
||||
|
||||
let metric = analyzer.analyze(&points).unwrap();
|
||||
|
||||
assert!(metric.coherence_score >= 0.0);
|
||||
assert!(metric.coherence_score <= 1.0);
|
||||
}
|
||||
|
||||
/// Test quantum coherence metric
|
||||
#[test]
|
||||
fn test_quantum_coherence_metric() {
|
||||
let state = QuantumState::bell_state_phi_plus();
|
||||
let metric = QuantumCoherenceMetric::compute(&state);
|
||||
|
||||
// Entangled state should have high coherence
|
||||
assert!(metric.l1_coherence >= 0.0);
|
||||
assert!(metric.relative_entropy_coherence >= 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROPERTY-BASED TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
|
||||
proptest! {
|
||||
/// Property: All quantum states are normalized
|
||||
#[test]
|
||||
fn prop_state_normalized(
|
||||
re in proptest::collection::vec(-10.0..10.0f64, 2..8),
|
||||
im in proptest::collection::vec(-10.0..10.0f64, 2..8)
|
||||
) {
|
||||
let n = re.len().min(im.len());
|
||||
let amplitudes: Vec<Complex64> = (0..n)
|
||||
.map(|i| Complex64::new(re[i], im[i]))
|
||||
.collect();
|
||||
|
||||
if let Ok(state) = QuantumState::from_amplitudes(amplitudes) {
|
||||
prop_assert!((state.norm() - 1.0).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
/// Property: Unitary matrices preserve norm
|
||||
#[test]
|
||||
fn prop_unitary_preserves_norm(
|
||||
theta in 0.0..2.0*PI
|
||||
) {
|
||||
let u = gates::rx(theta);
|
||||
let state = QuantumState::zero();
|
||||
|
||||
let evolved = state.evolve(&u).unwrap();
|
||||
|
||||
prop_assert!((evolved.norm() - 1.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
/// Property: Density matrix trace is always 1
|
||||
#[test]
|
||||
fn prop_density_trace_one(
|
||||
re in proptest::collection::vec(-10.0..10.0f64, 2..4),
|
||||
im in proptest::collection::vec(-10.0..10.0f64, 2..4)
|
||||
) {
|
||||
let n = re.len().min(im.len());
|
||||
let amplitudes: Vec<Complex64> = (0..n)
|
||||
.map(|i| Complex64::new(re[i], im[i]))
|
||||
.collect();
|
||||
|
||||
if let Ok(state) = QuantumState::from_amplitudes(amplitudes) {
|
||||
let density = state.density_matrix();
|
||||
prop_assert!((density.trace().re - 1.0).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EDGE CASE TESTS
|
||||
// =============================================================================
|
||||
|
||||
mod edge_case_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test zero vector handling
|
||||
#[test]
|
||||
fn test_zero_vector() {
|
||||
let zero = ComplexVector::zeros(3);
|
||||
assert_relative_eq!(zero.norm(), 0.0, epsilon = 1e-10);
|
||||
}
|
||||
|
||||
/// Test single qubit operations
|
||||
#[test]
|
||||
fn test_single_qubit() {
|
||||
let state = QuantumState::zero();
|
||||
assert_eq!(state.dimension(), 2);
|
||||
}
|
||||
|
||||
/// Test empty simplicial complex
|
||||
#[test]
|
||||
fn test_empty_complex() {
|
||||
let empty = SimplicialComplex::empty();
|
||||
assert_eq!(empty.num_simplices(0), 0);
|
||||
}
|
||||
|
||||
/// Test dimension errors
|
||||
#[test]
|
||||
fn test_dimension_mismatch() {
|
||||
let v1 = ComplexVector::zeros(2);
|
||||
let v2 = ComplexVector::zeros(3);
|
||||
|
||||
// This should panic or return error
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
v1.inner(&v2)
|
||||
});
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Test invalid quantum state
|
||||
#[test]
|
||||
fn test_invalid_state() {
|
||||
// All zeros is not a valid quantum state
|
||||
let result = QuantumState::from_amplitudes(vec![
|
||||
Complex64::new(0.0, 0.0),
|
||||
Complex64::new(0.0, 0.0),
|
||||
]);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
295
vendor/ruvector/examples/prime-radiant/tests/spectral_tests.rs
vendored
Normal file
295
vendor/ruvector/examples/prime-radiant/tests/spectral_tests.rs
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
//! Integration tests for the Spectral Invariants module
|
||||
|
||||
use prime_radiant::spectral::{
|
||||
Graph, SparseMatrix, SpectralAnalyzer, SpectralGap, Vector,
|
||||
CheegerAnalyzer, CheegerBounds, cheeger_inequality,
|
||||
SpectralClusterer, ClusterAssignment, ClusterConfig,
|
||||
CollapsePredictor, CollapsePrediction, Warning, WarningLevel,
|
||||
spectral_coherence_energy, SpectralEnergy, EnergyMinimizer,
|
||||
LanczosAlgorithm, PowerIteration,
|
||||
NodeId, EPS,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Graph Construction Helpers
|
||||
// ============================================================================
|
||||
|
||||
fn create_path_graph(n: usize) -> Graph {
|
||||
let edges: Vec<(usize, usize, f64)> = (0..n - 1)
|
||||
.map(|i| (i, i + 1, 1.0))
|
||||
.collect();
|
||||
Graph::from_edges(n, &edges)
|
||||
}
|
||||
|
||||
fn create_cycle_graph(n: usize) -> Graph {
|
||||
let mut edges: Vec<(usize, usize, f64)> = (0..n - 1)
|
||||
.map(|i| (i, i + 1, 1.0))
|
||||
.collect();
|
||||
edges.push((n - 1, 0, 1.0));
|
||||
Graph::from_edges(n, &edges)
|
||||
}
|
||||
|
||||
fn create_complete_graph(n: usize) -> Graph {
|
||||
let mut edges = Vec::new();
|
||||
for i in 0..n {
|
||||
for j in i + 1..n {
|
||||
edges.push((i, j, 1.0));
|
||||
}
|
||||
}
|
||||
Graph::from_edges(n, &edges)
|
||||
}
|
||||
|
||||
fn create_barbell_graph(clique_size: usize) -> Graph {
|
||||
let n = 2 * clique_size;
|
||||
let mut g = Graph::new(n);
|
||||
|
||||
// First clique
|
||||
for i in 0..clique_size {
|
||||
for j in i + 1..clique_size {
|
||||
g.add_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Second clique
|
||||
for i in clique_size..n {
|
||||
for j in i + 1..n {
|
||||
g.add_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Bridge
|
||||
g.add_edge(clique_size - 1, clique_size, 1.0);
|
||||
|
||||
g
|
||||
}
|
||||
|
||||
fn create_star_graph(n: usize) -> Graph {
|
||||
let edges: Vec<(usize, usize, f64)> = (1..n)
|
||||
.map(|i| (0, i, 1.0))
|
||||
.collect();
|
||||
Graph::from_edges(n, &edges)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Graph and SparseMatrix Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_graph_construction() {
|
||||
let g = create_complete_graph(5);
|
||||
|
||||
assert_eq!(g.n, 5);
|
||||
assert_eq!(g.num_edges(), 10);
|
||||
assert!(g.is_connected());
|
||||
assert_eq!(g.num_components(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_graph_degrees() {
|
||||
let g = create_complete_graph(5);
|
||||
let degrees = g.degrees();
|
||||
|
||||
for &d in °rees {
|
||||
assert!((d - 4.0).abs() < EPS);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disconnected_graph() {
|
||||
let g = Graph::from_edges(6, &[
|
||||
(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0),
|
||||
(3, 4, 1.0), (4, 5, 1.0), (5, 3, 1.0),
|
||||
]);
|
||||
|
||||
assert!(!g.is_connected());
|
||||
assert_eq!(g.num_components(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_laplacian_properties() {
|
||||
let g = create_complete_graph(4);
|
||||
let l = g.laplacian();
|
||||
|
||||
for i in 0..4 {
|
||||
let row_sum: f64 = (0..4).map(|j| l.get(i, j)).sum();
|
||||
assert!(row_sum.abs() < EPS, "Row sum should be zero");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Spectral Analyzer Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_spectral_analyzer_basic() {
|
||||
let g = create_cycle_graph(6);
|
||||
let mut analyzer = SpectralAnalyzer::new(g);
|
||||
analyzer.compute_laplacian_spectrum();
|
||||
|
||||
assert!(!analyzer.eigenvalues.is_empty());
|
||||
assert!(analyzer.eigenvalues[0].abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_algebraic_connectivity() {
|
||||
let complete = create_complete_graph(10);
|
||||
let path = create_path_graph(10);
|
||||
|
||||
let mut analyzer_complete = SpectralAnalyzer::new(complete);
|
||||
let mut analyzer_path = SpectralAnalyzer::new(path);
|
||||
|
||||
analyzer_complete.compute_laplacian_spectrum();
|
||||
analyzer_path.compute_laplacian_spectrum();
|
||||
|
||||
let ac_complete = analyzer_complete.algebraic_connectivity();
|
||||
let ac_path = analyzer_path.algebraic_connectivity();
|
||||
|
||||
assert!(ac_complete > ac_path);
|
||||
assert!(ac_complete > 0.0);
|
||||
assert!(ac_path > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fiedler_vector() {
|
||||
let g = create_barbell_graph(4);
|
||||
let mut analyzer = SpectralAnalyzer::new(g);
|
||||
analyzer.compute_laplacian_spectrum();
|
||||
|
||||
let fiedler = analyzer.fiedler_vector();
|
||||
assert!(fiedler.is_some());
|
||||
assert_eq!(fiedler.unwrap().len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bottleneck_detection() {
|
||||
let g = create_barbell_graph(5);
|
||||
let mut analyzer = SpectralAnalyzer::new(g);
|
||||
analyzer.compute_laplacian_spectrum();
|
||||
|
||||
let bottlenecks = analyzer.detect_bottlenecks();
|
||||
assert!(!bottlenecks.is_empty());
|
||||
|
||||
let has_bridge = bottlenecks.iter().any(|b| {
|
||||
b.crossing_edges.contains(&(4, 5))
|
||||
});
|
||||
assert!(has_bridge, "Bridge edge should be in bottleneck");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cheeger Analyzer Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_cheeger_bounds() {
|
||||
let g = create_complete_graph(10);
|
||||
let mut analyzer = CheegerAnalyzer::new(&g);
|
||||
let bounds = analyzer.compute_cheeger_bounds();
|
||||
|
||||
assert!(bounds.lower_bound >= 0.0);
|
||||
assert!(bounds.lower_bound <= bounds.cheeger_constant);
|
||||
assert!(bounds.cheeger_constant <= bounds.upper_bound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cheeger_well_connected() {
|
||||
let g = create_complete_graph(10);
|
||||
let mut analyzer = CheegerAnalyzer::new(&g);
|
||||
let bounds = analyzer.compute_cheeger_bounds();
|
||||
|
||||
assert!(bounds.is_well_connected());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Spectral Clustering Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_spectral_clustering_two_clusters() {
|
||||
let g = create_barbell_graph(5);
|
||||
let clusterer = SpectralClusterer::new(2);
|
||||
let assignment = clusterer.cluster(&g);
|
||||
|
||||
assert_eq!(assignment.k, 2);
|
||||
assert_eq!(assignment.labels.len(), 10);
|
||||
assert!(assignment.quality.modularity > 0.0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Collapse Predictor Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_collapse_predictor_stable() {
|
||||
let g = create_complete_graph(10);
|
||||
let predictor = CollapsePredictor::new();
|
||||
|
||||
let prediction = predictor.predict_collapse(&g);
|
||||
|
||||
assert!(prediction.risk_score < 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_warning_levels() {
|
||||
assert_eq!(WarningLevel::None.severity(), 0);
|
||||
assert_eq!(WarningLevel::Critical.severity(), 4);
|
||||
assert_eq!(WarningLevel::from_severity(2), WarningLevel::Medium);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Spectral Energy Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_spectral_energy_basic() {
|
||||
let g = create_complete_graph(10);
|
||||
let energy = spectral_coherence_energy(&g);
|
||||
|
||||
assert!(energy.laplacian_energy > 0.0);
|
||||
assert!(energy.coherence_energy > 0.0);
|
||||
assert!(energy.stability_score >= 0.0 && energy.stability_score <= 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spectral_energy_comparison() {
|
||||
let complete = create_complete_graph(10);
|
||||
let path = create_path_graph(10);
|
||||
|
||||
let energy_complete = spectral_coherence_energy(&complete);
|
||||
let energy_path = spectral_coherence_energy(&path);
|
||||
|
||||
assert!(energy_complete.coherence_energy > energy_path.coherence_energy);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lanczos Algorithm Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_power_iteration() {
|
||||
let g = create_complete_graph(5);
|
||||
let l = g.laplacian();
|
||||
|
||||
let power = PowerIteration::default();
|
||||
let (lambda, v) = power.largest_eigenvalue(&l);
|
||||
|
||||
let av = l.mul_vec(&v);
|
||||
let error: f64 = av.iter()
|
||||
.zip(v.iter())
|
||||
.map(|(avi, vi)| (avi - lambda * vi).powi(2))
|
||||
.sum::<f64>()
|
||||
.sqrt();
|
||||
|
||||
assert!(error < 0.1, "Eigenvalue error: {}", error);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lanczos_algorithm() {
|
||||
let g = create_cycle_graph(8);
|
||||
let l = g.laplacian();
|
||||
|
||||
let lanczos = LanczosAlgorithm::new(5);
|
||||
let (eigenvalues, eigenvectors) = lanczos.compute_smallest(&l);
|
||||
|
||||
assert!(!eigenvalues.is_empty());
|
||||
assert!(eigenvalues[0].abs() < 0.01);
|
||||
}
|
||||
Reference in New Issue
Block a user