#![allow(clippy::collapsible_if)]
use crate::AletheiaDB;
use crate::api::transaction::ReadOps;
use crate::core::error::{Error, Result};
use crate::core::id::NodeId;
use crate::core::vector::ops;
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub struct SynergyResult {
pub baseline_vector: Vec<f32>,
pub emergent_vector: Vec<f32>,
pub synergy_score: f32,
}
pub struct Synergy<'a> {
db: &'a AletheiaDB,
}
impl<'a> Synergy<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn calculate_synergy(&self, nodes: &[NodeId], property_name: &str) -> Result<f32> {
let result = self.analyze(nodes, property_name)?;
Ok(result.synergy_score)
}
pub fn analyze(&self, nodes: &[NodeId], property_name: &str) -> Result<SynergyResult> {
if nodes.is_empty() {
return Err(Error::other("Cannot analyze empty node list"));
}
let node_set: HashSet<NodeId> = nodes.iter().cloned().collect();
let mut vectors = Vec::new();
let mut valid_nodes = Vec::new();
let mut baseline_vector = Vec::new();
let mut emergent_components = Vec::new();
self.db.read(|tx| {
for &node_id in nodes {
if let Ok(node) = tx.get_node(node_id) {
if let Some(prop) = node.get_property(property_name).and_then(|p| p.as_vector())
{
vectors.push(prop.to_vec());
valid_nodes.push(node_id);
}
}
}
if vectors.is_empty() {
return Err(Error::other(
"None of the provided nodes have the specified vector property",
));
}
baseline_vector = Self::average_vectors(&vectors)?;
for (i, &node_id) in valid_nodes.iter().enumerate() {
let mut neighbor_vectors = Vec::new();
let outgoing = tx.get_outgoing_edges(node_id);
for edge_id in outgoing {
if let Ok(edge) = tx.get_edge(edge_id) {
let target = edge.target;
if node_set.contains(&target) {
if let Some(idx) = valid_nodes.iter().position(|&id| id == target) {
neighbor_vectors.push(vectors[idx].clone());
}
}
}
}
let incoming = tx.get_incoming_edges(node_id);
for edge_id in incoming {
if let Ok(edge) = tx.get_edge(edge_id) {
let source = edge.source;
if node_set.contains(&source) {
if let Some(idx) = valid_nodes.iter().position(|&id| id == source) {
neighbor_vectors.push(vectors[idx].clone());
}
}
}
}
let node_vec = &vectors[i];
if neighbor_vectors.is_empty() {
emergent_components.push(node_vec.clone());
} else {
let neighbor_avg = Self::average_vectors(&neighbor_vectors)?;
let alpha = 0.5_f32;
let mut combined = vec![0.0; node_vec.len()];
for j in 0..node_vec.len() {
combined[j] = (1.0 - alpha) * node_vec[j] + alpha * neighbor_avg[j];
}
emergent_components.push(combined);
}
}
Ok::<(), Error>(())
})?;
let mut emergent_vector = Self::average_vectors(&emergent_components)?;
ops::normalize_in_place(&mut emergent_vector);
let mut baseline_normalized = baseline_vector.clone();
ops::normalize_in_place(&mut baseline_normalized);
let similarity = ops::cosine_similarity(&baseline_normalized, &emergent_vector)?;
let synergy_score = (1.0_f32 - similarity).max(0.0);
Ok(SynergyResult {
baseline_vector: baseline_normalized,
emergent_vector,
synergy_score,
})
}
fn average_vectors(vectors: &[Vec<f32>]) -> Result<Vec<f32>> {
if vectors.is_empty() {
return Err(Error::other("Cannot average empty vector list"));
}
let dim = vectors[0].len();
let mut sum = vec![0.0; dim];
for vec in vectors {
if vec.len() != dim {
return Err(Error::other("Vector dimensions do not match"));
}
for i in 0..dim {
sum[i] += vec[i];
}
}
let count = vectors.len() as f32;
for val in &mut sum {
*val /= count;
}
Ok(sum)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::transaction::WriteOps;
use crate::core::property::PropertyMapBuilder;
#[test]
fn test_synergy_disconnected_nodes() {
let db = AletheiaDB::new().unwrap();
let mut n1 = NodeId::new(0).unwrap();
let mut n2 = NodeId::new(0).unwrap();
db.write(|tx| {
n1 = tx
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("embedding", &[1.0, 0.0])
.build(),
)
.unwrap();
n2 = tx
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("embedding", &[0.0, 1.0])
.build(),
)
.unwrap();
Ok::<(), Error>(())
})
.unwrap();
let synergy = Synergy::new(&db);
let result = synergy.analyze(&[n1, n2], "embedding").unwrap();
assert!(result.synergy_score < 0.001);
}
#[test]
fn test_synergy_connected_nodes() {
let db = AletheiaDB::new().unwrap();
let mut n1 = NodeId::new(0).unwrap();
let mut n2 = NodeId::new(0).unwrap();
let mut n3 = NodeId::new(0).unwrap();
db.write(|tx| {
n1 = tx
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("embedding", &[1.0, 0.0])
.build(),
)
.unwrap();
n2 = tx
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("embedding", &[0.0, 1.0])
.build(),
)
.unwrap();
n3 = tx
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("embedding", &[0.5, 0.5])
.build(),
)
.unwrap();
tx.create_edge(n1, n2, "LINK", Default::default()).unwrap();
tx.create_edge(n2, n3, "LINK", Default::default()).unwrap();
Ok::<(), Error>(())
})
.unwrap();
let synergy = Synergy::new(&db);
let result = synergy.analyze(&[n1, n2, n3], "embedding").unwrap();
assert!(result.synergy_score > 0.0);
}
#[test]
fn test_synergy_concurrency_torn_read() {
use std::sync::Arc;
use std::thread;
let db = Arc::new(AletheiaDB::new().unwrap());
let mut n1 = NodeId::new(0).unwrap();
let mut n2 = NodeId::new(0).unwrap();
db.write(|tx| {
n1 = tx
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("embedding", &[1.0, 0.0])
.build(),
)
.unwrap();
n2 = tx
.create_node(
"Node",
PropertyMapBuilder::new()
.insert_vector("embedding", &[0.0, 1.0])
.build(),
)
.unwrap();
Ok::<(), Error>(())
})
.unwrap();
let db_clone = db.clone();
let write_thread = thread::spawn(move || {
thread::sleep(std::time::Duration::from_millis(10));
db_clone
.write(|tx| {
tx.create_edge(n1, n2, "LINK", Default::default()).unwrap();
Ok::<(), Error>(())
})
.unwrap();
});
let synergy = Synergy::new(&db);
let result = synergy.analyze(&[n1, n2], "embedding").unwrap();
write_thread.join().unwrap();
assert!(result.synergy_score >= 0.0);
}
}