mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-11-10 01:24:41 +00:00
Implement Banded LCS calculation with scalar and SIMD support
Introduces the BandedLCS function for efficient LCS scoring, supporting both scalar and SIMD implementations. Adds comprehensive unit tests, benchmarking utilities, and error handling for sequence constraints.
This commit is contained in:
1 parent
f3250984a5
commit
75bb635a6c
7 files changed
+763
-7
No files matched your search
@@ -0,0 +1,111 @@
|
||||
package obialign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq"
|
||||
)
|
||||
|
||||
// randomDNA returns a random ACGT sequence of length n.
|
||||
func randomDNA(rng *rand.Rand, n int) []byte {
|
||||
const nuc = "ACGT"
|
||||
s := make([]byte, n)
|
||||
for i := range s {
|
||||
s[i] = nuc[rng.Intn(4)]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// mutate returns a copy of s in which each position is, with probability
|
||||
// rate, substituted, deleted or followed by an inserted base (equiprobable).
|
||||
func mutate(rng *rand.Rand, s []byte, rate float64) []byte {
|
||||
const nuc = "ACGT"
|
||||
out := make([]byte, 0, len(s)+len(s)/10)
|
||||
for _, c := range s {
|
||||
if rng.Float64() >= rate {
|
||||
out = append(out, c)
|
||||
continue
|
||||
}
|
||||
switch rng.Intn(3) {
|
||||
case 0:
|
||||
out = append(out, nuc[rng.Intn(4)])
|
||||
case 1: // deletion
|
||||
case 2:
|
||||
out = append(out, c, nuc[rng.Intn(4)])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// benchPairs builds a fixed set of pairs. "similar" pairs differ by about
|
||||
// 2% of mutated positions, "random" pairs are unrelated sequences.
|
||||
func benchPairs(n, length int, similar bool) [][2]*obiseq.BioSequence {
|
||||
rng := rand.New(rand.NewSource(42))
|
||||
pairs := make([][2]*obiseq.BioSequence, n)
|
||||
for i := range pairs {
|
||||
a := randomDNA(rng, length)
|
||||
var b []byte
|
||||
if similar {
|
||||
b = mutate(rng, a, 0.02)
|
||||
} else {
|
||||
b = randomDNA(rng, length)
|
||||
}
|
||||
pairs[i] = [2]*obiseq.BioSequence{
|
||||
obiseq.NewBioSequence("a", a, ""),
|
||||
obiseq.NewBioSequence("b", b, ""),
|
||||
}
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
// BenchmarkLegacyLCS measures the historical implementation (the one
|
||||
// FastLCSScore used before BandedLCS), kept as the "before" reference.
|
||||
func BenchmarkLegacyLCS(b *testing.B) {
|
||||
for _, similar := range []bool{true, false} {
|
||||
kind := "random"
|
||||
if similar {
|
||||
kind = "similar"
|
||||
}
|
||||
for _, length := range []int{100, 500, 2000} {
|
||||
pairs := benchPairs(64, length, similar)
|
||||
for _, maxErr := range []int{-1, 5, length / 20, length / 5} {
|
||||
name := fmt.Sprintf("%s/len=%d/maxErr=%d", kind, length, maxErr)
|
||||
b.Run(name, func(b *testing.B) {
|
||||
var buffer []uint64
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
p := pairs[i%len(pairs)]
|
||||
FastLCSEGFScoreByte(p[0].Sequence(), p[1].Sequence(), maxErr, false, &buffer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkBandedLCS runs on the same pairs as BenchmarkLegacyLCS.
|
||||
func BenchmarkBandedLCS(b *testing.B) {
|
||||
for _, similar := range []bool{true, false} {
|
||||
kind := "random"
|
||||
if similar {
|
||||
kind = "similar"
|
||||
}
|
||||
for _, length := range []int{100, 500, 2000} {
|
||||
pairs := benchPairs(64, length, similar)
|
||||
for _, maxErr := range []int{-1, 5, length / 20, length / 5} {
|
||||
name := fmt.Sprintf("%s/len=%d/maxErr=%d", kind, length, maxErr)
|
||||
b.Run(name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
p := pairs[i%len(pairs)]
|
||||
BandedLCS(p[0].Sequence(), p[1].Sequence(), maxErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user