From: gmungoc Date: Wed, 26 Oct 2016 15:22:45 +0000 (+0100) Subject: Merge branch 'bug/JAL-98consensusMemory' into develop X-Git-Tag: Release_2_10_1^2~22^2~3 X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=fc9aecdad8fb4d6ac38e504b094b29cf2223b32b;hp=08a75320d4e492edb4f308d0ab6c1044896dfd96;p=jalview.git Merge branch 'bug/JAL-98consensusMemory' into develop --- diff --git a/src/jalview/analysis/AAFrequency.java b/src/jalview/analysis/AAFrequency.java index fb49541..61e11c4 100755 --- a/src/jalview/analysis/AAFrequency.java +++ b/src/jalview/analysis/AAFrequency.java @@ -20,11 +20,14 @@ */ package jalview.analysis; +import jalview.analysis.ResidueCount.SymbolCounts; import jalview.datamodel.AlignedCodonFrame; 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; @@ -44,20 +47,8 @@ import java.util.List; */ public class AAFrequency { - private static final int TO_UPPER_CASE = 'A' - 'a'; // -32 - - public static final String MAXCOUNT = "C"; - - public static final String MAXRESIDUE = "R"; - - public static final String PID_GAPS = "G"; - - public static final String PID_NOGAPS = "N"; - public static final String PROFILE = "P"; - public static final String ENCODED_CHARS = "E"; - /* * Quick look-up of String value of char 'A' to 'Z' */ @@ -71,13 +62,13 @@ public class AAFrequency } } - public static final Hashtable[] calculate(List list, + public static final Profile[] calculate(List list, int start, int end) { return calculate(list, start, end, false); } - public static final Hashtable[] calculate(List sequences, + public static final Profile[] calculate(List sequences, int start, int end, boolean profile) { SequenceI[] seqs = new SequenceI[sequences.size()]; @@ -93,7 +84,7 @@ public class AAFrequency } } - Hashtable[] reply = new Hashtable[width]; + Profile[] reply = new Profile[width]; if (end >= width) { @@ -105,289 +96,242 @@ public class AAFrequency } } - public static final void calculate(SequenceI[] sequences, int start, - int end, Hashtable[] result, boolean profile) + /** + * Calculate the consensus symbol(s) for each column in the given range. + * + * @param sequences + * @param start + * start column (inclusive, base zero) + * @param end + * end column (exclusive) + * @param result + * array in which to store profile per column + * @param saveFullProfile + * if true, store all symbol counts + */ + public static final void calculate(final SequenceI[] sequences, + int start, int end, Profile[] result, boolean saveFullProfile) { - Hashtable residueHash; - int maxCount, nongap, i, j, v; - int jSize = sequences.length; - String maxResidue; - char c = '-'; - float percentage; + // long now = System.currentTimeMillis(); + int seqCount = sequences.length; + boolean nucleotide = false; + int nucleotideCount = 0; + int peptideCount = 0; - 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]; + /* + * Apply a heuristic to detect nucleotide data (which can + * be counted in more compact arrays); here we test for + * more than 90% nucleotide; recheck every 10 columns in case + * of misleading data e.g. highly conserved Alanine in peptide! + * Mistakenly guessing nucleotide has a small performance cost, + * as it will result in counting in sparse arrays. + * Mistakenly guessing peptide has a small space cost, + * as it will use a larger than necessary array to hold counts. + */ + if (nucleotideCount > 100 && column % 10 == 0) + { + nucleotide = (9 * peptideCount < nucleotideCount); + } + ResidueCount residueCounts = new ResidueCount(nucleotide); - for (j = 0; j < jSize; j++) + 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 == ' ') + char c = seq[column]; + residueCounts.add(c); + if (Comparison.isNucleotide(c)) { - c = '-'; + nucleotideCount++; } - - if (c == '-') + else if (!Comparison.isGap(c)) { - values['-']++; - continue; + peptideCount++; } - else if ('a' <= c && c <= 'z') - { - c += TO_UPPER_CASE; - } - - nongap++; - values[c]++; - } else { - values['-']++; - } - } - if (jSize == 1) - { - maxResidue = String.valueOf(c); - maxCount = 1; - } - else - { - for (v = 'A'; v <= 'Z'; v++) - { - // TODO why ignore values[v] == 1? - if (values[v] < 1 /* 2 */|| values[v] < maxCount) - { - continue; - } - - if (values[v] > maxCount) - { - maxResidue = CHARS[v - 'A']; - } - else if (values[v] == maxCount) - { - maxResidue += CHARS[v - 'A']; - } - maxCount = values[v]; + /* + * count a gap if the sequence doesn't reach this column + */ + residueCounts.addGap(); } } - if (maxResidue.length() == 0) - { - maxResidue = "-"; - } - 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(MAXCOUNT, new Integer(maxCount)); - residueHash.put(MAXRESIDUE, maxResidue); - percentage = ((float) maxCount * 100) / jSize; - residueHash.put(PID_GAPS, new Float(percentage)); + int maxCount = residueCounts.getModalCount(); + String maxResidue = residueCounts.getResiduesForCount(maxCount); + int gapCount = residueCounts.getGapCount(); + Profile profile = new Profile(seqCount, gapCount, maxCount, + maxResidue); - if (nongap > 0) + if (saveFullProfile) { - // calculate for non-gapped too - percentage = ((float) maxCount * 100) / nongap; + profile.setCounts(residueCounts); } - residueHash.put(PID_NOGAPS, new Float(percentage)); - result[i] = residueHash; + result[column] = profile; } + // long elapsed = System.currentTimeMillis() - now; + // System.out.println(elapsed); } /** - * Compute all or part of the annotation row from the given consensus - * hashtable + * 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 consensus - * - pre-allocated annotation row - * @param hconsensus - * @param iStart - * @param width - * @param ignoreGapsInConsensusCalculation - * @param includeAllConsSymbols - * @param nseq + * @param profileSizes + * counts of sizes of profiles so far encountered + * @return */ - public static void completeConsensus(AlignmentAnnotation consensus, - Hashtable[] hconsensus, int iStart, int width, - boolean ignoreGapsInConsensusCalculation, - boolean includeAllConsSymbols, long nseq) + static int estimateProfileSize(SparseIntArray profileSizes) { - completeConsensus(consensus, hconsensus, iStart, width, - ignoreGapsInConsensusCalculation, includeAllConsSymbols, null, - nseq); + 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); } /** * Derive the consensus annotations to be added to the alignment for display. * This does not recompute the raw data, but may be called on a change in - * display options, such as 'show logo', which may in turn result in a change - * in the derived values. + * display options, such as 'ignore gaps', which may in turn result in a + * change in the derived values. * * @param consensus * the annotation row to add annotations to - * @param hconsensus + * @param profiles * the source consensus data * @param iStart * start column * @param width * end column - * @param ignoreGapsInConsensusCalculation - * if true, use the consensus calculated ignoring gaps - * @param includeAllConsSymbols + * @param ignoreGaps + * if true, normalise residue percentages ignoring gaps + * @param showSequenceLogo * if true include all consensus symbols, else just show modal * residue - * @param alphabet * @param nseq * number of sequences */ public static void completeConsensus(AlignmentAnnotation consensus, - Hashtable[] hconsensus, int iStart, int width, - boolean ignoreGapsInConsensusCalculation, - boolean includeAllConsSymbols, char[] alphabet, long nseq) + Profile[] profiles, int iStart, int width, boolean ignoreGaps, + boolean showSequenceLogo, long nseq) { + // long now = System.currentTimeMillis(); if (consensus == null || consensus.annotations == null || consensus.annotations.length < width) { - // called with a bad alignment annotation row - wait for it to be - // initialised properly + /* + * called with a bad alignment annotation row + * wait for it to be initialised properly + */ return; } - final Format fmt = getPercentageFormat(nseq); + final int dp = getPercentageDp(nseq); for (int i = iStart; i < width; i++) { - Hashtable hci; - if (i >= hconsensus.length || ((hci = hconsensus[i]) == null)) - { - // happens if sequences calculated over were shorter than alignment - // width - consensus.annotations[i] = null; - continue; - } - Float fv = (Float) hci - .get(ignoreGapsInConsensusCalculation ? PID_NOGAPS : PID_GAPS); - if (fv == null) + Profile profile; + if (i >= profiles.length || ((profile = profiles[i]) == null)) { + /* + * happens if sequences calculated over were + * shorter than alignment width + */ consensus.annotations[i] = null; - // data has changed below us .. give up and continue; } - float value = fv.floatValue(); - String maxRes = hci.get(AAFrequency.MAXRESIDUE).toString(); - StringBuilder mouseOver = new StringBuilder(64); - if (maxRes.length() > 1) - { - mouseOver.append("[").append(maxRes).append("] "); - maxRes = "+"; - } - else - { - mouseOver.append(hci.get(AAFrequency.MAXRESIDUE) + " "); - } - int[][] profile = (int[][]) hci.get(AAFrequency.PROFILE); - if (profile != null && includeAllConsSymbols) + + float value = profile.getPercentageIdentity(ignoreGaps); + + String description = getTooltip(profile, value, showSequenceLogo, + ignoreGaps, dp); + + String modalResidue = profile.getModalResidue(); + if ("".equals(modalResidue)) { - int sequenceCount = profile[1][0]; - int nonGappedCount = profile[1][1]; - int normalisedBy = ignoreGapsInConsensusCalculation ? nonGappedCount - : sequenceCount; - mouseOver.setLength(0); - if (alphabet != null) - { - for (int c = 0; c < alphabet.length; c++) - { - float tval = profile[0][alphabet[c]] * 100f / normalisedBy; - mouseOver - .append(((c == 0) ? "" : "; ")) - .append(alphabet[c]) - .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++; - } - } - } + modalResidue = "-"; } - else + else if (modalResidue.length() > 1) { - mouseOver.append( - (((fmt != null) ? fmt.form(value) : ((int) value)))) - .append("%"); + modalResidue = "+"; } - consensus.annotations[i] = new Annotation(maxRes, - mouseOver.toString(), ' ', value); + consensus.annotations[i] = new Annotation(modalResidue, + description, ' ', value); } + // long elapsed = System.currentTimeMillis() - now; + // System.out.println(-elapsed); } /** - * Returns a Format designed to show all significant figures for profile - * percentages. For less than 100 sequences, returns null (the integer - * percentage value will be displayed). For 100-999 sequences, returns "%3.1f" + * Returns a tooltip showing either + *
    + *
  • the full profile (percentages of all residues present), if + * showSequenceLogo is true, or
  • + *
  • just the modal (most common) residue(s), if showSequenceLogo is false
  • + *
+ * Percentages are as a fraction of all sequence, or only ungapped sequences + * if ignoreGaps is true. * - * @param nseq + * @param profile + * @param pid + * @param showSequenceLogo + * @param ignoreGaps + * @param dp + * the number of decimal places to format percentages to * @return */ - protected static Format getPercentageFormat(long nseq) + static String getTooltip(Profile profile, float pid, + boolean showSequenceLogo, boolean ignoreGaps, int dp) { - int scale = 0; - while (nseq >= 10) + ResidueCount counts = profile.getCounts(); + + String description = null; + if (counts != null && showSequenceLogo) { - scale++; - nseq /= 10; + int normaliseBy = ignoreGaps ? profile.getNonGapped() : profile + .getHeight(); + description = counts.getTooltip(normaliseBy, dp); + } + else + { + StringBuilder sb = new StringBuilder(64); + String maxRes = profile.getModalResidue(); + if (maxRes.length() > 1) + { + sb.append("[").append(maxRes).append("]"); + } + else + { + sb.append(maxRes); + } + if (maxRes.length() > 0) + { + sb.append(" "); + Format.appendPercentage(sb, pid, dp); + sb.append("%"); + } + description = sb.toString(); } - return scale <= 1 ? null : new Format("%3." + (scale - 1) + "f"); + return description; } /** @@ -399,46 +343,46 @@ public class AAFrequency * in descending order of percentage value * * - * @param hconsensus - * the data table from which to extract and sort values + * @param profile + * the data object from which to extract and sort values * @param ignoreGaps * if true, only non-gapped values are included in percentage * calculations * @return */ - public static int[] extractProfile(Hashtable hconsensus, + public static int[] extractProfile(Profile profile, boolean ignoreGaps) { int[] rtnval = new int[64]; - int[][] profile = (int[][]) hconsensus.get(AAFrequency.PROFILE); - if (profile == null) + ResidueCount counts = profile.getCounts(); + if (counts == 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++) - { - ca[c] = (char) c; - vl[c] = profile[0][c]; - } - QuickSort.sort(vl, ca); + + SymbolCounts symbolCounts = counts.getSymbolCounts(); + char[] symbols = symbolCounts.symbols; + int[] values = symbolCounts.values; + QuickSort.sort(values, symbols); 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.getNonGapped() : profile + .getHeight(); + + /* + * traverse the arrays in reverse order (highest counts first) + */ + for (int i = symbols.length - 1; i >= 0; i--) { - if (ca[c] != '-') - { - rtnval[nextArrayPos++] = ca[c]; - final int percentage = (int) (profile[0][ca[c]] * 100f / divisor); - rtnval[nextArrayPos++] = percentage; - totalPercentage += percentage; - distinctValuesCount++; - } + int theChar = symbols[i]; + int charCount = values[i]; + + rtnval[nextArrayPos++] = theChar; + final int percentage = (charCount * 100) / divisor; + rtnval[nextArrayPos++] = percentage; + totalPercentage += percentage; } - rtnval[0] = distinctValuesCount; + rtnval[0] = symbols.length; rtnval[1] = totalPercentage; int[] result = new int[rtnval.length + 1]; result[0] = AlignmentAnnotation.SEQUENCE_PROFILE; @@ -647,7 +591,7 @@ public class AAFrequency StringBuilder samePercent = new StringBuilder(); String percent = null; String lastPercent = null; - Format fmt = getPercentageFormat(nseqs); + int percentDecPl = getPercentageDp(nseqs); for (int j = codons.length - 1; j >= 0; j--) { @@ -669,7 +613,9 @@ public class AAFrequency final int pct = codonCount * 100 / totalCount; String codon = String .valueOf(CodingUtils.decodeCodon(codonEncoded)); - percent = fmt == null ? Integer.toString(pct) : fmt.form(pct); + StringBuilder sb = new StringBuilder(); + Format.appendPercentage(sb, pct, percentDecPl); + percent = sb.toString(); if (showProfileLogo || codonCount == modalCodonCount) { if (percent.equals(lastPercent) && j > 0) @@ -695,4 +641,23 @@ public class AAFrequency mouseOver.toString(), ' ', pid); } } + + /** + * Returns the number of decimal places to show for profile percentages. For + * less than 100 sequences, returns zero (the integer percentage value will be + * displayed). For 100-999 sequences, returns 1, for 1000-9999 returns 2, etc. + * + * @param nseq + * @return + */ + protected static int getPercentageDp(long nseq) + { + int scale = 0; + while (nseq >= 100) + { + scale++; + nseq /= 10; + } + return scale; + } } diff --git a/src/jalview/analysis/Conservation.java b/src/jalview/analysis/Conservation.java index 75dec63..8127747 100755 --- a/src/jalview/analysis/Conservation.java +++ b/src/jalview/analysis/Conservation.java @@ -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 resultHash = new Hashtable(); - for (char v = '-'; v < 'Z'; v++) + // Hashtable resultHash = new Hashtable(); + Map resultHash = new TreeMap(); + // 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 index 0000000..d94d031 --- /dev/null +++ b/src/jalview/analysis/Profile.java @@ -0,0 +1,157 @@ +package jalview.analysis; + + +/** + * A data bean to hold the result of computing a profile for a column of an + * alignment + * + * @author gmcarstairs + * + */ +public class Profile +{ + /* + * counts of keys (chars) + */ + private ResidueCount counts; + + /* + * the number of sequences in the profile + */ + private int height; + + /* + * the number of non-gapped sequences in the profile + */ + private int gapped; + + /* + * the highest count for any residue in the profile + */ + private int maxCount; + + /* + * the residue (e.g. K) or residues (e.g. KQW) with the + * highest count in the profile + */ + private String modalResidue; + + /** + * Constructor which allows derived data to be stored without having to store + * the full profile + * + * @param seqCount + * the number of sequences in the profile + * @param gaps + * the number of gapped sequences + * @param max + * the highest count for any residue + * @param modalres + * the residue (or concatenated residues) with the highest count + */ + public Profile(int seqCount, int gaps, int max, String modalRes) + { + this.height = seqCount; + this.gapped = gaps; + this.maxCount = max; + this.modalResidue = modalRes; + } + + /** + * Set the full profile of counts + * + * @param residueCounts + */ + public void setCounts(ResidueCount residueCounts) + { + this.counts = residueCounts; + } + + /** + * Returns the percentage identity of the profile, i.e. the highest proportion + * of conserved (equal) symbols. The percentage is as a fraction of all + * sequences, or only ungapped sequences if flag ignoreGaps is set true. + * + * @param ignoreGaps + * @return + */ + public float getPercentageIdentity(boolean ignoreGaps) + { + if (height == 0) + { + return 0f; + } + float pid = 0f; + if (ignoreGaps && gapped < height) + { + pid = (maxCount * 100f) / (height - gapped); + } + else + { + pid = (maxCount * 100f) / height; + } + return pid; + } + + /** + * Returns the full symbol counts for this profile + * + * @return + */ + public ResidueCount getCounts() + { + return counts; + } + + /** + * Returns the number of sequences in the profile + * + * @return + */ + public int getHeight() + { + return height; + } + + /** + * Returns the number of sequences in the profile which had a gap character + * (or were too short to be included in this column's profile) + * + * @return + */ + public int getGapped() + { + return gapped; + } + + /** + * Returns the highest count for any symbol(s) in the profile + * + * @return + */ + public int getMaxCount() + { + return maxCount; + } + + /** + * Returns the symbol (or concatenated symbols) which have the highest count + * in the profile, or an empty string if there were no symbols counted + * + * @return + */ + public String getModalResidue() + { + return modalResidue; + } + + /** + * Answers the number of non-gapped sequences in the profile + * + * @return + */ + public int getNonGapped() + { + return height - gapped; + } +} diff --git a/src/jalview/analysis/ResidueCount.java b/src/jalview/analysis/ResidueCount.java new file mode 100644 index 0000000..75decf2 --- /dev/null +++ b/src/jalview/analysis/ResidueCount.java @@ -0,0 +1,621 @@ +package jalview.analysis; + +import jalview.util.Comparison; +import jalview.util.Format; +import jalview.util.QuickSort; +import jalview.util.SparseCount; + +/** + * A class to count occurrences of residues in a profile, optimised for speed + * and memory footprint. + * @author gmcarstairs + * + */ +public class ResidueCount +{ + /** + * A data bean to hold the results of counting symbols + */ + public class SymbolCounts + { + /** + * the symbols seen (as char values), in no particular order + */ + public final char[] symbols; + + /** + * the counts for each symbol, in the same order as the symbols + */ + public final int[] values; + + SymbolCounts(char[] s, int[] v) + { + symbols = s; + values = v; + } + } + + private static final int TOUPPERCASE = 'A' - 'a'; + + /* + * nucleotide symbols to count (including N unknown) + */ + private static final String NUCS = "ACGNTU"; + + /* + * amino acid symbols to count (including X unknown) + * NB we also include U so as to support counting of RNA bases + * in the "don't know" case of nucleotide / peptide + */ + private static final String AAS = "ACDEFGHIKLMNPQRSTUVWXY"; + + private static final int GAP_COUNT = 0; + + /* + * fast lookup tables holding the index into our count + * arrays of each symbol; index 0 is reserved for gap counting + */ + private static int[] NUC_INDEX = new int[26]; + + private static int[] AA_INDEX = new int[26]; + static + { + for (int i = 0; i < NUCS.length(); i++) + { + NUC_INDEX[NUCS.charAt(i) - 'A'] = i + 1; + } + for (int i = 0; i < AAS.length(); i++) + { + AA_INDEX[AAS.charAt(i) - 'A'] = i + 1; + } + } + + /* + * counts array, just big enough for the nucleotide or peptide + * character set (plus gap counts in position 0) + */ + private short[] counts; + + /* + * alternative array of int counts for use if any count + * exceeds the maximum value of short (32767) + */ + private int[] intCounts; + + /* + * flag set if we switch from short to int counts + */ + private boolean useIntCounts; + + /* + * general-purpose counter, only for use for characters + * that are not in the expected alphabet + */ + private SparseCount otherData; + + /* + * keeps track of the maximum count value recorded + * (if this class every allows decrements, would need to + * calculate this on request instead) + */ + int maxCount; + + /* + * if we think we are counting nucleotide, can get by with smaller + * array to hold counts + */ + private boolean isNucleotide; + + /** + * Default constructor allocates arrays able to count either nucleotide or + * peptide bases. Use this constructor if not sure which the data is. + */ + public ResidueCount() + { + this(false); + } + + /** + * Constructor that allocates an array just big enough for the anticipated + * characters, plus one position to count gaps + */ + public ResidueCount(boolean nucleotide) + { + isNucleotide = nucleotide; + int charsToCount = nucleotide ? NUCS.length() : AAS.length(); + counts = new short[charsToCount + 1]; + } + + /** + * Increments the count for the given character. The supplied character may be + * upper or lower case but counts are for the upper case only. Gap characters + * (space, ., -) are all counted together. + * + * @param c + * @return the new value of the count for the character + */ + public int add(final char c) + { + char u = toUpperCase(c); + int newValue = 0; + int offset = getOffset(u); + + /* + * offset 0 is reserved for gap counting, so 0 here means either + * an unexpected character, or a gap character passed in error + */ + if (offset == 0) + { + if (Comparison.isGap(u)) + { + newValue = addGap(); + } + else + { + newValue = addOtherCharacter(u); + } + } + else + { + newValue = increment(offset); + } + return newValue; + } + + /** + * Increment the count at the specified offset. If this would result in short + * overflow, promote to counting int values instead. + * + * @param offset + * @return the new value of the count at this offset + */ + int increment(int offset) + { + int newValue = 0; + if (useIntCounts) + { + newValue = intCounts[offset]; + intCounts[offset] = ++newValue; + } + else + { + if (counts[offset] == Short.MAX_VALUE) + { + handleOverflow(); + newValue = intCounts[offset]; + intCounts[offset] = ++newValue; + } + else + { + newValue = counts[offset]; + counts[offset] = (short) ++newValue; + } + } + maxCount = Math.max(maxCount, newValue); + return newValue; + } + + /** + * Switch from counting in short to counting in int + */ + synchronized void handleOverflow() + { + intCounts = new int[counts.length]; + for (int i = 0; i < counts.length; i++) + { + intCounts[i] = counts[i]; + } + counts = null; + useIntCounts = true; + } + + /** + * Returns this character's offset in the count array + * + * @param c + * @return + */ + int getOffset(char c) + { + int offset = 0; + if ('A' <= c && c <= 'Z') + { + offset = isNucleotide ? NUC_INDEX[c - 'A'] : AA_INDEX[c - 'A']; + } + return offset; + } + + /** + * @param c + * @return + */ + protected char toUpperCase(final char c) + { + char u = c; + if ('a' <= c && c <= 'z') + { + u = (char) (c + TOUPPERCASE); + } + return u; + } + + /** + * Increment count for some unanticipated character. The first time this + * called, a SparseCount is instantiated to hold these 'extra' counts. + * + * @param c + * @return the new value of the count for the character + */ + int addOtherCharacter(char c) + { + if (otherData == null) + { + otherData = new SparseCount(); + } + int newValue = otherData.add(c, 1); + maxCount = Math.max(maxCount, newValue); + return newValue; + } + + /** + * Set count for some unanticipated character. The first time this called, a + * SparseCount is instantiated to hold these 'extra' counts. + * + * @param c + * @param value + */ + void setOtherCharacter(char c, int value) + { + if (otherData == null) + { + otherData = new SparseCount(); + } + otherData.put(c, value); + } + + /** + * Increment count of gap characters + * + * @return the new count of gaps + */ + public int addGap() + { + int newValue; + if (useIntCounts) + { + newValue = ++intCounts[GAP_COUNT]; + } + else + { + newValue = ++counts[GAP_COUNT]; + } + return newValue; + } + + /** + * Answers true if we are counting ints (only after overflow of short counts) + * + * @return + */ + boolean isCountingInts() + { + return useIntCounts; + } + + /** + * Sets the count for the given character. The supplied character may be upper + * or lower case but counts are for the upper case only. + * + * @param c + * @param count + */ + public void put(char c, int count) + { + char u = toUpperCase(c); + int offset = getOffset(u); + + /* + * offset 0 is reserved for gap counting, so 0 here means either + * an unexpected character, or a gap character passed in error + */ + if (offset == 0) + { + if (Comparison.isGap(u)) + { + set(0, count); + } + else + { + setOtherCharacter(u, count); + maxCount = Math.max(maxCount, count); + } + } + else + { + set(offset, count); + maxCount = Math.max(maxCount, count); + } + } + + /** + * Sets the count at the specified offset. If this would result in short + * overflow, promote to counting int values instead. + * + * @param offset + * @param value + */ + void set(int offset, int value) + { + if (useIntCounts) + { + intCounts[offset] = value; + } + else + { + if (value > Short.MAX_VALUE || value < Short.MIN_VALUE) + { + handleOverflow(); + intCounts[offset] = value; + } + else + { + counts[offset] = (short) value; + } + } + } + + /** + * Returns the count for the given character, or zero if no count held + * + * @param c + * @return + */ + public int getCount(char c) + { + char u = toUpperCase(c); + int offset = getOffset(u); + if (offset == 0) + { + if (!Comparison.isGap(u)) + { + // should have called getGapCount() + return otherData == null ? 0 : otherData.get(u); + } + } + return useIntCounts ? intCounts[offset] : counts[offset]; + } + + public int getGapCount() + { + return useIntCounts ? intCounts[0] : counts[0]; + } + + /** + * Answers true if this object wraps a counter for unexpected characters + * + * @return + */ + boolean isUsingOtherData() + { + return otherData != null; + } + + /** + * Returns the character (or concatenated characters) for the symbol(s) with + * the given count in the profile. Can be used to get the modal residue by + * supplying the modal count value. Returns an empty string if no symbol has + * the given count. The symbols are in alphabetic order of standard peptide or + * nucleotide characters, followed by 'other' symbols if any. + * + * @return + */ + public String getResiduesForCount(int count) + { + if (count == 0) + { + return ""; + } + + /* + * find counts for the given value and append the + * corresponding symbol + */ + StringBuilder modal = new StringBuilder(); + if (useIntCounts) + { + for (int i = 1; i < intCounts.length; i++) + { + if (intCounts[i] == count) + { + modal.append(isNucleotide ? NUCS.charAt(i - 1) : AAS + .charAt(i - 1)); + } + } + } + else + { + for (int i = 1; i < counts.length; i++) + { + if (counts[i] == count) + { + modal.append(isNucleotide ? NUCS.charAt(i - 1) : AAS + .charAt(i - 1)); + } + } + } + if (otherData != null) + { + for (int i = 0; i < otherData.size(); i++) + { + if (otherData.valueAt(i) == count) + { + modal.append((char) otherData.keyAt(i)); + } + } + } + return modal.toString(); + } + + /** + * Returns the highest count for any symbol(s) in the profile (excluding gap) + * + * @return + */ + public int getModalCount() + { + return maxCount; + } + + /** + * Returns the number of distinct symbols with a non-zero count (excluding the + * gap symbol) + * + * @return + */ + public int size() { + int size = 0; + if (useIntCounts) + { + for (int i = 1; i < intCounts.length; i++) + { + if (intCounts[i] > 0) + { + size++; + } + } + } + else + { + for (int i = 1; i < counts.length; i++) + { + if (counts[i] > 0) + { + size++; + } + } + } + + /* + * include 'other' characters recorded (even if count is zero + * though that would be a strange use case) + */ + if (otherData != null) + { + size += otherData.size(); + } + + return size; + } + + /** + * Returns a data bean holding those symbols that have a non-zero count + * (excluding the gap symbol), with their counts. + * + * @return + */ + public SymbolCounts getSymbolCounts() + { + int size = size(); + char[] symbols = new char[size]; + int[] values = new int[size]; + int j = 0; + + if (useIntCounts) + { + for (int i = 1; i < intCounts.length; i++) + { + if (intCounts[i] > 0) + { + char symbol = isNucleotide ? NUCS.charAt(i - 1) : AAS + .charAt(i - 1); + symbols[j] = symbol; + values[j] = intCounts[i]; + j++; + } + } + } + else + { + for (int i = 1; i < counts.length; i++) + { + if (counts[i] > 0) + { + char symbol = isNucleotide ? NUCS.charAt(i - 1) : AAS + .charAt(i - 1); + symbols[j] = symbol; + values[j] = counts[i]; + j++; + } + } + } + if (otherData != null) + { + for (int i = 0; i < otherData.size(); i++) + { + symbols[j] = (char) otherData.keyAt(i); + values[j] = otherData.valueAt(i); + j++; + } + } + + return new SymbolCounts(symbols, values); + } + + /** + * Returns a tooltip string showing residues in descending order of their + * percentage frequency in the profile + * + * @param normaliseBy + * the divisor for residue counts (may or may not include gapped + * sequence count) + * @param percentageDecPl + * the number of decimal places to show in percentages + * @return + */ + public String getTooltip(int normaliseBy, int percentageDecPl) + { + SymbolCounts symbolCounts = getSymbolCounts(); + char[] ca = symbolCounts.symbols; + int[] vl = symbolCounts.values; + + /* + * sort characters into ascending order of their counts + */ + QuickSort.sort(vl, ca); + + /* + * traverse in reverse order (highest count first) to build tooltip + */ + boolean first = true; + StringBuilder sb = new StringBuilder(64); + for (int c = ca.length - 1; c >= 0; c--) + { + final char residue = ca[c]; + // TODO combine residues which share a percentage + // (see AAFrequency.completeCdnaConsensus) + float tval = (vl[c] * 100f) / normaliseBy; + sb.append(first ? "" : "; ").append(residue).append(" "); + Format.appendPercentage(sb, tval, percentageDecPl); + sb.append("%"); + first = false; + } + return sb.toString(); + } + + /** + * Returns a string representation of the symbol counts, for debug purposes. + */ + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("[ "); + SymbolCounts sc = getSymbolCounts(); + for (int i = 0; i < sc.symbols.length; i++) + { + sb.append(sc.symbols[i]).append(":").append(sc.values[i]).append(" "); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/jalview/api/AlignViewportI.java b/src/jalview/api/AlignViewportI.java index bd7d53d..70463e7 100644 --- a/src/jalview/api/AlignViewportI.java +++ b/src/jalview/api/AlignViewportI.java @@ -21,6 +21,7 @@ package jalview.api; import jalview.analysis.Conservation; +import jalview.analysis.Profile; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.AlignmentView; @@ -81,7 +82,7 @@ public interface AlignViewportI extends ViewStyleI ColumnSelection getColumnSelection(); - Hashtable[] getSequenceConsensusHash(); + Profile[] getSequenceConsensusHash(); /** * Get consensus data table for the cDNA complement of this alignment (if any) @@ -144,7 +145,7 @@ public interface AlignViewportI extends ViewStyleI * * @param hconsensus */ - void setSequenceConsensusHash(Hashtable[] hconsensus); + void setSequenceConsensusHash(Profile[] hconsensus); /** * Set the cDNA complement consensus for the viewport diff --git a/src/jalview/datamodel/SequenceGroup.java b/src/jalview/datamodel/SequenceGroup.java index 9a408e3..98fd8f2 100755 --- a/src/jalview/datamodel/SequenceGroup.java +++ b/src/jalview/datamodel/SequenceGroup.java @@ -22,14 +22,13 @@ package jalview.datamodel; import jalview.analysis.AAFrequency; import jalview.analysis.Conservation; +import jalview.analysis.Profile; import jalview.schemes.ColourSchemeI; import java.awt.Color; import java.util.ArrayList; -import java.util.Hashtable; import java.util.List; import java.util.Map; -import java.util.Vector; /** * Collects a set contiguous ranges on a set of sequences @@ -45,8 +44,6 @@ public class SequenceGroup implements AnnotatedCollectionI Conservation conserve; - Vector aaFrequency; - boolean displayBoxes = true; boolean displayText = true; @@ -531,7 +528,7 @@ public class SequenceGroup implements AnnotatedCollectionI boolean upd = false; try { - Hashtable cnsns[] = AAFrequency.calculate(sequences, startRes, + Profile[] cnsns = AAFrequency.calculate(sequences, startRes, endRes + 1, showSequenceLogo); if (consensus != null) { @@ -603,9 +600,9 @@ public class SequenceGroup implements AnnotatedCollectionI c.completeAnnotations(conservation, null, startRes, endRes + 1); } - public Hashtable[] consensusData = null; + public Profile[] consensusData = null; - private void _updateConsensusRow(Hashtable[] cnsns, long nseq) + private void _updateConsensusRow(Profile[] cnsns, long nseq) { if (consensus == null) { diff --git a/src/jalview/ext/android/ContainerHelpers.java b/src/jalview/ext/android/ContainerHelpers.java new file mode 100644 index 0000000..f536819 --- /dev/null +++ b/src/jalview/ext/android/ContainerHelpers.java @@ -0,0 +1,102 @@ +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 + } + + // This is Arrays.binarySearch(), but doesn't do any argument validation. + static int binarySearch(short[] array, int size, short 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 + } +} diff --git a/src/jalview/ext/android/SparseIntArray.java b/src/jalview/ext/android/SparseIntArray.java new file mode 100644 index 0000000..2b9c4af --- /dev/null +++ b/src/jalview/ext/android/SparseIntArray.java @@ -0,0 +1,425 @@ +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. + * + *

+ * 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). + *

