mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-11-09 09:24:41 +00:00
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:
1 parent
f75e774bd5
commit
ad065f6853
10 files changed
+2265
-59
No files matched your search
@@ -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
|
||||
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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
+1208
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
|
+20
-52
@@ -4,10 +4,8 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault"
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiiter"
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer"
|
||||
)
|
||||
|
||||
@@ -20,13 +18,6 @@ type WordSearch struct {
|
||||
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
|
||||
// PrimerLength bases present, without any error, in at least MinSequences
|
||||
// example sequences.
|
||||
@@ -44,12 +35,12 @@ func MinSequences(examples int, quorum float64) int {
|
||||
return int(math.Floor(float64(examples)*quorum + 1e-9))
|
||||
}
|
||||
|
||||
// FindStrictWords reads every sequence of seqs once, indexes the example
|
||||
// sequences, and returns the words reaching the quorum. Sequences no longer
|
||||
// than the word size are counted as examples but cannot contain any word, as
|
||||
// in ecoPrimers. Words are canonical (the smaller of the word and its reverse
|
||||
// complement), so both strands are searched.
|
||||
func FindStrictWords(seqs obiiter.IBioSequence, sel Selection, cfg WordSearch) (*StrictWords, error) {
|
||||
// FindStrictWords indexes the example sequences of the corpus and returns the
|
||||
// words reaching the quorum. Sequences no longer than the word size are
|
||||
// counted as examples but cannot contain any word, as in ecoPrimers. Words are
|
||||
// canonical (the smaller of the word and its reverse complement), so both
|
||||
// strands are searched.
|
||||
func FindStrictWords(c *Corpus, cfg WordSearch) (*StrictWords, error) {
|
||||
if cfg.PrimerLength < 2 || cfg.PrimerLength > 31 {
|
||||
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()
|
||||
|
||||
var nextID atomic.Uint32
|
||||
var examples, counters, ignored atomic.Int64
|
||||
|
||||
consumer := func(it obiiter.IBioSequence, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
for it.Next() {
|
||||
for _, seq := range it.Get().Slice() {
|
||||
switch sel.Role(seq) {
|
||||
case Example:
|
||||
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)
|
||||
jobs := make(chan int)
|
||||
var wg sync.WaitGroup
|
||||
for range obidefault.ParallelWorkers() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := range jobs {
|
||||
if seq := c.Seqs[i]; c.Roles[i] == Example && seq.Len() > cfg.PrimerLength {
|
||||
counter.AddSequence(uint32(i), seq.Sequence(), cfg.Circular)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 1; i < obidefault.ParallelWorkers(); i++ {
|
||||
wg.Add(1)
|
||||
go consumer(seqs.Split(), &wg)
|
||||
for i := range c.Seqs {
|
||||
jobs <- i
|
||||
}
|
||||
wg.Add(1)
|
||||
go consumer(seqs, &wg)
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
|
||||
res := &StrictWords{
|
||||
PrimerLength: cfg.PrimerLength,
|
||||
Counts: Counts{
|
||||
Examples: int(examples.Load()),
|
||||
CounterExamples: int(counters.Load()),
|
||||
Ignored: int(ignored.Load()),
|
||||
},
|
||||
}
|
||||
res := &StrictWords{PrimerLength: cfg.PrimerLength, Counts: c.Counts()}
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -107,8 +107,11 @@ func TestFindStrictWords(t *testing.T) {
|
||||
add("zz1", randDNA(rng, 50)+motif)
|
||||
|
||||
sel := Selection{Restrict: idPrefix("ex").Or(idPrefix("cx")), Ignore: idPrefix("cx"), Except: idPrefix("cx")}
|
||||
res, err := FindStrictWords(obiiter.IBatchOver("t", data, 3), sel,
|
||||
WordSearch{PrimerLength: k, Quorum: 0.7, Partitions: 8, TmpDir: t.TempDir()})
|
||||
corpus, err := LoadCorpus(obiiter.IBatchOver("t", data, 3), sel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, err := FindStrictWords(corpus, WordSearch{PrimerLength: k, Quorum: 0.7, Partitions: 8, TmpDir: t.TempDir()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -151,14 +154,17 @@ func TestFindStrictWordsParameters(t *testing.T) {
|
||||
it := func() obiiter.IBioSequence {
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
if _, err := FindStrictWords(it(), Selection{Restrict: idPrefix("zz")},
|
||||
WordSearch{PrimerLength: 18, Quorum: 0.7}); err == nil {
|
||||
if _, err := LoadCorpus(it(), Selection{Restrict: idPrefix("zz")}); err == nil {
|
||||
t.Error("no example must fail")
|
||||
}
|
||||
}
|
||||
@@ -206,7 +212,11 @@ func TestStrictWordsAgainstEcoPrimers(t *testing.T) {
|
||||
if err != nil {
|
||||
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()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
Reference in new issue
Block a user