Merge branch 'develop' into bug/JAL-98consensusMemory
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 30 Sep 2016 13:23:46 +0000 (14:23 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 30 Sep 2016 13:23:46 +0000 (14:23 +0100)
src/jalview/analysis/AAFrequency.java
src/jalview/analysis/Conservation.java
src/jalview/analysis/Profile.java [new file with mode: 0644]
src/jalview/ext/android/ContainerHelpers.java [new file with mode: 0644]
src/jalview/ext/android/SparseIntArray.java [new file with mode: 0644]

index fb49541..49c2340 100755 (executable)
@@ -25,6 +25,8 @@ import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.SequenceI;
+import jalview.ext.android.SparseIntArray;
+import jalview.util.Comparison;
 import jalview.util.Format;
 import jalview.util.MappingUtils;
 import jalview.util.QuickSort;
@@ -108,46 +110,40 @@ public class AAFrequency
   public static final void calculate(SequenceI[] sequences, int start,
           int end, Hashtable[] result, boolean profile)
   {
+    long now = System.currentTimeMillis();
     Hashtable residueHash;
-    int maxCount, nongap, i, j, v;
-    int jSize = sequences.length;
-    String maxResidue;
+    int seqCount = sequences.length;
     char c = '-';
-    float percentage;
+    SparseIntArray profileSizes = new SparseIntArray();
 
-    int[] values = new int[255];
-
-    char[] seq;
-
-    for (i = start; i < end; i++)
+    for (int column = start; column < end; column++)
     {
       residueHash = new Hashtable();
-      maxCount = 0;
-      maxResidue = "";
-      nongap = 0;
-      values = new int[255];
-
-      for (j = 0; j < jSize; j++)
+      int maxCount = 0;
+      String maxResidue = "";
+      int nongap = 0;
+      // int [] values = new int[255];
+      int guessProfileSize = estimateProfileSize(profileSizes);
+      SparseIntArray values = new SparseIntArray(guessProfileSize);
+
+      for (int row = 0; row < seqCount; row++)
       {
-        if (sequences[j] == null)
+        if (sequences[row] == null)
         {
           System.err
                   .println("WARNING: Consensus skipping null sequence - possible race condition.");
           continue;
         }
-        seq = sequences[j].getSequence();
-        if (seq.length > i)
+        char[] seq = sequences[row].getSequence();
+        if (seq.length > column)
         {
-          c = seq[i];
-
-          if (c == '.' || c == ' ')
-          {
-            c = '-';
-          }
+          c = seq[column];
 
-          if (c == '-')
+          if (Comparison.isGap(c))
           {
-            values['-']++;
+            // values['-']++;
+            // values.put('-', values.get('-') + 1);
+            values.add('-', 1);
             continue;
           }
           else if ('a' <= c && c <= 'z')
@@ -156,38 +152,48 @@ public class AAFrequency
           }
 
           nongap++;
-          values[c]++;
-
+          // values[c]++;
+          // values.put(c, values.get(c) + 1);
+          values.add(c, 1);
         }
         else
         {
-          values['-']++;
+          /*
+           * here we count as a gap if the sequence doesn't
+           * reach this column (is that correct?)
+           */
+          // values['-']++;
+          // values.put('-', values.get('-') + 1);
+          values.add('-', 1);
         }
       }
-      if (jSize == 1)
+      if (seqCount == 1)
       {
         maxResidue = String.valueOf(c);
         maxCount = 1;
       }
       else
       {
-        for (v = 'A'; v <= 'Z'; v++)
+        // iterate over values keys not alphabet
+        // for (int v = 'A'; v <= 'Z'; v++)
+        for (int k = 0; k < values.size(); k++)
         {
-          // TODO why ignore values[v] == 1?
-          if (values[v] < 1 /* 2 */|| values[v] < maxCount)
+          int v = values.keyAt(k);
+          int count = values.valueAt(k); // values[v];
+          if (v == '-' || count < 1 || count < maxCount)
           {
             continue;
           }
 
-          if (values[v] > maxCount)
+          if (count > maxCount)
           {
-            maxResidue = CHARS[v - 'A'];
+            maxResidue = String.valueOf((char) v);// CHARS[v - 'A'];
           }
-          else if (values[v] == maxCount)
+          else if (count == maxCount)
           {
-            maxResidue += CHARS[v - 'A'];
+            maxResidue += String.valueOf((char) v); // CHARS[v - 'A'];
           }
-          maxCount = values[v];
+          maxCount = count;
         }
       }
       if (maxResidue.length() == 0)
@@ -196,14 +202,14 @@ public class AAFrequency
       }
       if (profile)
       {
-        // TODO use a 1-dimensional array with jSize, nongap in [0] and [1]
-        residueHash.put(PROFILE, new int[][] { values,
-            new int[] { jSize, nongap } });
+        // residueHash.put(PROFILE, new int[][] { values,
+        // new int[] { jSize, nongap } });
+        residueHash.put(PROFILE, new Profile(values, seqCount, nongap));
       }
       residueHash.put(MAXCOUNT, new Integer(maxCount));
       residueHash.put(MAXRESIDUE, maxResidue);
 
-      percentage = ((float) maxCount * 100) / jSize;
+      float percentage = ((float) maxCount * 100) / seqCount;
       residueHash.put(PID_GAPS, new Float(percentage));
 
       if (nongap > 0)
@@ -213,8 +219,36 @@ public class AAFrequency
       }
       residueHash.put(PID_NOGAPS, new Float(percentage));
 
