mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-11-09 09:24:41 +00:00
This commit introduces significant changes to the sequence processing pipeline, implementing parallel loading, k-mer budget control, explicit memory management, and refined logic for pair evaluation and resolution.
155 lines
4.9 KiB
Go
155 lines
4.9 KiB
Go
package obiprimer
|
|
|
|
// The three quality indices of a pair, over the sequences it amplifies.
|
|
//
|
|
// Every sequence the pair amplifies gives a barcode (the amplicon between the
|
|
// primers); all of them are taken into account, whatever their role: examples
|
|
// (the targets), counterexamples and non-targets.
|
|
//
|
|
// - Bc, coverage: target taxa amplified / target taxa.
|
|
// - Bs, specificity: target taxa identified / target taxa amplified. A target
|
|
// taxon is identified when ALL its barcodes belong to it alone: as soon as a
|
|
// barcode is shared with another taxon, target or not, counterexample or
|
|
// not, both taxa are badly identified.
|
|
// - Bd, barcode discrimination: the share of the barcodes carried by the
|
|
// targets that lead without ambiguity to one taxon.
|
|
//
|
|
// A sequence with no taxon at the chosen rank is an anonymous taxon, distinct
|
|
// from every other: a barcode it shares with a taxon is ambiguous.
|
|
|
|
// barcodeState is what is known of one barcode while the amplicons are scanned.
|
|
type barcodeState struct {
|
|
taxon int32 // 1 + the first taxon met, 0 while none
|
|
flags uint8
|
|
}
|
|
|
|
const (
|
|
barcodeAmbiguous = 1 << iota // two distinct taxa carry it
|
|
barcodeAnonymous // a sequence with no taxon carries it
|
|
barcodeTarget // a target sequence with a taxon carries it
|
|
)
|
|
|
|
// ambiguous reports whether the barcode does not lead to a single taxon.
|
|
func (b barcodeState) ambiguous() bool {
|
|
return b.flags&barcodeAmbiguous != 0 || (b.flags&barcodeAnonymous != 0 && b.taxon != 0)
|
|
}
|
|
|
|
// resolution is the outcome of the identification of the taxa of one pair.
|
|
type resolution struct {
|
|
targetTaxa int // target taxa amplified
|
|
counterTaxa int // counterexample taxa amplified
|
|
identified int // target taxa whose barcodes all belong to them alone
|
|
barcodes int // distinct barcodes carried by the targets
|
|
discriminant int // of which lead to a single taxon
|
|
}
|
|
|
|
// resolver computes the indices of pairs one after the other, reusing its maps
|
|
// (a pair needs a few thousand entries, and there are thousands of pairs).
|
|
type resolver struct {
|
|
state map[uint64]barcodeState
|
|
targets map[int32]struct{}
|
|
counters map[int32]struct{}
|
|
bad map[int32]struct{}
|
|
owner map[uint64]int32
|
|
}
|
|
|
|
func newResolver() *resolver {
|
|
return &resolver{
|
|
state: map[uint64]barcodeState{},
|
|
targets: map[int32]struct{}{},
|
|
counters: map[int32]struct{}{},
|
|
bad: map[int32]struct{}{},
|
|
owner: map[uint64]int32{},
|
|
}
|
|
}
|
|
|
|
// resolve computes the resolution of one pair from its amplicons, in any order.
|
|
func resolve(a ampSlice) resolution { return newResolver().resolve(a) }
|
|
|
|
func (rs *resolver) resolve(a ampSlice) resolution {
|
|
var res resolution
|
|
clear(rs.state)
|
|
clear(rs.targets)
|
|
clear(rs.counters)
|
|
clear(rs.bad)
|
|
|
|
for i, h := range a.hash {
|
|
taxon, role := unpackTaxon(a.tax[i])
|
|
st := rs.state[h]
|
|
switch {
|
|
case taxon < 0:
|
|
st.flags |= barcodeAnonymous
|
|
case st.taxon == 0:
|
|
st.taxon = taxon + 1
|
|
case st.taxon != taxon+1:
|
|
st.flags |= barcodeAmbiguous
|
|
}
|
|
if taxon >= 0 {
|
|
switch role {
|
|
case Example:
|
|
st.flags |= barcodeTarget
|
|
rs.targets[taxon] = struct{}{}
|
|
case CounterExample:
|
|
rs.counters[taxon] = struct{}{}
|
|
}
|
|
}
|
|
rs.state[h] = st
|
|
}
|
|
|
|
for i, h := range a.hash {
|
|
taxon, role := unpackTaxon(a.tax[i])
|
|
if role == Example && taxon >= 0 && rs.state[h].ambiguous() {
|
|
rs.bad[taxon] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for _, st := range rs.state {
|
|
if st.flags&barcodeTarget != 0 {
|
|
res.barcodes++
|
|
if !st.ambiguous() {
|
|
res.discriminant++
|
|
}
|
|
}
|
|
}
|
|
res.targetTaxa, res.counterTaxa = len(rs.targets), len(rs.counters)
|
|
res.identified = len(rs.targets) - len(rs.bad)
|
|
return res
|
|
}
|
|
|
|
// lenientIdentified is an upper bound of resolution.identified, computed on
|
|
// the target sequences alone and with the lenient rule of ecoPrimers (a taxon
|
|
// counts as soon as ONE of its barcodes belongs to it alone, among the targets).
|
|
//
|
|
// A taxon identified by resolve has all its barcodes alone among every amplified
|
|
// sequence, hence among the targets: it has at least one such barcode and is
|
|
// counted here too. So identified <= the second result, with the same number of
|
|
// target taxa, which lets a pair be rejected on the specificity threshold without
|
|
// gathering the amplicons of the non-target sequences.
|
|
func lenientIdentified(a ampSlice) (targetTaxa, identified int) {
|
|
return newResolver().lenientIdentified(a)
|
|
}
|
|
|
|
func (rs *resolver) lenientIdentified(a ampSlice) (targetTaxa, identified int) {
|
|
clear(rs.owner) // barcode -> its taxon, or -1 when shared
|
|
clear(rs.targets)
|
|
for i, h := range a.hash {
|
|
taxon, role := unpackTaxon(a.tax[i])
|
|
if role != Example || taxon < 0 {
|
|
continue
|
|
}
|
|
rs.targets[taxon] = struct{}{}
|
|
if prev, seen := rs.owner[h]; !seen {
|
|
rs.owner[h] = taxon
|
|
} else if prev != taxon {
|
|
rs.owner[h] = -1
|
|
}
|
|
}
|
|
clear(rs.bad) // here: the taxa owning a barcode
|
|
for _, t := range rs.owner {
|
|
if t >= 0 {
|
|
rs.bad[t] = struct{}{}
|
|
}
|
|
}
|
|
return len(rs.targets), len(rs.bad)
|
|
}
|