use crate::AletheiaDB;
use crate::core::error::Result;
use crate::core::history::EntityHistory;
use crate::core::id::NodeId;
use crate::core::temporal::time;
#[derive(Debug, Clone)]
pub struct TemporalFingerprint {
pub bins: Vec<f32>,
pub resolution_us: i64,
}
impl TemporalFingerprint {
pub fn similarity(&self, other: &Self) -> f32 {
if self.bins.len() != other.bins.len() {
panic!(
"Fingerprint length mismatch: {} vs {}",
self.bins.len(),
other.bins.len()
);
}
let dot_product: f32 = self
.bins
.iter()
.zip(other.bins.iter())
.map(|(a, b)| a * b)
.sum();
dot_product.clamp(0.0, 1.0)
}
}
pub trait Resonator {
fn resonate(&self, history: &EntityHistory) -> TemporalFingerprint;
}
pub struct ActivityDensityResonator {
pub window_size_us: i64,
pub num_bins: usize,
}
impl Default for ActivityDensityResonator {
fn default() -> Self {
Self {
window_size_us: 3600 * 1_000_000, num_bins: 60, }
}
}
impl Resonator for ActivityDensityResonator {
fn resonate(&self, history: &EntityHistory) -> TemporalFingerprint {
let mut bins = vec![0.0; self.num_bins];
let bin_size_us = self.window_size_us / self.num_bins as i64;
let now = time::now().wallclock();
let start_time = now - self.window_size_us;
for version in &history.versions {
let ts = version.temporal.valid_time().start().wallclock();
if ts >= start_time && ts <= now {
let offset = ts - start_time;
let bin_idx = (offset / bin_size_us) as usize;
if bin_idx < self.num_bins {
bins[bin_idx] += 1.0;
}
}
}
let magnitude: f32 = bins.iter().map(|x| x * x).sum::<f32>().sqrt();
if magnitude > 0.0 {
for x in &mut bins {
*x /= magnitude;
}
}
TemporalFingerprint {
bins,
resolution_us: bin_size_us,
}
}
}
#[cfg(feature = "semantic-temporal")]
pub struct EchoChamber<'a> {
db: &'a AletheiaDB,
resonator: Box<dyn Resonator>,
}
#[cfg(not(feature = "semantic-temporal"))]
#[deprecated(
note = "EchoChamber requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
pub struct EchoChamber<'a> {
_marker: std::marker::PhantomData<&'a AletheiaDB>,
}
#[cfg(feature = "semantic-temporal")]
impl<'a> EchoChamber<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self {
db,
resonator: Box::new(ActivityDensityResonator::default()),
}
}
pub fn with_resonator<R: Resonator + 'static>(mut self, resonator: R) -> Self {
self.resonator = Box::new(resonator);
self
}
pub fn find_echoes(&self, target: NodeId, candidates: &[NodeId]) -> Result<Vec<(NodeId, f32)>> {
let target_history = self.db.get_node_history(target)?;
let target_fp = self.resonator.resonate(&target_history);
let mut results = Vec::with_capacity(candidates.len());
for &candidate_id in candidates {
if candidate_id == target {
continue;
}
if let Ok(history) = self.db.get_node_history(candidate_id) {
let fp = self.resonator.resonate(&history);
let score = target_fp.similarity(&fp);
if score > 0.0 {
results.push((candidate_id, score));
}
}
}
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
Ok(results)
}
}
#[cfg(not(feature = "semantic-temporal"))]
#[allow(deprecated)]
impl<'a> EchoChamber<'a> {
#[allow(unused_variables)]
#[track_caller]
pub fn new(db: &'a AletheiaDB) -> Self {
panic!(
"EchoChamber requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
);
}
#[allow(unused_variables)]
#[track_caller]
pub fn with_resonator<R: Resonator + 'static>(self, resonator: R) -> Self {
panic!(
"EchoChamber requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
);
}
#[allow(unused_variables)]
#[track_caller]
pub fn find_echoes(&self, target: NodeId, candidates: &[NodeId]) -> Result<Vec<(NodeId, f32)>> {
panic!(
"EchoChamber requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
);
}
}
#[cfg(all(test, feature = "semantic-temporal"))]
mod tests {
use super::*;
use crate::api::transaction::WriteOps;
use crate::core::hlc::HybridTimestamp;
use crate::core::property::PropertyMapBuilder;
use crate::core::temporal::{Timestamp, time};
#[test]
fn test_temporal_fingerprint_similarity() {
let fp1 = TemporalFingerprint {
bins: vec![1.0, 0.0], resolution_us: 1000,
};
let fp2 = TemporalFingerprint {
bins: vec![0.0, 1.0], resolution_us: 1000,
};
let fp3 = TemporalFingerprint {
bins: vec![
std::f32::consts::FRAC_1_SQRT_2,
std::f32::consts::FRAC_1_SQRT_2,
], resolution_us: 1000,
};
assert!(fp1.similarity(&fp1) > 0.99); assert!(fp1.similarity(&fp2) < 0.01); assert!(fp1.similarity(&fp3) > 0.6 && fp1.similarity(&fp3) < 0.8); }
#[test]
fn test_echo_chamber_integration() {
let db = AletheiaDB::new().unwrap();
let now_wallclock = time::now().wallclock();
let window = 100 * 1_000_000;
let t_minus_80 = HybridTimestamp::new(now_wallclock - 80 * 1_000_000, 0).unwrap();
let t_minus_50 = HybridTimestamp::new(now_wallclock - 50 * 1_000_000, 0).unwrap();
let t_minus_20 = HybridTimestamp::new(now_wallclock - 20 * 1_000_000, 0).unwrap();
let props = PropertyMapBuilder::new().insert("val", 0).build();
let create_node_with_history = |timestamps: Vec<Timestamp>| -> NodeId {
assert!(
!timestamps.is_empty(),
"history requires at least one timestamp"
);
let id = {
let mut tx = db.write_transaction().unwrap();
let id = tx
.create_node_with_valid_time("Node", props.clone(), Some(timestamps[0]))
.unwrap();
tx.commit().unwrap();
id
};
for &ts in ×tamps[1..] {
let mut tx = db.write_transaction().unwrap();
tx.update_node_with_valid_time(id, props.clone(), Some(ts))
.unwrap();
tx.commit().unwrap();
}
id
};
let node_a = create_node_with_history(vec![t_minus_80, t_minus_50]);
let node_b = create_node_with_history(vec![t_minus_80, t_minus_50]);
let node_c = create_node_with_history(vec![t_minus_20]);
let resonator = ActivityDensityResonator {
window_size_us: window,
num_bins: 10,
};
let chamber = EchoChamber::new(&db).with_resonator(resonator);
let echoes = chamber.find_echoes(node_a, &[node_b, node_c]).unwrap();
assert_eq!(echoes.len(), 1, "Should only return non-zero similarity");
let (id, score) = echoes[0];
assert_eq!(id, node_b);
assert!(
score > 0.9,
"Node B should resonate strongly (score: {})",
score
);
let c_score = echoes.iter().find(|(id, _)| *id == node_c);
if let Some((_, score)) = c_score {
assert!(
*score < 0.1,
"Node C should not resonate (score: {})",
score
);
}
}
}
#[cfg(all(test, not(feature = "semantic-temporal")))]
#[allow(deprecated)]
mod stub_tests {
use super::*;
#[test]
#[should_panic(
expected = "EchoChamber requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
fn test_stub_panic_on_new() {
let db = AletheiaDB::new().unwrap();
let _ = EchoChamber::new(&db);
}
#[test]
#[should_panic(
expected = "EchoChamber requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
fn test_stub_panic_on_with_resonator() {
let chamber = EchoChamber {
_marker: std::marker::PhantomData,
};
let resonator = ActivityDensityResonator::default();
let _ = chamber.with_resonator(resonator);
}
#[test]
#[should_panic(
expected = "EchoChamber requires the 'nova' feature. Add 'features = [\"nova\"]' to your Cargo.toml."
)]
fn test_stub_panic_on_find_echoes() {
let chamber = EchoChamber {
_marker: std::marker::PhantomData,
};
let _ = chamber.find_echoes(NodeId::new(0).unwrap(), &[]);
}
}