feat: ADR-033 CRV signal-line integration + ruvector-crv 6-stage pipeline
Implement full CRV (Coordinate Remote Viewing) signal-line protocol mapping to WiFi CSI sensing via ruvector-crv: - Stage I: CsiGestaltClassifier (6 gestalt types from amplitude/phase) - Stage II: CsiSensoryEncoder (texture/color/temperature/sound/luminosity/dimension) - Stage III: Mesh topology encoding (AP nodes/links → GNN graph) - Stage IV: Coherence gate → AOL detection (signal vs noise separation) - Stage V: Pose interrogation via differentiable search - Stage VI: Person partitioning via MinCut clustering - Cross-session convergence for cross-room identity New files: - crv/mod.rs: 1,430 lines, 43 tests - crv_bench.rs: 8 criterion benchmarks (gestalt, sensory, pipeline, convergence) - ADR-033: 740-line architecture decision with 30+ acceptance criteria - patches/ruvector-crv: Fix ruvector-gnn 2.0.5 API mismatch Dependencies: ruvector-crv 0.1.1, ruvector-gnn 2.0.5 Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
1129
rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.lock
generated
Normal file
1129
rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.toml
Normal file
23
rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "ruvector-crv"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
authors = ["ruvector contributors"]
|
||||
description = "CRV (Coordinate Remote Viewing) protocol integration for ruvector - maps 6-stage signal line methodology to vector database subsystems"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/ruvnet/ruvector"
|
||||
|
||||
[lib]
|
||||
name = "ruvector_crv"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
ruvector-attention = "0.1.31"
|
||||
ruvector-gnn = { version = "2.0", default-features = false }
|
||||
ruvector-mincut = { version = "2.0", default-features = false, features = ["exact"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5"
|
||||
28
rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.toml.orig
generated
Normal file
28
rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.toml.orig
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "ruvector-crv"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
authors = ["ruvector contributors"]
|
||||
description = "CRV (Coordinate Remote Viewing) protocol integration for ruvector - maps 6-stage signal line methodology to vector database subsystems"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/ruvnet/ruvector"
|
||||
readme = "README.md"
|
||||
keywords = ["crv", "signal-line", "vector-search", "attention", "hyperbolic"]
|
||||
categories = ["algorithms", "science"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
ruvector-attention = { version = "0.1.31", path = "../ruvector-attention" }
|
||||
ruvector-gnn = { version = "2.0.1", path = "../ruvector-gnn", default-features = false }
|
||||
ruvector-mincut = { version = "2.0.1", path = "../ruvector-mincut", default-features = false, features = ["exact"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5"
|
||||
68
rust-port/wifi-densepose-rs/patches/ruvector-crv/README.md
Normal file
68
rust-port/wifi-densepose-rs/patches/ruvector-crv/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# ruvector-crv
|
||||
|
||||
CRV (Coordinate Remote Viewing) protocol integration for ruvector.
|
||||
|
||||
Maps the 6-stage CRV signal line methodology to ruvector's subsystems:
|
||||
|
||||
| CRV Stage | Data Type | ruvector Component |
|
||||
|-----------|-----------|-------------------|
|
||||
| Stage I (Ideograms) | Gestalt primitives | Poincaré ball hyperbolic embeddings |
|
||||
| Stage II (Sensory) | Textures, colors, temps | Multi-head attention vectors |
|
||||
| Stage III (Dimensional) | Spatial sketches | GNN graph topology |
|
||||
| Stage IV (Emotional) | AOL, intangibles | SNN temporal encoding |
|
||||
| Stage V (Interrogation) | Signal line probing | Differentiable search |
|
||||
| Stage VI (3D Model) | Composite model | MinCut partitioning |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```rust
|
||||
use ruvector_crv::{CrvConfig, CrvSessionManager, GestaltType, StageIData};
|
||||
|
||||
// Create session manager with default config (384 dimensions)
|
||||
let config = CrvConfig::default();
|
||||
let mut manager = CrvSessionManager::new(config);
|
||||
|
||||
// Create a session for a target coordinate
|
||||
manager.create_session("session-001".to_string(), "1234-5678".to_string()).unwrap();
|
||||
|
||||
// Add Stage I ideogram data
|
||||
let stage_i = StageIData {
|
||||
stroke: vec![(0.0, 0.0), (1.0, 0.5), (2.0, 1.0), (3.0, 0.5)],
|
||||
spontaneous_descriptor: "angular rising".to_string(),
|
||||
classification: GestaltType::Manmade,
|
||||
confidence: 0.85,
|
||||
};
|
||||
|
||||
let embedding = manager.add_stage_i("session-001", &stage_i).unwrap();
|
||||
assert_eq!(embedding.len(), 384);
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The Poincaré ball embedding for Stage I gestalts encodes the hierarchical
|
||||
gestalt taxonomy (root → manmade/natural/movement/energy/water/land) with
|
||||
exponentially less distortion than Euclidean space.
|
||||
|
||||
For AOL (Analytical Overlay) separation, the spiking neural network temporal
|
||||
encoding models signal-vs-noise discrimination: high-frequency spike bursts
|
||||
correlate with AOL contamination, while sustained low-frequency patterns
|
||||
indicate clean signal line data.
|
||||
|
||||
MinCut partitioning in Stage VI identifies natural cluster boundaries in the
|
||||
accumulated session graph, separating distinct target aspects.
|
||||
|
||||
## Cross-Session Convergence
|
||||
|
||||
Multiple sessions targeting the same coordinate can be analyzed for
|
||||
convergence — agreement between independent viewers strengthens the
|
||||
signal validity:
|
||||
|
||||
```rust
|
||||
// After adding data to multiple sessions for "1234-5678"...
|
||||
let convergence = manager.find_convergence("1234-5678", 0.75).unwrap();
|
||||
// convergence.scores contains similarity values for converging entries
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
@@ -0,0 +1,38 @@
|
||||
//! Error types for the CRV protocol integration.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// CRV-specific errors.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CrvError {
|
||||
/// Dimension mismatch between expected and actual vector sizes.
|
||||
#[error("Dimension mismatch: expected {expected}, got {actual}")]
|
||||
DimensionMismatch { expected: usize, actual: usize },
|
||||
|
||||
/// Invalid CRV stage number.
|
||||
#[error("Invalid stage: {0} (must be 1-6)")]
|
||||
InvalidStage(u8),
|
||||
|
||||
/// Empty input data.
|
||||
#[error("Empty input: {0}")]
|
||||
EmptyInput(String),
|
||||
|
||||
/// Session not found.
|
||||
#[error("Session not found: {0}")]
|
||||
SessionNotFound(String),
|
||||
|
||||
/// Encoding failure.
|
||||
#[error("Encoding error: {0}")]
|
||||
EncodingError(String),
|
||||
|
||||
/// Attention mechanism error.
|
||||
#[error("Attention error: {0}")]
|
||||
AttentionError(#[from] ruvector_attention::AttentionError),
|
||||
|
||||
/// Serialization error.
|
||||
#[error("Serialization error: {0}")]
|
||||
SerializationError(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// Result type alias for CRV operations.
|
||||
pub type CrvResult<T> = Result<T, CrvError>;
|
||||
178
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/lib.rs
Normal file
178
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/lib.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
//! # ruvector-crv
|
||||
//!
|
||||
//! CRV (Coordinate Remote Viewing) protocol integration for ruvector.
|
||||
//!
|
||||
//! Maps the 6-stage CRV signal line methodology to ruvector's subsystems:
|
||||
//!
|
||||
//! | CRV Stage | Data Type | ruvector Component |
|
||||
//! |-----------|-----------|-------------------|
|
||||
//! | Stage I (Ideograms) | Gestalt primitives | Poincaré ball hyperbolic embeddings |
|
||||
//! | Stage II (Sensory) | Textures, colors, temps | Multi-head attention vectors |
|
||||
//! | Stage III (Dimensional) | Spatial sketches | GNN graph topology |
|
||||
//! | Stage IV (Emotional) | AOL, intangibles | SNN temporal encoding |
|
||||
//! | Stage V (Interrogation) | Signal line probing | Differentiable search |
|
||||
//! | Stage VI (3D Model) | Composite model | MinCut partitioning |
|
||||
//!
|
||||
//! ## Quick Start
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ruvector_crv::{CrvConfig, CrvSessionManager, GestaltType, StageIData};
|
||||
//!
|
||||
//! // Create session manager with default config (384 dimensions)
|
||||
//! let config = CrvConfig::default();
|
||||
//! let mut manager = CrvSessionManager::new(config);
|
||||
//!
|
||||
//! // Create a session for a target coordinate
|
||||
//! manager.create_session("session-001".to_string(), "1234-5678".to_string()).unwrap();
|
||||
//!
|
||||
//! // Add Stage I ideogram data
|
||||
//! let stage_i = StageIData {
|
||||
//! stroke: vec![(0.0, 0.0), (1.0, 0.5), (2.0, 1.0), (3.0, 0.5)],
|
||||
//! spontaneous_descriptor: "angular rising".to_string(),
|
||||
//! classification: GestaltType::Manmade,
|
||||
//! confidence: 0.85,
|
||||
//! };
|
||||
//!
|
||||
//! let embedding = manager.add_stage_i("session-001", &stage_i).unwrap();
|
||||
//! assert_eq!(embedding.len(), 384);
|
||||
//! ```
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! The Poincaré ball embedding for Stage I gestalts encodes the hierarchical
|
||||
//! gestalt taxonomy (root → manmade/natural/movement/energy/water/land) with
|
||||
//! exponentially less distortion than Euclidean space.
|
||||
//!
|
||||
//! For AOL (Analytical Overlay) separation, the spiking neural network temporal
|
||||
//! encoding models signal-vs-noise discrimination: high-frequency spike bursts
|
||||
//! correlate with AOL contamination, while sustained low-frequency patterns
|
||||
//! indicate clean signal line data.
|
||||
//!
|
||||
//! MinCut partitioning in Stage VI identifies natural cluster boundaries in the
|
||||
//! accumulated session graph, separating distinct target aspects.
|
||||
//!
|
||||
//! ## Cross-Session Convergence
|
||||
//!
|
||||
//! Multiple sessions targeting the same coordinate can be analyzed for
|
||||
//! convergence — agreement between independent viewers strengthens the
|
||||
//! signal validity:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use ruvector_crv::{CrvConfig, CrvSessionManager};
|
||||
//! # let mut manager = CrvSessionManager::new(CrvConfig::default());
|
||||
//! // After adding data to multiple sessions for "1234-5678"...
|
||||
//! let convergence = manager.find_convergence("1234-5678", 0.75).unwrap();
|
||||
//! // convergence.scores contains similarity values for converging entries
|
||||
//! ```
|
||||
|
||||
pub mod error;
|
||||
pub mod session;
|
||||
pub mod stage_i;
|
||||
pub mod stage_ii;
|
||||
pub mod stage_iii;
|
||||
pub mod stage_iv;
|
||||
pub mod stage_v;
|
||||
pub mod stage_vi;
|
||||
pub mod types;
|
||||
|
||||
// Re-export main types
|
||||
pub use error::{CrvError, CrvResult};
|
||||
pub use session::CrvSessionManager;
|
||||
pub use stage_i::StageIEncoder;
|
||||
pub use stage_ii::StageIIEncoder;
|
||||
pub use stage_iii::StageIIIEncoder;
|
||||
pub use stage_iv::StageIVEncoder;
|
||||
pub use stage_v::StageVEngine;
|
||||
pub use stage_vi::StageVIModeler;
|
||||
pub use types::{
|
||||
AOLDetection, ConvergenceResult, CrossReference, CrvConfig, CrvSessionEntry,
|
||||
GeometricKind, GestaltType, SensoryModality, SignalLineProbe, SketchElement,
|
||||
SpatialRelationType, SpatialRelationship, StageIData, StageIIData, StageIIIData,
|
||||
StageIVData, StageVData, StageVIData, TargetPartition,
|
||||
};
|
||||
|
||||
/// Library version.
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
assert!(!VERSION.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_end_to_end_session() {
|
||||
let config = CrvConfig {
|
||||
dimensions: 32,
|
||||
..CrvConfig::default()
|
||||
};
|
||||
let mut manager = CrvSessionManager::new(config);
|
||||
|
||||
// Create two sessions for the same coordinate
|
||||
manager
|
||||
.create_session("viewer-a".to_string(), "target-001".to_string())
|
||||
.unwrap();
|
||||
manager
|
||||
.create_session("viewer-b".to_string(), "target-001".to_string())
|
||||
.unwrap();
|
||||
|
||||
// Viewer A: Stage I
|
||||
let s1_a = StageIData {
|
||||
stroke: vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5), (3.0, 0.0)],
|
||||
spontaneous_descriptor: "tall angular".to_string(),
|
||||
classification: GestaltType::Manmade,
|
||||
confidence: 0.85,
|
||||
};
|
||||
manager.add_stage_i("viewer-a", &s1_a).unwrap();
|
||||
|
||||
// Viewer B: Stage I (similar gestalt)
|
||||
let s1_b = StageIData {
|
||||
stroke: vec![(0.0, 0.0), (0.5, 1.2), (1.5, 0.8), (2.5, 0.0)],
|
||||
spontaneous_descriptor: "structured upward".to_string(),
|
||||
classification: GestaltType::Manmade,
|
||||
confidence: 0.78,
|
||||
};
|
||||
manager.add_stage_i("viewer-b", &s1_b).unwrap();
|
||||
|
||||
// Viewer A: Stage II
|
||||
let s2_a = StageIIData {
|
||||
impressions: vec![
|
||||
(SensoryModality::Texture, "rough stone".to_string()),
|
||||
(SensoryModality::Temperature, "cool".to_string()),
|
||||
(SensoryModality::Color, "gray".to_string()),
|
||||
],
|
||||
feature_vector: None,
|
||||
};
|
||||
manager.add_stage_ii("viewer-a", &s2_a).unwrap();
|
||||
|
||||
// Viewer B: Stage II (overlapping sensory)
|
||||
let s2_b = StageIIData {
|
||||
impressions: vec![
|
||||
(SensoryModality::Texture, "grainy rough".to_string()),
|
||||
(SensoryModality::Color, "dark gray".to_string()),
|
||||
(SensoryModality::Luminosity, "dim".to_string()),
|
||||
],
|
||||
feature_vector: None,
|
||||
};
|
||||
manager.add_stage_ii("viewer-b", &s2_b).unwrap();
|
||||
|
||||
// Verify entries
|
||||
assert_eq!(manager.session_entry_count("viewer-a"), 2);
|
||||
assert_eq!(manager.session_entry_count("viewer-b"), 2);
|
||||
|
||||
// Both sessions should have embeddings
|
||||
let entries_a = manager.get_session_embeddings("viewer-a").unwrap();
|
||||
let entries_b = manager.get_session_embeddings("viewer-b").unwrap();
|
||||
|
||||
assert_eq!(entries_a.len(), 2);
|
||||
assert_eq!(entries_b.len(), 2);
|
||||
|
||||
// All embeddings should be 32-dimensional
|
||||
for entry in entries_a.iter().chain(entries_b.iter()) {
|
||||
assert_eq!(entry.embedding.len(), 32);
|
||||
}
|
||||
}
|
||||
}
|
||||
629
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/session.rs
Normal file
629
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/session.rs
Normal file
@@ -0,0 +1,629 @@
|
||||
//! 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"));
|
||||
}
|
||||
}
|
||||
364
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_i.rs
Normal file
364
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_i.rs
Normal file
@@ -0,0 +1,364 @@
|
||||
//! Stage I Encoder: Ideogram Gestalts via Poincaré Ball Embeddings
|
||||
//!
|
||||
//! CRV Stage I captures gestalt primitives (manmade, natural, movement, energy,
|
||||
//! water, land) through ideogram traces. The hierarchical taxonomy of gestalts
|
||||
//! maps naturally to hyperbolic space, where the Poincaré ball model encodes
|
||||
//! tree-like structures with exponentially less distortion than Euclidean space.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! Ideogram stroke traces are converted to fixed-dimension feature vectors,
|
||||
//! then projected into the Poincaré ball. Gestalt classification uses hyperbolic
|
||||
//! distance to prototype embeddings for each gestalt type.
|
||||
|
||||
use crate::error::{CrvError, CrvResult};
|
||||
use crate::types::{CrvConfig, GestaltType, StageIData};
|
||||
use ruvector_attention::hyperbolic::{
|
||||
exp_map, frechet_mean, log_map, mobius_add, poincare_distance, project_to_ball,
|
||||
};
|
||||
|
||||
/// Stage I encoder using Poincaré ball hyperbolic embeddings.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StageIEncoder {
|
||||
/// Embedding dimensionality.
|
||||
dim: usize,
|
||||
/// Poincaré ball curvature (positive).
|
||||
curvature: f32,
|
||||
/// Prototype embeddings for each gestalt type in the Poincaré ball.
|
||||
/// Indexed by `GestaltType::index()`.
|
||||
prototypes: Vec<Vec<f32>>,
|
||||
}
|
||||
|
||||
impl StageIEncoder {
|
||||
/// Create a new Stage I encoder with default gestalt prototypes.
|
||||
pub fn new(config: &CrvConfig) -> Self {
|
||||
let dim = config.dimensions;
|
||||
let curvature = config.curvature;
|
||||
|
||||
// Initialize gestalt prototypes as points in the Poincaré ball.
|
||||
// Each prototype is placed at a distinct region of the ball,
|
||||
// with hierarchical relationships preserved by hyperbolic distance.
|
||||
let prototypes = Self::init_prototypes(dim, curvature);
|
||||
|
||||
Self {
|
||||
dim,
|
||||
curvature,
|
||||
prototypes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize gestalt prototype embeddings in the Poincaré ball.
|
||||
///
|
||||
/// Places each gestalt type at a distinct angular position with
|
||||
/// controlled radial distance from the origin. The hierarchical
|
||||
/// structure (root → gestalt types → sub-types) is preserved
|
||||
/// by the exponential volume growth of hyperbolic space.
|
||||
fn init_prototypes(dim: usize, curvature: f32) -> Vec<Vec<f32>> {
|
||||
let num_types = GestaltType::all().len();
|
||||
let mut prototypes = Vec::with_capacity(num_types);
|
||||
|
||||
for gestalt in GestaltType::all() {
|
||||
let idx = gestalt.index();
|
||||
// Place each prototype along a different axis direction
|
||||
// with a moderate radial distance (0.3-0.5 of ball radius).
|
||||
let mut proto = vec![0.0f32; dim];
|
||||
|
||||
// Use multiple dimensions to spread prototypes apart
|
||||
let base_dim = idx * (dim / num_types);
|
||||
let spread = dim / num_types;
|
||||
|
||||
for d in 0..spread.min(dim - base_dim) {
|
||||
let angle = std::f32::consts::PI * 2.0 * (d as f32) / (spread as f32);
|
||||
proto[base_dim + d] = 0.3 * angle.cos() / (spread as f32).sqrt();
|
||||
}
|
||||
|
||||
// Project to ball to ensure it's inside
|
||||
proto = project_to_ball(&proto, curvature, 1e-7);
|
||||
prototypes.push(proto);
|
||||
}
|
||||
|
||||
prototypes
|
||||
}
|
||||
|
||||
/// Encode an ideogram stroke trace into a fixed-dimension feature vector.
|
||||
///
|
||||
/// Extracts geometric features from the stroke: curvature statistics,
|
||||
/// velocity profile, angular distribution, and bounding box ratios.
|
||||
pub fn encode_stroke(&self, stroke: &[(f32, f32)]) -> CrvResult<Vec<f32>> {
|
||||
if stroke.is_empty() {
|
||||
return Err(CrvError::EmptyInput("Stroke trace is empty".to_string()));
|
||||
}
|
||||
|
||||
let mut features = vec![0.0f32; self.dim];
|
||||
|
||||
// Feature 1: Stroke statistics (first few dimensions)
|
||||
let n = stroke.len() as f32;
|
||||
let (cx, cy) = stroke
|
||||
.iter()
|
||||
.fold((0.0, 0.0), |(sx, sy), &(x, y)| (sx + x, sy + y));
|
||||
features[0] = cx / n; // centroid x
|
||||
features[1] = cy / n; // centroid y
|
||||
|
||||
// Feature 2: Bounding box aspect ratio
|
||||
let (min_x, max_x) = stroke
|
||||
.iter()
|
||||
.map(|p| p.0)
|
||||
.fold((f32::MAX, f32::MIN), |(mn, mx), v| (mn.min(v), mx.max(v)));
|
||||
let (min_y, max_y) = stroke
|
||||
.iter()
|
||||
.map(|p| p.1)
|
||||
.fold((f32::MAX, f32::MIN), |(mn, mx), v| (mn.min(v), mx.max(v)));
|
||||
let width = (max_x - min_x).max(1e-6);
|
||||
let height = (max_y - min_y).max(1e-6);
|
||||
features[2] = width / height; // aspect ratio
|
||||
|
||||
// Feature 3: Total path length (normalized)
|
||||
let mut path_length = 0.0f32;
|
||||
for i in 1..stroke.len() {
|
||||
let dx = stroke[i].0 - stroke[i - 1].0;
|
||||
let dy = stroke[i].1 - stroke[i - 1].1;
|
||||
path_length += (dx * dx + dy * dy).sqrt();
|
||||
}
|
||||
features[3] = path_length / (width + height).max(1e-6);
|
||||
|
||||
// Feature 4: Angular distribution (segment angles)
|
||||
if stroke.len() >= 3 {
|
||||
let num_angle_bins = 8.min(self.dim.saturating_sub(4));
|
||||
for i in 1..stroke.len().saturating_sub(1) {
|
||||
let dx1 = stroke[i].0 - stroke[i - 1].0;
|
||||
let dy1 = stroke[i].1 - stroke[i - 1].1;
|
||||
let dx2 = stroke[i + 1].0 - stroke[i].0;
|
||||
let dy2 = stroke[i + 1].1 - stroke[i].1;
|
||||
let angle = dy1.atan2(dx1) - dy2.atan2(dx2);
|
||||
let bin = ((angle + std::f32::consts::PI) / (2.0 * std::f32::consts::PI)
|
||||
* num_angle_bins as f32) as usize;
|
||||
let bin = bin.min(num_angle_bins - 1);
|
||||
if 4 + bin < self.dim {
|
||||
features[4 + bin] += 1.0 / (stroke.len() as f32 - 2.0).max(1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feature 5: Curvature variance (spread across remaining dimensions)
|
||||
if stroke.len() >= 3 {
|
||||
let mut curvatures = Vec::new();
|
||||
for i in 1..stroke.len() - 1 {
|
||||
let dx1 = stroke[i].0 - stroke[i - 1].0;
|
||||
let dy1 = stroke[i].1 - stroke[i - 1].1;
|
||||
let dx2 = stroke[i + 1].0 - stroke[i].0;
|
||||
let dy2 = stroke[i + 1].1 - stroke[i].1;
|
||||
let cross = dx1 * dy2 - dy1 * dx2;
|
||||
let ds1 = (dx1 * dx1 + dy1 * dy1).sqrt().max(1e-6);
|
||||
let ds2 = (dx2 * dx2 + dy2 * dy2).sqrt().max(1e-6);
|
||||
curvatures.push(cross / (ds1 * ds2));
|
||||
}
|
||||
if !curvatures.is_empty() {
|
||||
let mean_k: f32 = curvatures.iter().sum::<f32>() / curvatures.len() as f32;
|
||||
let var_k: f32 = curvatures.iter().map(|k| (k - mean_k).powi(2)).sum::<f32>()
|
||||
/ curvatures.len() as f32;
|
||||
if 12 < self.dim {
|
||||
features[12] = mean_k;
|
||||
}
|
||||
if 13 < self.dim {
|
||||
features[13] = var_k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize the feature vector
|
||||
let norm: f32 = features.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if norm > 1e-6 {
|
||||
let scale = 0.4 / norm; // keep within ball
|
||||
for f in &mut features {
|
||||
*f *= scale;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(features)
|
||||
}
|
||||
|
||||
/// Encode complete Stage I data into a Poincaré ball embedding.
|
||||
///
|
||||
/// Combines stroke features with the gestalt prototype via Möbius addition,
|
||||
/// producing a vector that encodes both the raw ideogram trace and its
|
||||
/// gestalt classification in hyperbolic space.
|
||||
pub fn encode(&self, data: &StageIData) -> CrvResult<Vec<f32>> {
|
||||
let stroke_features = self.encode_stroke(&data.stroke)?;
|
||||
|
||||
// Get the prototype for the classified gestalt type
|
||||
let prototype = &self.prototypes[data.classification.index()];
|
||||
|
||||
// Combine stroke features with gestalt prototype via Möbius addition.
|
||||
// This places the encoded vector near the gestalt prototype in
|
||||
// hyperbolic space, with the stroke features providing the offset.
|
||||
let combined = mobius_add(&stroke_features, prototype, self.curvature);
|
||||
|
||||
// Weight by confidence
|
||||
let weighted: Vec<f32> = combined
|
||||
.iter()
|
||||
.map(|&v| v * data.confidence + stroke_features[0] * (1.0 - data.confidence))
|
||||
.collect();
|
||||
|
||||
Ok(project_to_ball(&weighted, self.curvature, 1e-7))
|
||||
}
|
||||
|
||||
/// Classify a stroke embedding into a gestalt type by finding the
|
||||
/// nearest prototype in hyperbolic space.
|
||||
pub fn classify(&self, embedding: &[f32]) -> CrvResult<(GestaltType, f32)> {
|
||||
if embedding.len() != self.dim {
|
||||
return Err(CrvError::DimensionMismatch {
|
||||
expected: self.dim,
|
||||
actual: embedding.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut best_type = GestaltType::Manmade;
|
||||
let mut best_distance = f32::MAX;
|
||||
|
||||
for gestalt in GestaltType::all() {
|
||||
let proto = &self.prototypes[gestalt.index()];
|
||||
let dist = poincare_distance(embedding, proto, self.curvature);
|
||||
if dist < best_distance {
|
||||
best_distance = dist;
|
||||
best_type = *gestalt;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert distance to confidence (closer = higher confidence)
|
||||
let confidence = (-best_distance).exp();
|
||||
|
||||
Ok((best_type, confidence))
|
||||
}
|
||||
|
||||
/// Compute the Fréchet mean of multiple Stage I embeddings.
|
||||
///
|
||||
/// Useful for finding the consensus gestalt across multiple sessions
|
||||
/// targeting the same coordinate.
|
||||
pub fn consensus(&self, embeddings: &[&[f32]]) -> CrvResult<Vec<f32>> {
|
||||
if embeddings.is_empty() {
|
||||
return Err(CrvError::EmptyInput(
|
||||
"No embeddings for consensus".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(frechet_mean(embeddings, None, self.curvature, 50, 1e-5))
|
||||
}
|
||||
|
||||
/// Compute pairwise hyperbolic distance between two Stage I embeddings.
|
||||
pub fn distance(&self, a: &[f32], b: &[f32]) -> f32 {
|
||||
poincare_distance(a, b, self.curvature)
|
||||
}
|
||||
|
||||
/// Get the prototype embedding for a gestalt type.
|
||||
pub fn prototype(&self, gestalt: GestaltType) -> &[f32] {
|
||||
&self.prototypes[gestalt.index()]
|
||||
}
|
||||
|
||||
/// Map an embedding to tangent space at the origin for Euclidean operations.
|
||||
pub fn to_tangent(&self, embedding: &[f32]) -> Vec<f32> {
|
||||
let origin = vec![0.0f32; self.dim];
|
||||
log_map(embedding, &origin, self.curvature)
|
||||
}
|
||||
|
||||
/// Map a tangent vector back to the Poincaré ball.
|
||||
pub fn from_tangent(&self, tangent: &[f32]) -> Vec<f32> {
|
||||
let origin = vec![0.0f32; self.dim];
|
||||
exp_map(tangent, &origin, self.curvature)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_config() -> CrvConfig {
|
||||
CrvConfig {
|
||||
dimensions: 32,
|
||||
curvature: 1.0,
|
||||
..CrvConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encoder_creation() {
|
||||
let config = test_config();
|
||||
let encoder = StageIEncoder::new(&config);
|
||||
assert_eq!(encoder.dim, 32);
|
||||
assert_eq!(encoder.prototypes.len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stroke_encoding() {
|
||||
let config = test_config();
|
||||
let encoder = StageIEncoder::new(&config);
|
||||
|
||||
let stroke = vec![(0.0, 0.0), (1.0, 0.5), (2.0, 1.0), (3.0, 0.5), (4.0, 0.0)];
|
||||
let embedding = encoder.encode_stroke(&stroke).unwrap();
|
||||
assert_eq!(embedding.len(), 32);
|
||||
|
||||
// Should be inside the Poincaré ball
|
||||
let norm_sq: f32 = embedding.iter().map(|x| x * x).sum();
|
||||
assert!(norm_sq < 1.0 / config.curvature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_encode() {
|
||||
let config = test_config();
|
||||
let encoder = StageIEncoder::new(&config);
|
||||
|
||||
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 embedding = encoder.encode(&data).unwrap();
|
||||
assert_eq!(embedding.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classification() {
|
||||
let config = test_config();
|
||||
let encoder = StageIEncoder::new(&config);
|
||||
|
||||
// Encode and classify should round-trip for strong prototypes
|
||||
let proto = encoder.prototype(GestaltType::Energy).to_vec();
|
||||
let (classified, confidence) = encoder.classify(&proto).unwrap();
|
||||
assert_eq!(classified, GestaltType::Energy);
|
||||
assert!(confidence > 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_distance_symmetry() {
|
||||
let config = test_config();
|
||||
let encoder = StageIEncoder::new(&config);
|
||||
|
||||
let a = encoder.prototype(GestaltType::Manmade);
|
||||
let b = encoder.prototype(GestaltType::Natural);
|
||||
|
||||
let d_ab = encoder.distance(a, b);
|
||||
let d_ba = encoder.distance(b, a);
|
||||
|
||||
assert!((d_ab - d_ba).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tangent_roundtrip() {
|
||||
let config = test_config();
|
||||
let encoder = StageIEncoder::new(&config);
|
||||
|
||||
let proto = encoder.prototype(GestaltType::Water).to_vec();
|
||||
let tangent = encoder.to_tangent(&proto);
|
||||
let recovered = encoder.from_tangent(&tangent);
|
||||
|
||||
// Should approximately round-trip
|
||||
let error: f32 = proto
|
||||
.iter()
|
||||
.zip(&recovered)
|
||||
.map(|(a, b)| (a - b).abs())
|
||||
.sum::<f32>()
|
||||
/ proto.len() as f32;
|
||||
assert!(error < 0.1);
|
||||
}
|
||||
}
|
||||
268
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_ii.rs
Normal file
268
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_ii.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
//! Stage II Encoder: Sensory Data via Multi-Head Attention Vectors
|
||||
//!
|
||||
//! CRV Stage II captures sensory impressions (textures, colors, temperatures,
|
||||
//! sounds, etc.). Each sensory modality is encoded as a separate attention head,
|
||||
//! with the multi-head mechanism combining them into a unified 384-dimensional
|
||||
//! representation.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! Sensory descriptors are hashed into feature vectors per modality, then
|
||||
//! processed through multi-head attention where each head specializes in
|
||||
//! a different sensory channel.
|
||||
|
||||
use crate::error::{CrvError, CrvResult};
|
||||
use crate::types::{CrvConfig, SensoryModality, StageIIData};
|
||||
use ruvector_attention::traits::Attention;
|
||||
use ruvector_attention::MultiHeadAttention;
|
||||
|
||||
/// Number of sensory modality heads.
|
||||
const NUM_MODALITIES: usize = 8;
|
||||
|
||||
/// Stage II encoder using multi-head attention for sensory fusion.
|
||||
pub struct StageIIEncoder {
|
||||
/// Embedding dimensionality.
|
||||
dim: usize,
|
||||
/// Multi-head attention mechanism (one head per modality).
|
||||
attention: MultiHeadAttention,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for StageIIEncoder {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("StageIIEncoder")
|
||||
.field("dim", &self.dim)
|
||||
.field("attention", &"MultiHeadAttention { .. }")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl StageIIEncoder {
|
||||
/// Create a new Stage II encoder.
|
||||
pub fn new(config: &CrvConfig) -> Self {
|
||||
let dim = config.dimensions;
|
||||
// Ensure dim is divisible by NUM_MODALITIES
|
||||
let effective_heads = if dim % NUM_MODALITIES == 0 {
|
||||
NUM_MODALITIES
|
||||
} else {
|
||||
// Fall back to a divisor
|
||||
let mut h = NUM_MODALITIES;
|
||||
while dim % h != 0 && h > 1 {
|
||||
h -= 1;
|
||||
}
|
||||
h
|
||||
};
|
||||
|
||||
let attention = MultiHeadAttention::new(dim, effective_heads);
|
||||
|
||||
Self { dim, attention }
|
||||
}
|
||||
|
||||
/// Encode a sensory descriptor string into a feature vector.
|
||||
///
|
||||
/// Uses a deterministic hash-based encoding to convert text descriptors
|
||||
/// into fixed-dimension vectors. Each modality gets a distinct subspace.
|
||||
fn encode_descriptor(&self, modality: SensoryModality, descriptor: &str) -> Vec<f32> {
|
||||
let mut features = vec![0.0f32; self.dim];
|
||||
let modality_offset = modality_index(modality) * (self.dim / NUM_MODALITIES.max(1));
|
||||
let subspace_size = self.dim / NUM_MODALITIES.max(1);
|
||||
|
||||
// Simple deterministic hash encoding
|
||||
let bytes = descriptor.as_bytes();
|
||||
for (i, &byte) in bytes.iter().enumerate() {
|
||||
let dim_idx = modality_offset + (i % subspace_size);
|
||||
if dim_idx < self.dim {
|
||||
// Distribute byte values across the subspace with varied phases
|
||||
let phase = (i as f32) * 0.618_034; // golden ratio
|
||||
features[dim_idx] += (byte as f32 / 255.0) * (phase * std::f32::consts::PI).cos();
|
||||
}
|
||||
}
|
||||
|
||||
// Add modality-specific bias
|
||||
if modality_offset < self.dim {
|
||||
features[modality_offset] += 1.0;
|
||||
}
|
||||
|
||||
// L2 normalize
|
||||
let norm: f32 = features.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if norm > 1e-6 {
|
||||
for f in &mut features {
|
||||
*f /= norm;
|
||||
}
|
||||
}
|
||||
|
||||
features
|
||||
}
|
||||
|
||||
/// Encode Stage II data into a unified sensory embedding.
|
||||
///
|
||||
/// Each sensory impression becomes a key-value pair in the attention
|
||||
/// mechanism. A learned query (based on the modality distribution)
|
||||
/// attends over all impressions to produce the fused output.
|
||||
pub fn encode(&self, data: &StageIIData) -> CrvResult<Vec<f32>> {
|
||||
if data.impressions.is_empty() {
|
||||
return Err(CrvError::EmptyInput(
|
||||
"No sensory impressions".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// If a pre-computed feature vector exists, use it
|
||||
if let Some(ref fv) = data.feature_vector {
|
||||
if fv.len() == self.dim {
|
||||
return Ok(fv.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Encode each impression into a feature vector
|
||||
let encoded: Vec<Vec<f32>> = data
|
||||
.impressions
|
||||
.iter()
|
||||
.map(|(modality, descriptor)| self.encode_descriptor(*modality, descriptor))
|
||||
.collect();
|
||||
|
||||
// Build query from modality distribution
|
||||
let query = self.build_modality_query(&data.impressions);
|
||||
|
||||
let keys: Vec<&[f32]> = encoded.iter().map(|v| v.as_slice()).collect();
|
||||
let values: Vec<&[f32]> = encoded.iter().map(|v| v.as_slice()).collect();
|
||||
|
||||
let result = self.attention.compute(&query, &keys, &values)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Build a query vector from the distribution of modalities present.
|
||||
fn build_modality_query(&self, impressions: &[(SensoryModality, String)]) -> Vec<f32> {
|
||||
let mut query = vec![0.0f32; self.dim];
|
||||
let subspace_size = self.dim / NUM_MODALITIES.max(1);
|
||||
|
||||
// Count modality occurrences
|
||||
let mut counts = [0usize; NUM_MODALITIES];
|
||||
for (modality, _) in impressions {
|
||||
let idx = modality_index(*modality);
|
||||
if idx < NUM_MODALITIES {
|
||||
counts[idx] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Encode counts as the query
|
||||
let total: f32 = counts.iter().sum::<usize>() as f32;
|
||||
for (m, &count) in counts.iter().enumerate() {
|
||||
let weight = count as f32 / total.max(1.0);
|
||||
let offset = m * subspace_size;
|
||||
for d in 0..subspace_size.min(self.dim - offset) {
|
||||
query[offset + d] = weight * (1.0 + d as f32 * 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
// L2 normalize
|
||||
let norm: f32 = query.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if norm > 1e-6 {
|
||||
for f in &mut query {
|
||||
*f /= norm;
|
||||
}
|
||||
}
|
||||
|
||||
query
|
||||
}
|
||||
|
||||
/// Compute similarity between two Stage II embeddings.
|
||||
pub fn similarity(&self, a: &[f32], b: &[f32]) -> f32 {
|
||||
if a.len() != b.len() || a.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
|
||||
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if norm_a < 1e-6 || norm_b < 1e-6 {
|
||||
return 0.0;
|
||||
}
|
||||
dot / (norm_a * norm_b)
|
||||
}
|
||||
}
|
||||
|
||||
/// Map sensory modality to index.
|
||||
fn modality_index(m: SensoryModality) -> usize {
|
||||
match m {
|
||||
SensoryModality::Texture => 0,
|
||||
SensoryModality::Color => 1,
|
||||
SensoryModality::Temperature => 2,
|
||||
SensoryModality::Sound => 3,
|
||||
SensoryModality::Smell => 4,
|
||||
SensoryModality::Taste => 5,
|
||||
SensoryModality::Dimension => 6,
|
||||
SensoryModality::Luminosity => 7,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_config() -> CrvConfig {
|
||||
CrvConfig {
|
||||
dimensions: 32, // 32 / 8 = 4 dims per head
|
||||
..CrvConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encoder_creation() {
|
||||
let config = test_config();
|
||||
let encoder = StageIIEncoder::new(&config);
|
||||
assert_eq!(encoder.dim, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_descriptor_encoding() {
|
||||
let config = test_config();
|
||||
let encoder = StageIIEncoder::new(&config);
|
||||
|
||||
let v = encoder.encode_descriptor(SensoryModality::Texture, "rough grainy");
|
||||
assert_eq!(v.len(), 32);
|
||||
|
||||
// Should be normalized
|
||||
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
assert!((norm - 1.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_encode() {
|
||||
let config = test_config();
|
||||
let encoder = StageIIEncoder::new(&config);
|
||||
|
||||
let data = StageIIData {
|
||||
impressions: vec![
|
||||
(SensoryModality::Texture, "rough".to_string()),
|
||||
(SensoryModality::Color, "blue-gray".to_string()),
|
||||
(SensoryModality::Temperature, "cold".to_string()),
|
||||
],
|
||||
feature_vector: None,
|
||||
};
|
||||
|
||||
let embedding = encoder.encode(&data).unwrap();
|
||||
assert_eq!(embedding.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_similarity() {
|
||||
let config = test_config();
|
||||
let encoder = StageIIEncoder::new(&config);
|
||||
|
||||
let a = vec![1.0; 32];
|
||||
let b = vec![1.0; 32];
|
||||
let sim = encoder.similarity(&a, &b);
|
||||
assert!((sim - 1.0).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_impressions() {
|
||||
let config = test_config();
|
||||
let encoder = StageIIEncoder::new(&config);
|
||||
|
||||
let data = StageIIData {
|
||||
impressions: vec![],
|
||||
feature_vector: None,
|
||||
};
|
||||
|
||||
assert!(encoder.encode(&data).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
//! Stage III Encoder: Dimensional Data via GNN Graph Topology
|
||||
//!
|
||||
//! CRV Stage III captures spatial sketches and geometric relationships.
|
||||
//! These naturally form a graph where sketch elements are nodes and spatial
|
||||
//! relationships are edges. The GNN layer learns to propagate spatial
|
||||
//! context through the graph, producing an embedding that captures the
|
||||
//! full dimensional structure of the target.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! Sketch elements → node features, spatial relationships → edge weights.
|
||||
//! A GNN forward pass aggregates neighborhood information to produce
|
||||
//! a graph-level embedding.
|
||||
|
||||
use crate::error::{CrvError, CrvResult};
|
||||
use crate::types::{CrvConfig, GeometricKind, SpatialRelationType, StageIIIData};
|
||||
use ruvector_gnn::layer::RuvectorLayer;
|
||||
use ruvector_gnn::search::cosine_similarity;
|
||||
|
||||
/// Stage III encoder using GNN graph topology.
|
||||
#[derive(Debug)]
|
||||
pub struct StageIIIEncoder {
|
||||
/// Embedding dimensionality.
|
||||
dim: usize,
|
||||
/// GNN layer for spatial message passing.
|
||||
gnn_layer: RuvectorLayer,
|
||||
}
|
||||
|
||||
impl StageIIIEncoder {
|
||||
/// Create a new Stage III encoder.
|
||||
pub fn new(config: &CrvConfig) -> Self {
|
||||
let dim = config.dimensions;
|
||||
// Single GNN layer: input_dim -> hidden_dim, 1 head
|
||||
let gnn_layer = RuvectorLayer::new(dim, dim, 1, 0.0)
|
||||
.expect("ruvector-crv: valid GNN layer config (dim, dim, 1 head, 0.0 dropout)");
|
||||
|
||||
Self { dim, gnn_layer }
|
||||
}
|
||||
|
||||
/// Encode a sketch element into a node feature vector.
|
||||
fn encode_element(&self, label: &str, kind: GeometricKind, position: (f32, f32), scale: Option<f32>) -> Vec<f32> {
|
||||
let mut features = vec![0.0f32; self.dim];
|
||||
|
||||
// Geometric kind encoding (one-hot style in first 8 dims)
|
||||
let kind_idx = match kind {
|
||||
GeometricKind::Point => 0,
|
||||
GeometricKind::Line => 1,
|
||||
GeometricKind::Curve => 2,
|
||||
GeometricKind::Rectangle => 3,
|
||||
GeometricKind::Circle => 4,
|
||||
GeometricKind::Triangle => 5,
|
||||
GeometricKind::Polygon => 6,
|
||||
GeometricKind::Freeform => 7,
|
||||
};
|
||||
if kind_idx < self.dim {
|
||||
features[kind_idx] = 1.0;
|
||||
}
|
||||
|
||||
// Position encoding (normalized)
|
||||
if 8 < self.dim {
|
||||
features[8] = position.0;
|
||||
}
|
||||
if 9 < self.dim {
|
||||
features[9] = position.1;
|
||||
}
|
||||
|
||||
// Scale encoding
|
||||
if let Some(s) = scale {
|
||||
if 10 < self.dim {
|
||||
features[10] = s;
|
||||
}
|
||||
}
|
||||
|
||||
// Label hash encoding (spread across remaining dims)
|
||||
for (i, byte) in label.bytes().enumerate() {
|
||||
let idx = 11 + (i % (self.dim.saturating_sub(11)));
|
||||
if idx < self.dim {
|
||||
features[idx] += (byte as f32 / 255.0) * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// L2 normalize
|
||||
let norm: f32 = features.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if norm > 1e-6 {
|
||||
for f in &mut features {
|
||||
*f /= norm;
|
||||
}
|
||||
}
|
||||
|
||||
features
|
||||
}
|
||||
|
||||
/// Compute edge weight from spatial relationship type.
|
||||
fn relationship_weight(relation: SpatialRelationType) -> f32 {
|
||||
match relation {
|
||||
SpatialRelationType::Adjacent => 0.8,
|
||||
SpatialRelationType::Contains => 0.9,
|
||||
SpatialRelationType::Above => 0.6,
|
||||
SpatialRelationType::Below => 0.6,
|
||||
SpatialRelationType::Inside => 0.95,
|
||||
SpatialRelationType::Surrounding => 0.85,
|
||||
SpatialRelationType::Connected => 0.7,
|
||||
SpatialRelationType::Separated => 0.3,
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode Stage III data into a graph-level embedding.
|
||||
///
|
||||
/// Builds a graph from sketch elements and relationships,
|
||||
/// runs GNN message passing, then aggregates node embeddings
|
||||
/// into a single graph-level vector.
|
||||
pub fn encode(&self, data: &StageIIIData) -> CrvResult<Vec<f32>> {
|
||||
if data.sketch_elements.is_empty() {
|
||||
return Err(CrvError::EmptyInput(
|
||||
"No sketch elements".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Build label → index mapping
|
||||
let label_to_idx: std::collections::HashMap<&str, usize> = data
|
||||
.sketch_elements
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, elem)| (elem.label.as_str(), i))
|
||||
.collect();
|
||||
|
||||
// Encode each element as a node feature vector
|
||||
let node_features: Vec<Vec<f32>> = data
|
||||
.sketch_elements
|
||||
.iter()
|
||||
.map(|elem| {
|
||||
self.encode_element(&elem.label, elem.kind, elem.position, elem.scale)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// For each node, collect neighbor embeddings and edge weights
|
||||
// based on the spatial relationships
|
||||
let mut aggregated = vec![vec![0.0f32; self.dim]; node_features.len()];
|
||||
|
||||
for (node_idx, node_feat) in node_features.iter().enumerate() {
|
||||
let label = &data.sketch_elements[node_idx].label;
|
||||
|
||||
// Find all relationships involving this node
|
||||
let mut neighbor_feats = Vec::new();
|
||||
let mut edge_weights = Vec::new();
|
||||
|
||||
for rel in &data.relationships {
|
||||
if rel.from == *label {
|
||||
if let Some(&neighbor_idx) = label_to_idx.get(rel.to.as_str()) {
|
||||
neighbor_feats.push(node_features[neighbor_idx].clone());
|
||||
edge_weights.push(Self::relationship_weight(rel.relation) * rel.strength);
|
||||
}
|
||||
} else if rel.to == *label {
|
||||
if let Some(&neighbor_idx) = label_to_idx.get(rel.from.as_str()) {
|
||||
neighbor_feats.push(node_features[neighbor_idx].clone());
|
||||
edge_weights.push(Self::relationship_weight(rel.relation) * rel.strength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GNN forward pass for this node
|
||||
aggregated[node_idx] =
|
||||
self.gnn_layer
|
||||
.forward(node_feat, &neighbor_feats, &edge_weights);
|
||||
}
|
||||
|
||||
// Aggregate into graph-level embedding via mean pooling
|
||||
let mut graph_embedding = vec![0.0f32; self.dim];
|
||||
for node_emb in &aggregated {
|
||||
for (i, &v) in node_emb.iter().enumerate() {
|
||||
if i < self.dim {
|
||||
graph_embedding[i] += v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let n = aggregated.len() as f32;
|
||||
for v in &mut graph_embedding {
|
||||
*v /= n;
|
||||
}
|
||||
|
||||
Ok(graph_embedding)
|
||||
}
|
||||
|
||||
/// Compute similarity between two Stage III embeddings.
|
||||
pub fn similarity(&self, a: &[f32], b: &[f32]) -> f32 {
|
||||
cosine_similarity(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::{SketchElement, SpatialRelationship};
|
||||
|
||||
fn test_config() -> CrvConfig {
|
||||
CrvConfig {
|
||||
dimensions: 32,
|
||||
..CrvConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encoder_creation() {
|
||||
let config = test_config();
|
||||
let encoder = StageIIIEncoder::new(&config);
|
||||
assert_eq!(encoder.dim, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_element_encoding() {
|
||||
let config = test_config();
|
||||
let encoder = StageIIIEncoder::new(&config);
|
||||
|
||||
let features = encoder.encode_element(
|
||||
"building",
|
||||
GeometricKind::Rectangle,
|
||||
(0.5, 0.3),
|
||||
Some(2.0),
|
||||
);
|
||||
assert_eq!(features.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_encode() {
|
||||
let config = test_config();
|
||||
let encoder = StageIIIEncoder::new(&config);
|
||||
|
||||
let data = StageIIIData {
|
||||
sketch_elements: vec![
|
||||
SketchElement {
|
||||
label: "tower".to_string(),
|
||||
kind: GeometricKind::Rectangle,
|
||||
position: (0.5, 0.8),
|
||||
scale: Some(3.0),
|
||||
},
|
||||
SketchElement {
|
||||
label: "base".to_string(),
|
||||
kind: GeometricKind::Rectangle,
|
||||
position: (0.5, 0.2),
|
||||
scale: Some(5.0),
|
||||
},
|
||||
SketchElement {
|
||||
label: "path".to_string(),
|
||||
kind: GeometricKind::Line,
|
||||
position: (0.3, 0.1),
|
||||
scale: None,
|
||||
},
|
||||
],
|
||||
relationships: vec![
|
||||
SpatialRelationship {
|
||||
from: "tower".to_string(),
|
||||
to: "base".to_string(),
|
||||
relation: SpatialRelationType::Above,
|
||||
strength: 0.9,
|
||||
},
|
||||
SpatialRelationship {
|
||||
from: "path".to_string(),
|
||||
to: "base".to_string(),
|
||||
relation: SpatialRelationType::Adjacent,
|
||||
strength: 0.7,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let embedding = encoder.encode(&data).unwrap();
|
||||
assert_eq!(embedding.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_elements() {
|
||||
let config = test_config();
|
||||
let encoder = StageIIIEncoder::new(&config);
|
||||
|
||||
let data = StageIIIData {
|
||||
sketch_elements: vec![],
|
||||
relationships: vec![],
|
||||
};
|
||||
|
||||
assert!(encoder.encode(&data).is_err());
|
||||
}
|
||||
}
|
||||
339
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_iv.rs
Normal file
339
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_iv.rs
Normal file
@@ -0,0 +1,339 @@
|
||||
//! Stage IV Encoder: Emotional/AOL Data via SNN Temporal Encoding
|
||||
//!
|
||||
//! CRV Stage IV captures emotional impacts, tangibles, intangibles, and
|
||||
//! analytical overlay (AOL) detections. The spiking neural network (SNN)
|
||||
//! temporal encoding naturally models the signal-vs-noise discrimination
|
||||
//! that Stage IV demands:
|
||||
//!
|
||||
//! - High-frequency spike bursts correlate with AOL contamination
|
||||
//! - Sustained low-frequency patterns indicate clean signal line data
|
||||
//! - The refractory period prevents AOL cascade (analytical runaway)
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! Emotional intensity timeseries → SNN input currents.
|
||||
//! Network spike rate analysis detects AOL events.
|
||||
//! The embedding captures both the clean signal and AOL separation.
|
||||
|
||||
use crate::error::CrvResult;
|
||||
use crate::types::{AOLDetection, CrvConfig, StageIVData};
|
||||
use ruvector_mincut::snn::{LayerConfig, NetworkConfig, NeuronConfig, SpikingNetwork};
|
||||
|
||||
/// Stage IV encoder using spiking neural network temporal encoding.
|
||||
#[derive(Debug)]
|
||||
pub struct StageIVEncoder {
|
||||
/// Embedding dimensionality.
|
||||
dim: usize,
|
||||
/// AOL detection threshold (spike rate above this = likely AOL).
|
||||
aol_threshold: f32,
|
||||
/// SNN time step.
|
||||
dt: f64,
|
||||
/// Refractory period for AOL cascade prevention.
|
||||
refractory_period: f64,
|
||||
}
|
||||
|
||||
impl StageIVEncoder {
|
||||
/// Create a new Stage IV encoder.
|
||||
pub fn new(config: &CrvConfig) -> Self {
|
||||
Self {
|
||||
dim: config.dimensions,
|
||||
aol_threshold: config.aol_threshold,
|
||||
dt: config.snn_dt,
|
||||
refractory_period: config.refractory_period_ms,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a spiking network configured for emotional signal processing.
|
||||
///
|
||||
/// The network has 3 layers:
|
||||
/// - Input: receives emotional intensity as current
|
||||
/// - Hidden: processes temporal patterns
|
||||
/// - Output: produces the embedding dimensions
|
||||
fn create_network(&self, input_size: usize) -> SpikingNetwork {
|
||||
let hidden_size = (input_size * 2).max(16).min(128);
|
||||
let output_size = self.dim.min(64); // SNN output, will be expanded
|
||||
|
||||
let neuron_config = NeuronConfig {
|
||||
tau_membrane: 20.0,
|
||||
v_rest: 0.0,
|
||||
v_reset: 0.0,
|
||||
threshold: 1.0,
|
||||
t_refrac: self.refractory_period,
|
||||
resistance: 1.0,
|
||||
threshold_adapt: 0.1,
|
||||
tau_threshold: 100.0,
|
||||
homeostatic: true,
|
||||
target_rate: 0.01,
|
||||
tau_homeostatic: 1000.0,
|
||||
};
|
||||
|
||||
let config = NetworkConfig {
|
||||
layers: vec![
|
||||
LayerConfig::new(input_size).with_neuron_config(neuron_config.clone()),
|
||||
LayerConfig::new(hidden_size)
|
||||
.with_neuron_config(neuron_config.clone())
|
||||
.with_recurrence(),
|
||||
LayerConfig::new(output_size).with_neuron_config(neuron_config),
|
||||
],
|
||||
stdp_config: Default::default(),
|
||||
dt: self.dt,
|
||||
winner_take_all: false,
|
||||
wta_strength: 0.0,
|
||||
};
|
||||
|
||||
SpikingNetwork::new(config)
|
||||
}
|
||||
|
||||
/// Encode emotional intensity values into SNN input currents.
|
||||
fn emotional_to_currents(intensities: &[(String, f32)]) -> Vec<f64> {
|
||||
intensities
|
||||
.iter()
|
||||
.map(|(_, intensity)| *intensity as f64 * 5.0) // Scale to reasonable current
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Analyze spike output to detect AOL events.
|
||||
///
|
||||
/// High spike rate in a short window indicates the analytical mind
|
||||
/// is overriding the signal line (AOL contamination).
|
||||
fn detect_aol(
|
||||
&self,
|
||||
spike_rates: &[f64],
|
||||
window_ms: f64,
|
||||
) -> Vec<AOLDetection> {
|
||||
let mut detections = Vec::new();
|
||||
let threshold = self.aol_threshold as f64;
|
||||
|
||||
for (i, &rate) in spike_rates.iter().enumerate() {
|
||||
if rate > threshold {
|
||||
detections.push(AOLDetection {
|
||||
content: format!("AOL burst at timestep {}", i),
|
||||
timestamp_ms: (i as f64 * window_ms) as u64,
|
||||
flagged: rate > threshold * 1.5, // Auto-flag strong AOL
|
||||
anomaly_score: (rate / threshold).min(1.0) as f32,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
detections
|
||||
}
|
||||
|
||||
/// Encode Stage IV data into a temporal embedding.
|
||||
///
|
||||
/// Runs the SNN on emotional intensity data, analyzes spike patterns
|
||||
/// for AOL contamination, and produces a combined embedding that
|
||||
/// captures both clean signal and AOL separation.
|
||||
pub fn encode(&self, data: &StageIVData) -> CrvResult<Vec<f32>> {
|
||||
// Build input from emotional impact data
|
||||
let input_size = data.emotional_impact.len().max(1);
|
||||
let currents = Self::emotional_to_currents(&data.emotional_impact);
|
||||
|
||||
if currents.is_empty() {
|
||||
// Fall back to text-based encoding if no emotional intensity data
|
||||
return self.encode_from_text(data);
|
||||
}
|
||||
|
||||
// Run SNN simulation
|
||||
let mut network = self.create_network(input_size);
|
||||
let num_steps = 100; // 100ms simulation
|
||||
let mut spike_counts = vec![0usize; network.layer_size(network.num_layers() - 1)];
|
||||
let mut step_rates = Vec::new();
|
||||
|
||||
for step in 0..num_steps {
|
||||
// Inject currents (modulated by step for temporal variation)
|
||||
let modulated: Vec<f64> = currents
|
||||
.iter()
|
||||
.map(|&c| c * (1.0 + 0.3 * ((step as f64 * 0.1).sin())))
|
||||
.collect();
|
||||
network.inject_current(&modulated);
|
||||
|
||||
let spikes = network.step();
|
||||
for spike in &spikes {
|
||||
if spike.neuron_id < spike_counts.len() {
|
||||
spike_counts[spike.neuron_id] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Track rate per window
|
||||
if step % 10 == 9 {
|
||||
let rate = spikes.len() as f64 / 10.0;
|
||||
step_rates.push(rate);
|
||||
}
|
||||
}
|
||||
|
||||
// Build embedding from spike counts and output activities
|
||||
let output = network.get_output();
|
||||
let mut embedding = vec![0.0f32; self.dim];
|
||||
|
||||
// First portion: spike count features
|
||||
let spike_dims = spike_counts.len().min(self.dim / 3);
|
||||
let max_count = *spike_counts.iter().max().unwrap_or(&1) as f32;
|
||||
for (i, &count) in spike_counts.iter().take(spike_dims).enumerate() {
|
||||
embedding[i] = count as f32 / max_count.max(1.0);
|
||||
}
|
||||
|
||||
// Second portion: membrane potential output
|
||||
let pot_offset = self.dim / 3;
|
||||
let pot_dims = output.len().min(self.dim / 3);
|
||||
for (i, &v) in output.iter().take(pot_dims).enumerate() {
|
||||
if pot_offset + i < self.dim {
|
||||
embedding[pot_offset + i] = v as f32;
|
||||
}
|
||||
}
|
||||
|
||||
// Third portion: text-derived features from tangibles/intangibles
|
||||
let text_offset = 2 * self.dim / 3;
|
||||
self.encode_text_features(data, &mut embedding[text_offset..]);
|
||||
|
||||
// Encode AOL information
|
||||
let aol_detections = self.detect_aol(&step_rates, 10.0);
|
||||
let aol_count = (aol_detections.len() + data.aol_detections.len()) as f32;
|
||||
if self.dim > 2 {
|
||||
// Store AOL contamination level in last dimension
|
||||
embedding[self.dim - 1] = (aol_count / num_steps as f32).min(1.0);
|
||||
}
|
||||
|
||||
// L2 normalize
|
||||
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if norm > 1e-6 {
|
||||
for f in &mut embedding {
|
||||
*f /= norm;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(embedding)
|
||||
}
|
||||
|
||||
/// Text-based encoding fallback when no intensity timeseries is available.
|
||||
fn encode_from_text(&self, data: &StageIVData) -> CrvResult<Vec<f32>> {
|
||||
let mut embedding = vec![0.0f32; self.dim];
|
||||
self.encode_text_features(data, &mut embedding);
|
||||
|
||||
// L2 normalize
|
||||
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if norm > 1e-6 {
|
||||
for f in &mut embedding {
|
||||
*f /= norm;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(embedding)
|
||||
}
|
||||
|
||||
/// Encode text descriptors (tangibles, intangibles) into feature slots.
|
||||
fn encode_text_features(&self, data: &StageIVData, features: &mut [f32]) {
|
||||
if features.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hash tangibles
|
||||
for (i, tangible) in data.tangibles.iter().enumerate() {
|
||||
for (j, byte) in tangible.bytes().enumerate() {
|
||||
let idx = (i * 7 + j) % features.len();
|
||||
features[idx] += (byte as f32 / 255.0) * 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash intangibles
|
||||
for (i, intangible) in data.intangibles.iter().enumerate() {
|
||||
for (j, byte) in intangible.bytes().enumerate() {
|
||||
let idx = (i * 11 + j + features.len() / 2) % features.len();
|
||||
features[idx] += (byte as f32 / 255.0) * 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the AOL anomaly score for a given Stage IV embedding.
|
||||
///
|
||||
/// Higher values indicate more AOL contamination.
|
||||
pub fn aol_score(&self, embedding: &[f32]) -> f32 {
|
||||
if embedding.len() >= self.dim && self.dim > 2 {
|
||||
embedding[self.dim - 1].abs()
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_config() -> CrvConfig {
|
||||
CrvConfig {
|
||||
dimensions: 32,
|
||||
aol_threshold: 0.7,
|
||||
refractory_period_ms: 50.0,
|
||||
snn_dt: 1.0,
|
||||
..CrvConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encoder_creation() {
|
||||
let config = test_config();
|
||||
let encoder = StageIVEncoder::new(&config);
|
||||
assert_eq!(encoder.dim, 32);
|
||||
assert_eq!(encoder.aol_threshold, 0.7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_only_encode() {
|
||||
let config = test_config();
|
||||
let encoder = StageIVEncoder::new(&config);
|
||||
|
||||
let data = StageIVData {
|
||||
emotional_impact: vec![],
|
||||
tangibles: vec!["metal".to_string(), "concrete".to_string()],
|
||||
intangibles: vec!["historical significance".to_string()],
|
||||
aol_detections: vec![],
|
||||
};
|
||||
|
||||
let embedding = encoder.encode(&data).unwrap();
|
||||
assert_eq!(embedding.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_encode_with_snn() {
|
||||
let config = test_config();
|
||||
let encoder = StageIVEncoder::new(&config);
|
||||
|
||||
let data = StageIVData {
|
||||
emotional_impact: vec![
|
||||
("awe".to_string(), 0.8),
|
||||
("unease".to_string(), 0.3),
|
||||
("curiosity".to_string(), 0.6),
|
||||
],
|
||||
tangibles: vec!["stone wall".to_string()],
|
||||
intangibles: vec!["ancient purpose".to_string()],
|
||||
aol_detections: vec![AOLDetection {
|
||||
content: "looks like a castle".to_string(),
|
||||
timestamp_ms: 500,
|
||||
flagged: true,
|
||||
anomaly_score: 0.8,
|
||||
}],
|
||||
};
|
||||
|
||||
let embedding = encoder.encode(&data).unwrap();
|
||||
assert_eq!(embedding.len(), 32);
|
||||
|
||||
// Should be normalized
|
||||
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
assert!((norm - 1.0).abs() < 0.1 || norm < 0.01); // normalized or near-zero
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aol_detection() {
|
||||
let config = test_config();
|
||||
let encoder = StageIVEncoder::new(&config);
|
||||
|
||||
let rates = vec![0.1, 0.2, 0.9, 0.95, 0.3, 0.1];
|
||||
let detections = encoder.detect_aol(&rates, 10.0);
|
||||
|
||||
// Should detect the high-rate windows as AOL
|
||||
assert!(detections.len() >= 2);
|
||||
for d in &detections {
|
||||
assert!(d.anomaly_score > 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
222
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_v.rs
Normal file
222
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_v.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
//! Stage V: Interrogation via Differentiable Search with Soft Attention
|
||||
//!
|
||||
//! CRV Stage V involves probing the signal line by asking targeted questions
|
||||
//! about specific aspects of the target, then cross-referencing results
|
||||
//! across all accumulated data from Stages I-IV.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! Uses `ruvector_gnn::search::differentiable_search` to find the most
|
||||
//! relevant data entries for each probe query, with soft attention weights
|
||||
//! providing a continuous similarity measure rather than hard thresholds.
|
||||
//! This enables gradient-based refinement of probe queries.
|
||||
|
||||
use crate::error::{CrvError, CrvResult};
|
||||
use crate::types::{CrossReference, CrvConfig, SignalLineProbe, StageVData};
|
||||
use ruvector_gnn::search::{cosine_similarity, differentiable_search};
|
||||
|
||||
/// Stage V interrogation engine using differentiable search.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StageVEngine {
|
||||
/// Embedding dimensionality.
|
||||
dim: usize,
|
||||
/// Temperature for differentiable search softmax.
|
||||
temperature: f32,
|
||||
}
|
||||
|
||||
impl StageVEngine {
|
||||
/// Create a new Stage V engine.
|
||||
pub fn new(config: &CrvConfig) -> Self {
|
||||
Self {
|
||||
dim: config.dimensions,
|
||||
temperature: config.search_temperature,
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe the accumulated session embeddings with a query.
|
||||
///
|
||||
/// Performs differentiable search over the given candidate embeddings,
|
||||
/// returning soft attention weights and top-k candidates.
|
||||
pub fn probe(
|
||||
&self,
|
||||
query_embedding: &[f32],
|
||||
candidates: &[Vec<f32>],
|
||||
k: usize,
|
||||
) -> CrvResult<SignalLineProbe> {
|
||||
if candidates.is_empty() {
|
||||
return Err(CrvError::EmptyInput(
|
||||
"No candidates for probing".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let (top_candidates, attention_weights) =
|
||||
differentiable_search(query_embedding, candidates, k, self.temperature);
|
||||
|
||||
Ok(SignalLineProbe {
|
||||
query: String::new(), // Caller sets the text
|
||||
target_stage: 0, // Caller sets the stage
|
||||
attention_weights,
|
||||
top_candidates,
|
||||
})
|
||||
}
|
||||
|
||||
/// Cross-reference entries across stages to find correlations.
|
||||
///
|
||||
/// For each entry in `from_entries`, finds the most similar entries
|
||||
/// in `to_entries` using cosine similarity, producing cross-references
|
||||
/// above the given threshold.
|
||||
pub fn cross_reference(
|
||||
&self,
|
||||
from_stage: u8,
|
||||
from_entries: &[Vec<f32>],
|
||||
to_stage: u8,
|
||||
to_entries: &[Vec<f32>],
|
||||
threshold: f32,
|
||||
) -> Vec<CrossReference> {
|
||||
let mut refs = Vec::new();
|
||||
|
||||
for (from_idx, from_emb) in from_entries.iter().enumerate() {
|
||||
for (to_idx, to_emb) in to_entries.iter().enumerate() {
|
||||
if from_emb.len() == to_emb.len() {
|
||||
let score = cosine_similarity(from_emb, to_emb);
|
||||
if score >= threshold {
|
||||
refs.push(CrossReference {
|
||||
from_stage,
|
||||
from_entry: from_idx,
|
||||
to_stage,
|
||||
to_entry: to_idx,
|
||||
score,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score descending
|
||||
refs.sort_by(|a, b| {
|
||||
b.score
|
||||
.partial_cmp(&a.score)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
refs
|
||||
}
|
||||
|
||||
/// Encode Stage V data into a combined interrogation embedding.
|
||||
///
|
||||
/// Aggregates the attention weights from all probes to produce
|
||||
/// a unified view of which aspects of the target were most
|
||||
/// responsive to interrogation.
|
||||
pub fn encode(&self, data: &StageVData, all_embeddings: &[Vec<f32>]) -> CrvResult<Vec<f32>> {
|
||||
if data.probes.is_empty() {
|
||||
return Err(CrvError::EmptyInput("No probes in Stage V data".to_string()));
|
||||
}
|
||||
|
||||
let mut embedding = vec![0.0f32; self.dim];
|
||||
|
||||
// Weight each candidate embedding by its attention weight across all probes
|
||||
for probe in &data.probes {
|
||||
for (&candidate_idx, &weight) in probe
|
||||
.top_candidates
|
||||
.iter()
|
||||
.zip(probe.attention_weights.iter())
|
||||
{
|
||||
if candidate_idx < all_embeddings.len() {
|
||||
let emb = &all_embeddings[candidate_idx];
|
||||
for (i, &v) in emb.iter().enumerate() {
|
||||
if i < self.dim {
|
||||
embedding[i] += v * weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize by number of probes
|
||||
let num_probes = data.probes.len() as f32;
|
||||
for v in &mut embedding {
|
||||
*v /= num_probes;
|
||||
}
|
||||
|
||||
Ok(embedding)
|
||||
}
|
||||
|
||||
/// Compute the interrogation signal strength for a given embedding.
|
||||
///
|
||||
/// Higher values indicate more responsive signal line data.
|
||||
pub fn signal_strength(&self, embedding: &[f32]) -> f32 {
|
||||
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
norm
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_config() -> CrvConfig {
|
||||
CrvConfig {
|
||||
dimensions: 8,
|
||||
search_temperature: 1.0,
|
||||
..CrvConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engine_creation() {
|
||||
let config = test_config();
|
||||
let engine = StageVEngine::new(&config);
|
||||
assert_eq!(engine.dim, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_probe() {
|
||||
let config = test_config();
|
||||
let engine = StageVEngine::new(&config);
|
||||
|
||||
let query = vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
let candidates = vec![
|
||||
vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], // exact match
|
||||
vec![0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], // partial
|
||||
vec![0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], // orthogonal
|
||||
];
|
||||
|
||||
let probe = engine.probe(&query, &candidates, 2).unwrap();
|
||||
assert_eq!(probe.top_candidates.len(), 2);
|
||||
assert_eq!(probe.attention_weights.len(), 2);
|
||||
// Best match should be first
|
||||
assert_eq!(probe.top_candidates[0], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cross_reference() {
|
||||
let config = test_config();
|
||||
let engine = StageVEngine::new(&config);
|
||||
|
||||
let from = vec![
|
||||
vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
vec![0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
];
|
||||
let to = vec![
|
||||
vec![0.9, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], // similar to from[0]
|
||||
vec![0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], // different
|
||||
];
|
||||
|
||||
let refs = engine.cross_reference(1, &from, 2, &to, 0.5);
|
||||
assert!(!refs.is_empty());
|
||||
assert_eq!(refs[0].from_stage, 1);
|
||||
assert_eq!(refs[0].to_stage, 2);
|
||||
assert!(refs[0].score > 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_probe() {
|
||||
let config = test_config();
|
||||
let engine = StageVEngine::new(&config);
|
||||
|
||||
let query = vec![1.0; 8];
|
||||
let candidates: Vec<Vec<f32>> = vec![];
|
||||
|
||||
assert!(engine.probe(&query, &candidates, 5).is_err());
|
||||
}
|
||||
}
|
||||
387
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_vi.rs
Normal file
387
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_vi.rs
Normal file
@@ -0,0 +1,387 @@
|
||||
//! Stage VI: Composite Modeling via MinCut Partitioning
|
||||
//!
|
||||
//! CRV Stage VI builds a composite 3D model from all accumulated session data.
|
||||
//! The MinCut algorithm identifies natural cluster boundaries in the session
|
||||
//! graph, separating distinct target aspects that emerged across stages.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! All session embeddings form nodes in a weighted graph, with edge weights
|
||||
//! derived from cosine similarity. MinCut partitioning finds the natural
|
||||
//! separations between target aspects, producing distinct partitions that
|
||||
//! represent different facets of the target.
|
||||
|
||||
use crate::error::{CrvError, CrvResult};
|
||||
use crate::types::{CrvConfig, StageVIData, TargetPartition};
|
||||
use ruvector_gnn::search::cosine_similarity;
|
||||
use ruvector_mincut::prelude::*;
|
||||
|
||||
/// Stage VI composite modeler using MinCut partitioning.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StageVIModeler {
|
||||
/// Embedding dimensionality.
|
||||
dim: usize,
|
||||
/// Minimum edge weight to create an edge (similarity threshold).
|
||||
edge_threshold: f32,
|
||||
}
|
||||
|
||||
impl StageVIModeler {
|
||||
/// Create a new Stage VI modeler.
|
||||
pub fn new(config: &CrvConfig) -> Self {
|
||||
Self {
|
||||
dim: config.dimensions,
|
||||
edge_threshold: 0.2, // Low threshold to capture weak relationships too
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a similarity graph from session embeddings.
|
||||
///
|
||||
/// Each embedding becomes a vertex. Edges are created between
|
||||
/// pairs with cosine similarity above the threshold, with
|
||||
/// edge weight equal to the similarity score.
|
||||
fn build_similarity_graph(&self, embeddings: &[Vec<f32>]) -> Vec<(u64, u64, f64)> {
|
||||
let n = embeddings.len();
|
||||
let mut edges = Vec::new();
|
||||
|
||||
for i in 0..n {
|
||||
for j in (i + 1)..n {
|
||||
if embeddings[i].len() == embeddings[j].len() && !embeddings[i].is_empty() {
|
||||
let sim = cosine_similarity(&embeddings[i], &embeddings[j]);
|
||||
if sim > self.edge_threshold {
|
||||
edges.push((i as u64 + 1, j as u64 + 1, sim as f64));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
edges
|
||||
}
|
||||
|
||||
/// Compute centroid of a set of embeddings.
|
||||
fn compute_centroid(&self, embeddings: &[&[f32]]) -> Vec<f32> {
|
||||
if embeddings.is_empty() {
|
||||
return vec![0.0; self.dim];
|
||||
}
|
||||
|
||||
let mut centroid = vec![0.0f32; self.dim];
|
||||
for emb in embeddings {
|
||||
for (i, &v) in emb.iter().enumerate() {
|
||||
if i < self.dim {
|
||||
centroid[i] += v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let n = embeddings.len() as f32;
|
||||
for v in &mut centroid {
|
||||
*v /= n;
|
||||
}
|
||||
|
||||
centroid
|
||||
}
|
||||
|
||||
/// Partition session embeddings into target aspects using MinCut.
|
||||
///
|
||||
/// Returns the MinCut-based partition assignments and centroids.
|
||||
pub fn partition(
|
||||
&self,
|
||||
embeddings: &[Vec<f32>],
|
||||
stage_labels: &[(u8, usize)], // (stage, entry_index) for each embedding
|
||||
) -> CrvResult<StageVIData> {
|
||||
if embeddings.len() < 2 {
|
||||
// With fewer than 2 embeddings, return a single partition
|
||||
let centroid = if embeddings.is_empty() {
|
||||
vec![0.0; self.dim]
|
||||
} else {
|
||||
embeddings[0].clone()
|
||||
};
|
||||
|
||||
return Ok(StageVIData {
|
||||
partitions: vec![TargetPartition {
|
||||
label: "primary".to_string(),
|
||||
member_entries: stage_labels.to_vec(),
|
||||
centroid,
|
||||
separation_strength: 0.0,
|
||||
}],
|
||||
composite_description: "Single-aspect target".to_string(),
|
||||
partition_confidence: vec![1.0],
|
||||
});
|
||||
}
|
||||
|
||||
// Build similarity graph
|
||||
let edges = self.build_similarity_graph(embeddings);
|
||||
|
||||
if edges.is_empty() {
|
||||
// No significant similarities found - each embedding is its own partition
|
||||
let partitions: Vec<TargetPartition> = embeddings
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, emb)| TargetPartition {
|
||||
label: format!("aspect-{}", i),
|
||||
member_entries: if i < stage_labels.len() {
|
||||
vec![stage_labels[i]]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
centroid: emb.clone(),
|
||||
separation_strength: 1.0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let n = partitions.len();
|
||||
return Ok(StageVIData {
|
||||
partitions,
|
||||
composite_description: format!("{} disconnected aspects", n),
|
||||
partition_confidence: vec![0.5; n],
|
||||
});
|
||||
}
|
||||
|
||||
// Build MinCut structure
|
||||
let mincut_result = MinCutBuilder::new()
|
||||
.exact()
|
||||
.with_edges(edges.clone())
|
||||
.build();
|
||||
|
||||
let mincut = match mincut_result {
|
||||
Ok(mc) => mc,
|
||||
Err(_) => {
|
||||
// Fallback: single partition
|
||||
let centroid = self.compute_centroid(
|
||||
&embeddings.iter().map(|e| e.as_slice()).collect::<Vec<_>>(),
|
||||
);
|
||||
return Ok(StageVIData {
|
||||
partitions: vec![TargetPartition {
|
||||
label: "composite".to_string(),
|
||||
member_entries: stage_labels.to_vec(),
|
||||
centroid,
|
||||
separation_strength: 0.0,
|
||||
}],
|
||||
composite_description: "Unified composite model".to_string(),
|
||||
partition_confidence: vec![0.8],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let cut_value = mincut.min_cut_value();
|
||||
|
||||
// Use the MinCut value to determine partition boundary.
|
||||
// We partition into two groups based on connectivity:
|
||||
// vertices more connected to the "left" side vs "right" side.
|
||||
let n = embeddings.len();
|
||||
|
||||
// Simple 2-partition based on similarity to first vs last embedding
|
||||
let (group_a, group_b) = self.bisect_by_similarity(embeddings);
|
||||
|
||||
let centroid_a = self.compute_centroid(
|
||||
&group_a.iter().map(|&i| embeddings[i].as_slice()).collect::<Vec<_>>(),
|
||||
);
|
||||
let centroid_b = self.compute_centroid(
|
||||
&group_b.iter().map(|&i| embeddings[i].as_slice()).collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let members_a: Vec<(u8, usize)> = group_a
|
||||
.iter()
|
||||
.filter_map(|&i| stage_labels.get(i).copied())
|
||||
.collect();
|
||||
let members_b: Vec<(u8, usize)> = group_b
|
||||
.iter()
|
||||
.filter_map(|&i| stage_labels.get(i).copied())
|
||||
.collect();
|
||||
|
||||
let partitions = vec![
|
||||
TargetPartition {
|
||||
label: "primary-aspect".to_string(),
|
||||
member_entries: members_a,
|
||||
centroid: centroid_a,
|
||||
separation_strength: cut_value as f32,
|
||||
},
|
||||
TargetPartition {
|
||||
label: "secondary-aspect".to_string(),
|
||||
member_entries: members_b,
|
||||
centroid: centroid_b,
|
||||
separation_strength: cut_value as f32,
|
||||
},
|
||||
];
|
||||
|
||||
// Confidence based on separation strength
|
||||
let total_edges = edges.len() as f32;
|
||||
let conf = if total_edges > 0.0 {
|
||||
(cut_value as f32 / total_edges).min(1.0)
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
|
||||
Ok(StageVIData {
|
||||
partitions,
|
||||
composite_description: format!(
|
||||
"Bisected composite: {} embeddings, cut value {:.3}",
|
||||
n, cut_value
|
||||
),
|
||||
partition_confidence: vec![conf, conf],
|
||||
})
|
||||
}
|
||||
|
||||
/// Bisect embeddings into two groups by maximizing inter-group dissimilarity.
|
||||
///
|
||||
/// Uses a greedy approach: pick the two most dissimilar embeddings as seeds,
|
||||
/// then assign each remaining embedding to the nearer seed.
|
||||
fn bisect_by_similarity(&self, embeddings: &[Vec<f32>]) -> (Vec<usize>, Vec<usize>) {
|
||||
let n = embeddings.len();
|
||||
if n <= 1 {
|
||||
return ((0..n).collect(), vec![]);
|
||||
}
|
||||
|
||||
// Find the two most dissimilar embeddings
|
||||
let mut min_sim = f32::MAX;
|
||||
let mut seed_a = 0;
|
||||
let mut seed_b = 1;
|
||||
|
||||
for i in 0..n {
|
||||
for j in (i + 1)..n {
|
||||
if embeddings[i].len() == embeddings[j].len() && !embeddings[i].is_empty() {
|
||||
let sim = cosine_similarity(&embeddings[i], &embeddings[j]);
|
||||
if sim < min_sim {
|
||||
min_sim = sim;
|
||||
seed_a = i;
|
||||
seed_b = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut group_a = vec![seed_a];
|
||||
let mut group_b = vec![seed_b];
|
||||
|
||||
for i in 0..n {
|
||||
if i == seed_a || i == seed_b {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sim_a = if embeddings[i].len() == embeddings[seed_a].len() {
|
||||
cosine_similarity(&embeddings[i], &embeddings[seed_a])
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let sim_b = if embeddings[i].len() == embeddings[seed_b].len() {
|
||||
cosine_similarity(&embeddings[i], &embeddings[seed_b])
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
if sim_a >= sim_b {
|
||||
group_a.push(i);
|
||||
} else {
|
||||
group_b.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
(group_a, group_b)
|
||||
}
|
||||
|
||||
/// Encode the Stage VI partition result into a single embedding.
|
||||
///
|
||||
/// Produces a weighted combination of partition centroids.
|
||||
pub fn encode(&self, data: &StageVIData) -> CrvResult<Vec<f32>> {
|
||||
if data.partitions.is_empty() {
|
||||
return Err(CrvError::EmptyInput("No partitions".to_string()));
|
||||
}
|
||||
|
||||
let mut embedding = vec![0.0f32; self.dim];
|
||||
let mut total_weight = 0.0f32;
|
||||
|
||||
for (partition, &confidence) in data.partitions.iter().zip(data.partition_confidence.iter()) {
|
||||
let weight = confidence * partition.member_entries.len() as f32;
|
||||
for (i, &v) in partition.centroid.iter().enumerate() {
|
||||
if i < self.dim {
|
||||
embedding[i] += v * weight;
|
||||
}
|
||||
}
|
||||
total_weight += weight;
|
||||
}
|
||||
|
||||
if total_weight > 1e-6 {
|
||||
for v in &mut embedding {
|
||||
*v /= total_weight;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(embedding)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_config() -> CrvConfig {
|
||||
CrvConfig {
|
||||
dimensions: 8,
|
||||
..CrvConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modeler_creation() {
|
||||
let config = test_config();
|
||||
let modeler = StageVIModeler::new(&config);
|
||||
assert_eq!(modeler.dim, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partition_single() {
|
||||
let config = test_config();
|
||||
let modeler = StageVIModeler::new(&config);
|
||||
|
||||
let embeddings = vec![vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]];
|
||||
let labels = vec![(1, 0)];
|
||||
|
||||
let result = modeler.partition(&embeddings, &labels).unwrap();
|
||||
assert_eq!(result.partitions.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partition_two_clusters() {
|
||||
let config = test_config();
|
||||
let modeler = StageVIModeler::new(&config);
|
||||
|
||||
// Two clearly separated clusters
|
||||
let embeddings = vec![
|
||||
vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
vec![0.9, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
vec![0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0],
|
||||
vec![0.0, 0.0, 0.0, 0.0, 0.9, 0.1, 0.0, 0.0],
|
||||
];
|
||||
let labels = vec![(1, 0), (2, 0), (3, 0), (4, 0)];
|
||||
|
||||
let result = modeler.partition(&embeddings, &labels).unwrap();
|
||||
assert_eq!(result.partitions.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_partitions() {
|
||||
let config = test_config();
|
||||
let modeler = StageVIModeler::new(&config);
|
||||
|
||||
let data = StageVIData {
|
||||
partitions: vec![
|
||||
TargetPartition {
|
||||
label: "a".to_string(),
|
||||
member_entries: vec![(1, 0), (2, 0)],
|
||||
centroid: vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
separation_strength: 0.5,
|
||||
},
|
||||
TargetPartition {
|
||||
label: "b".to_string(),
|
||||
member_entries: vec![(3, 0)],
|
||||
centroid: vec![0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
separation_strength: 0.5,
|
||||
},
|
||||
],
|
||||
composite_description: "test".to_string(),
|
||||
partition_confidence: vec![0.8, 0.6],
|
||||
};
|
||||
|
||||
let embedding = modeler.encode(&data).unwrap();
|
||||
assert_eq!(embedding.len(), 8);
|
||||
}
|
||||
}
|
||||
360
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/types.rs
Normal file
360
rust-port/wifi-densepose-rs/patches/ruvector-crv/src/types.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
//! Core types for the CRV (Coordinate Remote Viewing) protocol.
|
||||
//!
|
||||
//! Defines the data structures for the 6-stage CRV signal line methodology,
|
||||
//! session management, and analytical overlay (AOL) detection.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Unique identifier for a CRV session.
|
||||
pub type SessionId = String;
|
||||
|
||||
/// Unique identifier for a target coordinate.
|
||||
pub type TargetCoordinate = String;
|
||||
|
||||
/// Unique identifier for a stage data entry.
|
||||
pub type EntryId = String;
|
||||
|
||||
/// Classification of gestalt primitives in Stage I.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum GestaltType {
|
||||
/// Human-made structures, artifacts
|
||||
Manmade,
|
||||
/// Organic, natural formations
|
||||
Natural,
|
||||
/// Dynamic, kinetic signals
|
||||
Movement,
|
||||
/// Thermal, electromagnetic, force
|
||||
Energy,
|
||||
/// Aqueous, fluid, wet
|
||||
Water,
|
||||
/// Solid, terrain, geological
|
||||
Land,
|
||||
}
|
||||
|
||||
impl GestaltType {
|
||||
/// Returns all gestalt types for iteration.
|
||||
pub fn all() -> &'static [GestaltType] {
|
||||
&[
|
||||
GestaltType::Manmade,
|
||||
GestaltType::Natural,
|
||||
GestaltType::Movement,
|
||||
GestaltType::Energy,
|
||||
GestaltType::Water,
|
||||
GestaltType::Land,
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns the index of this gestalt type in the canonical ordering.
|
||||
pub fn index(&self) -> usize {
|
||||
match self {
|
||||
GestaltType::Manmade => 0,
|
||||
GestaltType::Natural => 1,
|
||||
GestaltType::Movement => 2,
|
||||
GestaltType::Energy => 3,
|
||||
GestaltType::Water => 4,
|
||||
GestaltType::Land => 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stage I data: Ideogram traces and gestalt classifications.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StageIData {
|
||||
/// Raw ideogram stroke trace as a sequence of (x, y) coordinates.
|
||||
pub stroke: Vec<(f32, f32)>,
|
||||
/// First spontaneous descriptor word.
|
||||
pub spontaneous_descriptor: String,
|
||||
/// Classified gestalt type.
|
||||
pub classification: GestaltType,
|
||||
/// Confidence in the classification (0.0 - 1.0).
|
||||
pub confidence: f32,
|
||||
}
|
||||
|
||||
/// Sensory modality for Stage II data.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum SensoryModality {
|
||||
/// Surface textures (smooth, rough, grainy, etc.)
|
||||
Texture,
|
||||
/// Visual colors and patterns
|
||||
Color,
|
||||
/// Thermal impressions (hot, cold, warm)
|
||||
Temperature,
|
||||
/// Auditory impressions
|
||||
Sound,
|
||||
/// Olfactory impressions
|
||||
Smell,
|
||||
/// Taste impressions
|
||||
Taste,
|
||||
/// Size/scale impressions (large, small, vast)
|
||||
Dimension,
|
||||
/// Luminosity (bright, dark, glowing)
|
||||
Luminosity,
|
||||
}
|
||||
|
||||
/// Stage II data: Sensory impressions.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StageIIData {
|
||||
/// Sensory impressions as modality-descriptor pairs.
|
||||
pub impressions: Vec<(SensoryModality, String)>,
|
||||
/// Raw sensory feature vector (encoded from descriptors).
|
||||
pub feature_vector: Option<Vec<f32>>,
|
||||
}
|
||||
|
||||
/// Stage III data: Dimensional and spatial relationships.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StageIIIData {
|
||||
/// Spatial sketch as a set of named geometric primitives.
|
||||
pub sketch_elements: Vec<SketchElement>,
|
||||
/// Spatial relationships between elements.
|
||||
pub relationships: Vec<SpatialRelationship>,
|
||||
}
|
||||
|
||||
/// A geometric element in a Stage III sketch.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SketchElement {
|
||||
/// Unique label for this element.
|
||||
pub label: String,
|
||||
/// Type of geometric primitive.
|
||||
pub kind: GeometricKind,
|
||||
/// Position in sketch space (x, y).
|
||||
pub position: (f32, f32),
|
||||
/// Optional size/scale.
|
||||
pub scale: Option<f32>,
|
||||
}
|
||||
|
||||
/// Types of geometric primitives.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum GeometricKind {
|
||||
Point,
|
||||
Line,
|
||||
Curve,
|
||||
Rectangle,
|
||||
Circle,
|
||||
Triangle,
|
||||
Polygon,
|
||||
Freeform,
|
||||
}
|
||||
|
||||
/// Spatial relationship between two sketch elements.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpatialRelationship {
|
||||
/// Source element label.
|
||||
pub from: String,
|
||||
/// Target element label.
|
||||
pub to: String,
|
||||
/// Relationship type.
|
||||
pub relation: SpatialRelationType,
|
||||
/// Strength of the relationship (0.0 - 1.0).
|
||||
pub strength: f32,
|
||||
}
|
||||
|
||||
/// Types of spatial relationships.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SpatialRelationType {
|
||||
Adjacent,
|
||||
Contains,
|
||||
Above,
|
||||
Below,
|
||||
Inside,
|
||||
Surrounding,
|
||||
Connected,
|
||||
Separated,
|
||||
}
|
||||
|
||||
/// Stage IV data: Emotional, aesthetic, and intangible impressions.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StageIVData {
|
||||
/// Emotional impact descriptors with intensity.
|
||||
pub emotional_impact: Vec<(String, f32)>,
|
||||
/// Tangible object impressions.
|
||||
pub tangibles: Vec<String>,
|
||||
/// Intangible concept impressions (purpose, function, significance).
|
||||
pub intangibles: Vec<String>,
|
||||
/// Analytical overlay detections with timestamps.
|
||||
pub aol_detections: Vec<AOLDetection>,
|
||||
}
|
||||
|
||||
/// An analytical overlay (AOL) detection event.
|
||||
///
|
||||
/// AOL occurs when the viewer's analytical mind attempts to assign
|
||||
/// a known label/concept to incoming signal line data, potentially
|
||||
/// contaminating the raw perception.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AOLDetection {
|
||||
/// The AOL content (what the viewer's mind jumped to).
|
||||
pub content: String,
|
||||
/// Timestamp within the session (milliseconds from start).
|
||||
pub timestamp_ms: u64,
|
||||
/// Whether it was flagged and set aside ("AOL break").
|
||||
pub flagged: bool,
|
||||
/// Anomaly score from spike rate analysis (0.0 - 1.0).
|
||||
/// Higher scores indicate stronger AOL contamination.
|
||||
pub anomaly_score: f32,
|
||||
}
|
||||
|
||||
/// Stage V data: Interrogation and cross-referencing results.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StageVData {
|
||||
/// Probe queries and their results.
|
||||
pub probes: Vec<SignalLineProbe>,
|
||||
/// Cross-references to data from earlier stages.
|
||||
pub cross_references: Vec<CrossReference>,
|
||||
}
|
||||
|
||||
/// A signal line probe query.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SignalLineProbe {
|
||||
/// The question or aspect being probed.
|
||||
pub query: String,
|
||||
/// Stage being interrogated.
|
||||
pub target_stage: u8,
|
||||
/// Resulting soft attention weights over candidates.
|
||||
pub attention_weights: Vec<f32>,
|
||||
/// Top-k candidate indices from differentiable search.
|
||||
pub top_candidates: Vec<usize>,
|
||||
}
|
||||
|
||||
/// A cross-reference between stage data entries.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CrossReference {
|
||||
/// Source stage number.
|
||||
pub from_stage: u8,
|
||||
/// Source entry index.
|
||||
pub from_entry: usize,
|
||||
/// Target stage number.
|
||||
pub to_stage: u8,
|
||||
/// Target entry index.
|
||||
pub to_entry: usize,
|
||||
/// Similarity/relevance score.
|
||||
pub score: f32,
|
||||
}
|
||||
|
||||
/// Stage VI data: Composite 3D model from accumulated session data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StageVIData {
|
||||
/// Cluster partitions discovered by MinCut.
|
||||
pub partitions: Vec<TargetPartition>,
|
||||
/// Overall composite descriptor.
|
||||
pub composite_description: String,
|
||||
/// Confidence scores per partition.
|
||||
pub partition_confidence: Vec<f32>,
|
||||
}
|
||||
|
||||
/// A partition of the target, representing a distinct aspect or component.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TargetPartition {
|
||||
/// Human-readable label for this partition.
|
||||
pub label: String,
|
||||
/// Stage data entry indices that belong to this partition.
|
||||
pub member_entries: Vec<(u8, usize)>,
|
||||
/// Centroid embedding of this partition.
|
||||
pub centroid: Vec<f32>,
|
||||
/// MinCut value separating this partition from others.
|
||||
pub separation_strength: f32,
|
||||
}
|
||||
|
||||
/// A complete CRV session entry stored in the database.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CrvSessionEntry {
|
||||
/// Session identifier.
|
||||
pub session_id: SessionId,
|
||||
/// Target coordinate.
|
||||
pub coordinate: TargetCoordinate,
|
||||
/// CRV stage (1-6).
|
||||
pub stage: u8,
|
||||
/// Embedding vector for this entry.
|
||||
pub embedding: Vec<f32>,
|
||||
/// Arbitrary metadata.
|
||||
pub metadata: HashMap<String, serde_json::Value>,
|
||||
/// Timestamp in milliseconds.
|
||||
pub timestamp_ms: u64,
|
||||
}
|
||||
|
||||
/// Configuration for CRV session processing.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CrvConfig {
|
||||
/// Embedding dimensionality.
|
||||
pub dimensions: usize,
|
||||
/// Curvature for Poincare ball (Stage I). Positive value.
|
||||
pub curvature: f32,
|
||||
/// AOL anomaly detection threshold (Stage IV).
|
||||
pub aol_threshold: f32,
|
||||
/// SNN refractory period in ms (Stage IV).
|
||||
pub refractory_period_ms: f64,
|
||||
/// SNN time step in ms (Stage IV).
|
||||
pub snn_dt: f64,
|
||||
/// Differentiable search temperature (Stage V).
|
||||
pub search_temperature: f32,
|
||||
/// Convergence threshold for cross-session matching.
|
||||
pub convergence_threshold: f32,
|
||||
}
|
||||
|
||||
impl Default for CrvConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dimensions: 384,
|
||||
curvature: 1.0,
|
||||
aol_threshold: 0.7,
|
||||
refractory_period_ms: 50.0,
|
||||
snn_dt: 1.0,
|
||||
search_temperature: 1.0,
|
||||
convergence_threshold: 0.75,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a convergence analysis across multiple sessions.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConvergenceResult {
|
||||
/// Session pairs that converged.
|
||||
pub session_pairs: Vec<(SessionId, SessionId)>,
|
||||
/// Convergence scores per pair.
|
||||
pub scores: Vec<f32>,
|
||||
/// Stages where convergence was strongest.
|
||||
pub convergent_stages: Vec<u8>,
|
||||
/// Merged embedding representing the consensus signal.
|
||||
pub consensus_embedding: Option<Vec<f32>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_gestalt_type_all() {
|
||||
let all = GestaltType::all();
|
||||
assert_eq!(all.len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gestalt_type_index() {
|
||||
assert_eq!(GestaltType::Manmade.index(), 0);
|
||||
assert_eq!(GestaltType::Land.index(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = CrvConfig::default();
|
||||
assert_eq!(config.dimensions, 384);
|
||||
assert_eq!(config.curvature, 1.0);
|
||||
assert_eq!(config.aol_threshold, 0.7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_entry_serialization() {
|
||||
let entry = CrvSessionEntry {
|
||||
session_id: "sess-001".to_string(),
|
||||
coordinate: "1234-5678".to_string(),
|
||||
stage: 1,
|
||||
embedding: vec![0.1, 0.2, 0.3],
|
||||
metadata: HashMap::new(),
|
||||
timestamp_ms: 1000,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&entry).unwrap();
|
||||
let deserialized: CrvSessionEntry = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized.session_id, "sess-001");
|
||||
assert_eq!(deserialized.stage, 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user