use crate::AletheiaDB;
use crate::core::error::Result;
use crate::core::id::NodeId;
use crate::core::temporal::{TimeRange, Timestamp, time};
use crate::core::vector::{magnitude, normalize};
#[derive(Debug, Clone, PartialEq)]
pub struct OrbitMetrics {
pub neighbor_id: NodeId,
pub start_distance: Option<f32>,
pub end_distance: Option<f32>,
pub velocity: Option<f32>,
}
pub struct GravityWell<'a> {
db: &'a AletheiaDB,
}
impl<'a> GravityWell<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn analyze_orbit(
&self,
center_id: NodeId,
property: &str,
window: TimeRange,
) -> Result<Vec<OrbitMetrics>> {
let tx_time = time::now();
let edge_ids = self
.db
.get_outgoing_edges_at_time(center_id, window.end(), tx_time);
if edge_ids.is_empty() {
return Ok(Vec::new());
}
let edges = self
.db
.get_edges_at_time(&edge_ids, window.end(), tx_time)?;
let target_ids: Vec<NodeId> = edges
.into_iter()
.filter_map(|(_, edge_opt)| edge_opt.map(|e| e.target))
.collect();
let center_start_vec = self.get_vector_at(center_id, property, window.start(), tx_time)?;
let center_end_vec = self.get_vector_at(center_id, property, window.end(), tx_time)?;
let duration_secs = window.duration_micros().unwrap_or(0) as f32 / 1_000_000.0;
let mut metrics = Vec::with_capacity(target_ids.len());
for target_id in target_ids {
let target_start_vec =
self.get_vector_at(target_id, property, window.start(), tx_time)?;
let target_end_vec = self.get_vector_at(target_id, property, window.end(), tx_time)?;
let start_dist = if let (Some(c), Some(t)) = (¢er_start_vec, &target_start_vec) {
Some(cosine_distance(c, t))
} else {
None
};
let end_dist = if let (Some(c), Some(t)) = (¢er_end_vec, &target_end_vec) {
Some(cosine_distance(c, t))
} else {
None
};
let velocity = if let (Some(s), Some(e)) = (start_dist, end_dist) {
if duration_secs > 0.0 {
Some((e - s) / duration_secs)
} else {
Some(0.0)
}
} else {
None
};
metrics.push(OrbitMetrics {
neighbor_id: target_id,
start_distance: start_dist,
end_distance: end_dist,
velocity,
});
}
Ok(metrics)
}
fn get_vector_at(
&self,
node_id: NodeId,
property: &str,
valid_time: Timestamp,
tx_time: Timestamp,
) -> Result<Option<Vec<f32>>> {
if let Ok(history) = self.db.get_node_history(node_id) {
let mut best_match: Option<Vec<f32>> = None;
for version in history.versions {
if version.temporal.is_valid_at(valid_time)
&& let Some(val) = version.properties.get(property)
&& let Some(vec) = val.as_vector()
{
best_match = Some(vec.to_vec());
}
}
if let Some(vec) = best_match {
return Ok(Some(vec));
}
}
if let Ok(node) = self.db.get_node_at_time(node_id, valid_time, tx_time) {
return Self::extract_vector(&node, property);
}
if let Ok(node) = self.db.get_node(node_id) {
return Self::extract_vector(&node, property);
}
Ok(None)
}
fn extract_vector(node: &crate::core::graph::Node, property: &str) -> Result<Option<Vec<f32>>> {
if let Some(val) = node.properties.get(property)
&& let Some(vec) = val.as_vector()
{
return Ok(Some(vec.to_vec()));
}
Ok(None)
}
}
pub struct GravitySimulator<'a> {
db: &'a AletheiaDB,
}
impl<'a> GravitySimulator<'a> {
pub fn new(db: &'a AletheiaDB) -> Self {
Self { db }
}
pub fn simulate_pull(
&self,
center_id: NodeId,
property: &str,
mass: f32,
step_size: f32,
) -> Result<Vec<(NodeId, Vec<f32>)>> {
let center_node = self.db.get_node(center_id)?;
let center_vec = match center_node
.properties
.get(property)
.and_then(|v| v.as_vector())
{
Some(v) => v,
None => return Ok(Vec::new()), };
let edge_ids = self.db.get_outgoing_edges(center_id);
if edge_ids.is_empty() {
return Ok(Vec::new());
}
let mut proposed_updates = Vec::new();
for edge_id in edge_ids {
let target_id = match self.db.get_edge_target(edge_id) {
Ok(t) => t,
Err(_) => continue,
};
let target_node = match self.db.get_node(target_id) {
Ok(n) => n,
Err(_) => continue,
};
let target_vec = match target_node
.properties
.get(property)
.and_then(|v| v.as_vector())
{
Some(v) => v,
None => continue,
};
if center_vec.len() != target_vec.len() {
continue;
}
let mut diff = Vec::with_capacity(center_vec.len());
for (c, t) in center_vec.iter().zip(target_vec.iter()) {
diff.push(c - t);
}
let dist = magnitude(&diff);
let effective_dist = dist.max(0.0001);
let force = mass / (effective_dist * effective_dist + 0.1);
let scale = (force * step_size) / effective_dist;
let mut new_vec = Vec::with_capacity(target_vec.len());
for (t, d) in target_vec.iter().zip(diff.iter()) {
new_vec.push(t + d * scale);
}
let normalized_vec = normalize(&new_vec);
proposed_updates.push((target_id, normalized_vec));
}
Ok(proposed_updates)
}
}
fn cosine_distance(a: &[f32], b: &[f32]) -> f32 {
crate::core::vector::cosine_similarity(a, b)
.map(|sim| 1.0 - sim)
.unwrap_or(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::transaction::WriteOps;
use crate::core::property::PropertyMapBuilder;
#[test]
fn test_gravity_attraction() {
let db = AletheiaDB::new().unwrap();
let center_props = PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build();
let center = db.create_node("Sun", center_props).unwrap();
let neighbor_props = PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build();
let neighbor = db.create_node("Planet", neighbor_props).unwrap();
db.create_edge(
center,
neighbor,
"ORBITS",
PropertyMapBuilder::new().build(),
)
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
let t_start = time::now();
std::thread::sleep(std::time::Duration::from_millis(50));
let update_props = PropertyMapBuilder::new()
.insert_vector("vec", &[0.707, 0.707])
.build();
db.write(|tx| tx.update_node(neighbor, update_props))
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
let t_end = time::now();
let gravity = GravityWell::new(&db);
let window = TimeRange::new(t_start, t_end).unwrap();
let metrics = gravity.analyze_orbit(center, "vec", window).unwrap();
assert_eq!(metrics.len(), 1);
let m = &metrics[0];
assert!(m.start_distance.is_some(), "Start distance is None");
assert!(m.end_distance.is_some(), "End distance is None");
assert!(
m.start_distance.unwrap() > 0.9,
"Start distance should be high"
);
assert!(
m.end_distance.unwrap() < 0.4,
"End distance should be lower"
);
assert!(
m.velocity.unwrap() < 0.0,
"Velocity should be negative (attraction)"
);
}
#[test]
fn test_gravity_simulator() {
let db = AletheiaDB::new().unwrap();
let center = db
.create_node(
"Sun",
PropertyMapBuilder::new()
.insert_vector("vec", &[1.0, 0.0])
.build(),
)
.unwrap();
let neighbor = db
.create_node(
"Planet",
PropertyMapBuilder::new()
.insert_vector("vec", &[0.0, 1.0])
.build(),
)
.unwrap();
db.create_edge(
center,
neighbor,
"ORBITS",
PropertyMapBuilder::new().build(),
)
.unwrap();
let sim = GravitySimulator::new(&db);
let updates = sim.simulate_pull(center, "vec", 10.0, 0.1).unwrap();
assert_eq!(updates.len(), 1);
let (id, new_vec) = &updates[0];
assert_eq!(*id, neighbor);
assert!(
new_vec[0] > 0.1,
"X component should increase (got {})",
new_vec[0]
);
assert!(
new_vec[1] < 0.95,
"Y component should decrease (got {})",
new_vec[1]
);
let mag = new_vec.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((mag - 1.0).abs() < 1e-5, "Vector should be normalized");
}
}