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
|
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.
|
||||||
@@ -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"
|
"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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user