Introduce new sequence matching logic and corpus handling

This commit introduces the core matching functionality, including parallel corpus loading, the `Corpus` data structure, and implementations for `matchNaive` and `matchApat`. It also defines new matching rules, variant handling, and comprehensive unit and integration tests to validate the new logic and parameter handling.
This commit is contained in:
Eric Coissac committed 2026-11-03 22:35:51 +11:00
1 parent f75e774bd5
commit ad065f6853
10 files changed
+2265 -59

No files matched your search

+51
View File
@@ -250,3 +250,54 @@ suffisait. **Corrigé** (accord de l'utilisateur) : `globalLockerCounter` est un
Suite : étape 3 (recherche approchée des mots stricts, <= e substitutions, deux brins, dans exemples Suite : étape 3 (recherche approchée des mots stricts, <= e substitutions, deux brins, dans exemples
et contre-exemples), puis paires et indices. et contre-exemples), puis paires et indices.
## 11. Mode A, étape 3 : recherche approchée (2026-11-03)
Code : `pkg/obiprimer/corpus.go` (`Corpus`, `LoadCorpus` : exemples + contre-exemples en mémoire, ordre
d'entrée conservé, ignorés comptés seulement, comme ecoPrimers), `match.go` (types `Match`,
`WordMatches`, `MatchParams`, harnais parallèle `matchCorpus` indépendant du nombre de workers),
`match_naive.go` (oracle en Go pur), `match_apat.go` (variante `obiapat`). `FindStrictWords` travaille
maintenant sur un `Corpus`.
**Règle d'appariement** (celle d'ecoPrimers, figée dans `MatchParams`) : substitutions seules ; toute base
hors A, C, G, T (U lu comme T) dans la séquence compte comme une erreur ; toutes les fenêtres à <= e
erreurs sont rapportées, chevauchantes comprises ; fenêtres à cheval sur la jonction si circulaire.
**`obiapat` ne suit pas cette règle de lui-même** : il lit les codes IUPAC dans la séquence (un N
correspond à toutes les bases). La variante `obiapat` remplace donc les symboles ambigus par `z` (rien
ne lui correspond), sur une copie, seulement pour les séquences qui en contiennent.
Vérification de justesse :
- oracle validé à la main sur 12 cas (brins, N, IUPAC, U, jonction, chevauchement, séquence trop courte) ;
- `obiapat` == oracle position par position et nombre d'erreurs compris, 6 configurations (k = 12, 18,
25 ; e = 0 à 4 ; linéaire/circulaire ; séquences avec n, r, y, k, m ; mots plantés sur les deux brins
avec 0 à 3 substitutions et à la jonction) ;
- **bout en bout** : pour chaque mot strict, nombre de séquences exemples touchées == celui que
ecoPrimers a journalisé (`e = 3`) : **1 207 mots Cyprinidae et 255 mots Teleostei, 0 différence**.
Les nombres d'ecoPrimers sont dans `testdata/ecoprimers_*.words.tsv` ; test opt-in
`TestMatchApatAgainstEcoPrimers` (`OBIPRIMERS_TUTORIAL`, `OBIPRIMERS_WORKERS=n` pour fixer les cœurs).
(Cyprinidae : 1 207 des 1 208 mots d'ecoPrimers, le mot en trop de la section 10 est exclu.)
Mesures, `e = 3`, génomes circulaires, M3 Max :
| Jeu | Mots x séquences | Matches | 16 workers | 1 worker | Mémoire ajoutée |
|---|---|---|---|---|---|
| Cyprinidae | 1 207 x 325 (5,4 Mb) | 416 386 | 3,8 s | non mesuré | 53 Mo |
| Teleostei | 255 x 3 909 (65 Mb) | 999 407 | 10,7 s | **113 s** (293 Mb/s par passe mot-brin) | 151 Mo |
ecoPrimers complet (non décomposé par étape) : 134 s et 350 s, 1,9 et 2,1 Go.
Profil monothread (Teleostei, `go tool pprof`) : 74 % dans `runtime.cgocall` (le balayage C), 11 % en
surcoût de transition cgo (`pthread_cond_signal`), tri/GC marginaux. Il y a 2 millions d'appels cgo
(255 mots x 2 brins x 3 909 séquences).
Variantes du protocole :
1. `obiapat` mot par mot, parallélisé par séquence : **retenue comme référence de production** pour l'instant.
2. Arrêt anticipé sous le quorum de 90 % : **écartée, mesuré inutile** (Teleostei : 255/255 mots
conservés ; Cyprinidae : 1 201/1 208). Le parallélisme par séquence est déjà dans la variante 1.
3. Bit-parallèle multi-mots en Go (SIMD) : **non écrite**. Cible : les ~74 % de balayage C.
4. Index de voisinage / Aho-Corasick : écartée pour e >= 2 par le calcul (graines de 4 à 5 bases,
~250 000 candidats par graine et par mot sur 65 Mb) ; `obicorazick` reste pertinent pour e = 0.
Non mesurée.
Décision en suspens : écrire la variante 3 maintenant, ou d'abord finir le pipeline (paires et indices)
et profiler l'ensemble pour savoir si l'appariement domine encore.
+108
View File
@@ -0,0 +1,108 @@
package obiprimer
import (
"cmp"
"fmt"
"slices"
"sync"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq"
)
// Counts gives the number of sequences by role.
type Counts struct {
Examples int
CounterExamples int
Ignored int
}
// Corpus is the set of sequences used by the primer search: the examples and
// the counterexamples, in input order. Sequences with the Ignored role are not
// kept (as in ecoPrimers), only counted.
//
// The primer search needs several passes over the sequences (words are
// searched approximately in every one of them), so the corpus lives in memory.
type Corpus struct {
Seqs []*obiseq.BioSequence
Roles []Role // Roles[i] is Example or CounterExample
Ignored int // number of sequences dropped by the selection
}
// Counts returns the number of sequences by role.
func (c *Corpus) Counts() Counts {
n := Counts{Ignored: c.Ignored}
for _, r := range c.Roles {
if r == Example {
n.Examples++
} else {
n.CounterExamples++
}
}
return n
}
type corpusEntry struct {
batch, index int
seq *obiseq.BioSequence
role Role
}
// LoadCorpus reads every sequence of seqs, once, and keeps those selected as
// examples or counterexamples. The order of the input is preserved whatever
// the number of reader goroutines.
func LoadCorpus(seqs obiiter.IBioSequence, sel Selection) (*Corpus, error) {
var mu sync.Mutex
var entries []corpusEntry
ignored := 0
consumer := func(it obiiter.IBioSequence, wg *sync.WaitGroup) {
defer wg.Done()
var local []corpusEntry
localIgnored := 0
for it.Next() {
batch := it.Get()
for i, seq := range batch.Slice() {
if role := sel.Role(seq); role == Ignored {
localIgnored++
} else {
local = append(local, corpusEntry{batch.Order(), i, seq, role})
}
}
}
mu.Lock()
entries = append(entries, local...)
ignored += localIgnored
mu.Unlock()
}
var wg sync.WaitGroup
for i := 1; i < obidefault.ParallelWorkers(); i++ {
wg.Add(1)
go consumer(seqs.Split(), &wg)
}
wg.Add(1)
go consumer(seqs, &wg)
wg.Wait()
slices.SortFunc(entries, func(a, b corpusEntry) int {
if c := cmp.Compare(a.batch, b.batch); c != 0 {
return c
}
return cmp.Compare(a.index, b.index)
})
c := &Corpus{
Seqs: make([]*obiseq.BioSequence, len(entries)),
Roles: make([]Role, len(entries)),
Ignored: ignored,
}
for i, e := range entries {
c.Seqs[i], c.Roles[i] = e.seq, e.role
}
if c.Counts().Examples == 0 {
return nil, fmt.Errorf("obiprimer: no example sequence selected")
}
return c, nil
}
+156
View File
@@ -0,0 +1,156 @@
package obiprimer
import (
"cmp"
"fmt"
"slices"
"sync"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq"
)
// Match is one approximate occurrence of a word in a corpus sequence.
//
// Pos is the 0-based start of the window on the sequence as given (forward
// coordinates). When Reverse is false the window reads like the word; when it
// is true the window reads like the reverse complement of the word, i.e. the
// word occurs on the other strand. Errors is the number of substitutions.
type Match struct {
Seq uint32 // index in the Corpus
Pos uint32
Errors uint8
Reverse bool
}
// WordMatches gathers every occurrence of one canonical word, sorted by
// (Seq, Pos, Reverse).
type WordMatches struct {
Word uint64
Matches []Match
}
// MatchParams parameterizes the approximate search.
//
// Rules, those of ecoPrimers: only substitutions are allowed; a base other
// than A, C, G, T (or U, read as T) in the sequence never matches, it counts as
// one error; every window with at most MaxErrors errors is reported, windows
// overlapping each other included; on circular sequences windows may span the
// junction.
type MatchParams struct {
K int
MaxErrors int
Circular bool
}
func (p MatchParams) validate() error {
if p.K < 2 || p.K > 31 {
return fmt.Errorf("obiprimer: word size must be between 2 and 31, got %d", p.K)
}
if p.MaxErrors < 0 || p.MaxErrors >= p.K {
return fmt.Errorf("obiprimer: error count must be in [0,%d[, got %d", p.K, p.MaxErrors)
}
return nil
}
// hit is a Match before its sequence index is known to be final.
type hit struct {
word int
m Match
}
// scanFunc finds every occurrence of every word, on both strands, in one
// sequence and reports them through emit.
type scanFunc func(seq *obiseq.BioSequence, emit func(word, pos, errors int, reverse bool))
// scannerMaker returns an independent scanFunc; each worker gets its own so
// that scanners can keep private buffers.
type scannerMaker func() scanFunc
// wordStrings returns, for each canonical word, the word and its reverse
// complement as lowercase strings.
func wordStrings(words []uint64, k int) (fwd, rev []string) {
fwd = make([]string, len(words))
rev = make([]string, len(words))
for i, w := range words {
fwd[i] = string(obikmer.DecodeKmer(w, k, nil))
rev[i] = string(obikmer.DecodeKmer(obikmer.ReverseComplement(w, k), k, nil))
}
return
}
// matchCorpus runs the scanners over every sequence of the corpus in parallel
// and assembles the per-word results. The output does not depend on the number
// of workers.
func matchCorpus(c *Corpus, words []uint64, mk scannerMaker) []WordMatches {
perSeq := make([][]hit, len(c.Seqs))
jobs := make(chan int)
var wg sync.WaitGroup
for range obidefault.ParallelWorkers() {
wg.Add(1)
go func() {
defer wg.Done()
scan := mk()
for i := range jobs {
var hits []hit
scan(c.Seqs[i], func(word, pos, errors int, reverse bool) {
hits = append(hits, hit{word, Match{uint32(i), uint32(pos), uint8(errors), reverse}})
})
perSeq[i] = hits
}
}()
}
for i := range c.Seqs {
jobs <- i
}
close(jobs)
wg.Wait()
res := make([]WordMatches, len(words))
for i, w := range words {
res[i].Word = w
}
for _, hits := range perSeq {
for _, h := range hits {
res[h.word].Matches = append(res[h.word].Matches, h.m)
}
}
for i := range res {
slices.SortFunc(res[i].Matches, func(a, b Match) int {
if c := cmp.Compare(a.Seq, b.Seq); c != 0 {
return c
}
if c := cmp.Compare(a.Pos, b.Pos); c != 0 {
return c
}
if a.Reverse != b.Reverse {
if !a.Reverse {
return -1
}
return 1
}
return 0
})
}
return res
}
// SequencesMatched returns, for the matches of one word, the number of
// distinct example and counterexample sequences it occurs in.
func (w WordMatches) SequencesMatched(c *Corpus) (examples, counterExamples int) {
last := ^uint32(0)
for _, m := range w.Matches {
if m.Seq == last {
continue
}
last = m.Seq
if c.Roles[m.Seq] == Example {
examples++
} else {
counterExamples++
}
}
return
}
+87
View File
@@ -0,0 +1,87 @@
package obiprimer
import (
"fmt"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiapat"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq"
)
// apatSafe returns seq when it only holds a, c, g, t; otherwise a copy where u
// becomes t and every other symbol becomes z. obiapat reads IUPAC codes in the
// sequence (an n matches every base); z is the symbol it matches with nothing,
// which gives back the ecoPrimers rule "an ambiguous base is an error".
func apatSafe(seq *obiseq.BioSequence) *obiseq.BioSequence {
raw := seq.Sequence()
clean := true
for _, b := range raw {
if b != 'a' && b != 'c' && b != 'g' && b != 't' {
clean = false
break
}
}
if clean {
return seq
}
out := make([]byte, len(raw))
for i, b := range raw {
switch b {
case 'a', 'c', 'g', 't':
out[i] = b
case 'A', 'C', 'G', 'T':
out[i] = b + 'a' - 'A'
case 'u', 'U':
out[i] = 't'
default:
out[i] = 'z'
}
}
return obiseq.NewBioSequence(seq.Id(), out, "")
}
// matchApat searches the words with obiapat, the bit-parallel Baeza-Yates /
// Manber matcher of OBITools (the engine of ecoPrimers and obipcr).
func matchApat(c *Corpus, words []uint64, p MatchParams) ([]WordMatches, error) {
if err := p.validate(); err != nil {
return nil, err
}
fwd, rev := wordStrings(words, p.K)
pats := make([][2]obiapat.ApatPattern, len(words))
for i := range words {
var err error
if pats[i][0], err = obiapat.MakeApatPattern(fwd[i], p.MaxErrors, false); err != nil {
return nil, fmt.Errorf("obiprimer: pattern %s: %w", fwd[i], err)
}
if pats[i][1], err = obiapat.MakeApatPattern(rev[i], p.MaxErrors, false); err != nil {
return nil, fmt.Errorf("obiprimer: pattern %s: %w", rev[i], err)
}
}
mk := func() scanFunc {
var recycle []obiapat.ApatSequence
return func(seq *obiseq.BioSequence, emit func(word, pos, errors int, reverse bool)) {
n := seq.Len()
if n < p.K {
return
}
aseq, err := obiapat.MakeApatSequence(apatSafe(seq), p.Circular, recycle...)
if err != nil {
panic(fmt.Sprintf("obiprimer: cannot encode sequence %s: %v", seq.Id(), err))
}
recycle = append(recycle[:0], aseq)
for w := range pats {
for strand := range 2 {
for _, m := range pats[w][strand].FindAllIndex(aseq, 0, -1) {
// On circular sequences obiapat also scans a copy of the
// start: those hits repeat the ones at start-n.
if m[0] >= 0 && m[0] < n {
emit(w, m[0], m[2], strand == 1)
}
}
}
}
}
}
return matchCorpus(c, words, mk), nil
}
+78
View File
@@ -0,0 +1,78 @@
package obiprimer
import "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq"
// baseCode maps a nucleotide to 0..3 (A,C,G,T; U is T) and everything else to 4,
// which never equals a word base.
var baseCode = func() (t [256]byte) {
for i := range t {
t[i] = 4
}
for i, c := range "ACGT" {
t[c], t[c+'a'-'A'] = byte(i), byte(i)
}
t['U'], t['u'] = 3, 3
return
}()
func codesOf(s string) []byte {
out := make([]byte, len(s))
for i := range out {
out[i] = baseCode[s[i]]
}
return out
}
// matchNaive is the reference implementation: every window of every sequence
// is compared with every word, base by base. It is slow and exists to define
// the exact semantics (see MatchParams) that faster implementations must
// reproduce.
func matchNaive(c *Corpus, words []uint64, p MatchParams) ([]WordMatches, error) {
if err := p.validate(); err != nil {
return nil, err
}
fwd, rev := wordStrings(words, p.K)
fc, rc := make([][]byte, len(words)), make([][]byte, len(words))
for i := range words {
fc[i], rc[i] = codesOf(fwd[i]), codesOf(rev[i])
}
mk := func() scanFunc {
var codes []byte
return func(seq *obiseq.BioSequence, emit func(word, pos, errors int, reverse bool)) {
raw := seq.Sequence()
n := len(raw)
if n < p.K {
return
}
codes = codes[:0]
for _, b := range raw {
codes = append(codes, baseCode[b])
}
windows := n - p.K + 1
if p.Circular {
codes = append(codes, codes[:p.K-1]...)
windows = n
}
for pos := range windows {
win := codes[pos : pos+p.K]
for w := range words {
for strand, pat := range [2][]byte{fc[w], rc[w]} {
errs := 0
for j, b := range pat {
if win[j] != b {
if errs++; errs > p.MaxErrors {
break
}
}
}
if errs <= p.MaxErrors {
emit(w, pos, errs, strand == 1)
}
}
}
}
}
}
return matchCorpus(c, words, mk), nil
}
+285
View File
@@ -0,0 +1,285 @@
package obiprimer
import (
"bufio"
"fmt"
"math/rand"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
"time"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiformats"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq"
)
func corpusOf(roles []Role, seqs ...string) *Corpus {
c := &Corpus{Roles: roles}
for i, s := range seqs {
c.Seqs = append(c.Seqs, obiseq.NewBioSequence(fmt.Sprintf("s%d", i), []byte(s), ""))
}
return c
}
func canon(word string) uint64 { return obikmer.EncodeCanonicalKmer([]byte(word), len(word)) }
// The reference implementation is checked by hand first: everything else is
// compared with it.
func TestNaiveMatchSemantics(t *testing.T) {
const word = "AACCGGTA" // not a palindrome; reverse complement TACCGGTT
cases := []struct {
name string
seq string
circular bool
e int
want []Match
}{
{"exact forward", "ggaaccggtacc", false, 0, []Match{{0, 2, 0, false}}},
{"exact reverse strand", "ggtaccggttcc", false, 0, []Match{{0, 2, 0, true}}},
{"one error is not found at e=0", "ggaaccgctacc", false, 0, nil},
{"one error found at e=1 with its count", "ggaaccgctacc", false, 1, []Match{{0, 2, 1, false}}},
{"an N is an error", "ggaaccgntacc", false, 1, []Match{{0, 2, 1, false}}},
{"an N is never a match at e=0", "ggaaccgntacc", false, 0, nil},
{"an IUPAC code is an error, not a wildcard", "ggaaccgrtacc", false, 0, nil},
{"U reads as T", "ggaaccgguacc", false, 0, []Match{{0, 2, 0, false}}},
{"junction ignored on a linear sequence", "ccggtagggaa", false, 0, nil},
{"junction found on a circular sequence", "ccggtagggaa", true, 0, []Match{{0, 9, 0, false}}},
{"overlapping windows are all reported", "aaccggtaaccggta", false, 0, []Match{{0, 0, 0, false}, {0, 7, 0, false}}},
{"too short", "aaccgg", true, 3, nil},
}
for _, tc := range cases {
c := corpusOf([]Role{Example}, tc.seq)
got, err := matchNaive(c, []uint64{canon(word)}, MatchParams{K: len(word), MaxErrors: tc.e, Circular: tc.circular})
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got[0].Matches, tc.want) {
t.Errorf("%s: got %v, want %v", tc.name, got[0].Matches, tc.want)
}
}
}
func TestMatchParamsValidation(t *testing.T) {
c := corpusOf([]Role{Example}, "acgtacgtacgtacgt")
for _, p := range []MatchParams{{K: 1}, {K: 32}, {K: 8, MaxErrors: -1}, {K: 8, MaxErrors: 8}} {
if _, err := matchNaive(c, nil, p); err == nil {
t.Errorf("%+v must be rejected", p)
}
if _, err := matchApat(c, nil, p); err == nil {
t.Errorf("%+v must be rejected by obiapat variant", p)
}
}
}
func randSeq(rng *rand.Rand, n int, alphabet string) []byte {
b := make([]byte, n)
for i := range b {
b[i] = alphabet[rng.Intn(len(alphabet))]
}
return b
}
func mutate(rng *rand.Rand, s []byte, n int) []byte {
out := append([]byte(nil), s...)
for _, i := range rng.Perm(len(out))[:n] {
out[i] = "acgt"[rng.Intn(4)]
}
return out
}
// randomCorpus builds sequences with words planted on both strands, with 0..3
// substitutions, across the circular junction, and with ambiguity codes.
func randomCorpus(rng *rand.Rand, words []string, nseq int) *Corpus {
c := &Corpus{}
for i := 0; i < nseq; i++ {
alphabet := "acgt"
if i%3 == 0 {
alphabet = "acgtacgtacgtacgtacgtnnrykm"
}
s := randSeq(rng, 30+rng.Intn(500), alphabet)
for range 1 + rng.Intn(4) {
w := []byte(words[rng.Intn(len(words))])
if rng.Intn(2) == 0 {
w = []byte(revcomp(string(w)))
}
w = mutate(rng, []byte(string(w)), rng.Intn(4))
for j := range w {
w[j] += 'a' - 'A'
}
at := rng.Intn(len(s) - len(w) + 1)
if rng.Intn(5) == 0 { // across the junction
s = append(s[:0:0], append(append([]byte(nil), w[len(w)/2:]...), append(s[:len(s)-len(w)/2], w[:len(w)/2]...)...)...)
continue
}
copy(s[at:], w)
}
c.Seqs = append(c.Seqs, obiseq.NewBioSequence(fmt.Sprintf("r%d", i), s, ""))
c.Roles = append(c.Roles, Example)
}
return c
}
// Every implementation must return exactly what the reference returns.
func TestMatchersAgreeWithReference(t *testing.T) {
for _, tc := range []struct {
k, e int
circular bool
}{{18, 0, false}, {18, 1, true}, {18, 3, false}, {18, 3, true}, {12, 2, true}, {25, 4, false}} {
t.Run(fmt.Sprintf("k%d-e%d-circ%v", tc.k, tc.e, tc.circular), func(t *testing.T) {
rng := rand.New(rand.NewSource(int64(tc.k*100 + tc.e)))
var wordStr []string
var words []uint64
for range 7 {
w := string(randSeq(rng, tc.k, "ACGT"))
wordStr = append(wordStr, w)
words = append(words, canon(w))
}
c := randomCorpus(rng, wordStr, 40)
p := MatchParams{K: tc.k, MaxErrors: tc.e, Circular: tc.circular}
want, err := matchNaive(c, words, p)
if err != nil {
t.Fatal(err)
}
total := 0
for _, w := range want {
total += len(w.Matches)
}
if total < 10 {
t.Fatalf("test data too poor: only %d reference matches", total)
}
got, err := matchApat(c, words, p)
if err != nil {
t.Fatal(err)
}
for i := range want {
if !reflect.DeepEqual(got[i].Matches, want[i].Matches) {
t.Fatalf("word %d: obiapat gives %v\nreference gives %v", i, got[i].Matches, want[i].Matches)
}
}
})
}
}
func TestSequencesMatched(t *testing.T) {
c := corpusOf([]Role{Example, Example, CounterExample, CounterExample}, "a", "a", "a", "a")
w := WordMatches{Matches: []Match{{0, 1, 0, false}, {0, 9, 0, true}, {1, 3, 0, false}, {3, 0, 0, false}}}
if e, x := w.SequencesMatched(c); e != 2 || x != 1 {
t.Errorf("got %d examples, %d counterexamples", e, x)
}
}
func readEcoCounts(t *testing.T, path string) map[string]int {
t.Helper()
f, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
counts := map[string]int{}
sc := bufio.NewScanner(f)
for sc.Scan() {
var w string
var in, out int
if _, err := fmt.Sscanf(sc.Text(), "%s\t%d\t%d", &w, &in, &out); err != nil {
t.Fatal(err)
}
counts[w] = in
}
return counts
}
// Opt-in comparison with ecoPrimers on the tutorial data (same variable as the
// strict word test). For every strict word, ecoPrimers logs the number of
// example sequences in which the fuzzy search (-e 3, both strands, circular)
// finds it; testdata/ecoprimers_*.words.tsv holds those numbers.
func TestMatchApatAgainstEcoPrimers(t *testing.T) {
dir := os.Getenv("OBIPRIMERS_TUTORIAL")
if dir == "" {
t.Skip("OBIPRIMERS_TUTORIAL not set")
}
if v := os.Getenv("OBIPRIMERS_WORKERS"); v != "" { // e.g. 1, to time a single core
n, err := strconv.Atoi(v)
if err != nil {
t.Fatal(err)
}
obidefault.SetMaxCPU(n)
}
taxonomy, err := obiformats.LoadTaxonomy(filepath.Join(dir, "ncbitaxo_20250211.tgz"), false, false)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct{ name, taxid string }{{"Cyprinidae", "7953"}, {"Teleostei", "32443"}} {
t.Run(tc.name, func(t *testing.T) {
restrict, err := BelongingTo(taxonomy, []string{tc.taxid})
if err != nil {
t.Fatal(err)
}
seqs, err := obiformats.ReadSequencesFromFile(filepath.Join(dir, "mito.vert.fasta.gz"))
if err != nil {
t.Fatal(err)
}
corpus, err := LoadCorpus(seqs, Selection{Restrict: restrict})
if err != nil {
t.Fatal(err)
}
sw, err := FindStrictWords(corpus, WordSearch{PrimerLength: 18, Quorum: 0.7, Circular: true, TmpDir: t.TempDir()})
if err != nil {
t.Fatal(err)
}
words := make([]uint64, len(sw.Words))
for i, w := range sw.Words {
words[i] = w.Kmer
}
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
start := time.Now()
res, err := matchApat(corpus, words, MatchParams{K: 18, MaxErrors: 3, Circular: true})
elapsed := time.Since(start)
runtime.ReadMemStats(&after)
if err != nil {
t.Fatal(err)
}
nm := 0
for _, r := range res {
nm += len(r.Matches)
}
bases := 0
for _, s := range corpus.Seqs {
bases += s.Len()
}
t.Logf("obiapat: %d words x %d sequences (%.1f Mb) e=3 in %s (%.0f Mb/s per word-strand pass); %d matches; heap +%.0f MB, allocated %.0f MB",
len(words), len(corpus.Seqs), float64(bases)/1e6, elapsed.Round(time.Millisecond),
float64(bases)*float64(len(words))*2/1e6/elapsed.Seconds(), nm,
float64(after.HeapAlloc-before.HeapAlloc)/1e6, float64(after.TotalAlloc-before.TotalAlloc)/1e6)
eco := readEcoCounts(t, filepath.Join("testdata", "ecoprimers_"+tc.name+".words.tsv"))
compared, differ := 0, 0
for i, r := range res {
w := strings.ToUpper(string(obikmer.DecodeKmer(words[i], 18, nil)))
want, ok := eco[w]
if !ok {
t.Errorf("word %s is not in the ecoPrimers log", w)
continue
}
got, _ := r.SequencesMatched(corpus)
compared++
if got != want {
differ++
t.Errorf("word %s: %d example sequences, ecoPrimers %d", w, got, want)
}
}
t.Logf("%d words compared with ecoPrimers, %d differ", compared, differ)
})
}
}
File diff suppressed because it is too large. Load diff
+255
View File
@@ -0,0 +1,255 @@
AAAACTCGTGCCAGCCAC 3891 0
AAACCCAAAGGACTTGGC 3890 0
AAACGAACCCTTAATAGC 3875 0
AAACTCGTGCCAGCCACC 3894 0
AAACTGGGATTAGATACC 3896 0
AAAGTCCTACGTGATCTG 3901 0
AAATCATGATGCAAAAGG 3894 0
AACAAACGAACCCTTAAT 3888 0
AACATCGAGGTCGTAAAC 3893 0
AACATGGTAAGTGTACCG 3894 0
AACCAAAAACTCTTGGTG 3854 0
AACCCAAAGGACTTGGCG 3888 0
AACCCTTAATAGCGGCTG 3858 0
AACCGCGGTGGCTGGCAC 3896 0
AACGAACCCTTAATAGCG 3860 0
AACGATTAAAGTCCTACG 3700 0
AACGTCAGGTCGAGGTGT 3894 0
AACTCAGATCACGTAGGA 3899 0
AACTCGTGCCAGCCACCG 3893 0
AACTGGGATTAGATACCC 3896 0
AAGAGTTTTTGGTTCCTA 3848 0
AAGCCTCGCCTGTTTACC 3876 0
AAGGGTTCGTTTGTTCAA 3889 0
AAGGTAGCGCAATCACTT 3889 0
AAGTCCTACGTGATCTGA 3900 0
AAGTCGTAACATGGTAAG 3893 0
AAGTGCACCTTCCGGTAC 3888 0
AAGTGTACCGGAAGGTGC 3889 0
AATAGCGGCTGCACCATT 3839 0
AATCACTTGTCTTTTAAA 3856 0
AATCATGATGCAAAAGGT 3893 0
AATCCAGGTCAGTTTCTA 3903 0
AATCGTTGAACAAACGAA 3876 0
AATGTAGCCCATTTCTTC 3871 0
ACAAACGAACCCTTAATA 3885 0
ACAAACTGGGATTAGATA 3893 0
ACAAGTGATTGCGCTACC 3889 0
ACACACCGCCCGTCACTC 3891 0
ACACCGCCCGTCACTCTC 3890 0
ACACTTACCATGTTACGA 3893 0
ACATCCTAATGGTGCAGC 3858 0
ACATCGAGGTCGTAAACC 3894 0
ACATGGTAAGTGTACCGG 3894 0
ACCAAAAACTCTTGGTGC 3875 0
ACCAAGAGTTTTTGGTTC 3857 0
ACCATGTTACGACTTGCC 3891 0
ACCATTAGGATGTCCTGA 3891 0
ACCCAAAGGACTTGGCGG 3886 0
ACCCGTCTCTGTGGCAAA 3892 0
ACCCTAGGGATAACAGCG 3895 0
ACCCTTAATAGCGGCTGC 3861 0
ACCGCCCGTCACTCTCCC 3893 0
ACCGCGGTGGCTGGCACG 3895 0
ACCGGAAGGTGCACTTGG 3887 0
ACCTCGATGTTGGATCAG 3891 0
ACCTTCCGGTACACTTAC 3893 0
ACGAACCCTTAATAGCGG 3859 0
ACGACCTCGATGTTGGAT 3889 0
ACGATTAAAGTCCTACGT 3701 0
ACGTCAGGTCGAGGTGTA 3894 0
ACGTGATCTGAGTTCAGA 3901 0
ACTCAGATCACGTAGGAC 3899 0
ACTCGTGCCAGCCACCGC 3894 0
ACTCTTTTGCCACAGAGA 3867 0
ACTGGGATTAGATACCCC 3896 0
ACTTACCATGTTACGACT 3894 0
ACTTTAATCGTTGAACAA 3876 0
AGAACAGGCTCCTCTAGG 3893 0
AGACAAGTGATTGCGCTA 3883 0
AGAGGAGCCTGTTCTAGA 3876 0
AGAGTGACGGGCGGTGTG 3890 0
AGAGTTTTTGGTTCCTAA 3851 0
AGATAGAAACTGACCTGG 3900 0
AGATCACGTAGGACTTTA 3901 0
AGCCGCTATTAAGGGTTC 3856 0
AGCCTCGCCTGTTTACCA 3889 0
AGCCTGTTCTAGAACCGA 3871 0
AGCGGCTGCACCATTAGG 3846 0
AGGAACCAAAAACTCTTG 3842 0
AGGACATCCTAATGGTGC 3866 0
AGGACTTTAATCGTTGAA 3688 0
AGGAGCCTGTTCTAGAAC 3874 0
AGGATGTCCTGATCCAAC 3897 0
AGGCAAGTCGTAACATGG 3868 0
AGGCGATGTTTTTGGTAA 3894 0
AGGGATAACAGCGCAATC 3896 0
AGGGGAGGCAAGTCGTAA 3890 0
AGGGGGTTTACGACCTCG 3894 0
AGGGTTCGTTTGTTCAAC 3888 0
AGGTAGCGCAATCACTTG 3888 0
AGTCCTACGTGATCTGAG 3900 0
AGTGGGGTATCTAATCCC 3896 0
AGTTACCCTAGGGATAAC 3895 0
AGTTTTTGGTTCCTAAGA 3848 0
ATAGAAACTGACCTGGAT 3901 0
ATAGCGGCTGCACCATTA 3837 0
ATAGTGGGGTATCTAATC 3895 0
ATCACGTAGGACTTTAAT 3880 0
ATCACTTGTCTTTTAAAT 3856 0
ATCAGGACATCCTAATGG 3892 0
ATCATGATGCAAAAGGTA 3894 0
ATCCTAATGGTGCAGCCG 3852 0
ATCGAGGTCGTAAACCCC 3894 0
ATCGGTTCTAGAACAGGC 3862 0
ATCGTTGAACAAACGAAC 3877 0
ATCTGAGTTCAGACCGGA 3902 0
ATGATGCAAAAGGTACGA 3891 0
ATGGTAAGTGTACCGGAA 3895 0
ATGGTGCAGCCGCTATTA 3840 0
ATGTAGCCCATTTCTTCC 3871 0
ATGTCCTGATCCAACATC 3892 0
ATGTTACGACTTGCCTCC 3864 0
ATGTTGGATCAGGACATC 3893 0
ATGTTTTTGGTAAACAGG 3887 0
ATTAGATACCCCACTATG 3896 0
ATTAGGATGTCCTGATCC 3893 0
ATTGCGCTGTTATCCCTA 3896 0
CAAAAACTCTTGGTGCAA 3886 0
CAAACGAACCCTTAATAG 3882 0
CAAACTGGGATTAGATAC 3895 0
CAACATCGAGGTCGTAAA 3894 0
CAACGATTAAAGTCCTAC 3691 0
CAAGTCGTAACATGGTAA 3893 0
CAAGTGCACCTTCCGGTA 3888 0
CACCATTAGGATGTCCTG 3890 0
CACCGCCCGTCACTCTCC 3888 0
CACCTTCCGGTACACTTA 3891 0
CACGTAGGACTTTAATCG 3702 0
CACTCTTTTGCCACAGAG 3856 0
CACTTACCATGTTACGAC 3894 0
CATCCTAATGGTGCAGCC 3852 0
CATCGAGGTCGTAAACCC 3894 0
CATGATGCAAAAGGTACG 3890 0
CATGGTAAGTGTACCGGA 3894 0
CATGTTACGACTTGCCTC 3866 0
CATTAGGATGTCCTGATC 3893 0
CCAAAAACTCTTGGTGCA 3883 0
CCAACATCGAGGTCGTAA 3894 0
CCAAGAGTTTTTGGTTCC 3844 0
CCACTCTTTTGCCACAGA 3879 0
CCCACTCTTTTGCCACAG 3879 0
CCCGTCTCTGTGGCAAAA 3894 0
CCCTAGGGATAACAGCGC 3898 0
CCCTTAATAGCGGCTGCA 3853 0
CCGCCCGTCACTCTCCCC 3890 0
CCGCGGTGGCTGGCACGA 3895 0
CCGGTAAAACTCGTGCCA 3847 0
CCGGTCTGAACTCAGATC 3904 0
CCGTCTCTGTGGCAAAAG 3894 0
CCTACGTGATCTGAGTTC 3899 0
CCTAGGGATAACAGCGCA 3896 0
CCTCGATGTTGGATCAGG 3892 0
CCTCGCCTGTTTACCAAA 3892 0
CCTGTTCTAGAACCGATA 3862 0
CCTTAATAGCGGCTGCAC 3850 0
CCTTCCGGTACACTTACC 3893 0
CGAACCCTTAATAGCGGC 3855 0
CGACCTCGATGTTGGATC 3890 0
CGACGGCGGTATATAGGC 3890 0
CGATGTTGGATCAGGACA 3892 0
CGATGTTTTTGGTAAACA 3886 0
CGCCTGTTTACCAAAAAC 3889 0
CGCGGTGGCTGGCACGAG 3894 0
CGCTTAGCTGTTAACTAA 3864 0
CGGTAAAACTCGTGCCAG 3856 0
CGGTCTGAACTCAGATCA 3904 0
CGGTTCTAGAACAGGCTC 3873 0
CGTAACATGGTAAGTGTA 3892 0
CGTCAGGTCGAGGTGTAG 3893 0
CGTCTCTGTGGCAAAAGA 3891 0
CGTGATCTGAGTTCAGAC 3902 0
CGTTGAACAAACGAACCC 3887 0
CGTTTGTTCAACGATTAA 3877 0
CTAAATCATGATGCAAAA 3896 0
CTAATGGTGCAGCCGCTA 3840 0
CTACGTGATCTGAGTTCA 3899 0
CTAGAACAGGCTCCTCTA 3877 0
CTAGAGGAGCCTGTTCTA 3891 0
CTAGGGATAACAGCGCAA 3897 0
CTCCGGTCTGAACTCAGA 3903 0
CTCGATGTTGGATCAGGA 3892 0
CTCGCCTGTTTACCAAAA 3892 0
CTCGTACCTTTTGCATCA 3889 0
CTCTTTTGCCACAGAGAC 3869 0
CTGAACTCAGATCACGTA 3900 0
CTGCACCATTAGGATGTC 3861 0
CTGGGATTAGATACCCCA 3895 0
CTGTTATCCCTAGGGTAA 3897 0
CTGTTTACCAAAAACATC 3886 0
CTTAATAGCGGCTGCACC 3848 0
CTTAGCTGTTAACTAAGA 3878 0
CTTAGGAACCAAAAACTC 3847 0
CTTAGTTAACAGCTAAGC 3875 0
CTTCCGGTACACTTACCA 3891 0
CTTGGATTTGCACCAAGA 3899 0
CTTTAATCGTTGAACAAA 3876 0
CTTTTGCATCATGATTTA 3896 0
GAACAAACGAACCCTTAA 3890 0
GAAGGAACTCGGCAAACA 3889 0
GACAAGTGATTGCGCTAC 3889 0
GACCTCGATGTTGGATCA 3890 0
GACTTTAATCGTTGAACA 3686 0
GAGGAGCCTGTTCTAGAA 3876 0
GAGGCGATGTTTTTGGTA 3893 0
GATAGAAACTGACCTGGA 3900 0
GATCACGTAGGACTTTAA 3900 0
GATTAAAGTCCTACGTGA 3704 0
GCAAGTCGTAACATGGTA 3891 0
GCATAGTGGGGTATCTAA 3896 0
GCCGGTAAAACTCGTGCC 3827 0
GCCTCGCCTGTTTACCAA 3890 0
GCCTGTTTACCAAAAACA 3889 0
GCGATGTTTTTGGTAAAC 3888 0
GCGCTTAGCTGTTAACTA 3899 0
GCGGCTGCACCATTAGGA 3850 0
GCTAAATCATGATGCAAA 3893 0
GCTACACCTCGACCTGAC 3893 0
GCTGGCACGAGTTTTACC 3881 0
GCTGTTATCCCTAGGGTA 3896 0
GGACATCCTAATGGTGCA 3865 0
GGACTTTAATCGTTGAAC 3687 0
GGAGCCTGTTCTAGAACC 3874 0
GGATGTCCTGATCCAACA 3895 0
GGATTAGATACCCCACTA 3896 0
GGATTGCGCTGTTATCCC 3895 0
GGCGATGTTTTTGGTAAA 3890 0
GGCTGGCACGAGTTTTAC 3880 0
GGGAAGAAATGGGCTACA 3872 0
GGGAGGCAAGTCGTAACA 3891 0
GGGGAGGCAAGTCGTAAC 3890 0
GGGGGTTTACGACCTCGA 3894 0
GGTACACTTACCATGTTA 3893 0
GGTCTGAACTCAGATCAC 3902 0
GGTCTTAGGAACCAAAAA 3862 0
GGTTCGTTTGTTCAACGA 3877 0
GTAACATGGTAAGTGTAC 3893 0
GTACCTTTTGCATCATGA 3892 0
GTCCTGATCCAACATCGA 3892 0
GTCTTAGGAACCAAAAAC 3853 0
GTGGGGTATCTAATCCCA 3895 0
GTTACCCTAGGGATAACA 3896 0
GTTTGTTCAACGATTAAA 3877 0
TAAAACTCGTGCCAGCCA 3887 0
TAACCGCGGTGGCTGGCA 3895 0
TAAGGGTTCGTTTGTTCA 3889 0
TAATCGTTGAACAAACGA 3876 0
TACGACCTCGATGTTGGA 3892 0
TAGGACTTTAATCGTTGA 3689 0
TAGGATGTCCTGATCCAA 3897 0
TCCCACTCTTTTGCCACA 3866 0
TCGCCTGTTTACCAAAAA 3891 0
TGGATCAGGACATCCTAA 3894 0
TGGTCTTAGGAACCAAAA 3862 0
TGGTGCAGCCGCTATTAA 3847 0
TTGGTCTTAGGAACCAAA 3862 0
1 AAAACTCGTGCCAGCCAC 3891 0
2 AAACCCAAAGGACTTGGC 3890 0
3 AAACGAACCCTTAATAGC 3875 0
4 AAACTCGTGCCAGCCACC 3894 0
5 AAACTGGGATTAGATACC 3896 0
6 AAAGTCCTACGTGATCTG 3901 0
7 AAATCATGATGCAAAAGG 3894 0
8 AACAAACGAACCCTTAAT 3888 0
9 AACATCGAGGTCGTAAAC 3893 0
10 AACATGGTAAGTGTACCG 3894 0
11 AACCAAAAACTCTTGGTG 3854 0
12 AACCCAAAGGACTTGGCG 3888 0
13 AACCCTTAATAGCGGCTG 3858 0
14 AACCGCGGTGGCTGGCAC 3896 0
15 AACGAACCCTTAATAGCG 3860 0
16 AACGATTAAAGTCCTACG 3700 0
17 AACGTCAGGTCGAGGTGT 3894 0
18 AACTCAGATCACGTAGGA 3899 0
19 AACTCGTGCCAGCCACCG 3893 0
20 AACTGGGATTAGATACCC 3896 0
21 AAGAGTTTTTGGTTCCTA 3848 0
22 AAGCCTCGCCTGTTTACC 3876 0
23 AAGGGTTCGTTTGTTCAA 3889 0
24 AAGGTAGCGCAATCACTT 3889 0
25 AAGTCCTACGTGATCTGA 3900 0
26 AAGTCGTAACATGGTAAG 3893 0
27 AAGTGCACCTTCCGGTAC 3888 0
28 AAGTGTACCGGAAGGTGC 3889 0
29 AATAGCGGCTGCACCATT 3839 0
30 AATCACTTGTCTTTTAAA 3856 0
31 AATCATGATGCAAAAGGT 3893 0
32 AATCCAGGTCAGTTTCTA 3903 0
33 AATCGTTGAACAAACGAA 3876 0
34 AATGTAGCCCATTTCTTC 3871 0
35 ACAAACGAACCCTTAATA 3885 0
36 ACAAACTGGGATTAGATA 3893 0
37 ACAAGTGATTGCGCTACC 3889 0
38 ACACACCGCCCGTCACTC 3891 0
39 ACACCGCCCGTCACTCTC 3890 0
40 ACACTTACCATGTTACGA 3893 0
41 ACATCCTAATGGTGCAGC 3858 0
42 ACATCGAGGTCGTAAACC 3894 0
43 ACATGGTAAGTGTACCGG 3894 0
44 ACCAAAAACTCTTGGTGC 3875 0
45 ACCAAGAGTTTTTGGTTC 3857 0
46 ACCATGTTACGACTTGCC 3891 0
47 ACCATTAGGATGTCCTGA 3891 0
48 ACCCAAAGGACTTGGCGG 3886 0
49 ACCCGTCTCTGTGGCAAA 3892 0
50 ACCCTAGGGATAACAGCG 3895 0
51 ACCCTTAATAGCGGCTGC 3861 0
52 ACCGCCCGTCACTCTCCC 3893 0
53 ACCGCGGTGGCTGGCACG 3895 0
54 ACCGGAAGGTGCACTTGG 3887 0
55 ACCTCGATGTTGGATCAG 3891 0
56 ACCTTCCGGTACACTTAC 3893 0
57 ACGAACCCTTAATAGCGG 3859 0
58 ACGACCTCGATGTTGGAT 3889 0
59 ACGATTAAAGTCCTACGT 3701 0
60 ACGTCAGGTCGAGGTGTA 3894 0
61 ACGTGATCTGAGTTCAGA 3901 0
62 ACTCAGATCACGTAGGAC 3899 0
63 ACTCGTGCCAGCCACCGC 3894 0
64 ACTCTTTTGCCACAGAGA 3867 0
65 ACTGGGATTAGATACCCC 3896 0
66 ACTTACCATGTTACGACT 3894 0
67 ACTTTAATCGTTGAACAA 3876 0
68 AGAACAGGCTCCTCTAGG 3893 0
69 AGACAAGTGATTGCGCTA 3883 0
70 AGAGGAGCCTGTTCTAGA 3876 0
71 AGAGTGACGGGCGGTGTG 3890 0
72 AGAGTTTTTGGTTCCTAA 3851 0
73 AGATAGAAACTGACCTGG 3900 0
74 AGATCACGTAGGACTTTA 3901 0
75 AGCCGCTATTAAGGGTTC 3856 0
76 AGCCTCGCCTGTTTACCA 3889 0
77 AGCCTGTTCTAGAACCGA 3871 0
78 AGCGGCTGCACCATTAGG 3846 0
79 AGGAACCAAAAACTCTTG 3842 0
80 AGGACATCCTAATGGTGC 3866 0
81 AGGACTTTAATCGTTGAA 3688 0
82 AGGAGCCTGTTCTAGAAC 3874 0
83 AGGATGTCCTGATCCAAC 3897 0
84 AGGCAAGTCGTAACATGG 3868 0
85 AGGCGATGTTTTTGGTAA 3894 0
86 AGGGATAACAGCGCAATC 3896 0
87 AGGGGAGGCAAGTCGTAA 3890 0
88 AGGGGGTTTACGACCTCG 3894 0
89 AGGGTTCGTTTGTTCAAC 3888 0
90 AGGTAGCGCAATCACTTG 3888 0
91 AGTCCTACGTGATCTGAG 3900 0
92 AGTGGGGTATCTAATCCC 3896 0
93 AGTTACCCTAGGGATAAC 3895 0
94 AGTTTTTGGTTCCTAAGA 3848 0
95 ATAGAAACTGACCTGGAT 3901 0
96 ATAGCGGCTGCACCATTA 3837 0
97 ATAGTGGGGTATCTAATC 3895 0
98 ATCACGTAGGACTTTAAT 3880 0
99 ATCACTTGTCTTTTAAAT 3856 0
100 ATCAGGACATCCTAATGG 3892 0
101 ATCATGATGCAAAAGGTA 3894 0
102 ATCCTAATGGTGCAGCCG 3852 0
103 ATCGAGGTCGTAAACCCC 3894 0
104 ATCGGTTCTAGAACAGGC 3862 0
105 ATCGTTGAACAAACGAAC 3877 0
106 ATCTGAGTTCAGACCGGA 3902 0
107 ATGATGCAAAAGGTACGA 3891 0
108 ATGGTAAGTGTACCGGAA 3895 0
109 ATGGTGCAGCCGCTATTA 3840 0
110 ATGTAGCCCATTTCTTCC 3871 0
111 ATGTCCTGATCCAACATC 3892 0
112 ATGTTACGACTTGCCTCC 3864 0
113 ATGTTGGATCAGGACATC 3893 0
114 ATGTTTTTGGTAAACAGG 3887 0
115 ATTAGATACCCCACTATG 3896 0
116 ATTAGGATGTCCTGATCC 3893 0
117 ATTGCGCTGTTATCCCTA 3896 0
118 CAAAAACTCTTGGTGCAA 3886 0
119 CAAACGAACCCTTAATAG 3882 0
120 CAAACTGGGATTAGATAC 3895 0
121 CAACATCGAGGTCGTAAA 3894 0
122 CAACGATTAAAGTCCTAC 3691 0
123 CAAGTCGTAACATGGTAA 3893 0
124 CAAGTGCACCTTCCGGTA 3888 0
125 CACCATTAGGATGTCCTG 3890 0
126 CACCGCCCGTCACTCTCC 3888 0
127 CACCTTCCGGTACACTTA 3891 0
128 CACGTAGGACTTTAATCG 3702 0
129 CACTCTTTTGCCACAGAG 3856 0
130 CACTTACCATGTTACGAC 3894 0
131 CATCCTAATGGTGCAGCC 3852 0
132 CATCGAGGTCGTAAACCC 3894 0
133 CATGATGCAAAAGGTACG 3890 0
134 CATGGTAAGTGTACCGGA 3894 0
135 CATGTTACGACTTGCCTC 3866 0
136 CATTAGGATGTCCTGATC 3893 0
137 CCAAAAACTCTTGGTGCA 3883 0
138 CCAACATCGAGGTCGTAA 3894 0
139 CCAAGAGTTTTTGGTTCC 3844 0
140 CCACTCTTTTGCCACAGA 3879 0
141 CCCACTCTTTTGCCACAG 3879 0
142 CCCGTCTCTGTGGCAAAA 3894 0
143 CCCTAGGGATAACAGCGC 3898 0
144 CCCTTAATAGCGGCTGCA 3853 0
145 CCGCCCGTCACTCTCCCC 3890 0
146 CCGCGGTGGCTGGCACGA 3895 0
147 CCGGTAAAACTCGTGCCA 3847 0
148 CCGGTCTGAACTCAGATC 3904 0
149 CCGTCTCTGTGGCAAAAG 3894 0
150 CCTACGTGATCTGAGTTC 3899 0
151 CCTAGGGATAACAGCGCA 3896 0
152 CCTCGATGTTGGATCAGG 3892 0
153 CCTCGCCTGTTTACCAAA 3892 0
154 CCTGTTCTAGAACCGATA 3862 0
155 CCTTAATAGCGGCTGCAC 3850 0
156 CCTTCCGGTACACTTACC 3893 0
157 CGAACCCTTAATAGCGGC 3855 0
158 CGACCTCGATGTTGGATC 3890 0
159 CGACGGCGGTATATAGGC 3890 0
160 CGATGTTGGATCAGGACA 3892 0
161 CGATGTTTTTGGTAAACA 3886 0
162 CGCCTGTTTACCAAAAAC 3889 0
163 CGCGGTGGCTGGCACGAG 3894 0
164 CGCTTAGCTGTTAACTAA 3864 0
165 CGGTAAAACTCGTGCCAG 3856 0
166 CGGTCTGAACTCAGATCA 3904 0
167 CGGTTCTAGAACAGGCTC 3873 0
168 CGTAACATGGTAAGTGTA 3892 0
169 CGTCAGGTCGAGGTGTAG 3893 0
170 CGTCTCTGTGGCAAAAGA 3891 0
171 CGTGATCTGAGTTCAGAC 3902 0
172 CGTTGAACAAACGAACCC 3887 0
173 CGTTTGTTCAACGATTAA 3877 0
174 CTAAATCATGATGCAAAA 3896 0
175 CTAATGGTGCAGCCGCTA 3840 0
176 CTACGTGATCTGAGTTCA 3899 0
177 CTAGAACAGGCTCCTCTA 3877 0
178 CTAGAGGAGCCTGTTCTA 3891 0
179 CTAGGGATAACAGCGCAA 3897 0
180 CTCCGGTCTGAACTCAGA 3903 0
181 CTCGATGTTGGATCAGGA 3892 0
182 CTCGCCTGTTTACCAAAA 3892 0
183 CTCGTACCTTTTGCATCA 3889 0
184 CTCTTTTGCCACAGAGAC 3869 0
185 CTGAACTCAGATCACGTA 3900 0
186 CTGCACCATTAGGATGTC 3861 0
187 CTGGGATTAGATACCCCA 3895 0
188 CTGTTATCCCTAGGGTAA 3897 0
189 CTGTTTACCAAAAACATC 3886 0
190 CTTAATAGCGGCTGCACC 3848 0
191 CTTAGCTGTTAACTAAGA 3878 0
192 CTTAGGAACCAAAAACTC 3847 0
193 CTTAGTTAACAGCTAAGC 3875 0
194 CTTCCGGTACACTTACCA 3891 0
195 CTTGGATTTGCACCAAGA 3899 0
196 CTTTAATCGTTGAACAAA 3876 0
197 CTTTTGCATCATGATTTA 3896 0
198 GAACAAACGAACCCTTAA 3890 0
199 GAAGGAACTCGGCAAACA 3889 0
200 GACAAGTGATTGCGCTAC 3889 0
201 GACCTCGATGTTGGATCA 3890 0
202 GACTTTAATCGTTGAACA 3686 0
203 GAGGAGCCTGTTCTAGAA 3876 0
204 GAGGCGATGTTTTTGGTA 3893 0
205 GATAGAAACTGACCTGGA 3900 0
206 GATCACGTAGGACTTTAA 3900 0
207 GATTAAAGTCCTACGTGA 3704 0
208 GCAAGTCGTAACATGGTA 3891 0
209 GCATAGTGGGGTATCTAA 3896 0
210 GCCGGTAAAACTCGTGCC 3827 0
211 GCCTCGCCTGTTTACCAA 3890 0
212 GCCTGTTTACCAAAAACA 3889 0
213 GCGATGTTTTTGGTAAAC 3888 0
214 GCGCTTAGCTGTTAACTA 3899 0
215 GCGGCTGCACCATTAGGA 3850 0
216 GCTAAATCATGATGCAAA 3893 0
217 GCTACACCTCGACCTGAC 3893 0
218 GCTGGCACGAGTTTTACC 3881 0
219 GCTGTTATCCCTAGGGTA 3896 0
220 GGACATCCTAATGGTGCA 3865 0
221 GGACTTTAATCGTTGAAC 3687 0
222 GGAGCCTGTTCTAGAACC 3874 0
223 GGATGTCCTGATCCAACA 3895 0
224 GGATTAGATACCCCACTA 3896 0
225 GGATTGCGCTGTTATCCC 3895 0
226 GGCGATGTTTTTGGTAAA 3890 0
227 GGCTGGCACGAGTTTTAC 3880 0
228 GGGAAGAAATGGGCTACA 3872 0
229 GGGAGGCAAGTCGTAACA 3891 0
230 GGGGAGGCAAGTCGTAAC 3890 0
231 GGGGGTTTACGACCTCGA 3894 0
232 GGTACACTTACCATGTTA 3893 0
233 GGTCTGAACTCAGATCAC 3902 0
234 GGTCTTAGGAACCAAAAA 3862 0
235 GGTTCGTTTGTTCAACGA 3877 0
236 GTAACATGGTAAGTGTAC 3893 0
237 GTACCTTTTGCATCATGA 3892 0
238 GTCCTGATCCAACATCGA 3892 0
239 GTCTTAGGAACCAAAAAC 3853 0
240 GTGGGGTATCTAATCCCA 3895 0
241 GTTACCCTAGGGATAACA 3896 0
242 GTTTGTTCAACGATTAAA 3877 0
243 TAAAACTCGTGCCAGCCA 3887 0
244 TAACCGCGGTGGCTGGCA 3895 0
245 TAAGGGTTCGTTTGTTCA 3889 0
246 TAATCGTTGAACAAACGA 3876 0
247 TACGACCTCGATGTTGGA 3892 0
248 TAGGACTTTAATCGTTGA 3689 0
249 TAGGATGTCCTGATCCAA 3897 0
250 TCCCACTCTTTTGCCACA 3866 0
251 TCGCCTGTTTACCAAAAA 3891 0
252 TGGATCAGGACATCCTAA 3894 0
253 TGGTCTTAGGAACCAAAA 3862 0
254 TGGTGCAGCCGCTATTAA 3847 0
255 TTGGTCTTAGGAACCAAA 3862 0
+20 -52
View File
@@ -4,10 +4,8 @@ import (
"fmt" "fmt"
"math" "math"
"sync" "sync"
"sync/atomic"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter"
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer"
) )
@@ -20,13 +18,6 @@ type WordSearch struct {
TmpDir string // parent of the temporary directory ("" = system default) TmpDir string // parent of the temporary directory ("" = system default)
} }
// Counts gives the number of sequences by role.
type Counts struct {
Examples int
CounterExamples int
Ignored int
}
// StrictWords is the result of the strict word search: the canonical words of // StrictWords is the result of the strict word search: the canonical words of
// PrimerLength bases present, without any error, in at least MinSequences // PrimerLength bases present, without any error, in at least MinSequences
// example sequences. // example sequences.
@@ -44,12 +35,12 @@ func MinSequences(examples int, quorum float64) int {
return int(math.Floor(float64(examples)*quorum + 1e-9)) return int(math.Floor(float64(examples)*quorum + 1e-9))
} }
// FindStrictWords reads every sequence of seqs once, indexes the example // FindStrictWords indexes the example sequences of the corpus and returns the
// sequences, and returns the words reaching the quorum. Sequences no longer // words reaching the quorum. Sequences no longer than the word size are
// than the word size are counted as examples but cannot contain any word, as // counted as examples but cannot contain any word, as in ecoPrimers. Words are
// in ecoPrimers. Words are canonical (the smaller of the word and its reverse // canonical (the smaller of the word and its reverse complement), so both
// complement), so both strands are searched. // strands are searched.
func FindStrictWords(seqs obiiter.IBioSequence, sel Selection, cfg WordSearch) (*StrictWords, error) { func FindStrictWords(c *Corpus, cfg WordSearch) (*StrictWords, error) {
if cfg.PrimerLength < 2 || cfg.PrimerLength > 31 { if cfg.PrimerLength < 2 || cfg.PrimerLength > 31 {
return nil, fmt.Errorf("obiprimer: primer length must be between 2 and 31, got %d", cfg.PrimerLength) return nil, fmt.Errorf("obiprimer: primer length must be between 2 and 31, got %d", cfg.PrimerLength)
} }
@@ -66,50 +57,27 @@ func FindStrictWords(seqs obiiter.IBioSequence, sel Selection, cfg WordSearch) (
} }
defer counter.Close() defer counter.Close()
var nextID atomic.Uint32 jobs := make(chan int)
var examples, counters, ignored atomic.Int64 var wg sync.WaitGroup
for range obidefault.ParallelWorkers() {
consumer := func(it obiiter.IBioSequence, wg *sync.WaitGroup) { wg.Add(1)
defer wg.Done() go func() {
for it.Next() { defer wg.Done()
for _, seq := range it.Get().Slice() { for i := range jobs {
switch sel.Role(seq) { if seq := c.Seqs[i]; c.Roles[i] == Example && seq.Len() > cfg.PrimerLength {
case Example: counter.AddSequence(uint32(i), seq.Sequence(), cfg.Circular)
examples.Add(1)
if seq.Len() > cfg.PrimerLength {
counter.AddSequence(nextID.Add(1)-1, seq.Sequence(), cfg.Circular)
}
case CounterExample:
counters.Add(1)
default:
ignored.Add(1)
} }
} }
} }()
} }
for i := range c.Seqs {
var wg sync.WaitGroup jobs <- i
for i := 1; i < obidefault.ParallelWorkers(); i++ {
wg.Add(1)
go consumer(seqs.Split(), &wg)
} }
wg.Add(1) close(jobs)
go consumer(seqs, &wg)
wg.Wait() wg.Wait()
res := &StrictWords{ res := &StrictWords{PrimerLength: cfg.PrimerLength, Counts: c.Counts()}
PrimerLength: cfg.PrimerLength,
Counts: Counts{
Examples: int(examples.Load()),
CounterExamples: int(counters.Load()),
Ignored: int(ignored.Load()),
},
}
res.MinSequences = MinSequences(res.Counts.Examples, cfg.Quorum) res.MinSequences = MinSequences(res.Counts.Examples, cfg.Quorum)
if res.Counts.Examples == 0 {
return nil, fmt.Errorf("obiprimer: no example sequence selected")
}
res.Words, err = counter.CountAtLeast(max(res.MinSequences, 1)) res.Words, err = counter.CountAtLeast(max(res.MinSequences, 1))
if err != nil { if err != nil {
return nil, err return nil, err
+17 -7
View File
@@ -107,8 +107,11 @@ func TestFindStrictWords(t *testing.T) {
add("zz1", randDNA(rng, 50)+motif) add("zz1", randDNA(rng, 50)+motif)
sel := Selection{Restrict: idPrefix("ex").Or(idPrefix("cx")), Ignore: idPrefix("cx"), Except: idPrefix("cx")} sel := Selection{Restrict: idPrefix("ex").Or(idPrefix("cx")), Ignore: idPrefix("cx"), Except: idPrefix("cx")}
res, err := FindStrictWords(obiiter.IBatchOver("t", data, 3), sel, corpus, err := LoadCorpus(obiiter.IBatchOver("t", data, 3), sel)
WordSearch{PrimerLength: k, Quorum: 0.7, Partitions: 8, TmpDir: t.TempDir()}) if err != nil {
t.Fatal(err)
}
res, err := FindStrictWords(corpus, WordSearch{PrimerLength: k, Quorum: 0.7, Partitions: 8, TmpDir: t.TempDir()})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -151,14 +154,17 @@ func TestFindStrictWordsParameters(t *testing.T) {
it := func() obiiter.IBioSequence { it := func() obiiter.IBioSequence {
return obiiter.IBatchOver("t", obiseq.BioSequenceSlice{obiseq.NewBioSequence("a", []byte("acgtacgtacgtacgtacgtacgt"), "")}, 1) return obiiter.IBatchOver("t", obiseq.BioSequenceSlice{obiseq.NewBioSequence("a", []byte("acgtacgtacgtacgtacgtacgt"), "")}, 1)
} }
if _, err := FindStrictWords(it(), Selection{}, WordSearch{PrimerLength: 1, Quorum: 0.7}); err == nil { corpus, err := LoadCorpus(it(), Selection{})
if err != nil {
t.Fatal(err)
}
if _, err := FindStrictWords(corpus, WordSearch{PrimerLength: 1, Quorum: 0.7}); err == nil {
t.Error("primer length 1 must fail") t.Error("primer length 1 must fail")
} }
if _, err := FindStrictWords(it(), Selection{}, WordSearch{PrimerLength: 18, Quorum: 0}); err == nil { if _, err := FindStrictWords(corpus, WordSearch{PrimerLength: 18, Quorum: 0}); err == nil {
t.Error("quorum 0 must fail") t.Error("quorum 0 must fail")
} }
if _, err := FindStrictWords(it(), Selection{Restrict: idPrefix("zz")}, if _, err := LoadCorpus(it(), Selection{Restrict: idPrefix("zz")}); err == nil {
WordSearch{PrimerLength: 18, Quorum: 0.7}); err == nil {
t.Error("no example must fail") t.Error("no example must fail")
} }
} }
@@ -206,7 +212,11 @@ func TestStrictWordsAgainstEcoPrimers(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
res, err := FindStrictWords(seqs, Selection{Restrict: restrict}, corpus, err := LoadCorpus(seqs, Selection{Restrict: restrict})
if err != nil {
t.Fatal(err)
}
res, err := FindStrictWords(corpus,
WordSearch{PrimerLength: 18, Quorum: 0.7, Circular: true, TmpDir: t.TempDir()}) WordSearch{PrimerLength: 18, Quorum: 0.7, Circular: true, TmpDir: t.TempDir()})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)