use crate::AletheiaDB;
use crate::core::error::Result;
use crate::core::id::NodeId;
use crate::core::temporal::TimeRange;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq)]
pub struct FossilResult {
pub node_displacement: f32,
pub context_displacement: f32,
pub fossil_index: f32,
}
pub struct FossilDetector<'a> {
db: &'a AletheiaDB,
}
impl<'a> FossilDetector<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn detect_fossil(
&self,
node_id: NodeId,
window: TimeRange,
property_name: &str,
) -> Result<FossilResult> {
let node_displacement = self.calculate_displacement(node_id, window, property_name)?;
let mut neighbors = HashSet::new();
let outgoing = self.db.get_outgoing_edges(node_id);
for edge_id in outgoing {
if let Ok(target) = self.db.get_edge_target(edge_id) {
neighbors.insert(target);
}
}
let incoming = self.db.get_incoming_edges(node_id);
for edge_id in incoming {
if let Ok(source) = self.db.get_edge_source(edge_id) {
neighbors.insert(source);
}
}
let mut context_displacement_sum = 0.0;
let mut valid_neighbors = 0;
for neighbor in neighbors {
if let Ok(disp) = self.calculate_displacement(neighbor, window, property_name) {
context_displacement_sum += disp;
valid_neighbors += 1;
}
}
let context_displacement = if valid_neighbors > 0 {
context_displacement_sum / valid_neighbors as f32
} else {
0.0
};
let epsilon = 1e-6_f32; let fossil_index = context_displacement / (node_displacement + epsilon);
Ok(FossilResult {
node_displacement,
context_displacement,
fossil_index,
})
}
fn calculate_displacement(
&self,
node_id: NodeId,
window: TimeRange,
property_name: &str,
) -> Result<f32> {
let start_state = self
.db
.get_nodes_at_time(&[node_id], window.start(), window.start())?;
let end_state = self
.db
.get_nodes_at_time(&[node_id], window.end(), window.end())?;
let start_vec = start_state
.first()
.and_then(|(_, opt)| opt.as_ref())
.and_then(|node| node.get_property(property_name))
.and_then(|prop| prop.as_vector());
let end_vec = end_state
.first()
.and_then(|(_, opt)| opt.as_ref())
.and_then(|node| node.get_property(property_name))
.and_then(|prop| prop.as_vector());
match (start_vec, end_vec) {
(Some(s_vec), Some(e_vec)) => {
if s_vec.len() != e_vec.len() {
return Err(crate::core::error::Error::other(
"Dimension mismatch between start and end vectors",
));
}
let mut dist_sq = 0.0;
for i in 0..s_vec.len() {
let diff = e_vec[i] - s_vec[i];
dist_sq += diff * diff;
}
Ok(dist_sq.sqrt())
}
_ => {
Err(crate::core::error::Error::other(
"Node missing vector property at start or end of window",
))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::transaction::WriteOps;
use crate::core::property::PropertyMapBuilder;
use crate::core::temporal::time;
use std::time::Duration;
#[test]
fn test_fossil_detection() {
let db = AletheiaDB::new().unwrap();
let mut fossil_node = NodeId::new(0).unwrap();
let mut neighbor_a = NodeId::new(0).unwrap();
let mut neighbor_b = NodeId::new(0).unwrap();
db.write(|tx| {
fossil_node = tx
.create_node(
"Concept",
PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 0.0])
.build(),
)
.unwrap();
neighbor_a = tx
.create_node(
"Concept",
PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build(),
)
.unwrap();
neighbor_b = tx
.create_node(
"Concept",
PropertyMapBuilder::new()
.insert_vector("vec", &[-1.0, 0.0])
.build(),
)
.unwrap();
tx.create_edge(fossil_node, neighbor_a, "LINK", Default::default())
.unwrap();
tx.create_edge(fossil_node, neighbor_b, "LINK", Default::default())
.unwrap();
Ok::<(), crate::core::error::Error>(())
})
.unwrap();
std::thread::sleep(Duration::from_millis(10));
let time_start = time::now();
std::thread::sleep(Duration::from_millis(50));
db.write(|tx| {
tx.update_node(
neighbor_a,
PropertyMapBuilder::new()
.insert_vector("vec", &[10.0, 0.0]) .build(),
)
.unwrap();
tx.update_node(
neighbor_b,
PropertyMapBuilder::new()
.insert_vector("vec", &[-10.0, 0.0]) .build(),
)
.unwrap();
Ok::<(), crate::core::error::Error>(())
})
.unwrap();
let time_end = time::now();
let window = TimeRange::new(time_start, time_end).unwrap();
let detector = FossilDetector::new(&db);
let result = detector.detect_fossil(fossil_node, window, "vec").unwrap();
assert!(
result.node_displacement < 0.1,
"Node displacement should be ~0"
);
assert!(
result.context_displacement > 8.0,
"Context displacement should be ~9"
);
assert!(result.fossil_index > 1.0, "Fossil index should be high");
}
}