2026-11-03 14:41:30 +11:00
|
|
|
package obiprimer
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq"
|
|
|
|
|
)
|
|
|
|
|
|
2026-11-03 22:33:09 +11:00
|
|
|
// maxSeqLen bounds the length of a sequence: the position of a primer site and
|
|
|
|
|
// its strand share 32 bits (see seqMatch).
|
|
|
|
|
const maxSeqLen = 1 << 31
|
|
|
|
|
|
2026-11-03 14:41:30 +11:00
|
|
|
// Counts gives the number of sequences by role.
|
|
|
|
|
type Counts struct {
|
|
|
|
|
Examples int
|
|
|
|
|
CounterExamples int
|
2026-11-03 21:12:04 +11:00
|
|
|
Others int
|
2026-11-03 14:41:30 +11:00
|
|
|
}
|
|
|
|
|
|
2026-11-03 21:12:04 +11:00
|
|
|
// Corpus is the set of sequences of the primer search, in input order, each
|
|
|
|
|
// with its role: the examples (the targets), the counterexamples, and all the
|
|
|
|
|
// other sequences, which are loaded as well since the amplicons they give
|
|
|
|
|
// decide whether a target is identified.
|
2026-11-03 14:41:30 +11:00
|
|
|
//
|
|
|
|
|
// 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 {
|
2026-11-03 22:33:09 +11:00
|
|
|
Seqs []*obiseq.BioSequence // nil entries once the corpus is packed
|
|
|
|
|
Roles []Role // Roles[i] is the role of Seqs[i]
|
|
|
|
|
|
|
|
|
|
packed []packedSeq // see Pack
|
|
|
|
|
ids []string
|
2026-11-03 14:41:30 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Counts returns the number of sequences by role.
|
|
|
|
|
func (c *Corpus) Counts() Counts {
|
2026-11-03 21:12:04 +11:00
|
|
|
var n Counts
|
2026-11-03 14:41:30 +11:00
|
|
|
for _, r := range c.Roles {
|
2026-11-03 21:12:04 +11:00
|
|
|
switch r {
|
|
|
|
|
case Example:
|
2026-11-03 14:41:30 +11:00
|
|
|
n.Examples++
|
2026-11-03 21:12:04 +11:00
|
|
|
case CounterExample:
|
2026-11-03 14:41:30 +11:00
|
|
|
n.CounterExamples++
|
2026-11-03 21:12:04 +11:00
|
|
|
default:
|
|
|
|
|
n.Others++
|
2026-11-03 14:41:30 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return n
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-03 15:24:52 +11:00
|
|
|
// 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) {
|
2026-11-03 22:33:09 +11:00
|
|
|
for i := range c.Roles {
|
|
|
|
|
if c.ID(i) == id {
|
2026-11-03 15:24:52 +11:00
|
|
|
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)
|
|
|
|
|
}
|
2026-11-03 22:33:09 +11:00
|
|
|
|
|
|
|
|
// Len returns the number of sequences.
|
|
|
|
|
func (c *Corpus) Len() int { return len(c.Roles) }
|
|
|
|
|
|
|
|
|
|
// SeqLen returns the length of sequence i.
|
|
|
|
|
func (c *Corpus) SeqLen(i int) int {
|
|
|
|
|
if c.Seqs[i] != nil {
|
|
|
|
|
return c.Seqs[i].Len()
|
|
|
|
|
}
|
|
|
|
|
return c.packed[i].n
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ID returns the identifier of sequence i.
|
|
|
|
|
func (c *Corpus) ID(i int) string {
|
|
|
|
|
if c.Seqs[i] != nil {
|
|
|
|
|
return c.Seqs[i].Id()
|
|
|
|
|
}
|
|
|
|
|
return c.ids[i]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// bases returns the bases of sequence i, lower case. A packed sequence is
|
|
|
|
|
// unpacked into buf, grown if need be, which the caller keeps for the next call
|
|
|
|
|
// (the second result); an unpacked one is returned as it is. The result must
|
|
|
|
|
// not be modified, and is valid until the next call with the same buffer.
|
|
|
|
|
func (c *Corpus) bases(i int, buf []byte) (raw, newBuf []byte) {
|
|
|
|
|
if c.Seqs[i] != nil {
|
|
|
|
|
return c.Seqs[i].Sequence(), buf
|
|
|
|
|
}
|
|
|
|
|
buf = c.packed[i].unpack(buf)
|
|
|
|
|
return buf, buf
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pack replaces the sequences by their packed form, on 2 bits per base, and
|
|
|
|
|
// frees the sequence objects (their annotations, their slack): the primer search
|
|
|
|
|
// only needs the bases, the identifiers, and the taxa, which must have been
|
|
|
|
|
// assigned before (AssignTaxa). A packed corpus holds about a quarter of the
|
|
|
|
|
// memory of the bases themselves.
|
|
|
|
|
func (c *Corpus) Pack() {
|
|
|
|
|
if c.packed != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.packed = make([]packedSeq, len(c.Seqs))
|
|
|
|
|
c.ids = make([]string, len(c.Seqs))
|
|
|
|
|
parallelFor(len(c.Seqs), func(_, i int) {
|
|
|
|
|
c.packed[i] = packBases(c.Seqs[i].Sequence())
|
|
|
|
|
c.ids[i] = c.Seqs[i].Id()
|
|
|
|
|
c.Seqs[i] = nil
|
|
|
|
|
})
|
|
|
|
|
}
|