Skip to content

Commit 9eaa943

Browse files
author
Koeng101
authored
Pcr (#223)
* PCR design and simulation functional * Moved examples into example test file will full tutorial * Added 100% test coverage
1 parent 963dfbd commit 9eaa943

File tree

3 files changed

+328
-0
lines changed

3 files changed

+328
-0
lines changed

primers/pcr/example_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package pcr_test
2+
3+
import (
4+
"fmt"
5+
"github.com/TimothyStiles/poly/primers/pcr"
6+
)
7+
8+
// This example shows how to design a sequence.
9+
func Example_basic() {
10+
gene := "aataattacaccgagataacacatcatggataaaccgatactcaaagattctatgaagctatttgaggcacttggtacgatcaagtcgcgctcaatgtttggtggcttcggacttttcgctgatgaaacgatgtttgcactggttgtgaatgatcaacttcacatacgagcagaccagcaaacttcatctaacttcgagaagcaagggctaaaaccgtacgtttataaaaagcgtggttttccagtcgttactaagtactacgcgatttccgacgacttgtgggaatccagtgaacgcttgatagaagtagcgaagaagtcgttagaacaagccaatttggaaaaaaagcaacaggcaagtagtaagcccgacaggttgaaagacctgcctaacttacgactagcgactgaacgaatgcttaagaaagctggtataaaatcagttgaacaacttgaagagaaaggtgcattgaatgcttacaaagcgatacgtgactctcactccgcaaaagtaagtattgagctactctgggctttagaaggagcgataaacggcacgcactggagcgtcgttcctcaatctcgcagagaagagctggaaaatgcgctttcttaa"
11+
12+
// Our cloning scheme requires that we add some overhangs, so lets add them
13+
// now.
14+
forwardOverhang := "TTATAGGTCTCATACT"
15+
reverseOverhang := "ATGAAGAGACCATATA"
16+
17+
// Let's design primers with our overhangs for a targetTm of 55.
18+
fwd, rev := pcr.DesignPrimersWithOverhangs(gene, forwardOverhang, reverseOverhang, 55.0)
19+
20+
// Now we want to be sure that our primer set will only amplify our
21+
// target sequence, so we'll simulate a PCR reaction. We'll also want
22+
// to include other sequences that will be in our PCR. For example,
23+
// perhaps we know that `badFragment` will be within our reaction. This
24+
// could be another plasmid, another chromosome, or anything else.
25+
badFragment := "ATGACCATGATTACGCCAAGCTTGCATGCCTGCAGGTCGACTCTAGAGGATCCCCGGGTACCGAGCTCGAATTCACTGGCCGTCGTTTTACAACGTCGTGACTGGGAAAACCCTGGCGTTACCCAACTTAATCGCCTTGCAGCACATCCCCCTTTCGCCAGCTGGCGTAATAGCGAAGAGGCCCGCACCGATCGCCCTTCCCAACAGTTGCGCAGCCTGAATGGCGAATGGCGCCTGATGCGGTATTTTCTCCTTACGCATCTGTGCGGTATTTCACACCGCATATGGTGCACTCTCAGTACAATCTGCTCTGATGCCGCATAG"
26+
fragments, _ := pcr.Simulate([]string{gene, badFragment}, 55.0, false, []string{fwd, rev})
27+
28+
// Now let's make sure it only amplified our target.
29+
if len(fragments) != 1 {
30+
fmt.Println("Failed to amplify a single fragment!")
31+
}
32+
33+
// Else, print out our primers
34+
fmt.Printf("%s, %s\n", fwd, rev)
35+
// Output: TTATAGGTCTCATACTAATAATTACACCGAGATAACACATCATGG, TATATGGTCTCTTCATTTAAGAAAGCGCATTTTCCAGC
36+
}
37+
38+
func ExampleDesignPrimersWithOverhangs() {
39+
gene := "aataattacaccgagataacacatcatggataaaccgatactcaaagattctatgaagctatttgaggcacttggtacgatcaagtcgcgctcaatgtttggtggcttcggacttttcgctgatgaaacgatgtttgcactggttgtgaatgatcaacttcacatacgagcagaccagcaaacttcatctaacttcgagaagcaagggctaaaaccgtacgtttataaaaagcgtggttttccagtcgttactaagtactacgcgatttccgacgacttgtgggaatccagtgaacgcttgatagaagtagcgaagaagtcgttagaacaagccaatttggaaaaaaagcaacaggcaagtagtaagcccgacaggttgaaagacctgcctaacttacgactagcgactgaacgaatgcttaagaaagctggtataaaatcagttgaacaacttgaagagaaaggtgcattgaatgcttacaaagcgatacgtgactctcactccgcaaaagtaagtattgagctactctgggctttagaaggagcgataaacggcacgcactggagcgtcgttcctcaatctcgcagagaagagctggaaaatgcgctttcttaa"
40+
forwardOverhang := "TTATAGGTCTCATACT"
41+
reverseOverhang := "ATGAAGAGACCATATA"
42+
fwd, rev := pcr.DesignPrimersWithOverhangs(gene, forwardOverhang, reverseOverhang, 55.0)
43+
44+
fmt.Printf("%s, %s", fwd, rev)
45+
// Output: TTATAGGTCTCATACTAATAATTACACCGAGATAACACATCATGG, TATATGGTCTCTTCATTTAAGAAAGCGCATTTTCCAGC
46+
}
47+
48+
func ExampleDesignPrimers() {
49+
gene := "aataattacaccgagataacacatcatggataaaccgatactcaaagattctatgaagctatttgaggcacttggtacgatcaagtcgcgctcaatgtttggtggcttcggacttttcgctgatgaaacgatgtttgcactggttgtgaatgatcaacttcacatacgagcagaccagcaaacttcatctaacttcgagaagcaagggctaaaaccgtacgtttataaaaagcgtggttttccagtcgttactaagtactacgcgatttccgacgacttgtgggaatccagtgaacgcttgatagaagtagcgaagaagtcgttagaacaagccaatttggaaaaaaagcaacaggcaagtagtaagcccgacaggttgaaagacctgcctaacttacgactagcgactgaacgaatgcttaagaaagctggtataaaatcagttgaacaacttgaagagaaaggtgcattgaatgcttacaaagcgatacgtgactctcactccgcaaaagtaagtattgagctactctgggctttagaaggagcgataaacggcacgcactggagcgtcgttcctcaatctcgcagagaagagctggaaaatgcgctttcttaa"
50+
fwd, rev := pcr.DesignPrimers(gene, 55.0)
51+
52+
fmt.Printf("%s, %s", fwd, rev)
53+
// Output: AATAATTACACCGAGATAACACATCATGG, TTAAGAAAGCGCATTTTCCAGC
54+
}
55+
56+
func ExampleSimulate() {
57+
gene := "aataattacaccgagataacacatcatggataaaccgatactcaaagattctatgaagctatttgaggcacttggtacgatcaagtcgcgctcaatgtttggtggcttcggacttttcgctgatgaaacgatgtttgcactggttgtgaatgatcaacttcacatacgagcagaccagcaaacttcatctaacttcgagaagcaagggctaaaaccgtacgtttataaaaagcgtggttttccagtcgttactaagtactacgcgatttccgacgacttgtgggaatccagtgaacgcttgatagaagtagcgaagaagtcgttagaacaagccaatttggaaaaaaagcaacaggcaagtagtaagcccgacaggttgaaagacctgcctaacttacgactagcgactgaacgaatgcttaagaaagctggtataaaatcagttgaacaacttgaagagaaaggtgcattgaatgcttacaaagcgatacgtgactctcactccgcaaaagtaagtattgagctactctgggctttagaaggagcgataaacggcacgcactggagcgtcgttcctcaatctcgcagagaagagctggaaaatgcgctttcttaa"
58+
primers := []string{"TTATAGGTCTCATACTAATAATTACACCGAGATAACACATCATGG", "TATATGGTCTCTTCATTTAAGAAAGCGCATTTTCCAGC"}
59+
fragments, _ := pcr.Simulate([]string{gene}, 55.0, false, primers)
60+
61+
fmt.Println(fragments)
62+
// Output: [TTATAGGTCTCATACTAATAATTACACCGAGATAACACATCATGGATAAACCGATACTCAAAGATTCTATGAAGCTATTTGAGGCACTTGGTACGATCAAGTCGCGCTCAATGTTTGGTGGCTTCGGACTTTTCGCTGATGAAACGATGTTTGCACTGGTTGTGAATGATCAACTTCACATACGAGCAGACCAGCAAACTTCATCTAACTTCGAGAAGCAAGGGCTAAAACCGTACGTTTATAAAAAGCGTGGTTTTCCAGTCGTTACTAAGTACTACGCGATTTCCGACGACTTGTGGGAATCCAGTGAACGCTTGATAGAAGTAGCGAAGAAGTCGTTAGAACAAGCCAATTTGGAAAAAAAGCAACAGGCAAGTAGTAAGCCCGACAGGTTGAAAGACCTGCCTAACTTACGACTAGCGACTGAACGAATGCTTAAGAAAGCTGGTATAAAATCAGTTGAACAACTTGAAGAGAAAGGTGCATTGAATGCTTACAAAGCGATACGTGACTCTCACTCCGCAAAAGTAAGTATTGAGCTACTCTGGGCTTTAGAAGGAGCGATAAACGGCACGCACTGGAGCGTCGTTCCTCAATCTCGCAGAGAAGAGCTGGAAAATGCGCTTTCTTAAATGAAGAGACCATATA]
63+
}

primers/pcr/pcr.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
Package pcr designs and simulates simple PCR reactions.
3+
4+
PCR, or polymerase chain reaction, is a method developed in 1983 to copy DNA
5+
templates using small fragments of synthesized single-stranded DNA, amplifying
6+
those DNA templates to ~x1,000,000,000 their starting concentration. These small
7+
fragments, referred to as "primers" or "oligos", can be designed on a computer
8+
and then synthesized for amplifying a variety of different templates.
9+
10+
This package allows users to simulate a PCR reaction or design new primers to
11+
amplify a given template. This package assumes perfect annealing to template at
12+
a target temperature, so should only be used for PCR reactions where this is a
13+
reasonable assumption.
14+
15+
If you are trying to simulate amplification out of a large pool, such as an
16+
oligo pool, use the `Simulate` rather than `SimulateSimple` function to detect
17+
if there is concatemerization happening in your multiplex reaction. In most
18+
other cases, use `SimulateSimple`.
19+
20+
IMPORTANT! The targetTm in all functions is specifically for Taq polymerase.
21+
*/
22+
package pcr
23+
24+
import (
25+
"errors"
26+
"github.com/TimothyStiles/poly/primers"
27+
"github.com/TimothyStiles/poly/transform"
28+
"index/suffixarray"
29+
"sort"
30+
"strings"
31+
)
32+
33+
// https://doi.org/10.1089/dna.1994.13.75
34+
var minimalPrimerLength int = 15
35+
36+
// DesignPrimersWithOverhangs designs two primers to amplify a target sequence,
37+
// adding on an overhang to the forward and reverse strand. This overhang can
38+
// contain additional DNA needed for assembly, like Gibson assembly overhangs
39+
// or GoldenGate restriction enzyme sites.
40+
func DesignPrimersWithOverhangs(sequence, forwardOverhang, reverseOverhang string, targetTm float64) (string, string) {
41+
sequence = strings.ToUpper(sequence)
42+
forwardPrimer := sequence[0:minimalPrimerLength]
43+
for additionalNucleotides := 0; primers.MeltingTemp(forwardPrimer) < targetTm; additionalNucleotides++ {
44+
forwardPrimer = sequence[0 : minimalPrimerLength+additionalNucleotides]
45+
}
46+
reversePrimer := transform.ReverseComplement(sequence[len(sequence)-minimalPrimerLength:])
47+
for additionalNucleotides := 0; primers.MeltingTemp(reversePrimer) < targetTm; additionalNucleotides++ {
48+
reversePrimer = transform.ReverseComplement(sequence[len(sequence)-(minimalPrimerLength+additionalNucleotides):])
49+
}
50+
51+
// Add overhangs to primer
52+
forwardPrimer = forwardOverhang + forwardPrimer
53+
reversePrimer = transform.ReverseComplement(reverseOverhang) + reversePrimer
54+
55+
return forwardPrimer, reversePrimer
56+
}
57+
58+
// DesignPrimers designs two primers to amplify a target sequence and only that
59+
// target sequence (no overhangs).
60+
func DesignPrimers(sequence string, targetTm float64) (string, string) {
61+
return DesignPrimersWithOverhangs(sequence, "", "", targetTm)
62+
}
63+
64+
// SimulateSimple simulates a PCR reaction. It takes in a list of sequences and
65+
// a list of primers, with support for complex multiplex reactions, produces
66+
// a list of all possible PCR fragments from such a reaction. It does not
67+
// detect concatemerization, which could be useful or very detrimental to
68+
// your reactions. The variable `circular` is for if the target template is
69+
// circular, like a plasmid.
70+
func SimulateSimple(sequences []string, targetTm float64, circular bool, primerList []string) []string {
71+
// Set all primers to uppercase.
72+
for primerIndex := range primerList {
73+
primerList[primerIndex] = strings.ToUpper(primerList[primerIndex])
74+
}
75+
76+
var pcrFragments []string
77+
for _, sequence := range sequences {
78+
sequence = strings.ToUpper(sequence)
79+
// Suffix array construction allows function to operate on
80+
// very large sequences without being worried about exeuction
81+
// time. For small sequences, it doesn't really matter.
82+
// https://eli.thegreenplace.net/2016/suffix-arrays-in-the-go-standard-library/
83+
sequenceIndex := suffixarray.New([]byte(sequence))
84+
85+
primerLength := len(primerList)
86+
87+
forwardLocations := make(map[int][]int)
88+
reverseLocations := make(map[int][]int)
89+
minimalPrimers := make([]string, primerLength)
90+
for primerIndex, primer := range primerList {
91+
var minimalLength int
92+
for index := minimalPrimerLength; primers.MeltingTemp(primer[len(primer)-index:]) < targetTm; index++ {
93+
minimalLength = index
94+
if primer[len(primer)-index:] == primer {
95+
break
96+
}
97+
}
98+
// Use the minimal binding sites of the primer to find positions in the template
99+
minimalPrimer := primer[len(primer)-minimalLength:]
100+
if minimalPrimer != primer {
101+
minimalPrimers[primerIndex] = minimalPrimer
102+
// For each primer, we want to look for all possible binding sites in our gene.
103+
// We then append this to a list of binding sites for that primer.
104+
for _, forwardLocation := range sequenceIndex.Lookup([]byte(minimalPrimer), -1) {
105+
forwardLocations[forwardLocation] = append(forwardLocations[forwardLocation], primerIndex)
106+
}
107+
for _, reverseLocation := range sequenceIndex.Lookup([]byte(transform.ReverseComplement(minimalPrimer)), -1) {
108+
reverseLocations[reverseLocation] = append(reverseLocations[reverseLocation], primerIndex)
109+
}
110+
}
111+
}
112+
113+
var forwardLocationInts []int
114+
var reverseLocationInts []int
115+
for key := range forwardLocations {
116+
forwardLocationInts = append(forwardLocationInts, key)
117+
}
118+
for key := range reverseLocations {
119+
reverseLocationInts = append(reverseLocationInts, key)
120+
}
121+
sort.Ints(forwardLocationInts)
122+
sort.Ints(reverseLocationInts)
123+
124+
// Next, iterate through the forwardLocations list
125+
for index, forwardLocation := range forwardLocationInts {
126+
// First, make sure that this isn't the last element in forwardLocations
127+
if index+1 != len(forwardLocationInts) {
128+
// If this isn't the last element in forwardLocations, then we can select the first reverseLocation that is less than the next forwardLocation
129+
for _, reverseLocation := range reverseLocationInts {
130+
if (forwardLocation < reverseLocation) && (reverseLocation < forwardLocationInts[index+1]) {
131+
// If both are true, we have found the sequence we are aiming to PCR! Now, we get all primers from that forwardLocation and then
132+
// build PCR fragments with each one.
133+
pcrFragments = append(pcrFragments, generatePcrFragments(sequence, forwardLocation, reverseLocation, forwardLocations[forwardLocation], reverseLocations[reverseLocation], minimalPrimers, primerList)...)
134+
break
135+
}
136+
}
137+
} else {
138+
foundFragment := false
139+
for _, reverseLocation := range reverseLocationInts {
140+
if forwardLocation < reverseLocation {
141+
pcrFragments = append(pcrFragments, generatePcrFragments(sequence, forwardLocation, reverseLocation, forwardLocations[forwardLocation], reverseLocations[reverseLocation], minimalPrimers, primerList)...)
142+
foundFragment = true
143+
}
144+
}
145+
// If the sequence is circular and we haven't found a fragment yet, check the other side of the origin
146+
if circular && !foundFragment {
147+
for _, reverseLocation := range reverseLocationInts {
148+
if forwardLocationInts[0] > reverseLocation {
149+
// If either one of these are true, create a new pcrFragment and append to pcrFragments
150+
rotatedSequence := sequence[forwardLocation:] + sequence[:forwardLocation]
151+
rotatedForwardLocation := 0
152+
rotatedReverseLocation := len(sequence[forwardLocation:]) + reverseLocation
153+
pcrFragments = append(pcrFragments, generatePcrFragments(rotatedSequence, rotatedForwardLocation, rotatedReverseLocation, forwardLocations[forwardLocation], reverseLocations[reverseLocation], minimalPrimers, primerList)...)
154+
155+
}
156+
}
157+
}
158+
}
159+
}
160+
}
161+
return pcrFragments
162+
}
163+
164+
// Simulate simulates a PCR reaction, including concatemerization analysis. It
165+
// takes in a list of sequences and list of primers, produces all possible PCR
166+
// fragments in a given reaction, and then attempts to see if the output
167+
// fragments can amplify themselves. If they can, concatemerization is occuring
168+
// in your reaction, which can lead to confusing results. The variable
169+
// `circular` is for if the target template is circular, like a plasmid.
170+
func Simulate(sequences []string, targetTm float64, circular bool, primerList []string) ([]string, error) {
171+
initialAmplification := SimulateSimple(sequences, targetTm, circular, primerList)
172+
subsequentAmplification := SimulateSimple(sequences, targetTm, circular, append(primerList, initialAmplification...))
173+
if len(initialAmplification) != len(subsequentAmplification) {
174+
return initialAmplification, errors.New("Concatemerization detected in PCR.")
175+
}
176+
return initialAmplification, nil
177+
}
178+
179+
func generatePcrFragments(sequence string, forwardLocation int, reverseLocation int, forwardPrimerIndxs []int, reversePrimerIndxs []int, minimalPrimers []string, primerList []string) []string {
180+
var pcrFragments []string
181+
for forwardPrimerIndex := range forwardPrimerIndxs {
182+
minimalPrimer := minimalPrimers[forwardPrimerIndex]
183+
fullPrimerForward := primerList[forwardPrimerIndex]
184+
for _, reversePrimerIndex := range reversePrimerIndxs {
185+
fullPrimerReverse := transform.ReverseComplement(primerList[reversePrimerIndex])
186+
pcrFragment := fullPrimerForward[:len(fullPrimerForward)-len(minimalPrimer)] + sequence[forwardLocation:reverseLocation] + fullPrimerReverse
187+
pcrFragments = append(pcrFragments, pcrFragment)
188+
}
189+
}
190+
return pcrFragments
191+
}

primers/pcr/pcr_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package pcr
2+
3+
import (
4+
"testing"
5+
)
6+
7+
// gene is a gene for testing PCR
8+
var gene string = "aataattacaccgagataacacatcatggataaaccgatactcaaagattctatgaagctatttgaggcacttggtacgatcaagtcgcgctcaatgtttggtggcttcggacttttcgctgatgaaacgatgtttgcactggttgtgaatgatcaacttcacatacgagcagaccagcaaacttcatctaacttcgagaagcaagggctaaaaccgtacgtttataaaaagcgtggttttccagtcgttactaagtactacgcgatttccgacgacttgtgggaatccagtgaacgcttgatagaagtagcgaagaagtcgttagaacaagccaatttggaaaaaaagcaacaggcaagtagtaagcccgacaggttgaaagacctgcctaacttacgactagcgactgaacgaatgcttaagaaagctggtataaaatcagttgaacaacttgaagagaaaggtgcattgaatgcttacaaagcgatacgtgactctcactccgcaaaagtaagtattgagctactctgggctttagaaggagcgataaacggcacgcactggagcgtcgttcctcaatctcgcagagaagagctggaaaatgcgctttcttaa"
9+
10+
func TestSimulatePrimerRejection(t *testing.T) {
11+
// CTGCAGGTCGACTCTAG is too low tm for this function, so there is a break in the logic.
12+
primers := []string{"TATATGGTCTCTTCATTTAAGAAAGCGCATTTTCCAGC", "TTATAGGTCTCATACTAATAATTACACCGAGATAACACATCATGG", "CTGCAGGTCGACTCTAG"}
13+
fragments, _ := Simulate([]string{gene}, 55.0, false, primers)
14+
if len(fragments) != 1 {
15+
t.Errorf("Should only have one fragment")
16+
}
17+
}
18+
19+
func TestSimulateMoreThanOneForward(t *testing.T) {
20+
// This tests the first bit of logic in simulate.
21+
// If this primer isn't last forward binding primer AND there is
22+
// another reverse primer binding site.
23+
24+
// gatactcaaagattctatgaagctatttgaggcacttggtacg occurs internally inside of
25+
// gene
26+
internalPrimer := "gatactcaaagattctatgaagctatttgaggcacttggtacg"
27+
28+
// reversePrimer is a different primer from normal that will bind inside
29+
// of gene.
30+
reversePrimer := "tatcgctttgtaagcattcaatgcacctttctcttcaagttg"
31+
32+
// outsideForwardPrimer is a primer that binds out of the range of
33+
// reversePrimer
34+
outsideForwardPrimer := "gtcgttcctcaatctcgcagagaagagctggaaaatg"
35+
36+
primers := []string{internalPrimer, reversePrimer, outsideForwardPrimer}
37+
fragments, _ := Simulate([]string{gene}, 55.0, false, primers)
38+
if len(fragments) != 1 {
39+
t.Errorf("Should only have one fragment")
40+
}
41+
}
42+
43+
func TestSimulateCircular(t *testing.T) {
44+
// This tests for circular simulations.
45+
46+
// forwardPrimer binds near to the end of gene
47+
forwardPrimer := "actctgggctttagaaggagcgataaacggc"
48+
// reversePrimer binds to the beginning of gene, in the opposite direction
49+
reversePrimer := "aagtgcctcaaatagcttcatagaatctttgagtatcgg"
50+
51+
// targetFragment is what the amplification reaction should result in
52+
targetFragment := "ACTCTGGGCTTTAGAAGGAGCGATAAACGGCACGCACTGGAGCGTCGTTCCTCAATCTCGCAGAGAAGAGCTGGAAAATGCGCTTTCTTAAAATAATTACACCGAGATAACACATCATGGATAAACCGATACTCAAAGATTCTATGAAGCTATTTGAGGCACTT"
53+
54+
primers := []string{forwardPrimer, reversePrimer}
55+
fragments, _ := Simulate([]string{gene}, 55.0, true, primers)
56+
if fragments[0] != targetFragment {
57+
t.Errorf("Didn't get target fragment from circular pcr. Expected: %s, got: %s", targetFragment, fragments[0])
58+
}
59+
}
60+
61+
func TestSimulateConcatemerization(t *testing.T) {
62+
// This tests the concatermization detector
63+
64+
forwardPrimer := "AATAATTACACCGAGATAACACATCATGG"
65+
66+
// This reverse primer will add in a forward primer binding site,
67+
// allowing concatemerization
68+
reversePrimer := "CCATGATGTGTTATCTCGGTGTAATTATTTTAAGAAAGCGCATTTTCCAGC"
69+
70+
_, err := Simulate([]string{gene}, 55.0, false, []string{forwardPrimer, reversePrimer})
71+
if err == nil {
72+
t.Errorf("Should have gotten concatemerization")
73+
}
74+
}

0 commit comments

Comments
 (0)