Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user