Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
618
vendor/ruvector/crates/ruvector-crv/src/session.rs
vendored
Normal file
618
vendor/ruvector/crates/ruvector-crv/src/session.rs
vendored
Normal file
@@ -0,0 +1,618 @@
|
||||
//! CRV Session Manager
|
||||
//!
|
||||
//! Manages CRV sessions as directed acyclic graphs (DAGs), where each session
|
||||
//! progresses through stages I-VI. Provides cross-session convergence analysis
|
||||
//! to find agreement between multiple viewers targeting the same coordinate.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! Each session is a DAG of stage entries. Cross-session convergence is computed
|
||||
//! by finding entries with high embedding similarity across different sessions
|
||||
//! targeting the same coordinate.
|
||||
|
||||
use crate::error::{CrvError, CrvResult};
|
||||
use crate::stage_i::StageIEncoder;
|
||||
use crate::stage_ii::StageIIEncoder;
|
||||
use crate::stage_iii::StageIIIEncoder;
|
||||
use crate::stage_iv::StageIVEncoder;
|
||||
use crate::stage_v::StageVEngine;
|
||||
use crate::stage_vi::StageVIModeler;
|
||||
use crate::types::*;
|
||||
use ruvector_gnn::search::cosine_similarity;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A session entry stored in the session graph.
|
||||
#[derive(Debug, Clone)]
|
||||
struct SessionEntry {
|
||||
/// The stage data embedding.
|
||||
embedding: Vec<f32>,
|
||||
/// Stage number (1-6).
|
||||
stage: u8,
|
||||
/// Entry index within the stage.
|
||||
entry_index: usize,
|
||||
/// Metadata.
|
||||
metadata: HashMap<String, serde_json::Value>,
|
||||
/// Timestamp.
|
||||
timestamp_ms: u64,
|
||||
}
|
||||
|
||||
/// A complete CRV session with all stage data.
|
||||
#[derive(Debug)]
|
||||
struct Session {
|
||||
/// Session identifier.
|
||||
id: SessionId,
|
||||
/// Target coordinate.
|
||||
coordinate: TargetCoordinate,
|
||||
/// Entries organized by stage.
|
||||
entries: Vec<SessionEntry>,
|
||||
}
|
||||
|
||||
/// CRV Session Manager: coordinates all stage encoders and manages sessions.
|
||||
#[derive(Debug)]
|
||||
pub struct CrvSessionManager {
|
||||
/// Configuration.
|
||||
config: CrvConfig,
|
||||
/// Stage I encoder.
|
||||
stage_i: StageIEncoder,
|
||||
/// Stage II encoder.
|
||||
stage_ii: StageIIEncoder,
|
||||
/// Stage III encoder.
|
||||
stage_iii: StageIIIEncoder,
|
||||
/// Stage IV encoder.
|
||||
stage_iv: StageIVEncoder,
|
||||
/// Stage V engine.
|
||||
stage_v: StageVEngine,
|
||||
/// Stage VI modeler.
|
||||
stage_vi: StageVIModeler,
|
||||
/// Active sessions indexed by session ID.
|
||||
sessions: HashMap<SessionId, Session>,
|
||||
}
|
||||
|
||||
impl CrvSessionManager {
|
||||
/// Create a new session manager with the given configuration.
|
||||
pub fn new(config: CrvConfig) -> Self {
|
||||
let stage_i = StageIEncoder::new(&config);
|
||||
let stage_ii = StageIIEncoder::new(&config);
|
||||
let stage_iii = StageIIIEncoder::new(&config);
|
||||
let stage_iv = StageIVEncoder::new(&config);
|
||||
let stage_v = StageVEngine::new(&config);
|
||||
let stage_vi = StageVIModeler::new(&config);
|
||||
|
||||
Self {
|
||||
config,
|
||||
stage_i,
|
||||
stage_ii,
|
||||
stage_iii,
|
||||
stage_iv,
|
||||
stage_v,
|
||||
stage_vi,
|
||||
sessions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new session for a given target coordinate.
|
||||
pub fn create_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
coordinate: TargetCoordinate,
|
||||
) -> CrvResult<()> {
|
||||
if self.sessions.contains_key(&session_id) {
|
||||
return Err(CrvError::EncodingError(format!(
|
||||
"Session {} already exists",
|
||||
session_id
|
||||
)));
|
||||
}
|
||||
|
||||
self.sessions.insert(
|
||||
session_id.clone(),
|
||||
Session {
|
||||
id: session_id,
|
||||
coordinate,
|
||||
entries: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add Stage I data to a session.
|
||||
pub fn add_stage_i(&mut self, session_id: &str, data: &StageIData) -> CrvResult<Vec<f32>> {
|
||||
let embedding = self.stage_i.encode(data)?;
|
||||
self.add_entry(session_id, 1, embedding.clone(), HashMap::new())?;
|
||||
Ok(embedding)
|
||||
}
|
||||
|
||||
/// Add Stage II data to a session.
|
||||
pub fn add_stage_ii(&mut self, session_id: &str, data: &StageIIData) -> CrvResult<Vec<f32>> {
|
||||
let embedding = self.stage_ii.encode(data)?;
|
||||
self.add_entry(session_id, 2, embedding.clone(), HashMap::new())?;
|
||||
Ok(embedding)
|
||||
}
|
||||
|
||||
/// Add Stage III data to a session.
|
||||
pub fn add_stage_iii(&mut self, session_id: &str, data: &StageIIIData) -> CrvResult<Vec<f32>> {
|
||||
let embedding = self.stage_iii.encode(data)?;
|
||||
self.add_entry(session_id, 3, embedding.clone(), HashMap::new())?;
|
||||
Ok(embedding)
|
||||
}
|
||||
|
||||
/// Add Stage IV data to a session.
|
||||
pub fn add_stage_iv(&mut self, session_id: &str, data: &StageIVData) -> CrvResult<Vec<f32>> {
|
||||
let embedding = self.stage_iv.encode(data)?;
|
||||
self.add_entry(session_id, 4, embedding.clone(), HashMap::new())?;
|
||||
Ok(embedding)
|
||||
}
|
||||
|
||||
/// Run Stage V interrogation on a session.
|
||||
///
|
||||
/// Probes the accumulated session data with specified queries.
|
||||
pub fn run_stage_v(
|
||||
&mut self,
|
||||
session_id: &str,
|
||||
probe_queries: &[(&str, u8, Vec<f32>)], // (query text, target stage, query embedding)
|
||||
k: usize,
|
||||
) -> CrvResult<StageVData> {
|
||||
let session = self
|
||||
.sessions
|
||||
.get(session_id)
|
||||
.ok_or_else(|| CrvError::SessionNotFound(session_id.to_string()))?;
|
||||
|
||||
let all_embeddings: Vec<Vec<f32>> = session
|
||||
.entries
|
||||
.iter()
|
||||
.map(|e| e.embedding.clone())
|
||||
.collect();
|
||||
|
||||
let mut probes = Vec::new();
|
||||
let mut cross_refs = Vec::new();
|
||||
|
||||
for (query_text, target_stage, query_emb) in probe_queries {
|
||||
// Filter candidates to the target stage
|
||||
let stage_entries: Vec<Vec<f32>> = session
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| e.stage == *target_stage)
|
||||
.map(|e| e.embedding.clone())
|
||||
.collect();
|
||||
|
||||
if stage_entries.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut probe = self.stage_v.probe(query_emb, &stage_entries, k)?;
|
||||
probe.query = query_text.to_string();
|
||||
probe.target_stage = *target_stage;
|
||||
probes.push(probe);
|
||||
}
|
||||
|
||||
// Cross-reference between all stage pairs
|
||||
for from_stage in 1..=4u8 {
|
||||
for to_stage in (from_stage + 1)..=4u8 {
|
||||
let from_entries: Vec<Vec<f32>> = session
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| e.stage == from_stage)
|
||||
.map(|e| e.embedding.clone())
|
||||
.collect();
|
||||
let to_entries: Vec<Vec<f32>> = session
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| e.stage == to_stage)
|
||||
.map(|e| e.embedding.clone())
|
||||
.collect();
|
||||
|
||||
if !from_entries.is_empty() && !to_entries.is_empty() {
|
||||
let refs = self.stage_v.cross_reference(
|
||||
from_stage,
|
||||
&from_entries,
|
||||
to_stage,
|
||||
&to_entries,
|
||||
self.config.convergence_threshold,
|
||||
);
|
||||
cross_refs.extend(refs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stage_v_data = StageVData {
|
||||
probes,
|
||||
cross_references: cross_refs,
|
||||
};
|
||||
|
||||
// Encode Stage V result and add to session
|
||||
if !stage_v_data.probes.is_empty() {
|
||||
let embedding = self.stage_v.encode(&stage_v_data, &all_embeddings)?;
|
||||
self.add_entry(session_id, 5, embedding, HashMap::new())?;
|
||||
}
|
||||
|
||||
Ok(stage_v_data)
|
||||
}
|
||||
|
||||
/// Run Stage VI composite modeling on a session.
|
||||
pub fn run_stage_vi(&mut self, session_id: &str) -> CrvResult<StageVIData> {
|
||||
let session = self
|
||||
.sessions
|
||||
.get(session_id)
|
||||
.ok_or_else(|| CrvError::SessionNotFound(session_id.to_string()))?;
|
||||
|
||||
let embeddings: Vec<Vec<f32>> = session
|
||||
.entries
|
||||
.iter()
|
||||
.map(|e| e.embedding.clone())
|
||||
.collect();
|
||||
let labels: Vec<(u8, usize)> = session
|
||||
.entries
|
||||
.iter()
|
||||
.map(|e| (e.stage, e.entry_index))
|
||||
.collect();
|
||||
|
||||
let stage_vi_data = self.stage_vi.partition(&embeddings, &labels)?;
|
||||
|
||||
// Encode Stage VI result and add to session
|
||||
let embedding = self.stage_vi.encode(&stage_vi_data)?;
|
||||
self.add_entry(session_id, 6, embedding, HashMap::new())?;
|
||||
|
||||
Ok(stage_vi_data)
|
||||
}
|
||||
|
||||
/// Find convergence across multiple sessions targeting the same coordinate.
|
||||
///
|
||||
/// This is the core multi-viewer matching operation: given sessions from
|
||||
/// different viewers targeting the same coordinate, find which aspects
|
||||
/// of their signal line data converge (agree).
|
||||
pub fn find_convergence(
|
||||
&self,
|
||||
coordinate: &str,
|
||||
min_similarity: f32,
|
||||
) -> CrvResult<ConvergenceResult> {
|
||||
// Collect all sessions for this coordinate
|
||||
let relevant_sessions: Vec<&Session> = self
|
||||
.sessions
|
||||
.values()
|
||||
.filter(|s| s.coordinate == coordinate)
|
||||
.collect();
|
||||
|
||||
if relevant_sessions.len() < 2 {
|
||||
return Err(CrvError::EmptyInput(
|
||||
"Need at least 2 sessions for convergence analysis".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut session_pairs = Vec::new();
|
||||
let mut scores = Vec::new();
|
||||
let mut convergent_stages = Vec::new();
|
||||
|
||||
// Compare all pairs of sessions
|
||||
for i in 0..relevant_sessions.len() {
|
||||
for j in (i + 1)..relevant_sessions.len() {
|
||||
let sess_a = relevant_sessions[i];
|
||||
let sess_b = relevant_sessions[j];
|
||||
|
||||
// Compare stage-by-stage
|
||||
for stage in 1..=6u8 {
|
||||
let entries_a: Vec<&[f32]> = sess_a
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| e.stage == stage)
|
||||
.map(|e| e.embedding.as_slice())
|
||||
.collect();
|
||||
let entries_b: Vec<&[f32]> = sess_b
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| e.stage == stage)
|
||||
.map(|e| e.embedding.as_slice())
|
||||
.collect();
|
||||
|
||||
if entries_a.is_empty() || entries_b.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find best match for each entry in A against entries in B
|
||||
for emb_a in &entries_a {
|
||||
for emb_b in &entries_b {
|
||||
if emb_a.len() == emb_b.len() && !emb_a.is_empty() {
|
||||
let sim = cosine_similarity(emb_a, emb_b);
|
||||
if sim >= min_similarity {
|
||||
session_pairs.push((sess_a.id.clone(), sess_b.id.clone()));
|
||||
scores.push(sim);
|
||||
if !convergent_stages.contains(&stage) {
|
||||
convergent_stages.push(stage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute consensus embedding (mean of all converging embeddings)
|
||||
let consensus_embedding = if !scores.is_empty() {
|
||||
let mut consensus = vec![0.0f32; self.config.dimensions];
|
||||
let mut count = 0usize;
|
||||
|
||||
for session in &relevant_sessions {
|
||||
for entry in &session.entries {
|
||||
if convergent_stages.contains(&entry.stage) {
|
||||
for (i, &v) in entry.embedding.iter().enumerate() {
|
||||
if i < self.config.dimensions {
|
||||
consensus[i] += v;
|
||||
}
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
for v in &mut consensus {
|
||||
*v /= count as f32;
|
||||
}
|
||||
Some(consensus)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Sort convergent stages
|
||||
convergent_stages.sort();
|
||||
|
||||
Ok(ConvergenceResult {
|
||||
session_pairs,
|
||||
scores,
|
||||
convergent_stages,
|
||||
consensus_embedding,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all embeddings for a session.
|
||||
pub fn get_session_embeddings(&self, session_id: &str) -> CrvResult<Vec<CrvSessionEntry>> {
|
||||
let session = self
|
||||
.sessions
|
||||
.get(session_id)
|
||||
.ok_or_else(|| CrvError::SessionNotFound(session_id.to_string()))?;
|
||||
|
||||
Ok(session
|
||||
.entries
|
||||
.iter()
|
||||
.map(|e| CrvSessionEntry {
|
||||
session_id: session.id.clone(),
|
||||
coordinate: session.coordinate.clone(),
|
||||
stage: e.stage,
|
||||
embedding: e.embedding.clone(),
|
||||
metadata: e.metadata.clone(),
|
||||
timestamp_ms: e.timestamp_ms,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Get the number of entries in a session.
|
||||
pub fn session_entry_count(&self, session_id: &str) -> usize {
|
||||
self.sessions
|
||||
.get(session_id)
|
||||
.map(|s| s.entries.len())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Get the number of active sessions.
|
||||
pub fn session_count(&self) -> usize {
|
||||
self.sessions.len()
|
||||
}
|
||||
|
||||
/// Remove a session.
|
||||
pub fn remove_session(&mut self, session_id: &str) -> bool {
|
||||
self.sessions.remove(session_id).is_some()
|
||||
}
|
||||
|
||||
/// Get access to the Stage I encoder for direct operations.
|
||||
pub fn stage_i_encoder(&self) -> &StageIEncoder {
|
||||
&self.stage_i
|
||||
}
|
||||
|
||||
/// Get access to the Stage II encoder for direct operations.
|
||||
pub fn stage_ii_encoder(&self) -> &StageIIEncoder {
|
||||
&self.stage_ii
|
||||
}
|
||||
|
||||
/// Get access to the Stage IV encoder for direct operations.
|
||||
pub fn stage_iv_encoder(&self) -> &StageIVEncoder {
|
||||
&self.stage_iv
|
||||
}
|
||||
|
||||
/// Get access to the Stage V engine for direct operations.
|
||||
pub fn stage_v_engine(&self) -> &StageVEngine {
|
||||
&self.stage_v
|
||||
}
|
||||
|
||||
/// Get access to the Stage VI modeler for direct operations.
|
||||
pub fn stage_vi_modeler(&self) -> &StageVIModeler {
|
||||
&self.stage_vi
|
||||
}
|
||||
|
||||
/// Internal: add an entry to a session.
|
||||
fn add_entry(
|
||||
&mut self,
|
||||
session_id: &str,
|
||||
stage: u8,
|
||||
embedding: Vec<f32>,
|
||||
metadata: HashMap<String, serde_json::Value>,
|
||||
) -> CrvResult<()> {
|
||||
let session = self
|
||||
.sessions
|
||||
.get_mut(session_id)
|
||||
.ok_or_else(|| CrvError::SessionNotFound(session_id.to_string()))?;
|
||||
|
||||
let entry_index = session.entries.iter().filter(|e| e.stage == stage).count();
|
||||
|
||||
session.entries.push(SessionEntry {
|
||||
embedding,
|
||||
stage,
|
||||
entry_index,
|
||||
metadata,
|
||||
timestamp_ms: 0,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_config() -> CrvConfig {
|
||||
CrvConfig {
|
||||
dimensions: 32,
|
||||
convergence_threshold: 0.5,
|
||||
..CrvConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_creation() {
|
||||
let config = test_config();
|
||||
let mut manager = CrvSessionManager::new(config);
|
||||
|
||||
manager
|
||||
.create_session("sess-1".to_string(), "1234-5678".to_string())
|
||||
.unwrap();
|
||||
assert_eq!(manager.session_count(), 1);
|
||||
assert_eq!(manager.session_entry_count("sess-1"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_stage_i() {
|
||||
let config = test_config();
|
||||
let mut manager = CrvSessionManager::new(config);
|
||||
|
||||
manager
|
||||
.create_session("sess-1".to_string(), "1234-5678".to_string())
|
||||
.unwrap();
|
||||
|
||||
let data = StageIData {
|
||||
stroke: vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)],
|
||||
spontaneous_descriptor: "angular".to_string(),
|
||||
classification: GestaltType::Manmade,
|
||||
confidence: 0.9,
|
||||
};
|
||||
|
||||
let emb = manager.add_stage_i("sess-1", &data).unwrap();
|
||||
assert_eq!(emb.len(), 32);
|
||||
assert_eq!(manager.session_entry_count("sess-1"), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_stage_ii() {
|
||||
let config = test_config();
|
||||
let mut manager = CrvSessionManager::new(config);
|
||||
|
||||
manager
|
||||
.create_session("sess-1".to_string(), "coord-1".to_string())
|
||||
.unwrap();
|
||||
|
||||
let data = StageIIData {
|
||||
impressions: vec![
|
||||
(SensoryModality::Texture, "rough".to_string()),
|
||||
(SensoryModality::Color, "gray".to_string()),
|
||||
],
|
||||
feature_vector: None,
|
||||
};
|
||||
|
||||
let emb = manager.add_stage_ii("sess-1", &data).unwrap();
|
||||
assert_eq!(emb.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_session_flow() {
|
||||
let config = test_config();
|
||||
let mut manager = CrvSessionManager::new(config);
|
||||
|
||||
manager
|
||||
.create_session("sess-1".to_string(), "coord-1".to_string())
|
||||
.unwrap();
|
||||
|
||||
// Stage I
|
||||
let s1 = StageIData {
|
||||
stroke: vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)],
|
||||
spontaneous_descriptor: "angular".to_string(),
|
||||
classification: GestaltType::Manmade,
|
||||
confidence: 0.9,
|
||||
};
|
||||
manager.add_stage_i("sess-1", &s1).unwrap();
|
||||
|
||||
// Stage II
|
||||
let s2 = StageIIData {
|
||||
impressions: vec![
|
||||
(SensoryModality::Texture, "rough stone".to_string()),
|
||||
(SensoryModality::Temperature, "cold".to_string()),
|
||||
],
|
||||
feature_vector: None,
|
||||
};
|
||||
manager.add_stage_ii("sess-1", &s2).unwrap();
|
||||
|
||||
// Stage IV
|
||||
let s4 = StageIVData {
|
||||
emotional_impact: vec![("solemn".to_string(), 0.6)],
|
||||
tangibles: vec!["stone blocks".to_string()],
|
||||
intangibles: vec!["ancient".to_string()],
|
||||
aol_detections: vec![],
|
||||
};
|
||||
manager.add_stage_iv("sess-1", &s4).unwrap();
|
||||
|
||||
assert_eq!(manager.session_entry_count("sess-1"), 3);
|
||||
|
||||
// Get all entries
|
||||
let entries = manager.get_session_embeddings("sess-1").unwrap();
|
||||
assert_eq!(entries.len(), 3);
|
||||
assert_eq!(entries[0].stage, 1);
|
||||
assert_eq!(entries[1].stage, 2);
|
||||
assert_eq!(entries[2].stage, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_session() {
|
||||
let config = test_config();
|
||||
let mut manager = CrvSessionManager::new(config);
|
||||
|
||||
manager
|
||||
.create_session("sess-1".to_string(), "coord-1".to_string())
|
||||
.unwrap();
|
||||
|
||||
let result = manager.create_session("sess-1".to_string(), "coord-2".to_string());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_not_found() {
|
||||
let config = test_config();
|
||||
let mut manager = CrvSessionManager::new(config);
|
||||
|
||||
let s1 = StageIData {
|
||||
stroke: vec![(0.0, 0.0), (1.0, 1.0)],
|
||||
spontaneous_descriptor: "test".to_string(),
|
||||
classification: GestaltType::Natural,
|
||||
confidence: 0.5,
|
||||
};
|
||||
|
||||
let result = manager.add_stage_i("nonexistent", &s1);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_session() {
|
||||
let config = test_config();
|
||||
let mut manager = CrvSessionManager::new(config);
|
||||
|
||||
manager
|
||||
.create_session("sess-1".to_string(), "coord-1".to_string())
|
||||
.unwrap();
|
||||
assert_eq!(manager.session_count(), 1);
|
||||
|
||||
assert!(manager.remove_session("sess-1"));
|
||||
assert_eq!(manager.session_count(), 0);
|
||||
|
||||
assert!(!manager.remove_session("sess-1"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user