mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-11-09 17:24:41 +00:00
This update introduces new configuration options for pair selection, including multi-match filtering, circularity settings, and reference site validation. It also enhances the search functionality to correctly identify and track repeated sequences and reference locations, improving the accuracy of primer pair identification.
124 lines
3.2 KiB
Go
124 lines
3.2 KiB
Go
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
|
|
}
|
|
|
|
// IndexOfExample returns the index of the example sequence with the given
|
|
// identifier. It is an error if there is none, or if the sequence is a
|
|
// counterexample.
|
|
func (c *Corpus) IndexOfExample(id string) (int, error) {
|
|
for i, s := range c.Seqs {
|
|
if s.Id() == id {
|
|
if c.Roles[i] != Example {
|
|
return -1, fmt.Errorf("obiprimer: reference sequence %q is a counterexample, it must be an example", id)
|
|
}
|
|
return i, nil
|
|
}
|
|
}
|
|
return -1, fmt.Errorf("obiprimer: reference sequence %q is not among the selected example sequences", id)
|
|
}
|