mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-11-09 17: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.
172 lines
5.1 KiB
Go
172 lines
5.1 KiB
Go
package obikmer
|
|
|
|
import (
|
|
"math/rand"
|
|
"testing"
|
|
)
|
|
|
|
func randomKmer(rng *rand.Rand, k int) uint64 {
|
|
return rng.Uint64() & (uint64(1)<<(2*uint(k)) - 1)
|
|
}
|
|
|
|
func TestHammingDistance(t *testing.T) {
|
|
rng := rand.New(rand.NewSource(1))
|
|
for _, k := range []int{1, 2, 7, 21, 31} {
|
|
for n := 0; n < 200; n++ {
|
|
a, b := randomKmer(rng, k), randomKmer(rng, k)
|
|
sa, sb := DecodeKmer(a, k, nil), DecodeKmer(b, k, nil)
|
|
want := 0
|
|
for i := range sa {
|
|
if sa[i] != sb[i] {
|
|
want++
|
|
}
|
|
}
|
|
if got := HammingDistance(a, b, k); got != want {
|
|
t.Fatalf("k=%d %s/%s: got %d, want %d", k, sa, sb, got, want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHammingDistanceIgnoresHighBits(t *testing.T) {
|
|
if got := HammingDistance(0xFF00, 0x0000, 4); got != 0 {
|
|
t.Fatalf("bits above 2k must be ignored, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestKmerLCSKnown(t *testing.T) {
|
|
enc := func(s string) uint64 { return EncodeKmer([]byte(s), len(s)) }
|
|
cases := []struct {
|
|
a, b string
|
|
want KmerLCSResult
|
|
}{
|
|
{"ACGT", "ACGT", KmerLCSResult{4, 4}},
|
|
{"AAAA", "CCCC", KmerLCSResult{0, 4}},
|
|
{"ACGT", "ACGA", KmerLCSResult{3, 4}},
|
|
{"ACGT", "CGTA", KmerLCSResult{3, 5}}, // one shift: 1 indel on each side
|
|
}
|
|
for _, c := range cases {
|
|
if got := KmerLCS(enc(c.a), enc(c.b), 4); got != c.want {
|
|
t.Errorf("%s/%s: got %+v, want %+v", c.a, c.b, got, c.want)
|
|
}
|
|
}
|
|
if got := KmerLCS(0, 0, 32); got.LCS != -1 {
|
|
t.Errorf("k=32 must be rejected, got %+v", got)
|
|
}
|
|
}
|
|
|
|
// The batch implementation (SIMD when built with GOEXPERIMENT=simd) must agree
|
|
// with the scalar reference for all k and for batch sizes around the lane count.
|
|
func TestKmerLCSBatchMatchesReference(t *testing.T) {
|
|
rng := rand.New(rand.NewSource(2))
|
|
for _, k := range []int{1, 2, 3, 5, 8, 16, 21, 30, 31} {
|
|
for _, n := range []int{0, 1, 3, 7, 8, 9, 16, 17, 100} {
|
|
query := randomKmer(rng, k)
|
|
partners := make([]uint64, n)
|
|
for i := range partners {
|
|
partners[i] = randomKmer(rng, k)
|
|
}
|
|
if n > 2 {
|
|
partners[0] = query // identical
|
|
}
|
|
out := make([]KmerLCSResult, n)
|
|
if !KmerLCSBatch(query, partners, k, out) {
|
|
t.Fatalf("k=%d n=%d: batch refused", k, n)
|
|
}
|
|
for i, p := range partners {
|
|
if want := KmerLCS(query, p, k); out[i] != want {
|
|
t.Fatalf("k=%d n=%d partner %d: got %+v, want %+v", k, n, i, out[i], want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKmerLCSBatchRejectsBadInput(t *testing.T) {
|
|
if KmerLCSBatch(0, []uint64{1}, 0, make([]KmerLCSResult, 1)) {
|
|
t.Fatal("k=0 must be rejected")
|
|
}
|
|
if KmerLCSBatch(0, []uint64{1, 2}, 4, make([]KmerLCSResult, 1)) {
|
|
t.Fatal("short output must be rejected")
|
|
}
|
|
}
|
|
|
|
func benchBatch(b *testing.B, k int, f func(uint64, []uint64, int, []KmerLCSResult)) {
|
|
rng := rand.New(rand.NewSource(3))
|
|
partners := make([]uint64, 1024)
|
|
for i := range partners {
|
|
partners[i] = randomKmer(rng, k)
|
|
}
|
|
out := make([]KmerLCSResult, len(partners))
|
|
query := randomKmer(rng, k)
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
f(query, partners, k, out)
|
|
}
|
|
b.ReportMetric(float64(b.Elapsed().Nanoseconds())/float64(b.N*len(partners)), "ns/pair")
|
|
}
|
|
|
|
func BenchmarkKmerLCSScalar21(b *testing.B) { benchBatch(b, 21, kmerLCSBatchScalar) }
|
|
func BenchmarkKmerLCSBatch21(b *testing.B) { benchBatch(b, 21, kmerLCSBatch) }
|
|
func BenchmarkKmerLCSScalar11(b *testing.B) { benchBatch(b, 11, kmerLCSBatchScalar) }
|
|
func BenchmarkKmerLCSBatch11(b *testing.B) { benchBatch(b, 11, kmerLCSBatch) }
|
|
|
|
func TestKmerLCSPartnersMatchesReference(t *testing.T) {
|
|
rng := rand.New(rand.NewSource(6))
|
|
for _, k := range []int{1, 5, 21, 31} {
|
|
for _, n := range []int{0, 1, 7, 8, 9, 50} {
|
|
partners := make([]uint64, n)
|
|
for i := range partners {
|
|
partners[i] = randomKmer(rng, k)
|
|
}
|
|
prep := NewKmerLCSPartners(partners, k)
|
|
out := make([]KmerLCSResult, n)
|
|
for q := 0; q < 3; q++ { // reuse the same preparation for several queries
|
|
query := randomKmer(rng, k)
|
|
if !prep.Align(query, out) {
|
|
t.Fatalf("k=%d n=%d: Align refused", k, n)
|
|
}
|
|
for i, p := range partners {
|
|
if want := KmerLCS(query, p, k); out[i] != want {
|
|
t.Fatalf("k=%d n=%d partner %d: got %+v, want %+v", k, n, i, out[i], want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if NewKmerLCSPartners(nil, 0) != nil || NewKmerLCSPartners(nil, 32) != nil {
|
|
t.Fatal("invalid k must give nil")
|
|
}
|
|
}
|
|
|
|
// All-pairs scenario: many queries against the same partners.
|
|
func benchAllPairs(b *testing.B, k int, prepared bool) {
|
|
rng := rand.New(rand.NewSource(7))
|
|
partners := make([]uint64, 1024)
|
|
for i := range partners {
|
|
partners[i] = randomKmer(rng, k)
|
|
}
|
|
queries := make([]uint64, 64)
|
|
for i := range queries {
|
|
queries[i] = randomKmer(rng, k)
|
|
}
|
|
out := make([]KmerLCSResult, len(partners))
|
|
prep := NewKmerLCSPartners(partners, k)
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
for _, q := range queries {
|
|
if prepared {
|
|
prep.Align(q, out)
|
|
} else {
|
|
KmerLCSBatch(q, partners, k, out)
|
|
}
|
|
}
|
|
}
|
|
b.ReportMetric(float64(b.Elapsed().Nanoseconds())/float64(b.N*len(partners)*len(queries)), "ns/pair")
|
|
}
|
|
|
|
func BenchmarkAllPairsBatch21(b *testing.B) { benchAllPairs(b, 21, false) }
|
|
func BenchmarkAllPairsPrepared21(b *testing.B) { benchAllPairs(b, 21, true) }
|
|
func BenchmarkAllPairsBatch11(b *testing.B) { benchAllPairs(b, 11, false) }
|
|
func BenchmarkAllPairsPrepared11(b *testing.B) { benchAllPairs(b, 11, true) }
|