mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-11-09 17:24:41 +00:00
Introduces a mechanism for tracking progress during clique finding operations. This includes implementing a progress bar state management system, handling visibility checks, and integrating progress reporting into the clique finding logic and result processing.
164 lines
4.5 KiB
Go
164 lines
4.5 KiB
Go
package obigraph
|
|
|
|
import (
|
|
"context"
|
|
"math/bits"
|
|
"math/rand"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// adjacency matrix filter for small graphs
|
|
func matrixFilter(adj [][]bool) NeighborFilter {
|
|
return func(v int32, candidates, out []int32) []int32 {
|
|
for _, c := range candidates {
|
|
if adj[v][c] {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
}
|
|
|
|
func randomGraph(rng *rand.Rand, n int, p float64) [][]bool {
|
|
adj := make([][]bool, n)
|
|
for i := range adj {
|
|
adj[i] = make([]bool, n)
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
for j := i + 1; j < n; j++ {
|
|
if rng.Float64() < p {
|
|
adj[i][j], adj[j][i] = true, true
|
|
}
|
|
}
|
|
}
|
|
return adj
|
|
}
|
|
|
|
// maximum clique size by brute force over all the subsets (n <= 20)
|
|
func bruteMaxClique(adj [][]bool) int {
|
|
n := len(adj)
|
|
best := 0
|
|
for mask := 1; mask < 1<<n; mask++ {
|
|
size := bits.OnesCount(uint(mask))
|
|
if size <= best {
|
|
continue
|
|
}
|
|
ok := true
|
|
for i := 0; i < n && ok; i++ {
|
|
if mask>>i&1 == 0 {
|
|
continue
|
|
}
|
|
for j := i + 1; j < n; j++ {
|
|
if mask>>j&1 == 1 && !adj[i][j] {
|
|
ok = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if ok {
|
|
best = size
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
func isClique(adj [][]bool, c []int32) bool {
|
|
seen := map[int32]bool{}
|
|
for i, a := range c {
|
|
if seen[a] {
|
|
return false
|
|
}
|
|
seen[a] = true
|
|
for _, b := range c[i+1:] {
|
|
if !adj[a][b] {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestFindCliqueAgainstBruteForce(t *testing.T) {
|
|
rng := rand.New(rand.NewSource(1))
|
|
for trial := 0; trial < 40; trial++ {
|
|
n := 8 + rng.Intn(10)
|
|
adj := randomGraph(rng, n, 0.3+0.5*rng.Float64())
|
|
maxSize := bruteMaxClique(adj)
|
|
filter := matrixFilter(adj)
|
|
|
|
for _, workers := range []int{1, 3} {
|
|
r := FindClique(context.Background(), n, filter, maxSize, CliqueOptions{Workers: workers})
|
|
if !r.Reached || len(r.Clique) < maxSize || !isClique(adj, r.Clique) {
|
|
t.Fatalf("trial %d: target %d not reached: %+v", trial, maxSize, r)
|
|
}
|
|
|
|
r = FindClique(context.Background(), n, filter, maxSize+1, CliqueOptions{Workers: workers})
|
|
if r.Reached || !r.Exhausted {
|
|
t.Fatalf("trial %d: size %d cannot exist, got %+v", trial, maxSize+1, r)
|
|
}
|
|
// best effort: a valid clique, not necessarily a maximum one
|
|
if len(r.Clique) < 1 || len(r.Clique) > maxSize || !isClique(adj, r.Clique) {
|
|
t.Fatalf("trial %d: invalid best clique %v (maximum is %d)", trial, r.Clique, maxSize)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFindCliqueTrivialTargets(t *testing.T) {
|
|
adj := randomGraph(rand.New(rand.NewSource(2)), 5, 0.5)
|
|
if r := FindClique(context.Background(), 5, matrixFilter(adj), 0, CliqueOptions{}); !r.Reached {
|
|
t.Fatal("target 0 is always reached")
|
|
}
|
|
if r := FindClique(context.Background(), 0, matrixFilter(adj), 1, CliqueOptions{}); r.Reached {
|
|
t.Fatal("no clique in an empty graph")
|
|
}
|
|
if r := FindClique(context.Background(), 5, matrixFilter(adj), 1, CliqueOptions{}); !r.Reached || len(r.Clique) != 1 {
|
|
t.Fatalf("a single vertex is a clique: %+v", r)
|
|
}
|
|
}
|
|
|
|
// A hopeless search must stop with the deadline and return a valid clique.
|
|
// (When the target is far above the clique number of a sparse graph, pruning
|
|
// proves the absence at once: see TestFindCliqueAgainstBruteForce.)
|
|
func TestFindCliqueTimeout(t *testing.T) {
|
|
// dense graph: its largest clique is far below the target, but pruning only
|
|
// bites after many levels, so the search cannot finish within the deadline
|
|
const n = 300
|
|
adj := randomGraph(rand.New(rand.NewSource(3)), n, 0.9)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
|
|
defer cancel()
|
|
|
|
start := time.Now()
|
|
r := FindClique(ctx, n, matrixFilter(adj), 150, CliqueOptions{Workers: 2})
|
|
if elapsed := time.Since(start); elapsed > 3*time.Second {
|
|
t.Fatalf("deadline not honoured: %v", elapsed)
|
|
}
|
|
if r.Reached || r.Exhausted {
|
|
t.Fatalf("size 150 cannot be reached nor proven absent in time: %+v", r)
|
|
}
|
|
if len(r.Clique) < 2 || !isClique(adj, r.Clique) {
|
|
t.Fatalf("invalid best clique %v", r.Clique)
|
|
}
|
|
}
|
|
|
|
func TestFindCliqueProgress(t *testing.T) {
|
|
adj := randomGraph(rand.New(rand.NewSource(4)), 40, 0.7)
|
|
|
|
var seen []int
|
|
r := FindClique(context.Background(), 40, matrixFilter(adj), 5, CliqueOptions{
|
|
Progress: func(best int) { seen = append(seen, best) },
|
|
})
|
|
if !r.Reached {
|
|
t.Fatalf("size 5 should be reached: %+v", r)
|
|
}
|
|
if len(seen) == 0 || seen[len(seen)-1] != len(r.Clique) {
|
|
t.Fatalf("progress %v does not end at the clique size %d", seen, len(r.Clique))
|
|
}
|
|
for i := 1; i < len(seen); i++ {
|
|
if seen[i] <= seen[i-1] {
|
|
t.Fatalf("progress must be strictly increasing: %v", seen)
|
|
}
|
|
}
|
|
}
|