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