JAL-3949 Complete new abstracted logging framework in jalview.log. Updated log calls...
[jalview.git] / src / jalview / analysis / GeneticCodes.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.analysis;
22
23 import java.util.Locale;
24
25 import jalview.bin.Cache;
26
27 import java.io.BufferedReader;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.InputStreamReader;
31 import java.util.HashMap;
32 import java.util.LinkedHashMap;
33 import java.util.Map;
34 import java.util.StringTokenizer;
35
36 /**
37  * A singleton that provides instances of genetic code translation tables
38  * 
39  * @author gmcarstairs
40  * @see https://www.ncbi.nlm.nih.gov/Taxonomy/Utils/wprintgc.cgi
41  */
42 public final class GeneticCodes
43 {
44   private static final int CODON_LENGTH = 3;
45
46   private static final String QUOTE = "\"";
47
48   /*
49    * nucleotides as ordered in data file
50    */
51   private static final String NUCS = "TCAG";
52
53   private static final int NUCS_COUNT = NUCS.length();
54
55   private static final int NUCS_COUNT_SQUARED = NUCS_COUNT * NUCS_COUNT;
56
57   private static final int NUCS_COUNT_CUBED = NUCS_COUNT * NUCS_COUNT
58           * NUCS_COUNT;
59
60   private static final String AMBIGUITY_CODES_FILE = "/AmbiguityCodes.dat";
61
62   private static final String RESOURCE_FILE = "/GeneticCodes.dat";
63
64   private static GeneticCodes instance = new GeneticCodes();
65
66   private Map<String, String> ambiguityCodes;
67
68   /*
69    * loaded code tables, with keys in order of loading 
70    */
71   private Map<String, GeneticCodeI> codeTables;
72
73   /**
74    * Private constructor enforces singleton
75    */
76   private GeneticCodes()
77   {
78     if (instance == null)
79     {
80       ambiguityCodes = new HashMap<>();
81
82       /*
83        * LinkedHashMap preserves order of addition of entries,
84        * so we can assume the Standard Code Table is the first
85        */
86       codeTables = new LinkedHashMap<>();
87       loadAmbiguityCodes(AMBIGUITY_CODES_FILE);
88       loadCodes(RESOURCE_FILE);
89     }
90   }
91
92   /**
93    * Returns the singleton instance of this class
94    * 
95    * @return
96    */
97   public static GeneticCodes getInstance()
98   {
99     return instance;
100   }
101
102   /**
103    * Returns the known code tables, in order of loading.
104    * 
105    * @return
106    */
107   public Iterable<GeneticCodeI> getCodeTables()
108   {
109     return codeTables.values();
110   }
111
112   /**
113    * Answers the code table with the given id
114    * 
115    * @param id
116    * @return
117    */
118   public GeneticCodeI getCodeTable(String id)
119   {
120     return codeTables.get(id);
121   }
122
123   /**
124    * A convenience method that returns the standard code table (table 1). As
125    * implemented, this has to be the first table defined in the data file.
126    * 
127    * @return
128    */
129   public GeneticCodeI getStandardCodeTable()
130   {
131     return codeTables.values().iterator().next();
132   }
133
134   /**
135    * Loads the code tables from a data file
136    */
137   protected void loadCodes(String fileName)
138   {
139     try
140     {
141       InputStream is = getClass().getResourceAsStream(fileName);
142       if (is == null)
143       {
144         System.err.println("Resource file not found: " + fileName);
145         return;
146       }
147       BufferedReader dataIn = new BufferedReader(new InputStreamReader(is));
148
149       /*
150        * skip comments and start of table
151        */
152       String line = "";
153       while (line != null && !line.startsWith("Genetic-code-table"))
154       {
155         line = readLine(dataIn);
156       }
157       line = readLine(dataIn);
158
159       while (line.startsWith("{"))
160       {
161         line = loadOneTable(dataIn);
162       }
163     } catch (IOException | NullPointerException e)
164     {
165       Cache.error(
166               "Error reading genetic codes data file " + fileName + ": "
167               + e.getMessage());
168     }
169     if (codeTables.isEmpty())
170     {
171       System.err.println(
172               "No genetic code tables loaded, check format of file "
173                       + fileName);
174     }
175   }
176
177   /**
178    * Reads and saves Nucleotide ambiguity codes from a data file. The file may
179    * include comment lines (starting with #), a header 'DNA', and one line per
180    * ambiguity code, for example:
181    * <p>
182    * R&lt;tab&gt;AG
183    * <p>
184    * means that R is an ambiguity code meaning "A or G"
185    * 
186    * @param fileName
187    */
188   protected void loadAmbiguityCodes(String fileName)
189   {
190     try
191     {
192       InputStream is = getClass().getResourceAsStream(fileName);
193       if (is == null)
194       {
195         System.err.println("Resource file not found: " + fileName);
196         return;
197       }
198       BufferedReader dataIn = new BufferedReader(new InputStreamReader(is));
199       String line = "";
200       while (line != null)
201       {
202         line = readLine(dataIn);
203         if (line != null && !"DNA".equals(line.toUpperCase(Locale.ROOT)))
204         {
205           String[] tokens = line.split("\\t");
206           if (tokens.length == 2)
207           {
208           ambiguityCodes.put(tokens[0].toUpperCase(Locale.ROOT),
209                   tokens[1].toUpperCase(Locale.ROOT));
210           }
211           else
212           {
213             System.err.println(
214                     "Unexpected data in " + fileName + ": " + line);
215           }
216         }
217       }
218     } catch (IOException e)
219     {
220       Cache.error(
221               "Error reading nucleotide ambiguity codes data file: "
222                       + e.getMessage());
223     }
224   }
225
226   /**
227    * Reads up to and returns the next non-comment line, trimmed. Comment lines
228    * start with a #. Returns null at end of file.
229    * 
230    * @param dataIn
231    * @return
232    * @throws IOException
233    */
234   protected String readLine(BufferedReader dataIn) throws IOException
235   {
236     String line = dataIn.readLine();
237     while (line != null && line.startsWith("#"))
238     {
239       line = readLine(dataIn);
240     }
241     return line == null ? null : line.trim();
242   }
243
244   /**
245    * Reads the lines of the data file describing one translation table, and
246    * creates and stores an instance of GeneticCodeI. Returns the '{' line
247    * starting the next table, or the '}' line at end of all tables. Data format
248    * is
249    * 
250    * <pre>
251    * {
252    *   name "Vertebrate Mitochondrial" ,
253    *   name "SGC1" ,
254    *   id 2 ,
255    *   ncbieaa  "FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSS**VVVVAAAADDEEGGGG",
256    *   sncbieaa "----------**--------------------MMMM----------**---M------------"
257    *   -- Base1  TTTTTTTTTTTTTTTTCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAGGGGGGGGGGGGGGGG
258    *   -- Base2  TTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGG
259    *   -- Base3  TCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAG
260    * },
261    * </pre>
262    * 
263    * of which we parse the first name, the id, and the ncbieaa translations for
264    * codons as ordered by the Base1/2/3 lines. Note Base1/2/3 are included for
265    * readability and are in a fixed order, these are not parsed. The sncbieaa
266    * line marks alternative start codons, these are not parsed.
267    * 
268    * @param dataIn
269    * @return
270    * @throws IOException
271    */
272   protected String loadOneTable(BufferedReader dataIn) throws IOException
273   {
274     String name = null;
275     String id = null;
276     Map<String, String> codons = new HashMap<>();
277
278     String line = readLine(dataIn);
279
280     while (line != null && !line.startsWith("}"))
281     {
282       if (line.startsWith("name") && name == null)
283       {
284         name = line.substring(line.indexOf(QUOTE) + 1,
285                 line.lastIndexOf(QUOTE));
286       }
287       else if (line.startsWith("id"))
288       {
289         id = new StringTokenizer(line.substring(2)).nextToken();
290       }
291       else if (line.startsWith("ncbieaa"))
292       {
293         String aminos = line.substring(line.indexOf(QUOTE) + 1,
294                 line.lastIndexOf(QUOTE));
295         if (aminos.length() != NUCS_COUNT_CUBED) // 4 * 4 * 4 combinations
296         {
297           Cache.error("wrong data length in code table: " + line);
298         }
299         else
300         {
301           for (int i = 0; i < aminos.length(); i++)
302           {
303             String peptide = String.valueOf(aminos.charAt(i));
304             char codon1 = NUCS.charAt(i / NUCS_COUNT_SQUARED);
305             char codon2 = NUCS
306                     .charAt((i % NUCS_COUNT_SQUARED) / NUCS_COUNT);
307             char codon3 = NUCS.charAt(i % NUCS_COUNT);
308             String codon = new String(
309                     new char[]
310                     { codon1, codon2, codon3 });
311             codons.put(codon, peptide);
312           }
313         }
314       }
315       line = readLine(dataIn);
316     }
317
318     registerCodeTable(id, name, codons);
319     return readLine(dataIn);
320   }
321
322   /**
323    * Constructs and registers a GeneticCodeI instance with the codon
324    * translations as defined in the data file. For all instances except the
325    * first, any undeclared translations default to those in the standard code
326    * table.
327    * 
328    * @param id
329    * @param name
330    * @param codons
331    */
332   protected void registerCodeTable(final String id, final String name,
333           final Map<String, String> codons)
334   {
335     codeTables.put(id, new GeneticCodeI()
336     {
337       /*
338        * map of ambiguous codons to their 'product'
339        * (null if not all possible translations match)
340        */
341       Map<String, String> ambiguous = new HashMap<>();
342
343       @Override
344       public String translateCanonical(String codon)
345       {
346         return codons.get(codon.toUpperCase(Locale.ROOT));
347       }
348
349       @Override
350       public String translate(String codon)
351       {
352         String upper = codon.toUpperCase(Locale.ROOT);
353         String peptide = translateCanonical(upper);
354
355         /*
356          * if still not translated, check for ambiguity codes
357          */
358         if (peptide == null)
359         {
360           peptide = getAmbiguousTranslation(upper, ambiguous, this);
361         }
362         return peptide;
363       }
364
365       @Override
366       public String getId()
367       {
368         return id;
369       }
370
371       @Override
372       public String getName()
373       {
374         return name;
375       }
376     });
377   }
378
379   /**
380    * Computes all possible translations of a codon including one or more
381    * ambiguity codes, and stores and returns the result (null if not all
382    * translations match). If the codon includes no ambiguity codes, simply
383    * returns null.
384    * 
385    * @param codon
386    * @param ambiguous
387    * @param codeTable
388    * @return
389    */
390   protected String getAmbiguousTranslation(String codon,
391           Map<String, String> ambiguous, GeneticCodeI codeTable)
392   {
393     if (codon.length() != CODON_LENGTH)
394     {
395       return null;
396     }
397
398     boolean isAmbiguous = false;
399
400     char[][] expanded = new char[CODON_LENGTH][];
401     for (int i = 0; i < CODON_LENGTH; i++)
402     {
403       String base = String.valueOf(codon.charAt(i));
404       if (ambiguityCodes.containsKey(base))
405       {
406         isAmbiguous = true;
407         base = ambiguityCodes.get(base);
408       }
409       expanded[i] = base.toCharArray();
410     }
411
412     if (!isAmbiguous)
413     {
414       // no ambiguity code involved here
415       return null;
416     }
417
418     /*
419      * generate and translate all permutations of the ambiguous codon
420      * only return the translation if they all agree, else null
421      */
422     String peptide = null;
423     for (char c1 : expanded[0])
424     {
425       for (char c2 : expanded[1])
426       {
427         for (char c3 : expanded[2])
428         {
429           char[] cdn = new char[] { c1, c2, c3 };
430           String possibleCodon = String.valueOf(cdn);
431           String pep = codeTable.translate(possibleCodon);
432           if (pep == null || (peptide != null && !pep.equals(peptide)))
433           {
434             ambiguous.put(codon, null);
435             return null;
436           }
437           peptide = pep;
438         }
439       }
440     }
441
442     /*
443      * all translations of ambiguous codons matched!
444      */
445     ambiguous.put(codon, peptide);
446     return peptide;
447   }
448 }