-      result[i] = residueHash;
+      result[column] = residueHash;
+
+      profileSizes.add(values.size(), 1);
     }
+    long elapsed = System.currentTimeMillis() - now;
+    System.out.println(elapsed);
+  }
+
+  /**
+   * Make an estimate of the profile size we are going to compute i.e. how many
+   * different characters may be present in it. Overestimating has a cost of
+   * using more memory than necessary. Underestimating has a cost of needing to
+   * extend the SparseIntArray holding the profile counts.
+   * 
+   * @param profileSizes
+   *          counts of sizes of profiles so far encountered
+   * @return
+   */
+  static int estimateProfileSize(SparseIntArray profileSizes)
+  {
+    if (profileSizes.size() == 0)
+    {
+      return 4;
+    }
+
+    /*
+     * could do a statistical heuristic here e.g. 75%ile
+     * for now just return the largest value
+     */
+    return profileSizes.keyAt(profileSizes.size() - 1);
   }
 
   /**
@@ -268,6 +302,7 @@ public class AAFrequency
           boolean ignoreGapsInConsensusCalculation,
           boolean includeAllConsSymbols, char[] alphabet, long nseq)
   {
+     long now = System.currentTimeMillis();
     if (consensus == null || consensus.annotations == null
             || consensus.annotations.length < width)
     {
@@ -308,55 +343,55 @@ public class AAFrequency
       {
         mouseOver.append(hci.get(AAFrequency.MAXRESIDUE) + " ");
       }
-      int[][] profile = (int[][]) hci.get(AAFrequency.PROFILE);
+      // int[][] profile = (int[][]) hci.get(AAFrequency.PROFILE);
+      Profile profile = (Profile) hci.get(AAFrequency.PROFILE);
       if (profile != null && includeAllConsSymbols)
       {
-        int sequenceCount = profile[1][0];
-        int nonGappedCount = profile[1][1];
+        int sequenceCount = profile.height;// profile[1][0];
+        int nonGappedCount = profile.nonGapped;// [1][1];
         int normalisedBy = ignoreGapsInConsensusCalculation ? nonGappedCount
                 : sequenceCount;
         mouseOver.setLength(0);
-        if (alphabet != null)
+        // TODO do this sort once only in calculate()?
+        // char[][] ca = new char[profile[0].length][];
+        // /int length = profile[0].length;
+        int length = profile.profile.size();
+        char[] ca = new char[length];
+        // float[] vl = new float[length];
+        int[] vl = new int[length];
+        for (int c = 0; c < ca.length; c++)
+        {
+          int theChar = profile.profile.keyAt(c);
+          ca[c] = (char) theChar;// c;
+          // ca[c] = new char[]
+          // { (char) c };
+          vl[c] = profile.profile.valueAt(c);// profile[0][c];
+        }
+
+        /*
+         * sort characters into ascending order of their counts
+         */
+        QuickSort.sort(vl, ca);
+
+        /*
+         * traverse in reverse order (highest count first) to build tooltip
+         */
+        // for (int p = 0, c = ca.length - 1; profile[0][ca[c]] > 0; c--)
+        for (int p = 0, c = ca.length - 1; c >= 0; c--)
         {
-          for (int c = 0; c < alphabet.length; c++)
+          final char residue = ca[c];
+          if (residue != '-')
           {
-            float tval = profile[0][alphabet[c]] * 100f / normalisedBy;
+            // float tval = profile[0][residue] * 100f / normalisedBy;
+            // float tval = profile[0][residue] * 100f / normalisedBy;
+            float tval = (vl[c] * 100f) / normalisedBy;
             mouseOver
-                    .append(((c == 0) ? "" : "; "))
-                    .append(alphabet[c])
+                    .append((((p == 0) ? "" : "; ")))
+                    .append(residue)
                     .append(" ")
                     .append(((fmt != null) ? fmt.form(tval) : ((int) tval)))
                     .append("%");
-          }
-        }
-        else
-        {
-          // TODO do this sort once only in calculate()?
-          // char[][] ca = new char[profile[0].length][];
-          char[] ca = new char[profile[0].length];
-          float[] vl = new float[profile[0].length];
-          for (int c = 0; c < ca.length; c++)
-          {
-            ca[c] = (char) c;
-            // ca[c] = new char[]
-            // { (char) c };
-            vl[c] = profile[0][c];
-          }
-          QuickSort.sort(vl, ca);
-          for (int p = 0, c = ca.length - 1; profile[0][ca[c]] > 0; c--)
-          {
-            final char residue = ca[c];
-            if (residue != '-')
-            {
-              float tval = profile[0][residue] * 100f / normalisedBy;
-              mouseOver
-                      .append((((p == 0) ? "" : "; ")))
-                      .append(residue)
-                      .append(" ")
-                      .append(((fmt != null) ? fmt.form(tval)
-                              : ((int) tval))).append("%");
-              p++;
-            }
+            p++;
           }
         }
       }
