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.
87 lines
2.8 KiB
Go
87 lines
2.8 KiB
Go
package obiprimer
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sync"
|
|
|
|
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault"
|
|
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer"
|
|
)
|
|
|
|
// WordSearch holds the parameters of the strict word search.
|
|
type WordSearch struct {
|
|
PrimerLength int // word size (ecoPrimers -O, 18 by default; 2..31)
|
|
Quorum float64 // fraction of example sequences that must contain the word (ecoPrimers -q, 0.7)
|
|
Circular bool // sequences are circular (ecoPrimers -c)
|
|
Partitions int // partition files of the counter (default 256)
|
|
TmpDir string // parent of the temporary directory ("" = system default)
|
|
}
|
|
|
|
// 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.
|
|
type StrictWords struct {
|
|
PrimerLength int
|
|
MinSequences int // quorum in number of example sequences
|
|
Words []obikmer.SeqKmerCount
|
|
Counts Counts
|
|
}
|
|
|
|
// MinSequences returns the quorum in sequences: floor(examples * quorum).
|
|
// The tiny epsilon makes exact products (10 * 0.7 = 7) robust to the binary
|
|
// representation of the fraction.
|
|
func MinSequences(examples int, quorum float64) int {
|
|
return int(math.Floor(float64(examples)*quorum + 1e-9))
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
if cfg.Quorum <= 0 || cfg.Quorum > 1 {
|
|
return nil, fmt.Errorf("obiprimer: quorum must be in ]0,1], got %g", cfg.Quorum)
|
|
}
|
|
if cfg.Partitions <= 0 {
|
|
cfg.Partitions = 256
|
|
}
|
|
|
|
counter, err := obikmer.NewSeqKmerCounter(cfg.TmpDir, cfg.PrimerLength, 0, cfg.Partitions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer counter.Close()
|
|
|
|
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)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
for i := range c.Seqs {
|
|
jobs <- i
|
|
}
|
|
close(jobs)
|
|
wg.Wait()
|
|
|
|
res := &StrictWords{PrimerLength: cfg.PrimerLength, Counts: c.Counts()}
|
|
res.MinSequences = MinSequences(res.Counts.Examples, cfg.Quorum)
|
|
res.Words, err = counter.CountAtLeast(max(res.MinSequences, 1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return res, nil
|
|
}
|