diff --git a/docs/adr/ADR-033-crv-signal-line-sensing-integration.md b/docs/adr/ADR-033-crv-signal-line-sensing-integration.md new file mode 100644 index 0000000..c7b644b --- /dev/null +++ b/docs/adr/ADR-033-crv-signal-line-sensing-integration.md @@ -0,0 +1,740 @@ +# ADR-033: CRV Signal Line Sensing Integration -- Mapping 6-Stage Coordinate Remote Viewing to WiFi-DensePose Pipeline + +| Field | Value | +|-------|-------| +| **Status** | Proposed | +| **Date** | 2026-03-01 | +| **Deciders** | ruv | +| **Codename** | **CRV-Sense** -- Coordinate Remote Viewing Signal Line for WiFi Sensing | +| **Relates to** | ADR-016 (RuVector Integration), ADR-017 (RuVector Signal+MAT), ADR-024 (AETHER Embeddings), ADR-029 (RuvSense Multistatic), ADR-030 (Persistent Field Model), ADR-031 (RuView Viewpoint Fusion), ADR-032 (Mesh Security) | + +--- + +## 1. Context + +### 1.1 The CRV Signal Line Methodology + +Coordinate Remote Viewing (CRV) is a structured 6-stage protocol that progressively refines perception from coarse gestalt impressions (Stage I) through sensory details (Stage II), spatial dimensions (Stage III), noise separation (Stage IV), cross-referencing interrogation (Stage V), to a final composite 3D model (Stage VI). The `ruvector-crv` crate (v0.1.1, published on crates.io) maps these 6 stages to vector database subsystems: Poincare ball embeddings, multi-head attention, GNN graph topology, SNN temporal encoding, differentiable search, and MinCut partitioning. + +The WiFi-DensePose sensing pipeline follows a strikingly similar progressive refinement: + +1. Raw CSI arrives as an undifferentiated signal -- the system must first classify the gestalt character of the RF environment. +2. Per-subcarrier amplitude/phase/frequency features are extracted -- analogous to sensory impressions. +3. The AP mesh forms a spatial topology with node positions and link geometry -- a dimensional sketch. +4. Coherence gating separates valid signal from noise and interference -- analytically overlaid artifacts must be detected and removed. +5. Pose estimation queries earlier CSI features for cross-referencing -- interrogation of the accumulated evidence. +6. Final multi-person partitioning produces the composite DensePose output -- the 3D model. + +This structural isomorphism is not accidental. Both CRV and WiFi sensing solve the same abstract problem: extract structured information from a noisy, high-dimensional signal space through progressive refinement with explicit noise separation. + +### 1.2 The ruvector-crv Crate (v0.1.1) + +The `ruvector-crv` crate provides the following public API: + +| Component | Purpose | Upstream Dependency | +|-----------|---------|-------------------| +| `CrvSessionManager` | Session lifecycle: create, add stage data, convergence analysis | -- | +| `StageIEncoder` | Poincare ball hyperbolic embeddings for gestalt primitives | -- (internal hyperbolic math) | +| `StageIIEncoder` | Multi-head attention for sensory vectors | `ruvector-attention` | +| `StageIIIEncoder` | GNN graph topology encoding | `ruvector-gnn` | +| `StageIVEncoder` | SNN temporal encoding for AOL (Analytical Overlay) detection | -- (internal SNN) | +| `StageVEngine` | Differentiable search and cross-referencing | -- (internal soft attention) | +| `StageVIModeler` | MinCut partitioning for composite model | `ruvector-mincut` | +| `ConvergenceResult` | Cross-session agreement analysis | -- | +| `CrvConfig` | Configuration (384-d default, curvature, AOL threshold, SNN params) | -- | + +Key types: `GestaltType` (Manmade/Natural/Movement/Energy/Water/Land), `SensoryModality` (Texture/Color/Temperature/Sound/...), `AOLDetection` (content + anomaly score), `SignalLineProbe` (query + attention weights), `TargetPartition` (MinCut cluster + centroid). + +### 1.3 What Already Exists in WiFi-DensePose + +The following modules already implement pieces of the pipeline that CRV stages map onto: + +| Existing Module | Location | Relevant CRV Stage | +|----------------|----------|-------------------| +| `multiband.rs` | `wifi-densepose-signal/src/ruvsense/` | Stage I (gestalt from multi-band CSI) | +| `phase_align.rs` | `wifi-densepose-signal/src/ruvsense/` | Stage II (phase feature extraction) | +| `multistatic.rs` | `wifi-densepose-signal/src/ruvsense/` | Stage III (AP mesh spatial topology) | +| `coherence_gate.rs` | `wifi-densepose-signal/src/ruvsense/` | Stage IV (signal-vs-noise separation) | +| `field_model.rs` | `wifi-densepose-signal/src/ruvsense/` | Stage V (persistent field for querying) | +| `pose_tracker.rs` | `wifi-densepose-signal/src/ruvsense/` | Stage VI (person tracking output) | +| Viewpoint fusion | `wifi-densepose-ruvector/src/viewpoint/` | Cross-session (multi-viewpoint convergence) | + +The `wifi-densepose-ruvector` crate already depends on `ruvector-crv` in its `Cargo.toml`. This ADR defines how to wrap the CRV API with WiFi-DensePose domain types. + +### 1.4 The Key Insight: Cross-Session Convergence = Cross-Room Identity + +CRV's convergence analysis compares independent sessions targeting the same coordinate to find agreement in their embeddings. In WiFi-DensePose, different AP clusters in different rooms are independent "viewers" of the same person. When a person moves from Room A to Room B, the CRV convergence mechanism can find agreement between the Room A embedding trail and the Room B initial embeddings -- establishing identity continuity without cameras. + +--- + +## 2. Decision + +### 2.1 The 6-Stage CRV-to-WiFi Mapping + +Create a new `crv` module in the `wifi-densepose-ruvector` crate that wraps `ruvector-crv` with WiFi-DensePose domain types. Each CRV stage maps to a specific point in the sensing pipeline. + +``` ++-------------------------------------------------------------------+ +| CRV-Sense Pipeline (6 Stages) | ++-------------------------------------------------------------------+ +| | +| Raw CSI frames from ESP32 mesh (ADR-029) | +| | | +| v | +| +----------------------------------------------------------+ | +| | Stage I: CSI Gestalt Classification | | +| | CsiGestaltClassifier | | +| | Input: raw CSI frame (amplitude envelope + phase slope) | | +| | Output: GestaltType (Manmade/Natural/Movement/Energy) | | +| | Encoder: StageIEncoder (Poincare ball embedding) | | +| | Module: ruvsense/multiband.rs | | +| +----------------------------+-----------------------------+ | +| | | +| v | +| +----------------------------------------------------------+ | +| | Stage II: CSI Sensory Feature Extraction | | +| | CsiSensoryEncoder | | +| | Input: per-subcarrier CSI | | +| | Output: amplitude textures, phase patterns, freq colors | | +| | Encoder: StageIIEncoder (multi-head attention vectors) | | +| | Module: ruvsense/phase_align.rs | | +| +----------------------------+-----------------------------+ | +| | | +| v | +| +----------------------------------------------------------+ | +| | Stage III: AP Mesh Spatial Topology | | +| | MeshTopologyEncoder | | +| | Input: node positions, link SNR, baseline distances | | +| | Output: GNN graph embedding of mesh geometry | | +| | Encoder: StageIIIEncoder (GNN topology) | | +| | Module: ruvsense/multistatic.rs | | +| +----------------------------+-----------------------------+ | +| | | +| v | +| +----------------------------------------------------------+ | +| | Stage IV: Coherence Gating (AOL Detection) | | +| | CoherenceAolDetector | | +| | Input: phase coherence scores, gate decisions | | +| | Output: AOL-flagged frames removed, clean signal kept | | +| | Encoder: StageIVEncoder (SNN temporal encoding) | | +| | Module: ruvsense/coherence_gate.rs | | +| +----------------------------+-----------------------------+ | +| | | +| v | +| +----------------------------------------------------------+ | +| | Stage V: Pose Interrogation | | +| | PoseInterrogator | | +| | Input: pose hypothesis + accumulated CSI features | | +| | Output: soft attention over CSI history, top candidates | | +| | Engine: StageVEngine (differentiable search) | | +| | Module: ruvsense/field_model.rs | | +| +----------------------------+-----------------------------+ | +| | | +| v | +| +----------------------------------------------------------+ | +| | Stage VI: Multi-Person Partitioning | | +| | PersonPartitioner | | +| | Input: all person embedding clusters | | +| | Output: MinCut-separated person partitions + centroids | | +| | Modeler: StageVIModeler (MinCut partitioning) | | +| | Module: training pipeline (ruvector-mincut) | | +| +----------------------------+-----------------------------+ | +| | | +| v | +| +----------------------------------------------------------+ | +| | Cross-Session: Multi-Room Convergence | | +| | MultiViewerConvergence | | +| | Input: per-room embedding trails for candidate persons | | +| | Output: cross-room identity matches + confidence | | +| | Engine: CrvSessionManager::find_convergence() | | +| | Module: ruvsense/cross_room.rs | | +| +----------------------------------------------------------+ | ++-------------------------------------------------------------------+ +``` + +### 2.2 Stage I: CSI Gestalt Classification + +**CRV mapping:** Stage I ideograms classify the target's fundamental character (Manmade/Natural/Movement/Energy). In WiFi sensing, the raw CSI frame's amplitude envelope shape and phase slope direction provide an analogous gestalt classification of the RF environment. + +**WiFi domain types:** + +```rust +/// CSI-domain gestalt types mapped from CRV GestaltType. +/// +/// The CRV taxonomy maps to RF phenomenology: +/// - Manmade: structured multipath (walls, furniture, metallic reflectors) +/// - Natural: diffuse scattering (vegetation, irregular surfaces) +/// - Movement: Doppler-shifted components (human motion, fan, pet) +/// - Energy: high-amplitude transients (microwave, motor, interference) +/// - Water: slow fading envelope (humidity change, condensation) +/// - Land: static baseline (empty room, no perturbation) +pub struct CsiGestaltClassifier { + encoder: StageIEncoder, + config: CrvConfig, +} + +impl CsiGestaltClassifier { + /// Classify a raw CSI frame into a gestalt type. + /// + /// Extracts three features from the CSI frame: + /// 1. Amplitude envelope shape (ideogram stroke analog) + /// 2. Phase slope direction (spontaneous descriptor analog) + /// 3. Subcarrier correlation structure (classification signal) + /// + /// Returns a Poincare ball embedding (384-d by default) encoding + /// the hierarchical gestalt taxonomy with exponentially less + /// distortion than Euclidean space. + pub fn classify(&self, csi_frame: &CsiFrame) -> CrvResult<(GestaltType, Vec)>; +} +``` + +**Integration point:** `ruvsense/multiband.rs` already processes multi-band CSI. The `CsiGestaltClassifier` wraps this with Poincare ball embedding via `StageIEncoder`, producing a hyperbolic embedding that captures the gestalt hierarchy. + +### 2.3 Stage II: CSI Sensory Feature Extraction + +**CRV mapping:** Stage II collects sensory impressions (texture, color, temperature). In WiFi sensing, the per-subcarrier CSI features are the sensory modalities: + +| CRV Sensory Modality | WiFi CSI Analog | +|----------------------|-----------------| +| Texture | Amplitude variance pattern across subcarriers (smooth vs rough surface reflection) | +| Color | Frequency-domain spectral shape (which subcarriers carry the most energy) | +| Temperature | Phase drift rate (thermal expansion changes path length) | +| Luminosity | Overall signal power level (SNR) | +| Dimension | Delay spread (multipath extent maps to room size) | + +**WiFi domain types:** + +```rust +pub struct CsiSensoryEncoder { + encoder: StageIIEncoder, +} + +impl CsiSensoryEncoder { + /// Extract sensory features from per-subcarrier CSI data. + /// + /// Maps CSI signal characteristics to CRV sensory modalities: + /// - Amplitude variance -> Texture + /// - Spectral shape -> Color + /// - Phase drift rate -> Temperature + /// - Signal power -> Luminosity + /// - Delay spread -> Dimension + /// + /// Uses multi-head attention (ruvector-attention) to produce + /// a unified sensory embedding that captures cross-modality + /// correlations. + pub fn encode(&self, csi_subcarriers: &SubcarrierData) -> CrvResult>; +} +``` + +**Integration point:** `ruvsense/phase_align.rs` already computes per-subcarrier phase features. The `CsiSensoryEncoder` maps these to `StageIIData` sensory impressions and produces attention-weighted embeddings via `StageIIEncoder`. + +### 2.4 Stage III: AP Mesh Spatial Topology + +**CRV mapping:** Stage III sketches the spatial layout with geometric primitives and relationships. In WiFi sensing, the AP mesh nodes and their inter-node links form the spatial sketch: + +| CRV Sketch Element | WiFi Mesh Analog | +|-------------------|-----------------| +| `SketchElement` | AP node (position, antenna orientation) | +| `GeometricKind::Point` | Single AP location | +| `GeometricKind::Line` | Bistatic link between two APs | +| `SpatialRelationship` | Link quality, baseline distance, angular separation | + +**WiFi domain types:** + +```rust +pub struct MeshTopologyEncoder { + encoder: StageIIIEncoder, +} + +impl MeshTopologyEncoder { + /// Encode the AP mesh as a GNN graph topology. + /// + /// Each AP node becomes a SketchElement with its position and + /// antenna count. Each bistatic link becomes a SpatialRelationship + /// with strength proportional to link SNR. + /// + /// Uses ruvector-gnn to produce a graph embedding that captures + /// the mesh's geometric diversity index (GDI) and effective + /// viewpoint count. + pub fn encode(&self, mesh: &MultistaticArray) -> CrvResult>; +} +``` + +**Integration point:** `ruvsense/multistatic.rs` manages the AP mesh topology. The `MeshTopologyEncoder` translates `MultistaticArray` geometry into `StageIIIData` sketch elements and relationships, producing a GNN-encoded topology embedding via `StageIIIEncoder`. + +### 2.5 Stage IV: Coherence Gating as AOL Detection + +**CRV mapping:** Stage IV detects Analytical Overlay (AOL) -- moments when the analytical mind contaminates the raw signal with pre-existing assumptions. In WiFi sensing, the coherence gate (ADR-030/032) serves the same function: it detects when environmental interference, multipath changes, or hardware artifacts contaminate the CSI signal, and flags those frames for exclusion. + +| CRV AOL Concept | WiFi Coherence Analog | +|-----------------|---------------------| +| AOL event | Low-coherence frame (interference, multipath shift, hardware glitch) | +| AOL anomaly score | Coherence metric (0.0 = fully incoherent, 1.0 = fully coherent) | +| AOL break (flagged, set aside) | `GateDecision::Reject` or `GateDecision::PredictOnly` | +| Clean signal line | `GateDecision::Accept` with noise multiplier | +| Forced accept after timeout | `GateDecision::ForcedAccept` (ADR-032) with inflated noise | + +**WiFi domain types:** + +```rust +pub struct CoherenceAolDetector { + encoder: StageIVEncoder, +} + +impl CoherenceAolDetector { + /// Map coherence gate decisions to CRV AOL detection. + /// + /// The SNN temporal encoding models the spike pattern of + /// coherence violations over time: + /// - Burst of low-coherence frames -> high AOL anomaly score + /// - Sustained coherence -> low anomaly score (clean signal) + /// - Single transient -> moderate score (check and continue) + /// + /// Returns an embedding that encodes the temporal pattern of + /// signal quality, enabling downstream stages to weight their + /// attention based on signal cleanliness. + pub fn detect( + &self, + coherence_history: &[GateDecision], + timestamps: &[u64], + ) -> CrvResult<(Vec, Vec)>; +} +``` + +**Integration point:** `ruvsense/coherence_gate.rs` already produces `GateDecision` values. The `CoherenceAolDetector` translates the coherence gate's temporal stream into `StageIVData` with `AOLDetection` events, and the SNN temporal encoding via `StageIVEncoder` produces an embedding of signal quality over time. + +### 2.6 Stage V: Pose Interrogation via Differentiable Search + +**CRV mapping:** Stage V is the interrogation phase -- probing earlier stage data with specific queries to extract targeted information. In WiFi sensing, this maps to querying the accumulated CSI feature history with a pose hypothesis to find supporting or contradicting evidence. + +**WiFi domain types:** + +```rust +pub struct PoseInterrogator { + engine: StageVEngine, +} + +impl PoseInterrogator { + /// Cross-reference a pose hypothesis against CSI history. + /// + /// Uses differentiable search (soft attention with temperature + /// scaling) to find which historical CSI frames best support + /// or contradict the current pose estimate. + /// + /// Returns: + /// - Attention weights over the CSI history buffer + /// - Top-k supporting frames (highest attention) + /// - Cross-references linking pose keypoints to specific + /// CSI subcarrier features from earlier stages + pub fn interrogate( + &self, + pose_embedding: &[f32], + csi_history: &[CrvSessionEntry], + ) -> CrvResult<(StageVData, Vec)>; +} +``` + +**Integration point:** `ruvsense/field_model.rs` maintains the persistent electromagnetic field model (ADR-030). The `PoseInterrogator` wraps this with CRV Stage V semantics -- the field model's history becomes the corpus that `StageVEngine` searches over, and the pose hypothesis becomes the probe query. + +### 2.7 Stage VI: Multi-Person Partitioning via MinCut + +**CRV mapping:** Stage VI produces the composite 3D model by clustering accumulated data into distinct target partitions via MinCut. In WiFi sensing, this maps to multi-person separation -- partitioning the accumulated CSI embeddings into person-specific clusters. + +**WiFi domain types:** + +```rust +pub struct PersonPartitioner { + modeler: StageVIModeler, +} + +impl PersonPartitioner { + /// Partition accumulated embeddings into distinct persons. + /// + /// Uses MinCut (ruvector-mincut) to find natural cluster + /// boundaries in the embedding space. Each partition corresponds + /// to one person, with: + /// - A centroid embedding (person signature) + /// - Member frame indices (which CSI frames belong to this person) + /// - Separation strength (how distinct this person is from others) + /// + /// The MinCut value between partitions serves as a confidence + /// metric for person separation quality. + pub fn partition( + &self, + person_embeddings: &[CrvSessionEntry], + ) -> CrvResult<(StageVIData, Vec)>; +} +``` + +**Integration point:** The training pipeline in `wifi-densepose-train` already uses `ruvector-mincut` for `DynamicPersonMatcher` (ADR-016). The `PersonPartitioner` wraps this with CRV Stage VI semantics, framing person separation as composite model construction. + +### 2.8 Cross-Session Convergence: Multi-Room Identity Matching + +**CRV mapping:** CRV convergence analysis compares embeddings from independent sessions targeting the same coordinate to find agreement. In WiFi-DensePose, independent AP clusters in different rooms are independent "viewers" of the same person. + +**WiFi domain types:** + +```rust +pub struct MultiViewerConvergence { + session_manager: CrvSessionManager, +} + +impl MultiViewerConvergence { + /// Match person identities across rooms via CRV convergence. + /// + /// Each room's AP cluster is modeled as an independent CRV session. + /// When a person moves from Room A to Room B: + /// 1. Room A session contains the person's embedding trail (Stages I-VI) + /// 2. Room B session begins accumulating new embeddings + /// 3. Convergence analysis finds agreement between Room A's final + /// embeddings and Room B's initial embeddings + /// 4. Agreement score above threshold establishes identity continuity + /// + /// Returns ConvergenceResult with: + /// - Session pairs (room pairs) that converged + /// - Per-pair similarity scores + /// - Convergent stages (which CRV stages showed strongest agreement) + /// - Consensus embedding (merged identity signature) + pub fn match_across_rooms( + &self, + room_sessions: &[(RoomId, SessionId)], + threshold: f32, + ) -> CrvResult; +} +``` + +**Integration point:** `ruvsense/cross_room.rs` already handles cross-room identity continuity (ADR-030). The `MultiViewerConvergence` wraps the existing `CrossRoomTracker` with CRV convergence semantics, using `CrvSessionManager::find_convergence()` to compute embedding agreement. + +### 2.9 WifiCrvSession: Unified Pipeline Wrapper + +The top-level wrapper ties all six stages into a single pipeline: + +```rust +/// A WiFi-DensePose sensing session modeled as a CRV session. +/// +/// Wraps CrvSessionManager with CSI-specific convenience methods. +/// Each call to process_frame() advances through all six CRV stages +/// and appends stage embeddings to the session. +pub struct WifiCrvSession { + session_manager: CrvSessionManager, + gestalt: CsiGestaltClassifier, + sensory: CsiSensoryEncoder, + topology: MeshTopologyEncoder, + coherence: CoherenceAolDetector, + interrogator: PoseInterrogator, + partitioner: PersonPartitioner, + convergence: MultiViewerConvergence, +} + +impl WifiCrvSession { + /// Create a new WiFi CRV session with the given configuration. + pub fn new(config: WifiCrvConfig) -> Self; + + /// Process a single CSI frame through all six CRV stages. + /// + /// Returns the per-stage embeddings and the final person partitions. + pub fn process_frame( + &mut self, + frame: &CsiFrame, + mesh: &MultistaticArray, + coherence_state: &GateDecision, + pose_hypothesis: Option<&[f32]>, + ) -> CrvResult; + + /// Find convergence across room sessions for identity matching. + pub fn find_convergence( + &self, + room_sessions: &[(RoomId, SessionId)], + threshold: f32, + ) -> CrvResult; +} +``` + +--- + +## 3. Implementation Plan (File-Level) + +### 3.1 Phase 1: CRV Module Core (New Files) + +| File | Purpose | Upstream Dependency | +|------|---------|-------------------| +| `crates/wifi-densepose-ruvector/src/crv/mod.rs` | Module root, re-exports all CRV-Sense types | -- | +| `crates/wifi-densepose-ruvector/src/crv/config.rs` | `WifiCrvConfig` extending `CrvConfig` with WiFi-specific defaults (128-d instead of 384-d to match AETHER) | `ruvector-crv` | +| `crates/wifi-densepose-ruvector/src/crv/session.rs` | `WifiCrvSession` wrapping `CrvSessionManager` | `ruvector-crv` | +| `crates/wifi-densepose-ruvector/src/crv/output.rs` | `WifiCrvOutput` struct with per-stage embeddings and diagnostics | -- | + +### 3.2 Phase 2: Stage Encoders (New Files) + +| File | Purpose | Upstream Dependency | +|------|---------|-------------------| +| `crates/wifi-densepose-ruvector/src/crv/gestalt.rs` | `CsiGestaltClassifier` -- Stage I Poincare ball embedding | `ruvector-crv::StageIEncoder` | +| `crates/wifi-densepose-ruvector/src/crv/sensory.rs` | `CsiSensoryEncoder` -- Stage II multi-head attention | `ruvector-crv::StageIIEncoder`, `ruvector-attention` | +| `crates/wifi-densepose-ruvector/src/crv/topology.rs` | `MeshTopologyEncoder` -- Stage III GNN topology | `ruvector-crv::StageIIIEncoder`, `ruvector-gnn` | +| `crates/wifi-densepose-ruvector/src/crv/coherence.rs` | `CoherenceAolDetector` -- Stage IV SNN temporal encoding | `ruvector-crv::StageIVEncoder` | +| `crates/wifi-densepose-ruvector/src/crv/interrogation.rs` | `PoseInterrogator` -- Stage V differentiable search | `ruvector-crv::StageVEngine` | +| `crates/wifi-densepose-ruvector/src/crv/partition.rs` | `PersonPartitioner` -- Stage VI MinCut partitioning | `ruvector-crv::StageVIModeler`, `ruvector-mincut` | + +### 3.3 Phase 3: Cross-Session Convergence + +| File | Purpose | Upstream Dependency | +|------|---------|-------------------| +| `crates/wifi-densepose-ruvector/src/crv/convergence.rs` | `MultiViewerConvergence` -- cross-room identity matching | `ruvector-crv::CrvSessionManager` | + +### 3.4 Phase 4: Integration with Existing Modules (Edits to Existing Files) + +| File | Change | Notes | +|------|--------|-------| +| `crates/wifi-densepose-ruvector/src/lib.rs` | Add `pub mod crv;` | Expose new module | +| `crates/wifi-densepose-ruvector/Cargo.toml` | No change needed | `ruvector-crv` dependency already present | +| `crates/wifi-densepose-signal/src/ruvsense/multiband.rs` | Add trait impl for `CrvGestaltSource` | Allow gestalt classifier to consume multiband output | +| `crates/wifi-densepose-signal/src/ruvsense/phase_align.rs` | Add trait impl for `CrvSensorySource` | Allow sensory encoder to consume phase features | +| `crates/wifi-densepose-signal/src/ruvsense/coherence_gate.rs` | Add method to export `GateDecision` history as `Vec` | Bridge coherence gate to CRV Stage IV | +| `crates/wifi-densepose-signal/src/ruvsense/cross_room.rs` | Add `CrvConvergenceAdapter` trait impl | Bridge cross-room tracker to CRV convergence | + +--- + +## 4. DDD Design + +### 4.1 New Bounded Context: CrvSensing + +**Aggregate Root: `WifiCrvSession`** + +```rust +pub struct WifiCrvSession { + /// Underlying CRV session manager + session_manager: CrvSessionManager, + /// Per-stage encoders + stages: CrvStageEncoders, + /// Session configuration + config: WifiCrvConfig, + /// Running statistics for convergence quality + convergence_stats: ConvergenceStats, +} +``` + +**Value Objects:** + +```rust +/// Output of a single frame through the 6-stage pipeline. +pub struct WifiCrvOutput { + /// Per-stage embeddings (6 vectors, one per CRV stage). + pub stage_embeddings: [Vec; 6], + /// Gestalt classification for this frame. + pub gestalt: GestaltType, + /// AOL detections (frames flagged as noise-contaminated). + pub aol_events: Vec, + /// Person partitions from Stage VI. + pub partitions: Vec, + /// Processing latency per stage in microseconds. + pub stage_latencies_us: [u64; 6], +} + +/// WiFi-specific CRV configuration extending CrvConfig. +pub struct WifiCrvConfig { + /// Base CRV config (dimensions, curvature, thresholds). + pub crv: CrvConfig, + /// AETHER embedding dimension (default: 128, overrides CrvConfig.dimensions). + pub aether_dim: usize, + /// Coherence threshold for AOL detection (maps to aol_threshold). + pub coherence_threshold: f32, + /// Maximum CSI history frames for Stage V interrogation. + pub max_history_frames: usize, + /// Cross-room convergence threshold (default: 0.75). + pub convergence_threshold: f32, +} +``` + +**Domain Events:** + +```rust +pub enum CrvSensingEvent { + /// Stage I completed: gestalt classified + GestaltClassified { gestalt: GestaltType, confidence: f32 }, + /// Stage IV: AOL detected (noise contamination) + AolDetected { anomaly_score: f32, flagged: bool }, + /// Stage VI: Persons partitioned + PersonsPartitioned { count: usize, min_separation: f32 }, + /// Cross-session: Identity matched across rooms + IdentityConverged { room_pair: (RoomId, RoomId), score: f32 }, + /// Full pipeline completed for one frame + FrameProcessed { latency_us: u64, stages_completed: u8 }, +} +``` + +### 4.2 Integration with Existing Bounded Contexts + +**Signal (wifi-densepose-signal):** New traits `CrvGestaltSource` and `CrvSensorySource` allow the CRV module to consume signal processing outputs without tight coupling. The signal crate does not depend on the CRV crate -- the dependency flows one direction only. + +**Training (wifi-densepose-train):** The `PersonPartitioner` (Stage VI) produces the same MinCut partitions as the existing `DynamicPersonMatcher`. A shared trait `PersonSeparator` allows both to be used interchangeably. + +**Hardware (wifi-densepose-hardware):** No changes. The CRV module consumes CSI frames after they have been received and parsed by the hardware layer. + +--- + +## 5. RuVector Integration Map + +All seven `ruvector` crates exercised by the CRV-Sense integration: + +| CRV Stage | ruvector Crate | API Used | WiFi-DensePose Role | +|-----------|---------------|----------|-------------------| +| I (Gestalt) | -- (internal Poincare math) | `StageIEncoder::encode()` | Hyperbolic embedding of CSI gestalt taxonomy | +| II (Sensory) | `ruvector-attention` | `StageIIEncoder::encode()` | Multi-head attention over subcarrier features | +| III (Dimensional) | `ruvector-gnn` | `StageIIIEncoder::encode()` | GNN encoding of AP mesh topology | +| IV (AOL) | -- (internal SNN) | `StageIVEncoder::encode()` | SNN temporal encoding of coherence violations | +| V (Interrogation) | -- (internal soft attention) | `StageVEngine::search()` | Differentiable search over field model history | +| VI (Composite) | `ruvector-mincut` | `StageVIModeler::partition()` | MinCut person separation | +| Convergence | -- (cosine similarity) | `CrvSessionManager::find_convergence()` | Cross-room identity matching | + +Additionally, the CRV module benefits from existing ruvector integrations already in the workspace: + +| Existing Integration | ADR | CRV Stage Benefit | +|---------------------|-----|-------------------| +| `ruvector-attn-mincut` in `spectrogram.rs` | ADR-016 | Stage II (subcarrier attention for sensory features) | +| `ruvector-temporal-tensor` in `dataset.rs` | ADR-016 | Stage IV (compressed coherence history buffer) | +| `ruvector-solver` in `subcarrier.rs` | ADR-016 | Stage III (sparse interpolation for mesh topology) | +| `ruvector-attention` in `model.rs` | ADR-016 | Stage V (spatial attention for pose interrogation) | +| `ruvector-mincut` in `metrics.rs` | ADR-016 | Stage VI (person matching baseline) | + +--- + +## 6. Acceptance Criteria + +### 6.1 Stage I: CSI Gestalt Classification + +| ID | Criterion | Test Method | +|----|-----------|-------------| +| S1-1 | `CsiGestaltClassifier::classify()` returns a valid `GestaltType` for any well-formed CSI frame | Unit test: feed 100 synthetic CSI frames, verify all return one of 6 gestalt types | +| S1-2 | Poincare ball embedding has correct dimensionality (matching `WifiCrvConfig.aether_dim`) | Unit test: verify `embedding.len() == config.aether_dim` | +| S1-3 | Embedding norm is strictly less than 1.0 (Poincare ball constraint) | Unit test: verify L2 norm < 1.0 for all outputs | +| S1-4 | Movement gestalt is classified for CSI frames with Doppler signature | Unit test: synthetic Doppler-shifted CSI -> `GestaltType::Movement` | +| S1-5 | Energy gestalt is classified for CSI frames with transient interference | Unit test: synthetic interference burst -> `GestaltType::Energy` | + +### 6.2 Stage II: CSI Sensory Features + +| ID | Criterion | Test Method | +|----|-----------|-------------| +| S2-1 | `CsiSensoryEncoder::encode()` produces embedding of correct dimensionality | Unit test: verify output length | +| S2-2 | Amplitude variance maps to Texture modality in `StageIIData.impressions` | Unit test: verify Texture entry present for non-flat amplitude | +| S2-3 | Phase drift rate maps to Temperature modality | Unit test: inject linear phase drift, verify Temperature entry | +| S2-4 | Multi-head attention weights sum to 1.0 per head | Unit test: verify softmax normalization | + +### 6.3 Stage III: AP Mesh Topology + +| ID | Criterion | Test Method | +|----|-----------|-------------| +| S3-1 | `MeshTopologyEncoder::encode()` produces one `SketchElement` per AP node | Unit test: 4-node mesh produces 4 sketch elements | +| S3-2 | `SpatialRelationship` count equals number of bistatic links | Unit test: 4 nodes -> 6 links (fully connected) or configured subset | +| S3-3 | Relationship strength is proportional to link SNR | Unit test: verify monotonic relationship between SNR and strength | +| S3-4 | GNN embedding changes when node positions change | Unit test: perturb one node position, verify embedding changes | + +### 6.4 Stage IV: Coherence AOL Detection + +| ID | Criterion | Test Method | +|----|-----------|-------------| +| S4-1 | `CoherenceAolDetector::detect()` flags low-coherence frames as AOL events | Unit test: inject 10 `GateDecision::Reject` frames, verify 10 `AOLDetection` entries | +| S4-2 | Anomaly score correlates with coherence violation burst length | Unit test: burst of 5 violations scores higher than isolated violation | +| S4-3 | `GateDecision::Accept` frames produce no AOL detections | Unit test: all-accept history produces empty AOL list | +| S4-4 | SNN temporal encoding respects refractory period | Unit test: two violations within `refractory_period_ms` produce single spike | +| S4-5 | `GateDecision::ForcedAccept` (ADR-032) maps to AOL with moderate score | Unit test: forced accept frames flagged but not at max anomaly score | + +### 6.5 Stage V: Pose Interrogation + +| ID | Criterion | Test Method | +|----|-----------|-------------| +| S5-1 | `PoseInterrogator::interrogate()` returns attention weights over CSI history | Unit test: history of 50 frames produces 50 attention weights summing to 1.0 | +| S5-2 | Top-k candidates are the highest-attention frames | Unit test: verify `top_candidates` indices correspond to highest `attention_weights` | +| S5-3 | Cross-references link correct stage numbers | Unit test: verify `from_stage` and `to_stage` are in [1..6] | +| S5-4 | Empty history returns empty probe results | Unit test: empty `csi_history` produces zero candidates | + +### 6.6 Stage VI: Person Partitioning + +| ID | Criterion | Test Method | +|----|-----------|-------------| +| S6-1 | `PersonPartitioner::partition()` separates two well-separated embedding clusters into two partitions | Unit test: two Gaussian clusters with distance > 5 sigma -> two partitions | +| S6-2 | Each partition has a centroid embedding of correct dimensionality | Unit test: verify centroid length matches config | +| S6-3 | `separation_strength` (MinCut value) is positive for distinct persons | Unit test: verify separation_strength > 0.0 | +| S6-4 | Single-person scenario produces exactly one partition | Unit test: single cluster -> one partition | +| S6-5 | Partition `member_entries` indices are non-overlapping and exhaustive | Unit test: union of all member entries covers all input frames | + +### 6.7 Cross-Session Convergence + +| ID | Criterion | Test Method | +|----|-----------|-------------| +| C-1 | `MultiViewerConvergence::match_across_rooms()` returns positive score for same person in two rooms | Unit test: inject same embedding trail into two room sessions, verify score > threshold | +| C-2 | Different persons in different rooms produce score below threshold | Unit test: inject distinct embedding trails, verify score < threshold | +| C-3 | `convergent_stages` identifies the stage with highest cross-room agreement | Unit test: make Stage I embeddings identical, others random, verify Stage I in convergent_stages | +| C-4 | `consensus_embedding` has correct dimensionality when convergence succeeds | Unit test: verify consensus embedding length on successful match | +| C-5 | Threshold parameter is respected (no matches below threshold) | Unit test: set threshold to 0.99, verify only near-identical sessions match | + +### 6.8 End-to-End Pipeline + +| ID | Criterion | Test Method | +|----|-----------|-------------| +| E-1 | `WifiCrvSession::process_frame()` returns `WifiCrvOutput` with all 6 stage embeddings populated | Integration test: process 10 synthetic frames, verify 6 non-empty embeddings per frame | +| E-2 | Total pipeline latency < 5 ms per frame on x86 host | Benchmark: process 1000 frames, verify p95 latency < 5 ms | +| E-3 | Pipeline handles missing pose hypothesis gracefully (Stage V skipped or uses default) | Unit test: pass `None` for pose_hypothesis, verify no panic and output is valid | +| E-4 | Pipeline handles empty mesh (single AP) without panic | Unit test: single-node mesh produces valid output with degenerate Stage III | +| E-5 | Session state accumulates across frames (Stage V history grows) | Unit test: process 50 frames, verify Stage V candidate count increases | + +--- + +## 7. Consequences + +### 7.1 Positive + +- **Structured pipeline formalization**: The 6-stage CRV mapping provides a principled progressive refinement structure for the WiFi sensing pipeline, making the data flow explicit and each stage independently testable. +- **Cross-room identity without cameras**: CRV convergence analysis provides a mathematically grounded mechanism for matching person identities across AP clusters in different rooms, using only RF embeddings. +- **Noise separation as first-class concept**: Mapping coherence gating to CRV Stage IV (AOL detection) elevates noise separation from an implementation detail to a core architectural stage with its own embedding and temporal model. +- **Hyperbolic embeddings for gestalt hierarchy**: The Poincare ball embedding for Stage I captures the hierarchical RF environment taxonomy (Manmade > structural multipath, Natural > diffuse scattering, etc.) with exponentially less distortion than Euclidean space. +- **Reuse of ruvector ecosystem**: All seven ruvector crates are exercised through a single unified abstraction, maximizing the return on the existing ruvector integration (ADR-016). +- **No new external dependencies**: `ruvector-crv` is already a workspace dependency in `wifi-densepose-ruvector/Cargo.toml`. This ADR adds only new Rust source files. + +### 7.2 Negative + +- **Abstraction overhead**: The CRV stage mapping adds a layer of indirection over the existing signal processing pipeline. Each stage wrapper must translate between WiFi domain types and CRV types, adding code that could be a maintenance burden if the mapping proves ill-fitted. +- **Dimensional mismatch**: `ruvector-crv` defaults to 384 dimensions; AETHER embeddings (ADR-024) use 128 dimensions. The `WifiCrvConfig` overrides this, but encoder behavior at non-default dimensionality must be validated. +- **SNN overhead**: The Stage IV SNN temporal encoder adds per-frame computation for spike train simulation. On embedded targets (ESP32), this may exceed the 50 ms frame budget. Initial deployment is host-side only (aggregator, not firmware). +- **Convergence false positives**: Cross-room identity matching via embedding similarity may produce false matches for persons with similar body types and movement patterns in similar room geometries. Temporal proximity constraints (from ADR-030) are required to bound the false positive rate. +- **Testing complexity**: Six stages with independent encoders and a cross-session convergence layer require a comprehensive test matrix. The acceptance criteria in Section 6 define 30+ individual test cases. + +### 7.3 Risks + +| Risk | Probability | Impact | Mitigation | +|------|-------------|--------|------------| +| Poincare ball embedding unstable at boundary (norm approaching 1.0) | Medium | NaN propagation through pipeline | Clamp norm to 0.95 in `CsiGestaltClassifier`; add norm assertion in test suite | +| GNN encoder too slow for real-time mesh topology updates | Low | Stage III becomes bottleneck | Cache topology embedding; only recompute on node geometry change (rare) | +| SNN refractory period too short for 20 Hz coherence gate | Medium | False AOL detections at frame boundaries | Tune `refractory_period_ms` to match frame interval (50 ms) in `WifiCrvConfig` defaults | +| Cross-room convergence threshold too permissive | Medium | False identity matches across rooms | Default threshold 0.75 is conservative; ADR-030 temporal proximity constraint (<60s) adds second guard | +| MinCut partitioning produces too many or too few person clusters | Medium | Person count mismatch | Use expected person count hint (from occupancy detector) as MinCut constraint | +| CRV abstraction becomes tech debt if mapping proves poor fit | Low | Code removed in future ADR | All CRV code in isolated `crv` module; can be removed without affecting existing pipeline | + +--- + +## 8. Related ADRs + +| ADR | Relationship | +|-----|-------------| +| ADR-016 (RuVector Integration) | **Extended**: All 5 original ruvector crates plus `ruvector-crv` and `ruvector-gnn` now exercised through CRV pipeline | +| ADR-017 (RuVector Signal+MAT) | **Extended**: Signal processing outputs from ADR-017 feed into CRV Stages I-II | +| ADR-024 (AETHER Embeddings) | **Consumed**: Per-viewpoint AETHER 128-d embeddings are the representation fed into CRV stages | +| ADR-029 (RuvSense Multistatic) | **Extended**: Multistatic mesh topology encoded as CRV Stage III; TDM frames are the input to Stage I | +| ADR-030 (Persistent Field Model) | **Extended**: Field model history serves as the Stage V interrogation corpus; cross-room tracker bridges to CRV convergence | +| ADR-031 (RuView Viewpoint Fusion) | **Complementary**: RuView fuses viewpoints within a room; CRV convergence matches identities across rooms | +| ADR-032 (Mesh Security) | **Consumed**: Authenticated beacons and frame integrity (ADR-032) ensure CRV Stage IV AOL detection reflects genuine signal quality, not spoofed frames | + +--- + +## 9. References + +1. Swann, I. (1996). "Remote Viewing: The Real Story." Self-published manuscript. (Original CRV protocol documentation.) +2. Smith, P. H. (2005). "Reading the Enemy's Mind: Inside Star Gate, America's Psychic Espionage Program." Tom Doherty Associates. +3. Nickel, M. & Kiela, D. (2017). "Poincare Embeddings for Learning Hierarchical Representations." NeurIPS 2017. +4. Kipf, T. N. & Welling, M. (2017). "Semi-Supervised Classification with Graph Convolutional Networks." ICLR 2017. +5. Maass, W. (1997). "Networks of Spiking Neurons: The Third Generation of Neural Network Models." Neural Networks, 10(9):1659-1671. +6. Stoer, M. & Wagner, F. (1997). "A Simple Min-Cut Algorithm." Journal of the ACM, 44(4):585-591. +7. `ruvector-crv` v0.1.1. https://crates.io/crates/ruvector-crv +8. `ruvector-attention` v2.0. https://crates.io/crates/ruvector-attention +9. `ruvector-gnn` v2.0.1. https://crates.io/crates/ruvector-gnn +10. `ruvector-mincut` v2.0.1. https://crates.io/crates/ruvector-mincut +11. Geng, J. et al. (2023). "DensePose From WiFi." arXiv:2301.00250. +12. ADR-016 through ADR-032 (internal). diff --git a/rust-port/wifi-densepose-rs/Cargo.lock b/rust-port/wifi-densepose-rs/Cargo.lock index a76fcea..a9dea3b 100644 --- a/rust-port/wifi-densepose-rs/Cargo.lock +++ b/rust-port/wifi-densepose-rs/Cargo.lock @@ -488,12 +488,24 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -601,6 +613,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "console" version = "0.15.11" @@ -915,6 +937,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand 0.9.2", + "siphasher", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1291,9 +1325,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1634,6 +1670,28 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.34" @@ -1699,6 +1757,21 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "lzma-rust2" version = "0.15.7" @@ -1746,6 +1819,63 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "midstreamer-attractor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab86df06cf1705ca37692b4fc0027868f92e5170a7ebb1d706302f04b6044f70" +dependencies = [ + "midstreamer-temporal-compare", + "nalgebra", + "ndarray 0.16.1", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "midstreamer-quic" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ad2099588e987cdbedb039fdf8a56163a2f3dc1ff6bf5a39c63b9ce4e2248c" +dependencies = [ + "futures", + "js-sys", + "quinn", + "rcgen", + "rustls 0.22.4", + "serde", + "thiserror 2.0.18", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "midstreamer-scheduler" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9296b3f0a2b04e5c1a378ee7926e9f892895bface2ccebcfa407450c3aca269" +dependencies = [ + "crossbeam", + "parking_lot", + "serde", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "midstreamer-temporal-compare" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f935ba86c1632a3b5bc5e1cb56a308d4c5d2ec87c84db551c65f3e1001a642" +dependencies = [ + "dashmap", + "lru", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "mime" version = "0.3.17" @@ -1819,6 +1949,33 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "nalgebra" +version = "0.33.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b" +dependencies = [ + "approx", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "native-tls" version = "0.2.18" @@ -1955,6 +2112,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2147,6 +2315,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2443,6 +2621,63 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.37", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.37", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.44" @@ -2590,6 +2825,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1" +dependencies = [ + "pem", + "ring", + "time", + "yasna", +] + [[package]] name = "reborrow" version = "0.5.5" @@ -2643,6 +2890,20 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.8.15" @@ -2750,6 +3011,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2786,15 +3053,105 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.9", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.9", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -2813,6 +3170,18 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "ruvector-attention" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4c2b4ef9db0d5a038c5cb8e9e91ffc11c789db660132d50165d2ba6a71d23f" +dependencies = [ + "rand 0.8.5", + "rayon", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "ruvector-attention" version = "2.0.4" @@ -2859,6 +3228,38 @@ dependencies = [ "uuid", ] +[[package]] +name = "ruvector-crv" +version = "0.1.1" +dependencies = [ + "ruvector-attention 0.1.32", + "ruvector-gnn", + "ruvector-mincut", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "ruvector-gnn" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e17c1cf1ff3380026b299ff3c1ba3a5685c3d8d54700e6ab0b585b6cec21d7b" +dependencies = [ + "anyhow", + "dashmap", + "libc", + "ndarray 0.16.1", + "parking_lot", + "rand 0.8.5", + "rand_distr 0.4.3", + "rayon", + "ruvector-core", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "ruvector-mincut" version = "2.0.4" @@ -2908,6 +3309,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "safetensors" version = "0.3.3" @@ -3120,6 +3530,19 @@ dependencies = [ "libc", ] +[[package]] +name = "simba" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -3132,6 +3555,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -3750,6 +4179,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "unty" version = "0.0.4" @@ -4070,6 +4505,16 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "wifi-densepose-api" version = "0.2.0" @@ -4131,9 +4576,13 @@ dependencies = [ "byteorder", "chrono", "clap", + "criterion", + "midstreamer-quic", + "midstreamer-scheduler", "serde", "serde_json", "thiserror 1.0.69", + "tokio", "tracing", ] @@ -4195,11 +4644,17 @@ dependencies = [ name = "wifi-densepose-ruvector" version = "0.2.0" dependencies = [ - "ruvector-attention", + "approx", + "criterion", + "ruvector-attention 2.0.4", "ruvector-attn-mincut", + "ruvector-crv", + "ruvector-gnn", "ruvector-mincut", "ruvector-solver", "ruvector-temporal-tensor", + "serde", + "serde_json", "thiserror 1.0.69", ] @@ -4227,12 +4682,14 @@ version = "0.2.0" dependencies = [ "chrono", "criterion", + "midstreamer-attractor", + "midstreamer-temporal-compare", "ndarray 0.15.6", "num-complex", "num-traits", "proptest", "rustfft", - "ruvector-attention", + "ruvector-attention 2.0.4", "ruvector-attn-mincut", "ruvector-mincut", "ruvector-solver", @@ -4260,7 +4717,7 @@ dependencies = [ "num-traits", "petgraph", "proptest", - "ruvector-attention", + "ruvector-attention 2.0.4", "ruvector-attn-mincut", "ruvector-mincut", "ruvector-solver", @@ -4410,6 +4867,24 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -4437,6 +4912,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -4470,6 +4960,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4482,6 +4978,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4494,6 +4996,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4518,6 +5026,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4530,6 +5044,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4542,6 +5062,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4554,6 +5080,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4663,6 +5195,15 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/rust-port/wifi-densepose-rs/Cargo.toml b/rust-port/wifi-densepose-rs/Cargo.toml index f138a23..952c32e 100644 --- a/rust-port/wifi-densepose-rs/Cargo.toml +++ b/rust-port/wifi-densepose-rs/Cargo.toml @@ -103,12 +103,20 @@ proptest = "1.4" mockall = "0.12" wiremock = "0.5" -# ruvector integration (all at v2.0.4 — published on crates.io) +# midstreamer integration (published on crates.io) +midstreamer-quic = "0.1.0" +midstreamer-scheduler = "0.1.0" +midstreamer-temporal-compare = "0.1.0" +midstreamer-attractor = "0.1.0" + +# ruvector integration (published on crates.io) ruvector-mincut = "2.0.4" ruvector-attn-mincut = "2.0.4" ruvector-temporal-tensor = "2.0.4" ruvector-solver = "2.0.4" ruvector-attention = "2.0.4" +ruvector-crv = "0.1.1" +ruvector-gnn = { version = "2.0.5", default-features = false } # Internal crates @@ -123,6 +131,11 @@ wifi-densepose-wasm = { version = "0.2.0", path = "crates/wifi-densepose-wasm" } wifi-densepose-mat = { version = "0.2.0", path = "crates/wifi-densepose-mat" } wifi-densepose-ruvector = { version = "0.2.0", path = "crates/wifi-densepose-ruvector" } +# Patch ruvector-crv to fix RuvectorLayer::new() Result API mismatch +# with ruvector-gnn 2.0.5 (upstream ruvector-crv 0.1.1 was built against 2.0.1). +[patch.crates-io] +ruvector-crv = { path = "patches/ruvector-crv" } + [profile.release] lto = true codegen-units = 1 diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/Cargo.toml b/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/Cargo.toml index 5b07224..974e8ac 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/Cargo.toml +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/Cargo.toml @@ -16,4 +16,16 @@ ruvector-attn-mincut = { workspace = true } ruvector-temporal-tensor = { workspace = true } ruvector-solver = { workspace = true } ruvector-attention = { workspace = true } +ruvector-crv = { workspace = true } +ruvector-gnn = { workspace = true } thiserror = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } + +[dev-dependencies] +approx = "0.5" +criterion = { workspace = true } + +[[bench]] +name = "crv_bench" +harness = false diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/benches/crv_bench.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/benches/crv_bench.rs new file mode 100644 index 0000000..32405eb --- /dev/null +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/benches/crv_bench.rs @@ -0,0 +1,405 @@ +//! Benchmarks for CRV (Coordinate Remote Viewing) integration. +//! +//! Measures throughput of gestalt classification, sensory encoding, +//! full session pipelines, cross-session convergence, and embedding +//! dimension scaling using the `ruvector-crv` crate directly. + +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use ruvector_crv::{ + CrvConfig, CrvSessionManager, GestaltType, SensoryModality, StageIData, StageIIData, + StageIIIData, StageIVData, +}; +use ruvector_crv::types::{ + GeometricKind, SketchElement, SpatialRelationType, SpatialRelationship, +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Build a synthetic CSI-like ideogram stroke with `n` subcarrier points. +fn make_stroke(n: usize) -> Vec<(f32, f32)> { + (0..n) + .map(|i| { + let t = i as f32 / n as f32; + (t, (t * std::f32::consts::TAU).sin() * 0.5 + 0.5) + }) + .collect() +} + +/// Build a Stage I data frame representing a single CSI gestalt sample. +fn make_stage_i(gestalt: GestaltType) -> StageIData { + StageIData { + stroke: make_stroke(64), + spontaneous_descriptor: "angular rising".to_string(), + classification: gestalt, + confidence: 0.85, + } +} + +/// Build a Stage II sensory data frame. +fn make_stage_ii() -> StageIIData { + StageIIData { + impressions: vec![ + (SensoryModality::Texture, "rough metallic".to_string()), + (SensoryModality::Temperature, "warm".to_string()), + (SensoryModality::Color, "silver-gray".to_string()), + (SensoryModality::Luminosity, "reflective".to_string()), + (SensoryModality::Sound, "low hum".to_string()), + ], + feature_vector: None, + } +} + +/// Build a Stage III spatial sketch. +fn make_stage_iii() -> StageIIIData { + 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: "antenna".to_string(), + kind: GeometricKind::Line, + position: (0.5, 0.95), + scale: Some(1.0), + }, + ], + relationships: vec![ + SpatialRelationship { + from: "tower".to_string(), + to: "base".to_string(), + relation: SpatialRelationType::Above, + strength: 0.9, + }, + SpatialRelationship { + from: "antenna".to_string(), + to: "tower".to_string(), + relation: SpatialRelationType::Above, + strength: 0.85, + }, + ], + } +} + +/// Build a Stage IV emotional / AOL data frame. +fn make_stage_iv() -> StageIVData { + StageIVData { + emotional_impact: vec![ + ("awe".to_string(), 0.7), + ("curiosity".to_string(), 0.6), + ("unease".to_string(), 0.3), + ], + tangibles: vec!["metal structure".to_string(), "concrete".to_string()], + intangibles: vec!["transmission".to_string(), "power".to_string()], + aol_detections: vec![], + } +} + +/// Create a manager with one session pre-loaded with 4 stages of data. +fn populated_manager(dims: usize) -> (CrvSessionManager, String) { + let config = CrvConfig { + dimensions: dims, + ..CrvConfig::default() + }; + let mut mgr = CrvSessionManager::new(config); + let sid = "bench-sess".to_string(); + mgr.create_session(sid.clone(), "coord-001".to_string()) + .unwrap(); + mgr.add_stage_i(&sid, &make_stage_i(GestaltType::Manmade)) + .unwrap(); + mgr.add_stage_ii(&sid, &make_stage_ii()).unwrap(); + mgr.add_stage_iii(&sid, &make_stage_iii()).unwrap(); + mgr.add_stage_iv(&sid, &make_stage_iv()).unwrap(); + (mgr, sid) +} + +// --------------------------------------------------------------------------- +// Benchmarks +// --------------------------------------------------------------------------- + +/// Benchmark: classify a single CSI frame through Stage I (64 subcarriers). +fn gestalt_classify_single(c: &mut Criterion) { + let config = CrvConfig { + dimensions: 64, + ..CrvConfig::default() + }; + let mut manager = CrvSessionManager::new(config); + manager + .create_session("gc-single".to_string(), "coord-gc".to_string()) + .unwrap(); + + let data = make_stage_i(GestaltType::Manmade); + + c.bench_function("gestalt_classify_single", |b| { + b.iter(|| { + manager + .add_stage_i("gc-single", black_box(&data)) + .unwrap(); + }) + }); +} + +/// Benchmark: classify a batch of 100 CSI frames through Stage I. +fn gestalt_classify_batch(c: &mut Criterion) { + let config = CrvConfig { + dimensions: 64, + ..CrvConfig::default() + }; + + let gestalts = GestaltType::all(); + let frames: Vec = (0..100) + .map(|i| make_stage_i(gestalts[i % gestalts.len()])) + .collect(); + + c.bench_function("gestalt_classify_batch_100", |b| { + b.iter(|| { + let mut manager = CrvSessionManager::new(CrvConfig { + dimensions: 64, + ..CrvConfig::default() + }); + manager + .create_session("gc-batch".to_string(), "coord-gcb".to_string()) + .unwrap(); + + for frame in black_box(&frames) { + manager.add_stage_i("gc-batch", frame).unwrap(); + } + }) + }); +} + +/// Benchmark: extract sensory features from a single CSI frame (Stage II). +fn sensory_encode_single(c: &mut Criterion) { + let config = CrvConfig { + dimensions: 64, + ..CrvConfig::default() + }; + let mut manager = CrvSessionManager::new(config); + manager + .create_session("se-single".to_string(), "coord-se".to_string()) + .unwrap(); + + let data = make_stage_ii(); + + c.bench_function("sensory_encode_single", |b| { + b.iter(|| { + manager + .add_stage_ii("se-single", black_box(&data)) + .unwrap(); + }) + }); +} + +/// Benchmark: full session pipeline -- create session, add 10 mixed-stage +/// frames, run Stage V interrogation, and run Stage VI partitioning. +fn pipeline_full_session(c: &mut Criterion) { + let stage_i_data = make_stage_i(GestaltType::Manmade); + let stage_ii_data = make_stage_ii(); + let stage_iii_data = make_stage_iii(); + let stage_iv_data = make_stage_iv(); + + c.bench_function("pipeline_full_session", |b| { + let mut counter = 0u64; + b.iter(|| { + counter += 1; + let config = CrvConfig { + dimensions: 64, + ..CrvConfig::default() + }; + let mut manager = CrvSessionManager::new(config); + let sid = format!("pfs-{}", counter); + manager + .create_session(sid.clone(), "coord-pfs".to_string()) + .unwrap(); + + // 10 frames across stages I-IV + for _ in 0..3 { + manager + .add_stage_i(&sid, black_box(&stage_i_data)) + .unwrap(); + } + for _ in 0..3 { + manager + .add_stage_ii(&sid, black_box(&stage_ii_data)) + .unwrap(); + } + for _ in 0..2 { + manager + .add_stage_iii(&sid, black_box(&stage_iii_data)) + .unwrap(); + } + for _ in 0..2 { + manager + .add_stage_iv(&sid, black_box(&stage_iv_data)) + .unwrap(); + } + + // Stage V: interrogate with a probe embedding + let probe_emb = vec![0.1f32; 64]; + let probes: Vec<(&str, u8, Vec)> = vec![ + ("structure query", 1, probe_emb.clone()), + ("texture query", 2, probe_emb.clone()), + ]; + let _ = manager.run_stage_v(&sid, &probes, 3); + + // Stage VI: partition + let _ = manager.run_stage_vi(&sid); + }) + }); +} + +/// Benchmark: cross-session convergence analysis with 2 independent +/// sessions of 10 frames each, targeting the same coordinate. +fn convergence_two_sessions(c: &mut Criterion) { + let gestalts = [GestaltType::Manmade, GestaltType::Natural, GestaltType::Energy]; + let stage_ii_data = make_stage_ii(); + + c.bench_function("convergence_two_sessions", |b| { + let mut counter = 0u64; + b.iter(|| { + counter += 1; + let config = CrvConfig { + dimensions: 64, + convergence_threshold: 0.5, + ..CrvConfig::default() + }; + let mut manager = CrvSessionManager::new(config); + let coord = format!("conv-coord-{}", counter); + + // Session A: 10 frames + let sid_a = format!("viewer-a-{}", counter); + manager + .create_session(sid_a.clone(), coord.clone()) + .unwrap(); + for i in 0..5 { + let data = make_stage_i(gestalts[i % gestalts.len()]); + manager.add_stage_i(&sid_a, black_box(&data)).unwrap(); + } + for _ in 0..5 { + manager + .add_stage_ii(&sid_a, black_box(&stage_ii_data)) + .unwrap(); + } + + // Session B: 10 frames (similar but not identical) + let sid_b = format!("viewer-b-{}", counter); + manager + .create_session(sid_b.clone(), coord.clone()) + .unwrap(); + for i in 0..5 { + let data = make_stage_i(gestalts[(i + 1) % gestalts.len()]); + manager.add_stage_i(&sid_b, black_box(&data)).unwrap(); + } + for _ in 0..5 { + manager + .add_stage_ii(&sid_b, black_box(&stage_ii_data)) + .unwrap(); + } + + // Convergence analysis + let _ = manager.find_convergence(&coord, black_box(0.5)); + }) + }); +} + +/// Benchmark: session creation overhead alone. +fn crv_session_create(c: &mut Criterion) { + c.bench_function("crv_session_create", |b| { + b.iter(|| { + let config = CrvConfig { + dimensions: 32, + ..CrvConfig::default() + }; + let mut manager = CrvSessionManager::new(black_box(config)); + manager + .create_session( + black_box("sess-1".to_string()), + black_box("coord-1".to_string()), + ) + .unwrap(); + }) + }); +} + +/// Benchmark: embedding dimension scaling (32, 128, 384). +/// +/// Measures Stage I + Stage II encode time across different embedding +/// dimensions to characterize how cost grows with dimensionality. +fn crv_embedding_dimension_scaling(c: &mut Criterion) { + let stage_i_data = make_stage_i(GestaltType::Manmade); + let stage_ii_data = make_stage_ii(); + + let mut group = c.benchmark_group("crv_embedding_dimension_scaling"); + for dims in [32, 128, 384] { + group.bench_with_input(BenchmarkId::from_parameter(dims), &dims, |b, &dims| { + let mut counter = 0u64; + b.iter(|| { + counter += 1; + let config = CrvConfig { + dimensions: dims, + ..CrvConfig::default() + }; + let mut manager = CrvSessionManager::new(config); + let sid = format!("dim-{}-{}", dims, counter); + manager + .create_session(sid.clone(), "coord-dim".to_string()) + .unwrap(); + + // Encode one Stage I + one Stage II at this dimensionality + let emb_i = manager + .add_stage_i(&sid, black_box(&stage_i_data)) + .unwrap(); + let emb_ii = manager + .add_stage_ii(&sid, black_box(&stage_ii_data)) + .unwrap(); + + assert_eq!(emb_i.len(), dims); + assert_eq!(emb_ii.len(), dims); + }) + }); + } + group.finish(); +} + +/// Benchmark: Stage VI partitioning on a pre-populated session +/// (4 stages of accumulated data). +fn crv_stage_vi_partition(c: &mut Criterion) { + c.bench_function("crv_stage_vi_partition", |b| { + let mut counter = 0u64; + b.iter(|| { + counter += 1; + // Re-create the populated manager each iteration because + // run_stage_vi mutates the session (appends an entry). + let (mut mgr, sid) = populated_manager(64); + let _ = mgr.run_stage_vi(black_box(&sid)); + }) + }); +} + +// --------------------------------------------------------------------------- +// Criterion groups +// --------------------------------------------------------------------------- + +criterion_group!( + benches, + gestalt_classify_single, + gestalt_classify_batch, + sensory_encode_single, + pipeline_full_session, + convergence_two_sessions, + crv_session_create, + crv_embedding_dimension_scaling, + crv_stage_vi_partition, +); + +criterion_main!(benches); diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/src/crv/mod.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/src/crv/mod.rs new file mode 100644 index 0000000..410405d --- /dev/null +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/src/crv/mod.rs @@ -0,0 +1,1430 @@ +//! CRV (Coordinate Remote Viewing) signal-line integration for WiFi-DensePose. +//! +//! Maps the 6-stage CRV protocol from [`ruvector_crv`] to WiFi CSI sensing: +//! +//! | CRV Stage | WiFi-DensePose Mapping | +//! |-----------|------------------------| +//! | I (Gestalt) | CSI amplitude/phase pattern classification | +//! | II (Sensory) | CSI feature extraction (roughness, spectral centroid, power, ...) | +//! | III (Dimensional) | AP mesh topology as spatial graph | +//! | IV (Emotional/AOL) | Coherence gate state as AOL detection | +//! | V (Interrogation) | Differentiable search over accumulated CSI features | +//! | VI (Composite) | MinCut person partitioning | +//! +//! # Entry Point +//! +//! [`WifiCrvPipeline`] is the main facade. Create one with [`WifiCrvConfig`], +//! then feed CSI frames through the pipeline stages. + +use ruvector_crv::{ + AOLDetection, ConvergenceResult, CrvConfig, CrvError, CrvSessionManager, GestaltType, + GeometricKind, SensoryModality, SketchElement, SpatialRelationType, SpatialRelationship, + StageIData, StageIIData, StageIIIData, StageIVData, StageVData, StageVIData, +}; +use serde::{Deserialize, Serialize}; + +// --------------------------------------------------------------------------- +// Domain types +// --------------------------------------------------------------------------- + +/// An access point node in the WiFi mesh topology. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApNode { + /// Unique identifier for this access point. + pub id: String, + /// Position in 2D floor-plan coordinates (metres). + pub position: (f32, f32), + /// Estimated coverage radius (metres). + pub coverage_radius: f32, +} + +/// A link between two access points. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApLink { + /// Source AP identifier. + pub from: String, + /// Destination AP identifier. + pub to: String, + /// Measured signal strength between the two APs (0.0-1.0 normalised). + pub signal_strength: f32, +} + +/// Coherence gate state mapped to CRV AOL interpretation. +/// +/// The coherence gate from the viewpoint module produces a binary +/// accept/reject decision. This enum extends it with richer semantics +/// for the CRV pipeline. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum CoherenceGateState { + /// Clean signal line -- coherence is high, proceed normally. + Accept, + /// Mild AOL -- use prediction but flag for review. + PredictOnly, + /// Strong AOL -- pure noise, discard this frame. + Reject, + /// Environment shift detected -- recalibrate the pipeline. + Recalibrate, +} + +/// Result of processing a single CSI frame through Stages I and II. +#[derive(Debug, Clone)] +pub struct CsiCrvResult { + /// Classified gestalt type for this frame. + pub gestalt: GestaltType, + /// Confidence of the gestalt classification (0.0-1.0). + pub gestalt_confidence: f32, + /// Stage I embedding (Poincare ball). + pub gestalt_embedding: Vec, + /// Stage II sensory embedding. + pub sensory_embedding: Vec, +} + +/// Thresholds for gestalt classification from CSI statistics. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GestaltThresholds { + /// Variance threshold above which the signal is considered dynamic. + pub variance_high: f32, + /// Variance threshold below which the signal is considered static. + pub variance_low: f32, + /// Periodicity score above which the signal is considered periodic. + pub periodicity_threshold: f32, + /// Energy spike threshold (ratio of max to mean amplitude). + pub energy_spike_ratio: f32, + /// Structure score threshold for manmade detection. + pub structure_threshold: f32, + /// Null-subcarrier fraction above which the signal is classified as Water. + pub null_fraction_threshold: f32, +} + +impl Default for GestaltThresholds { + fn default() -> Self { + Self { + variance_high: 0.15, + variance_low: 0.03, + periodicity_threshold: 0.5, + energy_spike_ratio: 3.0, + structure_threshold: 0.6, + null_fraction_threshold: 0.3, + } + } +} + +/// Configuration for the WiFi CRV pipeline. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WifiCrvConfig { + /// Embedding dimensionality (passed to [`CrvConfig`]). + pub dimensions: usize, + /// Thresholds for CSI gestalt classification. + pub gestalt_thresholds: GestaltThresholds, + /// Convergence threshold for cross-session matching (0.0-1.0). + pub convergence_threshold: f32, +} + +impl Default for WifiCrvConfig { + fn default() -> Self { + Self { + dimensions: 32, + gestalt_thresholds: GestaltThresholds::default(), + convergence_threshold: 0.6, + } + } +} + +// --------------------------------------------------------------------------- +// CsiGestaltClassifier (Stage I) +// --------------------------------------------------------------------------- + +/// Classifies raw CSI amplitude/phase patterns into CRV gestalt types. +/// +/// The mapping from WiFi signal characteristics to gestalt primitives: +/// +/// - **Movement**: high variance + periodic (person walking) +/// - **Land**: low variance + stable (empty room) +/// - **Energy**: sudden amplitude spikes (door opening, appliance) +/// - **Natural**: smooth gradual changes (temperature drift, slow fading) +/// - **Manmade**: regular structured patterns (HVAC, machinery) +/// - **Water**: many null/zero subcarriers (deep absorption) +#[derive(Debug, Clone)] +pub struct CsiGestaltClassifier { + thresholds: GestaltThresholds, +} + +impl CsiGestaltClassifier { + /// Create a new classifier with the given thresholds. + pub fn new(thresholds: GestaltThresholds) -> Self { + Self { thresholds } + } + + /// Classify a CSI frame into a gestalt type with confidence. + /// + /// Computes variance, periodicity, energy-spike, structure, and + /// null-fraction metrics from the amplitude and phase arrays, then + /// selects the best-matching gestalt type. + /// + /// Returns `(gestalt_type, confidence)` where confidence is in `[0, 1]`. + pub fn classify(&self, amplitudes: &[f32], phases: &[f32]) -> (GestaltType, f32) { + if amplitudes.is_empty() { + return (GestaltType::Land, 0.0); + } + + let variance = Self::compute_variance(amplitudes); + let periodicity = Self::compute_periodicity(amplitudes); + let energy_spike = Self::compute_energy_spike(amplitudes); + let structure = Self::compute_structure(amplitudes, phases); + let null_frac = Self::compute_null_fraction(amplitudes); + + // Score each gestalt type using priority-based gating. + // + // Evaluation order: + // 1. Water (null subcarriers -- very distinctive, takes priority) + // 2. Energy (sudden spikes) + // 3. Movement (high variance + periodic) + // 4. Land (low variance, stable) + // 5. Natural (moderate variance, smooth) + // 6. Manmade (structured -- suppressed when others are strong) + let mut scores = [(GestaltType::Land, 0.0f32); 6]; + + // Water: many null subcarriers (highest priority). + let water_score = if null_frac > self.thresholds.null_fraction_threshold { + 0.7 + 0.3 * null_frac + } else { + 0.1 * null_frac + }; + scores[5] = (GestaltType::Water, water_score); + + // Energy: sudden spikes. + let energy_score = if energy_spike > self.thresholds.energy_spike_ratio { + (energy_spike / (self.thresholds.energy_spike_ratio * 2.0)).min(1.0) + } else { + 0.1 * energy_spike / self.thresholds.energy_spike_ratio.max(1e-6) + }; + scores[2] = (GestaltType::Energy, energy_score); + + // Movement: high variance + periodic. + // Suppress when water or energy are strong indicators. + let movement_suppress = water_score.max(energy_score); + let movement_score = if variance > self.thresholds.variance_high + && movement_suppress < 0.6 + { + 0.6 + 0.4 * periodicity + } else if variance > self.thresholds.variance_high { + (0.6 + 0.4 * periodicity) * (1.0 - movement_suppress) + } else { + 0.15 * periodicity + }; + scores[0] = (GestaltType::Movement, movement_score); + + // Land: low variance + stable. + let land_score = if variance < self.thresholds.variance_low { + 0.7 + 0.3 * (1.0 - periodicity) + } else { + 0.1 * (1.0 - variance.min(1.0)) + }; + scores[1] = (GestaltType::Land, land_score); + + // Natural: smooth gradual changes (moderate variance, low periodicity). + // Structure score being high should not prevent Natural when variance + // is in the moderate range and periodicity is low. + let natural_score = if variance > self.thresholds.variance_low + && variance < self.thresholds.variance_high + && periodicity < self.thresholds.periodicity_threshold + { + 0.7 + 0.3 * (1.0 - periodicity) + } else { + 0.1 + }; + scores[3] = (GestaltType::Natural, natural_score); + + // Manmade: regular structured patterns. + // Suppress when any other strong indicator is present. + let manmade_suppress = water_score + .max(energy_score) + .max(movement_score) + .max(natural_score); + let manmade_score = if structure > self.thresholds.structure_threshold + && manmade_suppress < 0.5 + { + 0.5 + 0.5 * structure + } else { + 0.15 * structure * (1.0 - manmade_suppress).max(0.0) + }; + scores[4] = (GestaltType::Manmade, manmade_score); + + // Pick the highest-scoring type. + let (best_type, best_score) = + scores + .iter() + .fold((GestaltType::Land, 0.0f32), |(bt, bs), &(gt, gs)| { + if gs > bs { + (gt, gs) + } else { + (bt, bs) + } + }); + + (best_type, best_score.clamp(0.0, 1.0)) + } + + /// Compute the variance of the amplitude array. + fn compute_variance(amplitudes: &[f32]) -> f32 { + let n = amplitudes.len() as f32; + if n < 2.0 { + return 0.0; + } + let mean = amplitudes.iter().sum::() / n; + let var = amplitudes.iter().map(|a| (a - mean).powi(2)).sum::() / (n - 1.0); + var / mean.powi(2).max(1e-6) // coefficient of variation squared + } + + /// Estimate periodicity via autocorrelation of detrended signal. + /// + /// Removes the linear trend first so that monotonic signals (ramps, drifts) + /// do not produce false periodicity peaks. Then searches for the highest + /// autocorrelation at lags >= 2 (lag 1 is always near 1.0 for smooth signals). + fn compute_periodicity(amplitudes: &[f32]) -> f32 { + let n = amplitudes.len(); + if n < 6 { + return 0.0; + } + + // Detrend: remove the least-squares linear fit. + let nf = n as f32; + let mean_x = (nf - 1.0) / 2.0; + let mean_y = amplitudes.iter().sum::() / nf; + let mut cov_xy = 0.0f32; + let mut var_x = 0.0f32; + for (i, &a) in amplitudes.iter().enumerate() { + let dx = i as f32 - mean_x; + cov_xy += dx * (a - mean_y); + var_x += dx * dx; + } + let slope = if var_x > 1e-12 { cov_xy / var_x } else { 0.0 }; + let intercept = mean_y - slope * mean_x; + + let detrended: Vec = amplitudes + .iter() + .enumerate() + .map(|(i, &a)| a - (slope * i as f32 + intercept)) + .collect(); + + // Autocorrelation at lag 0. + let r0: f32 = detrended.iter().map(|x| x * x).sum(); + if r0 < 1e-12 { + return 0.0; + } + + // Search for the peak autocorrelation at lags >= 2. + let mut max_r = 0.0f32; + for lag in 2..=(n / 2) { + let r: f32 = detrended + .iter() + .zip(detrended[lag..].iter()) + .map(|(a, b)| a * b) + .sum(); + max_r = max_r.max(r / r0); + } + + max_r.clamp(0.0, 1.0) + } + + /// Compute energy spike ratio (max / mean). + fn compute_energy_spike(amplitudes: &[f32]) -> f32 { + let mean = amplitudes.iter().sum::() / amplitudes.len().max(1) as f32; + let max = amplitudes.iter().cloned().fold(0.0f32, f32::max); + max / mean.max(1e-6) + } + + /// Compute a structure score from amplitude and phase regularity. + /// + /// High structure score indicates regular, repeating patterns typical + /// of manmade signals (e.g. periodic OFDM pilot tones, HVAC interference). + /// A purely smooth/monotonic signal (like a slow ramp) is penalised because + /// "structure" in the WiFi context implies non-trivial oscillation amplitude. + fn compute_structure(amplitudes: &[f32], phases: &[f32]) -> f32 { + if amplitudes.len() < 4 { + return 0.0; + } + + // Compute successive differences. + let diffs: Vec = amplitudes + .windows(2) + .map(|w| (w[1] - w[0]).abs()) + .collect(); + let mean_diff = diffs.iter().sum::() / diffs.len().max(1) as f32; + let var_diff = if diffs.len() > 1 { + diffs.iter().map(|d| (d - mean_diff).powi(2)).sum::() / (diffs.len() - 1) as f32 + } else { + 0.0 + }; + + // Low variance of differences implies regular structure. + let amp_regularity = 1.0 / (1.0 + var_diff); + + // Require non-trivial oscillation: mean diff must be a meaningful + // fraction of the signal range. A slow ramp (tiny diffs) should not + // score high on structure. + let min_a = amplitudes.iter().cloned().fold(f32::MAX, f32::min); + let max_a = amplitudes.iter().cloned().fold(f32::MIN, f32::max); + let range = (max_a - min_a).max(1e-6); + let diff_significance = (mean_diff / range).clamp(0.0, 1.0); + + // Phase regularity: how linear is the phase progression? + let phase_regularity = if phases.len() >= 4 { + let pd: Vec = phases.windows(2).map(|w| w[1] - w[0]).collect(); + let mean_pd = pd.iter().sum::() / pd.len() as f32; + let var_pd = pd.iter().map(|d| (d - mean_pd).powi(2)).sum::() + / (pd.len().max(1) - 1).max(1) as f32; + 1.0 / (1.0 + var_pd) + } else { + 0.5 + }; + + let raw = amp_regularity * 0.6 + phase_regularity * 0.4; + // Scale by diff significance so smooth/monotonic signals get low structure. + (raw * diff_significance).clamp(0.0, 1.0) + } + + /// Fraction of subcarriers with near-zero amplitude. + fn compute_null_fraction(amplitudes: &[f32]) -> f32 { + let threshold = 1e-3; + let nulls = amplitudes.iter().filter(|&&a| a.abs() < threshold).count(); + nulls as f32 / amplitudes.len().max(1) as f32 + } +} + +// --------------------------------------------------------------------------- +// CsiSensoryEncoder (Stage II) +// --------------------------------------------------------------------------- + +/// Extracts sensory-like features from CSI data for Stage II encoding. +/// +/// The mapping from signal processing metrics to sensory modalities: +/// +/// - **Texture** -> amplitude roughness (high-frequency variance) +/// - **Color** -> frequency-domain spectral centroid +/// - **Temperature** -> signal energy (total power) +/// - **Sound** -> temporal periodicity (breathing/heartbeat frequency) +/// - **Luminosity** -> SNR / coherence level +/// - **Dimension** -> subcarrier spread (bandwidth utilisation) +#[derive(Debug, Clone)] +pub struct CsiSensoryEncoder; + +impl CsiSensoryEncoder { + /// Create a new sensory encoder. + pub fn new() -> Self { + Self + } + + /// Extract sensory impressions from a CSI frame. + /// + /// Returns a list of `(SensoryModality, descriptor_string)` pairs + /// suitable for feeding into [`ruvector_crv::StageIIEncoder`]. + pub fn extract( + &self, + amplitudes: &[f32], + phases: &[f32], + ) -> Vec<(SensoryModality, String)> { + let mut impressions = Vec::new(); + + // Texture: amplitude roughness (high-freq variance). + let roughness = self.amplitude_roughness(amplitudes); + let texture_desc = if roughness > 0.5 { + "rough coarse" + } else if roughness > 0.2 { + "moderate grain" + } else { + "smooth flat" + }; + impressions.push((SensoryModality::Texture, texture_desc.to_string())); + + // Color: spectral centroid (maps to a pseudo colour). + let centroid = self.spectral_centroid(amplitudes); + let color_desc = if centroid > 0.7 { + "blue high-freq" + } else if centroid > 0.4 { + "green mid-freq" + } else { + "red low-freq" + }; + impressions.push((SensoryModality::Color, color_desc.to_string())); + + // Temperature: signal energy (total power). + let energy = self.signal_energy(amplitudes); + let temp_desc = if energy > 1.0 { + "hot high-power" + } else if energy > 0.3 { + "warm moderate" + } else { + "cold low-power" + }; + impressions.push((SensoryModality::Temperature, temp_desc.to_string())); + + // Sound: temporal periodicity. + let periodicity = CsiGestaltClassifier::compute_periodicity(amplitudes); + let sound_desc = if periodicity > 0.6 { + "rhythmic periodic" + } else if periodicity > 0.3 { + "hum steady" + } else { + "silent still" + }; + impressions.push((SensoryModality::Sound, sound_desc.to_string())); + + // Luminosity: phase coherence as SNR proxy. + let snr = self.phase_coherence(phases); + let lum_desc = if snr > 0.7 { + "bright clear" + } else if snr > 0.4 { + "dim moderate" + } else { + "dark noisy" + }; + impressions.push((SensoryModality::Luminosity, lum_desc.to_string())); + + // Dimension: subcarrier spread. + let spread = self.subcarrier_spread(amplitudes); + let dim_desc = if spread > 0.7 { + "vast wide" + } else if spread > 0.3 { + "medium regular" + } else { + "narrow compact" + }; + impressions.push((SensoryModality::Dimension, dim_desc.to_string())); + + impressions + } + + /// Amplitude roughness: mean absolute difference normalised by signal range. + /// + /// High roughness means large sample-to-sample jumps relative to the + /// dynamic range, indicating irregular high-frequency amplitude variation. + fn amplitude_roughness(&self, amplitudes: &[f32]) -> f32 { + if amplitudes.len() < 3 { + return 0.0; + } + let min_a = amplitudes.iter().cloned().fold(f32::MAX, f32::min); + let max_a = amplitudes.iter().cloned().fold(f32::MIN, f32::max); + let range = (max_a - min_a).max(1e-6); + + let mean_abs_diff: f32 = amplitudes + .windows(2) + .map(|w| (w[1] - w[0]).abs()) + .sum::() + / (amplitudes.len() - 1) as f32; + + (mean_abs_diff / range).clamp(0.0, 1.0) + } + + /// Spectral centroid: weighted mean of subcarrier indices. + fn spectral_centroid(&self, amplitudes: &[f32]) -> f32 { + let total: f32 = amplitudes.iter().sum(); + if total < 1e-12 { + return 0.5; + } + let weighted: f32 = amplitudes + .iter() + .enumerate() + .map(|(i, &a)| i as f32 * a) + .sum(); + let centroid = weighted / total; + let n = amplitudes.len().max(1) as f32; + (centroid / n).clamp(0.0, 1.0) + } + + /// Signal energy: mean squared amplitude. + fn signal_energy(&self, amplitudes: &[f32]) -> f32 { + let n = amplitudes.len().max(1) as f32; + amplitudes.iter().map(|a| a * a).sum::() / n + } + + /// Phase coherence: magnitude of the mean unit phasor. + fn phase_coherence(&self, phases: &[f32]) -> f32 { + if phases.is_empty() { + return 0.0; + } + let n = phases.len() as f32; + let sum_cos: f32 = phases.iter().map(|p| p.cos()).sum(); + let sum_sin: f32 = phases.iter().map(|p| p.sin()).sum(); + ((sum_cos / n).powi(2) + (sum_sin / n).powi(2)).sqrt() + } + + /// Subcarrier spread: fraction of subcarriers above a threshold. + fn subcarrier_spread(&self, amplitudes: &[f32]) -> f32 { + if amplitudes.is_empty() { + return 0.0; + } + let max = amplitudes.iter().cloned().fold(0.0f32, f32::max); + let threshold = max * 0.1; + let active = amplitudes.iter().filter(|&&a| a > threshold).count(); + active as f32 / amplitudes.len() as f32 + } +} + +// --------------------------------------------------------------------------- +// WifiCrvPipeline (main entry point) +// --------------------------------------------------------------------------- + +/// Main entry point for the WiFi CRV signal-line integration. +/// +/// Wraps [`CrvSessionManager`] with WiFi-DensePose domain logic so that +/// callers feed CSI frames and AP topology rather than raw CRV stage data. +pub struct WifiCrvPipeline { + /// Underlying CRV session manager. + manager: CrvSessionManager, + /// Gestalt classifier for Stage I. + gestalt: CsiGestaltClassifier, + /// Sensory encoder for Stage II. + sensory: CsiSensoryEncoder, + /// Pipeline configuration. + config: WifiCrvConfig, +} + +impl WifiCrvPipeline { + /// Create a new WiFi CRV pipeline. + pub fn new(config: WifiCrvConfig) -> Self { + let crv_config = CrvConfig { + dimensions: config.dimensions, + convergence_threshold: config.convergence_threshold, + ..CrvConfig::default() + }; + let manager = CrvSessionManager::new(crv_config); + let gestalt = CsiGestaltClassifier::new(config.gestalt_thresholds.clone()); + let sensory = CsiSensoryEncoder::new(); + + Self { + manager, + gestalt, + sensory, + config, + } + } + + /// Create a new CRV session for a room. + /// + /// The `session_id` identifies the sensing session and `room_id` + /// acts as the CRV target coordinate so that cross-session + /// convergence can be computed per room. + pub fn create_session( + &mut self, + session_id: &str, + room_id: &str, + ) -> Result<(), CrvError> { + self.manager + .create_session(session_id.to_string(), room_id.to_string()) + } + + /// Process a CSI frame through Stages I and II. + /// + /// Classifies the frame into a gestalt type, extracts sensory features, + /// and adds both embeddings to the session. + pub fn process_csi_frame( + &mut self, + session_id: &str, + amplitudes: &[f32], + phases: &[f32], + ) -> Result { + if amplitudes.is_empty() { + return Err(CrvError::EmptyInput( + "CSI amplitudes are empty".to_string(), + )); + } + + // Stage I: Gestalt classification. + let (gestalt_type, confidence) = self.gestalt.classify(amplitudes, phases); + + // Build a synthetic ideogram stroke from the amplitude envelope + // so the CRV Stage I encoder can produce a Poincare ball embedding. + let stroke: Vec<(f32, f32)> = amplitudes + .iter() + .enumerate() + .map(|(i, &a)| (i as f32 / amplitudes.len().max(1) as f32, a)) + .collect(); + + let stage_i = StageIData { + stroke, + spontaneous_descriptor: format!("{:?}", gestalt_type).to_lowercase(), + classification: gestalt_type, + confidence, + }; + + let gestalt_embedding = self.manager.add_stage_i(session_id, &stage_i)?; + + // Stage II: Sensory feature extraction. + let impressions = self.sensory.extract(amplitudes, phases); + let stage_ii = StageIIData { + impressions, + feature_vector: None, + }; + + let sensory_embedding = self.manager.add_stage_ii(session_id, &stage_ii)?; + + Ok(CsiCrvResult { + gestalt: gestalt_type, + gestalt_confidence: confidence, + gestalt_embedding, + sensory_embedding, + }) + } + + /// Add AP mesh topology as Stage III spatial data. + /// + /// Each AP becomes a sketch element positioned at its floor-plan + /// coordinates with scale proportional to coverage radius. Links + /// become spatial relationships with strength from signal strength. + /// + /// Returns the Stage III embedding. + pub fn add_mesh_topology( + &mut self, + session_id: &str, + nodes: &[ApNode], + links: &[ApLink], + ) -> Result, CrvError> { + if nodes.is_empty() { + return Err(CrvError::EmptyInput("No AP nodes provided".to_string())); + } + + let sketch_elements: Vec = nodes + .iter() + .map(|ap| SketchElement { + label: ap.id.clone(), + kind: GeometricKind::Circle, + position: ap.position, + scale: Some(ap.coverage_radius), + }) + .collect(); + + let relationships: Vec = links + .iter() + .map(|link| SpatialRelationship { + from: link.from.clone(), + to: link.to.clone(), + relation: SpatialRelationType::Connected, + strength: link.signal_strength, + }) + .collect(); + + let stage_iii = StageIIIData { + sketch_elements, + relationships, + }; + + self.manager.add_stage_iii(session_id, &stage_iii) + } + + /// Add a coherence gate state as Stage IV AOL data. + /// + /// Maps the coherence gate decision to AOL semantics: + /// - `Accept` -> clean signal line (no AOL) + /// - `PredictOnly` -> mild AOL (flagged but usable) + /// - `Reject` -> strong AOL (noise, discard) + /// - `Recalibrate` -> environment shift (AOL + tangible change) + /// + /// Returns the Stage IV embedding. + pub fn add_coherence_state( + &mut self, + session_id: &str, + state: CoherenceGateState, + score: f32, + ) -> Result, CrvError> { + let (emotional_impact, tangibles, intangibles, aol_detections) = match state { + CoherenceGateState::Accept => ( + vec![("confidence".to_string(), 0.8)], + vec!["stable environment".to_string()], + vec!["clean signal line".to_string()], + vec![], + ), + CoherenceGateState::PredictOnly => ( + vec![("uncertainty".to_string(), 0.5)], + vec!["prediction mode".to_string()], + vec!["mild interference".to_string()], + vec![AOLDetection { + content: "mild coherence loss".to_string(), + timestamp_ms: 0, + flagged: true, + anomaly_score: score.clamp(0.0, 1.0), + }], + ), + CoherenceGateState::Reject => ( + vec![("noise".to_string(), 0.9)], + vec![], + vec!["signal contaminated".to_string()], + vec![AOLDetection { + content: "strong incoherence".to_string(), + timestamp_ms: 0, + flagged: true, + anomaly_score: 1.0, + }], + ), + CoherenceGateState::Recalibrate => ( + vec![("disruption".to_string(), 0.7)], + vec!["environment change".to_string()], + vec!["recalibration needed".to_string()], + vec![AOLDetection { + content: "environment shift".to_string(), + timestamp_ms: 0, + flagged: false, + anomaly_score: score.clamp(0.0, 1.0), + }], + ), + }; + + let stage_iv = StageIVData { + emotional_impact, + tangibles, + intangibles, + aol_detections, + }; + + self.manager.add_stage_iv(session_id, &stage_iv) + } + + /// Run Stage V interrogation on a session. + /// + /// Given a query embedding (e.g. encoding of "is person moving?"), + /// probes the accumulated session data via differentiable search. + pub fn interrogate( + &mut self, + session_id: &str, + query_embedding: &[f32], + ) -> Result { + if query_embedding.is_empty() { + return Err(CrvError::EmptyInput( + "Query embedding is empty".to_string(), + )); + } + + // Probe all stages 1-4 with the query. + let probes: Vec<(&str, u8, Vec)> = (1..=4) + .map(|stage| ("csi-query", stage, query_embedding.to_vec())) + .collect(); + + let k = 3.min(self.manager.session_entry_count(session_id)); + if k == 0 { + return Err(CrvError::EmptyInput( + "Session has no entries to interrogate".to_string(), + )); + } + + self.manager.run_stage_v(session_id, &probes, k) + } + + /// Run Stage VI person partitioning on a session. + /// + /// Uses MinCut to partition the accumulated session data into + /// distinct target aspects -- in the WiFi sensing context these + /// correspond to distinct persons or environment zones. + pub fn partition_persons( + &mut self, + session_id: &str, + ) -> Result { + self.manager.run_stage_vi(session_id) + } + + /// Find cross-session convergence for a room. + /// + /// Analyses all sessions targeting the given `room_id` to find + /// agreement between independent sensing sessions. Higher convergence + /// indicates that multiple sessions see the same signal patterns. + pub fn find_cross_room_convergence( + &self, + room_id: &str, + min_similarity: f32, + ) -> Result { + self.manager.find_convergence(room_id, min_similarity) + } + + /// Get the number of entries in a session. + pub fn session_entry_count(&self, session_id: &str) -> usize { + self.manager.session_entry_count(session_id) + } + + /// Get the number of active sessions. + pub fn session_count(&self) -> usize { + self.manager.session_count() + } + + /// Remove a session. + pub fn remove_session(&mut self, session_id: &str) -> bool { + self.manager.remove_session(session_id) + } + + /// Get the pipeline configuration. + pub fn config(&self) -> &WifiCrvConfig { + &self.config + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + // -- Helpers -- + + fn test_config() -> WifiCrvConfig { + WifiCrvConfig { + dimensions: 32, + gestalt_thresholds: GestaltThresholds::default(), + convergence_threshold: 0.5, + } + } + + /// Generate a periodic amplitude signal. + fn periodic_signal(n: usize, freq: f32, amplitude: f32) -> Vec { + (0..n) + .map(|i| amplitude * (2.0 * std::f32::consts::PI * freq * i as f32 / n as f32).sin().abs() + 0.1) + .collect() + } + + /// Generate a constant (static) amplitude signal. + fn static_signal(n: usize, level: f32) -> Vec { + vec![level; n] + } + + /// Generate linear phases. + fn linear_phases(n: usize) -> Vec { + (0..n).map(|i| i as f32 * 0.1).collect() + } + + /// Generate random-ish phases. + fn varied_phases(n: usize) -> Vec { + (0..n) + .map(|i| (i as f32 * 2.718).sin() * std::f32::consts::PI) + .collect() + } + + // ---- Stage I: Gestalt Classification ---- + + #[test] + fn gestalt_movement_high_variance_periodic() { + let classifier = CsiGestaltClassifier::new(GestaltThresholds::default()); + let amps = periodic_signal(64, 4.0, 1.0); + let phases = linear_phases(64); + let (gestalt, conf) = classifier.classify(&s, &phases); + assert_eq!(gestalt, GestaltType::Movement); + assert!(conf > 0.3, "movement confidence should be reasonable: {conf}"); + } + + #[test] + fn gestalt_land_low_variance_stable() { + let classifier = CsiGestaltClassifier::new(GestaltThresholds::default()); + let amps = static_signal(64, 0.5); + let phases = linear_phases(64); + let (gestalt, conf) = classifier.classify(&s, &phases); + assert_eq!(gestalt, GestaltType::Land); + assert!(conf > 0.5, "land confidence: {conf}"); + } + + #[test] + fn gestalt_energy_spike() { + let classifier = CsiGestaltClassifier::new(GestaltThresholds::default()); + let mut amps = vec![0.1f32; 64]; + amps[32] = 5.0; // large spike + let phases = linear_phases(64); + let (gestalt, conf) = classifier.classify(&s, &phases); + assert_eq!(gestalt, GestaltType::Energy); + assert!(conf > 0.3, "energy confidence: {conf}"); + } + + #[test] + fn gestalt_water_null_subcarriers() { + let classifier = CsiGestaltClassifier::new(GestaltThresholds::default()); + let mut amps = vec![0.5f32; 64]; + // Set half the subcarriers to zero. + for i in 0..32 { + amps[i] = 0.0; + } + let phases = linear_phases(64); + let (gestalt, conf) = classifier.classify(&s, &phases); + assert_eq!(gestalt, GestaltType::Water); + assert!(conf > 0.3, "water confidence: {conf}"); + } + + #[test] + fn gestalt_manmade_structured() { + let classifier = CsiGestaltClassifier::new(GestaltThresholds { + structure_threshold: 0.4, + ..GestaltThresholds::default() + }); + // Perfectly regular alternating pattern. + let amps: Vec = (0..64).map(|i| if i % 2 == 0 { 1.0 } else { 0.8 }).collect(); + let phases = linear_phases(64); + let (gestalt, conf) = classifier.classify(&s, &phases); + assert_eq!(gestalt, GestaltType::Manmade); + assert!(conf > 0.3, "manmade confidence: {conf}"); + } + + #[test] + fn gestalt_natural_smooth_gradual() { + let classifier = CsiGestaltClassifier::new(GestaltThresholds { + variance_low: 0.001, + variance_high: 0.5, + ..GestaltThresholds::default() + }); + // Slow ramp -- moderate variance, low periodicity, low structure. + let amps: Vec = (0..64).map(|i| 0.3 + 0.005 * i as f32).collect(); + let phases = varied_phases(64); + let (gestalt, conf) = classifier.classify(&s, &phases); + assert_eq!(gestalt, GestaltType::Natural); + assert!(conf > 0.3, "natural confidence: {conf}"); + } + + #[test] + fn gestalt_empty_amplitudes() { + let classifier = CsiGestaltClassifier::new(GestaltThresholds::default()); + let (gestalt, conf) = classifier.classify(&[], &[]); + assert_eq!(gestalt, GestaltType::Land); + assert_eq!(conf, 0.0); + } + + #[test] + fn gestalt_single_subcarrier() { + let classifier = CsiGestaltClassifier::new(GestaltThresholds::default()); + let (gestalt, _conf) = classifier.classify(&[1.0], &[0.0]); + // With a single value variance is 0 => Land. + assert_eq!(gestalt, GestaltType::Land); + } + + // ---- Stage II: Sensory Feature Extraction ---- + + #[test] + fn sensory_extraction_returns_six_modalities() { + let encoder = CsiSensoryEncoder::new(); + let amps = periodic_signal(32, 2.0, 0.5); + let phases = linear_phases(32); + let impressions = encoder.extract(&s, &phases); + assert_eq!(impressions.len(), 6); + } + + #[test] + fn sensory_texture_rough_for_noisy() { + let encoder = CsiSensoryEncoder::new(); + // Very spiky signal -> rough texture. + let amps: Vec = (0..64) + .map(|i| if i % 2 == 0 { 2.0 } else { 0.01 }) + .collect(); + let phases = linear_phases(64); + let impressions = encoder.extract(&s, &phases); + let texture = &impressions[0]; + assert_eq!(texture.0, SensoryModality::Texture); + assert!( + texture.1.contains("rough") || texture.1.contains("coarse"), + "expected rough texture, got: {}", + texture.1 + ); + } + + #[test] + fn sensory_luminosity_bright_for_coherent() { + let encoder = CsiSensoryEncoder::new(); + let amps = static_signal(32, 1.0); + let phases = vec![0.5f32; 32]; // identical phases = high coherence + let impressions = encoder.extract(&s, &phases); + let lum = impressions.iter().find(|(m, _)| *m == SensoryModality::Luminosity); + assert!(lum.is_some()); + let desc = &lum.unwrap().1; + assert!( + desc.contains("bright"), + "expected bright for coherent phases, got: {desc}" + ); + } + + #[test] + fn sensory_temperature_cold_for_low_power() { + let encoder = CsiSensoryEncoder::new(); + let amps = static_signal(32, 0.01); + let phases = linear_phases(32); + let impressions = encoder.extract(&s, &phases); + let temp = impressions.iter().find(|(m, _)| *m == SensoryModality::Temperature); + assert!(temp.is_some()); + assert!( + temp.unwrap().1.contains("cold"), + "expected cold for low power" + ); + } + + #[test] + fn sensory_empty_amplitudes() { + let encoder = CsiSensoryEncoder::new(); + let impressions = encoder.extract(&[], &[]); + // Should still return impressions (with default/zero-ish values). + assert_eq!(impressions.len(), 6); + } + + // ---- Stage III: Mesh Topology ---- + + #[test] + fn mesh_topology_two_aps() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + + let nodes = vec![ + ApNode { + id: "ap-1".into(), + position: (0.0, 0.0), + coverage_radius: 10.0, + }, + ApNode { + id: "ap-2".into(), + position: (5.0, 0.0), + coverage_radius: 8.0, + }, + ]; + let links = vec![ApLink { + from: "ap-1".into(), + to: "ap-2".into(), + signal_strength: 0.7, + }]; + + let embedding = pipeline.add_mesh_topology("s1", &nodes, &links).unwrap(); + assert_eq!(embedding.len(), 32); + } + + #[test] + fn mesh_topology_empty_nodes_errors() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + let result = pipeline.add_mesh_topology("s1", &[], &[]); + assert!(result.is_err()); + } + + #[test] + fn mesh_topology_single_ap_no_links() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + + let nodes = vec![ApNode { + id: "ap-solo".into(), + position: (1.0, 2.0), + coverage_radius: 5.0, + }]; + + let embedding = pipeline.add_mesh_topology("s1", &nodes, &[]).unwrap(); + assert_eq!(embedding.len(), 32); + } + + // ---- Stage IV: Coherence -> AOL ---- + + #[test] + fn coherence_accept_clean_signal() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + + let emb = pipeline + .add_coherence_state("s1", CoherenceGateState::Accept, 0.9) + .unwrap(); + assert_eq!(emb.len(), 32); + } + + #[test] + fn coherence_reject_noisy() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + + let emb = pipeline + .add_coherence_state("s1", CoherenceGateState::Reject, 0.1) + .unwrap(); + assert_eq!(emb.len(), 32); + } + + #[test] + fn coherence_predict_only() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + + let emb = pipeline + .add_coherence_state("s1", CoherenceGateState::PredictOnly, 0.5) + .unwrap(); + assert_eq!(emb.len(), 32); + } + + #[test] + fn coherence_recalibrate() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + + let emb = pipeline + .add_coherence_state("s1", CoherenceGateState::Recalibrate, 0.6) + .unwrap(); + assert_eq!(emb.len(), 32); + } + + // ---- Full Pipeline Flow ---- + + #[test] + fn full_pipeline_create_process_interrogate_partition() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + + // Process two CSI frames. + let amps1 = periodic_signal(32, 2.0, 0.8); + let phases1 = linear_phases(32); + let result1 = pipeline.process_csi_frame("s1", &s1, &phases1).unwrap(); + assert_eq!(result1.gestalt_embedding.len(), 32); + assert_eq!(result1.sensory_embedding.len(), 32); + + let amps2 = static_signal(32, 0.5); + let phases2 = linear_phases(32); + let result2 = pipeline.process_csi_frame("s1", &s2, &phases2).unwrap(); + assert_ne!(result1.gestalt, result2.gestalt); + + // Add mesh topology. + let nodes = vec![ + ApNode { id: "ap-1".into(), position: (0.0, 0.0), coverage_radius: 10.0 }, + ApNode { id: "ap-2".into(), position: (5.0, 3.0), coverage_radius: 8.0 }, + ]; + let links = vec![ApLink { + from: "ap-1".into(), + to: "ap-2".into(), + signal_strength: 0.8, + }]; + pipeline.add_mesh_topology("s1", &nodes, &links).unwrap(); + + // Add coherence state. + pipeline + .add_coherence_state("s1", CoherenceGateState::Accept, 0.85) + .unwrap(); + + assert_eq!(pipeline.session_entry_count("s1"), 6); + + // Interrogate. + let query = vec![0.5f32; 32]; + let stage_v = pipeline.interrogate("s1", &query).unwrap(); + // Should have probes for stages 1-4 that have entries. + assert!(!stage_v.probes.is_empty()); + + // Partition. + let stage_vi = pipeline.partition_persons("s1").unwrap(); + assert!(!stage_vi.partitions.is_empty()); + } + + #[test] + fn pipeline_session_not_found() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + let result = pipeline.process_csi_frame("nonexistent", &[1.0], &[0.0]); + assert!(result.is_err()); + } + + #[test] + fn pipeline_empty_csi_frame() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + let result = pipeline.process_csi_frame("s1", &[], &[]); + assert!(result.is_err()); + } + + #[test] + fn pipeline_empty_query_interrogation() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + let result = pipeline.interrogate("s1", &[]); + assert!(result.is_err()); + } + + #[test] + fn pipeline_interrogate_empty_session() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + let result = pipeline.interrogate("s1", &[1.0; 32]); + assert!(result.is_err()); + } + + // ---- Cross-Session Convergence ---- + + #[test] + fn cross_session_convergence_same_room() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("viewer-a", "room-1").unwrap(); + pipeline.create_session("viewer-b", "room-1").unwrap(); + + // Both viewers see the same periodic signal. + let amps = periodic_signal(32, 2.0, 0.8); + let phases = linear_phases(32); + + pipeline + .process_csi_frame("viewer-a", &s, &phases) + .unwrap(); + pipeline + .process_csi_frame("viewer-b", &s, &phases) + .unwrap(); + + let convergence = pipeline + .find_cross_room_convergence("room-1", 0.5) + .unwrap(); + assert!( + !convergence.scores.is_empty(), + "identical frames should converge" + ); + assert!(convergence.scores[0] > 0.5); + } + + #[test] + fn cross_session_convergence_different_signals() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("a", "room-2").unwrap(); + pipeline.create_session("b", "room-2").unwrap(); + + // Very different signals. + let amps_a = periodic_signal(32, 8.0, 2.0); + let amps_b = static_signal(32, 0.01); + let phases = linear_phases(32); + + pipeline + .process_csi_frame("a", &s_a, &phases) + .unwrap(); + pipeline + .process_csi_frame("b", &s_b, &phases) + .unwrap(); + + let convergence = pipeline.find_cross_room_convergence("room-2", 0.95); + // May or may not converge at high threshold; the key is no panic. + assert!(convergence.is_ok()); + } + + #[test] + fn cross_session_needs_two_sessions() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("solo", "room-3").unwrap(); + pipeline + .process_csi_frame("solo", &[1.0; 32], &[0.0; 32]) + .unwrap(); + + let result = pipeline.find_cross_room_convergence("room-3", 0.5); + assert!(result.is_err(), "convergence requires at least 2 sessions"); + } + + // ---- Session management ---- + + #[test] + fn session_create_and_remove() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + assert_eq!(pipeline.session_count(), 1); + assert!(pipeline.remove_session("s1")); + assert_eq!(pipeline.session_count(), 0); + assert!(!pipeline.remove_session("s1")); + } + + #[test] + fn session_duplicate_errors() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + let result = pipeline.create_session("s1", "room-a"); + assert!(result.is_err()); + } + + // ---- Edge cases ---- + + #[test] + fn zero_amplitude_frame() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + + let amps = vec![0.0f32; 32]; + let phases = vec![0.0f32; 32]; + let result = pipeline.process_csi_frame("s1", &s, &phases); + // Should succeed (all-zero is a valid edge case). + assert!(result.is_ok()); + } + + #[test] + fn single_subcarrier_frame() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + + let result = pipeline.process_csi_frame("s1", &[1.0], &[0.5]); + assert!(result.is_ok()); + } + + #[test] + fn large_frame_256_subcarriers() { + let mut pipeline = WifiCrvPipeline::new(test_config()); + pipeline.create_session("s1", "room-a").unwrap(); + + let amps = periodic_signal(256, 10.0, 1.0); + let phases = linear_phases(256); + let result = pipeline.process_csi_frame("s1", &s, &phases); + assert!(result.is_ok()); + assert_eq!(result.unwrap().gestalt_embedding.len(), 32); + } + + // ---- CsiGestaltClassifier helpers ---- + + #[test] + fn compute_variance_static() { + let v = CsiGestaltClassifier::compute_variance(&[1.0; 32]); + assert!(v < 1e-6, "static signal should have near-zero variance"); + } + + #[test] + fn compute_periodicity_constant() { + let p = CsiGestaltClassifier::compute_periodicity(&[1.0; 32]); + // Constant signal: autocorrelation peak ratio depends on zero-variance handling. + assert!(p >= 0.0 && p <= 1.0); + } + + #[test] + fn compute_null_fraction_all_zeros() { + let f = CsiGestaltClassifier::compute_null_fraction(&[0.0; 32]); + assert!((f - 1.0).abs() < 1e-6, "all zeros should give null fraction 1.0"); + } + + #[test] + fn compute_null_fraction_none_zero() { + let f = CsiGestaltClassifier::compute_null_fraction(&[1.0; 32]); + assert!(f < 1e-6, "no nulls should give null fraction 0.0"); + } + + // ---- CsiSensoryEncoder helpers ---- + + #[test] + fn spectral_centroid_uniform() { + let encoder = CsiSensoryEncoder::new(); + let amps = vec![1.0f32; 32]; + let centroid = encoder.spectral_centroid(&s); + // Uniform -> centroid at midpoint. + assert!( + (centroid - 0.484).abs() < 0.1, + "uniform spectral centroid should be near 0.5, got {centroid}" + ); + } + + #[test] + fn signal_energy_known() { + let encoder = CsiSensoryEncoder::new(); + let energy = encoder.signal_energy(&[2.0, 2.0, 2.0, 2.0]); + assert!((energy - 4.0).abs() < 1e-6, "energy of [2,2,2,2] should be 4.0"); + } + + #[test] + fn phase_coherence_identical() { + let encoder = CsiSensoryEncoder::new(); + let c = encoder.phase_coherence(&[1.0; 100]); + assert!(c > 0.99, "identical phases should give coherence ~1.0, got {c}"); + } + + #[test] + fn phase_coherence_empty() { + let encoder = CsiSensoryEncoder::new(); + let c = encoder.phase_coherence(&[]); + assert_eq!(c, 0.0); + } + + #[test] + fn subcarrier_spread_all_active() { + let encoder = CsiSensoryEncoder::new(); + let spread = encoder.subcarrier_spread(&[1.0; 32]); + assert!((spread - 1.0).abs() < 1e-6, "all active should give spread 1.0"); + } + + #[test] + fn subcarrier_spread_empty() { + let encoder = CsiSensoryEncoder::new(); + let spread = encoder.subcarrier_spread(&[]); + assert_eq!(spread, 0.0); + } +} diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/src/lib.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/src/lib.rs index 20c43d9..164beb0 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/src/lib.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/src/lib.rs @@ -26,6 +26,7 @@ #![warn(missing_docs)] +pub mod crv; pub mod mat; pub mod signal; pub mod viewpoint; diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.lock b/rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.lock new file mode 100644 index 0000000..fa95d21 --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.lock @@ -0,0 +1,1129 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a640b26f007713818e9a9b65d34da1cf58538207b052916a83d80e43f3ffa4" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.15.5", + "indexmap", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "roaring" +version = "0.10.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e8d2cfa184d94d0726d650a9f4a1be7f9b76ac9fdb954219878dc00c1c1e7b" +dependencies = [ + "bytemuck", + "byteorder", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ruvector-attention" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc18d0ffdebacabce4a4c6030e4359682ffe667fd7aab0c3e5bbe547693da3a" +dependencies = [ + "rand", + "rayon", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "ruvector-core" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b36b82b634e327ebfcd1eb94a2963ea051db84a8d6ea2b25c4b5ae58d33a7079" +dependencies = [ + "anyhow", + "bincode", + "chrono", + "dashmap", + "ndarray", + "once_cell", + "parking_lot", + "rand", + "rand_distr", + "rkyv", + "serde", + "serde_json", + "thiserror 2.0.17", + "tracing", + "uuid", +] + +[[package]] +name = "ruvector-crv" +version = "0.1.1" +dependencies = [ + "approx", + "ruvector-attention", + "ruvector-gnn", + "ruvector-mincut", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "ruvector-gnn" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad1c6c0ef0e74296c7a5bd2485463786d15cf4314e8f4581d066b6e1ecf96c2" +dependencies = [ + "anyhow", + "dashmap", + "libc", + "ndarray", + "parking_lot", + "rand", + "rand_distr", + "rayon", + "ruvector-core", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "ruvector-mincut" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d05b186a9bf694063851c79b2aed290af7be46f7f9bdb3c32e3c0b7d63d33" +dependencies = [ + "anyhow", + "crossbeam", + "dashmap", + "ordered-float", + "parking_lot", + "petgraph", + "rand", + "rayon", + "roaring", + "ruvector-core", + "serde", + "serde_json", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.toml b/rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.toml new file mode 100644 index 0000000..a509415 --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.toml @@ -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" diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.toml.orig b/rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.toml.orig new file mode 100644 index 0000000..41a850d --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/Cargo.toml.orig @@ -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" diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/README.md b/rust-port/wifi-densepose-rs/patches/ruvector-crv/README.md new file mode 100644 index 0000000..7c3acb5 --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/README.md @@ -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 diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/error.rs b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/error.rs new file mode 100644 index 0000000..92a0ceb --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/error.rs @@ -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 = Result; diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/lib.rs b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/lib.rs new file mode 100644 index 0000000..67ef168 --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/lib.rs @@ -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); + } + } +} diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/session.rs b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/session.rs new file mode 100644 index 0000000..61bf27c --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/session.rs @@ -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, + /// Stage number (1-6). + stage: u8, + /// Entry index within the stage. + entry_index: usize, + /// Metadata. + metadata: HashMap, + /// 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, +} + +/// 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, +} + +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> { + 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> { + 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> { + 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> { + 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)], // (query text, target stage, query embedding) + k: usize, + ) -> CrvResult { + let session = self + .sessions + .get(session_id) + .ok_or_else(|| CrvError::SessionNotFound(session_id.to_string()))?; + + let all_embeddings: Vec> = + 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> = 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> = session + .entries + .iter() + .filter(|e| e.stage == from_stage) + .map(|e| e.embedding.clone()) + .collect(); + let to_entries: Vec> = 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 { + let session = self + .sessions + .get(session_id) + .ok_or_else(|| CrvError::SessionNotFound(session_id.to_string()))?; + + let embeddings: Vec> = + 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 { + // 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> { + 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, + metadata: HashMap, + ) -> 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")); + } +} diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_i.rs b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_i.rs new file mode 100644 index 0000000..73f5c5a --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_i.rs @@ -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>, +} + +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> { + 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> { + 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::() / curvatures.len() as f32; + let var_k: f32 = curvatures.iter().map(|k| (k - mean_k).powi(2)).sum::() + / 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::().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> { + 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 = 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> { + 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 { + 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 { + 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::() + / proto.len() as f32; + assert!(error < 0.1); + } +} diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_ii.rs b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_ii.rs new file mode 100644 index 0000000..6cfe252 --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_ii.rs @@ -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 { + 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::().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> { + 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> = 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 { + 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::() 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::().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::().sqrt(); + let norm_b: f32 = b.iter().map(|x| x * x).sum::().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::().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()); + } +} diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_iii.rs b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_iii.rs new file mode 100644 index 0000000..85e7d4e --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_iii.rs @@ -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) -> Vec { + 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::().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> { + 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> = 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()); + } +} diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_iv.rs b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_iv.rs new file mode 100644 index 0000000..2b95b9d --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_iv.rs @@ -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 { + 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 { + 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> { + // 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 = 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::().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> { + 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::().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::().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); + } + } +} diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_v.rs b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_v.rs new file mode 100644 index 0000000..69fe793 --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_v.rs @@ -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], + k: usize, + ) -> CrvResult { + 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], + to_stage: u8, + to_entries: &[Vec], + threshold: f32, + ) -> Vec { + 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]) -> CrvResult> { + 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::().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![]; + + assert!(engine.probe(&query, &candidates, 5).is_err()); + } +} diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_vi.rs b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_vi.rs new file mode 100644 index 0000000..0fd2f2a --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/stage_vi.rs @@ -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]) -> 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 { + 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], + stage_labels: &[(u8, usize)], // (stage, entry_index) for each embedding + ) -> CrvResult { + 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 = 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::>(), + ); + 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::>(), + ); + let centroid_b = self.compute_centroid( + &group_b.iter().map(|&i| embeddings[i].as_slice()).collect::>(), + ); + + 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]) -> (Vec, Vec) { + 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> { + 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); + } +} diff --git a/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/types.rs b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/types.rs new file mode 100644 index 0000000..540ae19 --- /dev/null +++ b/rust-port/wifi-densepose-rs/patches/ruvector-crv/src/types.rs @@ -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>, +} + +/// 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, + /// Spatial relationships between elements. + pub relationships: Vec, +} + +/// 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, +} + +/// 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, + /// Intangible concept impressions (purpose, function, significance). + pub intangibles: Vec, + /// Analytical overlay detections with timestamps. + pub aol_detections: Vec, +} + +/// 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, + /// Cross-references to data from earlier stages. + pub cross_references: Vec, +} + +/// 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, + /// Top-k candidate indices from differentiable search. + pub top_candidates: Vec, +} + +/// 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, + /// Overall composite descriptor. + pub composite_description: String, + /// Confidence scores per partition. + pub partition_confidence: Vec, +} + +/// 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, + /// 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, + /// Arbitrary metadata. + pub metadata: HashMap, + /// 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, + /// Stages where convergence was strongest. + pub convergent_stages: Vec, + /// Merged embedding representing the consensus signal. + pub consensus_embedding: Option>, +} + +#[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); + } +}