PageSourceSearch

https://ivaprime.com/static/js/nucleotides.js

js ivaprime.com collected 2026-10-03 21:37:56 UTC 21,638 bytes, 575 lines download raw bytes

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.