From: kiramt Date: Tue, 7 Mar 2017 10:01:14 +0000 (+0000) Subject: Merge remote-tracking branch 'origin/develop' into features/JAL-2388OverviewWindow X-Git-Tag: Release_2_10_2~3^2~92^2~49 X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=14a3cc9277d30d14bec559a12d9af72bd2955aff;hp=366b1c4a421d8c8455668b06e24620c2101d8a4b;p=jalview.git Merge remote-tracking branch 'origin/develop' into features/JAL-2388OverviewWindow --- diff --git a/src/jalview/analysis/PCA.java b/src/jalview/analysis/PCA.java index eaea7bf..9babaee 100755 --- a/src/jalview/analysis/PCA.java +++ b/src/jalview/analysis/PCA.java @@ -20,9 +20,7 @@ */ package jalview.analysis; -import jalview.datamodel.BinarySequence; -import jalview.datamodel.BinarySequence.InvalidSequenceTypeException; -import jalview.math.Matrix; +import jalview.math.MatrixI; import jalview.schemes.ResidueProperties; import jalview.schemes.ScoreMatrix; @@ -30,23 +28,22 @@ import java.io.PrintStream; /** * Performs Principal Component Analysis on given sequences - * - * @author $author$ - * @version $Revision$ */ public class PCA implements Runnable { - Matrix m; - - Matrix symm; + boolean jvCalcMode = true; - Matrix m2; + MatrixI symm; double[] eigenvalue; - Matrix eigenvector; + MatrixI eigenvector; - StringBuffer details = new StringBuffer(); + StringBuilder details = new StringBuilder(1024); + + private String[] seqs; + + private ScoreMatrix scoreMatrix; /** * Creates a new PCA object. By default, uses blosum62 matrix to generate @@ -77,87 +74,23 @@ public class PCA implements Runnable public PCA(String[] s, boolean nucleotides, String s_m) { + this.seqs = s; - BinarySequence[] bs = new BinarySequence[s.length]; - int ii = 0; - - while ((ii < s.length) && (s[ii] != null)) - { - bs[ii] = new BinarySequence(s[ii], nucleotides); - bs[ii].encode(); - ii++; - } - - BinarySequence[] bs2 = new BinarySequence[s.length]; - ii = 0; - ScoreMatrix smtrx = null; + scoreMatrix = null; String sm = s_m; if (sm != null) { - smtrx = ResidueProperties.getScoreMatrix(sm); + scoreMatrix = ResidueProperties.getScoreMatrix(sm); } - if (smtrx == null) + if (scoreMatrix == null) { // either we were given a non-existent score matrix or a scoremodel that // isn't based on a pairwise symbol score matrix - smtrx = ResidueProperties.getScoreMatrix(sm = (nucleotides ? "DNA" - : "BLOSUM62")); + scoreMatrix = ResidueProperties + .getScoreMatrix(sm = (nucleotides ? "DNA" : "BLOSUM62")); } details.append("PCA calculation using " + sm + " sequence similarity matrix\n========\n\n"); - while ((ii < s.length) && (s[ii] != null)) - { - bs2[ii] = new BinarySequence(s[ii], nucleotides); - if (smtrx != null) - { - try - { - bs2[ii].matrixEncode(smtrx); - } catch (InvalidSequenceTypeException x) - { - details.append("Unexpected mismatch of sequence type and score matrix. Calculation will not be valid!\n\n"); - } - } - ii++; - } - - // System.out.println("Created binary encoding"); - // printMemory(rt); - int count = 0; - - while ((count < bs.length) && (bs[count] != null)) - { - count++; - } - - double[][] seqmat = new double[count][bs[0].getDBinary().length]; - double[][] seqmat2 = new double[count][bs2[0].getDBinary().length]; - int i = 0; - - while (i < count) - { - seqmat[i] = bs[i].getDBinary(); - seqmat2[i] = bs2[i].getDBinary(); - i++; - } - - // System.out.println("Created array"); - // printMemory(rt); - // System.out.println(" --- Original matrix ---- "); - m = new Matrix(seqmat); - m2 = new Matrix(seqmat2); - - } - - /** - * Returns the matrix used in PCA calculation - * - * @return java.math.Matrix object - */ - - public Matrix getM() - { - return m; } /** @@ -170,7 +103,7 @@ public class PCA implements Runnable */ public double getEigenvalue(int i) { - return eigenvector.d[i]; + return eigenvector.getD()[i]; } /** @@ -189,9 +122,9 @@ public class PCA implements Runnable */ public float[][] getComponents(int l, int n, int mm, float factor) { - float[][] out = new float[m.rows][3]; + float[][] out = new float[getHeight()][3]; - for (int i = 0; i < m.rows; i++) + for (int i = 0; i < getHeight(); i++) { out[i][0] = (float) component(i, l) * factor; out[i][1] = (float) component(i, n) * factor; @@ -212,9 +145,9 @@ public class PCA implements Runnable public double[] component(int n) { // n = index of eigenvector - double[] out = new double[m.rows]; + double[] out = new double[getHeight()]; - for (int i = 0; i < m.rows; i++) + for (int i = 0; i < out.length; i++) { out[i] = component(i, n); } @@ -236,12 +169,12 @@ public class PCA implements Runnable { double out = 0.0; - for (int i = 0; i < symm.cols; i++) + for (int i = 0; i < symm.width(); i++) { - out += (symm.value[row][i] * eigenvector.value[i][n]); + out += (symm.getValue(row, i) * eigenvector.getValue(i, n)); } - return out / eigenvector.d[n]; + return out / eigenvector.getD()[n]; } public String getDetails() @@ -270,25 +203,17 @@ public class PCA implements Runnable } }; + // long now = System.currentTimeMillis(); try { details.append("PCA Calculation Mode is " + (jvCalcMode ? "Jalview variant" : "Original SeqSpace") + "\n"); - Matrix mt = m.transpose(); - details.append(" --- OrigT * Orig ---- \n"); - if (!jvCalcMode) - { - eigenvector = mt.preMultiply(m); // standard seqspace comparison matrix - } - else - { - eigenvector = mt.preMultiply(m2); // jalview variation on seqsmace - // method - } + eigenvector = scoreMatrix.computePairwiseScores(seqs); - eigenvector.print(ps); + details.append(" --- OrigT * Orig ---- \n"); + eigenvector.print(ps, "%8.2f"); symm = eigenvector.copy(); @@ -296,10 +221,10 @@ public class PCA implements Runnable details.append(" ---Tridiag transform matrix ---\n"); details.append(" --- D vector ---\n"); - eigenvector.printD(ps); + eigenvector.printD(ps, "%15.4e"); ps.println(); details.append("--- E vector ---\n"); - eigenvector.printE(ps); + eigenvector.printE(ps, "%15.4e"); ps.println(); // Now produce the diagonalization matrix @@ -313,9 +238,9 @@ public class PCA implements Runnable } details.append(" --- New diagonalization matrix ---\n"); - eigenvector.print(ps); + eigenvector.print(ps, "%8.2f"); details.append(" --- Eigenvalues ---\n"); - eigenvector.printD(ps); + eigenvector.printD(ps, "%15.4e"); ps.println(); /* * for (int seq=0;seq + * Note that this container keeps its mappings in an array data structure, using + * a binary search to find keys. The implementation is not intended to be + * appropriate for data structures that may contain large numbers of items. It + * is generally slower than a traditional HashMap, since lookups require a + * binary search and adds and removes require inserting and deleting entries in + * the array. For containers holding up to hundreds of items, the performance + * difference is not significant, less than 50%. + *

+ * + *

+ * It is possible to iterate over the items in this container using + * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using + * keyAt(int) with ascending values of the index will return the + * keys in ascending order, or the values corresponding to the keys in ascending + * order in the case of valueAt(int). + *

+ */ + +/* + * Change log: + * Jan 2017 cloned from SparseIntArray for Jalview to support SparseMatrix + * - SparseDoubleArray(double[]) constructor added + * - multiply() added for more efficient multiply (or divide) of a value + */ +public class SparseDoubleArray implements Cloneable +{ + private int[] mKeys; + + private double[] mValues; + + private int mSize; + + /** + * Creates a new SparseDoubleArray containing no mappings. + */ + public SparseDoubleArray() + { + this(10); + } + + /** + * Creates a new SparseDoubleArray containing no mappings that will not + * require any additional memory allocation to store the specified number of + * mappings. If you supply an initial capacity of 0, the sparse array will be + * initialized with a light-weight representation not requiring any additional + * array allocations. + */ + public SparseDoubleArray(int initialCapacity) + { + if (initialCapacity == 0) + { + mKeys = ContainerHelpers.EMPTY_INTS; + mValues = ContainerHelpers.EMPTY_DOUBLES; + } + else + { + initialCapacity = idealDoubleArraySize(initialCapacity); + mKeys = new int[initialCapacity]; + mValues = new double[initialCapacity]; + } + mSize = 0; + } + + /** + * Constructor given an array of double values; stores the non-zero values + * + * @param row + */ + public SparseDoubleArray(double[] row) + { + this(); + for (int i = 0; i < row.length; i++) + { + if (row[i] != 0d) + { + put(i, row[i]); + } + } + } + + @Override + public SparseDoubleArray clone() + { + SparseDoubleArray clone = null; + try + { + clone = (SparseDoubleArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) + { + /* ignore */ + } + return clone; + } + + /** + * Gets the value mapped from the specified key, or 0 if no such + * mapping has been made. + */ + public double get(int key) + { + return get(key, 0d); + } + + /** + * Gets the int mapped from the specified key, or the specified value if no + * such mapping has been made. + */ + public double get(int key, double valueIfKeyNotFound) + { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + if (i < 0) + { + return valueIfKeyNotFound; + } + else + { + return mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) + { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + if (i >= 0) + { + removeAt(i); + } + } + + /** + * Removes the mapping at the given index. + */ + public void removeAt(int index) + { + System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); + System.arraycopy(mValues, index + 1, mValues, index, mSize + - (index + 1)); + mSize--; + } + + /** + * Adds a mapping from the specified key to the specified value, replacing the + * previous mapping from the specified key if there was one. + */ + public void put(int key, double value) + { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + if (i >= 0) + { + mValues[i] = value; + } + else + { + i = ~i; + if (mSize >= mKeys.length) + { + int n = idealDoubleArraySize(mSize + 1); + int[] nkeys = new int[n]; + double[] nvalues = new double[n]; + // Log.e("SparseDoubleArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + mKeys = nkeys; + mValues = nvalues; + } + if (mSize - i != 0) + { + // Log.e("SparseDoubleArray", "move " + (mSize - i)); + System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); + System.arraycopy(mValues, i, mValues, i + 1, mSize - i); + } + mKeys[i] = key; + mValues[i] = value; + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseDoubleArray + * currently stores. + */ + public int size() + { + return mSize; + } + + /** + * Given an index in the range 0...size()-1, returns the key from + * the indexth key-value mapping that this SparseDoubleArray + * stores. + * + *

+ * The keys corresponding to indices in ascending order are guaranteed to be + * in ascending order, e.g., keyAt(0) will return the smallest + * key and keyAt(size()-1) will return the largest key. + *

+ */ + public int keyAt(int index) + { + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns the value + * from the indexth key-value mapping that this SparseDoubleArray + * stores. + * + *

+ * The values corresponding to indices in ascending order are guaranteed to be + * associated with keys in ascending order, e.g., valueAt(0) will + * return the value associated with the smallest key and + * valueAt(size()-1) will return the value associated with the + * largest key. + *

+ */ + public double valueAt(int index) + { + return mValues[index]; + } + + /** + * Returns the index for which {@link #keyAt} would return the specified key, + * or a negative number if the specified key is not mapped. + */ + public int indexOfKey(int key) + { + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the specified key, + * or a negative number if no keys map to the specified value. Beware that + * this is a linear search, unlike lookups by key, and that multiple keys can + * map to the same value and this will find only one of them. + */ + public int indexOfValue(double value) + { + for (int i = 0; i < mSize; i++) + { + if (mValues[i] == value) + { + return i; + } + } + return -1; + } + + /** + * Removes all key-value mappings from this SparseDoubleArray. + */ + public void clear() + { + mSize = 0; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where the key + * is greater than all existing keys in the array. + */ + public void append(int key, double value) + { + if (mSize != 0 && key <= mKeys[mSize - 1]) + { + put(key, value); + return; + } + int pos = mSize; + if (pos >= mKeys.length) + { + int n = idealDoubleArraySize(pos + 1); + int[] nkeys = new int[n]; + double[] nvalues = new double[n]; + // Log.e("SparseDoubleArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + mKeys = nkeys; + mValues = nvalues; + } + mKeys[pos] = key; + mValues[pos] = value; + mSize = pos + 1; + } + + /** + * Created by analogy with + * com.android.internal.util.ArrayUtils#idealLongArraySize + * + * @param i + * @return + */ + public static int idealDoubleArraySize(int need) + { + return idealByteArraySize(need * 8) / 8; + } + + /** + * Inlined here by copying from com.android.internal.util.ArrayUtils + * + * @param i + * @return + */ + public static int idealByteArraySize(int need) + { + for (int i = 4; i < 32; i++) + { + if (need <= (1 << i) - 12) + { + return (1 << i) - 12; + } + } + + return need; + } + + /** + * {@inheritDoc} + * + *

+ * This implementation composes a string by iterating over its mappings. + */ + @Override + public String toString() + { + if (size() <= 0) + { + return "{}"; + } + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i = 0; i < mSize; i++) + { + if (i > 0) + { + buffer.append(", "); + } + int key = keyAt(i); + buffer.append(key); + buffer.append('='); + double value = valueAt(i); + buffer.append(value); + } + buffer.append('}'); + return buffer.toString(); + } + + /** + * Method (copied from put) added for Jalview to efficiently increment a key's + * value if present, else add it with the given value. This avoids a double + * binary search (once to get the value, again to put the updated value). + * + * @param key + * @oparam toAdd + * @return the new value for the key + */ + public double add(int key, double toAdd) + { + double newValue = toAdd; + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + if (i >= 0) + { + mValues[i] += toAdd; + newValue = mValues[i]; + } + else + { + i = ~i; + if (mSize >= mKeys.length) + { + int n = idealDoubleArraySize(mSize + 1); + int[] nkeys = new int[n]; + double[] nvalues = new double[n]; + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + mKeys = nkeys; + mValues = nvalues; + } + if (mSize - i != 0) + { + System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); + System.arraycopy(mValues, i, mValues, i + 1, mSize - i); + } + mKeys[i] = key; + mValues[i] = toAdd; + mSize++; + } + return newValue; + } + + /** + * Method added for Jalview to efficiently multiply a key's value if present, + * else do nothing. This avoids a double binary search (once to get the value, + * again to put the updated value). + * + * @param key + * @oparam toAdd + * @return the new value for the key + */ + public double divide(int key, double divisor) + { + double newValue = 0d; + if (divisor == 0d) + { + return newValue; + } + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + if (i >= 0) + { + mValues[i] /= divisor; + newValue = mValues[i]; + } + return newValue; + } +} diff --git a/src/jalview/math/Matrix.java b/src/jalview/math/Matrix.java index 647fc3a..de0bf77 100755 --- a/src/jalview/math/Matrix.java +++ b/src/jalview/math/Matrix.java @@ -28,36 +28,43 @@ import java.io.PrintStream; /** * A class to model rectangular matrices of double values and operations on them */ -public class Matrix +public class Matrix implements MatrixI { /* * the cell values in row-major order */ - public double[][] value; + private double[][] value; /* * the number of rows */ - public int rows; + protected int rows; /* * the number of columns */ - public int cols; + protected int cols; - /** DOCUMENT ME!! */ - public double[] d; // Diagonal + protected double[] d; // Diagonal - /** DOCUMENT ME!! */ - public double[] e; // off diagonal + protected double[] e; // off diagonal /** * maximum number of iterations for tqli * */ - int maxIter = 45; // fudge - add 15 iterations, just in case + private static final int maxIter = 45; // fudge - add 15 iterations, just in + // case /** + * Default constructor + */ + public Matrix() + { + + } + + /** * Creates a new Matrix object. For example * *

@@ -82,11 +89,12 @@ public class Matrix
   }
 
   /**
-   * Returns a new matrix which is the transposes of this one
+   * Returns a new matrix which is the transpose of this one
    * 
    * @return DOCUMENT ME!
    */
-  public Matrix transpose()
+  @Override
+  public MatrixI transpose()
   {
     double[][] out = new double[cols][rows];
 
@@ -106,14 +114,16 @@ public class Matrix
    * 
    * @param ps
    *          DOCUMENT ME!
+   * @param format
    */
-  public void print(PrintStream ps)
+  @Override
+  public void print(PrintStream ps, String format)
   {
     for (int i = 0; i < rows; i++)
     {
       for (int j = 0; j < cols; j++)
       {
-        Format.print(ps, "%8.2f", value[i][j]);
+        Format.print(ps, format, getValue(i, j));
       }
 
       ps.println();
@@ -132,24 +142,27 @@ public class Matrix
    *           if the number of columns in the pre-multiplier is not equal to
    *           the number of rows in the multiplicand (this)
    */
-  public Matrix preMultiply(Matrix in)
+  @Override
+  public MatrixI preMultiply(MatrixI in)
   {
-    if (in.cols != this.rows)
+    if (in.width() != rows)
     {
       throw new IllegalArgumentException("Can't pre-multiply " + this.rows
-              + " rows by " + in.cols + " columns");
+              + " rows by " + in.width() + " columns");
     }
-    double[][] tmp = new double[in.rows][this.cols];
+    double[][] tmp = new double[in.height()][this.cols];
 
-    for (int i = 0; i < in.rows; i++)
+    for (int i = 0; i < in.height(); i++)
     {
       for (int j = 0; j < this.cols; j++)
       {
-        tmp[i][j] = 0.0;
-
-        for (int k = 0; k < in.cols; k++)
+        /*
+         * result[i][j] is the vector product of 
+         * in.row[i] and this.column[j]
+         */
+        for (int k = 0; k < in.width(); k++)
         {
-          tmp[i][j] += (in.value[i][k] * this.value[k][j]);
+          tmp[i][j] += (in.getValue(i, k) * this.value[k][j]);
         }
       }
     }
@@ -195,12 +208,13 @@ public class Matrix
    *           number of columns in the multiplicand (this)
    * @see #preMultiply(Matrix)
    */
-  public Matrix postMultiply(Matrix in)
+  @Override
+  public MatrixI postMultiply(MatrixI in)
   {
-    if (in.rows != this.cols)
+    if (in.height() != this.cols)
     {
       throw new IllegalArgumentException("Can't post-multiply " + this.cols
-              + " columns by " + in.rows + " rows");
+              + " columns by " + in.height() + " rows");
     }
     return in.preMultiply(this);
   }
@@ -210,7 +224,8 @@ public class Matrix
    * 
    * @return
    */
-  public Matrix copy()
+  @Override
+  public MatrixI copy()
   {
     double[][] newmat = new double[rows][cols];
 
@@ -225,10 +240,10 @@ public class Matrix
   /**
    * DOCUMENT ME!
    */
+  @Override
   public void tred()
   {
     int n = rows;
-    int l;
     int k;
     int j;
     int i;
@@ -244,7 +259,7 @@ public class Matrix
 
     for (i = n; i >= 2; i--)
     {
-      l = i - 1;
+      final int l = i - 1;
       h = 0.0;
       scale = 0.0;
 
@@ -252,22 +267,23 @@ public class Matrix
       {
         for (k = 1; k <= l; k++)
         {
-          scale += Math.abs(value[i - 1][k - 1]);
+          double v = Math.abs(getValue(i - 1, k - 1));
+          scale += v;
         }
 
         if (scale == 0.0)
         {
-          e[i - 1] = value[i - 1][l - 1];
+          e[i - 1] = getValue(i - 1, l - 1);
         }
         else
         {
           for (k = 1; k <= l; k++)
           {
-            value[i - 1][k - 1] /= scale;
-            h += (value[i - 1][k - 1] * value[i - 1][k - 1]);
+            double v = divideValue(i - 1, k - 1, scale);
+            h += v * v;
           }
 
-          f = value[i - 1][l - 1];
+          f = getValue(i - 1, l - 1);
 
           if (f > 0)
           {
@@ -280,46 +296,48 @@ public class Matrix
 
           e[i - 1] = scale * g;
           h -= (f * g);
-          value[i - 1][l - 1] = f - g;
+          setValue(i - 1, l - 1, f - g);
           f = 0.0;
 
           for (j = 1; j <= l; j++)
           {
-            value[j - 1][i - 1] = value[i - 1][j - 1] / h;
+            double val = getValue(i - 1, j - 1) / h;
+            setValue(j - 1, i - 1, val);
             g = 0.0;
 
             for (k = 1; k <= j; k++)
             {
-              g += (value[j - 1][k - 1] * value[i - 1][k - 1]);
+              g += (getValue(j - 1, k - 1) * getValue(i - 1, k - 1));
             }
 
             for (k = j + 1; k <= l; k++)
             {
-              g += (value[k - 1][j - 1] * value[i - 1][k - 1]);
+              g += (getValue(k - 1, j - 1) * getValue(i - 1, k - 1));
             }
 
             e[j - 1] = g / h;
-            f += (e[j - 1] * value[i - 1][j - 1]);
+            f += (e[j - 1] * getValue(i - 1, j - 1));
           }
 
           hh = f / (h + h);
 
           for (j = 1; j <= l; j++)
           {
-            f = value[i - 1][j - 1];
+            f = getValue(i - 1, j - 1);
             g = e[j - 1] - (hh * f);
             e[j - 1] = g;
 
             for (k = 1; k <= j; k++)
             {
-              value[j - 1][k - 1] -= ((f * e[k - 1]) + (g * value[i - 1][k - 1]));
+              double val = (f * e[k - 1]) + (g * getValue(i - 1, k - 1));
+              addValue(j - 1, k - 1, -val);
             }
           }
         }
       }
       else
       {
-        e[i - 1] = value[i - 1][l - 1];
+        e[i - 1] = getValue(i - 1, l - 1);
       }
 
       d[i - 1] = h;
@@ -330,7 +348,7 @@ public class Matrix
 
     for (i = 1; i <= n; i++)
     {
-      l = i - 1;
+      final int l = i - 1;
 
       if (d[i - 1] != 0.0)
       {
@@ -340,30 +358,66 @@ public class Matrix
 
           for (k = 1; k <= l; k++)
           {
-            g += (value[i - 1][k - 1] * value[k - 1][j - 1]);
+            g += (getValue(i - 1, k - 1) * getValue(k - 1, j - 1));
           }
 
           for (k = 1; k <= l; k++)
           {
-            value[k - 1][j - 1] -= (g * value[k - 1][i - 1]);
+            addValue(k - 1, j - 1, -(g * getValue(k - 1, i - 1)));
           }
         }
       }
 
-      d[i - 1] = value[i - 1][i - 1];
-      value[i - 1][i - 1] = 1.0;
+      d[i - 1] = getValue(i - 1, i - 1);
+      setValue(i - 1, i - 1, 1.0);
 
       for (j = 1; j <= l; j++)
       {
-        value[j - 1][i - 1] = 0.0;
-        value[i - 1][j - 1] = 0.0;
+        setValue(j - 1, i - 1, 0.0);
+        setValue(i - 1, j - 1, 0.0);
       }
     }
   }
 
   /**
+   * Adds f to the value at [i, j] and returns the new value
+   * 
+   * @param i
+   * @param j
+   * @param f
+   */
+  protected double addValue(int i, int j, double f)
+  {
+    double v = value[i][j] + f;
+    value[i][j] = v;
+    return v;
+  }
+
+  /**
+   * Divides the value at [i, j] by divisor and returns the new value. If d is
+   * zero, returns the unchanged value.
+   * 
+   * @param i
+   * @param j
+   * @param divisor
+   * @return
+   */
+  protected double divideValue(int i, int j, double divisor)
+  {
+    if (divisor == 0d)
+    {
+      return getValue(i, j);
+    }
+    double v = value[i][j];
+    v = v / divisor;
+    value[i][j] = v;
+    return v;
+  }
+
+  /**
    * DOCUMENT ME!
    */
+  @Override
   public void tqli() throws Exception
   {
     int n = rows;
@@ -376,7 +430,6 @@ public class Matrix
     double s;
     double r;
     double p;
-    ;
 
     double g;
     double f;
@@ -459,9 +512,9 @@ public class Matrix
 
             for (k = 1; k <= n; k++)
             {
-              f = value[k - 1][i];
-              value[k - 1][i] = (s * value[k - 1][i - 1]) + (c * f);
-              value[k - 1][i - 1] = (c * value[k - 1][i - 1]) - (s * f);
+              f = getValue(k - 1, i);
+              setValue(k - 1, i, (s * getValue(k - 1, i - 1)) + (c * f));
+              setValue(k - 1, i - 1, (c * getValue(k - 1, i - 1)) - (s * f));
             }
           }
 
@@ -473,6 +526,17 @@ public class Matrix
     }
   }
 
+  @Override
+  public double getValue(int i, int j)
+  {
+    return value[i][j];
+  }
+
+  public void setValue(int i, int j, double val)
+  {
+    value[i][j] = val;
+  }
+
   /**
    * DOCUMENT ME!
    */
@@ -725,16 +789,14 @@ public class Matrix
   }
 
   /**
-   * DOCUMENT ME!
+   * Answers the first argument with the sign of the second argument
    * 
    * @param a
-   *          DOCUMENT ME!
    * @param b
-   *          DOCUMENT ME!
    * 
-   * @return DOCUMENT ME!
+   * @return
    */
-  public double sign(double a, double b)
+  static double sign(double a, double b)
   {
     if (b < 0)
     {
@@ -770,12 +832,14 @@ public class Matrix
    * 
    * @param ps
    *          DOCUMENT ME!
+   * @param format
    */
-  public void printD(PrintStream ps)
+  @Override
+  public void printD(PrintStream ps, String format)
   {
     for (int j = 0; j < rows; j++)
     {
-      Format.print(ps, "%15.4e", d[j]);
+      Format.print(ps, format, d[j]);
     }
   }
 
@@ -784,12 +848,45 @@ public class Matrix
    * 
    * @param ps
    *          DOCUMENT ME!
+   * @param format TODO
    */
-  public void printE(PrintStream ps)
+  @Override
+  public void printE(PrintStream ps, String format)
   {
     for (int j = 0; j < rows; j++)
     {
-      Format.print(ps, "%15.4e", e[j]);
+      Format.print(ps, format, e[j]);
     }
   }
+
+  @Override
+  public double[] getD()
+  {
+    return d;
+  }
+
+  @Override
+  public double[] getE()
+  {
+    return e;
+  }
+  
+  @Override
+  public int height() {
+    return rows;
+  }
+
+  @Override
+  public int width()
+  {
+    return cols;
+  }
+
+  @Override
+  public double[] getRow(int i)
+  {
+    double[] row = new double[cols];
+    System.arraycopy(value[i], 0, row, 0, cols);
+    return row;
+  }
 }
diff --git a/src/jalview/math/MatrixI.java b/src/jalview/math/MatrixI.java
new file mode 100644
index 0000000..d74a98b
--- /dev/null
+++ b/src/jalview/math/MatrixI.java
@@ -0,0 +1,59 @@
+package jalview.math;
+
+import java.io.PrintStream;
+
+public interface MatrixI
+{
+  /**
+   * Answers the number of columns
+   * 
+   * @return
+   */
+  int width();
+
+  /**
+   * Answers the number of rows
+   * 
+   * @return
+   */
+  int height();
+
+  /**
+   * Answers the value at row i, column j
+   * 
+   * @param i
+   * @param j
+   * @return
+   */
+  double getValue(int i, int j);
+
+  /**
+   * Answers a copy of the values in the i'th row
+   * 
+   * @return
+   */
+  double[] getRow(int i);
+  
+  MatrixI copy();
+
+  MatrixI transpose();
+
+  MatrixI preMultiply(MatrixI m);
+
+  MatrixI postMultiply(MatrixI m);
+
+  double[] getD();
+
+  double[] getE();
+
+  void print(PrintStream ps, String format);
+
+  void printD(PrintStream ps, String format);
+
+  void printE(PrintStream ps, String format);
+
+  void tqli() throws Exception;
+
+  void tred();
+
+}
diff --git a/src/jalview/math/SparseMatrix.java b/src/jalview/math/SparseMatrix.java
new file mode 100644
index 0000000..72f0963
--- /dev/null
+++ b/src/jalview/math/SparseMatrix.java
@@ -0,0 +1,218 @@
+package jalview.math;
+
+import jalview.ext.android.SparseDoubleArray;
+
+/**
+ * A variant of Matrix intended for use for sparse (mostly zero) matrices. This
+ * class uses a SparseDoubleArray to hold each row of the matrix. The sparse
+ * array only stores non-zero values. This gives a smaller memory footprint, and
+ * fewer matrix calculation operations, for mostly zero matrices.
+ * 
+ * @author gmcarstairs
+ */
+public class SparseMatrix extends Matrix
+{
+  /*
+   * we choose columns for the sparse arrays as this allows
+   * optimisation of the preMultiply() method used in PCA.run()
+   */
+  SparseDoubleArray[] sparseColumns;
+
+  /**
+   * Constructor given data in [row][column] order
+   * 
+   * @param v
+   */
+  public SparseMatrix(double[][] v)
+  {
+    rows = v.length;
+    if (rows > 0) {
+      cols = v[0].length;
+    }
+    sparseColumns = new SparseDoubleArray[cols];
+
+    /*
+     * transpose v[row][col] into [col][row] order
+     */
+    for (int col = 0; col < cols; col++)
+    {
+      SparseDoubleArray sparseColumn = new SparseDoubleArray();
+      sparseColumns[col] = sparseColumn;
+      for (int row = 0; row < rows; row++)
+      {
+        double value = v[row][col];
+        if (value != 0d)
+        {
+          sparseColumn.put(row, value);
+        }
+      }
+    }
+  }
+
+  /**
+   * Answers the value at row i, column j
+   */
+  @Override
+  public double getValue(int i, int j)
+  {
+    return sparseColumns[j].get(i);
+  }
+
+  /**
+   * Sets the value at row i, column j to val
+   */
+  @Override
+  public void setValue(int i, int j, double val)
+  {
+    if (val == 0d)
+    {
+      sparseColumns[j].delete(i);
+    }
+    else
+    {
+      sparseColumns[j].put(i, val);
+    }
+  }
+
+  @Override
+  public double[] getColumn(int i)
+  {
+    double[] col = new double[height()];
+
+    SparseDoubleArray vals = sparseColumns[i];
+    for (int nonZero = 0; nonZero < vals.size(); nonZero++)
+    {
+      col[vals.keyAt(nonZero)] = vals.valueAt(nonZero);
+    }
+    return col;
+  }
+
+  @Override
+  public MatrixI copy()
+  {
+    double[][] vals = new double[height()][width()];
+    for (int i = 0; i < height(); i++)
+    {
+      vals[i] = getRow(i);
+    }
+    return new SparseMatrix(vals);
+  }
+
+  @Override
+  public MatrixI transpose()
+  {
+    double[][] out = new double[cols][rows];
+
+    /*
+     * for each column...
+     */
+    for (int i = 0; i < cols; i++)
+    {
+      /*
+       * put non-zero values into the corresponding row
+       * of the transposed matrix
+       */
+      SparseDoubleArray vals = sparseColumns[i];
+      for (int nonZero = 0; nonZero < vals.size(); nonZero++)
+      {
+        out[i][vals.keyAt(nonZero)] = vals.valueAt(nonZero);
+      }
+    }
+
+    return new SparseMatrix(out);
+  }
+
+  /**
+   * Answers a new matrix which is the product in.this. If the product contains
+   * less than 20% non-zero values, it is returned as a SparseMatrix, else as a
+   * Matrix.
+   * 

+ * This method is optimised for the sparse arrays which store column values + * for a SparseMatrix. Note that postMultiply is not so optimised. That would + * require redundantly also storing sparse arrays for the rows, which has not + * been done. Currently only preMultiply is used in Jalview. + */ + @Override + public MatrixI preMultiply(MatrixI in) + { + if (in.width() != rows) + { + throw new IllegalArgumentException("Can't pre-multiply " + this.rows + + " rows by " + in.width() + " columns"); + } + double[][] tmp = new double[in.height()][this.cols]; + + long count = 0L; + for (int i = 0; i < in.height(); i++) + { + for (int j = 0; j < this.cols; j++) + { + /* + * result[i][j] is the vector product of + * in.row[i] and this.column[j] + * we only need to use non-zero values from the column + */ + SparseDoubleArray vals = sparseColumns[j]; + boolean added = false; + for (int nonZero = 0; nonZero < vals.size(); nonZero++) + { + int myRow = vals.keyAt(nonZero); + double myValue = vals.valueAt(nonZero); + tmp[i][j] += (in.getValue(i, myRow) * myValue); + added = true; + } + if (added && tmp[i][j] != 0d) + { + count++; // non-zero entry in product + } + } + } + + /* + * heuristic rule - if product is more than 80% zero + * then construct a SparseMatrix, else a Matrix + */ + if (count * 5 < in.height() * cols) + { + return new SparseMatrix(tmp); + } + else + { + return new Matrix(tmp); + } + } + + @Override + protected double divideValue(int i, int j, double divisor) + { + if (divisor == 0d) + { + return getValue(i, j); + } + double v = sparseColumns[j].divide(i, divisor); + return v; + } + + @Override + protected double addValue(int i, int j, double addend) + { + double v = sparseColumns[j].add(i, addend); + return v; + } + + /** + * Returns the fraction of the whole matrix size that is actually modelled in + * sparse arrays (normally, the non-zero values) + * + * @return + */ + public float getFillRatio() + { + long count = 0L; + for (SparseDoubleArray col : sparseColumns) + { + count += col.size(); + } + return count / (float) (height() * width()); + } +} diff --git a/src/jalview/schemes/ResidueProperties.java b/src/jalview/schemes/ResidueProperties.java index 406814a..1e6142d 100755 --- a/src/jalview/schemes/ResidueProperties.java +++ b/src/jalview/schemes/ResidueProperties.java @@ -486,7 +486,7 @@ public class ResidueProperties -3, 3, 0, -1, -4 }, { -2, -2, 1, 6, -3, 0, 2, -1, -1, -3, -4, -1, -3, -3, -1, 0, -1, -4, -3, -3, 4, 1, -1, -4 }, - { 0, 3, -3, -3, 9, -3, -4, -3, -3, -1, -1, -3, -1, -2, -3, -1, -1, + { 0, -3, -3, -3, 9, -3, -4, -3, -3, -1, -1, -3, -1, -2, -3, -1, -1, -2, -2, -1, -3, -3, -2, -4 }, { -1, 1, 0, 0, -3, 5, 2, -2, 0, -3, -2, 1, 0, -3, -1, 0, -1, -2, -1, -2, 0, 3, -1, -4 }, diff --git a/src/jalview/schemes/ScoreMatrix.java b/src/jalview/schemes/ScoreMatrix.java index 5e5fa8d..d82f54c 100644 --- a/src/jalview/schemes/ScoreMatrix.java +++ b/src/jalview/schemes/ScoreMatrix.java @@ -21,10 +21,10 @@ package jalview.schemes; import jalview.analysis.scoremodels.PairwiseSeqScoreModel; -import jalview.api.analysis.ScoreModelI; +import jalview.math.Matrix; +import jalview.math.MatrixI; -public class ScoreMatrix extends PairwiseSeqScoreModel implements - ScoreModelI +public class ScoreMatrix extends PairwiseSeqScoreModel { String name; @@ -79,19 +79,21 @@ public class ScoreMatrix extends PairwiseSeqScoreModel implements } /** + * Answers the score for substituting first char in A1 with first char in A2 * * @param A1 * @param A2 - * @return score for substituting first char in A1 with first char in A2 + * @return */ public int getPairwiseScore(String A1, String A2) { return getPairwiseScore(A1.charAt(0), A2.charAt(0)); } + @Override public int getPairwiseScore(char c, char d) { - int pog = 0; + int score = 0; try { @@ -99,19 +101,19 @@ public class ScoreMatrix extends PairwiseSeqScoreModel implements : ResidueProperties.nucleotideIndex[c]; int b = (type == 0) ? ResidueProperties.aaIndex[d] : ResidueProperties.nucleotideIndex[d]; - - pog = matrix[a][b]; + score = matrix[a][b]; } catch (Exception e) { // System.out.println("Unknown residue in " + A1 + " " + A2); } - return pog; + return score; } /** * pretty print the matrix */ + @Override public String toString() { return outputMatrix(false); @@ -170,4 +172,47 @@ public class ScoreMatrix extends PairwiseSeqScoreModel implements } return sb.toString(); } + + /** + * Computes an NxN matrix where N is the number of sequences, and entry [i, j] + * is sequence[i] pairwise multiplied with sequence[j], as a sum of scores + * computed using the current score matrix. For example + *

    + *
  • Sequences:
  • + *
  • FKL
  • + *
  • R-D
  • + *
  • QIA
  • + *
  • GWC
  • + *
  • Score matrix is BLOSUM62
  • + *
  • Gaps treated same as X (unknown)
  • + *
  • product [0, 0] = F.F + K.K + L.L = 6 + 5 + 4 = 15
  • + *
  • product [1, 1] = R.R + -.- + D.D = 5 + -1 + 6 = 10
  • + *
  • product [2, 2] = Q.Q + I.I + A.A = 5 + 4 + 4 = 13
  • + *
  • product [3, 3] = G.G + W.W + C.C = 6 + 11 + 9 = 26
  • + *
  • product[0, 1] = F.R + K.- + L.D = -3 + -1 + -3 = -8 + *
  • and so on
  • + *
+ */ + public MatrixI computePairwiseScores(String[] seqs) + { + double[][] values = new double[seqs.length][]; + for (int row = 0; row < seqs.length; row++) + { + values[row] = new double[seqs.length]; + for (int col = 0; col < seqs.length; col++) + { + int total = 0; + int width = Math.min(seqs[row].length(), seqs[col].length()); + for (int i = 0; i < width; i++) + { + char c1 = seqs[row].charAt(i); + char c2 = seqs[col].charAt(i); + int score = getPairwiseScore(c1, c2); + total += score; + } + values[row][col] = total; + } + } + return new Matrix(values); + } } diff --git a/src/jalview/viewmodel/PCAModel.java b/src/jalview/viewmodel/PCAModel.java index b0af302..0623dab 100644 --- a/src/jalview/viewmodel/PCAModel.java +++ b/src/jalview/viewmodel/PCAModel.java @@ -30,6 +30,13 @@ import java.util.Vector; public class PCAModel { + /* + * Jalview 2.10.1 treated gaps as X (peptide) or N (nucleotide) + * for pairwise scoring; 2.10.2 uses gap score (last column) in + * score matrix (JAL-2397) + * Set this flag to true (via Groovy) for 2.10.1 behaviour + */ + private static boolean scoreGapAsAny = false; public PCAModel(AlignmentView seqstrings2, SequenceI[] seqs2, boolean nucleotide2) @@ -69,8 +76,9 @@ public class PCAModel public void run() { - - pca = new PCA(seqstrings.getSequenceStrings(' '), nucleotide, + char gapChar = scoreGapAsAny ? (nucleotide ? 'N' : 'X') : ' '; + String[] sequenceStrings = seqstrings.getSequenceStrings(gapChar); + pca = new PCA(sequenceStrings, nucleotide, score_matrix); pca.setJvCalcMode(jvCalcMode); pca.run(); @@ -83,32 +91,23 @@ public class PCAModel ii++; } - double[][] comps = new double[ii][ii]; - - for (int i = 0; i < ii; i++) - { - if (pca.getEigenvalue(i) > 1e-4) - { - comps[i] = pca.component(i); - } - } - - top = pca.getM().rows - 1; + int height = pca.getHeight(); + // top = pca.getM().height() - 1; + top = height - 1; points = new Vector(); float[][] scores = pca.getComponents(top - 1, top - 2, top - 3, 100); - for (int i = 0; i < pca.getM().rows; i++) + for (int i = 0; i < height; i++) { SequencePoint sp = new SequencePoint(seqs[i], scores[i]); points.addElement(sp); } - } public void updateRc(RotatableCanvasI rc) { - rc.setPoints(points, pca.getM().rows); + rc.setPoints(points, pca.getHeight()); } public boolean isNucleotide() @@ -146,9 +145,9 @@ public class PCAModel // note: actual indices for components are dim1-1, etc (patch for JAL-1123) float[][] scores = pca.getComponents(dim1 - 1, dim2 - 1, dim3 - 1, 100); - for (int i = 0; i < pca.getM().rows; i++) + for (int i = 0; i < pca.getHeight(); i++) { - ((SequencePoint) points.elementAt(i)).coord = scores[i]; + points.elementAt(i).coord = scores[i]; } } diff --git a/test/jalview/ext/android/SparseDoubleArrayTest.java b/test/jalview/ext/android/SparseDoubleArrayTest.java new file mode 100644 index 0000000..7d64a28 --- /dev/null +++ b/test/jalview/ext/android/SparseDoubleArrayTest.java @@ -0,0 +1,53 @@ +package jalview.ext.android; + +import static org.testng.Assert.assertEquals; + +import org.testng.annotations.Test; + +public class SparseDoubleArrayTest +{ + + @Test + public void testConstructor() + { + double[] d = new double[] { 0d, 0d, 1.2d, 0d, 0d, 3.4d }; + SparseDoubleArray s = new SparseDoubleArray(d); + for (int i = 0; i < d.length; i++) + { + assertEquals(s.get(i), d[i], "At [" + i + "]"); + } + } + + @Test + public void testAdd() + { + double[] d = new double[] { 0d, 0d, 1.2d, 0d, 0d, 3.4d }; + SparseDoubleArray s = new SparseDoubleArray(d); + // add to zero (absent) + s.add(0, 3.2d); + assertEquals(s.get(0), 3.2d); + // add to non-zero + s.add(0, 2.5d); + assertEquals(s.get(0), 5.7d); + // add negative value + s.add(2, -5.3d); + assertEquals(s.get(2), -4.1d); + // add to unset value + s.add(12, 9.8d); + assertEquals(s.get(12), 9.8d); + } + + @Test + public void testDivide() + { + double delta = 1.0e-10; + double[] d = new double[] { 0d, 2.4d, 1.2d, 0d, -4.8d, -3.6d }; + SparseDoubleArray s = new SparseDoubleArray(d); + assertEquals(s.divide(0, 1d), 0d); // no such entry + assertEquals(s.divide(2, 0d), 0d); // zero divisor + assertEquals(s.divide(1, 2d), 1.2d, delta); // + / + + assertEquals(s.divide(2, -2d), -0.6d, delta); // + / - + assertEquals(s.divide(4, 3d), -1.6d, delta); // - / + + assertEquals(s.divide(5, -3d), 1.2d, delta); // - / - + } +} diff --git a/test/jalview/math/MatrixTest.java b/test/jalview/math/MatrixTest.java index a4acbd0..961602d 100644 --- a/test/jalview/math/MatrixTest.java +++ b/test/jalview/math/MatrixTest.java @@ -8,9 +8,12 @@ import java.util.Arrays; import java.util.Random; import org.testng.annotations.Test; +import org.testng.internal.junit.ArrayAsserts; public class MatrixTest { + final static double DELTA = 0.0001d; + @Test(groups = "Timing") public void testPreMultiply_timing() { @@ -37,20 +40,20 @@ public class MatrixTest * 1x3 times 3x1 is 1x1 * 2x5 + 3x6 + 4*7 = 56 */ - Matrix m3 = m2.preMultiply(m1); - assertEquals(m3.rows, 1); - assertEquals(m3.cols, 1); - assertEquals(m3.value[0][0], 56d); + MatrixI m3 = m2.preMultiply(m1); + assertEquals(m3.height(), 1); + assertEquals(m3.width(), 1); + assertEquals(m3.getValue(0, 0), 56d); /* * 3x1 times 1x3 is 3x3 */ m3 = m1.preMultiply(m2); - assertEquals(m3.rows, 3); - assertEquals(m3.cols, 3); - assertEquals(Arrays.toString(m3.value[0]), "[10.0, 15.0, 20.0]"); - assertEquals(Arrays.toString(m3.value[1]), "[12.0, 18.0, 24.0]"); - assertEquals(Arrays.toString(m3.value[2]), "[14.0, 21.0, 28.0]"); + assertEquals(m3.height(), 3); + assertEquals(m3.width(), 3); + assertEquals(Arrays.toString(m3.getRow(0)), "[10.0, 15.0, 20.0]"); + assertEquals(Arrays.toString(m3.getRow(1)), "[12.0, 18.0, 24.0]"); + assertEquals(Arrays.toString(m3.getRow(2)), "[14.0, 21.0, 28.0]"); } @Test( @@ -85,7 +88,18 @@ public class MatrixTest private boolean matrixEquals(Matrix m1, Matrix m2) { - return Arrays.deepEquals(m1.value, m2.value); + if (m1.width() != m2.width() || m1.height() != m2.height()) + { + return false; + } + for (int i = 0; i < m1.height(); i++) + { + if (!Arrays.equals(m1.getRow(i), m2.getRow(i))) + { + return false; + } + } + return true; } @Test(groups = "Functional") @@ -99,18 +113,18 @@ public class MatrixTest * (3020 30200) * (5040 50400) */ - Matrix m1 = new Matrix(new double[][] { { 2, 3 }, { 4, 5 } }); - Matrix m2 = new Matrix(new double[][] { { 10, 100 }, { 1000, 10000 } }); - Matrix m3 = m1.postMultiply(m2); - assertEquals(Arrays.toString(m3.value[0]), "[3020.0, 30200.0]"); - assertEquals(Arrays.toString(m3.value[1]), "[5040.0, 50400.0]"); + MatrixI m1 = new Matrix(new double[][] { { 2, 3 }, { 4, 5 } }); + MatrixI m2 = new Matrix(new double[][] { { 10, 100 }, { 1000, 10000 } }); + MatrixI m3 = m1.postMultiply(m2); + assertEquals(Arrays.toString(m3.getRow(0)), "[3020.0, 30200.0]"); + assertEquals(Arrays.toString(m3.getRow(1)), "[5040.0, 50400.0]"); /* * also check m2.preMultiply(m1) - should be same as m1.postMultiply(m2) */ m3 = m2.preMultiply(m1); - assertEquals(Arrays.toString(m3.value[0]), "[3020.0, 30200.0]"); - assertEquals(Arrays.toString(m3.value[1]), "[5040.0, 50400.0]"); + assertEquals(Arrays.toString(m3.getRow(0)), "[3020.0, 30200.0]"); + assertEquals(Arrays.toString(m3.getRow(1)), "[5040.0, 50400.0]"); /* * m1 has more rows than columns @@ -120,15 +134,15 @@ public class MatrixTest m1 = new Matrix(new double[][] { { 2 }, { 3 } }); m2 = new Matrix(new double[][] { { 10, 100, 1000 } }); m3 = m1.postMultiply(m2); - assertEquals(m3.rows, 2); - assertEquals(m3.cols, 3); - assertEquals(Arrays.toString(m3.value[0]), "[20.0, 200.0, 2000.0]"); - assertEquals(Arrays.toString(m3.value[1]), "[30.0, 300.0, 3000.0]"); + assertEquals(m3.height(), 2); + assertEquals(m3.width(), 3); + assertEquals(Arrays.toString(m3.getRow(0)), "[20.0, 200.0, 2000.0]"); + assertEquals(Arrays.toString(m3.getRow(1)), "[30.0, 300.0, 3000.0]"); m3 = m2.preMultiply(m1); - assertEquals(m3.rows, 2); - assertEquals(m3.cols, 3); - assertEquals(Arrays.toString(m3.value[0]), "[20.0, 200.0, 2000.0]"); - assertEquals(Arrays.toString(m3.value[1]), "[30.0, 300.0, 3000.0]"); + assertEquals(m3.height(), 2); + assertEquals(m3.width(), 3); + assertEquals(Arrays.toString(m3.getRow(0)), "[20.0, 200.0, 2000.0]"); + assertEquals(Arrays.toString(m3.getRow(1)), "[30.0, 300.0, 3000.0]"); /* * m1 has more columns than rows @@ -141,19 +155,19 @@ public class MatrixTest m1 = new Matrix(new double[][] { { 2, 3, 4 } }); m2 = new Matrix(new double[][] { { 5, 4 }, { 6, 3 }, { 7, 2 } }); m3 = m1.postMultiply(m2); - assertEquals(m3.rows, 1); - assertEquals(m3.cols, 2); - assertEquals(m3.value[0][0], 56d); - assertEquals(m3.value[0][1], 25d); + assertEquals(m3.height(), 1); + assertEquals(m3.width(), 2); + assertEquals(m3.getRow(0)[0], 56d); + assertEquals(m3.getRow(0)[1], 25d); /* * and check premultiply equivalent */ m3 = m2.preMultiply(m1); - assertEquals(m3.rows, 1); - assertEquals(m3.cols, 2); - assertEquals(m3.value[0][0], 56d); - assertEquals(m3.value[0][1], 25d); + assertEquals(m3.height(), 1); + assertEquals(m3.width(), 2); + assertEquals(m3.getRow(0)[0], 56d); + assertEquals(m3.getRow(0)[1], 25d); } @Test(groups = "Functional") @@ -172,7 +186,7 @@ public class MatrixTest } } Matrix m1 = new Matrix(in); - Matrix m2 = m1.copy(); + Matrix m2 = (Matrix) m1.copy(); assertTrue(matrixEquals(m1, m2)); } @@ -200,12 +214,12 @@ public class MatrixTest // / origmat.print(System.out); // System.out.println(); // System.out.println(" --- transpose matrix ---- "); - Matrix trans = origmat.transpose(); + MatrixI trans = origmat.transpose(); // trans.print(System.out); // System.out.println(); // System.out.println(" --- OrigT * Orig ---- "); - Matrix symm = trans.postMultiply(origmat); + MatrixI symm = trans.postMultiply(origmat); // symm.print(System.out); // System.out.println(); @@ -255,4 +269,113 @@ public class MatrixTest // } // System.out.println(); } + + @Test(groups = "Timing") + public void testSign() + { + assertEquals(Matrix.sign(-1, -2), -1d); + assertEquals(Matrix.sign(-1, 2), 1d); + assertEquals(Matrix.sign(-1, 0), 1d); + assertEquals(Matrix.sign(1, -2), -1d); + assertEquals(Matrix.sign(1, 2), 1d); + assertEquals(Matrix.sign(1, 0), 1d); + } + + /** + * Helper method to make values for a sparse, pseudo-random symmetric matrix + * + * @param rows + * @param cols + * @param occupancy + * one in 'occupancy' entries will be non-zero + * @return + */ + public double[][] getSparseValues(int rows, int cols, int occupancy) + { + Random r = new Random(1729); + + /* + * generate whole number values between -12 and +12 + * (to mimic score matrices used in Jalview) + */ + double[][] d = new double[rows][cols]; + int m = 0; + for (int i = 0; i < rows; i++) + { + if (++m % occupancy == 0) + { + d[i][i] = r.nextInt() % 13; // diagonal + } + for (int j = 0; j < i; j++) + { + if (++m % occupancy == 0) + { + d[i][j] = r.nextInt() % 13; + d[j][i] = d[i][j]; + } + } + } + return d; + + } + + /** + * Verify that the results of method tred() are the same if the calculation is + * redone + */ + @Test(groups = "Functional") + public void testTred_reproducible() + { + /* + * make a pseudo-random symmetric matrix as required for tred/tqli + */ + int rows = 10; + int cols = rows; + double[][] d = getSparseValues(rows, cols, 3); + + /* + * make a copy of the values so m1, m2 are not + * sharing arrays! + */ + double[][] d1 = new double[rows][cols]; + for (int row = 0; row < rows; row++) + { + for (int col = 0; col < cols; col++) + { + d1[row][col] = d[row][col]; + } + } + Matrix m1 = new Matrix(d); + Matrix m2 = new Matrix(d1); + assertMatricesMatch(m1, m2); // sanity check + m1.tred(); + m2.tred(); + assertMatricesMatch(m1, m2); + } + + private void assertMatricesMatch(MatrixI m1, MatrixI m2) + { + if (m1.height() != m2.height()) + { + fail("height mismatch"); + } + if (m1.width() != m2.width()) + { + fail("width mismatch"); + } + for (int row = 0; row < m1.height(); row++) + { + for (int col = 0; col < m1.width(); col++) + { + double v2 = m2.getValue(row, col); + double v1 = m1.getValue(row, col); + if (Math.abs(v1 - v2) > DELTA) + { + fail(String.format("At [%d, %d] %f != %f", row, col, v1, v2)); + } + } + } + ArrayAsserts.assertArrayEquals(m1.getD(), m2.getD(), 0.00001d); + ArrayAsserts.assertArrayEquals(m1.getE(), m2.getE(), 0.00001d); + } } diff --git a/test/jalview/math/SparseMatrixTest.java b/test/jalview/math/SparseMatrixTest.java new file mode 100644 index 0000000..3c2ccaa --- /dev/null +++ b/test/jalview/math/SparseMatrixTest.java @@ -0,0 +1,416 @@ +package jalview.math; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.util.Random; + +import org.testng.annotations.Test; +import org.testng.internal.junit.ArrayAsserts; + +public class SparseMatrixTest +{ + final static double DELTA = 0.0001d; + + Random r = new Random(1729); + + @Test(groups = "Functional") + public void testConstructor() + { + MatrixI m1 = new SparseMatrix( + new double[][] { { 2, 0, 4 }, { 0, 6, 0 } }); + assertEquals(m1.getValue(0, 0), 2d); + assertEquals(m1.getValue(0, 1), 0d); + assertEquals(m1.getValue(0, 2), 4d); + assertEquals(m1.getValue(1, 0), 0d); + assertEquals(m1.getValue(1, 1), 6d); + assertEquals(m1.getValue(1, 2), 0d); + } + + @Test(groups = "Functional") + public void testTranspose() + { + MatrixI m1 = new SparseMatrix( + new double[][] { { 2, 0, 4 }, { 5, 6, 0 } }); + MatrixI m2 = m1.transpose(); + assertTrue(m2 instanceof SparseMatrix); + assertEquals(m2.height(), 3); + assertEquals(m2.width(), 2); + assertEquals(m2.getValue(0, 0), 2d); + assertEquals(m2.getValue(0, 1), 5d); + assertEquals(m2.getValue(1, 0), 0d); + assertEquals(m2.getValue(1, 1), 6d); + assertEquals(m2.getValue(2, 0), 4d); + assertEquals(m2.getValue(2, 1), 0d); + } + @Test(groups = "Functional") + public void testPreMultiply() + { + MatrixI m1 = new SparseMatrix(new double[][] { { 2, 3, 4 } }); // 1x3 + MatrixI m2 = new SparseMatrix(new double[][] { { 5 }, { 6 }, { 7 } }); // 3x1 + + /* + * 1x3 times 3x1 is 1x1 + * 2x5 + 3x6 + 4*7 = 56 + */ + MatrixI m3 = m2.preMultiply(m1); + assertFalse(m3 instanceof SparseMatrix); + assertEquals(m3.height(), 1); + assertEquals(m3.width(), 1); + assertEquals(m3.getValue(0, 0), 56d); + + /* + * 3x1 times 1x3 is 3x3 + */ + m3 = m1.preMultiply(m2); + assertEquals(m3.height(), 3); + assertEquals(m3.width(), 3); + assertEquals(m3.getValue(0, 0), 10d); + assertEquals(m3.getValue(0, 1), 15d); + assertEquals(m3.getValue(0, 2), 20d); + assertEquals(m3.getValue(1, 0), 12d); + assertEquals(m3.getValue(1, 1), 18d); + assertEquals(m3.getValue(1, 2), 24d); + assertEquals(m3.getValue(2, 0), 14d); + assertEquals(m3.getValue(2, 1), 21d); + assertEquals(m3.getValue(2, 2), 28d); + } + + @Test( + groups = "Functional", + expectedExceptions = { IllegalArgumentException.class }) + public void testPreMultiply_tooManyColumns() + { + Matrix m1 = new SparseMatrix( + new double[][] { { 2, 3, 4 }, { 3, 4, 5 } }); // 2x3 + + /* + * 2x3 times 2x3 invalid operation - + * multiplier has more columns than multiplicand has rows + */ + m1.preMultiply(m1); + fail("Expected exception"); + } + + @Test( + groups = "Functional", + expectedExceptions = { IllegalArgumentException.class }) + public void testPreMultiply_tooFewColumns() + { + Matrix m1 = new SparseMatrix( + new double[][] { { 2, 3, 4 }, { 3, 4, 5 } }); // 2x3 + + /* + * 3x2 times 3x2 invalid operation - + * multiplier has more columns than multiplicand has row + */ + m1.preMultiply(m1); + fail("Expected exception"); + } + + @Test(groups = "Functional") + public void testPostMultiply() + { + /* + * Square matrices + * (2 3) . (10 100) + * (4 5) (1000 10000) + * = + * (3020 30200) + * (5040 50400) + */ + MatrixI m1 = new SparseMatrix(new double[][] { { 2, 3 }, { 4, 5 } }); + MatrixI m2 = new SparseMatrix(new double[][] { { 10, 100 }, + { 1000, 10000 } }); + MatrixI m3 = m1.postMultiply(m2); + assertEquals(m3.getValue(0, 0), 3020d); + assertEquals(m3.getValue(0, 1), 30200d); + assertEquals(m3.getValue(1, 0), 5040d); + assertEquals(m3.getValue(1, 1), 50400d); + + /* + * also check m2.preMultiply(m1) - should be same as m1.postMultiply(m2) + */ + MatrixI m4 = m2.preMultiply(m1); + assertMatricesMatch(m3, m4, 0.00001d); + + /* + * m1 has more rows than columns + * (2).(10 100 1000) = (20 200 2000) + * (3) (30 300 3000) + */ + m1 = new SparseMatrix(new double[][] { { 2 }, { 3 } }); + m2 = new SparseMatrix(new double[][] { { 10, 100, 1000 } }); + m3 = m1.postMultiply(m2); + assertEquals(m3.height(), 2); + assertEquals(m3.width(), 3); + assertEquals(m3.getValue(0, 0), 20d); + assertEquals(m3.getValue(0, 1), 200d); + assertEquals(m3.getValue(0, 2), 2000d); + assertEquals(m3.getValue(1, 0), 30d); + assertEquals(m3.getValue(1, 1), 300d); + assertEquals(m3.getValue(1, 2), 3000d); + + m4 = m2.preMultiply(m1); + assertMatricesMatch(m3, m4, 0.00001d); + + /* + * m1 has more columns than rows + * (2 3 4) . (5 4) = (56 25) + * (6 3) + * (7 2) + * [0, 0] = 2*5 + 3*6 + 4*7 = 56 + * [0, 1] = 2*4 + 3*3 + 4*2 = 25 + */ + m1 = new SparseMatrix(new double[][] { { 2, 3, 4 } }); + m2 = new SparseMatrix(new double[][] { { 5, 4 }, { 6, 3 }, { 7, 2 } }); + m3 = m1.postMultiply(m2); + assertEquals(m3.height(), 1); + assertEquals(m3.width(), 2); + assertEquals(m3.getValue(0, 0), 56d); + assertEquals(m3.getValue(0, 1), 25d); + + /* + * and check premultiply equivalent + */ + m4 = m2.preMultiply(m1); + assertMatricesMatch(m3, m4, 0.00001d); + } + + @Test(groups = "Timing") + public void testSign() + { + assertEquals(Matrix.sign(-1, -2), -1d); + assertEquals(Matrix.sign(-1, 2), 1d); + assertEquals(Matrix.sign(-1, 0), 1d); + assertEquals(Matrix.sign(1, -2), -1d); + assertEquals(Matrix.sign(1, 2), 1d); + assertEquals(Matrix.sign(1, 0), 1d); + } + + /** + * Verify that the results of method tred() are the same for SparseMatrix as + * they are for Matrix (i.e. a regression test rather than an absolute test of + * correctness of results) + */ + @Test(groups = "Functional") + public void testTred_matchesMatrix() + { + /* + * make a pseudo-random symmetric matrix as required for tred/tqli + */ + int rows = 10; + int cols = rows; + double[][] d = getSparseValues(rows, cols, 3); + + /* + * make a copy of the values so m1, m2 are not + * sharing arrays! + */ + double[][] d1 = new double[rows][cols]; + for (int row = 0; row < rows; row++) + { + for (int col = 0; col < cols; col++) + { + d1[row][col] = d[row][col]; + } + } + Matrix m1 = new Matrix(d); + Matrix m2 = new SparseMatrix(d1); + assertMatricesMatch(m1, m2, 0.00001d); // sanity check + m1.tred(); + m2.tred(); + assertMatricesMatch(m1, m2, 0.00001d); + } + + private void assertMatricesMatch(MatrixI m1, MatrixI m2, double delta) + { + if (m1.height() != m2.height()) + { + fail("height mismatch"); + } + if (m1.width() != m2.width()) + { + fail("width mismatch"); + } + for (int row = 0; row < m1.height(); row++) + { + for (int col = 0; col < m1.width(); col++) + { + double v2 = m2.getValue(row, col); + double v1 = m1.getValue(row, col); + if (Math.abs(v1 - v2) > DELTA) + { + fail(String.format("At [%d, %d] %f != %f", row, col, v1, v2)); + } + } + } + ArrayAsserts.assertArrayEquals(m1.getD(), m2.getD(), delta); + ArrayAsserts.assertArrayEquals(m1.getE(), m2.getE(), 0.00001d); + } + + @Test + public void testGetValue() + { + double[][] d = new double[][] { { 0, 0, 1, 0, 0 }, { 2, 3, 0, 0, 0 }, + { 4, 0, 0, 0, 5 } }; + MatrixI m = new SparseMatrix(d); + for (int row = 0; row < 3; row++) + { + for (int col = 0; col < 5; col++) + { + assertEquals(m.getValue(row, col), d[row][col], + String.format("At [%d, %d]", row, col)); + } + } + } + + /** + * Verify that the results of method tqli() are the same for SparseMatrix as + * they are for Matrix (i.e. a regression test rather than an absolute test of + * correctness of results) + * + * @throws Exception + */ + @Test(groups = "Functional") + public void testTqli_matchesMatrix() throws Exception + { + /* + * make a pseudo-random symmetric matrix as required for tred + */ + int rows = 6; + int cols = rows; + double[][] d = getSparseValues(rows, cols, 3); + + /* + * make a copy of the values so m1, m2 are not + * sharing arrays! + */ + double[][] d1 = new double[rows][cols]; + for (int row = 0; row < rows; row++) + { + for (int col = 0; col < cols; col++) + { + d1[row][col] = d[row][col]; + } + } + Matrix m1 = new Matrix(d); + Matrix m2 = new SparseMatrix(d1); + + // have to do tred() before doing tqli() + m1.tred(); + m2.tred(); + assertMatricesMatch(m1, m2, 0.00001d); + + m1.tqli(); + m2.tqli(); + assertMatricesMatch(m1, m2, 0.00001d); + } + + /** + * Helper method to make values for a sparse, pseudo-random symmetric matrix + * + * @param rows + * @param cols + * @param occupancy + * one in 'occupancy' entries will be non-zero + * @return + */ + public double[][] getSparseValues(int rows, int cols, int occupancy) + { + /* + * generate whole number values between -12 and +12 + * (to mimic score matrices used in Jalview) + */ + double[][] d = new double[rows][cols]; + int m = 0; + for (int i = 0; i < rows; i++) + { + if (++m % occupancy == 0) + { + d[i][i] = r.nextInt() % 13; // diagonal + } + for (int j = 0; j < i; j++) + { + if (++m % occupancy == 0) + { + d[i][j] = r.nextInt() % 13; + d[j][i] = d[i][j]; + } + } + } + return d; + + } + + /** + * Test that verifies that the result of preMultiply is a SparseMatrix if more + * than 80% zeroes, else a Matrix + */ + @Test(groups = "Functional") + public void testPreMultiply_sparseProduct() + { + MatrixI m1 = new SparseMatrix(new double[][] { { 1 }, { 0 }, { 0 }, + { 0 }, { 0 } }); // 5x1 + MatrixI m2 = new SparseMatrix(new double[][] { { 1, 1, 1, 1 } }); // 1x4 + + /* + * m1.m2 makes a row of 4 1's, and 4 rows of zeros + * 20% non-zero so not 'sparse' + */ + MatrixI m3 = m2.preMultiply(m1); + assertFalse(m3 instanceof SparseMatrix); + + /* + * replace a 1 with a 0 in the product: + * it is now > 80% zero so 'sparse' + */ + m2 = new SparseMatrix(new double[][] { { 1, 1, 1, 0 } }); + m3 = m2.preMultiply(m1); + assertTrue(m3 instanceof SparseMatrix); + } + + @Test(groups = "Functional") + public void testFillRatio() + { + SparseMatrix m1 = new SparseMatrix(new double[][] { { 2, 0, 4, 1, 0 }, + { 0, 6, 0, 0, 0 } }); + assertEquals(m1.getFillRatio(), 0.4f); + } + + /** + * Verify that the results of method tred() are the same if the calculation is + * redone + */ + @Test(groups = "Functional") + public void testTred_reproducible() + { + /* + * make a pseudo-random symmetric matrix as required for tred/tqli + */ + int rows = 10; + int cols = rows; + double[][] d = getSparseValues(rows, cols, 3); + + /* + * make a copy of the values so m1, m2 are not + * sharing arrays! + */ + double[][] d1 = new double[rows][cols]; + for (int row = 0; row < rows; row++) + { + for (int col = 0; col < cols; col++) + { + d1[row][col] = d[row][col]; + } + } + Matrix m1 = new SparseMatrix(d); + Matrix m2 = new SparseMatrix(d1); + assertMatricesMatch(m1, m2, 1.0e16); // sanity check + m1.tred(); + m2.tred(); + assertMatricesMatch(m1, m2, 0.00001d); + } +} \ No newline at end of file diff --git a/test/jalview/schemes/ScoreMatrixPrinter.java b/test/jalview/schemes/ScoreMatrixPrinter.java index a743163..80241fb 100644 --- a/test/jalview/schemes/ScoreMatrixPrinter.java +++ b/test/jalview/schemes/ScoreMatrixPrinter.java @@ -26,7 +26,6 @@ import jalview.gui.JvOptionPane; import java.util.Map; import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; public class ScoreMatrixPrinter { @@ -38,7 +37,6 @@ public class ScoreMatrixPrinter JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION); } - @Test(groups = { "Functional" }) public void printAllMatrices() { for (Map.Entry sm : ResidueProperties.scoreMatrices @@ -49,7 +47,6 @@ public class ScoreMatrixPrinter } } - @Test(groups = { "Functional" }) public void printHTMLMatrices() { for (Map.Entry _sm : ResidueProperties.scoreMatrices diff --git a/test/jalview/schemes/ScoreMatrixTest.java b/test/jalview/schemes/ScoreMatrixTest.java new file mode 100644 index 0000000..e15dd41 --- /dev/null +++ b/test/jalview/schemes/ScoreMatrixTest.java @@ -0,0 +1,168 @@ +package jalview.schemes; + +import static org.testng.Assert.assertEquals; + +import jalview.math.MatrixI; + +import org.testng.annotations.Test; + +public class ScoreMatrixTest +{ + @Test(groups = "Functional") + public void testSymmetric() + { + verifySymmetric(ResidueProperties.getScoreMatrix("BLOSUM62")); + verifySymmetric(ResidueProperties.getScoreMatrix("PAM250")); + verifySymmetric(ResidueProperties.getScoreMatrix("DNA")); + } + + private void verifySymmetric(ScoreMatrix sm) + { + int[][] m = sm.getMatrix(); + int rows = m.length; + for (int row = 0; row < rows; row++) + { + assertEquals(m[row].length, rows); + for (int col = 0; col < rows; col++) + { + assertEquals(m[row][col], m[col][row], String.format("%s [%s, %s]", + sm.getName(), ResidueProperties.aa[row], + ResidueProperties.aa[col])); + } + } + + /* + * also check the score matrix is sized for + * the number of symbols scored, plus gap + */ + assertEquals(rows, (sm.isDNA() ? ResidueProperties.maxNucleotideIndex + : ResidueProperties.maxProteinIndex) + 1); + } + + /** + * A test that just asserts the expected values in the Blosum62 score matrix + */ + @Test(groups = "Functional") + public void testBlosum62_values() + { + ScoreMatrix sm = ResidueProperties.getScoreMatrix("BLOSUM62"); + + /* + * verify expected scores against ARNDCQEGHILKMFPSTWYVBZX + * scraped from https://www.ncbi.nlm.nih.gov/Class/FieldGuide/BLOSUM62.txt + */ + verifyValues(sm, 'A', new int[] { 4, -1, -2, -2, 0, -1, -1, 0, -2, -1, + -1, -1, -1, -2, -1, 1, 0, -3, -2, 0, -2, -1, 0 }); + verifyValues(sm, 'R', new int[] { -1, 5, 0, -2, -3, 1, 0, -2, 0, -3, + -2, 2, -1, -3, -2, -1, -1, -3, -2, -3, -1, 0, -1 }); + verifyValues(sm, 'N', new int[] { -2, 0, 6, 1, -3, 0, 0, 0, 1, -3, -3, + 0, -2, -3, -2, 1, 0, -4, -2, -3, 3, 0, -1 }); + verifyValues(sm, 'D', new int[] { -2, -2, 1, 6, -3, 0, 2, -1, -1, -3, + -4, -1, -3, -3, -1, 0, -1, -4, -3, -3, 4, 1, -1 }); + verifyValues(sm, 'C', new int[] { 0, -3, -3, -3, 9, -3, -4, -3, -3, -1, + -1, -3, -1, -2, -3, -1, -1, -2, -2, -1, -3, -3, -2 }); + verifyValues(sm, 'Q', new int[] { -1, 1, 0, 0, -3, 5, 2, -2, 0, -3, -2, + 1, 0, -3, -1, 0, -1, -2, -1, -2, 0, 3, -1 }); + verifyValues(sm, 'E', new int[] { -1, 0, 0, 2, -4, 2, 5, -2, 0, -3, -3, + 1, -2, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1 }); + verifyValues(sm, 'G', new int[] { 0, -2, 0, -1, -3, -2, -2, 6, -2, -4, + -4, -2, -3, -3, -2, 0, -2, -2, -3, -3, -1, -2, -1 }); + verifyValues(sm, 'H', new int[] { -2, 0, 1, -1, -3, 0, 0, -2, 8, -3, + -3, -1, -2, -1, -2, -1, -2, -2, 2, -3, 0, 0, -1 }); + verifyValues(sm, 'I', new int[] { -1, -3, -3, -3, -1, -3, -3, -4, -3, + 4, 2, -3, 1, 0, -3, -2, -1, -3, -1, 3, -3, -3, -1 }); + verifyValues(sm, 'L', new int[] { -1, -2, -3, -4, -1, -2, -3, -4, -3, + 2, 4, -2, 2, 0, -3, -2, -1, -2, -1, 1, -4, -3, -1 }); + verifyValues(sm, 'K', new int[] { -1, 2, 0, -1, -3, 1, 1, -2, -1, -3, + -2, 5, -1, -3, -1, 0, -1, -3, -2, -2, 0, 1, -1 }); + verifyValues(sm, 'M', new int[] { -1, -1, -2, -3, -1, 0, -2, -3, -2, 1, + 2, -1, 5, 0, -2, -1, -1, -1, -1, 1, -3, -1, -1 }); + verifyValues(sm, 'F', new int[] { -2, -3, -3, -3, -2, -3, -3, -3, -1, + 0, 0, -3, 0, 6, -4, -2, -2, 1, 3, -1, -3, -3, -1 }); + verifyValues(sm, 'P', new int[] { -1, -2, -2, -1, -3, -1, -1, -2, -2, + -3, -3, -1, -2, -4, 7, -1, -1, -4, -3, -2, -2, -1, -2 }); + verifyValues(sm, 'S', new int[] { 1, -1, 1, 0, -1, 0, 0, 0, -1, -2, -2, + 0, -1, -2, -1, 4, 1, -3, -2, -2, 0, 0, 0 }); + verifyValues(sm, 'T', new int[] { 0, -1, 0, -1, -1, -1, -1, -2, -2, -1, + -1, -1, -1, -2, -1, 1, 5, -2, -2, 0, -1, -1, 0 }); + verifyValues(sm, 'W', new int[] { -3, -3, -4, -4, -2, -2, -3, -2, -2, + -3, -2, -3, -1, 1, -4, -3, -2, 11, 2, -3, -4, -3, -2 }); + verifyValues(sm, 'Y', new int[] { -2, -2, -2, -3, -2, -1, -2, -3, 2, + -1, -1, -2, -1, 3, -3, -2, -2, 2, 7, -1, -3, -2, -1 }); + verifyValues(sm, 'V', new int[] { 0, -3, -3, -3, -1, -2, -2, -3, -3, 3, + 1, -2, 1, -1, -2, -2, 0, -3, -1, 4, -3, -2, -1 }); + verifyValues(sm, 'B', new int[] { -2, -1, 3, 4, -3, 0, 1, -1, 0, -3, + -4, 0, -3, -3, -2, 0, -1, -4, -3, -3, 4, 1, -1 }); + verifyValues(sm, 'Z', new int[] { -1, 0, 0, 1, -3, 3, 4, -2, 0, -3, -3, + 1, -1, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1 }); + verifyValues(sm, 'X', new int[] { 0, -1, -1, -1, -2, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -2, 0, 0, -2, -1, -1, -1, -1, -1 }); + } + /** + * Helper method to check pairwise scores for one residue + * + * @param sm + * @param res + * @param expected + * score values against 'res', in ResidueProperties.aaIndex order + */ + private void verifyValues(ScoreMatrix sm, char res, int[] expected) + { + for (int j = 0; j < expected.length; j++) + { + char c2 = ResidueProperties.aa[j].charAt(0); + assertEquals(sm.getPairwiseScore(res, c2), expected[j], + String.format("%s->%s", res, c2)); + } + } + + @Test(groups = "Functional") + public void testComputePairwiseScores() + { + String[] seqs = new String[] { "FKL", "R-D", "QIA", "GWC" }; + ScoreMatrix sm = ResidueProperties.getScoreMatrix("BLOSUM62"); + + MatrixI pairwise = sm.computePairwiseScores(seqs); + + /* + * should be NxN where N = number of sequences + */ + assertEquals(pairwise.height(), 4); + assertEquals(pairwise.width(), 4); + + /* + * should be symmetrical (because BLOSUM62 is) + */ + for (int i = 0; i < pairwise.height(); i++) + { + for (int j = 0; j < pairwise.width(); j++) + { + assertEquals(pairwise.getValue(i, j), pairwise.getValue(j, i), + "Not symmetric"); + } + } + /* + * verify expected BLOSUM dot product scores + */ + // F.F + K.K + L.L = 6 + 5 + 4 = 15 + assertEquals(pairwise.getValue(0, 0), 15d); + // R.R + -.- + D.D = 5 + 1 + 6 = 12 + assertEquals(pairwise.getValue(1, 1), 12d); + // Q.Q + I.I + A.A = 5 + 4 + 4 = 13 + assertEquals(pairwise.getValue(2, 2), 13d); + // G.G + W.W + C.C = 6 + 11 + 9 = 26 + assertEquals(pairwise.getValue(3, 3), 26d); + // F.R + K.- + L.D = -3 + -4 + -4 = -11 + assertEquals(pairwise.getValue(0, 1), -11d); + // F.Q + K.I + L.A = -3 + -3 + -1 = -7 + assertEquals(pairwise.getValue(0, 2), -7d); + // F.G + K.W + L.C = -3 + -3 + -1 = -7 + assertEquals(pairwise.getValue(0, 3), -7d); + // R.Q + -.I + D.A = 1 + -4 + -2 = -5 + assertEquals(pairwise.getValue(1, 2), -5d); + // R.G + -.W + D.C = -2 + -4 + -3 = -9 + assertEquals(pairwise.getValue(1, 3), -9d); + // Q.G + I.W + A.C = -2 + -3 + 0 = -5 + assertEquals(pairwise.getValue(2, 3), -5d); + } +}