Files
obitools4/pkg/obiprimer/corpus.go
T
Eric Coissac 57c2517b24 Implement advanced primer search filtering and reference handling
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.
2026-11-03 22:43:50 +11:00

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)
}