JAL-2620 alternative genetic code translation tables
[jalview.git] / src / jalview / analysis / GeneticCodes.java
1 package jalview.analysis;
2
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.io.InputStreamReader;
7 import java.util.HashMap;
8 import java.util.LinkedHashMap;
9 import java.util.Map;
10
11 /**
12  * A singleton that provides instances of genetic code translation tables
13  * 
14  * @author gmcarstairs
15  * @see https://www.ncbi.nlm.nih.gov/Taxonomy/Utils/wprintgc.cgi
16  */
17 public class GeneticCodes
18 {
19   private static final String RESOURCE_FILE = "/GeneticCodes.dat";
20
21   private static GeneticCodes instance = new GeneticCodes();
22
23   private Map<String, String> ambiguityCodes;
24
25   /*
26    * loaded code tables, with keys in order of loading 
27    */
28   private Map<String, GeneticCodeI> codeTables;
29
30   /**
31    * Returns the singleton instance of this class
32    * 
33    * @return
34    */
35   public static GeneticCodes getInstance()
36   {
37     return instance;
38   }
39
40   /**
41    * Private constructor enforces singleton
42    */
43   private GeneticCodes()
44   {
45     if (instance == null)
46     {
47       ambiguityCodes = new HashMap<>();
48
49       /*
50        * LinkedHashMap preserves order of addition of entries,
51        * so we can assume the Standard Code Table is the first
52        */
53       codeTables = new LinkedHashMap<>();
54       loadCodes(RESOURCE_FILE);
55     }
56   };
57
58   /**
59    * Returns the known code tables, in order of loading.
60    * 
61    * @return
62    */
63   public Iterable<GeneticCodeI> getCodeTables()
64   {
65     return codeTables.values();
66   }
67
68   /**
69    * Answers the code table with the given id
70    * 
71    * @param id
72    * @return
73    */
74   public GeneticCodeI getCodeTable(String id)
75   {
76     return codeTables.get(id);
77   }
78
79   /**
80    * A convenience method that returns the standard code table (table 1). As
81    * implemented, this has to be the first table defined in the data file.
82    * 
83    * @return
84    */
85   public GeneticCodeI getStandardCodeTable()
86   {
87     return codeTables.values().iterator().next();
88   }
89
90   /**
91    * Loads the code tables from a data file
92    */
93   protected void loadCodes(String fileName)
94   {
95     try
96     {
97       InputStream is = getClass().getResourceAsStream(fileName);
98       BufferedReader dataIn = new BufferedReader(new InputStreamReader(is));
99
100       String line = loadAmbiguityCodes(dataIn);
101
102       do
103       {
104         line = loadOneTable(line, dataIn);
105       } while (line != null);
106     } catch (IOException e)
107     {
108       System.err.println("Error reading genetic codes data file: "
109               + e.getMessage());
110     }
111   }
112
113   /**
114    * Reads for header line "Ambiguity Codes" and saves following data up to the
115    * first "Table". Returns the next ("Table") line.
116    * 
117    * @param dataIn
118    * @return
119    * @throws IOException
120    */
121   protected String loadAmbiguityCodes(BufferedReader dataIn)
122           throws IOException
123   {
124     /*
125      * get first non-comment line
126      */
127     String line = readLine(dataIn);
128     if (line == null || !line.toUpperCase().startsWith("AMBIGUITY"))
129     {
130       return line;
131     }
132     while (true)
133     {
134       line = readLine(dataIn);
135       if (line == null || line.toUpperCase().startsWith("TABLE"))
136       {
137         return line;
138       }
139       String[] tokens = line.split("\\t");
140       ambiguityCodes.put(tokens[0].toUpperCase(), tokens[1].toUpperCase());
141     }
142   }
143
144   /**
145    * Reads up to and returns the next non-comment line. Comment lines start with
146    * a #.
147    * 
148    * @param dataIn
149    * @return
150    * @throws IOException
151    */
152   protected String readLine(BufferedReader dataIn) throws IOException
153   {
154     String line = dataIn.readLine();
155     while (line != null && line.startsWith("#"))
156     {
157       line = readLine(dataIn);
158     }
159     return line;
160   }
161
162   /**
163    * Reads the next lines of the data file describing one translation table, and
164    * creates an instance of GeneticCodeI for it. Returns the next line of the
165    * file (or null at end of file).
166    * 
167    * @param nextLine
168    * 
169    * @param dataIn
170    * @return
171    * @throws IOException
172    */
173   protected String loadOneTable(String nextLine, BufferedReader dataIn) throws IOException
174   {
175     String line = nextLine;
176     if (line == null)
177     {
178       return null;
179     }
180     
181     /*
182      * next line should be tab-delimited "Table", id and description
183      */
184     String[] tokens = line.split("\\t");
185     String id = tokens[1];
186     String name = tokens[2];
187
188     /*
189      * followed by codon translations
190      * - the full set for the first (Standard) code
191      * - variations (if any) for other codes
192      */
193     Map<String, String> codons = new HashMap<>();
194     while (true)
195     {
196       line = readLine(dataIn);
197       if (line == null)
198       {
199         registerCodeTable(id, name, codons);
200         return null;
201       }
202       tokens = line.split("\\t");
203       String codon = tokens[0];
204       String peptide = tokens[1];
205       if ("Table".equalsIgnoreCase(codon))
206       {
207         /*
208          * start of next code table - construct this one,
209          * and return the next line of the data file
210          */
211         registerCodeTable(id, name, codons);
212         return line;
213       }
214       codons.put(codon.toUpperCase(), peptide.toUpperCase());
215     }
216   }
217
218   /**
219    * Constructs and registers a GeneticCodeI instance with the codon
220    * translations as defined in the data file. For all instances except the
221    * first, any undeclared translations default to those in the standard code
222    * table.
223    * 
224    * @param id
225    * @param name
226    * @param codons
227    */
228   protected void registerCodeTable(final String id, final String name,
229           final Map<String, String> codons)
230   {
231     codeTables.put(id, new GeneticCodeI()
232     {
233       /*
234        * map of ambiguous codons to their 'product'
235        * (null if not all possible translations match)
236        */
237       Map<String, String> ambiguous = new HashMap<>();
238
239       @Override
240       public String translateCanonical(String codon)
241       {
242         codon = codon.toUpperCase();
243         String peptide = codons.get(codon);
244         if (peptide == null)
245         {
246           /*
247            * delegate an unspecified codon to the Standard Table, 
248            * (unless this is the Standard Table!)
249            * but don't delegate ambiguity resolution
250            */
251           GeneticCodeI standardCodeTable = getStandardCodeTable();
252           if (this != standardCodeTable)
253           {
254             peptide = standardCodeTable.translateCanonical(codon);
255           }
256         }
257         return peptide;
258       }
259
260       @Override
261       public String translate(String codon)
262       {
263         codon = codon.toUpperCase();
264         String peptide = translateCanonical(codon);
265
266         /*
267          * if still not translated, check for ambiguity codes
268          */
269         if (peptide == null)
270         {
271           peptide = getAmbiguousTranslation(codon, ambiguous, this);
272         }
273
274         return peptide;
275       }
276
277       @Override
278       public String getId()
279       {
280         return id;
281       }
282
283       @Override
284       public String getName()
285       {
286         return name;
287       }
288     });
289   }
290
291   /**
292    * Computes all possible translations of a codon including one or more
293    * ambiguity codes, and stores and returns the result (null if not all
294    * translations match). If the codon includes no ambiguity codes, simply
295    * returns null.
296    * 
297    * @param codon
298    * @param ambiguous
299    * @param codeTable
300    * @return
301    */
302   protected String getAmbiguousTranslation(String codon,
303           Map<String, String> ambiguous, GeneticCodeI codeTable)
304   {
305     if (codon.length() != 3)
306     {
307       return null;
308     }
309
310     boolean isAmbiguous = false;
311     String base1 = String.valueOf(codon.charAt(0));
312     if (ambiguityCodes.containsKey(base1))
313     {
314       isAmbiguous = true;
315       base1 = ambiguityCodes.get(base1);
316     }
317     String base2 = String.valueOf(codon.charAt(1));
318     if (ambiguityCodes.containsKey(base2))
319     {
320       isAmbiguous = true;
321       base2 = ambiguityCodes.get(base2);
322     }
323     String base3 = String.valueOf(codon.charAt(2));
324     if (ambiguityCodes.containsKey(base3))
325     {
326       isAmbiguous = true;
327       base3 = ambiguityCodes.get(base3);
328     }
329
330     if (!isAmbiguous)
331     {
332       // no ambiguity code involved here
333       return null;
334     }
335
336     /*
337      * generate and translate all permutations of the ambiguous codon
338      * only return the translation if they all agree, else null
339      */
340     String peptide = null;
341     for (char c1 : base1.toCharArray())
342     {
343       for (char c2 : base2.toCharArray())
344       {
345         for (char c3 : base3.toCharArray())
346         {
347           char[] cdn = new char[] { c1, c2, c3 };
348           String possibleCodon = String.valueOf(cdn);
349           String pep = codeTable.translate(possibleCodon);
350           if (pep == null || (peptide != null && !pep.equals(peptide)))
351           {
352             ambiguous.put(codon, null);
353             return null;
354           }
355           peptide = pep;
356         }
357       }
358     }
359
360     /*
361      * all translations of ambiguous codons matched!
362      */
363     ambiguous.put(codon, peptide);
364     return peptide;
365   }
366 }