1const Nucleotides = new class { 2 constructor() { 3 /** 4 * MAP of complementary single letter codes. 5 */ 6 this.complements = { 7 'A': 'T', 8 'C': 'G', 9 'G': 'C', 10 'T': 'A', 11 'R': 'Y', // A or G -> T or C 12 'Y': 'R', // T or C -> A or G 13 'S': 'S', // G or C -> G or C 14 'W': 'W', // A or T -> A or T 15 'K': 'M', // G or T -> A or C 16 'M': 'K', // A or C -> G or T 17 'B': 'V', // C or G or T -> G or C or A 18 'D': 'H', // A or G or T -> T or C or A 19 'H': 'D', // A or C or T -> T or G or A 20 'V': 'B', // A or C or G -> T or G or C 21 'N': 'N', // any 22 '.': '-', // gap 23 '-': '-' // gap 24 }; 25 26 27 /** 28 * Codon table. DNA -> AA 29 */ 30 this.codonTable = { 31 'GCT': 'A', 'GCC': 'A', 'GCA': 'A', 'GCG': 'A', 32 'TGT': 'C', 'TGC': 'C', 33 'GAT': 'D', 'GAC': 'D', 34 'GAA': 'E', 'GAG': 'E', 35 'TTT': 'F', 'TTC': 'F', 36 'GGT': 'G', 'GGC': 'G', 'GGA': 'G', 'GGG': 'G', 37 'CAT': 'H', 'CAC': 'H', 38 'ATT': 'I', 'ATC': 'I', 'ATA': 'I', 39 'AAA': 'K', 'AAG': 'K', 40 'TTA': 'L', 'TTG': 'L', 'CTT': 'L', 'CTC': 'L', 'CTA': 'L', 'CTG': 'L', 41 'ATG': 'M', 42 'AAT': 'N', 'AAC': 'N', 43 'CCT': 'P', 'CCC': 'P', 'CCA': 'P', 'CCG': 'P', 44 'CAA': 'Q', 'CAG': 'Q', 45 'CGT': 'R', 'CGC': 'R', 'CGA': 'R', 'CGG': 'R', 'AGA': 'R', 'AGG': 'R', 46 'TCT': 'S', 'TCC': 'S', 'TCA': 'S', 'TCG': 'S', 'AGT': 'S', 'AGC': 'S', 47 'ACT': 'T', 'ACC': 'T', 'ACA': 'T', 'ACG': 'T', 48 'GTT': 'V', 'GTC': 'V', 'GTA': 'V', 'GTG': 'V', 49 'TGG': 'W', 50 'TAT': 'Y', 'TAC': 'Y', 51 'TAA': '*', 'TAG': '*', 'TGA': '*' 52 }; 53 54 55 /** 56 * Load data once page is loaded. 57 * Its nicer for the user to immediately see something and then load 58 * these in the background. 59 */ 60 this.commonFeatures = null; 61 this.codonWeights = null; 62 this.commonInsertions = null; 63 document.addEventListener('DOMContentLoaded', function () { 64 fetch('static/data/commonFeatures.json') 65 .then(response => response.json()) 66 .then(json => { 67 Nucleotides.commonFeatures = json; 68 }); 69 70 fetch('static/data/codonWeights.json') 71 .then(response => response.json()) 72 .then(json => { 73 Nucleotides.codonWeights = json; 74 }); 75 76 77 fetch('static/data/commonInsertions.json') 78 .then(response => response.json()) 79 .then(json => { 80 Nucleotides.commonInsertions = json; 81 }); 82 }); 83 }; 84 85 86 /** 87 * Check if a given sequence is a purely nucleotide sequence 88 * 89 * @param {String} inputSequence - Input sequence to be checked 90 * @returns {Boolean} - True for nucleotide sequences, false for anything else 91 */ 92 isNucleotideSequence(inputSequence) { 93 return [...inputSequence].every(char => char in this.complements); 94 }; 95 96 97 /** 98 * Sanitize DNA/RNA sequence leaving only allowed IUPAC nucleotide 99 * one letter codes. 100 * 101 * @param {String} inputSequence - String to be sanitized 102 * @returns {String} - Sanitized string 103 */ 104 sanitizeSequence(inputSequence) { 105 return inputSequence.toUpperCase() 106 .trim() 107 .replace(/[\r\n\t\s]+/g, "") 108 .replace(/[^ACGTURYSWKMBDHVN\.\-]/g, ""); 109 }; 110 111 112 /** 113 * Sanitize DNA sequence leaving only allowed DNA nucleotide 114 * one letter codes. 115 * 116 * @param {String} inputSequence - String to be sanitized 117 * @returns {String} - Sanitized string 118 */ 119 sanitizeDNASequence(inputSequence) { 120 return inputSequence.toUpperCase() 121 .trim() 122 .replace(/[\r\n\t\s]+/g, "") 123 .replace(/[^ACTG\.\-]/g, ""); 124 } 125 126 /** 127 * Sanitize AA sequence leaving only allowed AA 128 * one letter codes (including X, *, and -). 129 * 130 * @param {String} inputSequence - String to be sanitized 131 * @returns {String} - Sanitized string 132 */ 133 sanitizeAASequence(inputSequence) { 134 return inputSequence.toUpperCase() 135 .trim() 136 .replace(/[\r\n\t\s]+/g, "") 137 .replace(/[^ACDEFGHIKLMNPQRSTVWYX\*\-\.]/g, "") 138 } 139 140 141 /** 142 * Create the complementary sequence to a given DNA/RNA sequence. 143 * 144 * @param {String} inputSequence - Template sequence. 145 * @returns {String} - Complementary sequence. 146 */ 147 complementary(inputSequence) { 148 const seq = Nucleotides.sanitizeSequence(inputSequence); 149 let result = new Array(seq.length); 150 151 for (let i = 0; i < seq.length; i++) { 152 result[i] = Nucleotides.complements[seq[i]]; 153 }; 154 155 return result.join(''); 156 }; 157 158 159 /** 160 * Create the reverse complementary sequence to a given DNA/RNA sequence. 161 * 162 * @param {String} inputSequence - Template sequence 163 * @returns {String}
163 - Reverse complementary sequence 164 */ 165 reverseComplementary(inputSequence) { 166 return this.complementary(inputSequence).split("").reverse().join("") 167 }; 168 169 170 /** 171 * Translates a DNA/RNA sequence. Extra nucleotides are ignored. 172 * 173 * @param {String} inputSequence - DNA/RNA Sequence to be translated. 174 * @returns {String} - Translated sequence. 175 */ 176 translate(inputSequence) { 177 // Sanitize then convert RNA to DNA 178 inputSequence = Nucleotides.sanitizeSequence(inputSequence).replace("U", "T") 179 // Iterate over the string and add translated amino acid 180 // 1 letter codes to a string. 181 let outputSequence = ""; 182 for (let i = 0; i < inputSequence.length - (inputSequence.length % 3); i += 3) { 183 outputSequence += Nucleotides.codonTable[inputSequence.slice(i, i+3)] ?? "?"; 184 }; 185 return outputSequence; 186 }; 187 188 189 /** 190 * Optimise amino acid sequence using codon frequency tables 191 * for the specified organism 192 * 193 * @param {String} inputAA - Amino acid sequence to optimise 194 * @param {String} targetOrganism - Organism for which the codons should be optimised 195 * @returns {String} - Optimised DNA sequence 196 */ 197 optimizeAA(inputAA, targetOrganism) { 198 return inputAA 199 .split("") 200 .map(aa => { 201 const possibilities = Object.entries(this.codonWeights[targetOrganism][aa]).map(([value, weight]) => ({ 202 weight: parseFloat(weight), 203 value 204 })); 205 206 const totalWeight = possibilities.reduce((sum, p) => sum + p.weight, 0); 207 208 let rand = Math.random() * totalWeight; 209 for (const p of possibilities) { 210 rand -= p.weight; 211 if (rand <= 0) return p.value; 212 }; 213 }) 214 .join(""); 215 }; 216 217 218 /** 219 * Calculate fraction of GC content of a given sequence. 220 * 221 * @param {String} inputSequence 222 * @returns {Number} - GC fraction 223 */ 224 fractionGC(inputSequence) { 225 const gcCount = (inputSequence.match(/[GC]/g) || []).length; 226 return gcCount / inputSequence.length; 227 }; 228 229 230 /** 231 * Calculate melting temperature of a DNA duplex with specified sequence. 232 * 233 * @param {String} seq - Sequence 234 * @param {String} method - Melting temperature algorithm (oligoCalc, nnSantaLucia) 235 * @returns {Number} - Melting temperature 236 */ 237 getMeltingTemperature(seq, method=null) { 238 method = (method) ? method: UserPreferences.get("TmAlgorithm"); 239 240 // Convert global primer concentration from nM to M 241 const primerConcentrationM = UserPreferences.get("primerConc") * 1E-9; 242 243 const tm = this.meltingTemperatureAlgorithms[method](seq, primerConcentrationM); 244 245 const saltConc = UserPreferences.get("saltConc"); 246 const tmCorrectedSalt = (method !== "oligoCalc" && saltConc && saltConc !== NaN && saltConc !== 0) 247 ? saltCorrections[UserPreferences.get("saltCorr")](tm, seq, saltConc) 248 : tm; 249 250 const dmsoConc = UserPreferences.get("dmsoConc"); 251 const tmCorrectedSaltDMSO = (method !== "oligoCalc" && dmsoConc && dmsoConc !== NaN && dmsoConc !== 0) 252 ? tmCorrectedSalt - 0.6*dmsoConc 253 : tmCorrectedSalt; 254 255 // Clamp output to absolute zero 256 return Math.max(tmCorrectedSaltDMSO, -273.15); 257 }; 258 259 260 meltingTemperatureAlgorithms = { 261 /** 262 * Nearest-neighbour algorithm as described by SantaLucia (1998). 263 * 264 * @param {String} seq - Primer sequence 265 * @param {Number} C - Primer concentration 266 * @returns {Number} - Melting temperature 267 */ 268 nnSantaLucia: (seq, C) => { 269 // Enthalpy data (cal mol-1) 270 const deltaH_dict = { 271 "AA": -7.9E3, 272 "TT": -7.9E3, 273 "AT": -7.2E3, 274 "TA": -7.2E3, 275 "CA": -8.5E3, 276 "TG": -8.5E3, 277 "GT": -8.4E3, 278 "AC": -8.4E3, 279 "CT": -7.8E3, 280 "AG": -7.8E3, 281 "GA": -8.2E3, 282 "TC": -8.2E3, 283 "CG": -10.6E3, 284 "GC": -9.8E3, 285 "GG": -8.0E3, 286 "CC": -8.0E3, 287 }; 288 289 // Entropy data (cal K-1 mol-1) 290 const deltaS_dict = { 291 "AA": -22.2, 292 "TT": -22.2,
293 "AT": -20.4, 294 "TA": -21.3, 295 "CA": -22.7, 296 "TG": -22.7, 297 "GT": -22.4, 298 "AC": -22.4, 299 "CT": -21.0, 300 "AG": -21.0, 301 "GA": -22.2, 302 "TC": -22.2, 303 "CG": -27.2, 304 "GC": -24.4, 305 "GG": -19.9, 306 "CC": -19.9 307 }; 308 309 let deltaH0 = 0; // cal mol-1 enthalpy 310 let deltaS0 = 0; // cal K-1 mol-1 entropy 311 312 /** 313 * Symmetry correction 314 * If the primer is completely symmetric, there is an 315 * entropy gain and the symmetry fraction is different. 316 */ 317 let symmFraction = 4; 318 if (seq === Utilities.complementary(seq)) { 319 deltaS0 += -1.4; 320 symmFraction = 1; 321 }; 322 323 /** 324 * Nucleation term 325 * The first pair to anneal is the nucleation point, but 326 * since G-C bonds are so strong it basically always starts 327 * annealing there. If there is a G or C anywhere in the sequence 328 * add GC contributions otherwise, the AT contributions. 329 */ 330 if (seq.includes("G") || seq.includes("C")) { 331 deltaH0 += 0.1E3; 332 deltaS0 += -2.8; 333 } else { 334 deltaH0 += 2.3E3; 335 deltaS0 += 4.1; 336 }; 337 338 // Loop over the possible pairs and add the contributions 339 for (let pair in deltaH_dict) { 340 const pairCount = Utilities.countSubstringOccurences(pair, seq); 341 deltaH0 += pairCount * deltaH_dict[pair]; 342 deltaS0 += pairCount * deltaS_dict[pair]; 343 }; 344 345 // Ideal gas constant 346 const R = 1.987; // cal mol-1 K-1 347 348 return (deltaH0 / (deltaS0 + R * Math.log(C / symmFraction))) - 273.15; 349 }, 350 351 352 /** 353 * Algorithm from the Oligo Calc online calculator (http://biotools.nubic.northwestern.edu/oligocalc.html) 354 * 355 * @param {String} seq - Primer sequence 356 * @param {Number} C - Not used 357 * @returns {Number} - Melting temperature 358 */ 359 oligoCalc: (seq, C) => { 360 if (seq.length == 0) { 361 return -273.15; 362 }; 363 364 const nrBases = {}; 365 ["G", "C"].forEach(base => { 366 nrBases[base] = Utilities.countSubstringOccurences(base, seq); 367 }); 368 return 64.9 + 41 * (((nrBases["G"] + nrBases["C"]) - 16.4) / seq.length); 369 } 370 }; 371 372 373 saltCorrections = { 374 /** 375 * Schildkraut and Lifosn salt correction 376 * 377 * @param {Number} T1 - Initial melting temperature 378 * @param {Number} seq - Primer sequence 379 * @param {Number} C - Salt concentration 380 * @returns {Number} - Corrected melting tempeature 381 */ 382 SchildkrautLifson : (T1, seq, C) => { 383 return T1 + 16.6 * Math.log(C); 384 }, 385 386 /** 387 * Owczarzy salt correction 388 * 389 * @param {Number} T1 - Initial melting temperature 390 * @param {Number} seq - Primer sequence 391 * @param {Number} C - Salt concentration 392 * @returns {Number} - Corrected melting tempeature 393 */ 394 Owczarzy : (T1, seq, C) => { 395 const fGC = fractionGC(seq); 396 const reciprocT2 = (1/T1) + ((4.29*fGC - 3.95)*1E-5*Math.log(C)) + 9.4*1E-6*(Math.log(C)**2); 397 return 1/reciprocT2; 398 } 399 }; 400 401 402 /** 403 * Check if sequence cotains only ACTG bases 404 * 405 * @param {String} seq - Sequence to validate 406 * @throws {AmbiguousBaseError} 407 */ 408 validatePrimerSequence(seq) { 409 const validBases = new Set(["A", "C", "T", "G"]); 410 411 for (let i = 0, len = seq.length; i < len; i++) { 412 const base = seq[i]; 413 if (!validBases.has(base)) { 414 throw new AmbiguousBaseError(seq) 415 }; 416 }; 417 }; 418 419 420 /** 421 * Detect common features in the specified plasmid sequence and return 422 * a dictionary of all detected features. 423 * 424 * @param {String} plasmidSequence 425 * @returns {Object} 426 */ 427 // TO DO: Detect common features in a specific range 428 detectCommonFeatures(plasmidSequence) { 429 const plasmidSequenceComp = this.complementary(plasmidSequence); 430 let detectedFeatures = {}
431 // Check for common features first in the forward strand then 432 // then once more for the complementary strand 433 for (let i = 0; i < 2; i++) { 434 // Select current sequence in 5'->3' 435 const currentSequence = (i === 0) ? plasmidSequence: plasmidSequenceComp.split("").reverse().join(""); 436 437 // Iterate over all features in the database and check if 438 // they are present 439 for (const commonFeatureIndex in Nucleotides.commonFeatures) { 440 // Get current feature info 441 const commonFeatureDict = Nucleotides.commonFeatures[commonFeatureIndex]; 442 const featureLabel = commonFeatureDict["label"]; 443 const featureSequenceType = commonFeatureDict["sequence type"]; 444 const featureSequence = commonFeatureDict["sequence"]; 445 446 // Initialize regex 447 const regex = new RegExp(featureSequence, 'g'); 448 let match; 449 450 // Check if a feature with the same name is already present and 451 // save its span 452 const similarFeatures = []; 453 // Iterate over the values in the dictionary 454 Object.values(detectedFeatures).forEach((feature) => { 455 if (feature.label == featureLabel) { 456 similarFeatures.push(feature.span); 457 }; 458 }); 459 460 /** 461 * If checking for amino acid sequence 462 */ 463 if (featureSequenceType === "AA") { 464 // Generate reading frames offset by 1 nucleotide 465 const readingFrames = [ 466 Nucleotides.translate(currentSequence), 467 Nucleotides.translate(currentSequence.slice(1) + currentSequence.slice(0, 1)), 468 Nucleotides.translate(currentSequence.slice(2) + currentSequence.slice(0, 2)) 469 ]; 470 // Iterate over reading frames and check for features 471 for (let j = 0; j < 3; j++) { 472 // While there are matches 473 while ((match = regex.exec(readingFrames[j])) !== null) { 474 // Generate new feature info 475 const newFeatureId = Utilities.newUUID(); 476 const newFeatureDirectionality = (i === 0) ? "fwd": "rev"; 477 const newFeatureSpanStart = (i === 0) ? match.index*3 + j + 1: currentSequence.length - j - match.index*3 - featureSequence.length*3 + 1; 478 const newFeaturSpanEnd = newFeatureSpanStart + featureSequence.length*3 - 1; 479 const newFeatureSpan = [newFeatureSpanStart, newFeaturSpanEnd]; 480 481 // Check if any feature in similarFeatures has an 482 // overlapping span with the new feature. If there's 483 // at least one hit, do not add the new feature. 484 let canAdd = true; 485 Object.values(similarFeatures).forEach((similarFeatureSpan) => { 486 if (similarFeatureSpan[0] <= newFeatureSpanStart && newFeaturSpanEnd <= similarFeatureSpan[1]) { 487 canAdd = false; 488 }; 489 }); 490 491 // Add new feature. 492 if (canAdd) { 493 detectedFeatures[newFeatureId] = { 494 label: featureLabel, 495 type: commonFeatureDict["type"], 496 directionality: newFeatureDirectionality, 497 span: newFeatureSpan, 498 translation: featureSequence, 499 note: (commonFeatureDict["note"] !== null) ? commonFeatureDict["note"]: "", 500 color: Utilities.getRandomDefaultColor() 501 }; 502 }; 503 }; 504 }; 505 506 /** 507 * If checking for DNA sequence 508 */ 509 } else if (featureSequenceType === "DNA") { 510 // While there are matches 511 while ((match = regex.exec(currentSequence)) !== null) { 512 // Generate new feature info 513 const newFeatureId = Utilities.newUUID(); 514 const newFeatureDirectionality = (i === 0) ? "fwd": "rev"; 515 const newFeatureSpanStart = (i === 0) ? match.index + 1: currentSequence.length - match.index - featureSequence.length + 1; 516 const newFeaturSpanEnd = newFeatureSpanStart + featureSequence.length - 1; 517 const newFeatureSpan = [newFeatureSpanStart, newFeaturSpanEnd]; 518 519 520 // Check if any feature in similarFeatures has an 521 // overlapping span with the new feature. If there's 522 // at least one hit, do not add the new feature. 523 let canAdd = true;
524 Object.values(similarFeatures).forEach((similarFeatureSpan) => { 525 if (similarFeatureSpan[0] <= newFeatureSpanStart && newFeaturSpanEnd <= similarFeatureSpan[1]) { 526 canAdd = false; 527 }; 528 }); 529 530 // Add new feature. 531 if (canAdd) { 532 detectedFeatures[newFeatureId] = { 533 label: featureLabel, 534 type: commonFeatureDict["type"], 535 directionality: newFeatureDirectionality, 536 span: newFeatureSpan, 537 note: (commonFeatureDict["note"] !== null) ? commonFeatureDict["note"]: "", 538 color: Utilities.getRandomDefaultColor() 539 }; 540 }; 541 }; 542 }; 543 }; 544 }; 545 546 547 return detectedFeatures; 548 }; 549 550 551 estimateProteinMW(sequence) { 552 const aaMass = { 553 A: 71.08, R: 156.19, N: 114.11, D: 115.09, C: 103.15, 554 Q: 128.14, E: 129.12, G: 57.05, H: 137.14, I: 113.16, 555 L: 113.16, K: 128.17, M: 131.19, F: 147.18, P: 97.12, 556 S: 87.08, T: 101.11, W: 186.21, Y: 163.18, V: 99.13 557 }; 558 const WATER_MASS = 18.015; // Average isotopic mass of water (H2O), in Da 559 560 sequence = this.sanitizeAASequence(sequence); 561 562 let mass = 0; 563 for (let i = 0; i < sequence.length; i++) { 564 const aa = sequence[i]; 565 if (aaMass[aa]) mass += aaMass[aa]; 566 }; 567 568 // Add the average isotopic mass of one water molecule 569 if (sequence.length > 0) { 570 mass += WATER_MASS; 571 }; 572 573 return mass; // in Daltons (Da) 574 }; 575};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.