use crate::AletheiaDB;
use crate::core::error::{Error, Result};
use crate::core::temporal::Timestamp;
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub struct TremorScore {
pub centroid_a: Vec<f32>,
pub centroid_b: Vec<f32>,
pub shift_magnitude: f32,
pub nodes_analyzed_a: usize,
pub nodes_analyzed_b: usize,
}
pub struct TremorEngine<'a> {
db: &'a AletheiaDB,
}
impl<'a> TremorEngine<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn detect_shift(
&self,
time_a: Timestamp,
time_b: Timestamp,
property_name: &str,
) -> Result<TremorScore> {
let mut all_node_ids = HashSet::new();
let results = self.db.query().scan(None).execute(self.db)?;
for row in results {
let row = row?;
if let Some(node) = row.entity.as_node() {
all_node_ids.insert(node.id);
}
}
let node_ids: Vec<_> = all_node_ids.into_iter().collect();
let (centroid_a, count_a) = self.calculate_centroid_at(&node_ids, time_a, property_name)?;
let (centroid_b, count_b) = self.calculate_centroid_at(&node_ids, time_b, property_name)?;
if count_a == 0 || count_b == 0 {
return Err(Error::other(
"Insufficient data to calculate centroids (0 valid vectors found in one or both time windows).",
));
}
if centroid_a.len() != centroid_b.len() {
return Err(Error::other("Dimension mismatch between time windows."));
}
let shift_magnitude = centroid_a
.iter()
.zip(centroid_b.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f32>()
.sqrt();
Ok(TremorScore {
centroid_a,
centroid_b,
shift_magnitude,
nodes_analyzed_a: count_a,
nodes_analyzed_b: count_b,
})
}
fn calculate_centroid_at(
&self,
node_ids: &[crate::core::id::NodeId],
time: Timestamp,
property_name: &str,
) -> Result<(Vec<f32>, usize)> {
let mut sum_vector = Vec::new();
let mut count = 0;
let nodes_at_time = self.db.get_nodes_at_time(node_ids, time, time)?;
for (_, node_opt) in nodes_at_time {
if let Some(node) = node_opt
&& let Some(prop) = node.get_property(property_name)
&& let Some(vec) = prop.as_vector()
{
if sum_vector.is_empty() {
sum_vector = vec![0.0; vec.len()];
}
if vec.len() == sum_vector.len() {
for i in 0..vec.len() {
sum_vector[i] += vec[i];
}
count += 1;
}
}
}
if count > 0 {
for v in &mut sum_vector {
*v /= count as f32;
}
}
Ok((sum_vector, count))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::transaction::WriteOps;
use crate::core::property::PropertyMapBuilder;
#[test]
fn test_tremor_shift_detection() {
let db = AletheiaDB::new().unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
let mut n1 = crate::core::id::NodeId::new(0).unwrap();
let mut n2 = crate::core::id::NodeId::new(0).unwrap();
db.write(|tx| {
n1 = tx
.create_node(
"Concept",
PropertyMapBuilder::new()
.insert_vector("vec", &[-0.1, 0.0])
.build(),
)
.unwrap();
n2 = tx
.create_node(
"Concept",
PropertyMapBuilder::new()
.insert_vector("vec", &[0.1, 0.0])
.build(),
)
.unwrap();
Ok::<(), Error>(())
})
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
let time_a = crate::core::temporal::time::now();
std::thread::sleep(std::time::Duration::from_millis(50));
db.write(|tx| {
tx.update_node(
n1,
PropertyMapBuilder::new()
.insert_vector("vec", &[9.9, 10.0])
.build(),
)
.unwrap();
tx.update_node(
n2,
PropertyMapBuilder::new()
.insert_vector("vec", &[10.1, 10.0])
.build(),
)
.unwrap();
Ok::<(), Error>(())
})
.unwrap();
let time_b = crate::core::temporal::time::now();
let engine = TremorEngine::new(&db);
let score = engine.detect_shift(time_a, time_b, "vec").unwrap();
assert!((score.centroid_a[0] - 0.0).abs() < 0.1);
assert!((score.centroid_a[1] - 0.0).abs() < 0.1);
assert!((score.centroid_b[0] - 10.0).abs() < 0.1);
assert!((score.centroid_b[1] - 10.0).abs() < 0.1);
assert!((score.shift_magnitude - 14.14).abs() < 0.1);
assert_eq!(score.nodes_analyzed_a, 2);
assert_eq!(score.nodes_analyzed_b, 2);
}
#[test]
fn test_tremor_no_shift() {
let db = AletheiaDB::new().unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
let mut n1 = crate::core::id::NodeId::new(0).unwrap();
db.write(|tx| {
n1 = tx
.create_node(
"Concept",
PropertyMapBuilder::new()
.insert_vector("vec", &[5.0, 5.0])
.build(),
)
.unwrap();
Ok::<(), Error>(())
})
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
let time_a = crate::core::temporal::time::now();
std::thread::sleep(std::time::Duration::from_millis(50));
db.write(|tx| {
tx.update_node(
n1,
PropertyMapBuilder::new()
.insert_vector("vec", &[5.0, 5.0])
.insert("status", "unchanged")
.build(),
)
.unwrap();
Ok::<(), Error>(())
})
.unwrap();
let time_b = crate::core::temporal::time::now();
let engine = TremorEngine::new(&db);
let score = engine.detect_shift(time_a, time_b, "vec").unwrap();
assert!(score.shift_magnitude < 0.01);
}
}