Implement progress tracking for clique finding

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.
This commit is contained in:
Eric Coissac committed 2026-11-03 09:26:29 +11:00
1 parent db2b398dd0
commit 1d692d7e0b
5 files changed
+141 -1

No files matched your search

+21
View File
@@ -1,5 +1,7 @@
package obidefault
import "os"
var __no_progress_bar__ = false
func ProgressBar() bool {
@@ -17,3 +19,22 @@ func SetNoProgressBar(b bool) {
func NoProgressBarPtr() *bool {
return &__no_progress_bar__
}
// ProgressBarVisible tells whether a progress bar can be displayed: it is not
// disabled by --no-progressbar, the standard error output is a terminal and
// the standard output is not piped to another command.
func ProgressBarVisible() bool {
if !ProgressBar() {
return false
}
if e, err := os.Stderr.Stat(); err != nil || (e.Mode()&os.ModeCharDevice) != os.ModeCharDevice {
return false
}
if o, err := os.Stdout.Stat(); err == nil && (o.Mode()&os.ModeNamedPipe) == os.ModeNamedPipe {
return false
}
return true
}
+7
View File
@@ -21,6 +21,10 @@ type CliqueOptions struct {
Workers int
// Seed seeds the random vertex orders of the restarts.
Seed int64
// Progress, if set, is called with the size of the best clique each time a
// larger one is found. Calls are serialized and made while a lock is held:
// it must return quickly and must not call FindClique.
Progress func(best int)
}
// CliqueResult is the outcome of FindClique.
@@ -68,6 +72,9 @@ func FindClique(ctx context.Context, n int, filter NeighborFilter, target int, o
mutex.Lock()
if len(clique) > len(best) {
best = append(best[:0], clique...)
if opts.Progress != nil {
opts.Progress(len(best))
}
}
mutex.Unlock()
}
+20
View File
@@ -141,3 +141,23 @@ func TestFindCliqueTimeout(t *testing.T) {
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)
}
}
}
+7 -1
View File
@@ -163,8 +163,14 @@ func CLIOligoFamily() ([]string, error) {
}
filter := obikmer.NewKmerDissimilarityFilter(kmers, size, metric, dmin)
progress := startCliqueProgress(target)
result := obigraph.FindClique(ctx, len(kmers), filter, target,
obigraph.CliqueOptions{Workers: obidefault.ParallelWorkers(), Seed: 1})
obigraph.CliqueOptions{
Workers: obidefault.ParallelWorkers(),
Seed: 1,
Progress: progress.Update,
})
progress.Stop(result.Reached)
switch {
case result.Reached:
+86
View File
@@ -0,0 +1,86 @@
package obioligotag
import (
"fmt"
"os"
"sync"
"time"
"github.com/schollz/progressbar/v3"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault"
)
// cliqueProgress displays the progress of the search of a family: the bar goes
// from 0 to the required family size and follows the size of the best family
// found so far. A nil *cliqueProgress does nothing, which is what is used when
// no progress bar has to be displayed.
type cliqueProgress struct {
bar *progressbar.ProgressBar
max int
stop chan struct{}
done sync.WaitGroup
}
// startCliqueProgress starts the progress bar of a search for a family of
// target oligonucleotides, or returns nil if no bar can be displayed.
func startCliqueProgress(target int) *cliqueProgress {
if !obidefault.ProgressBarVisible() {
return nil
}
p := &cliqueProgress{
max: target,
stop: make(chan struct{}),
bar: progressbar.NewOptions(target,
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionSetWidth(15),
progressbar.OptionShowCount(),
progressbar.OptionSetElapsedTime(true),
progressbar.OptionSetPredictTime(false),
progressbar.OptionThrottle(100*time.Millisecond),
progressbar.OptionSetDescription("[Searching a family of oligonucleotides]"),
),
}
// The search may stay a long time on the same family: keep the elapsed
// time alive.
p.done.Add(1)
go func() {
defer p.done.Done()
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
p.bar.Add(0)
case <-p.stop:
return
}
}
}()
return p
}
// Update reports the size of the best family found so far.
func (p *cliqueProgress) Update(best int) {
if p != nil {
p.bar.Set(min(best, p.max))
}
}
// Stop ends the display. The bar is filled only if the family was found.
func (p *cliqueProgress) Stop(reached bool) {
if p == nil {
return
}
close(p.stop)
p.done.Wait()
if reached {
p.bar.Finish()
} else {
p.bar.Exit()
}
fmt.Fprintln(os.Stderr)
}