Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
586
vendor/ruvector/crates/ruvector-cognitive-container/src/container.rs
vendored
Normal file
586
vendor/ruvector/crates/ruvector-cognitive-container/src/container.rs
vendored
Normal file
@@ -0,0 +1,586 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::epoch::{ContainerEpochBudget, EpochController, Phase};
|
||||
use crate::error::{ContainerError, Result};
|
||||
use crate::memory::{MemoryConfig, MemorySlab};
|
||||
use crate::witness::{
|
||||
CoherenceDecision, ContainerWitnessReceipt, VerificationResult, WitnessChain,
|
||||
};
|
||||
|
||||
/// Top-level container configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContainerConfig {
|
||||
/// Memory layout.
|
||||
pub memory: MemoryConfig,
|
||||
/// Per-epoch tick budgets.
|
||||
pub epoch_budget: ContainerEpochBudget,
|
||||
/// Unique identifier for this container instance.
|
||||
pub instance_id: u64,
|
||||
/// Maximum number of witness receipts retained.
|
||||
pub max_receipts: usize,
|
||||
}
|
||||
|
||||
impl Default for ContainerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
memory: MemoryConfig::default(),
|
||||
epoch_budget: ContainerEpochBudget::default(),
|
||||
instance_id: 0,
|
||||
max_receipts: 1024,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A graph-structure delta to apply during the ingest phase.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Delta {
|
||||
EdgeAdd { u: usize, v: usize, weight: f64 },
|
||||
EdgeRemove { u: usize, v: usize },
|
||||
WeightUpdate { u: usize, v: usize, new_weight: f64 },
|
||||
Observation { node: usize, value: f64 },
|
||||
}
|
||||
|
||||
/// Bitmask tracking which pipeline components completed during a tick.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ComponentMask(pub u8);
|
||||
|
||||
impl ComponentMask {
|
||||
pub const INGEST: Self = Self(0b0000_0001);
|
||||
pub const MINCUT: Self = Self(0b0000_0010);
|
||||
pub const SPECTRAL: Self = Self(0b0000_0100);
|
||||
pub const EVIDENCE: Self = Self(0b0000_1000);
|
||||
pub const WITNESS: Self = Self(0b0001_0000);
|
||||
pub const ALL: Self = Self(0b0001_1111);
|
||||
|
||||
/// Returns `true` if all bits in `other` are set in `self`.
|
||||
pub fn contains(&self, other: Self) -> bool {
|
||||
self.0 & other.0 == other.0
|
||||
}
|
||||
|
||||
/// Set all bits present in `other`.
|
||||
pub fn insert(&mut self, other: Self) {
|
||||
self.0 |= other.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Output of a single `tick()` invocation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TickResult {
|
||||
/// The witness receipt generated for this epoch.
|
||||
pub receipt: ContainerWitnessReceipt,
|
||||
/// True if any pipeline phase was skipped due to budget exhaustion.
|
||||
pub partial: bool,
|
||||
/// Bitmask of completed components.
|
||||
pub components_completed: u8,
|
||||
/// Wall-clock duration in microseconds.
|
||||
pub tick_time_us: u64,
|
||||
}
|
||||
|
||||
/// Internal graph representation.
|
||||
struct GraphState {
|
||||
num_vertices: usize,
|
||||
num_edges: usize,
|
||||
edges: Vec<(usize, usize, f64)>,
|
||||
min_cut_value: f64,
|
||||
canonical_hash: [u8; 32],
|
||||
}
|
||||
|
||||
impl GraphState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
num_vertices: 0,
|
||||
num_edges: 0,
|
||||
edges: Vec::new(),
|
||||
min_cut_value: 0.0,
|
||||
canonical_hash: [0u8; 32],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal spectral analysis state.
|
||||
struct SpectralState {
|
||||
scs: f64,
|
||||
fiedler: f64,
|
||||
gap: f64,
|
||||
}
|
||||
|
||||
impl SpectralState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
scs: 0.0,
|
||||
fiedler: 0.0,
|
||||
gap: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal evidence accumulation state.
|
||||
struct EvidenceState {
|
||||
observations: Vec<f64>,
|
||||
accumulated_evidence: f64,
|
||||
threshold: f64,
|
||||
}
|
||||
|
||||
impl EvidenceState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
observations: Vec::new(),
|
||||
accumulated_evidence: 0.0,
|
||||
threshold: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializable snapshot of the container state.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContainerSnapshot {
|
||||
pub epoch: u64,
|
||||
pub config: ContainerConfig,
|
||||
pub graph_edges: Vec<(usize, usize, f64)>,
|
||||
pub spectral_scs: f64,
|
||||
pub evidence_accumulated: f64,
|
||||
}
|
||||
|
||||
/// A sealed cognitive container that orchestrates ingest, min-cut, spectral,
|
||||
/// evidence, and witness phases within a memory slab and epoch budget.
|
||||
pub struct CognitiveContainer {
|
||||
config: ContainerConfig,
|
||||
#[allow(dead_code)]
|
||||
slab: MemorySlab,
|
||||
epoch: EpochController,
|
||||
witness: WitnessChain,
|
||||
graph: GraphState,
|
||||
spectral: SpectralState,
|
||||
evidence: EvidenceState,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
impl CognitiveContainer {
|
||||
/// Create and initialize a new container.
|
||||
pub fn new(config: ContainerConfig) -> Result<Self> {
|
||||
let slab = MemorySlab::new(config.memory.clone())?;
|
||||
let epoch = EpochController::new(config.epoch_budget.clone());
|
||||
let witness = WitnessChain::new(config.max_receipts);
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
slab,
|
||||
epoch,
|
||||
witness,
|
||||
graph: GraphState::new(),
|
||||
spectral: SpectralState::new(),
|
||||
evidence: EvidenceState::new(),
|
||||
initialized: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute one full epoch: ingest deltas, recompute min-cut, update spectral
|
||||
/// metrics, accumulate evidence, and produce a witness receipt.
|
||||
pub fn tick(&mut self, deltas: &[Delta]) -> Result<TickResult> {
|
||||
if !self.initialized {
|
||||
return Err(ContainerError::NotInitialized);
|
||||
}
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
self.epoch.reset();
|
||||
let mut completed = ComponentMask(0);
|
||||
|
||||
// Phase 1: Ingest
|
||||
if self.epoch.try_budget(Phase::Ingest) {
|
||||
for delta in deltas {
|
||||
self.apply_delta(delta);
|
||||
}
|
||||
self.epoch.consume(deltas.len().max(1) as u64);
|
||||
completed.insert(ComponentMask::INGEST);
|
||||
}
|
||||
|
||||
// Phase 2: Min-cut
|
||||
if self.epoch.try_budget(Phase::MinCut) {
|
||||
self.recompute_mincut();
|
||||
self.epoch.consume(self.graph.num_edges.max(1) as u64);
|
||||
completed.insert(ComponentMask::MINCUT);
|
||||
}
|
||||
|
||||
// Phase 3: Spectral
|
||||
if self.epoch.try_budget(Phase::Spectral) {
|
||||
self.update_spectral();
|
||||
self.epoch.consume(self.graph.num_vertices.max(1) as u64);
|
||||
completed.insert(ComponentMask::SPECTRAL);
|
||||
}
|
||||
|
||||
// Phase 4: Evidence
|
||||
if self.epoch.try_budget(Phase::Evidence) {
|
||||
self.accumulate_evidence();
|
||||
self.epoch
|
||||
.consume(self.evidence.observations.len().max(1) as u64);
|
||||
completed.insert(ComponentMask::EVIDENCE);
|
||||
}
|
||||
|
||||
// Phase 5: Witness
|
||||
let decision = self.make_decision();
|
||||
let input_bytes = self.serialize_deltas(deltas);
|
||||
let mincut_bytes = self.graph.min_cut_value.to_le_bytes();
|
||||
let evidence_bytes = self.evidence.accumulated_evidence.to_le_bytes();
|
||||
|
||||
let receipt = self.witness.generate_receipt(
|
||||
&input_bytes,
|
||||
&mincut_bytes,
|
||||
self.spectral.scs,
|
||||
&evidence_bytes,
|
||||
decision,
|
||||
);
|
||||
completed.insert(ComponentMask::WITNESS);
|
||||
|
||||
Ok(TickResult {
|
||||
receipt,
|
||||
partial: completed.0 != ComponentMask::ALL.0,
|
||||
components_completed: completed.0,
|
||||
tick_time_us: start.elapsed().as_micros() as u64,
|
||||
})
|
||||
}
|
||||
|
||||
/// Reference to the container configuration.
|
||||
pub fn config(&self) -> &ContainerConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Current epoch counter (next epoch to be generated).
|
||||
pub fn current_epoch(&self) -> u64 {
|
||||
self.witness.current_epoch()
|
||||
}
|
||||
|
||||
/// Slice of all retained witness receipts.
|
||||
pub fn receipt_chain(&self) -> &[ContainerWitnessReceipt] {
|
||||
self.witness.receipt_chain()
|
||||
}
|
||||
|
||||
/// Verify the integrity of the internal witness chain.
|
||||
pub fn verify_chain(&self) -> VerificationResult {
|
||||
WitnessChain::verify_chain(self.witness.receipt_chain())
|
||||
}
|
||||
|
||||
/// Produce a serializable snapshot of the current container state.
|
||||
pub fn snapshot(&self) -> ContainerSnapshot {
|
||||
ContainerSnapshot {
|
||||
epoch: self.witness.current_epoch(),
|
||||
config: self.config.clone(),
|
||||
graph_edges: self.graph.edges.clone(),
|
||||
spectral_scs: self.spectral.scs,
|
||||
evidence_accumulated: self.evidence.accumulated_evidence,
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Private helpers ----
|
||||
|
||||
fn apply_delta(&mut self, delta: &Delta) {
|
||||
match delta {
|
||||
Delta::EdgeAdd { u, v, weight } => {
|
||||
self.graph.edges.push((*u, *v, *weight));
|
||||
self.graph.num_edges += 1;
|
||||
let max_node = (*u).max(*v) + 1;
|
||||
if max_node > self.graph.num_vertices {
|
||||
self.graph.num_vertices = max_node;
|
||||
}
|
||||
}
|
||||
Delta::EdgeRemove { u, v } => {
|
||||
self.graph.edges.retain(|(a, b, _)| !(*a == *u && *b == *v));
|
||||
self.graph.num_edges = self.graph.edges.len();
|
||||
}
|
||||
Delta::WeightUpdate { u, v, new_weight } => {
|
||||
for edge in &mut self.graph.edges {
|
||||
if edge.0 == *u && edge.1 == *v {
|
||||
edge.2 = *new_weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
Delta::Observation { value, .. } => {
|
||||
self.evidence.observations.push(*value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simplified Stoer-Wagner-style min-cut: find the minimum total weight
|
||||
/// among all vertex partitions. For small graphs this uses the minimum
|
||||
/// weighted vertex degree as a fast approximation.
|
||||
fn recompute_mincut(&mut self) {
|
||||
if self.graph.edges.is_empty() {
|
||||
self.graph.min_cut_value = 0.0;
|
||||
self.graph.canonical_hash = [0u8; 32];
|
||||
return;
|
||||
}
|
||||
|
||||
// Approximate min-cut via minimum weighted degree.
|
||||
let n = self.graph.num_vertices;
|
||||
let mut degree = vec![0.0f64; n];
|
||||
for &(u, v, w) in &self.graph.edges {
|
||||
if u < n {
|
||||
degree[u] += w;
|
||||
}
|
||||
if v < n {
|
||||
degree[v] += w;
|
||||
}
|
||||
}
|
||||
|
||||
self.graph.min_cut_value = degree
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&d| d > 0.0)
|
||||
.fold(f64::MAX, f64::min);
|
||||
if self.graph.min_cut_value == f64::MAX {
|
||||
self.graph.min_cut_value = 0.0;
|
||||
}
|
||||
|
||||
// Canonical hash: hash sorted edges.
|
||||
let mut sorted = self.graph.edges.clone();
|
||||
sorted.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
|
||||
let bytes: Vec<u8> = sorted
|
||||
.iter()
|
||||
.flat_map(|(u, v, w)| {
|
||||
let mut b = Vec::with_capacity(24);
|
||||
b.extend_from_slice(&u.to_le_bytes());
|
||||
b.extend_from_slice(&v.to_le_bytes());
|
||||
b.extend_from_slice(&w.to_le_bytes());
|
||||
b
|
||||
})
|
||||
.collect();
|
||||
self.graph.canonical_hash = crate::witness::deterministic_hash_public(&bytes);
|
||||
}
|
||||
|
||||
/// Simplified spectral metrics: SCS is the ratio of min-cut to total weight.
|
||||
fn update_spectral(&mut self) {
|
||||
let total_weight: f64 = self.graph.edges.iter().map(|e| e.2).sum();
|
||||
if total_weight > 0.0 {
|
||||
self.spectral.scs = self.graph.min_cut_value / total_weight;
|
||||
self.spectral.fiedler = self.spectral.scs;
|
||||
self.spectral.gap = 1.0 - self.spectral.scs;
|
||||
} else {
|
||||
self.spectral.scs = 0.0;
|
||||
self.spectral.fiedler = 0.0;
|
||||
self.spectral.gap = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple sequential probability ratio test (SPRT) style accumulation.
|
||||
fn accumulate_evidence(&mut self) {
|
||||
if self.evidence.observations.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mean: f64 = self.evidence.observations.iter().sum::<f64>()
|
||||
/ self.evidence.observations.len() as f64;
|
||||
self.evidence.accumulated_evidence += mean.abs();
|
||||
}
|
||||
|
||||
/// Decision logic based on spectral coherence and accumulated evidence.
|
||||
fn make_decision(&self) -> CoherenceDecision {
|
||||
if self.graph.edges.is_empty() {
|
||||
return CoherenceDecision::Inconclusive;
|
||||
}
|
||||
if self.spectral.scs >= 0.5 && self.evidence.accumulated_evidence < self.evidence.threshold
|
||||
{
|
||||
return CoherenceDecision::Pass;
|
||||
}
|
||||
if self.spectral.scs < 0.2 {
|
||||
let severity = ((1.0 - self.spectral.scs) * 10.0).min(255.0) as u8;
|
||||
return CoherenceDecision::Fail { severity };
|
||||
}
|
||||
CoherenceDecision::Inconclusive
|
||||
}
|
||||
|
||||
fn serialize_deltas(&self, deltas: &[Delta]) -> Vec<u8> {
|
||||
serde_json::to_vec(deltas).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn default_container() -> CognitiveContainer {
|
||||
CognitiveContainer::new(ContainerConfig::default()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_container_lifecycle() {
|
||||
let mut container = default_container();
|
||||
assert_eq!(container.current_epoch(), 0);
|
||||
|
||||
let result = container.tick(&[]).unwrap();
|
||||
assert_eq!(result.receipt.epoch, 0);
|
||||
assert_eq!(container.current_epoch(), 1);
|
||||
|
||||
match container.verify_chain() {
|
||||
VerificationResult::Valid { chain_length, .. } => {
|
||||
assert_eq!(chain_length, 1);
|
||||
}
|
||||
other => panic!("Expected Valid, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_container_tick_with_deltas() {
|
||||
let mut container = default_container();
|
||||
|
||||
let deltas = vec![
|
||||
Delta::EdgeAdd {
|
||||
u: 0,
|
||||
v: 1,
|
||||
weight: 1.0,
|
||||
},
|
||||
Delta::EdgeAdd {
|
||||
u: 1,
|
||||
v: 2,
|
||||
weight: 2.0,
|
||||
},
|
||||
Delta::EdgeAdd {
|
||||
u: 2,
|
||||
v: 0,
|
||||
weight: 1.5,
|
||||
},
|
||||
Delta::Observation {
|
||||
node: 0,
|
||||
value: 0.8,
|
||||
},
|
||||
];
|
||||
|
||||
let result = container.tick(&deltas).unwrap();
|
||||
assert!(!result.partial);
|
||||
assert_eq!(result.components_completed, ComponentMask::ALL.0);
|
||||
|
||||
// Graph should reflect the edges.
|
||||
let snap = container.snapshot();
|
||||
assert_eq!(snap.graph_edges.len(), 3);
|
||||
assert!(snap.spectral_scs > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_container_snapshot_restore() {
|
||||
let mut container = default_container();
|
||||
container
|
||||
.tick(&[Delta::EdgeAdd {
|
||||
u: 0,
|
||||
v: 1,
|
||||
weight: 3.0,
|
||||
}])
|
||||
.unwrap();
|
||||
|
||||
let snap = container.snapshot();
|
||||
let json = serde_json::to_string(&snap).expect("serialize snapshot");
|
||||
let restored: ContainerSnapshot =
|
||||
serde_json::from_str(&json).expect("deserialize snapshot");
|
||||
|
||||
assert_eq!(restored.epoch, snap.epoch);
|
||||
assert_eq!(restored.graph_edges.len(), snap.graph_edges.len());
|
||||
assert!((restored.spectral_scs - snap.spectral_scs).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_container_decision_logic() {
|
||||
let mut container = default_container();
|
||||
|
||||
// Empty graph => Inconclusive
|
||||
let r = container.tick(&[]).unwrap();
|
||||
assert_eq!(r.receipt.decision, CoherenceDecision::Inconclusive);
|
||||
|
||||
// Single edge: min-cut/total = 1.0 (high scs), no evidence => Pass
|
||||
let r = container
|
||||
.tick(&[Delta::EdgeAdd {
|
||||
u: 0,
|
||||
v: 1,
|
||||
weight: 5.0,
|
||||
}])
|
||||
.unwrap();
|
||||
assert_eq!(r.receipt.decision, CoherenceDecision::Pass);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_container_multiple_epochs() {
|
||||
let mut container = default_container();
|
||||
for i in 0..10 {
|
||||
container
|
||||
.tick(&[Delta::EdgeAdd {
|
||||
u: i,
|
||||
v: i + 1,
|
||||
weight: 1.0,
|
||||
}])
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(container.current_epoch(), 10);
|
||||
|
||||
match container.verify_chain() {
|
||||
VerificationResult::Valid {
|
||||
chain_length,
|
||||
first_epoch,
|
||||
last_epoch,
|
||||
} => {
|
||||
assert_eq!(chain_length, 10);
|
||||
assert_eq!(first_epoch, 0);
|
||||
assert_eq!(last_epoch, 9);
|
||||
}
|
||||
other => panic!("Expected Valid, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_container_edge_remove() {
|
||||
let mut container = default_container();
|
||||
container
|
||||
.tick(&[
|
||||
Delta::EdgeAdd {
|
||||
u: 0,
|
||||
v: 1,
|
||||
weight: 1.0,
|
||||
},
|
||||
Delta::EdgeAdd {
|
||||
u: 1,
|
||||
v: 2,
|
||||
weight: 2.0,
|
||||
},
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
container.tick(&[Delta::EdgeRemove { u: 0, v: 1 }]).unwrap();
|
||||
|
||||
let snap = container.snapshot();
|
||||
assert_eq!(snap.graph_edges.len(), 1);
|
||||
assert_eq!(snap.graph_edges[0], (1, 2, 2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_container_weight_update() {
|
||||
let mut container = default_container();
|
||||
container
|
||||
.tick(&[Delta::EdgeAdd {
|
||||
u: 0,
|
||||
v: 1,
|
||||
weight: 1.0,
|
||||
}])
|
||||
.unwrap();
|
||||
|
||||
container
|
||||
.tick(&[Delta::WeightUpdate {
|
||||
u: 0,
|
||||
v: 1,
|
||||
new_weight: 5.0,
|
||||
}])
|
||||
.unwrap();
|
||||
|
||||
let snap = container.snapshot();
|
||||
assert_eq!(snap.graph_edges[0].2, 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_component_mask() {
|
||||
let mut mask = ComponentMask(0);
|
||||
assert!(!mask.contains(ComponentMask::INGEST));
|
||||
|
||||
mask.insert(ComponentMask::INGEST);
|
||||
assert!(mask.contains(ComponentMask::INGEST));
|
||||
assert!(!mask.contains(ComponentMask::MINCUT));
|
||||
|
||||
mask.insert(ComponentMask::MINCUT);
|
||||
assert!(mask.contains(ComponentMask::INGEST));
|
||||
assert!(mask.contains(ComponentMask::MINCUT));
|
||||
|
||||
assert!(!mask.contains(ComponentMask::ALL));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user