1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
// Copyright (c) 2022 MASSA LABS <info@massa.net>
//! This file is responsible for clique computation
use massa_models::{
block_id::BlockId,
prehash::{PreHashMap, PreHashSet},
};
/// Computes max cliques of compatible blocks
pub fn compute_max_cliques(
gi_head: &PreHashMap<BlockId, PreHashSet<BlockId>>,
) -> Vec<PreHashSet<BlockId>> {
let mut max_cliques: Vec<PreHashSet<BlockId>> = Vec::new();
// algorithm adapted from IK_GPX as summarized in:
// Cazals et al., "A note on the problem of reporting maximal cliques"
// Theoretical Computer Science, 2008
// https://doi.org/10.1016/j.tcs.2008.05.010
// stack: r, p, x
let mut stack: Vec<(
PreHashSet<BlockId>,
PreHashSet<BlockId>,
PreHashSet<BlockId>,
)> = vec![(
PreHashSet::<BlockId>::default(),
gi_head.keys().cloned().collect(),
PreHashSet::<BlockId>::default(),
)];
while let Some((r, mut p, mut x)) = stack.pop() {
if p.is_empty() && x.is_empty() {
max_cliques.push(r);
continue;
}
// choose the pivot vertex following the GPX scheme:
// u_p = node from (p \/ x) that maximizes the cardinality of (P \ Neighbors(u_p, GI))
let &u_p = p
.union(&x)
.max_by_key(|&u| {
p.difference(&(&gi_head[u] | &vec![*u].into_iter().collect()))
.count()
})
.unwrap(); // p was checked to be non-empty before
// iterate over u_set = (p /\ Neighbors(u_p, GI))
let u_set: PreHashSet<BlockId> = &p & &(&gi_head[&u_p] | &vec![u_p].into_iter().collect());
for u_i in u_set.into_iter() {
p.remove(&u_i);
let u_i_set: PreHashSet<BlockId> = vec![u_i].into_iter().collect();
let comp_n_u_i: PreHashSet<BlockId> = &gi_head[&u_i] | &u_i_set;
stack.push((&r | &u_i_set, &p - &comp_n_u_i, &x - &comp_n_u_i));
x.insert(u_i);
}
}
max_cliques
}
/// Tests
#[cfg(test)]
mod tests {
use crate::state::clique_computation::compute_max_cliques;
use itertools::Itertools;
use massa_models::{
block_id::BlockId,
prehash::{PreHashMap, PreHashSet},
};
use rand::Rng;
#[test]
fn test_compute_max_cliques() {
// Define the maximum size of the graph and the number of iterations
const MAX_SIZE: usize = 10;
const ITERATIONS: usize = 1000;
// Generate random test cases and run the algorithm
let mut rng = rand::thread_rng();
for _ in 0..ITERATIONS {
// Generate a random graph size
let size = rng.gen_range(0..=MAX_SIZE);
// Generate random incompatibility relationships
let mut gi_head = PreHashMap::default();
for i in 0..size {
gi_head.insert(
BlockId::generate_from_hash(massa_hash::Hash::compute_from(&i.to_be_bytes())),
PreHashSet::default(),
);
}
for i in 0..size.saturating_sub(1) {
for j in (i + 1)..size {
// Generate a random compatibility relationship
let is_compatible = rng.gen_bool(0.5);
if !is_compatible {
let i_id = BlockId::generate_from_hash(massa_hash::Hash::compute_from(
&i.to_be_bytes(),
));
let j_id = BlockId::generate_from_hash(massa_hash::Hash::compute_from(
&j.to_be_bytes(),
));
// Add the incompatibility relationship to gi_head
gi_head.entry(i_id).or_default().insert(j_id);
gi_head.entry(j_id).or_default().insert(i_id);
}
}
}
// Check cliques
assert_cliques_valid(&gi_head, &compute_max_cliques(&gi_head));
}
}
/// Assert that a set of cliques is valid
fn assert_cliques_valid(
gi_head: &PreHashMap<BlockId, PreHashSet<BlockId>>,
max_cliques: &Vec<PreHashSet<BlockId>>,
) {
// Check that there is at least one clique
if max_cliques.is_empty() {
panic!("max_cliques is empty");
}
// Check that all cliques are unique
for (i, clique1) in max_cliques.iter().enumerate() {
for (j, clique2) in max_cliques.iter().enumerate() {
if i != j && clique1 == clique2 {
panic!("two of the cliques are identical");
}
}
}
for clique in max_cliques {
// Check that all pairs of vertices in the clique are compatible
for (v1, v2) in clique.iter().tuple_combinations() {
if gi_head[v1].contains(v2) || gi_head[v2].contains(v1) {
panic!("incompatible vertices found within the same clique");
}
}
// Check that the clique is maximal
for v in gi_head.keys() {
if !clique.contains(v) && clique.iter().all(|c| !gi_head[v].contains(c)) {
panic!("a clique is non-maximal");
}
}
}
// All cliques are valid, unique and maximal
}
}