use crate::AletheiaDB;
use crate::core::error::Result;
use crate::core::id::NodeId;
use crate::core::temporal::{TimeRange, time};
use crate::core::vector::ops::euclidean_distance;
#[derive(Debug, Clone, Copy)]
pub struct ThermalReading {
pub volatility: f32,
pub temperature: f32,
pub updates: usize,
}
pub struct Thermos<'a> {
db: &'a AletheiaDB,
}
impl<'a> Thermos<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn measure_node(
&self,
node_id: NodeId,
window: TimeRange,
property: &str,
) -> Result<ThermalReading> {
let history = self.db.get_node_history(node_id)?;
let mut vectors: Vec<(i64, Vec<f32>)> = Vec::new();
for version in history.versions {
if !version.temporal.valid_time().overlaps(&window) {
continue;
}
if let Some(vec) = version.properties.get(property).and_then(|v| v.as_vector()) {
let timestamp = version.temporal.valid_time().start().wallclock();
vectors.push((timestamp, vec.to_vec()));
}
}
vectors.sort_by_key(|(t, _)| *t);
if vectors.len() < 2 {
return Ok(ThermalReading {
volatility: 0.0,
temperature: 0.0,
updates: vectors.len(),
});
}
let mut total_distance = 0.0;
let update_count = vectors.len();
for i in 0..vectors.len() - 1 {
let (_, v1) = &vectors[i];
let (_, v2) = &vectors[i + 1];
let dist = euclidean_distance(v1, v2)?;
total_distance += dist;
}
let duration_secs = if let Some(micros) = window.duration_micros() {
micros as f32 / 1_000_000.0
} else {
let start_micros = window.start().wallclock();
let end_micros = time::now().wallclock();
(end_micros - start_micros).max(0) as f32 / 1_000_000.0
};
let temperature = if duration_secs > 1e-6 {
total_distance / duration_secs
} else {
0.0
};
Ok(ThermalReading {
volatility: total_distance,
temperature,
updates: update_count,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::transaction::WriteOps;
use crate::core::property::PropertyMapBuilder;
use crate::index::vector::{DistanceMetric, HnswConfig};
#[test]
fn test_thermos_basic_movement() {
let db = AletheiaDB::new().unwrap();
let config = HnswConfig::new(2, DistanceMetric::Euclidean);
db.enable_vector_index("vec", config).unwrap();
let t0 = time::now();
let props = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 0.0])
.build();
let node = db.create_node("Point", props).unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
db.write(|tx| {
tx.update_node(
node,
PropertyMapBuilder::new()
.insert_vector("vec", &[3.0, 0.0])
.build(),
)
})
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
db.write(|tx| {
tx.update_node(
node,
PropertyMapBuilder::new()
.insert_vector("vec", &[3.0, 4.0])
.build(),
)
})
.unwrap();
let t_end = time::now();
let thermos = Thermos::new(&db);
let window = TimeRange::new(t0, t_end).unwrap();
let reading = thermos.measure_node(node, window, "vec").unwrap();
assert!(
(reading.volatility - 7.0).abs() < 1e-5,
"Volatility should be 7.0, got {}",
reading.volatility
);
assert!(reading.temperature > 0.0);
assert_eq!(reading.updates, 3);
}
#[test]
fn test_thermos_static_node() {
let db = AletheiaDB::new().unwrap();
let t0 = time::now();
let props = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 1.0])
.build();
let node = db.create_node("Point", props).unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
let t_end = time::now();
let thermos = Thermos::new(&db);
let window = TimeRange::new(t0, t_end).unwrap();
let reading = thermos.measure_node(node, window, "vec").unwrap();
assert_eq!(reading.volatility, 0.0);
assert_eq!(reading.temperature, 0.0);
assert_eq!(reading.updates, 1); }
#[test]
fn test_thermos_no_vector_property() {
let db = AletheiaDB::new().unwrap();
let t0 = time::now();
let props = PropertyMapBuilder::new().insert("name", "Bob").build();
let node = db.create_node("Person", props).unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
let t_end = time::now();
let thermos = Thermos::new(&db);
let window = TimeRange::new(t0, t_end).unwrap();
let reading = thermos.measure_node(node, window, "embedding").unwrap();
assert_eq!(reading.volatility, 0.0);
assert_eq!(reading.updates, 0);
}
}