Files
wifi-densepose/examples/exo-ai-2025/crates/exo-temporal/src/transfer_timeline.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

199 lines
6.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Phase 3 Transfer Timeline
//!
//! Records domain transfer events in the EXO temporal causal graph so the
//! system can review its own transfer history and anticipate the next
//! beneficial `(src, dst)` pair to activate.
use ruvector_domain_expansion::DomainId;
use crate::{
AnticipationHint, ConsolidationConfig, ConsolidationResult, TemporalConfig, TemporalMemory,
};
use exo_core::{Metadata, Pattern, PatternId, SubstrateTime};
const DIM: usize = 64;
// ─── embedding helpers ────────────────────────────────────────────────────────
/// FNV-1a hash of a string, normalised to [0, 1].
fn domain_hash(id: &str) -> f32 {
let mut h: u32 = 0x811c_9dc5;
for b in id.bytes() {
h ^= b as u32;
h = h.wrapping_mul(0x0100_0193);
}
h as f32 / u32::MAX as f32
}
/// Build a 64-dim pattern embedding for a transfer event.
///
/// Layout:
/// * `[0]` src domain hash (normalised)
/// * `[1]` dst domain hash (normalised)
/// * `[2]` cycle (log-normalised to [0, 1] over 1 000 cycles)
/// * `[3]` delta_reward (clamped to [0, 1])
/// * `[4..64]` sinusoidal harmonics of `(src_hash + dst_hash)`
fn build_embedding(src: &DomainId, dst: &DomainId, cycle: u64, delta_reward: f32) -> Vec<f32> {
let mut emb = vec![0.0f32; DIM];
let sh = domain_hash(&src.0);
let dh = domain_hash(&dst.0);
emb[0] = sh;
emb[1] = dh;
emb[2] = (cycle as f32).ln_1p() / (1_000.0_f32).ln_1p();
emb[3] = delta_reward.clamp(0.0, 1.0);
for i in 4..DIM {
let phase = (sh + dh) * i as f32 * std::f32::consts::PI / DIM as f32;
emb[i] = phase.sin() * 0.5 + 0.5;
}
emb
}
// ─── TransferTimeline ─────────────────────────────────────────────────────────
/// Records transfer events in the temporal causal graph and provides
/// anticipation hints for the next beneficial transfer.
pub struct TransferTimeline {
memory: TemporalMemory,
last_transfer_id: Option<PatternId>,
/// Total transfer events recorded (short-term + consolidated).
count: usize,
}
impl TransferTimeline {
/// Create with a low salience threshold so even weak transfers are kept.
pub fn new() -> Self {
let config = TemporalConfig {
consolidation: ConsolidationConfig {
salience_threshold: 0.1,
..Default::default()
},
..Default::default()
};
Self {
memory: TemporalMemory::new(config),
last_transfer_id: None,
count: 0,
}
}
/// Record a transfer event.
///
/// `delta_reward` is the improvement in arm reward after transfer
/// (`> 0` = positive transfer, `< 0` = negative transfer).
///
/// Each event is linked causally to the previous one so the temporal
/// causal graph can trace the full transfer trajectory.
pub fn record_transfer(
&mut self,
src: &DomainId,
dst: &DomainId,
cycle: u64,
delta_reward: f32,
) -> crate::Result<PatternId> {
let embedding = build_embedding(src, dst, cycle, delta_reward);
let salience = delta_reward.abs().clamp(0.1, 1.0);
let antecedents: Vec<PatternId> = self.last_transfer_id.iter().copied().collect();
let pattern = Pattern {
id: PatternId::new(),
embedding,
metadata: Metadata::default(),
timestamp: SubstrateTime::now(),
antecedents: antecedents.clone(),
salience,
};
let id = self.memory.store(pattern, &antecedents)?;
self.last_transfer_id = Some(id);
self.count += 1;
Ok(id)
}
/// Consolidate short-term transfer events to long-term memory.
pub fn consolidate(&self) -> ConsolidationResult {
self.memory.consolidate()
}
/// Return anticipation hints based on recent transfer causality.
///
/// If a previous transfer was recorded the hints suggest continuing
/// the same causal chain and sequential pattern.
pub fn anticipate_next(&self) -> Vec<AnticipationHint> {
match self.last_transfer_id {
Some(id) => vec![
AnticipationHint::CausalChain { context: id },
AnticipationHint::SequentialPattern { recent: vec![id] },
],
None => vec![],
}
}
/// Total number of transfer events recorded.
pub fn count(&self) -> usize {
self.count
}
/// Causal graph reference for advanced queries.
pub fn causal_graph(&self) -> &crate::CausalGraph {
self.memory.causal_graph()
}
}
impl Default for TransferTimeline {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_record_and_count() {
let mut tl = TransferTimeline::new();
let src = DomainId("retrieval".to_string());
let dst = DomainId("graph".to_string());
tl.record_transfer(&src, &dst, 1, 0.3).unwrap();
tl.record_transfer(&src, &dst, 2, 0.5).unwrap();
assert_eq!(tl.count(), 2);
}
#[test]
fn test_consolidate() {
let mut tl = TransferTimeline::new();
let src = DomainId("a".to_string());
let dst = DomainId("b".to_string());
for i in 0..5 {
tl.record_transfer(&src, &dst, i, 0.4).unwrap();
}
let result = tl.consolidate();
assert!(result.num_consolidated >= 1);
}
#[test]
fn test_anticipate_empty() {
let tl = TransferTimeline::new();
assert!(tl.anticipate_next().is_empty());
}
#[test]
fn test_anticipate_after_record() {
let mut tl = TransferTimeline::new();
let src = DomainId("x".to_string());
let dst = DomainId("y".to_string());
tl.record_transfer(&src, &dst, 1, 0.4).unwrap();
let hints = tl.anticipate_next();
assert!(!hints.is_empty());
}
#[test]
fn test_embedding_values() {
let src = DomainId("retrieval".to_string());
let dst = DomainId("graph".to_string());
let emb = build_embedding(&src, &dst, 42, 0.7);
assert_eq!(emb.len(), DIM);
assert!((emb[3] - 0.7).abs() < 1e-6);
}
}