@@ -369,6 +404,8 @@ public class AAFrequency
       consensus.annotations[i] = new Annotation(maxRes,
               mouseOver.toString(), ' ', value);
     }
+     long elapsed = System.currentTimeMillis() - now;
+     System.out.println(-elapsed);
   }
 
   /**
@@ -410,29 +447,47 @@ public class AAFrequency
           boolean ignoreGaps)
   {
     int[] rtnval = new int[64];
-    int[][] profile = (int[][]) hconsensus.get(AAFrequency.PROFILE);
+    // int[][] profile = (int[][]) hconsensus.get(AAFrequency.PROFILE);
+    Profile profile = (Profile) hconsensus.get(AAFrequency.PROFILE);
     if (profile == null)
     {
       return null;
     }
-    char[] ca = new char[profile[0].length];
-    float[] vl = new float[profile[0].length];
-    for (int c = 0; c < ca.length; c++)
+    // int profileLength = profile[0].length;
+    int profileLength = profile.profile.size();
+    char[] ca = new char[profileLength];
+    float[] vl = new float[profileLength];
+    // for (int c = 0; c < ca.length; c++)
+    // {
+    // ca[c] = (char) c;
+    // vl[c] = profile[0][c];
+    // }
+    for (int i = 0; i < profileLength; i++)
     {
-      ca[c] = (char) c;
-      vl[c] = profile[0][c];
+      int c = profile.profile.keyAt(i);
+      ca[i] = (char) c;
+      vl[i] = profile.profile.valueAt(i);
     }
     QuickSort.sort(vl, ca);
     int nextArrayPos = 2;
     int totalPercentage = 0;
     int distinctValuesCount = 0;
-    final int divisor = profile[1][ignoreGaps ? 1 : 0];
-    for (int c = ca.length - 1; profile[0][ca[c]] > 0; c--)
+    final int divisor = ignoreGaps ? profile.nonGapped : profile.height;
+    // final int divisor = profile[1][ignoreGaps ? 1 : 0];
+    int j = profile.profile.size();
+    for (int i = 0; i < j; i++)
+//    for (int c = ca.length - 1; profile[0][ca[c]] > 0; c--)
     {
-      if (ca[c] != '-')
+      int theChar = profile.profile.keyAt(i);
+      int charCount = profile.profile.valueAt(i);
+    
+//      if (ca[c] != '-')
+        if (theChar != '-')
       {
-        rtnval[nextArrayPos++] = ca[c];
-        final int percentage = (int) (profile[0][ca[c]] * 100f / divisor);
+//        rtnval[nextArrayPos++] = ca[c];
+        rtnval[nextArrayPos++] = theChar;
+//        final int percentage = (int) (profile[0][ca[c]] * 100f / divisor);
+        final int percentage = (charCount * 100) / divisor;
         rtnval[nextArrayPos++] = percentage;
         totalPercentage += percentage;
         distinctValuesCount++;
index 711710b..0467900 100755 (executable)
@@ -24,12 +24,13 @@ import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
+import jalview.ext.android.SparseIntArray;
 import jalview.schemes.ResidueProperties;
 
 import java.awt.Color;
-import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 import java.util.Vector;
 
 /**
@@ -188,21 +189,22 @@ public class Conservation
    */
   public void calculate()
   {
-    int thresh, j, jSize = sequences.length;
-    int[] values; // Replaces residueHash
-    char c;
+    int jSize = sequences.length;
+    // int[] values; // Replaces residueHash
+    SparseIntArray values = new SparseIntArray();
 
-    total = new Hashtable[maxLength];
+    total = new Map[maxLength];
 
     for (int i = start; i <= end; i++)
     {
-      values = new int[255];
+      // values = new int[255];
+      values.clear();
 
-      for (j = 0; j < jSize; j++)
+      for (int j = 0; j < jSize; j++)
       {
         if (sequences[j].getLength() > i)
         {
-          c = sequences[j].getCharAt(i);
+          char c = sequences[j].getCharAt(i);
 
           if (canonicaliseAa)
           { // lookup the base aa code symbol
@@ -228,23 +230,29 @@ public class Conservation
 
             c = toUpperCase(c);
           }
-          values[c]++;
+          // values[c]++;
+          values.add(c, 1);
         }
         else
         {
-          values['-']++;
+          // values['-']++;
+          values.add('-', 1);
         }
       }
 
       // What is the count threshold to count the residues in residueHash()
-      thresh = (threshold * (jSize)) / 100;
+      int thresh = (threshold * jSize) / 100;
 
       // loop over all the found residues
-      Hashtable<String, Integer> resultHash = new Hashtable<String, Integer>();
-      for (char v = '-'; v < 'Z'; v++)
+      // Hashtable<String, Integer> resultHash = new Hashtable<String,
+      // Integer>();
+      Map<String, Integer> resultHash = new TreeMap<String, Integer>();
+      // for (char v = '-'; v < 'Z'; v++)
+      for (int key = 0; key < values.size(); key++)
       {
-
-        if (values[v] > thresh)
+        char v = (char) values.keyAt(key);
+        // if (values[v] > thresh)
+        if (values.valueAt(key) > thresh)
         {
           String res = String.valueOf(v);
 
@@ -359,7 +367,7 @@ public class Conservation
    */
   public void verdict(boolean consflag, float percentageGaps)
   {
-    StringBuffer consString = new StringBuffer();
+    StringBuilder consString = new StringBuilder(end);
 
     // NOTE THIS SHOULD CHECK IF THE CONSEQUENCE ALREADY
     // EXISTS AND NOT OVERWRITE WITH '-', BUT THIS CASE
@@ -374,7 +382,9 @@ public class Conservation
       int[] gapcons = countConsNGaps(i);
       int totGaps = gapcons[1];
       float pgaps = ((float) totGaps * 100) / sequences.length;
-      consSymbs[i - start] = new String();
+      StringBuilder positives = new StringBuilder(64);
+      StringBuilder negatives = new StringBuilder(32);
+      // consSymbs[i - start] = "";
 
       if (percentageGaps > pgaps)
       {
@@ -389,7 +399,9 @@ public class Conservation
           {
             if (result == 1)
             {
-              consSymbs[i - start] = type + " " + consSymbs[i - start];
+              // consSymbs[i - start] = type + " " + consSymbs[i - start];
+              positives.append(positives.length() == 0 ? "" : " ");
+              positives.append(type);
               count++;
             }
           }
@@ -399,16 +411,31 @@ public class Conservation
             {
               if (result == 0)
               {
-                consSymbs[i - start] = consSymbs[i - start] + " !" + type;
+                /*
+                 * add negatively conserved properties on the end
+                 */
+                // consSymbs[i - start] = consSymbs[i - start] + " !" + type;
+                negatives.append(negatives.length() == 0 ? "" : " ");
+                negatives.append("!").append(type);
               }
               else
               {
-                consSymbs[i - start] = type + " " + consSymbs[i - start];
+                /*
+                 * put positively conserved properties on the front
+                 */
+                // consSymbs[i - start] = type + " " + consSymbs[i - start];
+                positives.append(positives.length() == 0 ? "" : " ");
+                positives.append(type);
               }
               count++;
             }
           }
         }
+        if (negatives.length() > 0)
+        {
+          positives.append(" ").append(negatives);
+        }
+        consSymbs[i - start] = positives.toString();
 
         if (count < 10)
         {
diff --git a/src/jalview/analysis/Profile.java b/src/jalview/analysis/Profile.java
new file mode 100644 (file)
index 0000000..b5857c7
--- /dev/null
@@ -0,0 +1,33 @@
+package jalview.analysis;
+
+import jalview.ext.android.SparseIntArray;
+
+public class Profile
+{
+  /*
+   * array of keys (chars) and values (counts) 
+   */
+  public final SparseIntArray profile;
+
+  /*
+   * the number of sequences in the profile
+   */
+  public final int height;
+
+  /*
+   * the number of non-gapped sequences in the profile
+   */
+  public final int nonGapped;
+
+  public Profile(SparseIntArray counts, int ht, int nongappedCount)
+  {
+    this.profile = counts;
+    this.height = ht;
+    this.nonGapped = nongappedCount;
+  }
+
+  public SparseIntArray getProfile()
+  {
+    return profile;
+  }
+}
diff --git a/src/jalview/ext/android/ContainerHelpers.java b/src/jalview/ext/android/ContainerHelpers.java
new file mode 100644 (file)
index 0000000..cae77b5
--- /dev/null
@@ -0,0 +1,77 @@
+package jalview.ext.android;
+
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class ContainerHelpers
+{
+  static final boolean[] EMPTY_BOOLEANS = new boolean[0];
+
+  static final int[] EMPTY_INTS = new int[0];
+
+  static final long[] EMPTY_LONGS = new long[0];
+
+  static final Object[] EMPTY_OBJECTS = new Object[0];
+
+  // This is Arrays.binarySearch(), but doesn't do any argument validation.
+  static int binarySearch(int[] array, int size, int value)
+  {
+    int lo = 0;
+    int hi = size - 1;
+    while (lo <= hi)
+    {
+      final int mid = (lo + hi) >>> 1;
+      final int midVal = array[mid];
+      if (midVal < value)
+      {
+        lo = mid + 1;
+      }
+      else if (midVal > value)
+      {
+        hi = mid - 1;
+      }
+      else
+      {
+        return mid; // value found
+      }
+    }
+    return ~lo; // value not present
+  }
+
+  static int binarySearch(long[] array, int size, long value)
+  {
+    int lo = 0;
+    int hi = size - 1;
+    while (lo <= hi)
+    {
+      final int mid = (lo + hi) >>> 1;
+      final long midVal = array[mid];
+      if (midVal < value)
+      {
+        lo = mid + 1;
+      }
+      else if (midVal > value)
+      {
+        hi = mid - 1;
+      }
+      else
+      {
+        return mid; // value found
+      }
+    }
+    return ~lo; // value not present
+  }
+}
diff --git a/src/jalview/ext/android/SparseIntArray.java b/src/jalview/ext/android/SparseIntArray.java
new file mode 100644 (file)
index 0000000..4ddf776
--- /dev/null
@@ -0,0 +1,390 @@
+package jalview.ext.android;
+
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * SparseIntArrays map integers to integers. Unlike a normal array of integers,
+ * there can be gaps in the indices. It is intended to be more memory efficient
+ * than using a HashMap to map Integers to Integers, both because it avoids
+ * auto-boxing keys and values and its data structure doesn't rely on an extra
+ * entry object for each mapping.
+ *
+ * <p>
+ * 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%.
+ * </p>
+ *
+ * <p>
+ * It is possible to iterate over the items in this container using
+ * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
+ * <code>keyAt(int)</code> 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 <code>valueAt(int)<code>.
+ * </p>
+ */
+public class SparseIntArray implements Cloneable
+{
+  private int[] mKeys;
+
+  private int[] mValues;
+
+  private int mSize;
+
+  /**
+   * Creates a new SparseIntArray containing no mappings.
+   */
+  public SparseIntArray()
+  {
+    this(10);
+  }
+
+  /**
+   * Creates a new SparseIntArray 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 SparseIntArray(int initialCapacity)
+  {
+    if (initialCapacity == 0)
+    {
+      mKeys = ContainerHelpers.EMPTY_INTS;
+      mValues = ContainerHelpers.EMPTY_INTS;
+    }
+    else
+    {
+      initialCapacity = idealIntArraySize(initialCapacity);
+      mKeys = new int[initialCapacity];
+      mValues = new int[initialCapacity];
+    }
+    mSize = 0;
+  }
+
+  @Override
+  public SparseIntArray clone()
+  {
+    SparseIntArray clone = null;
+    try
+    {
+      clone = (SparseIntArray) super.clone();
+      clone.mKeys = mKeys.clone();
+      clone.mValues = mValues.clone();
+    } catch (CloneNotSupportedException cnse)
+    {
+      /* ignore */
+    }
+    return clone;
+  }
+
+  /**
+   * Gets the int mapped from the specified key, or <code>0</code> if no such
+   * mapping has been made.
+   */
+  public int get(int key)
+  {
+    return get(key, 0);
+  }
+
+  /**
+   * Gets the int mapped from the specified key, or the specified value if no
+   * such mapping has been made.
+   */
+  public int get(int key, int 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, int value)
+  {
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+    if (i >= 0)
+    {
+      mValues[i] = value;
+    }
+    else
+    {
+      i = ~i;
+      if (mSize >= mKeys.length)
+      {
+        int n = idealIntArraySize(mSize + 1);
+        int[] nkeys = new int[n];
+        int[] nvalues = new int[n];
+        // Log.e("SparseIntArray", "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("SparseIntArray", "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 SparseIntArray currently
+   * stores.
+   */
+  public int size()
+  {
+    return mSize;
+  }
+
+  /**
+   * Given an index in the range <code>0...size()-1</code>, returns the key from
+   * the <code>index</code>th key-value mapping that this SparseIntArray stores.
+   *
+   * <p>
+   * The keys corresponding to indices in ascending order are guaranteed to be
+   * in ascending order, e.g., <code>keyAt(0)</code> will return the smallest
+   * key and <code>keyAt(size()-1)</code> will return the largest key.
+   * </p>
+   */
+  public int keyAt(int index)
+  {
+    return mKeys[index];
+  }
+
+  /**
+   * Given an index in the range <code>0...size()-1</code>, returns the value
+   * from the <code>index</code>th key-value mapping that this SparseIntArray
+   * stores.
+   *
+   * <p>
+   * The values corresponding to indices in ascending order are guaranteed to be
+   * associated with keys in ascending order, e.g., <code>valueAt(0)</code> will
+   * return the value associated with the smallest key and
+   * <code>valueAt(size()-1)</code> will return the value associated with the
+   * largest key.
+   * </p>
+   */
+  public int 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(int value)
+  {
+    for (int i = 0; i < mSize; i++)
+    {
+      if (mValues[i] == value)
+      {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Removes all key-value mappings from this SparseIntArray.
+   */
+  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, int value)
+  {
+    if (mSize != 0 && key <= mKeys[mSize - 1])
+    {
+      put(key, value);
+      return;
+    }
+    int pos = mSize;
+    if (pos >= mKeys.length)
+    {
+      int n = idealIntArraySize(pos + 1);
+      int[] nkeys = new int[n];
+      int[] nvalues = new int[n];
+      // Log.e("SparseIntArray", "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;
+  }
+
+  /**
+   * Inlined here by copying from com.android.internal.util.ArrayUtils
+   * 
+   * @param i
+   * @return
+   */
+  public static int idealIntArraySize(int need)
+  {
+    return idealByteArraySize(need * 4) / 4;
+  }
+
+  /**
+   * 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}
+   *
+   * <p>
+   * 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('=');
+      int 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
+   */
+  public void add(int key, int toAdd)
+  {
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+    if (i >= 0)
+    {
+      mValues[i] += toAdd;
+    }
+    else
+    {
+      i = ~i;
+      if (mSize >= mKeys.length)
+      {
+        int n = idealIntArraySize(mSize + 1);
+        int[] nkeys = new int[n];
+        int[] nvalues = new int[n];
+        // Log.e("SparseIntArray", "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("SparseIntArray", "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] = toAdd;
+      mSize++;
+    }
+  }
+}