JAL-2416 allow alphabet symbol (optional) in first column of score table
[jalview.git] / src / jalview / analysis / scoremodels / ScoreMatrix.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.scoremodels;
22
23 import jalview.api.analysis.ScoreModelI;
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.Arrays;
30 import java.util.StringTokenizer;
31
32 public class ScoreMatrix extends PairwiseSeqScoreModel implements
33         ScoreModelI
34 {
35   public static final short UNMAPPED = (short) -1;
36
37   private static final String DELIMITERS = " ,\t";
38
39   private static final String COMMENT_CHAR = "#";
40
41   private static final String BAD_ASCII_ERROR = "Unexpected character %s in getPairwiseScore";
42
43   private static final int MAX_ASCII = 127;
44
45   /*
46    * the name of the model as shown in menus
47    */
48   private String name;
49
50   /*
51    * the characters that the model provides scores for
52    */
53   private char[] symbols;
54
55   /*
56    * the score matrix; both dimensions must equal the number of symbols
57    * matrix[i][j] is the substitution score for replacing symbols[i] with symbols[j]
58    */
59   private float[][] matrix;
60
61   /*
62    * quick lookup to convert from an ascii character value to the index
63    * of the corresponding symbol in the score matrix 
64    */
65   private short[] symbolIndex;
66
67   /*
68    * true for Protein Score matrix, false for dna score matrix
69    */
70   private boolean peptide;
71
72   /**
73    * Constructor
74    * 
75    * @param name
76    *          Unique, human readable name for the matrix
77    * @param alphabet
78    *          the symbols to which scores apply
79    * @param matrix
80    *          Pairwise scores indexed according to the symbol alphabet
81    */
82   public ScoreMatrix(String name, char[] alphabet, float[][] matrix)
83   {
84     this.matrix = matrix;
85     this.name = name;
86     this.symbols = alphabet;
87
88     symbolIndex = buildSymbolIndex(alphabet);
89
90     /*
91      * crude heuristic for now...
92      */
93     peptide = alphabet.length >= 20;
94   }
95
96   /**
97    * Returns an array A where A[i] is the position in the alphabet array of the
98    * character whose value is i. For example if the alphabet is { 'A', 'D', 'X'
99    * } then A['D'] = A[68] = 1.
100    * <p>
101    * Unmapped characters (not in the alphabet) get an index of -1.
102    * <p>
103    * Mappings are added automatically for lower case symbols (for non case
104    * sensitive scoring), unless they are explicitly present in the alphabet (are
105    * scored separately in the score matrix).
106    * 
107    * @param alphabet
108    * @return
109    */
110   static short[] buildSymbolIndex(char[] alphabet)
111   {
112     short[] index = new short[MAX_ASCII + 1];
113     Arrays.fill(index, UNMAPPED);
114     short pos = 0;
115     for (char c : alphabet)
116     {
117       if (c <= MAX_ASCII)
118       {
119         index[c] = pos;
120       }
121
122       /*
123        * also map lower-case character (unless separately mapped)
124        */
125       if (c >= 'A' && c <= 'Z')
126       {
127         short lowerCase = (short) (c + ('a' - 'A'));
128         if (index[lowerCase] == UNMAPPED)
129         {
130           index[lowerCase] = pos;
131         }
132       }
133       pos++;
134     }
135     return index;
136   }
137
138   @Override
139   public String getName()
140   {
141     return name;
142   }
143
144   @Override
145   public boolean isDNA()
146   {
147     return !peptide;
148   }
149
150   @Override
151   public boolean isProtein()
152   {
153     return peptide;
154   }
155
156   @Override
157   public float[][] getMatrix()
158   {
159     return matrix;
160   }
161
162   /**
163    * Returns the pairwise score for substituting c with d, or zero if c or d is
164    * an unscored or unexpected character
165    */
166   @Override
167   public float getPairwiseScore(char c, char d)
168   {
169     if (c > MAX_ASCII)
170     {
171       System.err.println(String.format(BAD_ASCII_ERROR, c));
172       return 0;
173     }
174     if (d > MAX_ASCII)
175     {
176       System.err.println(String.format(BAD_ASCII_ERROR, d));
177       return 0;
178     }
179
180     int cIndex = symbolIndex[c];
181     int dIndex = symbolIndex[d];
182     if (cIndex != UNMAPPED && dIndex != UNMAPPED)
183     {
184       return matrix[cIndex][dIndex];
185     }
186     return 0;
187   }
188
189   /**
190    * pretty print the matrix
191    */
192   @Override
193   public String toString()
194   {
195     return outputMatrix(false);
196   }
197
198   /**
199    * Print the score matrix, optionally formatted as html, with the alphabet symbols as column headings and at the start of each row
200    * @param html
201    * @return
202    */
203   public String outputMatrix(boolean html)
204   {
205     StringBuilder sb = new StringBuilder(512);
206
207     /*
208      * heading row with alphabet
209      */
210     if (html)
211     {
212       sb.append("<table border=\"1\">");
213       sb.append(html ? "<tr><th></th>" : "");
214     }
215     for (char sym : symbols)
216     {
217       if (html)
218       {
219         sb.append("<th>&nbsp;").append(sym).append("&nbsp;</th>");
220       }
221       else
222       {
223         sb.append("\t").append(sym);
224       }
225     }
226     sb.append(html ? "</tr>\n" : "\n");
227
228     /*
229      * table of scores
230      */
231     for (char c1 : symbols)
232     {
233       if (html)
234       {
235         sb.append("<tr><td>");
236       }
237       sb.append(c1).append(html ? "</td>" : "");
238       for (char c2 : symbols)
239       {
240         sb.append(html ? "<td>" : "\t")
241                 .append(matrix[symbolIndex[c1]][symbolIndex[c2]])
242                 .append(html ? "</td>" : "");
243       }
244       sb.append(html ? "</tr>\n" : "\n");
245     }
246     if (html)
247     {
248       sb.append("</table>");
249     }
250     return sb.toString();
251   }
252
253   /**
254    * Parse a score matrix from the given input stream and returns a ScoreMatrix
255    * object. If parsing fails, error messages are written to syserr and null is
256    * returned. It is the caller's responsibility to close the input stream.
257    * Expected format:
258    * 
259    * <pre>
260    * ScoreMatrix displayName
261    * # comment lines begin with hash sign
262    * # symbol alphabet should be the next non-comment line
263    * ARNDCQEGHILKMFPSTWYVBZX *
264    * # scores matrix, with space, comma or tab delimited values
265    * # [i, j] = score for substituting symbol[i] with symbol[j]
266    * # first column in each row is optionally the 'substituted' symbol
267    * A 4 -1 -2 -2 0 -1 -1 0 -2 -1 -1 -1 -1 -2 -1 1 0 -3 -2 0 -2 -1 0 -4 -4
268    * ..etc..
269    * </pre>
270    * 
271    * @param is
272    * @return
273    */
274   public static ScoreMatrix parse(InputStream is)
275   {
276     ScoreMatrix sm = null;
277     BufferedReader br = new BufferedReader(new InputStreamReader(is));
278     int lineNo = 0;
279     String name = null;
280     String alphabet = null;
281     float[][] scores = null;
282     int size = 0;
283     int row = 0;
284
285     try
286     {
287       String data;
288
289       while ((data = br.readLine()) != null)
290       {
291         lineNo++;
292         data = data.trim();
293         if (data.startsWith(COMMENT_CHAR))
294         {
295           continue;
296         }
297         if (data.toLowerCase().startsWith("scorematrix"))
298         {
299           /*
300            * Parse name from ScoreMatrix <name>
301            */
302           if (name != null)
303           {
304             System.err
305                     .println("Warning: 'ScoreMatrix' repeated in file at line "
306                             + lineNo);
307           }
308           StringTokenizer nameLine = new StringTokenizer(data, DELIMITERS);
309           if (nameLine.countTokens() != 2)
310           {
311             System.err
312                     .println("Format error: expected 'ScoreMatrix <name>', found '"
313                             + data + "' at line " + lineNo);
314             return null;
315           }
316           nameLine.nextToken();
317           name = nameLine.nextToken();
318           continue;
319         }
320         else if (name == null)
321         {
322           System.err
323                   .println("Format error: 'ScoreMatrix <name>' should be the first non-comment line");
324           return null;
325         }
326
327         /*
328          * next line after ScoreMatrix should be the alphabet of scored symbols
329          */
330         if (alphabet == null)
331         {
332           alphabet = data;
333           size = alphabet.length();
334           scores = new float[size][];
335           continue;
336         }
337
338         /*
339          * too much information?
340          */
341         if (row >= size && data.length() > 0) {
342           System.err
343                   .println("Unexpected extra input line in score model file "
344                           + data);
345           return null;
346         }
347         
348         /*
349          * subsequent lines should be the symbol scores
350          * optionally with the symbol as the first column for readability
351          */
352         StringTokenizer scoreLine = new StringTokenizer(data, DELIMITERS);
353         if (scoreLine.countTokens() == size + 1)
354         {
355           /*
356            * check 'guide' symbol is the row'th letter of the alphabet
357            */
358           String symbol = scoreLine.nextToken();
359           if (symbol.length() > 1
360                   || symbol.charAt(0) != alphabet.charAt(row))
361           {
362             System.err
363                     .println(String
364                             .format("Error parsing score matrix at line %d, expected %s but found %s",
365                                     lineNo, alphabet.charAt(row), symbol));
366             return null;
367           }
368         }
369         if (scoreLine.countTokens() != size)
370         {
371           System.err.println(String.format(
372                   "Expected %d scores at line %d but found %d", size,
373                   lineNo, scoreLine.countTokens()));
374           return null;
375         }
376         scores[row] = new float[size];
377         int col = 0;
378         String value = null;
379         while (scoreLine.hasMoreTokens())
380         {
381           try
382           {
383             value = scoreLine.nextToken();
384             scores[row][col] = Float.valueOf(value);
385             col++;
386           } catch (NumberFormatException e)
387           {
388             System.err.println(String.format(
389                     "Invalid score value %s at line %d column %d", value,
390                     lineNo, col));
391             return null;
392           }
393         }
394         row++;
395       }
396     } catch (IOException e)
397     {
398       System.err.println("Error reading score matrix file: "
399               + e.getMessage() + " at line " + lineNo);
400     }
401
402     /*
403      * out of data - check we found enough
404      */
405     if (row < size)
406     {
407       System.err
408               .println(String
409                       .format("Expected %d rows of score data in score matrix but only found %d",
410                               size, row));
411       return null;
412     }
413
414     /*
415      * If we get here, then name, alphabet and scores have been parsed successfully
416      */
417     sm = new ScoreMatrix(name, alphabet.toCharArray(), scores);
418     return sm;
419   }
420 }