Files
ruvnet--RuView/v2/crates/wifi-densepose-engine/src/mesh_guard.rs
T
rUv 29de574e63 Beyond-SOTA engine/signal/train improvements: mesh partition guard, FFT CIR solver, canonical frame decoder, falsifiable occupancy benchmark, governed streaming, adapter provenance (#1018)
* docs(research): add RuView beyond-SOTA system review (00)

First document of the beyond-SOTA research series: capability audit of
the current RuView engine with role-to-crate maturity matrix, ruvsense
module inventory, gap analysis, and risk register.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* docs(research): add beyond-SOTA architecture design (02, in progress)

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* docs(research): finalize beyond-SOTA architecture (02)

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* docs(research): add benchmark/validation methodology snapshot (03)

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* docs(research): add beyond-SOTA series index with validation results; changelog

README index ties the 5 research docs together with the session's
measured validation evidence: 2,797 workspace tests / 0 failed, Python
proof PASS (bit-exact), and paired pre/post criterion CIR benchmarks.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* perf(signal): precompute CIR warm-start system; hoist tomography solver allocs

Exact, determinism-safe optimizations (bit-identical float results):

- cir.rs: diag(PhiH Phi)+lambda*I and its CSR matrix depend only on Phi
  and lambda (fixed at CirEstimator::new) but were rebuilt every frame
  (O(K*G) pass + CSR allocation). Now built once in new() via
  build_warm_start_system; summation order unchanged.
- tomography.rs: ISTA gradient buffer hoisted out of the 100-iteration
  loop (fill(0.0) reset) and the Frobenius Lipschitz bound moved from
  per-reconstruct to construction.

Verified: signal 456 tests green; engine 11/11 green including
cycle_is_deterministic and witness-stability tests. Criterion paired
pre/post: cir_estimate/he40 -3.9% (p<0.01), multiband -1.2/-1.4%.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* fix(worldgraph): bound SemanticState growth with deterministic retention

StreamingEngine::process_cycle appended one SemanticState belief per cycle
with no eviction — ~1.7M nodes/day at 20 Hz (beyond-SOTA roadmap finding #6).

Add WorldGraph::prune_semantic_states(max): deterministic eviction of the
oldest beliefs by (valid_from_unix_ms, id); structural nodes (rooms, zones,
sensors, anchors, tracks, events) are never eligible. Wire it into the
engine after each belief append (DEFAULT_SEMANTIC_RETENTION = 7,200, ~6 min
at 20 Hz; set_semantic_retention to tune). The WorldGraph holds current
beliefs; durable history is the recorder's job, so no audit data is lost.

3 new tests: end-to-end bounded growth, oldest-only eviction, deterministic
equal-timestamp tie-break. Workspace gate: 2,865 passed, 0 failed.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* feat(sensing-server): route live frames through the governed StreamingEngine

Closes the live-trust-path gap (ADR-136 section 8, beyond-SOTA system review):
the running server fused live CSI with the bare MultistaticFuser, while the
privacy/provenance/witness control plane (ADR-135..146) only ever ran on
synthetic in-test frames. The privacy control plane was therefore bypassable
on the real path.

New engine_bridge module drives StreamingEngine::process_cycle from the
server's live NodeState map, reusing the existing NodeState -> MultiBandCsiFrame
conversion. It lazily wires each contributing node as a WorldGraph sensor
(idempotent), bounds belief growth via the retention cap, and forwards explicit
timestamps/calibration ids so the path stays deterministic and replayable.

Wired additively into both live ESP32/WiFi fusion sites in main.rs via a
split-borrow off the write guard, so person-count behavior is unchanged; the
latest BLAKE3 witness is stored on AppState. Every published belief now carries
evidence + model + calibration + privacy decision and a deterministic witness.

Adds wifi-densepose-engine/-worldgraph/-bfld/-geo deps. 6 new bridge tests
(witnessed belief with full provenance, cross-run determinism, idempotent node
registration, retention bound, privacy-mode propagation). sensing-server suite
430+128 green; workspace gate 2,904 passed / 0 failed.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* feat(train): falsifiable occupancy benchmark with anti-overfitting gate

Makes the presence/person-count "beyond SOTA" claim falsifiable in code
instead of aspirational (the unfalsifiability gap from the beyond-SOTA system
review). occupancy_bench grades predictions vs ground truth and gates a SOTA
claim behind one claim_allowed invariant requiring ALL of:

- DataProvenance::Measured — synthetic/mock data is scorable for regression
  but never claimable (anti-mock-contamination; the CLAUDE.md Kconfig-bug
  lesson made structural).
- A leak-free EvalSplit — validate() refuses any split where a subject OR
  environment id appears in both train and test (subject leakage /
  per-environment overfitting).
- n_test >= min_test_samples (small-N guard).
- Presence F1 whose bootstrap-CI lower bound (deterministic seeded splitmix64)
  clears the threshold — not the point estimate.
- Count MAE within threshold.

The claim string is unreadable except through the gate (NO_CLAIM otherwise),
same discipline as the ruview-gamma acceptance gate. What remains is data, not
method: a frozen, SHA-pinned, subject/environment-disjoint measured replay set
turns the claim into a passing/failing test.

Lives in wifi-densepose-train (the eval bounded context, alongside ablation/
eval/metrics). 10 tests cover each refusal path; warning-clean under the
crate's missing_docs lint. Workspace gate 2,914 passed / 0 failed. Doc 03
updated.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* feat(engine): per-room adapter provenance + drift-to-recalibration advisor

Closes the trust-chain gap where an ~11 KB per-room LoRA adapter (ADR-150
section 3.4) could silently change inference without the witness noticing:
provenance carried only "rfenc-v<N>" with no notion of adapter identity.

- StreamingEngine::set_room_adapter(AdapterInfo): pins the adapter's
  content-derived id into provenance model_version
  ("rfenc-v1+adapter:<id>") — and therefore into the BLAKE3 witness — so
  swapping or clearing adapter weights always shifts the witness. Engine test
  proves base -> adapter -> other-adapter -> cleared all witness differently
  and cleared == base.
- RecalibrationAdvisor: recommends re-running the ADR-135 empty-room baseline
  / refitting the room adapter on sustained low fusion coherence (streak
  threshold, default 60 cycles ~ 3 s at 20 Hz) or an ADR-142 change-point.
  Surfaced as TrustedOutput::recalibration_recommended, stored on the
  sensing-server AppState alongside the witness at both live fusion sites.
- Bridge plumbing: EngineBridge::{set_room_adapter, clear_room_adapter} +
  live-path test that the adapter id flows into the live witness.

Scope note (honest): this is the deployable provenance/trigger half of the
"retrained model" roadmap item. Fitting the adapter itself runs in the
existing external calibration service (aether-arena/calibration/); a trained
RF-encoder checkpoint still does not exist in-tree.

Engine 15 tests, bridge 7 tests. Workspace gate: 2,918 passed / 0 failed.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* fix(mat): gate api module behind its feature — standalone no-default-features builds

pub mod api was unconditional while its only dependency, serde, is optional
behind the 'api' feature, so any build without default features failed with
101 unresolved-serde errors (masked in --workspace runs by feature
unification). The api module and its create_router/AppState re-export are now
cfg(feature = "api")-gated with docsrs annotations.

All combos compile: bare --no-default-features (was 101 errors, now 0),
--no-default-features --features api, and full default (177 tests pass).
Workspace gate: 2,918 passed / 0 failed.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* perf(signal): opt-in FFT operator for the CIR ISTA solver (8-14x measured)

Phi is a sub-DFT, so each ISTA mat-vec can run as one length-G FFT
(O(G log G)) instead of a dense O(K*G) product — the dominant-latency-hazard
finding from the beyond-SOTA optimization roadmap.

New CirConfig::fft_operator, default FALSE: the dense path stays the
bit-exact witness default. The FFT evaluates the same sums in a different
order, so enabling it shifts float results in the last bits and requires
regenerating any pinned witness — strictly opt-in per deployment.

FftOperator (rustfft, planned once at CirEstimator::new, scratch buffers
reused across the ISTA loop) dispatches inside ista_solve:
  Phi x   = scale * forward-FFT(x) sampled at bins (k_idx mod G)
  Phi^H v = scale * unnormalised inverse-FFT of v scattered into those bins
Warm-start and Lipschitz estimation stay dense at construction.

Measured (criterion, same run, same machine):
  ht20: 2.22 ms -> 265 us  (8.4x)
  ht40: 10.26 ms -> 717 us (14.3x)
The real HE40 grid (K=484, G=1452) scales further per the O(K*G)/O(G log G)
ratio.

3 new tests: FFT<->dense matvec equivalence to float tolerance on ht20 and
he40 grids; end-to-end dominant-tap agreement on a single-path frame; all
default configs keep FFT off. New cir_estimate_fft bench group.

Workspace gate: 2,921 passed / 0 failed (default path bit-exact, witnesses
unchanged).

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* feat(core): canonical frame decoder — capture-to-claim replay (ADR-136)

The encode half of the ADR-136 frame contract existed (ComplexSample,
to_canonical_bytes, witness_hash) but there was no decoder: a captured
canonical frame could be witnessed but never reconstructed, blocking
replay-from-capture.

CsiFrame::from_canonical_bytes is the exact inverse: same id, metadata,
complex payload, and witness hash (tested as the round-trip law AC7 — the
replayed frame re-encodes byte-identically). Amplitude/phase are recomputed
from the payload (projections, not independent state). Every malformed-input
class fails closed (AC8): header truncation -> Truncated, payload truncation
-> PayloadMismatch, unknown discriminants, non-UTF-8 device id, trailing
bytes. Nil calibration uuid decodes as None per the documented encoding.

Core: 36 tests pass. Workspace gate: 2,937 passed / 0 failed.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* feat(engine): dynamic min-cut mesh partition guard (ruvector-mincut)

Maintains an exact min-cut over the live mesh coupling graph — nodes are
sensing nodes, coupling is the product of fusion attention weights — and
surfaces per cycle, as TrustedOutput::mesh:

- cut value: the global "how close is the array to partitioning" number,
  a structural measure per-node heuristics miss;
- weak side: which specific nodes would split off (failure/jamming triage,
  feeds ADR-032 posture);
- at-risk flag: counts as a structural event for the drift->recalibration
  advisor (alongside ADR-142 change-points).

Degenerate cases fail toward risk: a node with zero coupling is reported as
already partitioned (cut 0, that node as the weak side).

Measured cost policy (criterion, 12-node mesh — the honest part):
- weights quantized (1/64) + change-gated: steady-state cycles do ZERO graph
  work and reuse the cached cut (~7.3 us, ~23x cheaper than building);
- on any real change a full exact rebuild (~171 us) is used, because ONE
  DynamicMinCut delete+insert measured ~240 us — the subpolynomial machinery
  amortizes on much larger graphs, so rebuild-on-change is the measured
  optimum at mesh scale (one-edge case -28% after switching policy);
- full process_cycle with the guard: ~33 us for 4 nodes vs the 50 ms budget.

9 mesh_guard tests (weak-node detection, steady-state zero updates,
sub-quantum gating, join/drop rebuild, determinism, disconnection) + an
engine-level wiring test (down-weighted node -> weak side -> recalibration).
Engine 24 tests; workspace gate 2,946 passed / 0 failed.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* feat(engine): mesh partition risk demotes privacy + enters the witness (ADR-032)

Completes the mesh-guard integration: its at_risk signal was advisory-only
(fed the recalibration advisor). It now also contributes to the ADR-141
privacy demotion alongside fusion- and array-level contradictions — a mesh
close to partitioning makes the fused belief less trustworthy, so the cycle
emits at a more restricted class (monotonic; information only removed).

Because effective_class feeds the BLAKE3 witness, a fragmenting array now
shifts the witness: partition risk is auditable, not just logged. The mesh
computation moved ahead of the demotion step in process_cycle; mesh_guard_mut
exposes risk-threshold tuning.

Test: a forced-risk 3-node cycle demotes PrivateHome Anonymous->Restricted
and shifts the witness vs a clean baseline. Engine 25 tests; workspace gate
2,947 passed / 0 failed.

https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH

* fix: public-PR review findings — privacy-path honesty, gate holes, mesh-guard cliff

- sensing-server: engine errors logged+counted (no silent swallow), trust
  state exposed via status surface, privacy-demotion claims aligned with
  the actual parallel-audit-path behavior
- occupancy_bench: vacuous-F1 hole closed (degenerate test sets fail with
  their own criterion); CI-lower-bound test made probative
- mesh_guard: quantization scaled to observed coupling range — >=65-node
  balanced meshes no longer permanently at_risk (regression test)
- engine: both wiring tests made probative (same-topology witness compare,
  deterministic risk-crossing fixture)
- mat: axum/tokio optional behind api; real serde feature (api enables it)
- core: canonical decoder strict (non-zero reserved bytes and nil UUID
  rejected — injective on accepted domain, forged-bytes tests)
- CHANGELOG: un-spliced the FFT/adapter bullet mangle

Co-Authored-By: claude-flow <ruv@ruv.net>

* chore: strip private-track references for public PR

Reword the occupancy-benchmark changelog bullet to drop a cross-reference
to the private research track, and restore the WorldGraph retention bullet
header that was glued onto the preceding MAT bullet.

Co-Authored-By: claude-flow <ruv@ruv.net>

* chore: lockfile refresh for cherry-picked feature set

Co-Authored-By: claude-flow <ruv@ruv.net>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-11 16:08:54 -04:00

365 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Mesh partition guard: dynamic min-cut over the live multistatic node graph.
//!
//! The fusion mesh (nodes = sensing nodes, edge weights = fusion coupling
//! derived from per-node attention weights) changes *incrementally* at cycle
//! rate — one node's coupling drifts, a node joins or drops. This module
//! maintains a [`ruvector_mincut::DynamicMinCut`] over that graph and exposes,
//! per cycle:
//!
//! - the **min-cut value** — the cheapest set of couplings whose loss splits
//! the mesh in two: a principled, global "how close is the array to
//! partitioning" number (vs per-node heuristics that miss multi-node
//! structure);
//! - the **weak side** — which specific nodes are about to partition (feeds
//! failure/jamming triage, ADR-032 posture);
//! - an **at-risk flag** consumed by the engine: it counts as a structural
//! event for the drift→recalibration advisor.
//!
//! ## Cost model (the optimization)
//!
//! Weights are quantized (default 1/64; a *nonzero* coupling below one quantum
//! saturates to quantum 1 so a live coupling is never erased — see
//! [`MeshGuard::weight_quantum`]) and updates are **change-gated**: an
//! edge is touched only when its quantized weight actually moves, so the
//! steady-state cycle applies *zero* graph updates and reuses the cached cut —
//! O(active-changes) per cycle, not O(n²) rebuilds. The exact (deterministic)
//! algorithm is used; mesh sizes are ≤ tens of nodes, far inside its budget.
use std::collections::BTreeMap;
use ruvector_mincut::{DynamicMinCut, MinCutBuilder};
/// Per-cycle report from the mesh guard.
#[derive(Debug, Clone, PartialEq)]
pub struct MeshPartitionReport {
/// Current min-cut value over the coupling graph (higher = more robust).
pub cut_value: f64,
/// True when the mesh has ≥ `min_nodes` nodes and the cut value fell to or
/// below the risk threshold — the array is close to splitting.
pub at_risk: bool,
/// The smaller side of the min-cut partition (node ids): the nodes that
/// would be isolated if the weak couplings failed.
pub weak_side: Vec<u8>,
/// Incremental edge updates applied this cycle (0 in steady state).
pub updates_applied: usize,
}
/// Dynamic min-cut guard over the live mesh.
pub struct MeshGuard {
mincut: Option<DynamicMinCut>,
/// Node set the structure was built over (sorted). A change forces rebuild.
nodes: Vec<u8>,
/// Quantized edge weights currently installed, keyed `(u, v)` with `u < v`.
edges: BTreeMap<(u8, u8), i64>,
/// Weight quantum: weights are snapped to multiples of this before
/// comparison/installation, gating out sub-quantum jitter.
///
/// Policy: a **nonzero** coupling below one quantum saturates to quantum 1
/// instead of quantizing to 0 — quantization never erases a live coupling.
/// (Without the floor, a balanced mesh of ≥ 65 nodes — attention weights
/// ~1/n ⇒ couplings ~1/n < 1/64 — had every edge erased and was reported
/// permanently "already partitioned"/at-risk.) Exact zero stays zero: a
/// truly absent coupling *is* a partition. Relative weakness below one
/// quantum is not resolved; lower this quantum if that resolution matters.
pub weight_quantum: f64,
/// Cut value at or below which the mesh counts as at partition risk.
pub risk_threshold: f64,
/// Minimum node count for risk to be meaningful (a 2-node mesh always has
/// a trivial cut; default 3).
pub min_nodes: usize,
}
impl Default for MeshGuard {
fn default() -> Self {
Self {
mincut: None,
nodes: Vec::new(),
edges: BTreeMap::new(),
weight_quantum: 1.0 / 64.0,
risk_threshold: 0.25,
min_nodes: 3,
}
}
}
impl MeshGuard {
/// Quantize a raw weight to the guard's grid (floor; weights are ≥ 0).
/// Nonzero sub-quantum weights saturate to quantum 1 — see the
/// [`Self::weight_quantum`] policy (review finding: sub-quantum couplings
/// must not produce a false "already partitioned").
fn quantize(&self, w: f64) -> i64 {
let w = w.max(0.0);
let q = (w / self.weight_quantum).floor() as i64;
if q == 0 && w > 0.0 {
1
} else {
q
}
}
/// Update the guard with this cycle's mesh: `nodes` are the contributing
/// node ids and `coupling(i, j)` returns the fusion coupling between
/// `nodes[i]` and `nodes[j]` (symmetric, ≥ 0).
///
/// Returns `None` for meshes of fewer than 2 nodes (no cut exists).
pub fn update(
&mut self,
nodes: &[u8],
coupling: impl Fn(usize, usize) -> f64,
) -> Option<MeshPartitionReport> {
if nodes.len() < 2 {
// Mesh degenerated: drop state so a later rebuild starts clean.
self.mincut = None;
self.nodes.clear();
self.edges.clear();
return None;
}
let mut sorted: Vec<u8> = nodes.to_vec();
sorted.sort_unstable();
sorted.dedup();
// Desired quantized edge set for this cycle.
let mut desired: BTreeMap<(u8, u8), i64> = BTreeMap::new();
for i in 0..nodes.len() {
for j in (i + 1)..nodes.len() {
let (a, b) = if nodes[i] < nodes[j] {
(nodes[i], nodes[j])
} else {
(nodes[j], nodes[i])
};
if a == b {
continue;
}
let q = self.quantize(coupling(i, j));
desired.insert((a, b), q);
}
}
// Change detection: count quantized-weight moves vs the installed set.
let changed = if self.mincut.is_none() || self.nodes != sorted {
usize::MAX // node set changed / first cycle: rebuild unconditionally
} else {
desired
.iter()
.filter(|(k, &q)| self.edges.get(k).copied().unwrap_or(0) != q)
.count()
};
let mut updates = 0usize;
if changed > 0 {
// Measured policy (criterion, 12-node mesh): a full exact rebuild
// is ~170 µs while ONE DynamicMinCut delete+insert is ~240 µs —
// the incremental machinery's overheads target much larger graphs.
// At mesh scale the optimum is: change-gate aggressively (the
// steady state below is ~7 µs and covers almost every cycle) and
// rebuild whenever anything actually moved.
let edges: Vec<(u64, u64, f64)> = desired
.iter()
.filter(|(_, &q)| q > 0)
.map(|(&(a, b), &q)| {
(u64::from(a), u64::from(b), q as f64 * self.weight_quantum)
})
.collect();
updates = if changed == usize::MAX { edges.len() } else { changed };
self.mincut = MinCutBuilder::new().exact().with_edges(edges).build().ok();
self.nodes = sorted;
self.edges = desired;
}
// changed == 0: steady state — zero graph work, cached cut reused.
// Nodes with no positive coupling never enter the cut structure (zero
// edges are not installed) — they are already partitioned. Report them
// as the degenerate cut before consulting the structure.
let mut isolated: Vec<u8> = self
.nodes
.iter()
.copied()
.filter(|&v| {
!self
.edges
.iter()
.any(|(&(a, b), &q)| q > 0 && (a == v || b == v))
})
.collect();
if !isolated.is_empty() {
isolated.sort_unstable();
return Some(MeshPartitionReport {
cut_value: 0.0,
at_risk: self.nodes.len() >= self.min_nodes,
weak_side: isolated,
updates_applied: updates,
});
}
let mc = self.mincut.as_ref()?;
// A disconnected coupling graph is the degenerate cut: value 0.
let cut_value = if mc.is_connected() { mc.min_cut_value() } else { 0.0 };
let (side_a, side_b) = mc.partition();
let weak_raw = if side_a.len() <= side_b.len() { side_a } else { side_b };
let mut weak_side: Vec<u8> = weak_raw.into_iter().map(|v| v as u8).collect();
weak_side.sort_unstable();
let at_risk = self.nodes.len() >= self.min_nodes && cut_value <= self.risk_threshold;
Some(MeshPartitionReport { cut_value, at_risk, weak_side, updates_applied: updates })
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Triangle with one weakly-attached node: the cut isolates that node and
/// the cut value equals its total coupling.
#[test]
fn weakly_attached_node_is_the_weak_side() {
let mut g = MeshGuard::default();
let nodes = [0u8, 1, 2];
// 01 strongly coupled; node 2 hangs on by 0.05 + 0.05.
let w = |i: usize, j: usize| match (i.min(j), i.max(j)) {
(0, 1) => 1.0,
_ => 0.05,
};
let r = g.update(&nodes, w).expect("3-node mesh");
assert!(r.cut_value <= 0.13, "cut {} should be ~0.10", r.cut_value);
assert_eq!(r.weak_side, vec![2]);
assert!(r.at_risk, "weak coupling must flag partition risk");
}
#[test]
fn strong_mesh_is_not_at_risk() {
let mut g = MeshGuard::default();
let r = g.update(&[0, 1, 2, 3], |_, _| 0.9).expect("mesh");
assert!(r.cut_value > g.risk_threshold);
assert!(!r.at_risk);
}
#[test]
fn two_node_mesh_reports_but_never_risks() {
let mut g = MeshGuard::default();
let r = g.update(&[0, 1], |_, _| 0.01).expect("2-node mesh");
// Trivial cut exists but min_nodes=3 keeps the flag off.
assert!(!r.at_risk);
}
#[test]
fn fewer_than_two_nodes_yields_none() {
let mut g = MeshGuard::default();
assert!(g.update(&[7], |_, _| 1.0).is_none());
assert!(g.update(&[], |_, _| 1.0).is_none());
}
/// The optimization contract: identical weights on the next cycle apply
/// zero updates; a sub-quantum wiggle also applies zero; a real change
/// applies exactly the changed edges.
#[test]
fn steady_state_applies_zero_updates() {
let mut g = MeshGuard::default();
let nodes = [0u8, 1, 2, 3];
let first = g.update(&nodes, |_, _| 0.5).unwrap();
assert_eq!(first.updates_applied, 6); // cold build installs all edges
let second = g.update(&nodes, |_, _| 0.5).unwrap();
assert_eq!(second.updates_applied, 0);
// Sub-quantum jitter (quantum is 1/64 ≈ 0.0156) is gated out.
let third = g.update(&nodes, |_, _| 0.5 + 0.004).unwrap();
assert_eq!(third.updates_applied, 0);
// One genuinely changed edge touches exactly one edge.
let fourth = g
.update(&nodes, |i, j| if (i.min(j), i.max(j)) == (0, 1) { 0.1 } else { 0.5 })
.unwrap();
assert_eq!(fourth.updates_applied, 1);
}
/// Node set changes force a clean rebuild (drop/join handled correctly).
#[test]
fn node_join_and_drop_rebuild() {
let mut g = MeshGuard::default();
g.update(&[0, 1, 2], |_, _| 0.8).unwrap();
// Node 3 joins.
let joined = g.update(&[0, 1, 2, 3], |_, _| 0.8).unwrap();
assert_eq!(joined.updates_applied, 6); // rebuild over 4 nodes
// Node 0 drops.
let dropped = g.update(&[1, 2, 3], |_, _| 0.8).unwrap();
assert_eq!(dropped.updates_applied, 3);
assert!(!dropped.at_risk);
}
/// Determinism: same inputs, same report (cut value + weak side).
#[test]
fn reports_are_deterministic() {
let run = || {
let mut g = MeshGuard::default();
let w = |i: usize, j: usize| match (i.min(j), i.max(j)) {
(0, 1) => 0.9,
(1, 2) => 0.6,
_ => 0.07,
};
g.update(&[0, 1, 2], w).unwrap()
};
let a = run();
let b = run();
assert_eq!(a.cut_value.to_bits(), b.cut_value.to_bits());
assert_eq!(a.weak_side, b.weak_side);
}
/// Regression (review finding 3): a balanced mesh of ≥ 65 nodes has every
/// pairwise coupling at ~1/n < quantum (1/64). The old floor-to-zero
/// quantization erased all edges and reported the mesh permanently
/// "already partitioned" (cut 0, at_risk). Nonzero sub-quantum couplings
/// now saturate to one quantum, so the mesh reports a healthy cut.
#[test]
fn large_balanced_mesh_is_not_at_risk() {
let mut g = MeshGuard::default();
let nodes: Vec<u8> = (0..70u8).collect();
// Attention-weight product coupling: (1/n)·(1/n)·n = 1/n ≈ 0.0143 < 1/64.
let n = nodes.len() as f64;
let r = g.update(&nodes, |_, _| 1.0 / n).expect("70-node mesh");
assert!(
r.cut_value > 0.0,
"live couplings must not quantize to zero"
);
// Min cut isolates one node: 69 edges × one quantum (1/64) ≈ 1.08,
// well above the 0.25 default risk threshold.
assert!(r.cut_value > g.risk_threshold);
assert!(
!r.at_risk,
"balanced large mesh must not be at partition risk"
);
assert!(r.weak_side.len() < nodes.len(), "no false full partition");
}
/// Sub-quantum couplings saturate to one quantum but exact zero is still a
/// real partition (the floor must not invent couplings).
#[test]
fn sub_quantum_saturates_but_zero_stays_zero() {
let mut g = MeshGuard::default();
// 0.001 < 1/64 everywhere: connected, tiny cut, flagged at risk
// (cut = 2 × 1/64 ≈ 0.031 ≤ 0.25) — but NOT "already partitioned".
let r = g.update(&[0, 1, 2], |_, _| 0.001).expect("mesh");
assert!(r.cut_value > 0.0);
assert!(r.at_risk);
// Exact zero to node 2: degenerate cut 0, node 2 isolated.
let mut g2 = MeshGuard::default();
let r2 = g2
.update(&[0, 1, 2], |i, j| if i == 2 || j == 2 { 0.0 } else { 0.5 })
.expect("mesh");
assert_eq!(r2.cut_value, 0.0);
assert_eq!(r2.weak_side, vec![2]);
}
/// A fully partitioned mesh (zero coupling to one node) reports cut 0.
#[test]
fn disconnected_mesh_is_cut_zero() {
let mut g = MeshGuard::default();
let w = |i: usize, j: usize| {
if i == 2 || j == 2 { 0.0 } else { 0.9 }
};
let r = g.update(&[0, 1, 2], w).unwrap();
assert_eq!(r.cut_value, 0.0);
assert!(r.at_risk);
assert_eq!(r.weak_side, vec![2]);
}
}