Merge branch 'features/JAL-2393customMatrices' into develop
[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.PairwiseScoreModelI;
24 import jalview.api.analysis.SimilarityParamsI;
25 import jalview.datamodel.AlignmentView;
26 import jalview.math.Matrix;
27 import jalview.math.MatrixI;
28 import jalview.util.Comparison;
29
30 import java.util.Arrays;
31
32 /**
33  * A class that models a substitution score matrix for any given alphabet of
34  * symbols. Instances of this class are immutable and thread-safe.
35  */
36 public class ScoreMatrix extends SimilarityScoreModel implements
37         PairwiseScoreModelI
38 {
39   private static final char GAP_CHARACTER = Comparison.GAP_DASH;
40
41   /*
42    * an arbitrary score to assign for identity of an unknown symbol
43    * (this is the value on the diagonal in the * column of the NCBI matrix)
44    * (though a case could be made for using the minimum diagonal value)
45    */
46   private static final int UNKNOWN_IDENTITY_SCORE = 1;
47
48   /*
49    * Jalview 2.10.1 treated gaps as X (peptide) or N (nucleotide)
50    * for pairwise scoring; 2.10.2 uses gap score (last column) in
51    * score matrix (JAL-2397)
52    * Set this flag to true (via Groovy) for 2.10.1 behaviour
53    */
54   private static boolean scoreGapAsAny = false;
55
56   public static final short UNMAPPED = (short) -1;
57
58   private static final String BAD_ASCII_ERROR = "Unexpected character %s in getPairwiseScore";
59
60   private static final int MAX_ASCII = 127;
61
62   /*
63    * the name of the model as shown in menus
64    * each score model in use should have a unique name
65    */
66   private String name;
67
68   /*
69    * a description for the model as shown in tooltips
70    */
71   private String description;
72
73   /*
74    * the characters that the model provides scores for
75    */
76   private char[] symbols;
77
78   /*
79    * the score matrix; both dimensions must equal the number of symbols
80    * matrix[i][j] is the substitution score for replacing symbols[i] with symbols[j]
81    */
82   private float[][] matrix;
83
84   /*
85    * quick lookup to convert from an ascii character value to the index
86    * of the corresponding symbol in the score matrix 
87    */
88   private short[] symbolIndex;
89
90   /*
91    * true for Protein Score matrix, false for dna score matrix
92    */
93   private boolean peptide;
94
95   private float minValue;
96
97   private float maxValue;
98   
99   /**
100    * Constructor given a name, symbol alphabet, and matrix of scores for pairs
101    * of symbols. The matrix should be square and of the same size as the
102    * alphabet, for example 20x20 for a 20 symbol alphabet.
103    * 
104    * @param theName
105    *          Unique, human readable name for the matrix
106    * @param alphabet
107    *          the symbols to which scores apply
108    * @param values
109    *          Pairwise scores indexed according to the symbol alphabet
110    */
111   public ScoreMatrix(String theName, char[] alphabet, float[][] values)
112   {
113     this(theName, null, alphabet, values);
114   }
115
116   /**
117    * Constructor given a name, description, symbol alphabet, and matrix of
118    * scores for pairs of symbols. The matrix should be square and of the same
119    * size as the alphabet, for example 20x20 for a 20 symbol alphabet.
120    * 
121    * @param theName
122    *          Unique, human readable name for the matrix
123    * @param theDescription
124    *          descriptive display name suitable for use in menus
125    * @param alphabet
126    *          the symbols to which scores apply
127    * @param values
128    *          Pairwise scores indexed according to the symbol alphabet
129    */
130   public ScoreMatrix(String theName, String theDescription,
131           char[] alphabet, float[][] values)
132   {
133     if (alphabet.length != values.length)
134     {
135       throw new IllegalArgumentException(
136               "score matrix size must match alphabet size");
137     }
138     for (float[] row : values)
139     {
140       if (row.length != alphabet.length)
141       {
142         throw new IllegalArgumentException(
143                 "score matrix size must be square");
144       }
145     }
146
147     this.matrix = values;
148     this.name = theName;
149     this.description = theDescription;
150     this.symbols = alphabet;
151
152     symbolIndex = buildSymbolIndex(alphabet);
153
154     findMinMax();
155
156     /*
157      * crude heuristic for now...
158      */
159     peptide = alphabet.length >= 20;
160   }
161
162   /**
163    * Record the minimum and maximum score values
164    */
165   protected void findMinMax()
166   {
167     float min = Float.MAX_VALUE;
168     float max = -Float.MAX_VALUE;
169     if (matrix != null)
170     {
171       for (float[] row : matrix)
172       {
173         if (row != null)
174         {
175           for (float f : row)
176           {
177             min = Math.min(min, f);
178             max = Math.max(max, f);
179           }
180         }
181       }
182     }
183     minValue = min;
184     maxValue = max;
185   }
186
187   /**
188    * Returns an array A where A[i] is the position in the alphabet array of the
189    * character whose value is i. For example if the alphabet is { 'A', 'D', 'X'
190    * } then A['D'] = A[68] = 1.
191    * <p>
192    * Unmapped characters (not in the alphabet) get an index of -1.
193    * <p>
194    * Mappings are added automatically for lower case symbols (for non case
195    * sensitive scoring), unless they are explicitly present in the alphabet (are
196    * scored separately in the score matrix).
197    * <p>
198    * the gap character (space, dash or dot) included in the alphabet (if any) is
199    * recorded in a field
200    * 
201    * @param alphabet
202    * @return
203    */
204   short[] buildSymbolIndex(char[] alphabet)
205   {
206     short[] index = new short[MAX_ASCII + 1];
207     Arrays.fill(index, UNMAPPED);
208     short pos = 0;
209     for (char c : alphabet)
210     {
211       if (c <= MAX_ASCII)
212       {
213         index[c] = pos;
214       }
215
216       /*
217        * also map lower-case character (unless separately mapped)
218        */
219       if (c >= 'A' && c <= 'Z')
220       {
221         short lowerCase = (short) (c + ('a' - 'A'));
222         if (index[lowerCase] == UNMAPPED)
223         {
224           index[lowerCase] = pos;
225         }
226       }
227       pos++;
228     }
229     return index;
230   }
231
232   @Override
233   public String getName()
234   {
235     return name;
236   }
237
238   @Override
239   public String getDescription()
240   {
241     return description;
242   }
243
244   @Override
245   public boolean isDNA()
246   {
247     return !peptide;
248   }
249
250   @Override
251   public boolean isProtein()
252   {
253     return peptide;
254   }
255
256   /**
257    * Returns a copy of the score matrix as used in getPairwiseScore. If using
258    * this matrix directly, callers <em>must</em> also call
259    * <code>getMatrixIndex</code> in order to get the matrix index for each
260    * character (symbol).
261    * 
262    * @return
263    * @see #getMatrixIndex(char)
264    */
265   public float[][] getMatrix()
266   {
267     float[][] v = new float[matrix.length][matrix.length];
268     for (int i = 0; i < matrix.length; i++)
269     {
270       v[i] = Arrays.copyOf(matrix[i], matrix[i].length);
271     }
272     return v;
273   }
274
275   /**
276    * Answers the matrix index for a given character, or -1 if unmapped in the
277    * matrix. Use this method only if using <code>getMatrix</code> in order to
278    * compute scores directly (without symbol lookup) for efficiency.
279    * 
280    * @param c
281    * @return
282    * @see #getMatrix()
283    */
284   public int getMatrixIndex(char c)
285   {
286     if (c < symbolIndex.length)
287     {
288       return symbolIndex[c];
289     }
290     else
291     {
292       return UNMAPPED;
293     }
294   }
295
296   /**
297    * Returns the pairwise score for substituting c with d. If either c or d is
298    * an unexpected character, returns 1 for identity (c == d), else the minimum
299    * score value in the matrix.
300    */
301   @Override
302   public float getPairwiseScore(char c, char d)
303   {
304     if (c >= symbolIndex.length)
305     {
306       System.err.println(String.format(BAD_ASCII_ERROR, c));
307       return 0;
308     }
309     if (d >= symbolIndex.length)
310     {
311       System.err.println(String.format(BAD_ASCII_ERROR, d));
312       return 0;
313     }
314
315     int cIndex = symbolIndex[c];
316     int dIndex = symbolIndex[d];
317     if (cIndex != UNMAPPED && dIndex != UNMAPPED)
318     {
319       return matrix[cIndex][dIndex];
320     }
321
322     /*
323      * one or both symbols not found in the matrix
324      * currently scoring as 1 (for identity) or the minimum
325      * matrix score value (otherwise)
326      * (a case could be made for using minimum row/column value instead)
327      */
328     return c == d ? UNKNOWN_IDENTITY_SCORE : getMinimumScore();
329   }
330
331   /**
332    * pretty print the matrix
333    */
334   @Override
335   public String toString()
336   {
337     return outputMatrix(false);
338   }
339
340   /**
341    * Print the score matrix, optionally formatted as html, with the alphabet
342    * symbols as column headings and at the start of each row.
343    * <p>
344    * The non-html format should give an output which can be parsed as a score
345    * matrix file
346    * 
347    * @param html
348    * @return
349    */
350   public String outputMatrix(boolean html)
351   {
352     StringBuilder sb = new StringBuilder(512);
353
354     /*
355      * heading row with alphabet
356      */
357     if (html)
358     {
359       sb.append("<table border=\"1\">");
360       sb.append(html ? "<tr><th></th>" : "");
361     }
362     else
363     {
364       sb.append("ScoreMatrix ").append(getName()).append("\n");
365     }
366     for (char sym : symbols)
367     {
368       if (html)
369       {
370         sb.append("<th>&nbsp;").append(sym).append("&nbsp;</th>");
371       }
372       else
373       {
374         sb.append("\t").append(sym);
375       }
376     }
377     sb.append(html ? "</tr>\n" : "\n");
378
379     /*
380      * table of scores
381      */
382     for (char c1 : symbols)
383     {
384       if (html)
385       {
386         sb.append("<tr><td>");
387       }
388       sb.append(c1).append(html ? "</td>" : "");
389       for (char c2 : symbols)
390       {
391         sb.append(html ? "<td>" : "\t")
392                 .append(matrix[symbolIndex[c1]][symbolIndex[c2]])
393                 .append(html ? "</td>" : "");
394       }
395       sb.append(html ? "</tr>\n" : "\n");
396     }
397     if (html)
398     {
399       sb.append("</table>");
400     }
401     return sb.toString();
402   }
403
404   /**
405    * Answers the number of symbols coded for (also equal to the number of rows
406    * and columns of the score matrix)
407    * 
408    * @return
409    */
410   public int getSize()
411   {
412     return symbols.length;
413   }
414
415   /**
416    * Computes an NxN matrix where N is the number of sequences, and entry [i, j]
417    * is sequence[i] pairwise multiplied with sequence[j], as a sum of scores
418    * computed using the current score matrix. For example
419    * <ul>
420    * <li>Sequences:</li>
421    * <li>FKL</li>
422    * <li>R-D</li>
423    * <li>QIA</li>
424    * <li>GWC</li>
425    * <li>Score matrix is BLOSUM62</li>
426    * <li>Gaps treated same as X (unknown)</li>
427    * <li>product [0, 0] = F.F + K.K + L.L = 6 + 5 + 4 = 15</li>
428    * <li>product [1, 1] = R.R + -.- + D.D = 5 + -1 + 6 = 10</li>
429    * <li>product [2, 2] = Q.Q + I.I + A.A = 5 + 4 + 4 = 13</li>
430    * <li>product [3, 3] = G.G + W.W + C.C = 6 + 11 + 9 = 26</li>
431    * <li>product[0, 1] = F.R + K.- + L.D = -3 + -1 + -3 = -8
432    * <li>and so on</li>
433    * </ul>
434    * This method is thread-safe.
435    */
436   @Override
437   public MatrixI findSimilarities(AlignmentView seqstrings,
438           SimilarityParamsI options)
439   {
440     char gapChar = scoreGapAsAny ? (seqstrings.isNa() ? 'N' : 'X')
441             : GAP_CHARACTER;
442     String[] seqs = seqstrings.getSequenceStrings(gapChar);
443     return findSimilarities(seqs, options);
444   }
445
446   /**
447    * Computes pairwise similarities of a set of sequences using the given
448    * parameters
449    * 
450    * @param seqs
451    * @param params
452    * @return
453    */
454   protected MatrixI findSimilarities(String[] seqs, SimilarityParamsI params)
455   {
456     double[][] values = new double[seqs.length][];
457     for (int row = 0; row < seqs.length; row++)
458     {
459       values[row] = new double[seqs.length];
460       for (int col = 0; col < seqs.length; col++)
461       {
462         double total = computeSimilarity(seqs[row], seqs[col], params);
463         values[row][col] = total;
464       }
465     }
466     return new Matrix(values);
467   }
468
469   /**
470    * Calculates the pairwise similarity of two strings using the given
471    * calculation parameters
472    * 
473    * @param seq1
474    * @param seq2
475    * @param params
476    * @return
477    */
478   protected double computeSimilarity(String seq1, String seq2,
479           SimilarityParamsI params)
480   {
481     int len1 = seq1.length();
482     int len2 = seq2.length();
483     double total = 0;
484
485     int width = Math.max(len1, len2);
486     for (int i = 0; i < width; i++)
487     {
488       if (i >= len1 || i >= len2)
489       {
490         /*
491          * off the end of one sequence; stop if we are only matching
492          * on the shorter sequence length, else treat as trailing gap
493          */
494         if (params.denominateByShortestLength())
495         {
496           break;
497         }
498       }
499
500       char c1 = i >= len1 ? GAP_CHARACTER : seq1.charAt(i);
501       char c2 = i >= len2 ? GAP_CHARACTER : seq2.charAt(i);
502       boolean gap1 = Comparison.isGap(c1);
503       boolean gap2 = Comparison.isGap(c2);
504
505       if (gap1 && gap2)
506       {
507         /*
508          * gap-gap: include if options say so, else ignore
509          */
510         if (!params.includeGappedColumns())
511         {
512           continue;
513         }
514       }
515       else if (gap1 || gap2)
516       {
517         /*
518          * gap-residue: score if options say so
519          */
520         if (!params.includeGaps())
521         {
522           continue;
523         }
524       }
525       float score = getPairwiseScore(c1, c2);
526       total += score;
527     }
528     return total;
529   }
530
531   /**
532    * Answers a hashcode computed from the symbol alphabet and the matrix score
533    * values
534    */
535   @Override
536   public int hashCode()
537   {
538     int hs = Arrays.hashCode(symbols);
539     for (float[] row : matrix)
540     {
541       hs = hs * 31 + Arrays.hashCode(row);
542     }
543     return hs;
544   }
545
546   /**
547    * Answers true if the argument is a ScoreMatrix with the same symbol alphabet
548    * and score values, else false
549    */
550   @Override
551   public boolean equals(Object obj)
552   {
553     if (!(obj instanceof ScoreMatrix))
554     {
555       return false;
556     }
557     ScoreMatrix sm = (ScoreMatrix) obj;
558     if (Arrays.equals(symbols, sm.symbols)
559             && Arrays.deepEquals(matrix, sm.matrix))
560     {
561       return true;
562     }
563     return false;
564   }
565
566   /**
567    * Returns the alphabet the matrix scores for, as a string of characters
568    * 
569    * @return
570    */
571   String getSymbols()
572   {
573     return new String(symbols);
574   }
575
576   public float getMinimumScore()
577   {
578     return minValue;
579   }
580
581   public float getMaximumScore()
582   {
583     return maxValue;
584   }
585 }