git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
509 lines
15 KiB
Rust
509 lines
15 KiB
Rust
//! Integration tests for ruvector-graph-transformer.
|
|
//!
|
|
//! Tests the composition of all modules through proof-gated operations.
|
|
|
|
use ruvector_graph_transformer::{
|
|
AttestationChain, GraphTransformer, GraphTransformerConfig, ProofGate,
|
|
};
|
|
use ruvector_verified::{
|
|
gated::{ProofKind, ProofTier},
|
|
proof_store::create_attestation,
|
|
ProofEnvironment,
|
|
};
|
|
|
|
// ---- Proof-gated tests ----
|
|
|
|
#[test]
|
|
fn test_proof_gate_create_and_read() {
|
|
let gate = ProofGate::new(42u32);
|
|
assert_eq!(*gate.read(), 42);
|
|
assert!(gate.attestation_chain().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_proof_gate_dim_mutation_succeeds() {
|
|
let mut gate = ProofGate::new(vec![0.0f32; 128]);
|
|
let result = gate.mutate_with_dim_proof(128, 128, |v| {
|
|
v[0] = 42.0;
|
|
});
|
|
assert!(result.is_ok());
|
|
assert_eq!(gate.read()[0], 42.0);
|
|
assert_eq!(gate.attestation_chain().len(), 1);
|
|
|
|
// Verify attestation
|
|
let entry = gate.attestation_chain().latest().unwrap();
|
|
assert_eq!(entry.sequence, 0);
|
|
assert!(entry.attestation.verification_timestamp_ns > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_proof_gate_dim_mutation_fails_on_mismatch() {
|
|
let mut gate = ProofGate::new(vec![0.0f32; 64]);
|
|
let result = gate.mutate_with_dim_proof(128, 64, |v| {
|
|
v[0] = 1.0; // should not execute
|
|
});
|
|
assert!(result.is_err());
|
|
assert_eq!(gate.read()[0], 0.0); // unchanged
|
|
assert!(gate.attestation_chain().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_proof_gate_routed_mutation() {
|
|
let mut gate = ProofGate::new(100i32);
|
|
let result = gate.mutate_with_routed_proof(ProofKind::Reflexivity, 5, 5, |v| *v += 50);
|
|
assert!(result.is_ok());
|
|
let (decision, attestation) = result.unwrap();
|
|
assert_eq!(decision.tier, ProofTier::Reflex);
|
|
assert_eq!(*gate.read(), 150);
|
|
assert!(attestation.verification_timestamp_ns > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_proof_gate_pipeline_mutation() {
|
|
let mut gate = ProofGate::new(String::from("initial"));
|
|
let stages = vec![
|
|
("embed".into(), 1u32, 2u32),
|
|
("align".into(), 2, 3),
|
|
("call".into(), 3, 4),
|
|
];
|
|
let result = gate.mutate_with_pipeline_proof(&stages, |s| {
|
|
*s = String::from("transformed");
|
|
});
|
|
assert!(result.is_ok());
|
|
assert_eq!(gate.read().as_str(), "transformed");
|
|
}
|
|
|
|
#[test]
|
|
fn test_attestation_chain_integrity() {
|
|
let mut chain = AttestationChain::new();
|
|
let env = ProofEnvironment::new();
|
|
for i in 0..10 {
|
|
let att = create_attestation(&env, i);
|
|
chain.append(att);
|
|
}
|
|
assert_eq!(chain.len(), 10);
|
|
assert!(chain.verify_integrity());
|
|
assert!(!chain.is_empty());
|
|
assert_ne!(chain.chain_hash(), 0);
|
|
}
|
|
|
|
// ---- Sublinear attention tests ----
|
|
|
|
#[cfg(feature = "sublinear")]
|
|
mod sublinear_tests {
|
|
use ruvector_graph_transformer::config::SublinearConfig;
|
|
use ruvector_graph_transformer::SublinearGraphAttention;
|
|
|
|
#[test]
|
|
fn test_lsh_attention_basic() {
|
|
let config = SublinearConfig {
|
|
lsh_buckets: 4,
|
|
ppr_samples: 8,
|
|
sparsification_factor: 0.5,
|
|
};
|
|
let attn = SublinearGraphAttention::new(8, config);
|
|
|
|
let features: Vec<Vec<f32>> = (0..10).map(|i| vec![i as f32 * 0.1; 8]).collect();
|
|
|
|
let result = attn.lsh_attention(&features);
|
|
assert!(result.is_ok());
|
|
let outputs = result.unwrap();
|
|
assert_eq!(outputs.len(), 10);
|
|
for out in &outputs {
|
|
assert_eq!(out.len(), 8);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_ppr_attention_on_small_graph() {
|
|
let config = SublinearConfig {
|
|
lsh_buckets: 4,
|
|
ppr_samples: 3,
|
|
sparsification_factor: 0.5,
|
|
};
|
|
let attn = SublinearGraphAttention::new(4, config);
|
|
|
|
let features = vec![
|
|
vec![1.0, 0.0, 0.0, 0.0],
|
|
vec![0.0, 1.0, 0.0, 0.0],
|
|
vec![0.0, 0.0, 1.0, 0.0],
|
|
vec![0.0, 0.0, 0.0, 1.0],
|
|
vec![0.5, 0.5, 0.0, 0.0],
|
|
];
|
|
let edges = vec![
|
|
(0, 1, 1.0),
|
|
(1, 2, 1.0),
|
|
(2, 3, 1.0),
|
|
(3, 4, 1.0),
|
|
(4, 0, 1.0),
|
|
];
|
|
|
|
let result = attn.ppr_attention(&features, &edges);
|
|
assert!(result.is_ok());
|
|
assert_eq!(result.unwrap().len(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_spectral_attention_on_small_graph() {
|
|
let config = SublinearConfig {
|
|
lsh_buckets: 4,
|
|
ppr_samples: 4,
|
|
sparsification_factor: 0.5,
|
|
};
|
|
let attn = SublinearGraphAttention::new(4, config);
|
|
|
|
let features = vec![
|
|
vec![1.0, 0.5, 0.3, 0.1],
|
|
vec![0.5, 1.0, 0.4, 0.2],
|
|
vec![0.3, 0.4, 1.0, 0.5],
|
|
];
|
|
let edges = vec![(0, 1, 2.0), (1, 2, 1.0), (0, 2, 0.5)];
|
|
|
|
let result = attn.spectral_attention(&features, &edges);
|
|
assert!(result.is_ok());
|
|
}
|
|
}
|
|
|
|
// ---- Physics tests ----
|
|
|
|
#[cfg(feature = "physics")]
|
|
mod physics_tests {
|
|
use ruvector_graph_transformer::config::PhysicsConfig;
|
|
use ruvector_graph_transformer::HamiltonianGraphNet;
|
|
|
|
#[test]
|
|
fn test_hamiltonian_step_energy_conservation() {
|
|
let config = PhysicsConfig {
|
|
dt: 0.001,
|
|
leapfrog_steps: 1,
|
|
energy_tolerance: 0.1,
|
|
};
|
|
let mut hgn = HamiltonianGraphNet::new(4, config);
|
|
|
|
let features = vec![vec![0.1, 0.2, 0.3, 0.4], vec![0.4, 0.3, 0.2, 0.1]];
|
|
let state = hgn.init_state(&features).unwrap();
|
|
let edges = vec![(0, 1, 0.1)];
|
|
|
|
let result = hgn.step(&state, &edges).unwrap();
|
|
let energy_diff = (result.energy_after - result.energy_before).abs();
|
|
assert!(
|
|
energy_diff < 0.1,
|
|
"energy not conserved: diff={}",
|
|
energy_diff
|
|
);
|
|
assert!(result.energy_conserved);
|
|
assert!(result.attestation.is_some());
|
|
}
|
|
}
|
|
|
|
// ---- Biological tests ----
|
|
|
|
#[cfg(feature = "biological")]
|
|
mod biological_tests {
|
|
use ruvector_graph_transformer::config::BiologicalConfig;
|
|
use ruvector_graph_transformer::{HebbianLayer, SpikingGraphAttention};
|
|
|
|
#[test]
|
|
fn test_spiking_attention_update() {
|
|
let config = BiologicalConfig {
|
|
tau_membrane: 10.0,
|
|
threshold: 0.3,
|
|
stdp_rate: 0.01,
|
|
max_weight: 5.0,
|
|
};
|
|
let mut sga = SpikingGraphAttention::new(3, 4, config);
|
|
|
|
let features = vec![
|
|
vec![0.8, 0.6, 0.4, 0.2],
|
|
vec![0.1, 0.2, 0.3, 0.4],
|
|
vec![0.9, 0.7, 0.5, 0.3],
|
|
];
|
|
let weights = vec![
|
|
vec![0.0, 0.5, 0.3],
|
|
vec![0.5, 0.0, 0.2],
|
|
vec![0.3, 0.2, 0.0],
|
|
];
|
|
let adjacency = vec![(0, 1), (1, 2), (0, 2)];
|
|
|
|
let result = sga.step(&features, &weights, &adjacency).unwrap();
|
|
assert_eq!(result.features.len(), 3);
|
|
|
|
// Verify weight bounds
|
|
for row in &result.weights {
|
|
for &w in row {
|
|
assert!(w.abs() <= 5.0, "weight {} exceeds bound", w);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_hebbian_weight_bounds() {
|
|
let hebb = HebbianLayer::new(4, 1.0, 2.0);
|
|
let pre = vec![1.0, 1.0, 1.0, 1.0];
|
|
let post = vec![1.0, 1.0, 1.0, 1.0];
|
|
let mut weights = vec![0.0; 4];
|
|
|
|
for _ in 0..100 {
|
|
hebb.update(&pre, &post, &mut weights).unwrap();
|
|
}
|
|
assert!(hebb.verify_bounds(&weights));
|
|
}
|
|
}
|
|
|
|
// ---- Self-organizing tests ----
|
|
|
|
#[cfg(feature = "self-organizing")]
|
|
mod self_organizing_tests {
|
|
use ruvector_graph_transformer::config::SelfOrganizingConfig;
|
|
use ruvector_graph_transformer::self_organizing::{GrowthRule, GrowthRuleKind};
|
|
use ruvector_graph_transformer::{DevelopmentalProgram, MorphogeneticField};
|
|
|
|
#[test]
|
|
fn test_morphogenetic_step_topology_invariants() {
|
|
let config = SelfOrganizingConfig {
|
|
diffusion_rate: 0.05,
|
|
reaction_rate: 0.04,
|
|
max_growth_steps: 100,
|
|
coherence_threshold: 0.0,
|
|
};
|
|
let mut field = MorphogeneticField::new(5, config);
|
|
|
|
let edges = vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)];
|
|
|
|
for _ in 0..5 {
|
|
let result = field.step(&edges).unwrap();
|
|
// Concentrations must remain bounded [0.0, 2.0]
|
|
for &a in &result.activator {
|
|
assert!(a >= 0.0 && a <= 2.0);
|
|
}
|
|
for &b in &result.inhibitor {
|
|
assert!(b >= 0.0 && b <= 2.0);
|
|
}
|
|
// Bounds-passing step should produce attestation
|
|
assert!(result.attestation.is_some());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_developmental_growth_rules() {
|
|
let rules = vec![GrowthRule {
|
|
activator_threshold: 0.5,
|
|
max_degree: 3,
|
|
connection_weight: 1.0,
|
|
kind: GrowthRuleKind::Branch,
|
|
}];
|
|
let mut program = DevelopmentalProgram::new(rules, 10);
|
|
|
|
let activator = vec![0.8, 0.6, 0.2, 0.9];
|
|
let degrees = vec![1, 1, 1, 1];
|
|
let edges = vec![(0, 1), (2, 3)];
|
|
|
|
let result = program.grow_step(&activator, °rees, &edges).unwrap();
|
|
assert!(result.edges_added > 0);
|
|
assert!(result.attestation.is_some());
|
|
}
|
|
}
|
|
|
|
// ---- Verified training tests ----
|
|
|
|
#[cfg(feature = "verified-training")]
|
|
mod verified_training_tests {
|
|
use ruvector_gnn::RuvectorLayer;
|
|
use ruvector_graph_transformer::config::VerifiedTrainingConfig;
|
|
use ruvector_graph_transformer::{RollbackStrategy, TrainingInvariant, VerifiedTrainer};
|
|
|
|
#[test]
|
|
fn test_verified_training_single_step_certificate() {
|
|
let config = VerifiedTrainingConfig {
|
|
lipschitz_bound: 100.0,
|
|
verify_monotonicity: true,
|
|
learning_rate: 0.001,
|
|
..Default::default()
|
|
};
|
|
let invariants = vec![TrainingInvariant::WeightNormBound {
|
|
max_norm: 1000.0,
|
|
rollback_strategy: RollbackStrategy::DeltaApply,
|
|
}];
|
|
let mut trainer = VerifiedTrainer::new(4, 8, config, invariants);
|
|
|
|
let layer = RuvectorLayer::new(4, 8, 2, 0.0).unwrap();
|
|
let features = vec![vec![1.0, 0.0, 0.0, 0.0]];
|
|
let neighbors = vec![vec![]];
|
|
let weights = vec![vec![]];
|
|
let targets = vec![vec![0.0; 8]];
|
|
|
|
let result = trainer.train_step(&features, &neighbors, &weights, &targets, &layer);
|
|
assert!(result.is_ok());
|
|
let result = result.unwrap();
|
|
assert_eq!(result.step, 1);
|
|
assert!(result.loss >= 0.0);
|
|
assert!(result.weights_committed);
|
|
assert!(result.attestation.verification_timestamp_ns > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_verified_training_multiple_steps() {
|
|
let config = VerifiedTrainingConfig {
|
|
lipschitz_bound: 100.0,
|
|
verify_monotonicity: false,
|
|
learning_rate: 0.001,
|
|
..Default::default()
|
|
};
|
|
let invariants = vec![TrainingInvariant::WeightNormBound {
|
|
max_norm: 1000.0,
|
|
rollback_strategy: RollbackStrategy::DeltaApply,
|
|
}];
|
|
let mut trainer = VerifiedTrainer::new(4, 8, config, invariants);
|
|
|
|
let layer = RuvectorLayer::new(4, 8, 2, 0.0).unwrap();
|
|
|
|
for _ in 0..3 {
|
|
let result = trainer
|
|
.train_step(
|
|
&[vec![1.0; 4]],
|
|
&[vec![]],
|
|
&[vec![]],
|
|
&[vec![0.0; 8]],
|
|
&layer,
|
|
)
|
|
.unwrap();
|
|
assert!(result.weights_committed);
|
|
}
|
|
|
|
assert_eq!(trainer.step_count(), 3);
|
|
assert_eq!(trainer.step_results().len(), 3);
|
|
}
|
|
}
|
|
|
|
// ---- Manifold tests ----
|
|
|
|
#[cfg(feature = "manifold")]
|
|
mod manifold_tests {
|
|
use ruvector_graph_transformer::config::ManifoldConfig;
|
|
use ruvector_graph_transformer::manifold::{hyperbolic_geodesic, spherical_geodesic};
|
|
use ruvector_graph_transformer::ProductManifoldAttention;
|
|
|
|
#[test]
|
|
fn test_product_manifold_attention_curvature() {
|
|
let config = ManifoldConfig {
|
|
spherical_dim: 4,
|
|
hyperbolic_dim: 4,
|
|
euclidean_dim: 4,
|
|
curvature: -1.0,
|
|
};
|
|
let mut attn = ProductManifoldAttention::new(config);
|
|
assert_eq!(attn.total_dim(), 12);
|
|
|
|
let query = vec![0.5; 12];
|
|
let keys = vec![vec![0.3; 12], vec![0.7; 12]];
|
|
let values = vec![vec![1.0; 12], vec![2.0; 12]];
|
|
|
|
let result = attn.compute(&query, &keys, &values).unwrap();
|
|
assert_eq!(result.output.len(), 12);
|
|
|
|
// Verify curvatures
|
|
assert!(result.curvatures.spherical > 0.0);
|
|
assert!(result.curvatures.hyperbolic < 0.0);
|
|
assert!((result.curvatures.euclidean).abs() < 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn test_spherical_geodesic_distance() {
|
|
let a = vec![1.0, 0.0];
|
|
let b = vec![0.0, 1.0];
|
|
let dist = spherical_geodesic(&a, &b);
|
|
assert!((dist - std::f32::consts::FRAC_PI_2).abs() < 1e-4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_hyperbolic_geodesic_distance() {
|
|
let a = vec![0.0, 0.0];
|
|
let b = vec![0.1, 0.0];
|
|
let dist = hyperbolic_geodesic(&a, &b, -1.0);
|
|
assert!(dist > 0.0);
|
|
assert!(dist.is_finite());
|
|
}
|
|
}
|
|
|
|
// ---- Temporal tests ----
|
|
|
|
#[cfg(feature = "temporal")]
|
|
mod temporal_tests {
|
|
use ruvector_graph_transformer::config::TemporalConfig;
|
|
use ruvector_graph_transformer::CausalGraphTransformer;
|
|
|
|
#[test]
|
|
fn test_causal_attention_ordering() {
|
|
let config = TemporalConfig {
|
|
decay_rate: 0.9,
|
|
max_lag: 10,
|
|
granger_lags: 3,
|
|
};
|
|
let transformer = CausalGraphTransformer::new(4, config);
|
|
|
|
let sequence = vec![
|
|
vec![1.0, 0.0, 0.0, 0.0],
|
|
vec![0.0, 1.0, 0.0, 0.0],
|
|
vec![0.0, 0.0, 1.0, 0.0],
|
|
vec![0.0, 0.0, 0.0, 1.0],
|
|
vec![0.5, 0.5, 0.0, 0.0],
|
|
];
|
|
|
|
let result = transformer.temporal_attention(&sequence).unwrap();
|
|
assert_eq!(result.output.len(), 5);
|
|
assert_eq!(result.attention_weights.len(), 5);
|
|
|
|
// Verify causal ordering: no future attention
|
|
assert!(transformer.verify_causal_ordering(&result.attention_weights));
|
|
}
|
|
|
|
#[test]
|
|
fn test_granger_causality_extraction() {
|
|
let config = TemporalConfig {
|
|
decay_rate: 0.9,
|
|
max_lag: 5,
|
|
granger_lags: 2,
|
|
};
|
|
let transformer = CausalGraphTransformer::new(4, config);
|
|
|
|
let mut series = Vec::new();
|
|
for t in 0..30 {
|
|
let x = (t as f32 * 0.1).sin();
|
|
let y = (t as f32 * 0.2).cos();
|
|
series.push(vec![x, y, 0.0, 0.0]);
|
|
}
|
|
|
|
let result = transformer.granger_causality(&series, 0, 1).unwrap();
|
|
assert_eq!(result.source, 0);
|
|
assert_eq!(result.target, 1);
|
|
assert_eq!(result.lags, 2);
|
|
assert!(result.f_statistic >= 0.0);
|
|
}
|
|
}
|
|
|
|
// ---- Integration: Composing multiple modules ----
|
|
|
|
#[test]
|
|
fn test_graph_transformer_unified_entry() {
|
|
let config = GraphTransformerConfig::default();
|
|
let gt = GraphTransformer::new(config);
|
|
assert_eq!(gt.embed_dim(), 64);
|
|
|
|
let gate = gt.create_gate(vec![1.0, 2.0, 3.0]);
|
|
assert_eq!(gate.read().len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_proof_gate_multiple_mutations() {
|
|
let mut gate = ProofGate::new(0u64);
|
|
|
|
for i in 1..=5u32 {
|
|
let result = gate.mutate_with_dim_proof(i, i, |v| *v += 1);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
assert_eq!(*gate.read(), 5);
|
|
assert_eq!(gate.attestation_chain().len(), 5);
|
|
assert!(gate.attestation_chain().verify_integrity());
|
|
}
|