Files
wifi-densepose/vendor/ruvector/crates/cognitum-gate-kernel/src/canonical_witness.rs

914 lines
31 KiB
Rust

//! Canonical witness fragments using pseudo-deterministic min-cut.
//!
//! Produces reproducible, hash-stable witness fragments by computing
//! a canonical min-cut partition via lexicographic tie-breaking.
//!
//! All structures are `#[repr(C)]` aligned, use fixed-size arrays, and
//! operate entirely on the stack (no heap allocation). This module is
//! designed for no_std WASM tiles with a ~2.1KB temporary memory footprint.
#![allow(missing_docs)]
use crate::shard::{CompactGraph, MAX_SHARD_VERTICES};
use core::mem::size_of;
// ============================================================================
// Fixed-point weight for deterministic comparison
// ============================================================================
/// Fixed-point weight for deterministic, total-order comparison.
///
/// Uses 16.16 fixed-point representation (upper 16 bits integer, lower 16
/// bits fractional). This avoids floating-point non-determinism in
/// partition comparisons.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[repr(transparent)]
pub struct FixedPointWeight(pub u32);
impl FixedPointWeight {
/// Zero weight constant
pub const ZERO: Self = Self(0);
/// One (1.0) in 16.16 fixed-point
pub const ONE: Self = Self(65536);
/// Maximum representable weight
pub const MAX: Self = Self(u32::MAX);
/// Convert from a `ShardEdge` weight (u16, 0.01 precision) to fixed-point.
///
/// The shard weight is scaled up by shifting left 8 bits, mapping
/// the 0-65535 range into the 16.16 fixed-point space.
#[inline(always)]
pub const fn from_u16_weight(w: u16) -> Self {
Self((w as u32) << 8)
}
/// Saturating addition (clamps at `u32::MAX`)
#[inline(always)]
pub const fn saturating_add(self, other: Self) -> Self {
Self(self.0.saturating_add(other.0))
}
/// Saturating subtraction (clamps at 0)
#[inline(always)]
pub const fn saturating_sub(self, other: Self) -> Self {
Self(self.0.saturating_sub(other.0))
}
/// Truncate to u16 by shifting right 8 bits (inverse of `from_u16_weight`)
#[inline(always)]
pub const fn to_u16(self) -> u16 {
(self.0 >> 8) as u16
}
}
// ============================================================================
// Cactus node and arena
// ============================================================================
/// A single node in the arena-allocated cactus tree.
///
/// Represents a vertex (or contracted 2-edge-connected component) in the
/// simplified cactus structure derived from the tile's compact graph.
#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct CactusNode {
/// Vertex ID in the original graph
pub id: u16,
/// Parent index in `ArenaCactus::nodes` (0xFFFF = root / no parent)
pub parent: u16,
/// Degree in the cactus tree
pub degree: u8,
/// Flags (reserved)
pub flags: u8,
/// Weight of the edge connecting this node to its parent
pub weight_to_parent: FixedPointWeight,
}
impl CactusNode {
/// Sentinel value indicating no parent (root node)
pub const NO_PARENT: u16 = 0xFFFF;
/// Create an empty / default node
#[inline(always)]
pub const fn empty() -> Self {
Self {
id: 0,
parent: Self::NO_PARENT,
degree: 0,
flags: 0,
weight_to_parent: FixedPointWeight::ZERO,
}
}
}
// Compile-time size check: repr(C) layout is 12 bytes
// (u16 + u16 + u8 + u8 + 2-pad + u32 = 12, aligned to 4)
// 256 nodes * 12 = 3072 bytes (~3KB), fits in 14.5KB headroom.
const _: () = assert!(size_of::<CactusNode>() == 12, "CactusNode must be 12 bytes");
/// Arena-allocated cactus tree for a single tile (up to 256 vertices).
///
/// The cactus captures the 2-edge-connected component structure of the
/// tile's local graph. It is built entirely on the stack (~2KB) and used
/// to derive a canonical min-cut partition.
#[repr(C)]
pub struct ArenaCactus {
/// Node storage (one per vertex in the original graph)
pub nodes: [CactusNode; 256],
/// Number of active nodes
pub n_nodes: u16,
/// Root node index
pub root: u16,
/// Value of the global minimum cut found
pub min_cut_value: FixedPointWeight,
}
impl ArenaCactus {
/// Build a cactus from the tile's `CompactGraph`.
///
/// Algorithm (simplified):
/// 1. BFS spanning tree from the lowest-ID active vertex.
/// 2. Identify back edges and compute 2-edge-connected components
/// via low-link (Tarjan-style on edges).
/// 3. Contract each 2-edge-connected component into a single cactus
/// node; the inter-component bridge edges become cactus edges.
/// 4. Track the minimum-weight bridge as the global min-cut value.
pub fn build_from_compact_graph(graph: &CompactGraph) -> Self {
let mut cactus = ArenaCactus {
nodes: [CactusNode::empty(); 256],
n_nodes: 0,
root: 0xFFFF,
min_cut_value: FixedPointWeight::MAX,
};
if graph.num_vertices == 0 {
cactus.min_cut_value = FixedPointWeight::ZERO;
return cactus;
}
// ---- Phase 1: BFS spanning tree ----
// BFS queue (fixed-size ring buffer)
let mut queue = [0u16; 256];
let mut q_head: usize = 0;
let mut q_tail: usize = 0;
// Per-vertex BFS state
let mut visited = [false; MAX_SHARD_VERTICES];
let mut parent = [0xFFFFu16; MAX_SHARD_VERTICES];
let mut depth = [0u16; MAX_SHARD_VERTICES];
// Component ID for 2-edge-connected grouping
let mut comp_id = [0xFFFFu16; MAX_SHARD_VERTICES];
// Find lowest-ID active vertex as root
let mut root_v = 0xFFFFu16;
for v in 0..MAX_SHARD_VERTICES {
if graph.vertices[v].is_active() {
root_v = v as u16;
break;
}
}
if root_v == 0xFFFF {
cactus.min_cut_value = FixedPointWeight::ZERO;
return cactus;
}
// BFS
visited[root_v as usize] = true;
parent[root_v as usize] = 0xFFFF;
queue[q_tail] = root_v;
q_tail += 1;
while q_head < q_tail {
let u = queue[q_head] as usize;
q_head += 1;
let neighbors = graph.neighbors(u as u16);
for adj in neighbors {
let w = adj.neighbor as usize;
if !visited[w] {
visited[w] = true;
parent[w] = u as u16;
depth[w] = depth[u] + 1;
if q_tail < 256 {
queue[q_tail] = w as u16;
q_tail += 1;
}
}
}
}
// ---- Phase 2: Identify 2-edge-connected components ----
// For each back edge (u,w) where w is an ancestor of u in the BFS tree,
// all vertices on the path from u to w belong to the same 2-edge-connected
// component. We perform path marking for each back edge.
let mut next_comp: u16 = 0;
// Mark tree edges as bridges initially; back edges will un-bridge them
// We iterate edges and find back edges (both endpoints visited, not parent-child)
for e_idx in 0..graph.edges.len() {
let edge = &graph.edges[e_idx];
if !edge.is_active() {
continue;
}
let u = edge.source as usize;
let w = edge.target as usize;
if !visited[u] || !visited[w] {
continue;
}
// Check if this is a back edge (non-tree edge)
let is_tree = (parent[w] == u as u16 && depth[w] == depth[u] + 1)
|| (parent[u] == w as u16 && depth[u] == depth[w] + 1);
if is_tree {
continue; // Skip tree edges
}
// Back edge found: mark the path from u to w as same component
// Walk u and w up to their LCA, assigning a single component ID
let c = if comp_id[u] != 0xFFFF {
comp_id[u]
} else if comp_id[w] != 0xFFFF {
comp_id[w]
} else {
let c = next_comp;
next_comp = next_comp.saturating_add(1);
c
};
// Walk from u towards root, marking component
let mut a = u as u16;
while a != 0xFFFF && comp_id[a as usize] != c {
if comp_id[a as usize] == 0xFFFF {
comp_id[a as usize] = c;
}
a = parent[a as usize];
}
// Walk from w towards root, marking component
let mut b = w as u16;
while b != 0xFFFF && comp_id[b as usize] != c {
if comp_id[b as usize] == 0xFFFF {
comp_id[b as usize] = c;
}
b = parent[b as usize];
}
}
// Assign each unmarked visited vertex its own component
for v in 0..MAX_SHARD_VERTICES {
if visited[v] && comp_id[v] == 0xFFFF {
comp_id[v] = next_comp;
next_comp = next_comp.saturating_add(1);
}
}
// ---- Phase 3: Build cactus from component structure ----
// Each unique comp_id becomes a cactus node.
// The representative vertex is the lowest-ID vertex in the component.
let mut comp_repr = [0xFFFFu16; 256]; // comp_id -> representative vertex
let mut comp_to_node = [0xFFFFu16; 256]; // comp_id -> cactus node index
// Find representative (lowest vertex ID) for each component
for v in 0..MAX_SHARD_VERTICES {
if !visited[v] {
continue;
}
let c = comp_id[v] as usize;
if c < 256 && (comp_repr[c] == 0xFFFF || (v as u16) < comp_repr[c]) {
comp_repr[c] = v as u16;
}
}
// Create cactus nodes for each component
let mut n_cactus: u16 = 0;
for c in 0..next_comp.min(256) as usize {
if comp_repr[c] != 0xFFFF {
let idx = n_cactus as usize;
if idx < 256 {
cactus.nodes[idx] = CactusNode {
id: comp_repr[c],
parent: CactusNode::NO_PARENT,
degree: 0,
flags: 0,
weight_to_parent: FixedPointWeight::ZERO,
};
comp_to_node[c] = n_cactus;
n_cactus += 1;
}
}
}
cactus.n_nodes = n_cactus;
// Set root to the node containing root_v
let root_comp = comp_id[root_v as usize] as usize;
if root_comp < 256 {
cactus.root = comp_to_node[root_comp];
}
// ---- Phase 4: Connect cactus nodes via bridge edges ----
// A tree edge (parent[v] -> v) where comp_id[parent[v]] != comp_id[v]
// is a bridge. It becomes a cactus edge.
for v in 0..MAX_SHARD_VERTICES {
if !visited[v] || parent[v] == 0xFFFF {
continue;
}
let p = parent[v] as usize;
let cv = comp_id[v] as usize;
let cp = comp_id[p] as usize;
if cv != cp && cv < 256 && cp < 256 {
let node_v = comp_to_node[cv];
let node_p = comp_to_node[cp];
if node_v < 256
&& node_p < 256
&& cactus.nodes[node_v as usize].parent == CactusNode::NO_PARENT
&& node_v != cactus.root
{
// Compute bridge weight: sum of edge weights between the
// two components along this boundary
let bridge_weight = Self::compute_bridge_weight(graph, v as u16, parent[v]);
cactus.nodes[node_v as usize].parent = node_p;
cactus.nodes[node_v as usize].weight_to_parent = bridge_weight;
cactus.nodes[node_p as usize].degree += 1;
cactus.nodes[node_v as usize].degree += 1;
// Track minimum cut
if bridge_weight < cactus.min_cut_value {
cactus.min_cut_value = bridge_weight;
}
}
}
}
// If no bridges found, min cut is sum of all edge weights (graph is
// 2-edge-connected) or zero if there are no edges
if cactus.min_cut_value == FixedPointWeight::MAX {
if graph.num_edges == 0 {
cactus.min_cut_value = FixedPointWeight::ZERO;
} else {
// 2-edge-connected: min cut is at least the minimum degree
// weight sum. Compute as total weight / 2 as rough upper bound
// or just report the minimum vertex weighted degree.
cactus.min_cut_value = Self::min_vertex_weight_degree(graph);
}
}
cactus
}
/// Compute bridge weight between two vertices that are in different
/// 2-edge-connected components.
fn compute_bridge_weight(graph: &CompactGraph, v: u16, p: u16) -> FixedPointWeight {
// Find the edge between v and p and return its weight
if let Some(eid) = graph.find_edge(v, p) {
FixedPointWeight::from_u16_weight(graph.edges[eid as usize].weight)
} else {
FixedPointWeight::ONE
}
}
/// Compute minimum vertex weighted degree in the graph.
fn min_vertex_weight_degree(graph: &CompactGraph) -> FixedPointWeight {
let mut min_weight = FixedPointWeight::MAX;
for v in 0..MAX_SHARD_VERTICES {
if !graph.vertices[v].is_active() || graph.vertices[v].degree == 0 {
continue;
}
let mut weight_sum = FixedPointWeight::ZERO;
let neighbors = graph.neighbors(v as u16);
for adj in neighbors {
let eid = adj.edge_id as usize;
if eid < graph.edges.len() && graph.edges[eid].is_active() {
weight_sum = weight_sum
.saturating_add(FixedPointWeight::from_u16_weight(graph.edges[eid].weight));
}
}
if weight_sum < min_weight {
min_weight = weight_sum;
}
}
if min_weight == FixedPointWeight::MAX {
FixedPointWeight::ZERO
} else {
min_weight
}
}
/// Derive the canonical (lex-smallest) partition from this cactus.
///
/// Finds the minimum-weight edge in the cactus, removes it to create
/// two subtrees, and assigns the subtree with the lex-smallest vertex
/// set to side A. Ties are broken by selecting the edge whose removal
/// yields the lex-smallest side-A bitset.
pub fn canonical_partition(&self) -> CanonicalPartition {
let mut best = CanonicalPartition::empty();
if self.n_nodes <= 1 {
// Trivial: all vertices on side A
best.cardinality_a = self.n_nodes;
best.cut_value = FixedPointWeight::ZERO;
best.compute_hash();
return best;
}
// Find the minimum-weight cactus edge. For each non-root node whose
// edge to its parent has weight == min_cut_value, compute the
// resulting partition and keep the lex-smallest.
let mut found = false;
for i in 0..self.n_nodes as usize {
let node = &self.nodes[i];
if node.parent == CactusNode::NO_PARENT {
continue; // Root has no parent edge
}
if node.weight_to_parent != self.min_cut_value {
continue; // Not a minimum edge
}
// Removing this edge splits the cactus into:
// subtree rooted at node i vs everything else
let mut candidate = CanonicalPartition::empty();
candidate.cut_value = self.min_cut_value;
// Mark the subtree rooted at node i as side B
self.mark_subtree(i as u16, &mut candidate);
// Count cardinalities
candidate.recount();
// Ensure canonical orientation: side A should have lex-smallest
// vertex set. If side B is lex-smaller, flip.
if !candidate.is_canonical() {
candidate.flip();
}
candidate.compute_hash();
if !found || candidate.side < best.side {
best = candidate;
found = true;
}
}
if !found {
best.compute_hash();
}
best
}
/// Mark all nodes in the subtree rooted at `start` to side B.
fn mark_subtree(&self, start: u16, partition: &mut CanonicalPartition) {
// The cactus tree has parent pointers, so we find all nodes
// whose ancestor chain leads to `start` (before reaching the root
// or a node not descended from `start`).
partition.set_side(self.nodes[start as usize].id, true);
for i in 0..self.n_nodes as usize {
if i == start as usize {
continue;
}
// Walk ancestor chain to see if this node is in start's subtree
let mut cur = i as u16;
let mut in_subtree = false;
let mut steps = 0u16;
while cur != CactusNode::NO_PARENT && steps < 256 {
if cur == start {
in_subtree = true;
break;
}
cur = self.nodes[cur as usize].parent;
steps += 1;
}
if in_subtree {
partition.set_side(self.nodes[i].id, true);
}
}
}
/// Compute a 16-bit digest of the cactus structure for embedding
/// in the witness fragment.
pub fn digest(&self) -> u16 {
let mut hash: u32 = 0x811c9dc5;
for i in 0..self.n_nodes as usize {
let node = &self.nodes[i];
hash ^= node.id as u32;
hash = hash.wrapping_mul(0x01000193);
hash ^= node.parent as u32;
hash = hash.wrapping_mul(0x01000193);
hash ^= node.weight_to_parent.0;
hash = hash.wrapping_mul(0x01000193);
}
(hash & 0xFFFF) as u16
}
}
// ============================================================================
// Canonical partition
// ============================================================================
/// A canonical two-way partition of vertices into sides A and B.
///
/// The bitset encodes 256 vertices (1 bit each = 32 bytes). A cleared
/// bit means side A, a set bit means side B. The canonical orientation
/// guarantees that side A contains the lex-smallest vertex set.
#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct CanonicalPartition {
/// Bitset: 256 vertices, 1 bit each (0 = side A, 1 = side B)
pub side: [u8; 32],
/// Number of vertices on side A
pub cardinality_a: u16,
/// Number of vertices on side B
pub cardinality_b: u16,
/// Cut value (weight of edges crossing the partition)
pub cut_value: FixedPointWeight,
/// 32-bit FNV-1a hash of the `side` bitset
pub canonical_hash: [u8; 4],
}
impl CanonicalPartition {
/// Create an empty partition (all vertices on side A)
#[inline]
pub const fn empty() -> Self {
Self {
side: [0u8; 32],
cardinality_a: 0,
cardinality_b: 0,
cut_value: FixedPointWeight::ZERO,
canonical_hash: [0u8; 4],
}
}
/// Set which side a vertex belongs to.
///
/// `side_b = false` means side A, `side_b = true` means side B.
#[inline]
pub fn set_side(&mut self, vertex: u16, side_b: bool) {
if vertex >= 256 {
return;
}
let byte_idx = (vertex / 8) as usize;
let bit_idx = vertex % 8;
if side_b {
self.side[byte_idx] |= 1 << bit_idx;
} else {
self.side[byte_idx] &= !(1 << bit_idx);
}
}
/// Get which side a vertex belongs to (false = A, true = B).
#[inline]
pub fn get_side(&self, vertex: u16) -> bool {
if vertex >= 256 {
return false;
}
let byte_idx = (vertex / 8) as usize;
let bit_idx = vertex % 8;
(self.side[byte_idx] >> bit_idx) & 1 != 0
}
/// Compute the FNV-1a hash of the side bitset.
pub fn compute_hash(&mut self) {
self.canonical_hash = fnv1a_hash(&self.side);
}
/// Check if this partition is in canonical orientation.
///
/// Canonical means: side A (the cleared bits) represents the
/// lex-smallest vertex set. Equivalently, the first set bit in
/// the bitset must be 1 (vertex 0 is on side A) OR, if vertex 0
/// is on side B, we should flip.
///
/// More precisely: the complement of `side` (i.e. the A-set bitset)
/// must be lex-smaller-or-equal to `side` (the B-set bitset).
pub fn is_canonical(&self) -> bool {
// Compare side vs. its complement byte-by-byte.
// The complement represents side-A. If complement < side, canonical.
// If complement > side, not canonical (should flip).
// If equal, canonical by convention.
for i in 0..32 {
let complement = !self.side[i];
if complement < self.side[i] {
return true;
}
if complement > self.side[i] {
return false;
}
}
true // Equal (symmetric partition)
}
/// Flip the partition so that side A and side B swap.
pub fn flip(&mut self) {
for i in 0..32 {
self.side[i] = !self.side[i];
}
let tmp = self.cardinality_a;
self.cardinality_a = self.cardinality_b;
self.cardinality_b = tmp;
}
/// Recount cardinalities from the bitset.
pub fn recount(&mut self) {
let mut count_b: u16 = 0;
for i in 0..32 {
count_b += self.side[i].count_ones() as u16;
}
self.cardinality_b = count_b;
// cardinality_a is total vertices minus B, but we only know
// about the vertices that were explicitly placed. We approximate
// with 256 - B here; the caller may adjust.
self.cardinality_a = 256u16.saturating_sub(count_b);
}
}
// ============================================================================
// Canonical witness fragment
// ============================================================================
/// Canonical witness fragment (16 bytes, same as `WitnessFragment`).
///
/// Extends the original witness fragment with pseudo-deterministic
/// partition information derived from the cactus tree.
#[derive(Debug, Copy, Clone, Default)]
#[repr(C, align(16))]
pub struct CanonicalWitnessFragment {
/// Tile ID (0-255)
pub tile_id: u8,
/// Truncated epoch (tick & 0xFF)
pub epoch: u8,
/// Vertices on side A of the canonical partition
pub cardinality_a: u16,
/// Vertices on side B of the canonical partition
pub cardinality_b: u16,
/// Cut value (original weight format, truncated)
pub cut_value: u16,
/// FNV-1a hash of the canonical partition bitset
pub canonical_hash: [u8; 4],
/// Number of boundary edges
pub boundary_edges: u16,
/// Truncated hash of the cactus structure
pub cactus_digest: u16,
}
// Compile-time size assertion
const _: () = assert!(
size_of::<CanonicalWitnessFragment>() == 16,
"CanonicalWitnessFragment must be exactly 16 bytes"
);
// ============================================================================
// FNV-1a hash (no_std, no allocation)
// ============================================================================
/// Compute a 32-bit FNV-1a hash of the given byte slice.
///
/// FNV-1a is a simple, fast, non-cryptographic hash with good
/// distribution properties. It is fully deterministic and portable.
#[inline]
pub fn fnv1a_hash(data: &[u8]) -> [u8; 4] {
let mut hash: u32 = 0x811c9dc5; // FNV offset basis
for &byte in data {
hash ^= byte as u32;
hash = hash.wrapping_mul(0x01000193); // FNV prime
}
hash.to_le_bytes()
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
use crate::shard::CompactGraph;
use crate::TileState;
use core::mem::size_of;
#[test]
fn test_fixed_point_weight_ordering() {
let a = FixedPointWeight(100);
let b = FixedPointWeight(200);
let c = FixedPointWeight(100);
assert!(a < b);
assert!(b > a);
assert_eq!(a, c);
assert!(a <= c);
assert!(a >= c);
// Check from_u16_weight ordering
let w1 = FixedPointWeight::from_u16_weight(50);
let w2 = FixedPointWeight::from_u16_weight(100);
assert!(w1 < w2);
// Saturating add
let sum = w1.saturating_add(w2);
assert_eq!(sum, FixedPointWeight((50u32 << 8) + (100u32 << 8)));
// Saturating add at max
let max_sum = FixedPointWeight::MAX.saturating_add(FixedPointWeight::ONE);
assert_eq!(max_sum, FixedPointWeight::MAX);
}
#[test]
fn test_canonical_partition_determinism() {
// Build the same graph twice, verify same partition hash
let build_graph = || {
let mut g = CompactGraph::new();
g.add_edge(0, 1, 100);
g.add_edge(1, 2, 100);
g.add_edge(2, 3, 100);
g.add_edge(3, 0, 100);
g.add_edge(0, 2, 50); // Diagonal, lighter weight
g.recompute_components();
g
};
let g1 = build_graph();
let g2 = build_graph();
let c1 = ArenaCactus::build_from_compact_graph(&g1);
let c2 = ArenaCactus::build_from_compact_graph(&g2);
let p1 = c1.canonical_partition();
let p2 = c2.canonical_partition();
assert_eq!(p1.canonical_hash, p2.canonical_hash);
assert_eq!(p1.side, p2.side);
assert_eq!(p1.cut_value, p2.cut_value);
}
#[test]
fn test_fnv1a_known_values() {
// Empty input
let h0 = fnv1a_hash(&[]);
assert_eq!(
u32::from_le_bytes(h0),
0x811c9dc5,
"FNV-1a of empty should be the offset basis"
);
// Single zero byte
let h1 = fnv1a_hash(&[0]);
let expected = 0x811c9dc5u32 ^ 0;
let expected = expected.wrapping_mul(0x01000193);
assert_eq!(u32::from_le_bytes(h1), expected);
// Determinism: same input -> same output
let data = [1, 2, 3, 4, 5, 6, 7, 8];
let a = fnv1a_hash(&data);
let b = fnv1a_hash(&data);
assert_eq!(a, b);
// Different input -> (almost certainly) different output
let c = fnv1a_hash(&[8, 7, 6, 5, 4, 3, 2, 1]);
assert_ne!(a, c);
}
#[test]
fn test_arena_cactus_simple_triangle() {
let mut g = CompactGraph::new();
g.add_edge(0, 1, 100);
g.add_edge(1, 2, 100);
g.add_edge(2, 0, 100);
g.recompute_components();
let cactus = ArenaCactus::build_from_compact_graph(&g);
// A triangle is 2-edge-connected, so the cactus should have
// a single node (all 3 vertices collapsed into one component).
assert!(
cactus.n_nodes >= 1,
"Triangle cactus should have at least 1 node"
);
// The partition should be trivial since there is only one component
let partition = cactus.canonical_partition();
partition.canonical_hash; // Just ensure it doesn't panic
}
#[test]
fn test_canonical_witness_fragment_size() {
assert_eq!(
size_of::<CanonicalWitnessFragment>(),
16,
"CanonicalWitnessFragment must be exactly 16 bytes"
);
}
#[test]
fn test_canonical_witness_reproducibility() {
// Build two identical tile states and verify they produce the
// same canonical witness fragment.
let build_tile = || {
let mut tile = TileState::new(42);
tile.ingest_delta(&crate::delta::Delta::edge_add(0, 1, 100));
tile.ingest_delta(&crate::delta::Delta::edge_add(1, 2, 100));
tile.ingest_delta(&crate::delta::Delta::edge_add(2, 3, 200));
tile.ingest_delta(&crate::delta::Delta::edge_add(3, 0, 200));
tile.tick(10);
tile
};
let t1 = build_tile();
let t2 = build_tile();
let w1 = t1.canonical_witness();
let w2 = t2.canonical_witness();
assert_eq!(w1.tile_id, w2.tile_id);
assert_eq!(w1.epoch, w2.epoch);
assert_eq!(w1.cardinality_a, w2.cardinality_a);
assert_eq!(w1.cardinality_b, w2.cardinality_b);
assert_eq!(w1.cut_value, w2.cut_value);
assert_eq!(w1.canonical_hash, w2.canonical_hash);
assert_eq!(w1.boundary_edges, w2.boundary_edges);
assert_eq!(w1.cactus_digest, w2.cactus_digest);
}
#[test]
fn test_partition_set_get_side() {
let mut p = CanonicalPartition::empty();
// All on side A initially
for v in 0..256u16 {
assert!(!p.get_side(v), "vertex {} should be on side A", v);
}
// Set some to side B
p.set_side(0, true);
p.set_side(7, true);
p.set_side(8, true);
p.set_side(255, true);
assert!(p.get_side(0));
assert!(p.get_side(7));
assert!(p.get_side(8));
assert!(p.get_side(255));
assert!(!p.get_side(1));
assert!(!p.get_side(254));
// Clear
p.set_side(0, false);
assert!(!p.get_side(0));
}
#[test]
fn test_partition_flip() {
let mut p = CanonicalPartition::empty();
p.set_side(0, true);
p.set_side(1, true);
p.cardinality_a = 254;
p.cardinality_b = 2;
p.flip();
assert!(!p.get_side(0));
assert!(!p.get_side(1));
assert!(p.get_side(2));
assert_eq!(p.cardinality_a, 2);
assert_eq!(p.cardinality_b, 254);
}
#[test]
fn test_empty_graph_cactus() {
let g = CompactGraph::new();
let cactus = ArenaCactus::build_from_compact_graph(&g);
assert_eq!(cactus.n_nodes, 0);
assert_eq!(cactus.min_cut_value, FixedPointWeight::ZERO);
}
#[test]
fn test_single_edge_cactus() {
let mut g = CompactGraph::new();
g.add_edge(0, 1, 150);
g.recompute_components();
let cactus = ArenaCactus::build_from_compact_graph(&g);
assert!(
cactus.n_nodes >= 2,
"Single edge should have 2 cactus nodes"
);
let partition = cactus.canonical_partition();
// One vertex on each side
assert!(
partition.cardinality_b >= 1,
"Should have at least 1 vertex on side B"
);
}
}