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:
Eric Coissac committed 2026-11-03 09:28:46 +11:00
1 parent f3250984a5
commit 75bb635a6c
7 files changed
+763 -7

No files matched your search

+111
View File
@@ -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)
}
})
}
}
}
}