use crate::AletheiaDB;
use crate::core::error::{Error, Result, VectorError};
use crate::core::id::NodeId;
use crate::core::vector::ops;
pub struct DissonanceEngine<'a> {
db: &'a AletheiaDB,
}
impl<'a> DissonanceEngine<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn calculate_dissonance(&self, node_id: NodeId, vector_property: &str) -> Result<f32> {
let node = self.db.get_node(node_id)?;
let Some(prop) = node.properties.get(vector_property) else {
return Err(Error::Vector(VectorError::IndexError(format!(
"Property '{}' not found on node {}",
vector_property, node_id
))));
};
let Some(node_vec) = prop.as_vector() else {
return Err(Error::Vector(VectorError::IndexError(format!(
"Property '{}' is not a vector",
vector_property
))));
};
let outgoing = self.db.get_outgoing_edges(node_id);
let incoming = self.db.get_incoming_edges(node_id);
let mut neighbors = std::collections::HashSet::new();
for edge_id in outgoing {
let edge = self.db.get_edge(edge_id)?;
neighbors.insert(edge.target);
}
for edge_id in incoming {
let edge = self.db.get_edge(edge_id)?;
neighbors.insert(edge.source);
}
neighbors.remove(&node_id);
if neighbors.is_empty() {
return Ok(0.0);
}
let mut total_graph_sim = 0.0;
let mut valid_neighbors = 0;
for &neighbor_id in &neighbors {
if let Ok(neighbor) = self.db.get_node(neighbor_id)
&& let Some(n_prop) = neighbor.properties.get(vector_property)
&& let Some(n_vec) = n_prop.as_vector()
{
let sim = ops::cosine_similarity(node_vec, n_vec)?;
total_graph_sim += sim;
valid_neighbors += 1;
}
}
if valid_neighbors == 0 {
return Ok(0.0);
}
let avg_graph_sim = total_graph_sim / valid_neighbors as f32;
let k = neighbors.len();
let knn_results = self
.db
.search_vectors_in(vector_property, node_vec, k + 1)?;
let mut total_knn_sim = 0.0;
let mut valid_knn = 0;
for (id, score) in knn_results {
if id == node_id {
continue; }
total_knn_sim += score;
valid_knn += 1;
if valid_knn >= k {
break;
}
}
if valid_knn == 0 {
return Ok(0.0);
}
let avg_knn_sim = total_knn_sim / valid_knn as f32;
let dissonance = avg_knn_sim - avg_graph_sim;
Ok(dissonance)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::property::PropertyMapBuilder;
use crate::index::vector::{DistanceMetric, HnswConfig};
#[test]
fn test_dissonance_high() {
let db = AletheiaDB::new().unwrap();
db.enable_vector_index("vec", HnswConfig::new(2, DistanceMetric::Cosine))
.unwrap();
let a = db
.create_node(
"X",
PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build(),
)
.unwrap();
let b = db
.create_node(
"Y",
PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build(),
)
.unwrap();
for _ in 0..5 {
db.create_node(
"X",
PropertyMapBuilder::new()
.insert_vector("vec", &[0.99, 0.01])
.build(),
)
.unwrap();
}
db.create_edge(a, b, "CONNECTED_TO", PropertyMapBuilder::new().build())
.unwrap();
let engine = DissonanceEngine::new(&db);
let dissonance = engine.calculate_dissonance(a, "vec").unwrap();
println!("High Dissonance Score: {}", dissonance);
assert!(
dissonance > 0.5,
"Expected high dissonance for orthogonal connection"
);
}
#[test]
fn test_dissonance_low() {
let db = AletheiaDB::new().unwrap();
db.enable_vector_index("vec", HnswConfig::new(2, DistanceMetric::Cosine))
.unwrap();
let a = db
.create_node(
"X",
PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build(),
)
.unwrap();
let b = db
.create_node(
"X",
PropertyMapBuilder::new()
.insert_vector("vec", &[0.99, 0.01])
.build(),
)
.unwrap();
for _ in 0..5 {
db.create_node(
"X",
PropertyMapBuilder::new()
.insert_vector("vec", &[0.98, 0.02])
.build(),
)
.unwrap();
}
db.create_edge(a, b, "CONNECTED_TO", PropertyMapBuilder::new().build())
.unwrap();
let engine = DissonanceEngine::new(&db);
let dissonance = engine.calculate_dissonance(a, "vec").unwrap();
println!("Low Dissonance Score: {}", dissonance);
assert!(
dissonance < 0.1,
"Expected low dissonance for similar connection"
);
}
}