Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
461
crates/ruvector-graph/tests/hyperedge_tests.rs
Normal file
461
crates/ruvector-graph/tests/hyperedge_tests.rs
Normal file
@@ -0,0 +1,461 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user