mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-11-09 17:24:41 +00:00
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.
176 lines
5.1 KiB
Go
176 lines
5.1 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|
|
}
|