mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-11-09 09:24:41 +00:00
Introduces k-mer distance structures and SIMD implementation for LCS simulation. Adds support for concurrent, read-only k-mer partners, implements data preparation for SIMD alignment, and adds comprehensive testing and benchmarking capabilities.
137 lines
4.1 KiB
Go
137 lines
4.1 KiB
Go
package obikmer
|
|
|
|
import "math/bits"
|
|
|
|
// KmerLCSResult is the result of a k-mer LCS alignment: the length of the
|
|
// Longest Common Subsequence and the length of the shortest alignment
|
|
// achieving it (same semantics as obialign.FastLCSScore).
|
|
type KmerLCSResult struct {
|
|
LCS int
|
|
AlignLen int
|
|
}
|
|
|
|
// Distance returns AlignLen - LCS, the LCS-mode edit distance.
|
|
func (r KmerLCSResult) Distance() int {
|
|
return r.AlignLen - r.LCS
|
|
}
|
|
|
|
// HammingDistance returns the number of positions at which the two k-mers
|
|
// differ. Both k-mers are encoded with 2 bits per base (see EncodeKmer);
|
|
// bits above the 2*k lowest ones are ignored. k must be between 0 and 32.
|
|
func HammingDistance(a, b uint64, k int) int {
|
|
diff := a ^ b
|
|
diff = (diff | diff>>1) & 0x5555555555555555
|
|
diff &= ^uint64(0) >> (64 - 2*k)
|
|
return bits.OnesCount64(diff)
|
|
}
|
|
|
|
// KmerLCS aligns two k-mers of the same size in LCS mode (match +1, mismatch
|
|
// and indel 0) and returns the LCS length together with the length of the
|
|
// shortest alignment among those reaching it, so that the distance is
|
|
// AlignLen - LCS.
|
|
//
|
|
// It is the scalar reference implementation, straightforward on purpose:
|
|
// it is the oracle against which the vectorised versions are tested.
|
|
// It returns {-1, -1} if k is not between 1 and 31.
|
|
func KmerLCS(a, b uint64, k int) KmerLCSResult {
|
|
if k < 1 || k > 31 {
|
|
return KmerLCSResult{-1, -1}
|
|
}
|
|
|
|
var ba, bb [32]uint8
|
|
for i := k - 1; i >= 0; i-- {
|
|
ba[i] = uint8(a & 3)
|
|
bb[i] = uint8(b & 3)
|
|
a >>= 2
|
|
b >>= 2
|
|
}
|
|
|
|
// One DP row: best (score, alignment length) per column.
|
|
var score, length [32]int
|
|
for j := 0; j <= k; j++ {
|
|
length[j] = j
|
|
}
|
|
|
|
better := func(s1, l1, s2, l2 int) bool {
|
|
return s1 > s2 || (s1 == s2 && l1 < l2)
|
|
}
|
|
|
|
for i := 1; i <= k; i++ {
|
|
diagS, diagL := score[0], length[0]
|
|
score[0], length[0] = 0, i
|
|
|
|
for j := 1; j <= k; j++ {
|
|
upS, upL := score[j], length[j]
|
|
|
|
bestS, bestL := diagS, diagL+1
|
|
if ba[j-1] == bb[i-1] {
|
|
bestS++
|
|
}
|
|
if better(upS, upL+1, bestS, bestL) {
|
|
bestS, bestL = upS, upL+1
|
|
}
|
|
if better(score[j-1], length[j-1]+1, bestS, bestL) {
|
|
bestS, bestL = score[j-1], length[j-1]+1
|
|
}
|
|
|
|
diagS, diagL = upS, upL
|
|
score[j], length[j] = bestS, bestL
|
|
}
|
|
}
|
|
|
|
return KmerLCSResult{score[k], length[k]}
|
|
}
|
|
|
|
// KmerLCSBatch computes KmerLCS(query, partners[i], k) for every partner and
|
|
// stores it in out[i]. out must be at least as long as partners.
|
|
//
|
|
// When built with GOEXPERIMENT=simd the partners are aligned several at a
|
|
// time, one per SIMD lane; otherwise the scalar reference is used.
|
|
// It returns false, leaving out untouched, if k is not between 1 and 31 or
|
|
// if out is too short.
|
|
func KmerLCSBatch(query uint64, partners []uint64, k int, out []KmerLCSResult) bool {
|
|
if k < 1 || k > 31 || len(out) < len(partners) {
|
|
return false
|
|
}
|
|
kmerLCSBatch(query, partners, k, out)
|
|
return true
|
|
}
|
|
|
|
// KmerLCSPartners is a fixed set of k-mers prepared once to be aligned against
|
|
// many queries with KmerLCSBatch-like speed, without redoing the per-call
|
|
// reorganisation of the partners. It is read-only after creation and can be
|
|
// shared between goroutines.
|
|
type KmerLCSPartners struct {
|
|
prepared kmerLCSPrepared
|
|
n int
|
|
}
|
|
|
|
// NewKmerLCSPartners prepares partners for repeated alignment.
|
|
// It returns nil if k is not between 1 and 31. The slice is not copied when
|
|
// built without GOEXPERIMENT=simd, so it must not be modified afterwards.
|
|
func NewKmerLCSPartners(partners []uint64, k int) *KmerLCSPartners {
|
|
if k < 1 || k > 31 {
|
|
return nil
|
|
}
|
|
return &KmerLCSPartners{prepared: prepareKmerLCS(partners, k), n: len(partners)}
|
|
}
|
|
|
|
// Len returns the number of prepared partners.
|
|
func (p *KmerLCSPartners) Len() int { return p.n }
|
|
|
|
// Align computes the alignment of query against every prepared partner and
|
|
// stores it in out. It returns false if out is shorter than Len().
|
|
func (p *KmerLCSPartners) Align(query uint64, out []KmerLCSResult) bool {
|
|
if len(out) < p.n {
|
|
return false
|
|
}
|
|
p.prepared.align(query, out[:p.n])
|
|
return true
|
|
}
|
|
|
|
func kmerLCSBatchScalar(query uint64, partners []uint64, k int, out []KmerLCSResult) {
|
|
for i, p := range partners {
|
|
out[i] = KmerLCS(query, p, k)
|
|
}
|
|
}
|