Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ members = [
"crates/ruvllm-cli",
"crates/ruvllm-wasm",
"crates/prime-radiant",
"crates/ruvector-cohomology",
"crates/ruvector-delta-core",
"crates/ruvector-delta-wasm",
"crates/ruvector-delta-index",
Expand Down
16 changes: 14 additions & 2 deletions crates/prime-radiant/src/cohomology/obstruction.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
//! Obstruction Detection
//!
//! Obstructions are cohomological objects that indicate global inconsistency.
//! A non-trivial obstruction means that local data cannot be patched together
//! into a global section.
//!
//! Precisely (ADR-272): for a *linear* sheaf the zero section is always a
//! global section, so a nontrivial `H^1` does **not** mean "no global
//! section exists". The correct statements on a graph are:
//!
//! - `H^0(G, F) = ker delta` is the space of global sections;
//! - `H^1(G, F) = C^1 / im delta` measures **edge data** that cannot be
//! written as the coboundary of any vertex assignment.
//!
//! A non-trivial obstruction therefore means the *observed edge data*
//! (affine constraints) cannot be explained by any global vertex assignment
//! — an irreducible contradiction in the observations, not the absence of
//! global sections. The affine syndrome engine built on this correction
//! lives in the `ruvector-cohomology` crate.

use super::cocycle::{Cocycle, SheafCoboundary};
use super::laplacian::{HarmonicRepresentative, LaplacianConfig, SheafLaplacian};
Expand Down
53 changes: 53 additions & 0 deletions crates/ruvector-cohomology/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[package]
name = "ruvector-cohomology"
version = "0.1.0"
edition = "2021"
rust-version = "1.77"
license = "MIT OR Apache-2.0"
authors = ["RuVector Team <team@ruvector.dev>"]
description = "Dynamic cohomological error correction: affine sheaf syndrome, sparse semantic repair, and mincut containment over dynamic cellular sheaves"
repository = "https://github.com/ruvnet/ruvector"
homepage = "https://ruv.io/ruvector"
documentation = "https://docs.rs/ruvector-cohomology"
keywords = ["sheaf", "cohomology", "error-correction", "graph", "consistency"]
categories = ["algorithms", "science", "mathematics"]

[lib]
crate-type = ["rlib"]

[dependencies]
serde = { workspace = true }
thiserror = { workspace = true }
sha2 = "0.10"

# Optional coupling to the dynamic mincut engine for global quarantine
# boundaries. The crate always ships a deterministic internal s-t max-flow
# for seeded containment; this feature adds the subpolynomial global cut.
ruvector-mincut = { version = "2.0", path = "../ruvector-mincut", default-features = false, features = ["exact"], optional = true }

[features]
default = []
mincut = ["dep:ruvector-mincut"]

[dev-dependencies]
serde_json = { workspace = true }

[[bench]]
name = "batch"
harness = false

[[bench]]
name = "dynamic_edits"
harness = false

[[bench]]
name = "planted_cycles"
harness = false

[[bench]]
name = "repair"
harness = false

[[bench]]
name = "mincut_coupling"
harness = false
19 changes: 19 additions & 0 deletions crates/ruvector-cohomology/benches/batch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//! Batch syndrome benchmark: assembly + full solve at increasing scale.

mod common;

use common::{build_ring_engine, time_ms};
use ruvector_cohomology::dynamic::DynamicCohomology;

fn main() {
for &(n, dim, chords) in &[(100u64, 4usize, 50u64), (1_000, 4, 500), (5_000, 8, 2_500)] {
let mut engine = build_ring_engine(n, dim, chords, 42);
let (result, ms) = time_ms(|| engine.flush().expect("flush"));
println!(
"batch: n={n} dim={dim} edges={} energy={:.3e} flush={:.2}ms",
n + chords,
result.energy,
ms
);
}
}
78 changes: 78 additions & 0 deletions crates/ruvector-cohomology/benches/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Shared bench helpers: deterministic graph generators and timing.
#![allow(dead_code)]

use ruvector_cohomology::dynamic::{CohomologyEdit, DynamicCohomology, DynamicSheafEngine, EngineConfig};
use ruvector_cohomology::operator::RestrictionMap;
use std::time::Instant;

/// Deterministic pseudo-random stream for reproducible benchmarks.
pub struct Rng(pub u64);

impl Rng {
pub fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E3779B97F4A7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
}

pub fn unit(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
}

/// Build a ring of `n` vertices with stalk dimension `dim`, identity maps,
/// coherent observations, plus `extra_chords` random chords.
pub fn build_ring_engine(n: u64, dim: usize, extra_chords: u64, seed: u64) -> DynamicSheafEngine {
let mut engine = DynamicSheafEngine::new(EngineConfig::default());
let mut rng = Rng(seed);
for i in 0..n {
assert!(engine
.apply_edit(CohomologyEdit::InsertVertex { id: i, dim })
.accepted);
}
let add_edge = |engine: &mut DynamicSheafEngine, a: u64, b: u64| {
let receipt = engine.apply_edit(CohomologyEdit::InsertEdge {
a,
b,
rho_a: RestrictionMap::Identity { dim },
rho_b: RestrictionMap::Identity { dim },
weight: 1.0,
observation: vec![0.0; dim],
});
receipt.accepted
};
for i in 0..n {
add_edge(&mut engine, i, (i + 1) % n);
}
let mut added = 0;
while added < extra_chords {
let a = rng.next_u64() % n;
let b = rng.next_u64() % n;
if a != b && add_edge(&mut engine, a, b) {
added += 1;
}
}
engine
}

/// Time a closure, returning (result, elapsed ms).
pub fn time_ms<T>(f: impl FnOnce() -> T) -> (T, f64) {
let start = Instant::now();
let out = f();
(out, start.elapsed().as_secs_f64() * 1e3)
}

/// Report a labeled percentile summary of per-op latencies (ms).
pub fn report(label: &str, mut samples: Vec<f64>) {
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let pct = |p: f64| samples[((samples.len() - 1) as f64 * p) as usize];
println!(
"{label}: n={} median={:.4}ms p99={:.4}ms max={:.4}ms",
samples.len(),
pct(0.5),
pct(0.99),
samples[samples.len() - 1]
);
}
34 changes: 34 additions & 0 deletions crates/ruvector-cohomology/benches/dynamic_edits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! Dynamic edit latency benchmark: median/p99 local edit and estimate cost.

mod common;

use common::{build_ring_engine, report, time_ms, Rng};
use ruvector_cohomology::dynamic::{CohomologyEdit, DynamicCohomology};

fn main() {
let n = 10_000u64;
let dim = 8usize;
let mut engine = build_ring_engine(n, dim, 5_000, 7);
engine.flush().expect("initial flush");

let mut rng = Rng(99);
let mut edit_samples = Vec::new();
let mut estimate_samples = Vec::new();
for _ in 0..10_000 {
let a = rng.next_u64() % n;
let b = (a + 1) % n;
let value: Vec<f64> = (0..dim).map(|_| rng.unit() * 0.01).collect();
let (receipt, ms) = time_ms(|| {
engine.apply_edit(CohomologyEdit::UpdateObservation { a, b, value: value.clone() })
});
assert!(receipt.accepted);
edit_samples.push(ms);
let (_, ems) = time_ms(|| engine.estimate());
estimate_samples.push(ems);
}
report("local edit (UpdateObservation)", edit_samples);
report("estimate()", estimate_samples);

let (result, ms) = time_ms(|| engine.flush().expect("flush"));
println!("exact flush after 10k edits: {:.2}ms energy={:.3e}", ms, result.energy);
}
42 changes: 42 additions & 0 deletions crates/ruvector-cohomology/benches/mincut_coupling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Combined syndrome → tension → quarantine → repair benchmark.

mod common;

use common::{build_ring_engine, time_ms};
use ruvector_cohomology::dynamic::{CohomologyEdit, DynamicCohomology};
use ruvector_cohomology::mincut_bridge::{run_containment, ContainmentPolicy, TensionWeights};
use ruvector_cohomology::repair::RepairPolicy;

fn main() {
for policy in [ContainmentPolicy::IsolateFirst, ContainmentPolicy::RepairFirst] {
let mut engine = build_ring_engine(200, 4, 100, 3);
assert!(engine
.apply_edit(CohomologyEdit::UpdateObservation {
a: 10,
b: 11,
value: vec![3.0; 4],
})
.accepted);
let (receipt, ms) = time_ms(|| {
run_containment(
&mut engine,
&TensionWeights::default(),
&RepairPolicy { lambda: 0.05, ..RepairPolicy::default() },
policy,
0.5,
1e-9,
)
.expect("containment")
});
println!(
"containment {:?}: pre={:.3e} post={:.3e} cut_edges={} isolated={} verified={} {:.2}ms",
policy,
receipt.pre_energy,
receipt.post_energy,
receipt.boundary.cut_edges.len(),
receipt.boundary.isolated_vertices.len(),
receipt.verified,
ms
);
}
}
50 changes: 50 additions & 0 deletions crates/ruvector-cohomology/benches/planted_cycles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Planted-contradiction localization benchmark: precision/recall of the
//! ranked syndrome support against known planted inconsistent edges.

mod common;

use common::{build_ring_engine, Rng};
use ruvector_cohomology::dynamic::{CohomologyEdit, DynamicCohomology};
use ruvector_cohomology::EdgeKey;

fn main() {
let n = 500u64;
let dim = 4usize;
let planted = 5usize;
// Localization precision depends on cycle redundancy: on a bare ring the
// syndrome is a constant harmonic 1-form (no localization is possible);
// with ~2 chords per vertex the residual concentrates on planted edges.
let mut engine = build_ring_engine(n, dim, 1000, 11);
let mut rng = Rng(23);

// Plant contradictions on ring edges (guaranteed to lie on cycles).
let mut planted_edges = Vec::new();
while planted_edges.len() < planted {
let a = rng.next_u64() % n;
let b = (a + 1) % n;
let key = EdgeKey::new(a, b).unwrap();
if planted_edges.contains(&key) {
continue;
}
let value: Vec<f64> = (0..dim).map(|_| 1.0 + rng.unit()).collect();
assert!(engine
.apply_edit(CohomologyEdit::UpdateObservation { a, b, value })
.accepted);
planted_edges.push(key);
}

let result = engine.flush().expect("flush");
let predicted: Vec<EdgeKey> = result
.ranked_support
.iter()
.take(planted)
.map(|c| c.edge)
.collect();
let hits = predicted.iter().filter(|e| planted_edges.contains(e)).count();
let precision = hits as f64 / predicted.len() as f64;
let recall = hits as f64 / planted_edges.len() as f64;
println!(
"planted_cycles: n={n} planted={planted} energy={:.3e} precision={precision:.2} recall={recall:.2}",
result.energy
);
}
31 changes: 31 additions & 0 deletions crates/ruvector-cohomology/benches/repair.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! Group-sparse repair benchmark: latency and post-repair energy.

mod common;

use common::{build_ring_engine, time_ms};
use ruvector_cohomology::dynamic::{CohomologyEdit, DynamicCohomology};
use ruvector_cohomology::repair::RepairPolicy;

fn main() {
for &(n, dim) in &[(50u64, 4usize), (200, 4), (500, 8)] {
let mut engine = build_ring_engine(n, dim, n / 2, 5);
// One planted contradiction.
assert!(engine
.apply_edit(CohomologyEdit::UpdateObservation {
a: 0,
b: 1,
value: vec![2.0; dim],
})
.accepted);
let policy = RepairPolicy { lambda: 0.05, ..RepairPolicy::default() };
let (plan, ms) = time_ms(|| engine.propose_repair(policy).expect("repair"));
println!(
"repair: n={n} dim={dim} support={} pre={:.3e} post={:.3e} obj={:.3e} {:.2}ms",
plan.corrections.len(),
plan.pre_energy,
plan.post_energy,
plan.objective,
ms
);
}
}
Loading
Loading