mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-11-09 09:24:41 +00:00
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.
112 lines
2.9 KiB
Go
112 lines
2.9 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|