JAL-2416 more validation in ScoreMatrix constructor
[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 java.util.Arrays;
24
25 public class ScoreMatrix implements PairwiseScoreModelI
26 {
27   public static final short UNMAPPED = (short) -1;
28
29   private static final String BAD_ASCII_ERROR = "Unexpected character %s in getPairwiseScore";
30
31   private static final int MAX_ASCII = 127;
32
33   /*
34    * the name of the model as shown in menus
35    */
36   private String name;
37
38   /*
39    * the characters that the model provides scores for
40    */
41   private char[] symbols;
42
43   /*
44    * the score matrix; both dimensions must equal the number of symbols
45    * matrix[i][j] is the substitution score for replacing symbols[i] with symbols[j]
46    */
47   private float[][] matrix;
48
49   /*
50    * quick lookup to convert from an ascii character value to the index
51    * of the corresponding symbol in the score matrix 
52    */
53   private short[] symbolIndex;
54
55   /*
56    * true for Protein Score matrix, false for dna score matrix
57    */
58   private boolean peptide;
59
60   /**
61    * Constructor given a name, symbol alphabet, and matrix of scores for pairs
62    * of symbols. The matrix should be square and of the same size as the
63    * alphabet, for example 20x20 for a 20 symbol alphabet.
64    * 
65    * @param name
66    *          Unique, human readable name for the matrix
67    * @param alphabet
68    *          the symbols to which scores apply
69    * @param matrix
70    *          Pairwise scores indexed according to the symbol alphabet
71    */
72   public ScoreMatrix(String name, char[] alphabet, float[][] matrix)
73   {
74     if (alphabet.length != matrix.length)
75     {
76       throw new IllegalArgumentException(
77               "score matrix size must match alphabet size");
78     }
79     for (float[] row : matrix)
80     {
81       if (row.length != alphabet.length)
82       {
83         throw new IllegalArgumentException(
84                 "score matrix size must be square");
85       }
86     }
87
88     this.matrix = matrix;
89     this.name = name;
90     this.symbols = alphabet;
91
92     symbolIndex = buildSymbolIndex(alphabet);
93
94     /*
95      * crude heuristic for now...
96      */
97     peptide = alphabet.length >= 20;
98   }
99
100   /**
101    * Returns an array A where A[i] is the position in the alphabet array of the
102    * character whose value is i. For example if the alphabet is { 'A', 'D', 'X'
103    * } then A['D'] = A[68] = 1.
104    * <p>
105    * Unmapped characters (not in the alphabet) get an index of -1.
106    * <p>
107    * Mappings are added automatically for lower case symbols (for non case
108    * sensitive scoring), unless they are explicitly present in the alphabet (are
109    * scored separately in the score matrix).
110    * 
111    * @param alphabet
112    * @return
113    */
114   static short[] buildSymbolIndex(char[] alphabet)
115   {
116     short[] index = new short[MAX_ASCII + 1];
117     Arrays.fill(index, UNMAPPED);
118     short pos = 0;
119     for (char c : alphabet)
120     {
121       if (c <= MAX_ASCII)
122       {
123         index[c] = pos;
124       }
125
126       /*
127        * also map lower-case character (unless separately mapped)
128        */
129       if (c >= 'A' && c <= 'Z')
130       {
131         short lowerCase = (short) (c + ('a' - 'A'));
132         if (index[lowerCase] == UNMAPPED)
133         {
134           index[lowerCase] = pos;
135         }
136       }
137       pos++;
138     }
139     return index;
140   }
141
142   @Override
143   public String getName()
144   {
145     return name;
146   }
147
148   @Override
149   public boolean isDNA()
150   {
151     return !peptide;
152   }
153
154   @Override
155   public boolean isProtein()
156   {
157     return peptide;
158   }
159
160   /**
161    * Returns the score matrix as used in getPairwiseScore. If using this matrix
162    * directly, callers <em>must</em> also call <code>getMatrixIndex</code> in
163    * order to get the matrix index for each character (symbol).
164    * 
165    * @return
166    * @see #getMatrixIndex(char)
167    */
168   public float[][] getMatrix()
169   {
170     return matrix;
171   }
172
173   /**
174    * Answers the matrix index for a given character, or -1 if unmapped in the
175    * matrix. Use this method only if using <code>getMatrix</code> in order to
176    * compute scores directly (without symbol lookup) for efficiency.
177    * 
178    * @param c
179    * @return
180    * @see #getMatrix()
181    */
182   public int getMatrixIndex(char c)
183   {
184     if (c < symbolIndex.length)
185     {
186       return symbolIndex[c];
187     }
188     else
189     {
190       return UNMAPPED;
191     }
192   }
193
194   /**
195    * Returns the pairwise score for substituting c with d, or zero if c or d is
196    * an unscored or unexpected character
197    */
198   @Override
199   public float getPairwiseScore(char c, char d)
200   {
201     if (c >= symbolIndex.length)
202     {
203       System.err.println(String.format(BAD_ASCII_ERROR, c));
204       return 0;
205     }
206     if (d >= symbolIndex.length)
207     {
208       System.err.println(String.format(BAD_ASCII_ERROR, d));
209       return 0;
210     }
211
212     int cIndex = symbolIndex[c];
213     int dIndex = symbolIndex[d];
214     if (cIndex != UNMAPPED && dIndex != UNMAPPED)
215     {
216       return matrix[cIndex][dIndex];
217     }
218     return 0;
219   }
220
221   /**
222    * pretty print the matrix
223    */
224   @Override
225   public String toString()
226   {
227     return outputMatrix(false);
228   }
229
230   /**
231    * Print the score matrix, optionally formatted as html, with the alphabet symbols as column headings and at the start of each row
232    * @param html
233    * @return
234    */
235   public String outputMatrix(boolean html)
236   {
237     StringBuilder sb = new StringBuilder(512);
238
239     /*
240      * heading row with alphabet
241      */
242     if (html)
243     {
244       sb.append("<table border=\"1\">");
245       sb.append(html ? "<tr><th></th>" : "");
246     }
247     for (char sym : symbols)
248     {
249       if (html)
250       {
251         sb.append("<th>&nbsp;").append(sym).append("&nbsp;</th>");
252       }
253       else
254       {
255         sb.append("\t").append(sym);
256       }
257     }
258     sb.append(html ? "</tr>\n" : "\n");
259
260     /*
261      * table of scores
262      */
263     for (char c1 : symbols)
264     {
265       if (html)
266       {
267         sb.append("<tr><td>");
268       }
269       sb.append(c1).append(html ? "</td>" : "");
270       for (char c2 : symbols)
271       {
272         sb.append(html ? "<td>" : "\t")
273                 .append(matrix[symbolIndex[c1]][symbolIndex[c2]])
274                 .append(html ? "</td>" : "");
275       }
276       sb.append(html ? "</tr>\n" : "\n");
277     }
278     if (html)
279     {
280       sb.append("</table>");
281     }
282     return sb.toString();
283   }
284
285   /**
286    * Answers the number of symbols coded for (also equal to the number of rows
287    * and columns of the score matrix)
288    * 
289    * @return
290    */
291   public int getSize()
292   {
293     return symbols.length;
294   }
295 }