mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-11-09 09:24:41 +00:00
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.
This commit is contained in:
1 parent
c156f11947
commit
57c2517b24
15 files changed
+2041
-9
No files matched your search
@@ -106,3 +106,18 @@ func LoadCorpus(seqs obiiter.IBioSequence, sel Selection) (*Corpus, error) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package obiprimer
|
||||
|
||||
import "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer"
|
||||
|
||||
// DropRepeatedWords removes the strict words found more than once (on either
|
||||
// strand) in at least one example sequence. It is the first half of the
|
||||
// ecoPrimers -U option (no multi-match).
|
||||
func DropRepeatedWords(words []obikmer.SeqKmerCount) (kept []obikmer.SeqKmerCount, dropped int) {
|
||||
kept = make([]obikmer.SeqKmerCount, 0, len(words))
|
||||
for _, w := range words {
|
||||
if w.Repeated {
|
||||
dropped++
|
||||
continue
|
||||
}
|
||||
kept = append(kept, w)
|
||||
}
|
||||
return kept, dropped
|
||||
}
|
||||
|
||||
// DropMultiMatch removes the words having two or more approximate occurrences,
|
||||
// both strands counted, in at least one sequence of the corpus, whatever its
|
||||
// role. It is the second half of the ecoPrimers -U option: the primers that
|
||||
// can anneal at several places of a sequence are discarded.
|
||||
func DropMultiMatch(wm []WordMatches) (kept []WordMatches, dropped int) {
|
||||
kept = make([]WordMatches, 0, len(wm))
|
||||
for _, w := range wm {
|
||||
multi := false
|
||||
for i := 1; i < len(w.Matches); i++ {
|
||||
if w.Matches[i].Seq == w.Matches[i-1].Seq {
|
||||
multi = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if multi {
|
||||
dropped++
|
||||
continue
|
||||
}
|
||||
kept = append(kept, w)
|
||||
}
|
||||
return kept, dropped
|
||||
}
|
||||
@@ -22,6 +22,8 @@ type PairParams struct {
|
||||
FalsePositive float64 // -x: and less than this fraction of the counterexamples (0.1)
|
||||
MinSpecificity float64 // -T: pairs with a lower Bs are dropped (0.6)
|
||||
Circular bool // -c
|
||||
Reference int // -R: corpus index of the reference sequence (used when HasReference)
|
||||
HasReference bool
|
||||
}
|
||||
|
||||
func (p PairParams) validate() error {
|
||||
|
||||
@@ -31,6 +31,14 @@ type PairStats struct {
|
||||
Tm1, Tm2 float64 // melting temperature of the primers against their perfect target
|
||||
MinTm1, MinTm2 float64 // lowest melting temperature over the example amplicons
|
||||
GC1, GC2 int
|
||||
|
||||
// With a reference sequence (-R): where the pair amplifies it. RefLocation
|
||||
// is the GenBank location of the two primer sites (1-based, left to right),
|
||||
// wrapped in complement() when Primer1 binds the other strand; RefAmplicon
|
||||
// is the amplified region, primers excluded, read from Primer1 to Primer2.
|
||||
// Both are empty when the pair does not amplify the reference.
|
||||
RefLocation string
|
||||
RefAmplicon string
|
||||
}
|
||||
|
||||
// Score is the ranking criterion of ecoPrimers.
|
||||
@@ -52,6 +60,12 @@ type ampRecord struct {
|
||||
tm1, tm2 float32 // NaN when unavailable
|
||||
}
|
||||
|
||||
// refHit is an amplicon of a retained pair on the reference sequence.
|
||||
type refHit struct {
|
||||
up, down seqMatch
|
||||
swapped bool
|
||||
}
|
||||
|
||||
// ampHash hashes the amplicon between the two primers read from the side of
|
||||
// the first primer of the pair (reverse complemented when the upstream match is
|
||||
// the second primer), so that identical amplicons give the same value
|
||||
@@ -132,7 +146,7 @@ func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obi
|
||||
}
|
||||
|
||||
// Phase 2: the amplicons of the retained pairs only.
|
||||
records := ps.collectAmplicons(c, p, index, model)
|
||||
records, refs := ps.collectAmplicons(c, p, index, model)
|
||||
byPair := make([][]ampRecord, len(kept))
|
||||
for _, r := range records {
|
||||
byPair[r.pair] = append(byPair[r.pair], r)
|
||||
@@ -142,6 +156,12 @@ func FindPairs(c *Corpus, taxa *Taxa, wm []WordMatches, p PairParams, model *obi
|
||||
for i, k := range kept {
|
||||
st, ok := ps.evaluate(c, taxa, p, k, acc[k], byPair[i], model)
|
||||
if ok {
|
||||
if h, found := refs[int32(i)]; found {
|
||||
// evaluate orders the primers by string; the amplicon is read from the first one
|
||||
s1 := orientedWord(ps.primers[k.p1].Word, p.PrimerLength, k.a1)
|
||||
s2 := orientedWord(ps.primers[k.p2].Word, p.PrimerLength, k.a2)
|
||||
st.RefLocation, st.RefAmplicon = referenceSites(c.Seqs[p.Reference].Sequence(), h, p, s2 < s1)
|
||||
}
|
||||
out = append(out, st)
|
||||
}
|
||||
}
|
||||
@@ -162,7 +182,7 @@ func boolCmp(a, b bool) int {
|
||||
}
|
||||
|
||||
// collectAmplicons gathers, in parallel, one record per amplicon of a retained pair.
|
||||
func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey]int32, model *obithermo.Model) []ampRecord {
|
||||
func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey]int32, model *obithermo.Model) ([]ampRecord, map[int32]refHit) {
|
||||
lp := p.PrimerLength
|
||||
workers := obidefault.ParallelWorkers()
|
||||
parts := make([][]ampRecord, workers)
|
||||
@@ -175,6 +195,10 @@ func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey
|
||||
}
|
||||
nan := float32(math.NaN())
|
||||
|
||||
// The reference sequence is handled by a single worker, which is the only
|
||||
// writer of refs; the first (leftmost) amplicon of each pair is kept.
|
||||
refs := map[int32]refHit{}
|
||||
|
||||
jobs := make(chan int)
|
||||
var wg sync.WaitGroup
|
||||
for w := range workers {
|
||||
@@ -191,6 +215,11 @@ func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if p.HasReference && s == p.Reference {
|
||||
if old, seen := refs[pi]; !seen || o.up.pos < old.up.pos {
|
||||
refs[pi] = refHit{o.up, o.down, o.swapped}
|
||||
}
|
||||
}
|
||||
rec := ampRecord{pair: pi, seq: int32(s), tm1: nan, tm2: nan}
|
||||
if isExample {
|
||||
rec.hash = ampHash(raw, int(o.up.pos)+lp, int(o.down.pos), o.swapped)
|
||||
@@ -233,7 +262,7 @@ func (ps *primerSet) collectAmplicons(c *Corpus, p PairParams, index map[pairKey
|
||||
for _, part := range parts {
|
||||
all = append(all, part...)
|
||||
}
|
||||
return all
|
||||
return all, refs
|
||||
}
|
||||
|
||||
func b2i(b bool) int {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package obiprimer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq"
|
||||
)
|
||||
|
||||
// span writes the 1-based GenBank range of the lp bases starting at the 0-based
|
||||
// position pos. On a circular sequence a range running over the junction is
|
||||
// split in two.
|
||||
func span(pos, lp, n int, circular bool) string {
|
||||
end := pos + lp
|
||||
if end > n && circular {
|
||||
return fmt.Sprintf("%d..%d,1..%d", pos+1, n, end-n)
|
||||
}
|
||||
return fmt.Sprintf("%d..%d", pos+1, end)
|
||||
}
|
||||
|
||||
// referenceSites describes an amplicon of a pair on the reference sequence raw.
|
||||
//
|
||||
// The location lists the two primer sites left to right as join(a..b,c..d),
|
||||
// inside complement() when the first primer of the pair (Primer1 once the pair
|
||||
// is in string order) binds the reverse strand. The amplicon is the region
|
||||
// between the sites, read 5'->3' from the first primer, in lower case.
|
||||
//
|
||||
// swappedByOrder says whether putting the pair in string order exchanged the
|
||||
// two primers of the key.
|
||||
func referenceSites(raw []byte, h refHit, p PairParams, swappedByOrder bool) (location, amplicon string) {
|
||||
lp, n := p.PrimerLength, len(raw)
|
||||
|
||||
// The key slot 1 is the upstream primer unless the match order was swapped;
|
||||
// the exchange due to the string order flips it again.
|
||||
firstIsUpstream := h.swapped == swappedByOrder
|
||||
|
||||
location = "join(" + span(int(h.up.pos), lp, n, p.Circular) + "," + span(int(h.down.pos), lp, n, p.Circular) + ")"
|
||||
region := []byte(strings.ToLower(string(raw[int(h.up.pos)+lp : int(h.down.pos)])))
|
||||
if firstIsUpstream {
|
||||
return location, string(region)
|
||||
}
|
||||
rc := obiseq.NewBioSequence("", region, "").ReverseComplement(true)
|
||||
return "complement(" + location + ")", string(rc.Sequence())
|
||||
}
|
||||
@@ -19,6 +19,8 @@ type SearchParams struct {
|
||||
FalsePositive float64 // -x: fraction of the counterexamples it may match (0.1)
|
||||
MinSpecificity float64 // -T: pairs with a lower Bs are dropped (0.6)
|
||||
Circular bool // -c: sequences are circular
|
||||
NoMultiMatch bool // -U: discard the primers matching several times in a sequence
|
||||
Reference string // -R: identifier of an example sequence on which the amplicons are located ("" = none)
|
||||
|
||||
Partitions int // partitions of the k-mer counter (default 256)
|
||||
TmpDir string
|
||||
@@ -41,6 +43,8 @@ type SearchInfo struct {
|
||||
StrictWords int // words found without error in at least StrictQuorum of the examples
|
||||
MinStrict int // that quorum, in sequences
|
||||
Matches int // approximate occurrences of those words
|
||||
Repeated int // strict words dropped because repeated in an example (-U)
|
||||
MultiMatch int // words dropped because matching several times in a sequence (-U)
|
||||
Pair PairInfo
|
||||
}
|
||||
|
||||
@@ -63,6 +67,16 @@ func Search(c *Corpus, taxa *Taxa, p SearchParams) ([]PairStats, *SearchInfo, er
|
||||
return nil, nil, err
|
||||
}
|
||||
info := &SearchInfo{Counts: sw.Counts, StrictWords: len(sw.Words), MinStrict: sw.MinSequences}
|
||||
if p.NoMultiMatch {
|
||||
sw.Words, info.Repeated = DropRepeatedWords(sw.Words)
|
||||
}
|
||||
|
||||
refIndex := -1
|
||||
if p.Reference != "" {
|
||||
if refIndex, err = c.IndexOfExample(p.Reference); err != nil {
|
||||
return nil, info, err
|
||||
}
|
||||
}
|
||||
|
||||
words := make([]uint64, len(sw.Words))
|
||||
for i, w := range sw.Words {
|
||||
@@ -72,6 +86,9 @@ func Search(c *Corpus, taxa *Taxa, p SearchParams) ([]PairStats, *SearchInfo, er
|
||||
if err != nil {
|
||||
return nil, info, err
|
||||
}
|
||||
if p.NoMultiMatch {
|
||||
wm, info.MultiMatch = DropMultiMatch(wm)
|
||||
}
|
||||
for _, w := range wm {
|
||||
info.Matches += len(w.Matches)
|
||||
}
|
||||
@@ -85,6 +102,8 @@ func Search(c *Corpus, taxa *Taxa, p SearchParams) ([]PairStats, *SearchInfo, er
|
||||
FalsePositive: p.FalsePositive,
|
||||
MinSpecificity: p.MinSpecificity,
|
||||
Circular: p.Circular,
|
||||
Reference: refIndex,
|
||||
HasReference: refIndex >= 0,
|
||||
}, p.Model)
|
||||
info.Pair = pinfo
|
||||
return pairs, info, err
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package obiprimer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiformats"
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obitax"
|
||||
)
|
||||
|
||||
// loadTutorial reads the Cyprinidae examples of the tutorial data (the opt-in
|
||||
// tests need OBIPRIMERS_TUTORIAL, see TestStrictWordsAgainstEcoPrimers).
|
||||
func loadTutorial(t *testing.T) (*Corpus, *Taxa, *obitax.Taxonomy) {
|
||||
t.Helper()
|
||||
dir := os.Getenv("OBIPRIMERS_TUTORIAL")
|
||||
if dir == "" {
|
||||
t.Skip("OBIPRIMERS_TUTORIAL not set")
|
||||
}
|
||||
taxonomy, err := obiformats.LoadTaxonomy(filepath.Join(dir, "ncbitaxo_20250211.tgz"), false, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
restrict, err := BelongingTo(taxonomy, []string{"7953"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seqs, err := obiformats.ReadSequencesFromFile(filepath.Join(dir, "mito.vert.fasta.gz"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
corpus, err := LoadCorpus(seqs, Selection{Restrict: restrict})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
taxa, err := corpus.AssignTaxa(taxonomy, "species")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return corpus, taxa, taxonomy
|
||||
}
|
||||
|
||||
func tutorialParams(t *testing.T) SearchParams {
|
||||
p := DefaultSearchParams()
|
||||
p.MinLength, p.MaxLength, p.ThreePrime, p.Circular = 30, 150, 2, true
|
||||
p.TmpDir = t.TempDir()
|
||||
return p
|
||||
}
|
||||
|
||||
// ecoPrimers -U on the Cyprinidae tutorial case (-e 3 -3 2 -l 30 -L 150 -c -r 7953 -U)
|
||||
// keeps 1366 pairs. testdata/ecoprimers_Cyprinidae_U.pairs.tsv holds, for each,
|
||||
// the primers, the amplified example and counterexample sequences, the amplified
|
||||
// example and counterexample taxa, and the identified taxa.
|
||||
func TestNoMultiMatchAgainstEcoPrimers(t *testing.T) {
|
||||
corpus, taxa, _ := loadTutorial(t)
|
||||
p := tutorialParams(t)
|
||||
p.NoMultiMatch = true
|
||||
pairs, info, err := Search(corpus, taxa, p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%+v", *info)
|
||||
|
||||
f, err := os.Open(filepath.Join("testdata", "ecoprimers_Cyprinidae_U.pairs.tsv"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
want := map[string][5]int{}
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
var p1, p2 string
|
||||
var v [5]int
|
||||
if _, err := fmt.Sscanf(sc.Text(), "%s\t%s\t%d\t%d\t%d\t%d\t%d", &p1, &p2, &v[0], &v[1], &v[2], &v[3], &v[4]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want[p1+"/"+p2] = v
|
||||
}
|
||||
|
||||
if len(pairs) != len(want) {
|
||||
t.Errorf("%d pairs, ecoPrimers -U reports %d", len(pairs), len(want))
|
||||
}
|
||||
bad := 0
|
||||
for _, g := range pairs {
|
||||
v, ok := want[g.Primer1+"/"+g.Primer2]
|
||||
if !ok {
|
||||
t.Errorf("pair %s/%s is not in the ecoPrimers -U result", g.Primer1, g.Primer2)
|
||||
continue
|
||||
}
|
||||
got := [5]int{g.Examples, g.CounterExamples, g.ExampleTaxa, g.CounterTaxa, g.Identified}
|
||||
if got != v {
|
||||
if bad++; bad <= 3 {
|
||||
t.Errorf("pair %s/%s: got %v, ecoPrimers %v", g.Primer1, g.Primer2, got, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var locRE = regexp.MustCompile(`(\d+)\.\.(\d+)`)
|
||||
|
||||
// ecoPrimers -R is only right for the references that happen to come first: it
|
||||
// indexes the amplicons of a pair by sequence number. On NC_083213 only 382 of
|
||||
// its 3405 locations are real primer sites, against all 3405 for obiprimers.
|
||||
// Here the locations are checked against the sequence itself, so no
|
||||
// ecoPrimers output is needed: each location must give two real primer sites
|
||||
// (at most 3 substitutions) and the amplicon must be the region between them,
|
||||
// read from the first primer.
|
||||
func TestReferenceSitesAreRealSites(t *testing.T) {
|
||||
corpus, taxa, _ := loadTutorial(t)
|
||||
|
||||
for _, ref := range []string{"NC_022183", "NC_083213"} {
|
||||
t.Run(ref, func(t *testing.T) {
|
||||
p := tutorialParams(t)
|
||||
p.Reference = ref
|
||||
pairs, _, err := Search(corpus, taxa, p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
idx, err := corpus.IndexOfExample(ref)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seq := strings.ToLower(string(corpus.Seqs[idx].Sequence()))
|
||||
|
||||
hamming := func(a, b string) int {
|
||||
d := 0
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
d++
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
located, bad := 0, 0
|
||||
for _, pr := range pairs {
|
||||
if pr.RefLocation == "" {
|
||||
continue
|
||||
}
|
||||
located++
|
||||
m := locRE.FindAllStringSubmatch(pr.RefLocation, -1)
|
||||
if len(m) != 2 {
|
||||
t.Fatalf("location %q", pr.RefLocation)
|
||||
}
|
||||
var a1, b1, a2, b2 int
|
||||
fmt.Sscanf(m[0][0], "%d..%d", &a1, &b1)
|
||||
fmt.Sscanf(m[1][0], "%d..%d", &a2, &b2)
|
||||
left, right := seq[a1-1:b1], seq[a2-1:b2]
|
||||
between := seq[b1 : a2-1]
|
||||
|
||||
p1, p2 := strings.ToLower(pr.Primer1), strings.ToLower(pr.Primer2)
|
||||
var ok bool
|
||||
if strings.HasPrefix(pr.RefLocation, "complement(") {
|
||||
ok = hamming(strings.ToLower(revcomp(strings.ToUpper(right))), p1) <= 3 && hamming(left, p2) <= 3 &&
|
||||
pr.RefAmplicon == strings.ToLower(revcomp(strings.ToUpper(between)))
|
||||
} else {
|
||||
ok = hamming(left, p1) <= 3 && hamming(strings.ToLower(revcomp(strings.ToUpper(right))), p2) <= 3 &&
|
||||
pr.RefAmplicon == between
|
||||
}
|
||||
if !ok {
|
||||
if bad++; bad <= 3 {
|
||||
t.Errorf("pair %s/%s: %s is not a real location", pr.Primer1, pr.Primer2, pr.RefLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Logf("%d of %d pairs amplify %s; %d wrong locations", located, len(pairs), ref, bad)
|
||||
if located == 0 {
|
||||
t.Error("no pair amplifies the reference")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package obiprimer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obikmer"
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq"
|
||||
)
|
||||
|
||||
// synthetic builds nseq example sequences sharing the block F + insert + rc(R),
|
||||
// each with its own flanks and its own substitution inside the insert (so that
|
||||
// every sequence has a distinct amplicon, hence Bs = 1, with one taxon each).
|
||||
// extraF, if not empty, is appended to the flank of sequence extraIn: that
|
||||
// sequence then holds the forward site twice.
|
||||
type synthetic struct {
|
||||
corpus *Corpus
|
||||
taxa *Taxa
|
||||
f, r, i string // forward primer, reverse primer (5'->3'), insert of sequence 0
|
||||
pos int // 0-based start of the block in every sequence
|
||||
}
|
||||
|
||||
func newSynthetic(nseq int, repeatForwardIn int, seed int64) synthetic {
|
||||
rng := rand.New(rand.NewSource(seed))
|
||||
f := randDNA(rng, 18)
|
||||
r := randDNA(rng, 18)
|
||||
ins := randDNA(rng, 40)
|
||||
const pos = 60
|
||||
|
||||
c := &Corpus{}
|
||||
taxa := &Taxa{Rank: "species", ExampleTaxa: nseq}
|
||||
for k := 0; k < nseq; k++ {
|
||||
in := []byte(ins)
|
||||
in[3+k] = "ACGT"[(strings.IndexByte("ACGT", in[3+k])+1)%4] // one private substitution
|
||||
s := randDNA(rng, pos) + f + string(in) + revcomp(r) + randDNA(rng, 60)
|
||||
if k == repeatForwardIn {
|
||||
s += randDNA(rng, 20) + f // the forward site a second time
|
||||
}
|
||||
c.Seqs = append(c.Seqs, obiseq.NewBioSequence(fmt.Sprintf("s%d", k), []byte(strings.ToLower(s)), ""))
|
||||
c.Roles = append(c.Roles, Example)
|
||||
taxa.Of = append(taxa.Of, int32(k))
|
||||
}
|
||||
return synthetic{c, taxa, f, r, ins, pos}
|
||||
}
|
||||
|
||||
func (s synthetic) params() SearchParams {
|
||||
p := DefaultSearchParams()
|
||||
p.MaxErrors = 1
|
||||
p.MinLength, p.MaxLength = 20, 100
|
||||
p.Partitions = 8
|
||||
p.TmpDir = ""
|
||||
return p
|
||||
}
|
||||
|
||||
// find returns the pair made of the primers f and r, whatever their order.
|
||||
func find(pairs []PairStats, f, r string) *PairStats {
|
||||
for i := range pairs {
|
||||
if (pairs[i].Primer1 == f && pairs[i].Primer2 == r) || (pairs[i].Primer1 == r && pairs[i].Primer2 == f) {
|
||||
return &pairs[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSearchFindsThePlantedPair(t *testing.T) {
|
||||
s := newSynthetic(8, -1, 11)
|
||||
pairs, info, err := Search(s.corpus, s.taxa, s.params())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := find(pairs, s.f, s.r)
|
||||
if p == nil {
|
||||
t.Fatalf("the planted pair was not found among %d pairs (%+v)", len(pairs), info)
|
||||
}
|
||||
if p.Examples != 8 || p.MinLen != 40 || p.MaxLen != 40 || p.Bs != 1 || p.Bc != 1 {
|
||||
t.Errorf("unexpected indices %+v", *p)
|
||||
}
|
||||
if p.RefLocation != "" {
|
||||
t.Error("no reference was asked: no location expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferenceLocation(t *testing.T) {
|
||||
forwardFirst, reverseFirst := 0, 0
|
||||
for seed := int64(1); seed <= 24; seed++ {
|
||||
s := newSynthetic(8, -1, seed)
|
||||
p := s.params()
|
||||
p.Reference = "s3"
|
||||
pairs, _, err := Search(s.corpus, s.taxa, p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pair := find(pairs, s.f, s.r)
|
||||
if pair == nil {
|
||||
t.Fatalf("seed %d: planted pair not found", seed)
|
||||
}
|
||||
|
||||
// 1-based sites: forward primer at 61..78, then 40 bases of insert, then the reverse site
|
||||
wantLoc := "join(61..78,119..136)"
|
||||
insert := strings.ToLower(string(s.corpus.Seqs[3].Sequence()[s.pos+18 : s.pos+58]))
|
||||
if pair.Primer1 == s.f { // the amplicon is read from the first primer
|
||||
forwardFirst++
|
||||
if pair.RefLocation != wantLoc || pair.RefAmplicon != insert {
|
||||
t.Errorf("seed %d, forward first: got %s %s", seed, pair.RefLocation, pair.RefAmplicon)
|
||||
}
|
||||
} else {
|
||||
reverseFirst++
|
||||
rc := strings.ToLower(revcomp(strings.ToUpper(insert)))
|
||||
if pair.RefLocation != "complement("+wantLoc+")" || pair.RefAmplicon != rc {
|
||||
t.Errorf("seed %d, reverse first: got %s %s", seed, pair.RefLocation, pair.RefAmplicon)
|
||||
}
|
||||
}
|
||||
if len(pair.RefAmplicon) != pair.MinLen {
|
||||
t.Errorf("seed %d: amplicon length %d, pair length %d", seed, len(pair.RefAmplicon), pair.MinLen)
|
||||
}
|
||||
}
|
||||
if forwardFirst == 0 || reverseFirst == 0 {
|
||||
t.Errorf("both orientations must be exercised: %d forward first, %d reverse first", forwardFirst, reverseFirst)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferenceErrors(t *testing.T) {
|
||||
s := newSynthetic(8, -1, 11)
|
||||
p := s.params()
|
||||
p.Reference = "nope"
|
||||
if _, _, err := Search(s.corpus, s.taxa, p); err == nil {
|
||||
t.Error("an unknown reference must be refused")
|
||||
}
|
||||
s.corpus.Roles[2] = CounterExample
|
||||
p.Reference = "s2"
|
||||
if _, _, err := Search(s.corpus, s.taxa, p); err == nil {
|
||||
t.Error("a counterexample cannot be the reference")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferenceOnACircularJunction(t *testing.T) {
|
||||
// span writes a range running over the end of a circular sequence in two parts
|
||||
if got := span(95, 10, 100, true); got != "96..100,1..5" {
|
||||
t.Errorf("circular span %q", got)
|
||||
}
|
||||
if got := span(95, 10, 100, false); got != "96..105" {
|
||||
t.Errorf("linear span %q", got)
|
||||
}
|
||||
if got := span(10, 18, 100, true); got != "11..28" {
|
||||
t.Errorf("inner span %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoMultiMatchDropsRepeatedPrimers(t *testing.T) {
|
||||
// the forward primer is found twice in sequence 2
|
||||
s := newSynthetic(8, 2, 11)
|
||||
|
||||
with, infoWith, err := Search(s.corpus, s.taxa, s.params())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if find(with, s.f, s.r) == nil {
|
||||
t.Fatalf("without -U the pair must be found (%+v)", infoWith)
|
||||
}
|
||||
|
||||
p := s.params()
|
||||
p.NoMultiMatch = true
|
||||
without, info, err := Search(s.corpus, s.taxa, p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if find(without, s.f, s.r) != nil {
|
||||
t.Error("with -U the repeated forward primer must be discarded, and its pair with it")
|
||||
}
|
||||
if info.Repeated == 0 {
|
||||
t.Errorf("the repeated word was not counted: %+v", info)
|
||||
}
|
||||
if len(without) >= len(with) {
|
||||
t.Errorf("-U must not add pairs: %d with, %d without", len(with), len(without))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropMultiMatch(t *testing.T) {
|
||||
wm := []WordMatches{
|
||||
{Word: 1, Matches: []Match{{0, 5, 0, false}, {1, 9, 0, true}}}, // one per sequence: kept
|
||||
{Word: 2, Matches: []Match{{0, 5, 0, false}, {0, 50, 1, false}}}, // two in sequence 0
|
||||
{Word: 3, Matches: []Match{{2, 5, 0, false}, {2, 5, 0, true}}}, // both strands of one sequence
|
||||
{Word: 4}, // no match at all: kept
|
||||
}
|
||||
kept, dropped := DropMultiMatch(wm)
|
||||
if dropped != 2 || len(kept) != 2 || kept[0].Word != 1 || kept[1].Word != 4 {
|
||||
t.Errorf("kept %v, dropped %d", kept, dropped)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropRepeatedWords(t *testing.T) {
|
||||
words := []obikmer.SeqKmerCount{{Kmer: 1}, {Kmer: 2, Repeated: true}, {Kmer: 3}}
|
||||
kept, dropped := DropRepeatedWords(words)
|
||||
if dropped != 1 || len(kept) != 2 || kept[0].Kmer != 1 || kept[1].Kmer != 3 {
|
||||
t.Errorf("kept %v, dropped %d", kept, dropped)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user