+ */ +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 0 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 0...size()-1, returns the key from + * the indexth key-value mapping that this SparseIntArray 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 SparseIntArray + * 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 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} + * + *

+ * 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 + * @return the new value of the count for the key + * @throw ArithmeticException if the result would exceed the maximum value of + * an int + */ + public int add(int key, int toAdd) + { + int newValue = toAdd; + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + if (i >= 0) + { + checkOverflow(mValues[i], toAdd); + mValues[i] += toAdd; + newValue = mValues[i]; + } + else + { + i = ~i; + if (mSize >= mKeys.length) + { + int n = idealIntArraySize(mSize + 1); + int[] nkeys = new int[n]; + int[] nvalues = new int[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; + } + + /** + * Throws ArithmeticException if adding addend to value would exceed the range + * of int + * + * @param value + * @param addend + */ + static void checkOverflow(int value, int addend) + { + /* + * test cases being careful to avoid overflow while testing! + */ + if (addend > 0) + { + if (value > 0 && Integer.MAX_VALUE - value < addend) + { + throw new ArithmeticException("Integer overflow adding " + addend + + " to " + value); + } + } + else if (addend < 0) + { + if (value < 0 && Integer.MIN_VALUE - value > addend) + { + throw new ArithmeticException("Integer underflow adding " + addend + + " to " + value); + } + } + } +} diff --git a/src/jalview/ext/android/SparseShortArray.java b/src/jalview/ext/android/SparseShortArray.java new file mode 100644 index 0000000..375d745 --- /dev/null +++ b/src/jalview/ext/android/SparseShortArray.java @@ -0,0 +1,439 @@ +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. + */ +/** + * SparseShortArrays map shorts to shorts. Unlike a normal array of shorts, + * there can be gaps in the indices. It is intended to be more memory efficient + * than using a HashMap to map Shorts to Shorts, both because it avoids + * auto-boxing keys and values and its data structure doesn't rely on an extra + * entry object for each mapping. + * + *

+ * 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). + *

+ */ +/** + * A copy of SparseShortArray designed to store short values (to minimise space + * usage). + *

+ * Note that operations append, put, add throw ArithmeticException if the + * resulting value overflows the range of a short. + */ +public class SparseShortArray implements Cloneable +{ + private short[] mKeys; + + private short[] mValues; + + private int mSize; + + /** + * Creates a new SparseShortArray containing no mappings. + */ + public SparseShortArray() + { + this(10); + } + + /** + * Creates a new SparseShortArray 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 SparseShortArray(int initialCapacity) + { + if (initialCapacity == 0) + { + mKeys = new short[0]; + mValues = new short[0]; + } + else + { + initialCapacity = idealShortArraySize(initialCapacity); + mKeys = new short[initialCapacity]; + mValues = new short[initialCapacity]; + } + mSize = 0; + } + + @Override + public SparseShortArray clone() + { + SparseShortArray clone = null; + try + { + clone = (SparseShortArray) 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 0 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. + * + * @throws ArithmeticException + * if key is outside the range of a short value + */ + public int get(int key, int valueIfKeyNotFound) + { + checkOverflow(key); + int i = ContainerHelpers.binarySearch(mKeys, mSize, (short) key); + if (i < 0) + { + return valueIfKeyNotFound; + } + else + { + return mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + * + * @throws ArithmeticException + * if key is outside the range of a short value + */ + public void delete(int key) + { + checkOverflow(key); + int i = ContainerHelpers.binarySearch(mKeys, mSize, (short) 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. + * + * @throws ArithmeticException + * if either argument is outside the range of a short value + */ + public void put(int key, int value) + { + checkOverflow(key); + checkOverflow(value); + int i = ContainerHelpers.binarySearch(mKeys, mSize, (short) key); + if (i >= 0) + { + mValues[i] = (short) value; + } + else + { + i = ~i; + if (mSize >= mKeys.length) + { + int n = idealShortArraySize(mSize + 1); + short[] nkeys = new short[n]; + short[] nvalues = new short[n]; + // Log.e("SparseShortArray", "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("SparseShortArray", "move " + (mSize - i)); + System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); + System.arraycopy(mValues, i, mValues, i + 1, mSize - i); + } + mKeys[i] = (short) key; + mValues[i] = (short) value; + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseShortArray + * 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 SparseShortArray + * 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 short 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 SparseShortArray + * 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 short 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. + * + * @throws ArithmeticException + * if key is outside the range of a short value + */ + public int indexOfKey(int key) + { + checkOverflow(key); + return ContainerHelpers.binarySearch(mKeys, mSize, (short) 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 SparseShortArray. + */ + 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 = idealShortArraySize(pos + 1); + short[] nkeys = new short[n]; + short[] nvalues = new short[n]; + // Log.e("SparseShortArray", "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; + } + checkOverflow(key); + checkOverflow(value); + mKeys[pos] = (short) key; + mValues[pos] = (short) value; + mSize = pos + 1; + } + + /** + * Throws an exception if the value is outside the range of a short. + * + * @param value + * @throws ArithmeticException + */ + public static void checkOverflow(int value) + { + if (value > Short.MAX_VALUE || value < Short.MIN_VALUE) + { + throw new ArithmeticException(String.valueOf(value)); + } + } + + /** + * Inlined here by copying from com.android.internal.util.ArrayUtils + * + * @param i + * @return + */ + public static int idealShortArraySize(int need) + { + return idealByteArraySize(need * 2) / 2; + } + + /** + * 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('='); + 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 + * @return the new value of the count for the key + * @throws ArithmeticException + * if key, or result of adding toAdd, is outside the range of a + * short value + */ + public int add(int key, int toAdd) + { + int newValue = toAdd; + checkOverflow(key); + int i = ContainerHelpers.binarySearch(mKeys, mSize, (short) key); + if (i >= 0) + { + checkOverflow(toAdd + mValues[i]); + mValues[i] += (short) toAdd; + newValue = mValues[i]; + } + else + { + checkOverflow(toAdd); + i = ~i; + if (mSize >= mKeys.length) + { + int n = idealShortArraySize(mSize + 1); + short[] nkeys = new short[n]; + short[] nvalues = new short[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] = (short) key; + mValues[i] = (short) toAdd; + mSize++; + } + return newValue; + } +} diff --git a/src/jalview/renderer/AnnotationRenderer.java b/src/jalview/renderer/AnnotationRenderer.java index 3fdcb3b..ed758c5 100644 --- a/src/jalview/renderer/AnnotationRenderer.java +++ b/src/jalview/renderer/AnnotationRenderer.java @@ -22,6 +22,7 @@ package jalview.renderer; import jalview.analysis.AAFrequency; import jalview.analysis.CodingUtils; +import jalview.analysis.Profile; import jalview.analysis.Rna; import jalview.analysis.StructureFrequency; import jalview.api.AlignViewportI; @@ -73,7 +74,7 @@ public class AnnotationRenderer private ColumnSelection columnSelection; - private Hashtable[] hconsensus; + private Profile[] hconsensus; private Hashtable[] complementConsensus; diff --git a/src/jalview/schemes/Blosum62ColourScheme.java b/src/jalview/schemes/Blosum62ColourScheme.java index 9d09259..37c31f9 100755 --- a/src/jalview/schemes/Blosum62ColourScheme.java +++ b/src/jalview/schemes/Blosum62ColourScheme.java @@ -20,16 +20,19 @@ */ package jalview.schemes; -import jalview.analysis.AAFrequency; import jalview.datamodel.AnnotatedCollectionI; import jalview.datamodel.SequenceCollectionI; import jalview.datamodel.SequenceI; +import jalview.util.Comparison; import java.awt.Color; import java.util.Map; public class Blosum62ColourScheme extends ResidueColourScheme { + private static final Color LIGHT_BLUE = new Color(204, 204, 255); + private static final Color DARK_BLUE = new Color(154, 154, 255); + public Blosum62ColourScheme() { super(); @@ -52,14 +55,16 @@ public class Blosum62ColourScheme extends ResidueColourScheme Color currentColour; - if (!jalview.util.Comparison.isGap(res)) + if (!Comparison.isGap(res)) { - String max = (String) consensus[j].get(AAFrequency.MAXRESIDUE); + /* + * test if this is the consensus (or joint consensus) residue + */ + String max = consensus[j].getModalResidue(); if (max.indexOf(res) > -1) { - // TODO use a constant here? - currentColour = new Color(154, 154, 255); + currentColour = DARK_BLUE; } else { @@ -74,8 +79,7 @@ public class Blosum62ColourScheme extends ResidueColourScheme if (c > 0) { - // TODO use a constant here? - currentColour = new Color(204, 204, 255); + currentColour = LIGHT_BLUE; } else { diff --git a/src/jalview/schemes/ColourSchemeI.java b/src/jalview/schemes/ColourSchemeI.java index effdf59..165ece9 100755 --- a/src/jalview/schemes/ColourSchemeI.java +++ b/src/jalview/schemes/ColourSchemeI.java @@ -20,6 +20,7 @@ */ package jalview.schemes; +import jalview.analysis.Profile; import jalview.datamodel.AnnotatedCollectionI; import jalview.datamodel.SequenceCollectionI; import jalview.datamodel.SequenceI; @@ -52,7 +53,7 @@ public interface ColourSchemeI /** * assign the given consensus profile for the colourscheme */ - public void setConsensus(java.util.Hashtable[] h); + public void setConsensus(Profile[] hconsensus); /** * assign the given conservation to the colourscheme diff --git a/src/jalview/schemes/FollowerColourScheme.java b/src/jalview/schemes/FollowerColourScheme.java index eac467a..0dcf960 100644 --- a/src/jalview/schemes/FollowerColourScheme.java +++ b/src/jalview/schemes/FollowerColourScheme.java @@ -21,8 +21,7 @@ package jalview.schemes; import jalview.analysis.Conservation; - -import java.util.Hashtable; +import jalview.analysis.Profile; /** * Colourscheme that takes its colours from some other colourscheme @@ -41,7 +40,7 @@ public class FollowerColourScheme extends ResidueColourScheme } @Override - public void setConsensus(Hashtable[] consensus) + public void setConsensus(Profile[] consensus) { if (colourScheme != null) { diff --git a/src/jalview/schemes/PIDColourScheme.java b/src/jalview/schemes/PIDColourScheme.java index 9dd763d..ccc69c2 100755 --- a/src/jalview/schemes/PIDColourScheme.java +++ b/src/jalview/schemes/PIDColourScheme.java @@ -20,9 +20,9 @@ */ package jalview.schemes; -import jalview.analysis.AAFrequency; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; +import jalview.util.Comparison; import java.awt.Color; @@ -67,20 +67,22 @@ public class PIDColourScheme extends ResidueColourScheme return Color.white; } - if ((Integer - .parseInt(consensus[j].get(AAFrequency.MAXCOUNT).toString()) != -1) - && consensus[j].contains(String.valueOf(c))) + /* + * test whether this is the consensus (or joint consensus) residue + */ + boolean matchesConsensus = consensus[j].getModalResidue().contains( + String.valueOf(c)); + if (matchesConsensus) { - sc = ((Float) consensus[j].get(ignoreGaps)).floatValue(); + sc = consensus[j].getPercentageIdentity(ignoreGaps); - if (!jalview.util.Comparison.isGap(c)) + if (!Comparison.isGap(c)) { for (int i = 0; i < thresholds.length; i++) { if (sc > thresholds[i]) { currentColour = pidColours[i]; - break; } } diff --git a/src/jalview/schemes/ResidueColourScheme.java b/src/jalview/schemes/ResidueColourScheme.java index bca98cf..56c573c 100755 --- a/src/jalview/schemes/ResidueColourScheme.java +++ b/src/jalview/schemes/ResidueColourScheme.java @@ -20,15 +20,15 @@ */ package jalview.schemes; -import jalview.analysis.AAFrequency; import jalview.analysis.Conservation; +import jalview.analysis.Profile; import jalview.datamodel.AnnotatedCollectionI; import jalview.datamodel.SequenceCollectionI; import jalview.datamodel.SequenceI; +import jalview.util.Comparison; import jalview.util.MessageManager; import java.awt.Color; -import java.util.Hashtable; import java.util.Map; /** @@ -48,17 +48,21 @@ public class ResidueColourScheme implements ColourSchemeI int threshold = 0; /* Set when threshold colouring to either pid_gaps or pid_nogaps */ - protected String ignoreGaps = AAFrequency.PID_GAPS; + protected boolean ignoreGaps = false; - /** Consenus as a hashtable array */ - Hashtable[] consensus; + /* + * Consensus data indexed by column + */ + Profile[] consensus; - /** Conservation string as a char array */ + /* + * Conservation string as a char array + */ char[] conservation; - int conservationLength = 0; - - /** DOCUMENT ME!! */ + /* + * The conservation slider percentage setting + */ int inc = 30; /** @@ -100,6 +104,7 @@ public class ResidueColourScheme implements ColourSchemeI /** * Find a colour without an index in a sequence */ + @Override public Color findColour(char c) { return colors == null ? Color.white : colors[symbolIndex[c]]; @@ -133,58 +138,62 @@ public class ResidueColourScheme implements ColourSchemeI * * @return Returns the percentage threshold */ + @Override public int getThreshold() { return threshold; } /** - * DOCUMENT ME! + * Sets the percentage consensus threshold value, and whether gaps are ignored + * in percentage identity calculation * - * @param ct - * DOCUMENT ME! + * @param consensusThreshold + * @param ignoreGaps */ - public void setThreshold(int ct, boolean ignoreGaps) + @Override + public void setThreshold(int consensusThreshold, boolean ignoreGaps) { - threshold = ct; - if (ignoreGaps) - { - this.ignoreGaps = AAFrequency.PID_NOGAPS; - } - else - { - this.ignoreGaps = AAFrequency.PID_GAPS; - } + threshold = consensusThreshold; + this.ignoreGaps = ignoreGaps; } /** - * DOCUMENT ME! + * Answers true if there is a consensus profile for the specified column, and + * the given residue matches the consensus (or joint consensus) residue for + * the column, and the percentage identity for the profile is equal to or + * greater than the current threshold; else answers false. The percentage + * calculation depends on whether or not we are ignoring gapped sequences. * - * @param s - * DOCUMENT ME! - * @param j - * DOCUMENT ME! + * @param residue + * @param column + * (index into consensus profiles) * - * @return DOCUMENT ME! + * @return + * @see #setThreshold(int, boolean) */ - public boolean aboveThreshold(char c, int j) + public boolean aboveThreshold(char residue, int column) { - if ('a' <= c && c <= 'z') + if ('a' <= residue && residue <= 'z') { // TO UPPERCASE !!! // Faster than toUpperCase - c -= ('a' - 'A'); + residue -= ('a' - 'A'); } - if (consensus == null || consensus.length < j || consensus[j] == null) + if (consensus == null || consensus.length < column + || consensus[column] == null) { return false; } - if ((((Integer) consensus[j].get(AAFrequency.MAXCOUNT)).intValue() != -1) - && consensus[j].contains(String.valueOf(c))) + /* + * test whether this is the consensus (or joint consensus) residue + */ + if (consensus[column].getModalResidue().contains( + String.valueOf(residue))) { - if (((Float) consensus[j].get(ignoreGaps)).floatValue() >= threshold) + if (consensus[column].getPercentageIdentity(ignoreGaps) >= threshold) { return true; } @@ -193,6 +202,7 @@ public class ResidueColourScheme implements ColourSchemeI return false; } + @Override public boolean conservationApplied() { return conservationColouring; @@ -204,11 +214,13 @@ public class ResidueColourScheme implements ColourSchemeI conservationColouring = conservationApplied; } + @Override public void setConservationInc(int i) { inc = i; } + @Override public int getConservationInc() { return inc; @@ -220,7 +232,8 @@ public class ResidueColourScheme implements ColourSchemeI * @param consensus * DOCUMENT ME! */ - public void setConsensus(Hashtable[] consensus) + @Override + public void setConsensus(Profile[] consensus) { if (consensus == null) { @@ -230,6 +243,7 @@ public class ResidueColourScheme implements ColourSchemeI this.consensus = consensus; } + @Override public void setConservation(Conservation cons) { if (cons == null) @@ -240,71 +254,88 @@ public class ResidueColourScheme implements ColourSchemeI else { conservationColouring = true; - int i, iSize = cons.getConsSequence().getLength(); + int iSize = cons.getConsSequence().getLength(); conservation = new char[iSize]; - for (i = 0; i < iSize; i++) + for (int i = 0; i < iSize; i++) { conservation[i] = cons.getConsSequence().getCharAt(i); } - conservationLength = conservation.length; } } /** - * DOCUMENT ME! + * Applies a combination of column conservation score, and conservation + * percentage slider, to 'bleach' out the residue colours towards white. + *

+ * If a column is fully conserved (identical residues, conservation score 11, + * shown as *), or all 10 physico-chemical properties are conserved + * (conservation score 10, shown as +), then the colour is left unchanged. + *

+ * Otherwise a 'bleaching' factor is computed and applied to the colour. This + * is designed to fade colours for scores of 0-9 completely to white at slider + * positions ranging from 18% - 100% respectively. * - * @param s - * DOCUMENT ME! - * @param i - * DOCUMENT ME! + * @param currentColour + * @param column * - * @return DOCUMENT ME! + * @return bleached (or unmodified) colour */ - - Color applyConservation(Color currentColour, int i) + Color applyConservation(Color currentColour, int column) { + if (conservation == null || conservation.length <= column) + { + return currentColour; + } + char conservationScore = conservation[column]; + + /* + * if residues are fully conserved (* or 11), or all properties + * are conserved (+ or 10), leave colour unchanged + */ + if (conservationScore == '*' || conservationScore == '+' + || conservationScore == (char) 10 + || conservationScore == (char) 11) + { + return currentColour; + } - if ((conservationLength > i) && (conservation[i] != '*') - && (conservation[i] != '+')) + if (Comparison.isGap(conservationScore)) { - if (jalview.util.Comparison.isGap(conservation[i])) - { - currentColour = Color.white; - } - else - { - float t = 11 - (conservation[i] - '0'); - if (t == 0) - { - return Color.white; - } - - int red = currentColour.getRed(); - int green = currentColour.getGreen(); - int blue = currentColour.getBlue(); - - int dr = 255 - red; - int dg = 255 - green; - int db = 255 - blue; - - dr *= t / 10f; - dg *= t / 10f; - db *= t / 10f; - - red += (inc / 20f) * dr; - green += (inc / 20f) * dg; - blue += (inc / 20f) * db; - - if (red > 255 || green > 255 || blue > 255) - { - currentColour = Color.white; - } - else - { - currentColour = new Color(red, green, blue); - } - } + return Color.white; + } + + /* + * convert score 0-9 to a bleaching factor 1.1 - 0.2 + */ + float bleachFactor = (11 - (conservationScore - '0')) / 10f; + + /* + * scale this by the percentage slider / 20 + */ + bleachFactor *= (inc / 20f); + + int red = currentColour.getRed(); + int green = currentColour.getGreen(); + int blue = currentColour.getBlue(); + + /* + * bleach colours towards white (255, 255, 255), + * depending on the consensus score and the conservation slider value + * scores of: 0 1 2 3 4 5 6 7 8 9 + * fade to white at slider value: 18 20 22 25 29 33 40 50 67 100% + */ + red += (255 - red) * bleachFactor; + green += (255 - green) * bleachFactor; + blue += (255 - blue) * bleachFactor; + + if (red > 255 || green > 255 || blue > 255) + { + currentColour = Color.white; + } + else + { + currentColour = new Color(red, green, blue); } return currentColour; } diff --git a/src/jalview/util/Format.java b/src/jalview/util/Format.java index d14e4ad..7121985 100755 --- a/src/jalview/util/Format.java +++ b/src/jalview/util/Format.java @@ -947,4 +947,29 @@ public class Format { return formatString; } + + /** + * Bespoke method to format percentage float value to the specified number of + * decimal places. Avoids use of general-purpose format parsers as a + * processing hotspot. + * + * @param sb + * @param value + * @param dp + */ + public static void appendPercentage(StringBuilder sb, float value, int dp) + { + sb.append((int) value); + if (dp > 0) + { + sb.append("."); + while (dp > 0) + { + value = value - (int) value; + value *= 10; + sb.append((int) value); + dp--; + } + } + } } diff --git a/src/jalview/util/SparseCount.java b/src/jalview/util/SparseCount.java new file mode 100644 index 0000000..e6b45f2 --- /dev/null +++ b/src/jalview/util/SparseCount.java @@ -0,0 +1,154 @@ +package jalview.util; + +import jalview.ext.android.SparseIntArray; +import jalview.ext.android.SparseShortArray; + +/** + * A class to count occurrences of characters with minimal memory footprint. + * Sparse arrays of short values are used to hold the counts, with automatic + * promotion to arrays of int if any count exceeds the maximum value for a + * short. + * + * @author gmcarstairs + * + */ +public class SparseCount +{ + private static final int DEFAULT_PROFILE_SIZE = 2; + + /* + * array of keys (chars) and values (counts) + * held either as shorts or (if shorts overflow) as ints + */ + private SparseShortArray shortProfile; + + private SparseIntArray intProfile; + + /* + * flag is set true after short overflow occurs + */ + private boolean useInts; + + /** + * Constructor which initially creates a new sparse array of short values to + * hold counts. + * + * @param profileSize + */ + public SparseCount(int profileSize) + { + this.shortProfile = new SparseShortArray(profileSize); + } + + /** + * Constructor which allocates an initial count array for only two distinct + * values (the array will grow if needed) + */ + public SparseCount() + { + this(DEFAULT_PROFILE_SIZE); + } + + /** + * Adds the given value for the given key (or sets the initial value), and + * returns the new value + * + * @param key + * @param value + */ + public int add(int key, int value) + { + int newValue = 0; + if (useInts) + { + newValue = intProfile.add(key, value); + } + else + { + try { + newValue = shortProfile.add(key, value); + } catch (ArithmeticException e) { + handleOverflow(); + newValue = intProfile.add(key, value); + } + } + return newValue; + } + + /** + * Switch from counting shorts to counting ints + */ + synchronized void handleOverflow() + { + int size = shortProfile.size(); + intProfile = new SparseIntArray(size); + for (int i = 0; i < size; i++) + { + short key = shortProfile.keyAt(i); + short value = shortProfile.valueAt(i); + intProfile.put(key, value); + } + shortProfile = null; + useInts = true; + } + + /** + * Returns the size of the profile (number of distinct items counted) + * + * @return + */ + public int size() + { + return useInts ? intProfile.size() : shortProfile.size(); + } + + /** + * Returns the value for the key (zero if no such key) + * + * @param key + * @return + */ + public int get(int key) + { + return useInts ? intProfile.get(key) : shortProfile.get(key); + } + + /** + * Sets the value for the given key + * + * @param key + * @param value + */ + public void put(int key, int value) + { + if (useInts) + { + intProfile.put(key, value); + } + else + { + shortProfile.put(key, value); + } + } + + public int keyAt(int k) + { + return useInts ? intProfile.keyAt(k) : shortProfile.keyAt(k); + } + + public int valueAt(int k) + { + return useInts ? intProfile.valueAt(k) : shortProfile.valueAt(k); + } + + /** + * Answers true if this object wraps arrays of int values, false if using + * short values + * + * @return + */ + boolean isUsingInt() + { + return useInts; + } +} diff --git a/src/jalview/viewmodel/AlignmentViewport.java b/src/jalview/viewmodel/AlignmentViewport.java index c1c88c1..c01be4e 100644 --- a/src/jalview/viewmodel/AlignmentViewport.java +++ b/src/jalview/viewmodel/AlignmentViewport.java @@ -22,6 +22,7 @@ package jalview.viewmodel; import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder; import jalview.analysis.Conservation; +import jalview.analysis.Profile; import jalview.api.AlignCalcManagerI; import jalview.api.AlignViewportI; import jalview.api.AlignmentViewPanel; @@ -700,7 +701,7 @@ public abstract class AlignmentViewport implements AlignViewportI, /** * results of alignment consensus analysis for visible portion of view */ - protected Hashtable[] hconsensus = null; + protected Profile[] hconsensus = null; /** * results of cDNA complement consensus visible portion of view @@ -734,7 +735,7 @@ public abstract class AlignmentViewport implements AlignViewportI, } @Override - public void setSequenceConsensusHash(Hashtable[] hconsensus) + public void setSequenceConsensusHash(Profile[] hconsensus) { this.hconsensus = hconsensus; } @@ -746,7 +747,7 @@ public abstract class AlignmentViewport implements AlignViewportI, } @Override - public Hashtable[] getSequenceConsensusHash() + public Profile[] getSequenceConsensusHash() { return hconsensus; } @@ -939,7 +940,9 @@ public abstract class AlignmentViewport implements AlignViewportI, groupConservation = null; hconsensus = null; hcomplementConsensus = null; - // TODO removed listeners from changeSupport? + // colour scheme may hold reference to consensus + globalColourScheme = null; + // TODO remove listeners from changeSupport? changeSupport = null; setAlignment(null); } diff --git a/src/jalview/workers/ComplementConsensusThread.java b/src/jalview/workers/ComplementConsensusThread.java index 529df6f..431fbec 100644 --- a/src/jalview/workers/ComplementConsensusThread.java +++ b/src/jalview/workers/ComplementConsensusThread.java @@ -96,7 +96,6 @@ public class ComplementConsensusThread extends ConsensusThread * @param consensusData * the computed consensus data */ - @Override protected void deriveConsensus(AlignmentAnnotation consensusAnnotation, Hashtable[] consensusData) { @@ -104,4 +103,16 @@ public class ComplementConsensusThread extends ConsensusThread alignViewport.isShowSequenceLogo(), getSequences().length); } + @Override + public void updateResultAnnotation(boolean immediate) + { + AlignmentAnnotation consensus = getConsensusAnnotation(); + Hashtable[] hconsensus = getViewportConsensus(); + if (immediate || !calcMan.isWorking(this) && consensus != null + && hconsensus != null) + { + deriveConsensus(consensus, hconsensus); + } + } + } diff --git a/src/jalview/workers/ConsensusThread.java b/src/jalview/workers/ConsensusThread.java index 5f0ec84..c88a24a 100644 --- a/src/jalview/workers/ConsensusThread.java +++ b/src/jalview/workers/ConsensusThread.java @@ -21,6 +21,7 @@ package jalview.workers; import jalview.analysis.AAFrequency; +import jalview.analysis.Profile; import jalview.api.AlignViewportI; import jalview.api.AlignmentViewPanel; import jalview.datamodel.AlignmentAnnotation; @@ -29,8 +30,6 @@ import jalview.datamodel.Annotation; import jalview.datamodel.SequenceI; import jalview.schemes.ColourSchemeI; -import java.util.Hashtable; - public class ConsensusThread extends AlignCalcWorker { public ConsensusThread(AlignViewportI alignViewport, @@ -125,7 +124,7 @@ public class ConsensusThread extends AlignCalcWorker */ protected void computeConsensus(AlignmentI alignment) { - Hashtable[] hconsensus = new Hashtable[alignment.getWidth()]; + Profile[] hconsensus = new Profile[alignment.getWidth()]; SequenceI[] aseqs = getSequences(); AAFrequency.calculate(aseqs, 0, alignment.getWidth(), hconsensus, true); @@ -145,7 +144,7 @@ public class ConsensusThread extends AlignCalcWorker /** * @param hconsensus */ - protected void setColourSchemeConsensus(Hashtable[] hconsensus) + protected void setColourSchemeConsensus(Profile[] hconsensus) { ColourSchemeI globalColourScheme = alignViewport .getGlobalColourScheme(); @@ -178,7 +177,7 @@ public class ConsensusThread extends AlignCalcWorker public void updateResultAnnotation(boolean immediate) { AlignmentAnnotation consensus = getConsensusAnnotation(); - Hashtable[] hconsensus = getViewportConsensus(); + Profile[] hconsensus = (Profile[]) getViewportConsensus(); if (immediate || !calcMan.isWorking(this) && consensus != null && hconsensus != null) { @@ -192,15 +191,15 @@ public class ConsensusThread extends AlignCalcWorker * * @param consensusAnnotation * the annotation to be populated - * @param consensusData + * @param hconsensus * the computed consensus data */ protected void deriveConsensus(AlignmentAnnotation consensusAnnotation, - Hashtable[] consensusData) + Profile[] hconsensus) { long nseq = getSequences().length; - AAFrequency.completeConsensus(consensusAnnotation, consensusData, 0, - consensusData.length, alignViewport.isIgnoreGapsConsensus(), + AAFrequency.completeConsensus(consensusAnnotation, hconsensus, 0, + hconsensus.length, alignViewport.isIgnoreGapsConsensus(), alignViewport.isShowSequenceLogo(), nseq); } @@ -209,8 +208,9 @@ public class ConsensusThread extends AlignCalcWorker * * @return */ - protected Hashtable[] getViewportConsensus() + protected Object[] getViewportConsensus() { + // TODO convert ComplementConsensusThread to use Profile return alignViewport.getSequenceConsensusHash(); } } diff --git a/test/jalview/analysis/AAFrequencyTest.java b/test/jalview/analysis/AAFrequencyTest.java index eff6bbf..0ddbddc 100644 --- a/test/jalview/analysis/AAFrequencyTest.java +++ b/test/jalview/analysis/AAFrequencyTest.java @@ -23,65 +23,62 @@ package jalview.analysis; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertNull; +import jalview.datamodel.AlignmentAnnotation; +import jalview.datamodel.Annotation; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceI; -import java.util.Hashtable; - import org.testng.annotations.Test; public class AAFrequencyTest { - private static final String C = AAFrequency.MAXCOUNT; - - private static final String R = AAFrequency.MAXRESIDUE; - - private static final String G = AAFrequency.PID_GAPS; - - private static final String N = AAFrequency.PID_NOGAPS; - - private static final String P = AAFrequency.PROFILE; - @Test(groups = { "Functional" }) public void testCalculate_noProfile() { - SequenceI seq1 = new Sequence("Seq1", "CAGT"); - SequenceI seq2 = new Sequence("Seq2", "CACT"); - SequenceI seq3 = new Sequence("Seq3", "C--G"); - SequenceI seq4 = new Sequence("Seq4", "CA-t"); + SequenceI seq1 = new Sequence("Seq1", "CAG-T"); + SequenceI seq2 = new Sequence("Seq2", "CAC-T"); + SequenceI seq3 = new Sequence("Seq3", "C---G"); + SequenceI seq4 = new Sequence("Seq4", "CA--t"); SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 }; - Hashtable[] result = new Hashtable[seq1.getLength()]; + Profile[] result = new Profile[seq1.getLength()]; AAFrequency.calculate(seqs, 0, seq1.getLength(), result, false); // col 0 is 100% C - Hashtable col = result[0]; - assertEquals(100f, (Float) col.get(G), 0.0001f); - assertEquals(100f, (Float) col.get(N), 0.0001f); - assertEquals(4, col.get(C)); - assertEquals("C", col.get(R)); - assertNull(col.get(P)); + Profile col = result[0]; + assertEquals(100f, col.getPercentageIdentity(false)); + assertEquals(100f, col.getPercentageIdentity(true)); + assertEquals(4, col.getMaxCount()); + assertEquals("C", col.getModalResidue()); + assertNull(col.getCounts()); // col 1 is 75% A col = result[1]; - assertEquals(75f, (Float) col.get(G), 0.0001f); - assertEquals(100f, (Float) col.get(N), 0.0001f); - assertEquals(3, col.get(C)); - assertEquals("A", col.get(R)); + assertEquals(75f, col.getPercentageIdentity(false)); + assertEquals(100f, col.getPercentageIdentity(true)); + assertEquals(3, col.getMaxCount()); + assertEquals("A", col.getModalResidue()); // col 2 is 50% G 50% C or 25/25 counting gaps col = result[2]; - assertEquals(25f, (Float) col.get(G), 0.0001f); - assertEquals(50f, (Float) col.get(N), 0.0001f); - assertEquals(1, col.get(C)); - assertEquals("CG", col.get(R)); + assertEquals(25f, col.getPercentageIdentity(false)); + assertEquals(50f, col.getPercentageIdentity(true)); + assertEquals(1, col.getMaxCount()); + assertEquals("CG", col.getModalResidue()); - // col 3 is 75% T 25% G + // col 3 is all gaps col = result[3]; - assertEquals(75f, (Float) col.get(G), 0.0001f); - assertEquals(75f, (Float) col.get(N), 0.0001f); - assertEquals(3, col.get(C)); - assertEquals("T", col.get(R)); + assertEquals(0f, col.getPercentageIdentity(false)); + assertEquals(0f, col.getPercentageIdentity(true)); + assertEquals(0, col.getMaxCount()); + assertEquals("", col.getModalResidue()); + + // col 4 is 75% T 25% G + col = result[4]; + assertEquals(75f, col.getPercentageIdentity(false)); + assertEquals(75f, col.getPercentageIdentity(true)); + assertEquals(3, col.getMaxCount()); + assertEquals("T", col.getModalResidue()); } @Test(groups = { "Functional" }) @@ -92,33 +89,33 @@ public class AAFrequencyTest SequenceI seq3 = new Sequence("Seq3", "C--G"); SequenceI seq4 = new Sequence("Seq4", "CA-t"); SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 }; - Hashtable[] result = new Hashtable[seq1.getLength()]; + Profile[] result = new Profile[seq1.getLength()]; AAFrequency.calculate(seqs, 0, seq1.getLength(), result, true); - int[][] profile = (int[][]) result[0].get(P); - assertEquals(4, profile[0]['C']); - assertEquals(4, profile[1][0]); // no of seqs - assertEquals(4, profile[1][1]); // nongapped in column - - profile = (int[][]) result[1].get(P); - assertEquals(3, profile[0]['A']); - assertEquals(4, profile[1][0]); - assertEquals(3, profile[1][1]); - - profile = (int[][]) result[2].get(P); - assertEquals(1, profile[0]['G']); - assertEquals(1, profile[0]['C']); - assertEquals(4, profile[1][0]); - assertEquals(2, profile[1][1]); - - profile = (int[][]) result[3].get(P); - assertEquals(3, profile[0]['T']); - assertEquals(1, profile[0]['G']); - assertEquals(4, profile[1][0]); - assertEquals(4, profile[1][1]); + Profile profile = result[0]; + assertEquals(4, profile.getCounts().getCount('C')); + assertEquals(4, profile.getHeight()); + assertEquals(4, profile.getNonGapped()); + + profile = result[1]; + assertEquals(3, profile.getCounts().getCount('A')); + assertEquals(4, profile.getHeight()); + assertEquals(3, profile.getNonGapped()); + + profile = result[2]; + assertEquals(1, profile.getCounts().getCount('C')); + assertEquals(1, profile.getCounts().getCount('G')); + assertEquals(4, profile.getHeight()); + assertEquals(2, profile.getNonGapped()); + + profile = result[3]; + assertEquals(3, profile.getCounts().getCount('T')); + assertEquals(1, profile.getCounts().getCount('G')); + assertEquals(4, profile.getHeight()); + assertEquals(4, profile.getNonGapped()); } - @Test(groups = { "Functional" }) + @Test(groups = { "Functional" }, enabled = false) public void testCalculate_withProfileTiming() { SequenceI seq1 = new Sequence("Seq1", "CAGT"); @@ -126,7 +123,7 @@ public class AAFrequencyTest SequenceI seq3 = new Sequence("Seq3", "C--G"); SequenceI seq4 = new Sequence("Seq4", "CA-t"); SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 }; - Hashtable[] result = new Hashtable[seq1.getLength()]; + Profile[] result = new Profile[seq1.getLength()]; // ensure class loaded and initialized AAFrequency.calculate(seqs, 0, seq1.getLength(), result, true); @@ -139,14 +136,86 @@ public class AAFrequencyTest System.out.println(System.currentTimeMillis() - start); } + /** + * Test generation of consensus annotation with options 'include gaps' + * (profile percentages are of all sequences, whether gapped or not), and + * 'show logo' (the full profile with all residue percentages is reported in + * the description for the tooltip) + */ + @Test(groups = { "Functional" }) + public void testCompleteConsensus_includeGaps_showLogo() + { + /* + * first compute the profiles + */ + SequenceI seq1 = new Sequence("Seq1", "CAG-T"); + SequenceI seq2 = new Sequence("Seq2", "CAC-T"); + SequenceI seq3 = new Sequence("Seq3", "C---G"); + SequenceI seq4 = new Sequence("Seq4", "CA--t"); + SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 }; + Profile[] profiles = new Profile[seq1.getLength()]; + AAFrequency.calculate(seqs, 0, seq1.getLength(), profiles, true); + + AlignmentAnnotation consensus = new AlignmentAnnotation("Consensus", + "PID", new Annotation[seq1.getLength()]); + AAFrequency + .completeConsensus(consensus, profiles, 0, 5, false, true, 4); + + Annotation ann = consensus.annotations[0]; + assertEquals("C 100%", ann.description); + assertEquals("C", ann.displayCharacter); + ann = consensus.annotations[1]; + assertEquals("A 75%", ann.description); + assertEquals("A", ann.displayCharacter); + ann = consensus.annotations[2]; + assertEquals("C 25%; G 25%", ann.description); + assertEquals("+", ann.displayCharacter); + ann = consensus.annotations[3]; + assertEquals("", ann.description); + assertEquals("-", ann.displayCharacter); + ann = consensus.annotations[4]; + assertEquals("T 75%; G 25%", ann.description); + assertEquals("T", ann.displayCharacter); + } + + /** + * Test generation of consensus annotation with options 'ignore gaps' (profile + * percentages are of the non-gapped sequences) and 'no logo' (only the modal + * residue[s] percentage is reported in the description for the tooltip) + */ @Test(groups = { "Functional" }) - public void testGetPercentageFormat() + public void testCompleteConsensus_ignoreGaps_noLogo() { - assertNull(AAFrequency.getPercentageFormat(0)); - assertNull(AAFrequency.getPercentageFormat(99)); - assertEquals("%3.1f", AAFrequency.getPercentageFormat(100).toString()); - assertEquals("%3.1f", AAFrequency.getPercentageFormat(999).toString()); - assertEquals("%3.2f", AAFrequency.getPercentageFormat(1000).toString()); - assertEquals("%3.2f", AAFrequency.getPercentageFormat(9999).toString()); + /* + * first compute the profiles + */ + SequenceI seq1 = new Sequence("Seq1", "CAG-T"); + SequenceI seq2 = new Sequence("Seq2", "CAC-T"); + SequenceI seq3 = new Sequence("Seq3", "C---G"); + SequenceI seq4 = new Sequence("Seq4", "CA--t"); + SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 }; + Profile[] profiles = new Profile[seq1.getLength()]; + AAFrequency.calculate(seqs, 0, seq1.getLength(), profiles, true); + + AlignmentAnnotation consensus = new AlignmentAnnotation("Consensus", + "PID", new Annotation[seq1.getLength()]); + AAFrequency + .completeConsensus(consensus, profiles, 0, 5, true, false, 4); + + Annotation ann = consensus.annotations[0]; + assertEquals("C 100%", ann.description); + assertEquals("C", ann.displayCharacter); + ann = consensus.annotations[1]; + assertEquals("A 100%", ann.description); + assertEquals("A", ann.displayCharacter); + ann = consensus.annotations[2]; + assertEquals("[CG] 50%", ann.description); + assertEquals("+", ann.displayCharacter); + ann = consensus.annotations[3]; + assertEquals("", ann.description); + assertEquals("-", ann.displayCharacter); + ann = consensus.annotations[4]; + assertEquals("T 75%", ann.description); + assertEquals("T", ann.displayCharacter); } } diff --git a/test/jalview/analysis/ResidueCountTest.java b/test/jalview/analysis/ResidueCountTest.java new file mode 100644 index 0000000..4a71f89 --- /dev/null +++ b/test/jalview/analysis/ResidueCountTest.java @@ -0,0 +1,404 @@ +package jalview.analysis; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import jalview.analysis.ResidueCount.SymbolCounts; + +import org.junit.Assert; +import org.testng.annotations.Test; + +public class ResidueCountTest +{ + /** + * Test a mix of add and put for nucleotide counting + */ + @Test(groups = "Functional") + public void test_countNucleotide() + { + ResidueCount rc = new ResidueCount(true); + assertEquals(rc.getCount('A'), 0); + assertEquals(rc.getGapCount(), 0); + // add then add + assertEquals(rc.add('A'), 1); + assertEquals(rc.add('a'), 2); + // put then add + rc.put('g', 3); + assertEquals(rc.add('G'), 4); + // add then put + assertEquals(rc.add('c'), 1); + rc.put('C', 4); + assertEquals(rc.add('N'), 1); + + assertEquals(rc.getCount('a'), 2); + assertEquals(rc.getCount('A'), 2); + assertEquals(rc.getCount('G'), 4); + assertEquals(rc.getCount('c'), 4); + assertEquals(rc.getCount('T'), 0); // never seen + assertEquals(rc.getCount('N'), 1); + assertEquals(rc.getCount('?'), 0); + assertEquals(rc.getCount('-'), 0); + + assertFalse(rc.isCountingInts()); + assertFalse(rc.isUsingOtherData()); + } + + /** + * Test adding to gap count (either using addGap or add) + */ + @Test(groups = "Functional") + public void testAddGap() + { + ResidueCount rc = new ResidueCount(true); + rc.addGap(); + rc.add('-'); + rc.add('.'); + rc.add(' '); + + assertEquals(rc.getGapCount(), 4); + assertEquals(rc.getCount(' '), 4); + assertEquals(rc.getCount('-'), 4); + assertEquals(rc.getCount('.'), 4); + assertFalse(rc.isUsingOtherData()); + assertFalse(rc.isCountingInts()); + } + + @Test(groups = "Functional") + public void testOverflow() + { + /* + * overflow from add + */ + ResidueCount rc = new ResidueCount(true); + rc.addGap(); + rc.put('A', Short.MAX_VALUE - 1); + assertFalse(rc.isCountingInts()); + rc.add('A'); + assertFalse(rc.isCountingInts()); + rc.add('A'); + assertTrue(rc.isCountingInts()); + assertEquals(rc.getCount('a'), Short.MAX_VALUE + 1); + rc.add('A'); + assertTrue(rc.isCountingInts()); + assertEquals(rc.getCount('a'), Short.MAX_VALUE + 2); + assertEquals(rc.getGapCount(), 1); + rc.addGap(); + assertEquals(rc.getGapCount(), 2); + + /* + * overflow from put + */ + rc = new ResidueCount(true); + rc.put('G', Short.MAX_VALUE + 1); + assertTrue(rc.isCountingInts()); + assertEquals(rc.getCount('g'), Short.MAX_VALUE + 1); + rc.put('G', 1); + assertTrue(rc.isCountingInts()); + assertEquals(rc.getCount('g'), 1); + + /* + * underflow from put + */ + rc = new ResidueCount(true); + rc.put('G', Short.MIN_VALUE - 1); + assertTrue(rc.isCountingInts()); + assertEquals(rc.getCount('g'), Short.MIN_VALUE - 1); + } + + /** + * Test a mix of add and put for peptide counting + */ + @Test(groups = "Functional") + public void test_countPeptide() + { + ResidueCount rc = new ResidueCount(false); + rc.put('q', 4); + rc.add('Q'); + rc.add('X'); + rc.add('x'); + rc.add('W'); + rc.put('w', 7); + rc.put('m', 12); + rc.put('M', 13); + + assertEquals(rc.getCount('q'), 5); + assertEquals(rc.getCount('X'), 2); + assertEquals(rc.getCount('W'), 7); + assertEquals(rc.getCount('m'), 13); + assertEquals(rc.getCount('G'), 0); + assertEquals(rc.getCount('-'), 0); + + assertFalse(rc.isCountingInts()); + assertFalse(rc.isUsingOtherData()); + } + + @Test(groups = "Functional") + public void test_unexpectedPeptide() + { + ResidueCount rc = new ResidueCount(false); + // expected characters (upper or lower case): + String aas = "ACDEFGHIKLMNPQRSTVWXY"; + String lower = aas.toLowerCase(); + for (int i = 0; i < aas.length(); i++) + { + rc.put(aas.charAt(i), i); + rc.add(lower.charAt(i)); + } + for (int i = 0; i < aas.length(); i++) + { + assertEquals(rc.getCount(aas.charAt(i)), i + 1); + } + assertFalse(rc.isUsingOtherData()); + + rc.put('J', 4); + assertTrue(rc.isUsingOtherData()); + assertEquals(rc.getCount('J'), 4); + rc.add('j'); + assertEquals(rc.getCount('J'), 5); + } + + @Test(groups = "Functional") + public void test_unexpectedNucleotide() + { + ResidueCount rc = new ResidueCount(true); + // expected characters (upper or lower case): + String nucs = "ACGTUN"; + String lower = nucs.toLowerCase(); + for (int i = 0; i < nucs.length(); i++) + { + rc.put(nucs.charAt(i), i); + rc.add(lower.charAt(i)); + } + for (int i = 0; i < nucs.length(); i++) + { + assertEquals(rc.getCount(nucs.charAt(i)), i + 1); + } + assertFalse(rc.isUsingOtherData()); + + rc.add('J'); + assertTrue(rc.isUsingOtherData()); + } + + @Test(groups = "Functional") + public void testGetModalCount() + { + ResidueCount rc = new ResidueCount(true); + rc.add('c'); + rc.add('g'); + rc.add('c'); + assertEquals(rc.getModalCount(), 2); + + // modal count is in the 'short overflow' counts + rc = new ResidueCount(); + rc.add('c'); + rc.put('g', Short.MAX_VALUE); + rc.add('G'); + assertEquals(rc.getModalCount(), Short.MAX_VALUE + 1); + + // modal count is in the 'other data' counts + rc = new ResidueCount(false); + rc.add('Q'); + rc.add('{'); + rc.add('{'); + assertEquals(rc.getModalCount(), 2); + + // verify modal count excludes gap + rc = new ResidueCount(); + rc.add('Q'); + rc.add('P'); + rc.add('Q'); + rc.addGap(); + rc.addGap(); + rc.addGap(); + assertEquals(rc.getModalCount(), 2); + } + + @Test(groups = "Functional") + public void testGetResiduesForCount() + { + ResidueCount rc = new ResidueCount(true); + rc.add('c'); + rc.add('g'); + rc.add('c'); + assertEquals(rc.getResiduesForCount(2), "C"); + assertEquals(rc.getResiduesForCount(1), "G"); + assertEquals(rc.getResiduesForCount(3), ""); + assertEquals(rc.getResiduesForCount(0), ""); + assertEquals(rc.getResiduesForCount(-1), ""); + + // modal count is in the 'short overflow' counts + rc = new ResidueCount(true); + rc.add('c'); + rc.put('g', Short.MAX_VALUE); + rc.add('G'); + assertEquals(rc.getResiduesForCount(Short.MAX_VALUE + 1), "G"); + assertEquals(rc.getResiduesForCount(1), "C"); + + // peptide modal count is in the 'short overflow' counts + rc = new ResidueCount(false); + rc.add('c'); + rc.put('p', Short.MAX_VALUE); + rc.add('P'); + assertEquals(rc.getResiduesForCount(Short.MAX_VALUE + 1), "P"); + assertEquals(rc.getResiduesForCount(1), "C"); + + // modal count is in the 'other data' counts + rc = new ResidueCount(); + rc.add('Q'); + rc.add('{'); + rc.add('{'); + assertEquals(rc.getResiduesForCount(1), "Q"); + assertEquals(rc.getResiduesForCount(2), "{"); + + // residues share modal count + rc = new ResidueCount(); + rc.add('G'); + rc.add('G'); + rc.add('c'); + rc.add('C'); + rc.add('U'); + assertEquals(rc.getResiduesForCount(1), "U"); + assertEquals(rc.getResiduesForCount(2), "CG"); + + // expected and unexpected symbols share modal count + rc = new ResidueCount(); + rc.add('G'); + rc.add('t'); + rc.add('['); + rc.add('['); + rc.add('t'); + rc.add('G'); + rc.add('c'); + rc.add('C'); + rc.add('U'); + assertEquals(rc.getResiduesForCount(1), "U"); + assertEquals(rc.getResiduesForCount(2), "CGT["); + } + + @Test(groups = "Functional") + public void testGetSymbolCounts_nucleotide() + { + ResidueCount rc = new ResidueCount(true); + rc.add('g'); + rc.add('c'); + rc.add('G'); + rc.add('J'); // 'otherData' + rc.add('g'); + rc.add('N'); + rc.put('[', 0); // 'otherdata' + + SymbolCounts sc = rc.getSymbolCounts(); + Assert.assertArrayEquals(new char[] { 'C', 'G', 'N', 'J', '[' }, + sc.symbols); + Assert.assertArrayEquals(new int[] { 1, 3, 1, 1, 0 }, sc.values); + + // now with overflow to int counts + rc.put('U', Short.MAX_VALUE); + rc.add('u'); + sc = rc.getSymbolCounts(); + Assert.assertArrayEquals(new char[] { 'C', 'G', 'N', 'U', 'J', '[' }, + sc.symbols); + Assert.assertArrayEquals(new int[] { 1, 3, 1, 32768, 1, 0 }, sc.values); + } + + @Test(groups = "Functional") + public void testGetSymbolCounts_peptide() + { + ResidueCount rc = new ResidueCount(false); + rc.add('W'); + rc.add('q'); + rc.add('W'); + rc.add('Z'); // 'otherData' + rc.add('w'); + rc.add('L'); + + SymbolCounts sc = rc.getSymbolCounts(); + Assert.assertArrayEquals(new char[] { 'L', 'Q', 'W', 'Z' }, sc.symbols); + Assert.assertArrayEquals(new int[] { 1, 1, 3, 1 }, sc.values); + + // now with overflow to int counts + rc.put('W', Short.MAX_VALUE); + rc.add('W'); + sc = rc.getSymbolCounts(); + Assert.assertArrayEquals(new char[] { 'L', 'Q', 'W', 'Z' }, sc.symbols); + Assert.assertArrayEquals(new int[] { 1, 1, 32768, 1 }, sc.values); + } + + @Test(groups = "Functional") + public void testToString() + { + ResidueCount rc = new ResidueCount(); + rc.add('q'); + rc.add('c'); + rc.add('Q'); + assertEquals(rc.toString(), "[ C:1 Q:2 ]"); + + // add 'other data' + rc.add('{'); + assertEquals(rc.toString(), "[ C:1 Q:2 {:1 ]"); + + // switch from short to int counting: + rc.put('G', Short.MAX_VALUE); + rc.add('g'); + assertEquals(rc.toString(), "[ C:1 G:32768 Q:2 {:1 ]"); + } + + @Test(groups = "Functional") + public void testGetTooltip() + { + ResidueCount rc = new ResidueCount(); + + // no counts! + assertEquals(rc.getTooltip(20, 1), ""); + + /* + * count 7 C, 6 K, 7 Q, 10 P, 9 W, 1 F (total 40) + */ + for (int i = 0; i < 7; i++) + { + rc.add('c'); + rc.add('q'); + } + for (int i = 0; i < 10; i++) + { + rc.add('p'); + } + for (int i = 0; i < 9; i++) + { + rc.add('W'); + } + for (int i = 0; i < 6; i++) + { + rc.add('K'); + } + rc.add('F'); + + assertEquals(rc.getTooltip(40, 0), + "P 25%; W 22%; C 17%; Q 17%; K 15%; F 2%"); + + assertEquals(rc.getTooltip(30, 1), + "P 33.3%; W 30.0%; C 23.3%; Q 23.3%; K 20.0%; F 3.3%"); + } + + @Test(groups = "Functional") + public void testPut() + { + ResidueCount rc = new ResidueCount(); + rc.put('q', 3); + assertEquals(rc.getCount('Q'), 3); + rc.put(' ', 4); + assertEquals(rc.getGapCount(), 4); + rc.put('.', 5); + assertEquals(rc.getGapCount(), 5); + rc.put('-', 6); + assertEquals(rc.getGapCount(), 6); + + rc.put('?', 5); + assertEquals(rc.getCount('?'), 5); + rc.put('?', 6); + rc.put('!', 7); + assertEquals(rc.getCount('?'), 6); + assertEquals(rc.getCount('!'), 7); + } +} diff --git a/test/jalview/ext/android/SparseIntArrayTest.java b/test/jalview/ext/android/SparseIntArrayTest.java new file mode 100644 index 0000000..0ce0467 --- /dev/null +++ b/test/jalview/ext/android/SparseIntArrayTest.java @@ -0,0 +1,124 @@ +package jalview.ext.android; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +import org.testng.annotations.Test; + +/* + * Tests for SparseIntArray. Unlike SparseShortArray, SparseIntArray does not throw + * any exception for overflow. + */ +public class SparseIntArrayTest +{ + @Test(groups = "Functional") + public void testPut() + { + SparseIntArray counter = new SparseIntArray(); + + /* + * either key or value may be in the range of int + */ + counter.put(Integer.MAX_VALUE, Integer.MIN_VALUE); + counter.put(Integer.MIN_VALUE, Integer.MAX_VALUE); + assertEquals(counter.get(Integer.MAX_VALUE), Integer.MIN_VALUE); + assertEquals(counter.get(Integer.MIN_VALUE), Integer.MAX_VALUE); + } + + @Test(groups = "Functional") + public void testAdd() + { + SparseIntArray counter = new SparseIntArray(); + + assertEquals(counter.add('P', 2), 2); + assertEquals(counter.add('P', 3), 5); + counter.put('Q', 7); + assertEquals(counter.add('Q', 4), 11); + + counter.put('x', Integer.MAX_VALUE); + try + { + counter.add('x', 1); + fail("expected exception"); + } catch (ArithmeticException e) + { + // expected + } + + counter.put('y', Integer.MIN_VALUE); + try + { + counter.add('y', -1); + fail("expected exception"); + } catch (ArithmeticException e) + { + // expected + } + } + + @Test(groups = "Functional") + public void testCheckOverflow() + { + // things that don't overflow: + SparseIntArray.checkOverflow(Integer.MAX_VALUE, 0); + SparseIntArray.checkOverflow(Integer.MAX_VALUE, -1); + SparseIntArray.checkOverflow(Integer.MAX_VALUE, Integer.MIN_VALUE); + SparseIntArray.checkOverflow(Integer.MAX_VALUE, -Integer.MAX_VALUE); + SparseIntArray.checkOverflow(0, -Integer.MAX_VALUE); + SparseIntArray.checkOverflow(0, Integer.MIN_VALUE); + SparseIntArray.checkOverflow(Integer.MIN_VALUE, 0); + SparseIntArray.checkOverflow(Integer.MIN_VALUE, 1); + SparseIntArray.checkOverflow(Integer.MIN_VALUE, Integer.MAX_VALUE); + + // and some that do + try + { + SparseIntArray.checkOverflow(Integer.MAX_VALUE, 1); + fail("expected exception"); + } catch (ArithmeticException e) + { + // expected + } + try + { + SparseIntArray.checkOverflow(Integer.MAX_VALUE - 1, 2); + fail("expected exception"); + } catch (ArithmeticException e) + { + // expected + } + try + { + SparseIntArray.checkOverflow(1, Integer.MAX_VALUE); + fail("expected exception"); + } catch (ArithmeticException e) + { + // expected + } + try + { + SparseIntArray.checkOverflow(Integer.MIN_VALUE, -1); + fail("expected exception"); + } catch (ArithmeticException e) + { + // expected + } + try + { + SparseIntArray.checkOverflow(Integer.MIN_VALUE + 1, -2); + fail("expected exception"); + } catch (ArithmeticException e) + { + // expected + } + try + { + SparseIntArray.checkOverflow(-1, Integer.MIN_VALUE); + fail("expected exception"); + } catch (ArithmeticException e) + { + // expected + } + } + +} diff --git a/test/jalview/ext/android/SparseShortArrayTest.java b/test/jalview/ext/android/SparseShortArrayTest.java new file mode 100644 index 0000000..351f640 --- /dev/null +++ b/test/jalview/ext/android/SparseShortArrayTest.java @@ -0,0 +1,94 @@ +package jalview.ext.android; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +import org.testng.annotations.Test; + +public class SparseShortArrayTest +{ + @Test(groups = "Functional") + public void testPut() + { + SparseShortArray counter = new SparseShortArray(); + + /* + * either key or value may be in the range of short + */ + counter.put(Short.MAX_VALUE, Short.MIN_VALUE); + counter.put(Short.MIN_VALUE, Short.MAX_VALUE); + + // put a too large value + try + { + counter.put(0, Short.MAX_VALUE + 1); + fail("Expected exception"); + } catch (ArithmeticException e) + { + // expected; + } + + // put a too small value + try + { + counter.put(1, Short.MIN_VALUE - 1); + fail("Expected exception"); + } catch (ArithmeticException e) + { + // expected; + } + + // put a too large key + try + { + counter.put(Short.MAX_VALUE + 1, 0); + fail("Expected exception"); + } catch (ArithmeticException e) + { + // expected; + } + + // put a too small key + try + { + counter.put(Short.MIN_VALUE - 1, 2); + fail("Expected exception"); + } catch (ArithmeticException e) + { + // expected; + } + } + + @Test(groups = "Functional") + public void testAdd() + { + SparseShortArray counter = new SparseShortArray(); + + assertEquals(counter.add('P', 2), 2); + assertEquals(counter.add('P', 3), 5); + counter.put('Q', 7); + assertEquals(counter.add('Q', 4), 11); + + // increment giving overflow + counter.put('x', Short.MAX_VALUE); + try + { + counter.add('x', 1); + fail("Expected exception"); + } catch (ArithmeticException e) + { + // expected; + } + + // decrement giving underflow + counter.put('y', Short.MIN_VALUE); + try + { + counter.add('y', -1); + fail("Expected exception"); + } catch (ArithmeticException e) + { + // expected; + } + } +} diff --git a/test/jalview/schemes/ResidueColourSchemeTest.java b/test/jalview/schemes/ResidueColourSchemeTest.java new file mode 100644 index 0000000..8173dd1 --- /dev/null +++ b/test/jalview/schemes/ResidueColourSchemeTest.java @@ -0,0 +1,147 @@ +package jalview.schemes; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertTrue; + +import jalview.analysis.Profile; + +import java.awt.Color; + +import org.testng.annotations.Test; + +public class ResidueColourSchemeTest +{ + @Test(groups = "Functional") + public void testAboveThreshold() + { + /* + * make up profiles for this alignment: + * AR-Q + * AR-- + * SR-T + * SR-T + */ + Profile[] profiles = new Profile[4]; + profiles[0] = new Profile(4, 0, 2, "AS"); + profiles[1] = new Profile(4, 0, 4, "R"); + profiles[2] = new Profile(4, 4, 0, ""); + profiles[3] = new Profile(4, 1, 2, "T"); + ResidueColourScheme rcs = new ResidueColourScheme(); + rcs.setConsensus(profiles); + + /* + * no threshold + */ + rcs.setThreshold(0, true); + assertTrue(rcs.aboveThreshold('a', 0)); + assertTrue(rcs.aboveThreshold('S', 0)); + assertFalse(rcs.aboveThreshold('W', 0)); + assertTrue(rcs.aboveThreshold('R', 1)); + assertFalse(rcs.aboveThreshold('W', 2)); + assertTrue(rcs.aboveThreshold('t', 3)); + assertFalse(rcs.aboveThreshold('Q', 3)); + + /* + * with threshold, include gaps + */ + rcs.setThreshold(60, false); + assertFalse(rcs.aboveThreshold('a', 0)); + assertFalse(rcs.aboveThreshold('S', 0)); + assertTrue(rcs.aboveThreshold('R', 1)); + assertFalse(rcs.aboveThreshold('W', 2)); + assertFalse(rcs.aboveThreshold('t', 3)); // 50% < 60% + + /* + * with threshold, ignore gaps + */ + rcs.setThreshold(60, true); + assertFalse(rcs.aboveThreshold('a', 0)); + assertFalse(rcs.aboveThreshold('S', 0)); + assertTrue(rcs.aboveThreshold('R', 1)); + assertFalse(rcs.aboveThreshold('W', 2)); + assertTrue(rcs.aboveThreshold('t', 3)); // 67% > 60% + } + + /** + * Test colour bleaching based on conservation score and conservation slider. + * Scores of 10 or 11 should leave colours unchanged. Gap is always white. + */ + @Test(groups = "Functional") + public void testApplyConservation() + { + ResidueColourScheme rcs = new ResidueColourScheme(); + + // no conservation present - no fading + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 12)); + + // cheat by setting conservation sequence directly + // rather than calculating it - good enough for this test + String consensus = "0123456789+*-"; + rcs.conservation = consensus.toCharArray(); + + // column out of range: + assertEquals(Color.RED, + rcs.applyConservation(Color.RED, consensus.length())); + + /* + * with 100% threshold, 'fade factor' is + * (11-score)/10 * 100/20 = (11-score)/2 + * which is >= 1 for all scores i.e. all fade to white except +, * + */ + rcs.setConservationInc(100); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 0)); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 1)); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 2)); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 3)); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 4)); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 5)); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 6)); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 7)); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 8)); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 9)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 10)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 11)); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 12)); + + /* + * with 0% threshold, there should be no fading + */ + rcs.setConservationInc(0); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 0)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 1)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 2)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 3)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 4)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 5)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 6)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 7)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 8)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 9)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 10)); + assertEquals(Color.RED, rcs.applyConservation(Color.RED, 11)); + assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 12)); // gap + + /* + * with 40% threshold, 'fade factor' is + * (11-score)/10 * 40/20 = (11-score)/5 + * which is {>1, >1, >1, >1, >1, >1, 1, 0.8, 0.6, 0.4} for score 0-9 + * e.g. score 7 colour fades 80% of the way to white (255, 255, 255) + */ + rcs.setConservationInc(40); + Color colour = new Color(155, 105, 55); + assertEquals(Color.WHITE, rcs.applyConservation(colour, 0)); + assertEquals(Color.WHITE, rcs.applyConservation(colour, 1)); + assertEquals(Color.WHITE, rcs.applyConservation(colour, 2)); + assertEquals(Color.WHITE, rcs.applyConservation(colour, 3)); + assertEquals(Color.WHITE, rcs.applyConservation(colour, 4)); + assertEquals(Color.WHITE, rcs.applyConservation(colour, 5)); + assertEquals(Color.WHITE, rcs.applyConservation(colour, 6)); + assertEquals(new Color(235, 225, 215), rcs.applyConservation(colour, 7)); + assertEquals(new Color(215, 195, 175), rcs.applyConservation(colour, 8)); + assertEquals(new Color(195, 165, 135), rcs.applyConservation(colour, 9)); + assertEquals(colour, rcs.applyConservation(colour, 10)); + assertEquals(colour, rcs.applyConservation(colour, 11)); + assertEquals(Color.WHITE, rcs.applyConservation(colour, 12)); + } +} diff --git a/test/jalview/util/FormatTest.java b/test/jalview/util/FormatTest.java new file mode 100644 index 0000000..18199f9 --- /dev/null +++ b/test/jalview/util/FormatTest.java @@ -0,0 +1,32 @@ +package jalview.util; + +import static org.testng.Assert.assertEquals; + +import org.testng.annotations.Test; + +public class FormatTest +{ + @Test(groups = "Functional") + public void testAppendPercentage() + { + StringBuilder sb = new StringBuilder(); + Format.appendPercentage(sb, 123.456f, 0); + assertEquals(sb.toString(), "123"); + + sb.setLength(0); + Format.appendPercentage(sb, 123.456f, 1); + assertEquals(sb.toString(), "123.4"); + + sb.setLength(0); + Format.appendPercentage(sb, 123.456f, 2); + assertEquals(sb.toString(), "123.45"); + + sb.setLength(0); + Format.appendPercentage(sb, 123.456f, 3); + assertEquals(sb.toString(), "123.456"); + + sb.setLength(0); + Format.appendPercentage(sb, 123.456f, 4); + assertEquals(sb.toString(), "123.4560"); + } +} diff --git a/test/jalview/util/SparseCountTest.java b/test/jalview/util/SparseCountTest.java new file mode 100644 index 0000000..c9a07a1 --- /dev/null +++ b/test/jalview/util/SparseCountTest.java @@ -0,0 +1,79 @@ +package jalview.util; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import org.testng.annotations.Test; +public class SparseCountTest +{ + @Test(groups = "Functional") + public void testAdd() + { + SparseCount p = new SparseCount(8); + p.add('a', 1); + p.add('b', 2); + p.add('a', 3); + p.add('b', -4); + assertEquals(p.size(), 2); + assertEquals(p.get('a'), 4); + assertEquals(p.get('b'), -2); + } + + @Test(groups = "Functional") + public void testPut() + { + SparseCount p = new SparseCount(8); + p.put('a', 3); + p.add('b', 2); + p.put('b', 4); + assertEquals(p.size(), 2); + assertEquals(p.get('a'), 3); + assertEquals(p.get('b'), 4); + } + + /** + * Test handling overflow of short by switching to counting ints + */ + @Test(groups = "Functional") + public void testOverflow() + { + SparseCount p = new SparseCount(8); + p.put('a', Short.MAX_VALUE - 1); + p.add('a', 1); + assertFalse(p.isUsingInt()); + p.add('a', 1); + assertTrue(p.isUsingInt()); + } + + /** + * Test handling underflow of short by switching to counting ints + */ + @Test(groups = "Functional") + public void testUnderflow() + { + SparseCount p = new SparseCount(8); + p.put('a', Short.MIN_VALUE + 1); + p.add('a', -1); + assertFalse(p.isUsingInt()); + p.add('a', -1); + assertTrue(p.isUsingInt()); + } + + @Test(groups = "Functional") + public void testKeyAt_ValueAt() + { + SparseCount p = new SparseCount(8); + p.put('W', 12); + p.put('K', 9); + p.put('R', 6); + assertEquals(p.size(), 3); + assertEquals(p.keyAt(0), 'K'); + assertEquals(p.valueAt(0), 9); + assertEquals(p.keyAt(1), 'R'); + assertEquals(p.valueAt(1), 6); + assertEquals(p.keyAt(2), 'W'); + assertEquals(p.valueAt(2), 12); + } + +}