Files
obitools4/pkg/obisimilarity/index.go
T
Eric Coissac e511650f5b Refactor similarity engine and implement parallel clustering
Introduces parallel processing for clustering, refines scoring mechanisms, and expands test coverage for the alignment functionality.

This change introduces parallel processing capabilities within the clustering logic, updates the core similarity engine with new data structures and scoring methods, and implements a comprehensive test suite for the alignment tool. It also adds new command-line options for configuring alignment parameters and reference lengths.
2026-11-03 14:04:32 +11:00

107 lines
3.0 KiB
Go

package obisimilarity
import "sort"
// Member is a copy of the entry of a sequence in an Index, with its own table.
type Member struct {
Entry
ID int // identifier given by the caller
Rank int // rank in the order of insertion
}
const tabChunk = 1024
// A Bucket holds the members of one length, in the order of insertion, and
// their tables by blocks (see tableBlock).
type Bucket struct {
Members []Member
blocks []tableBlock
width int
}
// Commons caches the result of the filter on one block of a bucket.
type Commons struct {
v [64]uint8
blk int
}
// Reset must be called when changing of bucket.
func (c *Commons) Reset() { c.blk = -1 }
// Common returns the sum of the minima of the 4-mer counters of the sequence
// whose Sparse words are given and of the member k of the bucket (without the
// overflow term), or -1 if it is not available, in which case PairScore
// computes it.
func (b *Bucket) Common(k int, sparse []uint16, c *Commons) int {
if b.width == 0 || sparse == nil {
return -1
}
if blk := k / b.width; blk != c.blk {
b.blocks[blk].common(b.width, sparse, c.v[:])
c.blk = blk
}
return int(c.v[k%b.width])
}
// Index stores sequences by length. A sequence can only be similar to the ones
// whose length is compatible with its own (see Scoring.LengthCompatible), so
// it only visits the buckets of these lengths, instead of all the sequences,
// and the filter works on blocks of sequences at once.
//
// An Index can grow, but not while it is read.
type Index struct {
width int // sequences per block of transposed tables, 0 if unused
byLen map[int]*Bucket
lens []int // lengths that have a bucket, increasing
tabs [][]Table4 // chunks holding the tables of the members
n int
}
// NewIndex returns an empty index.
func NewIndex() *Index {
return &Index{width: simdBlockWidth(), byLen: make(map[int]*Bucket)}
}
// Len is the number of sequences in the index.
func (x *Index) Len() int { return x.n }
// Lengths returns the lengths that have a bucket, in increasing order.
func (x *Index) Lengths() []int { return x.lens }
// Bucket returns the bucket of the sequences of the given length.
func (x *Index) Bucket(length int) *Bucket { return x.byLen[length] }
// Add stores a copy of the entry e as the next member of the index, with the
// identifier id, and returns it.
func (x *Index) Add(e Entry, id int) Member {
if x.n%tabChunk == 0 {
x.tabs = append(x.tabs, make([]Table4, tabChunk))
}
tab := &x.tabs[len(x.tabs)-1][x.n%tabChunk]
if e.Table != nil {
*tab = *e.Table
}
e.Table = tab
b := x.byLen[e.Length]
if b == nil {
b = &Bucket{width: x.width}
x.byLen[e.Length] = b
x.lens = append(x.lens, e.Length)
sort.Ints(x.lens)
}
if w := x.width; w > 0 && !e.Ambiguous {
if len(b.Members)%w == 0 {
b.blocks = append(b.blocks, newTableBlock(w))
}
b.blocks[len(b.blocks)-1].set(w, len(b.Members)%w, tab)
} else if w > 0 && len(b.Members)%w == 0 {
b.blocks = append(b.blocks, newTableBlock(w))
}
m := Member{Entry: e, ID: id, Rank: x.n}
b.Members = append(b.Members, m)
x.n++
return m
}