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