5c699d1d32def3f538becc6f61975ff7c6b66e2f
[jalview.git] / test / jalview / analysis / scoremodels / ScoreMatrixTest.java
1 package jalview.analysis.scoremodels;
2
3 import static org.testng.Assert.assertEquals;
4 import static org.testng.Assert.assertNotNull;
5 import static org.testng.Assert.assertNotSame;
6 import static org.testng.Assert.assertTrue;
7 import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
8
9 import jalview.api.analysis.SimilarityParamsI;
10 import jalview.io.DataSourceType;
11 import jalview.io.FileParse;
12 import jalview.io.ScoreMatrixFile;
13 import jalview.math.MatrixI;
14 import jalview.schemes.ResidueProperties;
15
16 import java.io.IOException;
17 import java.net.MalformedURLException;
18 import java.util.Arrays;
19
20 import org.testng.annotations.Test;
21
22 public class ScoreMatrixTest
23 {
24   @Test(groups = "Functional")
25   public void testConstructor()
26   {
27     // note score matrix does not have to be symmetric (though it should be!)
28     float[][] scores = new float[3][];
29     scores[0] = new float[] { 1f, 2f, 3f };
30     scores[1] = new float[] { 4f, 5f, 6f };
31     scores[2] = new float[] { 7f, 8f, 9f };
32     ScoreMatrix sm = new ScoreMatrix("Test", "ABC".toCharArray(), scores);
33     assertEquals(sm.getSize(), 3);
34     assertArrayEquals(scores, sm.getMatrix());
35     assertEquals(sm.getPairwiseScore('A', 'a'), 1f);
36     assertEquals(sm.getPairwiseScore('b', 'c'), 6f);
37     assertEquals(sm.getPairwiseScore('c', 'b'), 8f);
38     assertEquals(sm.getPairwiseScore('A', 'D'), 0f);
39     assertEquals(sm.getMatrixIndex('c'), 2);
40     assertEquals(sm.getMatrixIndex(' '), -1);
41   }
42
43   @Test(
44     groups = "Functional",
45     expectedExceptions = { IllegalArgumentException.class })
46   public void testConstructor_matrixTooSmall()
47   {
48     float[][] scores = new float[2][];
49     scores[0] = new float[] { 1f, 2f };
50     scores[1] = new float[] { 3f, 4f };
51     new ScoreMatrix("Test", "ABC".toCharArray(), scores);
52   }
53
54   @Test(
55     groups = "Functional",
56     expectedExceptions = { IllegalArgumentException.class })
57   public void testConstructor_matrixTooBig()
58   {
59     float[][] scores = new float[2][];
60     scores[0] = new float[] { 1f, 2f };
61     scores[1] = new float[] { 3f, 4f };
62     new ScoreMatrix("Test", "A".toCharArray(), scores);
63   }
64
65   @Test(
66     groups = "Functional",
67     expectedExceptions = { IllegalArgumentException.class })
68   public void testConstructor_matrixNotSquare()
69   {
70     float[][] scores = new float[2][];
71     scores[0] = new float[] { 1f, 2f };
72     scores[1] = new float[] { 3f };
73     new ScoreMatrix("Test", "AB".toCharArray(), scores);
74   }
75
76   @Test(groups = "Functional")
77   public void testBuildSymbolIndex()
78   {
79     short[] index = ScoreMatrix.buildSymbolIndex("AX-. yxYp".toCharArray());
80
81     assertEquals(index.length, 128); // ASCII character set size
82
83     assertEquals(index['A'], 0);
84     assertEquals(index['a'], 0); // lower-case mapping added
85     assertEquals(index['X'], 1);
86     assertEquals(index['-'], 2);
87     assertEquals(index['.'], 3);
88     assertEquals(index[' '], 4);
89     assertEquals(index['y'], 5); // lower-case override
90     assertEquals(index['x'], 6); // lower-case override
91     assertEquals(index['Y'], 7);
92     assertEquals(index['p'], 8);
93     assertEquals(index['P'], -1); // lower-case doesn't map upper-case
94
95     /*
96      * check all unmapped symbols have index for unmapped
97      */
98     for (int c = 0; c < index.length; c++)
99     {
100       if (!"AaXx-. Yyp".contains(String.valueOf((char) c)))
101       {
102         assertEquals(index[c], -1);
103       }
104     }
105   }
106
107   /**
108    * check that characters not in the basic ASCII set are simply ignored
109    */
110   @Test(groups = "Functional")
111   public void testBuildSymbolIndex_nonAscii()
112   {
113     char[] weird = new char[] { 128, 245, 'P' };
114     short[] index = ScoreMatrix.buildSymbolIndex(weird);
115     assertEquals(index.length, 128);
116     assertEquals(index['P'], 2);
117     assertEquals(index['p'], 2);
118     for (int c = 0; c < index.length; c++)
119     {
120       if (c != 'P' && c != 'p')
121       {
122         assertEquals(index[c], -1);
123       }
124     }
125   }
126
127   @Test(groups = "Functional")
128   public void testGetMatrix()
129   {
130     ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
131     float[][] m = sm.getMatrix();
132     assertEquals(m.length, sm.getSize());
133     assertEquals(m[2][4], -3f);
134     // verify a defensive copy is returned
135     float[][] m2 = sm.getMatrix();
136     assertNotSame(m, m2);
137     assertTrue(Arrays.deepEquals(m, m2));
138   }
139
140   @Test(groups = "Functional")
141   public void testGetMatrixIndex()
142   {
143     ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
144     assertEquals(sm.getMatrixIndex('A'), 0);
145     assertEquals(sm.getMatrixIndex('R'), 1);
146     assertEquals(sm.getMatrixIndex('r'), 1);
147     assertEquals(sm.getMatrixIndex('N'), 2);
148     assertEquals(sm.getMatrixIndex('D'), 3);
149     assertEquals(sm.getMatrixIndex('X'), 22);
150     assertEquals(sm.getMatrixIndex('x'), 22);
151     assertEquals(sm.getMatrixIndex('-'), 23);
152     assertEquals(sm.getMatrixIndex('*'), 24);
153     assertEquals(sm.getMatrixIndex('.'), -1);
154     assertEquals(sm.getMatrixIndex(' '), -1);
155     assertEquals(sm.getMatrixIndex('?'), -1);
156     assertEquals(sm.getMatrixIndex((char) 128), -1);
157   }
158
159   @Test(groups = "Functional")
160   public void testGetSize()
161   {
162     ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
163     assertEquals(sm.getMatrix().length, sm.getSize());
164   }
165
166   @Test(groups = "Functional")
167   public void testComputePairwiseScores()
168   {
169     /*
170      * NB score matrix expects '-' for gap
171      */
172     String[] seqs = new String[] { "FKL", "R-D", "QIA", "GWC" };
173     ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
174
175     MatrixI pairwise = sm.findSimilarities(seqs, SimilarityParams.Jalview);
176
177     /*
178      * should be NxN where N = number of sequences
179      */
180     assertEquals(pairwise.height(), 4);
181     assertEquals(pairwise.width(), 4);
182
183     /*
184      * should be symmetrical (because BLOSUM62 is)
185      */
186     for (int i = 0; i < pairwise.height(); i++)
187     {
188       for (int j = i + 1; j < pairwise.width(); j++)
189       {
190         assertEquals(pairwise.getValue(i, j), pairwise.getValue(j, i),
191                 String.format("Not symmetric at [%d, %d]", i, j));
192       }
193     }
194     /*
195      * verify expected BLOSUM dot product scores
196      */
197     // F.F + K.K + L.L = 6 + 5 + 4 = 15
198     assertEquals(pairwise.getValue(0, 0), 15d);
199     // R.R + -.- + D.D = 5 + 1 + 6 = 12
200     assertEquals(pairwise.getValue(1, 1), 12d);
201     // Q.Q + I.I + A.A = 5 + 4 + 4 = 13
202     assertEquals(pairwise.getValue(2, 2), 13d);
203     // G.G + W.W + C.C = 6 + 11 + 9 = 26
204     assertEquals(pairwise.getValue(3, 3), 26d);
205     // F.R + K.- + L.D = -3 + -4 + -4 = -11
206     assertEquals(pairwise.getValue(0, 1), -11d);
207     // F.Q + K.I + L.A = -3 + -3 + -1 = -7
208     assertEquals(pairwise.getValue(0, 2), -7d);
209     // F.G + K.W + L.C = -3 + -3 + -1 = -7
210     assertEquals(pairwise.getValue(0, 3), -7d);
211     // R.Q + -.I + D.A = 1 + -4 + -2 = -5
212     assertEquals(pairwise.getValue(1, 2), -5d);
213     // R.G + -.W + D.C = -2 + -4 + -3 = -9
214     assertEquals(pairwise.getValue(1, 3), -9d);
215     // Q.G + I.W + A.C = -2 + -3 + 0 = -5
216     assertEquals(pairwise.getValue(2, 3), -5d);
217   }
218
219   /**
220    * Test that the result of outputMatrix can be reparsed to give an identical
221    * ScoreMatrix
222    * 
223    * @throws IOException
224    * @throws MalformedURLException
225    */
226   @Test(groups = "Functional")
227   public void testOutputMatrix_roundTrip() throws MalformedURLException,
228           IOException
229   {
230     ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
231     String output = sm.outputMatrix(false);
232     FileParse fp = new FileParse(output, DataSourceType.PASTE);
233     ScoreMatrixFile parser = new ScoreMatrixFile(fp);
234     ScoreMatrix sm2 = parser.parseMatrix();
235     assertNotNull(sm2);
236     assertTrue(sm2.equals(sm));
237   }
238
239   @Test(groups = "Functional")
240   public void testEqualsAndHashCode()
241   {
242     ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
243     ScoreMatrix sm2 = new ScoreMatrix(sm.getName(), sm.getSymbols()
244             .toCharArray(), sm.getMatrix());
245     assertTrue(sm.equals(sm2));
246     assertEquals(sm.hashCode(), sm2.hashCode());
247   }
248
249   /**
250    * Tests for scoring options where the longer length of two sequences is used
251    */
252   @Test(groups = "Functional")
253   public void testcomputeSimilarity_matchLongestSequence()
254   {
255     /*
256      * ScoreMatrix expects '-' for gaps
257      */
258     String s1 = "FR-K-S";
259     String s2 = "FS--L";
260     ScoreMatrix blosum = ScoreModels.getInstance().getBlosum62();
261   
262     /*
263      * score gap-gap and gap-char
264      * shorter sequence treated as if with trailing gaps
265      * score = F^F + R^S + -^- + K^- + -^L + S^-
266      * = 6 + -1 + 1 + -4 + -4 + -4 = -6
267      */
268     SimilarityParamsI params = new SimilarityParams(true, true, true, false);
269     assertEquals(blosum.computeSimilarity(s1, s2, params), -6d);
270     // matchGap (arg2) is ignored:
271     params = new SimilarityParams(true, false, true, false);
272     assertEquals(blosum.computeSimilarity(s1, s2, params), -6d);
273   
274     /*
275      * score gap-char but not gap-gap
276      * score = F^F + R^S + 0 + K^- + -^L + S^-
277      * = 6 + -1 + 0 + -4 + -4 + -4 = -7
278      */
279     params = new SimilarityParams(false, true, true, false);
280     assertEquals(blosum.computeSimilarity(s1, s2, params), -7d);
281     // matchGap (arg2) is ignored:
282     params = new SimilarityParams(false, false, true, false);
283     assertEquals(blosum.computeSimilarity(s1, s2, params), -7d);
284   
285     /*
286      * score gap-gap but not gap-char
287      * score = F^F + R^S + -^- + 0 + 0 + 0
288      * = 6 + -1 + 1 = 6
289      */
290     params = new SimilarityParams(true, false, false, false);
291     assertEquals(blosum.computeSimilarity(s1, s2, params), 6d);
292     // matchGap (arg2) is ignored:
293     params = new SimilarityParams(true, true, false, false);
294     assertEquals(blosum.computeSimilarity(s1, s2, params), 6d);
295   
296     /*
297      * score neither gap-gap nor gap-char
298      * score = F^F + R^S + 0 + 0 + 0 + 0
299      * = 6 + -1  = 5
300      */
301     params = new SimilarityParams(false, false, false, false);
302     assertEquals(blosum.computeSimilarity(s1, s2, params), 5d);
303     // matchGap (arg2) is ignored:
304     params = new SimilarityParams(false, true, false, false);
305     assertEquals(blosum.computeSimilarity(s1, s2, params), 5d);
306   }
307
308   /**
309    * Tests for scoring options where only the shorter length of two sequences is
310    * used
311    */
312   @Test(groups = "Functional")
313   public void testcomputeSimilarity_matchShortestSequence()
314   {
315     /*
316      * ScoreMatrix expects '-' for gaps
317      */
318     String s1 = "FR-K-S";
319     String s2 = "FS--L";
320     ScoreMatrix blosum = ScoreModels.getInstance().getBlosum62();
321
322     /*
323      * score gap-gap and gap-char
324      * match shorter sequence only
325      * score = F^F + R^S + -^- + K^- + -^L
326      * = 6 + -1 + 1 + -4 + -4 = -2
327      */
328     SimilarityParamsI params = new SimilarityParams(true, true, true, true);
329     assertEquals(blosum.computeSimilarity(s1, s2, params), -2d);
330     // matchGap (arg2) is ignored:
331     params = new SimilarityParams(true, false, true, true);
332     assertEquals(blosum.computeSimilarity(s1, s2, params), -2d);
333   
334     /*
335      * score gap-char but not gap-gap
336      * score = F^F + R^S + 0 + K^- + -^L
337      * = 6 + -1 + 0 + -4 + -4 = -3
338      */
339     params = new SimilarityParams(false, true, true, true);
340     assertEquals(blosum.computeSimilarity(s1, s2, params), -3d);
341     // matchGap (arg2) is ignored:
342     params = new SimilarityParams(false, false, true, true);
343     assertEquals(blosum.computeSimilarity(s1, s2, params), -3d);
344   
345     /*
346      * score gap-gap but not gap-char
347      * score = F^F + R^S + -^- + 0 + 0
348      * = 6 + -1 + 1 = 6
349      */
350     params = new SimilarityParams(true, false, false, true);
351     assertEquals(blosum.computeSimilarity(s1, s2, params), 6d);
352     // matchGap (arg2) is ignored:
353     params = new SimilarityParams(true, true, false, true);
354     assertEquals(blosum.computeSimilarity(s1, s2, params), 6d);
355   
356     /*
357      * score neither gap-gap nor gap-char
358      * score = F^F + R^S + 0 + 0 + 0
359      * = 6 + -1  = 5
360      */
361     params = new SimilarityParams(false, false, false, true);
362     assertEquals(blosum.computeSimilarity(s1, s2, params), 5d);
363     // matchGap (arg2) is ignored:
364     params = new SimilarityParams(false, true, false, true);
365     assertEquals(blosum.computeSimilarity(s1, s2, params), 5d);
366   }
367
368   @Test(groups = "Functional")
369   public void testSymmetric()
370   {
371     verifySymmetric(ScoreModels.getInstance().getBlosum62());
372     verifySymmetric(ScoreModels.getInstance().getPam250());
373     verifySymmetric(ScoreModels.getInstance().getDefaultModel(false)); // dna
374   }
375
376   private void verifySymmetric(ScoreMatrix sm)
377   {
378     float[][] m = sm.getMatrix();
379     int rows = m.length;
380     for (int row = 0; row < rows; row++)
381     {
382       assertEquals(m[row].length, rows);
383       for (int col = 0; col < rows; col++)
384       {
385         assertEquals(m[row][col], m[col][row], String.format("%s [%s, %s]",
386                 sm.getName(), ResidueProperties.aa[row],
387                 ResidueProperties.aa[col]));
388       }
389     }
390   }
391
392   /**
393    * A test that just asserts the expected values in the Blosum62 score matrix
394    */
395   @Test(groups = "Functional")
396   public void testBlosum62_values()
397   {
398     ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
399
400     /*
401      * verify expected scores against ARNDCQEGHILKMFPSTWYVBZX
402      * scraped from https://www.ncbi.nlm.nih.gov/Class/FieldGuide/BLOSUM62.txt
403      */
404     verifyValues(sm, 'A', new float[] { 4, -1, -2, -2, 0, -1, -1, 0, -2,
405         -1,
406         -1, -1, -1, -2, -1, 1, 0, -3, -2, 0, -2, -1, 0 });
407     verifyValues(sm, 'R', new float[] { -1, 5, 0, -2, -3, 1, 0, -2, 0, -3,
408         -2, 2, -1, -3, -2, -1, -1, -3, -2, -3, -1, 0, -1 });
409     verifyValues(sm, 'N', new float[] { -2, 0, 6, 1, -3, 0, 0, 0, 1, -3,
410         -3,
411         0, -2, -3, -2, 1, 0, -4, -2, -3, 3, 0, -1 });
412     verifyValues(sm, 'D', new float[] { -2, -2, 1, 6, -3, 0, 2, -1, -1, -3,
413         -4, -1, -3, -3, -1, 0, -1, -4, -3, -3, 4, 1, -1 });
414     verifyValues(sm, 'C', new float[] { 0, -3, -3, -3, 9, -3, -4, -3, -3,
415         -1,
416         -1, -3, -1, -2, -3, -1, -1, -2, -2, -1, -3, -3, -2 });
417     verifyValues(sm, 'Q', new float[] { -1, 1, 0, 0, -3, 5, 2, -2, 0, -3,
418         -2,
419         1, 0, -3, -1, 0, -1, -2, -1, -2, 0, 3, -1 });
420     verifyValues(sm, 'E', new float[] { -1, 0, 0, 2, -4, 2, 5, -2, 0, -3,
421         -3,
422         1, -2, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1 });
423     verifyValues(sm, 'G', new float[] { 0, -2, 0, -1, -3, -2, -2, 6, -2,
424         -4,
425         -4, -2, -3, -3, -2, 0, -2, -2, -3, -3, -1, -2, -1 });
426     verifyValues(sm, 'H', new float[] { -2, 0, 1, -1, -3, 0, 0, -2, 8, -3,
427         -3, -1, -2, -1, -2, -1, -2, -2, 2, -3, 0, 0, -1 });
428     verifyValues(sm, 'I', new float[] { -1, -3, -3, -3, -1, -3, -3, -4, -3,
429         4, 2, -3, 1, 0, -3, -2, -1, -3, -1, 3, -3, -3, -1 });
430     verifyValues(sm, 'L', new float[] { -1, -2, -3, -4, -1, -2, -3, -4, -3,
431         2, 4, -2, 2, 0, -3, -2, -1, -2, -1, 1, -4, -3, -1 });
432     verifyValues(sm, 'K', new float[] { -1, 2, 0, -1, -3, 1, 1, -2, -1, -3,
433         -2, 5, -1, -3, -1, 0, -1, -3, -2, -2, 0, 1, -1 });
434     verifyValues(sm, 'M', new float[] { -1, -1, -2, -3, -1, 0, -2, -3, -2,
435         1,
436         2, -1, 5, 0, -2, -1, -1, -1, -1, 1, -3, -1, -1 });
437     verifyValues(sm, 'F', new float[] { -2, -3, -3, -3, -2, -3, -3, -3, -1,
438         0, 0, -3, 0, 6, -4, -2, -2, 1, 3, -1, -3, -3, -1 });
439     verifyValues(sm, 'P', new float[] { -1, -2, -2, -1, -3, -1, -1, -2, -2,
440         -3, -3, -1, -2, -4, 7, -1, -1, -4, -3, -2, -2, -1, -2 });
441     verifyValues(sm, 'S', new float[] { 1, -1, 1, 0, -1, 0, 0, 0, -1, -2,
442         -2,
443         0, -1, -2, -1, 4, 1, -3, -2, -2, 0, 0, 0 });
444     verifyValues(sm, 'T', new float[] { 0, -1, 0, -1, -1, -1, -1, -2, -2,
445         -1,
446         -1, -1, -1, -2, -1, 1, 5, -2, -2, 0, -1, -1, 0 });
447     verifyValues(sm, 'W', new float[] { -3, -3, -4, -4, -2, -2, -3, -2, -2,
448         -3, -2, -3, -1, 1, -4, -3, -2, 11, 2, -3, -4, -3, -2 });
449     verifyValues(sm, 'Y', new float[] { -2, -2, -2, -3, -2, -1, -2, -3, 2,
450         -1, -1, -2, -1, 3, -3, -2, -2, 2, 7, -1, -3, -2, -1 });
451     verifyValues(sm, 'V', new float[] { 0, -3, -3, -3, -1, -2, -2, -3, -3,
452         3,
453         1, -2, 1, -1, -2, -2, 0, -3, -1, 4, -3, -2, -1 });
454     verifyValues(sm, 'B', new float[] { -2, -1, 3, 4, -3, 0, 1, -1, 0, -3,
455         -4, 0, -3, -3, -2, 0, -1, -4, -3, -3, 4, 1, -1 });
456     verifyValues(sm, 'Z', new float[] { -1, 0, 0, 1, -3, 3, 4, -2, 0, -3,
457         -3,
458         1, -1, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1 });
459     verifyValues(sm, 'X', new float[] { 0, -1, -1, -1, -2, -1, -1, -1, -1,
460         -1, -1, -1, -1, -1, -2, 0, 0, -2, -1, -1, -1, -1, -1 });
461   }
462
463   /**
464    * Helper method to check pairwise scores for one residue
465    * 
466    * @param sm
467    * @param res
468    * @param expected
469    *          score values against 'res', in ResidueProperties.aaIndex order
470    */
471   private void verifyValues(ScoreMatrix sm, char res, float[] expected)
472   {
473     for (int j = 0; j < expected.length; j++)
474     {
475       char c2 = ResidueProperties.aa[j].charAt(0);
476       assertEquals(sm.getPairwiseScore(res, c2), expected[j],
477               String.format("%s->%s", res, c2));
478     }
479   }
480 }