cd35206b676fd86ef02dd9694316da8dfff3fbbf
[jalview.git] / src / jalview / analysis / ResidueCount.java
1 package jalview.analysis;
2
3 import jalview.util.Comparison;
4 import jalview.util.Format;
5 import jalview.util.QuickSort;
6 import jalview.util.SparseCount;
7
8 /**
9  * A class to count occurrences of residues in a profile, optimised for speed
10  * and memory footprint.
11  * @author gmcarstairs
12  *
13  */
14 public class ResidueCount
15 {
16   /**
17    * A data bean to hold the results of counting symbols
18    */
19   public class SymbolCounts
20   {
21     /**
22      * the symbols seen (as char values), in no particular order
23      */
24     public final char[] symbols;
25
26     /**
27      * the counts for each symbol, in the same order as the symbols
28      */
29     public final int[] values;
30
31     SymbolCounts(char[] s, int[] v)
32     {
33       symbols = s;
34       values = v;
35     }
36   }
37
38   private static final int TOUPPERCASE = 'A' - 'a';
39
40   /*
41    * nucleotide symbols to count (including N unknown)
42    */
43   private static final String NUCS = "ACGNTU";
44
45   /*
46    * amino acid symbols to count (including X unknown)
47    * NB we also include U so as to support counting of RNA bases
48    * in the "don't know" case of nucleotide / peptide
49    */
50   private static final String AAS = "ACDEFGHIKLMNPQRSTUVWXY";
51
52   private static final int GAP_COUNT = 0;
53
54   /*
55    * fast lookup tables holding the index into our count
56    * arrays of each symbol; index 0 is reserved for gap counting
57    */
58   private static int[] NUC_INDEX = new int[26];
59
60   private static int[] AA_INDEX = new int[26];
61   static
62   {
63     for (int i = 0; i < NUCS.length(); i++)
64     {
65       NUC_INDEX[NUCS.charAt(i) - 'A'] = i + 1;
66     }
67     for (int i = 0; i < AAS.length(); i++)
68     {
69       AA_INDEX[AAS.charAt(i) - 'A'] = i + 1;
70     }
71   }
72
73   /*
74    * counts array, just big enough for the nucleotide or peptide
75    * character set (plus gap counts in position 0)
76    */
77   private short[] counts;
78
79   /*
80    * alternative array of int counts for use if any count 
81    * exceeds the maximum value of short (32767)
82    */
83   private int[] intCounts;
84
85   /*
86    * flag set if we switch from short to int counts
87    */
88   private boolean useIntCounts;
89
90   /*
91    * general-purpose counter, only for use for characters
92    * that are not in the expected alphabet
93    */
94   private SparseCount otherData;
95
96   /*
97    * keeps track of the maximum count value recorded
98    * (if this class every allows decrements, would need to
99    * calculate this on request instead) 
100    */
101   int maxCount;
102
103   /*
104    * if we think we are counting nucleotide, can get by with smaller
105    * array to hold counts
106    */
107   private boolean isNucleotide;
108
109   /**
110    * Default constructor allocates arrays able to count either nucleotide or
111    * peptide bases. Use this constructor if not sure which the data is.
112    */
113   public ResidueCount()
114   {
115     this(false);
116   }
117
118   /**
119    * Constructor that allocates an array just big enough for the anticipated
120    * characters, plus one position to count gaps
121    */
122   public ResidueCount(boolean nucleotide)
123   {
124     isNucleotide = nucleotide;
125     int charsToCount = nucleotide ? NUCS.length() : AAS.length();
126     counts = new short[charsToCount + 1];
127   }
128
129   /**
130    * Increments the count for the given character. The supplied character may be
131    * upper or lower case but counts are for the upper case only. Gap characters
132    * (space, ., -) are all counted together.
133    * 
134    * @param c
135    * @return the new value of the count for the character
136    */
137   public int add(final char c)
138   {
139     char u = toUpperCase(c);
140     int newValue = 0;
141     int offset = getOffset(u);
142
143     /*
144      * offset 0 is reserved for gap counting, so 0 here means either
145      * an unexpected character, or a gap character passed in error
146      */
147     if (offset == 0)
148     {
149       if (Comparison.isGap(u))
150       {
151         newValue = addGap();
152       }
153       else
154       {
155         newValue = addOtherCharacter(u);
156       }
157     }
158     else
159     {
160       newValue = increment(offset);
161     }
162     return newValue;
163   }
164
165   /**
166    * Increment the count at the specified offset. If this would result in short
167    * overflow, promote to counting int values instead.
168    * 
169    * @param offset
170    * @return the new value of the count at this offset
171    */
172   int increment(int offset)
173   {
174     int newValue = 0;
175     if (useIntCounts)
176     {
177       newValue = intCounts[offset];
178       intCounts[offset] = ++newValue;
179     }
180     else
181     {
182       if (counts[offset] == Short.MAX_VALUE)
183       {
184         handleOverflow();
185         newValue = intCounts[offset];
186         intCounts[offset] = ++newValue;
187       }
188       else
189       {
190         newValue = counts[offset];
191         counts[offset] = (short) ++newValue;
192       }
193     }
194     maxCount = Math.max(maxCount, newValue);
195     return newValue;
196   }
197
198   /**
199    * Switch from counting in short to counting in int
200    */
201   synchronized void handleOverflow()
202   {
203     intCounts = new int[counts.length];
204     for (int i = 0; i < counts.length; i++)
205     {
206       intCounts[i] = counts[i];
207     }
208     counts = null;
209     useIntCounts = true;
210   }
211
212   /**
213    * Returns this character's offset in the count array
214    * 
215    * @param c
216    * @return
217    */
218   int getOffset(char c)
219   {
220     int offset = 0;
221     if ('A' <= c && c <= 'Z')
222     {
223       offset = isNucleotide ? NUC_INDEX[c - 'A'] : AA_INDEX[c - 'A'];
224     }
225     return offset;
226   }
227
228   /**
229    * @param c
230    * @return
231    */
232   protected char toUpperCase(final char c)
233   {
234     char u = c;
235     if ('a' <= c && c <= 'z')
236     {
237       u = (char) (c + TOUPPERCASE);
238     }
239     return u;
240   }
241
242   /**
243    * Increment count for some unanticipated character. The first time this
244    * called, a SparseCount is instantiated to hold these 'extra' counts.
245    * 
246    * @param c
247    * @return the new value of the count for the character
248    */
249   int addOtherCharacter(char c)
250   {
251     if (otherData == null)
252     {
253       otherData = new SparseCount();
254     }
255     int newValue = otherData.add(c, 1);
256     maxCount = Math.max(maxCount, newValue);
257     return newValue;
258   }
259
260   /**
261    * Set count for some unanticipated character. The first time this called, a
262    * SparseCount is instantiated to hold these 'extra' counts.
263    * 
264    * @param c
265    * @param value
266    */
267   void setOtherCharacter(char c, int value)
268   {
269     if (otherData == null)
270     {
271       otherData = new SparseCount();
272     }
273     otherData.put(c, value);
274   }
275
276   /**
277    * Increment count of gap characters
278    * 
279    * @return the new count of gaps
280    */
281   public int addGap()
282   {
283     int newValue;
284     if (useIntCounts)
285     {
286       newValue = ++intCounts[GAP_COUNT];
287     }
288     else
289     {
290       newValue = ++counts[GAP_COUNT];
291     }
292     return newValue;
293   }
294
295   /**
296    * Answers true if we are counting ints (only after overflow of short counts)
297    * 
298    * @return
299    */
300   boolean isCountingInts()
301   {
302     return useIntCounts;
303   }
304
305   /**
306    * Sets the count for the given character. The supplied character may be upper
307    * or lower case but counts are for the upper case only.
308    * 
309    * @param c
310    * @param count
311    */
312   public void put(char c, int count)
313   {
314     char u = toUpperCase(c);
315     int offset = getOffset(u);
316
317     /*
318      * offset 0 is reserved for gap counting, so 0 here means either
319      * an unexpected character, or a gap character passed in error
320      */
321     if (offset == 0)
322     {
323       if (Comparison.isGap(u))
324       {
325         set(0, count);
326       }
327       else
328       {
329         setOtherCharacter(u, count);
330         maxCount = Math.max(maxCount, count);
331       }
332     }
333     else
334     {
335       set(offset, count);
336       maxCount = Math.max(maxCount, count);
337     }
338   }
339
340   /**
341    * Sets the count at the specified offset. If this would result in short
342    * overflow, promote to counting int values instead.
343    * 
344    * @param offset
345    * @param value
346    */
347   void set(int offset, int value)
348   {
349     if (useIntCounts)
350     {
351       intCounts[offset] = value;
352     }
353     else
354     {
355       if (value > Short.MAX_VALUE || value < Short.MIN_VALUE)
356       {
357         handleOverflow();
358         intCounts[offset] = value;
359       }
360       else
361       {
362         counts[offset] = (short) value;
363       }
364     }
365   }
366
367   /**
368    * Returns the count for the given character, or zero if no count held
369    * 
370    * @param c
371    * @return
372    */
373   public int getCount(char c)
374   {
375     char u = toUpperCase(c);
376     int offset = getOffset(u);
377     if (offset == 0)
378     {
379       if (!Comparison.isGap(u))
380       {
381         // should have called getGapCount()
382         return otherData == null ? 0 : otherData.get(u);
383       }
384     }
385     return useIntCounts ? intCounts[offset] : counts[offset];
386   }
387
388   public int getGapCount()
389   {
390     return useIntCounts ? intCounts[0] : counts[0];
391   }
392
393   /**
394    * Answers true if this object wraps a counter for unexpected characters
395    * 
396    * @return
397    */
398   boolean isUsingOtherData()
399   {
400     return otherData != null;
401   }
402
403   /**
404    * Returns the character (or concatenated characters) for the symbol(s) with
405    * the given count in the profile. Can be used to get the modal residue by
406    * supplying the modal count value. Returns an empty string if symbol has the
407    * given count. The symbols are in alphabetic order of standard peptide or
408    * nucleotide characters, followed by 'other' symbols if any.
409    * 
410    * @return
411    */
412   public String getResiduesForCount(int count)
413   {
414     if (count == 0)
415     {
416       return "";
417     }
418
419     /*
420      * find counts for the given value and append the
421      * corresponding symbol
422      */
423     StringBuilder modal = new StringBuilder();
424     if (useIntCounts)
425     {
426       for (int i = 1; i < intCounts.length; i++)
427       {
428         if (intCounts[i] == count)
429         {
430           modal.append(isNucleotide ? NUCS.charAt(i - 1) : AAS
431                   .charAt(i - 1));
432         }
433       }
434     }
435     else
436     {
437       for (int i = 1; i < counts.length; i++)
438       {
439         if (counts[i] == count)
440         {
441           modal.append(isNucleotide ? NUCS.charAt(i - 1) : AAS
442                   .charAt(i - 1));
443         }
444       }
445     }
446     if (otherData != null)
447     {
448       for (int i = 0; i < otherData.size(); i++)
449       {
450         if (otherData.valueAt(i) == count)
451         {
452           modal.append((char) otherData.keyAt(i));
453         }
454       }
455     }
456     return modal.toString();
457   }
458
459   /**
460    * Returns the highest count for any symbol(s) in the profile (excluding gap)
461    * 
462    * @return
463    */
464   public int getModalCount()
465   {
466     return maxCount;
467   }
468
469   /**
470    * Returns the number of distinct symbols with a non-zero count (excluding the
471    * gap symbol)
472    * 
473    * @return
474    */
475   public int size() {
476     int size = 0;
477     if (useIntCounts)
478     {
479       for (int i = 1; i < intCounts.length; i++)
480       {
481         if (intCounts[i] > 0)
482         {
483           size++;
484         }
485       }
486     }
487     else
488     {
489       for (int i = 1; i < counts.length; i++)
490       {
491         if (counts[i] > 0)
492         {
493           size++;
494         }
495       }
496     }
497     if (otherData != null)
498     {
499       size += otherData.size();
500     }
501
502     return size;
503   }
504
505   /**
506    * Returns a data bean holding those symbols that have a non-zero count
507    * (excluding the gap symbol), with their counts.
508    * 
509    * @return
510    */
511   public SymbolCounts getSymbolCounts()
512   {
513     char[] symbols = new char[size()];
514     int[] values = new int[size()];
515     int j = 0;
516
517     if (useIntCounts)
518     {
519       for (int i = 1; i < intCounts.length; i++)
520       {
521         if (intCounts[i] > 0)
522         {
523           char symbol = isNucleotide ? NUCS.charAt(i - 1) : AAS
524                   .charAt(i - 1);
525           symbols[j] = symbol;
526           values[j] = intCounts[i];
527           j++;
528         }
529       }
530     }
531     else
532     {
533       for (int i = 1; i < counts.length; i++)
534       {
535         if (counts[i] > 0)
536         {
537           char symbol = isNucleotide ? NUCS.charAt(i - 1) : AAS
538                   .charAt(i - 1);
539           symbols[j] = symbol;
540           values[j] = counts[i];
541           j++;
542         }
543       }
544     }
545     if (otherData != null)
546     {
547       for (int i = 0; i < otherData.size(); i++)
548       {
549         symbols[j] = (char) otherData.keyAt(i);
550         values[j] = otherData.valueAt(i);
551         j++;
552       }
553     }
554
555     return new SymbolCounts(symbols, values);
556   }
557
558   /**
559    * Returns a tooltip string showing residues in descending order of their
560    * percentage frequency in the profile
561    * 
562    * @param normaliseBy
563    *          the divisor for residue counts (may or may not include gapped
564    *          sequence count)
565    * @param percentageDecPl
566    *          the number of decimal places to show in percentages
567    * @return
568    */
569   public String getTooltip(int normaliseBy, int percentageDecPl)
570   {
571     SymbolCounts symbolCounts = getSymbolCounts();
572     char[] ca = symbolCounts.symbols;
573     int[] vl = symbolCounts.values;
574
575     /*
576      * sort characters into ascending order of their counts
577      */
578     QuickSort.sort(vl, ca);
579
580     /*
581      * traverse in reverse order (highest count first) to build tooltip
582      */
583     boolean first = true;
584     StringBuilder sb = new StringBuilder(64);
585     for (int c = ca.length - 1; c >= 0; c--)
586     {
587       final char residue = ca[c];
588       // TODO combine residues which share a percentage
589       // (see AAFrequency.completeCdnaConsensus)
590       float tval = (vl[c] * 100f) / normaliseBy;
591       sb.append(first ? "" : "; ").append(residue).append(" ");
592       Format.appendPercentage(sb, tval, percentageDecPl);
593       sb.append("%");
594       first = false;
595     }
596     return sb.toString();
597   }
598
599   /**
600    * Returns a string representation of the symbol counts, for debug purposes.
601    */
602   @Override
603   public String toString()
604   {
605     StringBuilder sb = new StringBuilder();
606     sb.append("[ ");
607     SymbolCounts sc = getSymbolCounts();
608     for (int i = 0; i < sc.symbols.length; i++)
609     {
610       sb.append(sc.symbols[i]).append(":").append(sc.values[i]).append(" ");
611     }
612     sb.append("]");
613     return sb.toString();
614   }
615 }