Files
obitools4/pkg/obiprimer/words.go
T
Eric Coissac ad065f6853 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.
2026-11-03 22:35:51 +11:00

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
}