Team:EPF-Lausanne/PrimerDesignHelper.js
From 2012.igem.org
window.tails = [ { name: "Biobrick (gene)", value: "biobrickGene", primer1Tail: "GTT TCT TCG AAT TCG CGG CCG CTT CTA G", primer2Tail: "GTT TCT TCC TGC AGC GGC CGC TAC TAG TA TTA TTA", messages: [ "Don't forget to include the first ATG and to exclude the stop codon!", "The recommended minimum length of a BioBrick primer is 20bp", "The recommended Tm of a BioBrick primer is between 55 and 65 °C", "Check that the following restriction sites are absent: EcoRI, SpeI, XbaI, PstI, NotI", "Check out <a href='http://openwetware.org/wiki/Synthetic_Biology:BioBricks/Part_fabrication'>OpenWetWare's reference page</a>" ], validate: function(primer1, primer2){ var start = primer1.substr(0, 3).toUpperCase();
if(start != "ATG"){ return "The starting ATG seems to be absent." } var end = primer2.substr(0, 3).toUpperCase(); // TAA TGA TAG if(end == "TTA" || end == "TCA" || end == "CTA"){ return "Remove the stop codons from the sequence!" } } }, { name: "Biobrick (other)", value: "biobrickOther", primer1Tail: "GTT TCT TCG AAT TCG CGG CCG CTT CTA GAG", primer2Tail: "GTT TCT TCC TGC AGC GGC CGC TAC TAG TA", messages: [ "The recommended minimum length of a BioBrick primer is 20bp", "The recommended Tm of a BioBrick primer is between 55 and 65 °C", "Check that the following restriction sites are absent: EcoRI, SpeI, XbaI, PstI, NotI", "Check out <a href='http://openwetware.org/wiki/Synthetic_Biology:BioBricks/Part_fabrication'>OpenWetWare's reference page</a>" ] } ]; window.calc = function(){ var data = document.getElementById("data").value; var isNormal = document.getElementById("primerTypeNormal").checked; var isCustom = document.getElementById("primerTypeCustom").checked; var isRS = document.getElementById("primerTypeRestrictionSites").checked; var isKnown = document.getElementById("primerTypeKnownTail").checked; var isNumbered = document.getElementById("toggleLines").checked; var goalTm = document.getElementById("goalTm").value;
try{ var messages = []; var toAdd = {}; var p = new Primer(data); var validate = function(primer1, primer2){};
if(isCustom){ p.setTails(Sequence.fromString(document.getElementById("primer1Tail").value), Sequence.fromString(document.getElementById("primer2Tail").value)); }
if(isRS){ var rs1 = Sequence.fromString(document.getElementById("primer1RS").value); var rs2 = Sequence.fromString(document.getElementById("primer2RS").value);
var match1 = rs1.findRS(); var match2 = rs2.findRS(); toAdd.primer1RSMatch = match1.length > 0 ? "- "+match1 : ""; toAdd.primer2RSMatch = match2.length > 0 ? "- "+match2 : "";
p.setTails(rs1, rs2.reverseComplement()); p.addRandomEdges(4);
messages.push("Click the 'Calculate' button to regenerate the random bases added on the end of the primers."); messages.push( "In the case of a plasmid where the piece between A and B is cut out, and A is used for the primer 1 RS (and B for the second). " + "The generated primers will put the RS such that the given DNA starts at the A side and ends at the B side (in short, " + "it will probably work as you expected, but still check what happens with some other tool)." ); }
if(isKnown){ var selected = $("#knownTailSelect").val();
var tail1 = ""; var tail2 = "";
for(var i = 0; i < window.tails.length; i++){ if(window.tails[i].value == selected){ tail1 = window.tails[i].primer1Tail; tail2 = window.tails[i].primer2Tail; if(window.tails[i].messages) messages = [].concat(window.tails[i].messages); if(window.tails[i].validate) validate = window.tails[i].validate; } }
p.setTails(Sequence.fromString(tail1), Sequence.fromString(tail2)); }
if(!p.isSameAsLast(data, goalTm) || window.isCurrentlyNumbered != isNumbered){ window.isCurrentlyNumbered = isNumbered; var primers = p.generate(goalTm);
for(var key in toAdd){ primers[key] = toAdd[key]; }
var result = validate(primers.primer1NoTail, primers.primer2NoTail);
if(result) messages.push(result);
if(messages.length > 0){ window.output({ primers: primers, messages: messages, numbered: isNumbered }); }else{ window.output({ primers: primers, numbered: isNumbered }); } } }catch(e){ window.output({ error: e.message, numbered: isNumbered }); } }; window.isCurrentlyNumbered = -1; window.output = function(options){ var output = { message: [], primer1: "No data", primer1Temp: "NaN", primer1bp: "NaN", primer1Tailbp: "NaN", primer1RSMatch: "", primer2: "No data", primer2Temp: "NaN", primer2bp: "NaN", primer2Tailbp: "NaN", primer2RSMatch: "", full: "", numbered: false }; if(options.numbered){ output.numbered = options.numbered; }
if(options.error){ output.message.push(options.error); } if(options.messages){ output.message = output.message.concat(options.messages); } if(options.primers){ for(var key in options.primers){ if(key == "message"){ output[key].push(options.primers[key]); }else{ output[key] = options.primers[key]; } } }
if(Math.abs(output.primer1Temp - output.primer2Temp) > 5){ output.message.push("The difference is more than 5 °C"); }
if(Math.min(output.primer1Temp, output.primer2Temp) > 72){ output.message.push("Consider using the two-step PCR protocol"); }
if(Math.min(output.primer1Temp, output.primer2Temp) < 45){ output.message.push("The annealing temperature should be more than 45 °C"); }
document.getElementById("primer1RSMatch").innerHTML = output.primer1RSMatch; document.getElementById("primer2RSMatch").innerHTML = output.primer2RSMatch;
document.getElementById("messages").innerHTML = output.message.length > 0 ? "- "+output.message.join("
- ")+"
document.getElementById("primer1").innerHTML = output.primer1; document.getElementById("primer1Temp").innerHTML = output.primer1Temp; document.getElementById("primer1bp").innerHTML = output.primer1Tailbp != 0 && output.primer1Tailbp != "NaN" ? output.primer1Tailbp+" + "+output.primer1bp : output.primer1bp; document.getElementById("primer2").innerHTML = output.primer2; document.getElementById("primer2Temp").innerHTML = output.primer2Temp; document.getElementById("primer2bp").innerHTML = output.primer2Tailbp != 0 && output.primer2Tailbp != "NaN" ? output.primer2Tailbp+" + "+output.primer2bp : output.primer2bp; document.getElementById("annealingTemp").innerHTML = Math.min(output.primer1Temp, output.primer2Temp);
var width = 80; var lines = window.wrap(output.full, width); var outputLines = []; if(output.numbered){ for(var i = 0; i < lines.length; i++){ outputLines[i] = window.pad(i*width+1, 6, true)+" - "+window.pad((i+1)*width, 6)+" "+lines[i]; } }else{ outputLines = lines; } document.getElementById("finalResult").innerHTML = outputLines.join("\n"); };
window.pad = function(str, width, right){ str += ""; while(str.length < width){ if(right){ str = " "+str; }else{ str += " "; } } return str; }
window.wrap = function(str, width){ var chunks = []; var i = 0, l = str.length; while(i < l){ if(i + width >= l){ chunks.push(str.substr(i, l-i)); }else{ chunks.push(str.substr(i, width)); } i += width; } return chunks; };
window.updateRadioElements = function(){ if(document.getElementById('primerTypeRestrictionSites').checked){ $('#restrictionSiteRow').show(); }else{ $('#restrictionSiteRow').hide(); } if(document.getElementById('primerTypeCustom').checked){ $('#customRow').show(); }else{ $('#customRow').hide(); } if(document.getElementById('primerTypeKnownTail').checked){ $('#knownTailRow').show(); }else{ $('#knownTailRow').hide(); } };var Primer;
Primer = (function() {
Primer.minLength = 8;
Primer.maxLength = 50;
Primer.maxDiff = 8;
Primer.lastData = "";
Primer.lastTail1 = "";
Primer.lastTail2 = "";
Primer.lastTm = 0;
Primer.lastRandEdgeSize = -1;
Primer.reset = function() { this.lastData = ""; this.lastTm = 0; this.lastTail1 = ""; this.lastTail2 = ""; return this.lastRandEdgeSize = -1; };
function Primer(data) { this.seq = Sequence.fromString(data); this.tail1 = new Sequence([]); this.tail2 = new Sequence([]); this.randEdgeSize = 0; }
Primer.prototype.setTails = function(tail1, tail2) { this.tail1 = tail1; this.tail2 = tail2; };
Primer.prototype.addRandomEdges = function(randEdgeSize) { this.randEdgeSize = randEdgeSize; };
Primer.prototype.isSameAsLast = function(data, tm) { return Primer.lastTail1.toString() === this.tail1.toString() && Primer.lastTail2.toString() === this.tail2.toString() && Primer.lastData === Sequence.fromString(data).toString() && Primer.lastTm === tm && Primer.lastRandEdgeSize === this.randEdgeSize; };
Primer.prototype.generate = function(goal) { var diff, l1, l2, maxLength, primer1Length, primer1Temp, primer2Length, primer2Temp, rand1, rand2, revCompl, temp, _i, _j, _ref, _ref1; if (2 * Primer.minLength > this.seq.length()) { throw new Error("The given sequence is too short"); } Primer.lastData = this.seq.toString(); Primer.lastTm = goal; Primer.lastTail1 = this.tail1; Primer.lastTail2 = this.tail2; Primer.lastRandEdgeSize = this.randEdgeSize; maxLength = Math.min(Math.floor(this.seq.length() / 2), Primer.maxLength); revCompl = this.seq.reverseComplement(); primer1Length = -1; primer1Temp = 0; primer2Length = -1; primer2Temp = 0; for (l1 = _i = _ref = Primer.minLength; _ref <= maxLength ? _i <= maxLength : _i >= maxLength; l1 = _ref <= maxLength ? ++_i : --_i) { temp = this.seq.snip(l1).tm(); diff = Math.abs(temp - goal); if (diff <= Primer.maxDiff && (primer1Length === -1 || Math.abs(primer1Temp - goal) > diff)) { primer1Length = l1; primer1Temp = temp; } if (temp >= goal) { break; } } for (l2 = _j = _ref1 = Primer.minLength; _ref1 <= maxLength ? _j <= maxLength : _j >= maxLength; l2 = _ref1 <= maxLength ? ++_j : --_j) { temp = revCompl.snip(l2).tm(); diff = Math.abs(temp - goal); if (diff <= Primer.maxDiff && (primer2Length === -1 || Math.abs(primer2Temp - goal) > diff)) { primer2Length = l2; primer2Temp = temp; } if (temp >= goal) { break; } } if (primer1Length < 0 || primer2Length < 0) { throw new Error("Couldn't find a primer of correct length that satisfies the conditions"); } rand1 = Sequence.fromRandom(this.randEdgeSize); rand2 = Sequence.fromRandom(this.randEdgeSize); return { primer1: rand1.toString() + " " + this.tail1.toString() + " " + this.seq.snip(primer1Length).toString(), primer1NoTail: this.seq.snip(primer1Length).toString(), primer1Temp: primer1Temp, primer1bp: primer1Length, primer1Tailbp: rand1.length() + this.tail1.length(), primer2: rand2.toString() + " " + this.tail2.toString() + " " + revCompl.snip(primer2Length).toString(), primer2NoTail: revCompl.snip(primer2Length).toString(), primer2Temp: primer2Temp, primer2bp: primer2Length, primer2Tailbp: rand2.length() + this.tail2.length(), full: rand1.toString() + this.tail1.toString() + this.seq.toString() + this.tail2.reverseComplement().toString() + rand2.reverseComplement().toString() }; };
return Primer;
})(); /* Immutable class that makes it possible to work with (very simplified) DNA sequences
- /
var Sequence,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
Sequence = (function() {
Sequence.allowed = ["A", "C", "G", "T"];
Sequence.RS = { "ATCGAT": "ClaI", "GGATCC": "BamHI", "AGATCT": "BglII", "TTTAAA": "DraI", "GAATTC": "EcoRI", "GATATC": "EcoRV", "AAGCTT": "HindIII", "GCGGCCGC": "NotI", "CTGCAG": "PstI", "GTCCAG": "SalI", "CCCGGG": "SmaI/XmaI", "ACTAGT": "SpeI" };
Sequence.fromString = function(data) { var seq; seq = new Sequence(data.toUpperCase().replace(/\s/ig, "").split()); seq.validateData(); return seq; };
Sequence.fromRandom = function(length) { var data, i; if (length === 0) { return new Sequence([]); } else { data = (function() { var _i, _results; _results = []; for (i = _i = 1; 1 <= length ? _i <= length : _i >= length; i = 1 <= length ? ++_i : --_i) { _results.push(Sequence.allowed[Math.floor(Math.random() * Sequence.allowed.length)]); } return _results; })(); return new Sequence(data); } };
/* The constructor assumes the data has been validated (for performance reasons -> premature and non-consequent optimization ^^). Anyway, the Sequence.fromString function is probably what you were looking for! */
function Sequence(data) { this.data = data; }
Sequence.prototype.validateData = function() { if (this.data.some(function(x) { return __indexOf.call(Sequence.allowed, x) < 0; })) { throw new Error("Invalid character encountered"); } };
Sequence.prototype.findRS = function() { var key; key = this.data.join(""); if (Sequence.RS[key]) { return Sequence.RS[key]; } else { return ""; } };
Sequence.prototype.length = function() { return this.data.length; };
Sequence.prototype.snip = function(length) { if (length >= 0 && length <= this.data.length) { return new Sequence(this.data.slice(0, length)); } else { throw new Error("Invalid snip length (tried to snip a piece of length " + length + " in a sequence of " + this.length() + "bp)"); } };
Sequence.prototype.reverseComplement = function() { var complement, k, l, revCompl, v, _i, _len, _ref; revCompl = []; complement = function(x) { switch (x) { case "A": return "T"; case "T": return "A"; case "C": return "G"; case "G": return "C"; default: throw new Error("Invalid character encountered (" + x + ")"); } }; l = this.data.length - 1; _ref = this.data; for (k = _i = 0, _len = _ref.length; _i < _len; k = ++_i) { v = _ref[k]; revCompl[l - k] = complement(v); } return new Sequence(revCompl); };
Sequence.prototype.append = function(seqToAdd) { return new Sequence(this.data.concat(seqToAdd.data)); };
Sequence.prototype.tm = function() { return TmCalc.calc(this.data); };
Sequence.prototype.toString = function() { return this.data.join(""); };
return Sequence;
})(); /* Container class that helps with Tm calculations. It has been seperated to enable easy improvements to the algorithm.
- /
var TmCalc;
TmCalc = (function() {
function TmCalc() {}
TmCalc.maxCacheSize = 500;
TmCalc.cache = {};
TmCalc.calc = function(data) { var key, keys, result; if (!this.cache[data]) { result = Math.round(this.calcFull(data) * 10) / 10; if (this.cache.length >= this.maxCacheSize) { keys = Object.keys(TmCalc.cache); key = keys[Math.floor(Math.random() * keys.length)]; delete TmCalc.cache[key]; } this.cache[data] = result; } return this.cache[data]; };
TmCalc.complement = function(n) { switch (n) { case "A": return "T"; case "T": return "A"; case "C": return "G"; case "G": return "C"; default: throw new Error("Invalid character encountered (" + n + ")"); } };
TmCalc.neighborH = { AA: 9.1, AT: 8.6, TA: 6.0, CA: 5.8, GT: 6.5, CT: 7.8, GA: 5.6, CG: 11.9, GC: 11.1, GG: 11.0 };
TmCalc.getH = function(n1, n2) { return this.get(this.neighborH, n1, n2); };
TmCalc.neighborG = { AA: 1.9, AT: 1.5, TA: 0.9, CA: 1.9, GT: 1.3, CT: 1.6, GA: 1.6, CG: 3.6, GC: 3.1, GG: 3.1 };
TmCalc.getG = function(n1, n2) { return this.get(this.neighborG, n1, n2); };
TmCalc.neighborS = { AA: 24.0, AT: 23.9, TA: 16.9, CA: 12.9, GT: 17.3, CT: 20.8, GA: 13.5, CG: 27.8, GC: 26.7, GG: 26.6 };
TmCalc.getS = function(n1, n2) { return this.get(this.neighborS, n1, n2); };
TmCalc.get = function(searchVar, n1, n2) { switch (n1 + n2) { case "AA": return searchVar["AA"]; case "TT": return searchVar["AA"]; case "AT": return searchVar["AT"]; case "TA": return searchVar["TA"]; case "TT": return searchVar["TA"]; case "CA": return searchVar["CA"]; case "TG": return searchVar["CA"]; case "GT": return searchVar["GT"]; case "AC": return searchVar["GT"]; case "CT": return searchVar["CT"]; case "AG": return searchVar["CT"]; case "GA": return searchVar["GA"]; case "TC": return searchVar["GA"]; case "CG": return searchVar["CG"]; case "TC": return searchVar["CG"]; case "GC": return searchVar["GC"]; case "TC": return searchVar["GC"]; case "GG": return searchVar["GG"]; case "CC": return searchVar["GG"]; default: throw new Error("No idea what happened... (tried getting " + n1 + n2 + ")"); } };
TmCalc.calcFull = function(data) { /* See (Breslauer et al. 1986): http://www.pnas.org/content/83/11/3746.full.pdf */
var R, i, n1, n2, naConc, predG, predH, predS, primerConc, _i, _ref; predH = 0; predG = -(5 + 0.4); predS = 0; primerConc = 500 * 10e-9; naConc = 0.03; R = 1.987; n1 = data[0]; for (i = _i = 1, _ref = data.length - 1; 1 <= _ref ? _i <= _ref : _i >= _ref; i = 1 <= _ref ? ++_i : --_i) { n2 = data[i]; predH += this.getH(n1, n2); predG += this.getG(n1, n2); predS += this.getS(n1, n2); n1 = n2; } return (1000 * (predH - 3.4) / (predS + R * Math.log(1 / primerConc))) - 272.9 + 7.21 * Math.log(naConc / 1000) / Math.log(10); };
/* Again, old @calcFull: (data, options) -> ## For more details see: http://www.basic.northwestern.edu/biotools/oligocalc.html ## freq = @calcFrequencies(data) if not options options = {} if not options.Na options.Na = 0.05 GC = freq["G"] + freq["C"] AT = freq["A"] + freq["T"] N = data.length if N < 14 AT*2 + GC*4 - 16.6*Math.log(0.050)/Math.log(10) + 16.6*Math.log(options.Na)/Math.log(10) else if N < 50 #18 100.5 + (41 * GC/N) - (820/N) + 16.6*Math.log(options.Na)/Math.log(10) #else if N < 50 # 81.5 + (41 * GC/N) - (500/N) + 16.6*Math.log(options.Na) - 0.62*options.F else 79.8 + 18.5*Math.log(options.Na)/Math.log(10) + (58.4 * GC/N) + (11.8 * (GC/N) * (GC/N)) - (820/N) @calcFrequencies: (data) -> freq = {A: 0, C: 0, G: 0, T: 0} data.forEach (x) -> freq[x]++ freq */
return TmCalc;
})();
/* Old version (not very accurate):
@calc: (data) -> Math.round(@calcFull(data)*100)/100 @calcFull: (data) -> freq = @calcFrequencies(data) if data.length < 14 4 * (freq["G"] + freq["C"]) + 2 * (freq["A"] + freq["T"]) else 64.9 + 41 * (freq["G"] + freq["C"] - 16.4)/data.length @calcFrequencies: (data) -> freq = {A: 0, C: 0, G: 0, T: 0} data.forEach (x) -> freq[x]++ freq
- /