#![allow(missing_docs)]
use crate::shard::{CompactGraph, MAX_SHARD_VERTICES};
use core::mem::size_of;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[repr(transparent)]
pub struct FixedPointWeight(pub u32);
impl FixedPointWeight {
pub const ZERO: Self = Self(0);
pub const ONE: Self = Self(65536);
pub const MAX: Self = Self(u32::MAX);
#[inline(always)]
pub const fn from_u16_weight(w: u16) -> Self {
Self((w as u32) << 8)
}
#[inline(always)]
pub const fn saturating_add(self, other: Self) -> Self {
Self(self.0.saturating_add(other.0))
}
#[inline(always)]
pub const fn saturating_sub(self, other: Self) -> Self {
Self(self.0.saturating_sub(other.0))
}
#[inline(always)]
pub const fn to_u16(self) -> u16 {
(self.0 >> 8) as u16
}
}
#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct CactusNode {
pub id: u16,
pub parent: u16,
pub degree: u8,
pub flags: u8,
pub weight_to_parent: FixedPointWeight,
}
impl CactusNode {
pub const NO_PARENT: u16 = 0xFFFF;
#[inline(always)]
pub const fn empty() -> Self {
Self {
id: 0,
parent: Self::NO_PARENT,
degree: 0,
flags: 0,
weight_to_parent: FixedPointWeight::ZERO,
}
}
}
const _: () = assert!(
size_of::<CactusNode>() == 12,
"CactusNode must be 12 bytes"
);
#[repr(C)]
pub struct ArenaCactus {
pub nodes: [CactusNode; 256],
pub n_nodes: u16,
pub root: u16,
pub min_cut_value: FixedPointWeight,
}
impl ArenaCactus {
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;
}
let mut queue = [0u16; 256];
let mut q_head: usize = 0;
let mut q_tail: usize = 0;
let mut visited = [false; MAX_SHARD_VERTICES];
let mut parent = [0xFFFFu16; MAX_SHARD_VERTICES];
let mut depth = [0u16; MAX_SHARD_VERTICES];
let mut comp_id = [0xFFFFu16; MAX_SHARD_VERTICES];
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;
}
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;
}
}
}
}
let mut next_comp: u16 = 0;
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;
}
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; }
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
};
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];
}
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];
}
}
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);
}
}
let mut comp_repr = [0xFFFFu16; 256]; let mut comp_to_node = [0xFFFFu16; 256];
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;
}
}
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;
let root_comp = comp_id[root_v as usize] as usize;
if root_comp < 256 {
cactus.root = comp_to_node[root_comp];
}
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
{
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;
if bridge_weight < cactus.min_cut_value {
cactus.min_cut_value = bridge_weight;
}
}
}
}
if cactus.min_cut_value == FixedPointWeight::MAX {
if graph.num_edges == 0 {
cactus.min_cut_value = FixedPointWeight::ZERO;
} else {
cactus.min_cut_value = Self::min_vertex_weight_degree(graph);
}
}
cactus
}
fn compute_bridge_weight(graph: &CompactGraph, v: u16, p: u16) -> FixedPointWeight {
if let Some(eid) = graph.find_edge(v, p) {
FixedPointWeight::from_u16_weight(graph.edges[eid as usize].weight)
} else {
FixedPointWeight::ONE
}
}
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
}
}
pub fn canonical_partition(&self) -> CanonicalPartition {
let mut best = CanonicalPartition::empty();
if self.n_nodes <= 1 {
best.cardinality_a = self.n_nodes;
best.cut_value = FixedPointWeight::ZERO;
best.compute_hash();
return best;
}
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; }
if node.weight_to_parent != self.min_cut_value {
continue; }
let mut candidate = CanonicalPartition::empty();
candidate.cut_value = self.min_cut_value;
self.mark_subtree(i as u16, &mut candidate);
candidate.recount();
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
}
fn mark_subtree(&self, start: u16, partition: &mut CanonicalPartition) {
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;
}
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);
}
}
}
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
}
}
#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct CanonicalPartition {
pub side: [u8; 32],
pub cardinality_a: u16,
pub cardinality_b: u16,
pub cut_value: FixedPointWeight,
pub canonical_hash: [u8; 4],
}
impl CanonicalPartition {
#[inline]
pub const fn empty() -> Self {
Self {
side: [0u8; 32],
cardinality_a: 0,
cardinality_b: 0,
cut_value: FixedPointWeight::ZERO,
canonical_hash: [0u8; 4],
}
}
#[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);
}
}
#[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
}
pub fn compute_hash(&mut self) {
self.canonical_hash = fnv1a_hash(&self.side);
}
pub fn is_canonical(&self) -> bool {
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 }
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;
}
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;
self.cardinality_a = 256u16.saturating_sub(count_b);
}
}
#[derive(Debug, Copy, Clone, Default)]
#[repr(C, align(16))]
pub struct CanonicalWitnessFragment {
pub tile_id: u8,
pub epoch: u8,
pub cardinality_a: u16,
pub cardinality_b: u16,
pub cut_value: u16,
pub canonical_hash: [u8; 4],
pub boundary_edges: u16,
pub cactus_digest: u16,
}
const _: () = assert!(
size_of::<CanonicalWitnessFragment>() == 16,
"CanonicalWitnessFragment must be exactly 16 bytes"
);
#[inline]
pub fn fnv1a_hash(data: &[u8]) -> [u8; 4] {
let mut hash: u32 = 0x811c9dc5; for &byte in data {
hash ^= byte as u32;
hash = hash.wrapping_mul(0x01000193); }
hash.to_le_bytes()
}
#[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);
let w1 = FixedPointWeight::from_u16_weight(50);
let w2 = FixedPointWeight::from_u16_weight(100);
assert!(w1 < w2);
let sum = w1.saturating_add(w2);
assert_eq!(sum, FixedPointWeight((50u32 << 8) + (100u32 << 8)));
let max_sum = FixedPointWeight::MAX.saturating_add(FixedPointWeight::ONE);
assert_eq!(max_sum, FixedPointWeight::MAX);
}
#[test]
fn test_canonical_partition_determinism() {
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); 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() {
let h0 = fnv1a_hash(&[]);
assert_eq!(
u32::from_le_bytes(h0),
0x811c9dc5,
"FNV-1a of empty should be the offset basis"
);
let h1 = fnv1a_hash(&[0]);
let expected = 0x811c9dc5u32 ^ 0;
let expected = expected.wrapping_mul(0x01000193);
assert_eq!(u32::from_le_bytes(h1), expected);
let data = [1, 2, 3, 4, 5, 6, 7, 8];
let a = fnv1a_hash(&data);
let b = fnv1a_hash(&data);
assert_eq!(a, b);
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);
assert!(
cactus.n_nodes >= 1,
"Triangle cactus should have at least 1 node"
);
let partition = cactus.canonical_partition();
partition.canonical_hash; }
#[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() {
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();
for v in 0..256u16 {
assert!(!p.get_side(v), "vertex {} should be on side A", v);
}
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));
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();
assert!(
partition.cardinality_b >= 1,
"Should have at least 1 vertex on side B"
);
}
}