Files
wifi-densepose/crates/ruvector-graph/tests/hyperedge_tests.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

462 lines
13 KiB
Rust

//! Hyperedge (N-ary relationship) tests
//!
//! Tests for hypergraph features supporting relationships between multiple nodes.
//! Based on the existing hypergraph implementation in ruvector-core.
use ruvector_core::advanced::hypergraph::{
Hyperedge, HypergraphIndex, TemporalGranularity, TemporalHyperedge,
};
use ruvector_core::types::DistanceMetric;
#[test]
fn test_create_binary_hyperedge() {
let edge = Hyperedge::new(
vec!["1".to_string(), "2".to_string()],
"Alice knows Bob".to_string(),
vec![0.1, 0.2, 0.3],
0.95,
);
assert_eq!(edge.order(), 2);
assert!(edge.contains_node(&"1".to_string()));
assert!(edge.contains_node(&"2".to_string()));
assert!(!edge.contains_node(&"3".to_string()));
}
#[test]
fn test_create_ternary_hyperedge() {
let edge = Hyperedge::new(
vec!["1".to_string(), "2".to_string(), "3".to_string()],
"Meeting between Alice, Bob, and Charlie".to_string(),
vec![0.5; 128],
0.90,
);
assert_eq!(edge.order(), 3);
assert!(edge.contains_node(&"1".to_string()));
assert!(edge.contains_node(&"2".to_string()));
assert!(edge.contains_node(&"3".to_string()));
}
#[test]
fn test_create_large_hyperedge() {
let nodes: Vec<String> = (0..100).map(|i| i.to_string()).collect();
let edge = Hyperedge::new(
nodes.clone(),
"Large group collaboration".to_string(),
vec![0.1; 64],
0.75,
);
assert_eq!(edge.order(), 100);
for node in nodes {
assert!(edge.contains_node(&node));
}
}
#[test]
fn test_hyperedge_confidence_clamping() {
let edge1 = Hyperedge::new(
vec!["1".to_string(), "2".to_string()],
"Test".to_string(),
vec![0.1],
1.5,
);
assert_eq!(edge1.confidence, 1.0);
let edge2 = Hyperedge::new(
vec!["1".to_string(), "2".to_string()],
"Test".to_string(),
vec![0.1],
-0.5,
);
assert_eq!(edge2.confidence, 0.0);
let edge3 = Hyperedge::new(
vec!["1".to_string(), "2".to_string()],
"Test".to_string(),
vec![0.1],
0.75,
);
assert_eq!(edge3.confidence, 0.75);
}
#[test]
fn test_temporal_hyperedge_creation() {
let edge = Hyperedge::new(
vec!["1".to_string(), "2".to_string(), "3".to_string()],
"Temporal relationship".to_string(),
vec![0.5; 32],
0.9,
);
let temporal = TemporalHyperedge::new(edge, TemporalGranularity::Hourly);
assert!(!temporal.is_expired());
assert!(temporal.timestamp > 0);
assert!(temporal.time_bucket() > 0);
}
#[test]
fn test_temporal_granularity_bucketing() {
let edge = Hyperedge::new(
vec!["1".to_string(), "2".to_string()],
"Test".to_string(),
vec![0.1],
1.0,
);
let hourly = TemporalHyperedge::new(edge.clone(), TemporalGranularity::Hourly);
let daily = TemporalHyperedge::new(edge.clone(), TemporalGranularity::Daily);
let monthly = TemporalHyperedge::new(edge.clone(), TemporalGranularity::Monthly);
// Different granularities should produce different buckets
assert!(hourly.time_bucket() >= daily.time_bucket());
assert!(daily.time_bucket() >= monthly.time_bucket());
}
#[test]
fn test_hypergraph_index_basic() {
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
// Add entities
index.add_entity("1".to_string(), vec![1.0, 0.0, 0.0]);
index.add_entity("2".to_string(), vec![0.0, 1.0, 0.0]);
index.add_entity("3".to_string(), vec![0.0, 0.0, 1.0]);
// Add hyperedge
let edge = Hyperedge::new(
vec!["1".to_string(), "2".to_string(), "3".to_string()],
"Triangle relationship".to_string(),
vec![0.33, 0.33, 0.34],
0.95,
);
index.add_hyperedge(edge).unwrap();
let stats = index.stats();
assert_eq!(stats.total_entities, 3);
assert_eq!(stats.total_hyperedges, 1);
}
#[test]
fn test_hypergraph_multiple_hyperedges() {
let mut index = HypergraphIndex::new(DistanceMetric::Euclidean);
// Add entities
for i in 1..=5 {
index.add_entity(i.to_string(), vec![i as f32; 64]);
}
// Add multiple hyperedges with different orders
let edge1 = Hyperedge::new(
vec!["1".to_string(), "2".to_string()],
"Binary".to_string(),
vec![0.5; 64],
1.0,
);
let edge2 = Hyperedge::new(
vec!["1".to_string(), "2".to_string(), "3".to_string()],
"Ternary".to_string(),
vec![0.5; 64],
1.0,
);
let edge3 = Hyperedge::new(
vec![
"1".to_string(),
"2".to_string(),
"3".to_string(),
"4".to_string(),
],
"Quaternary".to_string(),
vec![0.5; 64],
1.0,
);
let edge4 = Hyperedge::new(
vec![
"1".to_string(),
"2".to_string(),
"3".to_string(),
"4".to_string(),
"5".to_string(),
],
"Quinary".to_string(),
vec![0.5; 64],
1.0,
);
index.add_hyperedge(edge1).unwrap();
index.add_hyperedge(edge2).unwrap();
index.add_hyperedge(edge3).unwrap();
index.add_hyperedge(edge4).unwrap();
let stats = index.stats();
assert_eq!(stats.total_hyperedges, 4);
assert!(stats.avg_entity_degree > 0.0);
}
#[test]
fn test_hypergraph_search() {
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
// Add entities
for i in 1..=10 {
index.add_entity(i.to_string(), vec![i as f32 * 0.1; 32]);
}
// Add hyperedges
for i in 1..=5 {
let edge = Hyperedge::new(
vec![i.to_string(), (i + 1).to_string()],
format!("Edge {}", i),
vec![i as f32 * 0.1; 32],
0.9,
);
index.add_hyperedge(edge).unwrap();
}
// Search for similar hyperedges
let query = vec![0.3; 32];
let results = index.search_hyperedges(&query, 3);
assert_eq!(results.len(), 3);
// Results should be sorted by distance
for i in 0..results.len() - 1 {
assert!(results[i].1 <= results[i + 1].1);
}
}
#[test]
fn test_k_hop_neighbors_simple() {
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
// Create chain: 1-2-3-4
for i in 1..=4 {
index.add_entity(i.to_string(), vec![i as f32]);
}
let e1 = Hyperedge::new(
vec!["1".to_string(), "2".to_string()],
"e1".to_string(),
vec![1.0],
1.0,
);
let e2 = Hyperedge::new(
vec!["2".to_string(), "3".to_string()],
"e2".to_string(),
vec![1.0],
1.0,
);
let e3 = Hyperedge::new(
vec!["3".to_string(), "4".to_string()],
"e3".to_string(),
vec![1.0],
1.0,
);
index.add_hyperedge(e1).unwrap();
index.add_hyperedge(e2).unwrap();
index.add_hyperedge(e3).unwrap();
// 1-hop from node 1 should include 1 and 2
let neighbors_1hop = index.k_hop_neighbors("1".to_string(), 1);
assert!(neighbors_1hop.contains(&"1".to_string()));
assert!(neighbors_1hop.contains(&"2".to_string()));
// 2-hop from node 1 should include 1, 2, and 3
let neighbors_2hop = index.k_hop_neighbors("1".to_string(), 2);
assert!(neighbors_2hop.contains(&"1".to_string()));
assert!(neighbors_2hop.contains(&"2".to_string()));
assert!(neighbors_2hop.contains(&"3".to_string()));
// 3-hop from node 1 should include all nodes
let neighbors_3hop = index.k_hop_neighbors("1".to_string(), 3);
assert_eq!(neighbors_3hop.len(), 4);
}
#[test]
fn test_k_hop_neighbors_complex() {
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
// Create star topology: center node connected to 5 peripheral nodes
for i in 0..=5 {
index.add_entity(i.to_string(), vec![i as f32]);
}
// Center (0) connected to all others via hyperedges
for i in 1..=5 {
let edge = Hyperedge::new(
vec!["0".to_string(), i.to_string()],
format!("e{}", i),
vec![1.0],
1.0,
);
index.add_hyperedge(edge).unwrap();
}
// 1-hop from center should reach all nodes
let neighbors = index.k_hop_neighbors("0".to_string(), 1);
assert_eq!(neighbors.len(), 6); // All nodes
// 1-hop from peripheral node should reach center and itself
let neighbors = index.k_hop_neighbors("1".to_string(), 1);
assert!(neighbors.contains(&"0".to_string()));
assert!(neighbors.contains(&"1".to_string()));
}
#[test]
fn test_temporal_range_query() {
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
// Add entities
for i in 1..=3 {
index.add_entity(i.to_string(), vec![i as f32]);
}
// Add temporal hyperedges (they'll all be in current time bucket)
let edge1 = Hyperedge::new(
vec!["1".to_string(), "2".to_string()],
"t1".to_string(),
vec![1.0],
1.0,
);
let edge2 = Hyperedge::new(
vec!["2".to_string(), "3".to_string()],
"t2".to_string(),
vec![1.0],
1.0,
);
let temp1 = TemporalHyperedge::new(edge1, TemporalGranularity::Hourly);
let temp2 = TemporalHyperedge::new(edge2, TemporalGranularity::Hourly);
let bucket = temp1.time_bucket();
index.add_temporal_hyperedge(temp1).unwrap();
index.add_temporal_hyperedge(temp2).unwrap();
// Query current time bucket
let results = index.query_temporal_range(bucket, bucket);
assert_eq!(results.len(), 2);
}
#[test]
fn test_hyperedge_with_duplicate_nodes() {
// Test that hyperedge handles duplicate nodes appropriately
let edge = Hyperedge::new(
vec![
"1".to_string(),
"2".to_string(),
"2".to_string(),
"3".to_string(),
], // Duplicate node 2
"Duplicate test".to_string(),
vec![0.5; 16],
0.8,
);
assert_eq!(edge.order(), 4); // Includes duplicates
assert!(edge.contains_node(&"2".to_string()));
}
#[test]
fn test_hypergraph_error_on_missing_entity() {
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
// Only add entity 1, not 2
index.add_entity("1".to_string(), vec![1.0]);
// Try to create hyperedge with missing entity
let edge = Hyperedge::new(
vec!["1".to_string(), "2".to_string()],
"Test".to_string(),
vec![0.5],
1.0,
);
let result = index.add_hyperedge(edge);
assert!(result.is_err());
}
// ============================================================================
// Property-based tests
// ============================================================================
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
fn node_vec_strategy() -> impl Strategy<Value = Vec<String>> {
prop::collection::vec("[a-z]{1,5}".prop_map(|s| s), 2..20)
}
fn embedding_strategy(dim: usize) -> impl Strategy<Value = Vec<f32>> {
prop::collection::vec(-1.0f32..1.0f32, dim)
}
proptest! {
#[test]
fn test_hyperedge_order_property(
nodes in node_vec_strategy()
) {
let edge = Hyperedge::new(
nodes.clone(),
"Test".to_string(),
vec![0.5; 32],
0.9
);
assert_eq!(edge.order(), nodes.len());
}
#[test]
fn test_hyperedge_contains_all_nodes(
nodes in node_vec_strategy()
) {
let edge = Hyperedge::new(
nodes.clone(),
"Test".to_string(),
vec![0.5; 32],
0.9
);
for node in &nodes {
assert!(edge.contains_node(node));
}
}
#[test]
fn test_hypergraph_search_consistency(
query in embedding_strategy(32),
k in 1usize..10
) {
let mut index = HypergraphIndex::new(DistanceMetric::Cosine);
// Add entities
for i in 1..=10 {
index.add_entity(i.to_string(), vec![i as f32 * 0.1; 32]);
}
// Add hyperedges
for i in 1..=10 {
let edge = Hyperedge::new(
vec![i.to_string()],
format!("Edge {}", i),
vec![i as f32 * 0.1; 32],
0.9
);
index.add_hyperedge(edge).unwrap();
}
let results = index.search_hyperedges(&query, k.min(10));
assert!(results.len() <= k.min(10));
// Verify results are sorted
for i in 0..results.len().saturating_sub(1) {
assert!(results[i].1 <= results[i + 1].1);
}
}
}
}