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