JAL-98 first working version
[jalview.git] / src / jalview / analysis / AAFrequency.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.analysis;
22
23 import jalview.datamodel.AlignedCodonFrame;
24 import jalview.datamodel.AlignmentAnnotation;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.Annotation;
27 import jalview.datamodel.SequenceI;
28 import jalview.ext.android.SparseIntArray;
29 import jalview.util.Format;
30 import jalview.util.MappingUtils;
31 import jalview.util.QuickSort;
32
33 import java.util.Arrays;
34 import java.util.Hashtable;
35 import java.util.List;
36
37 /**
38  * Takes in a vector or array of sequences and column start and column end and
39  * returns a new Hashtable[] of size maxSeqLength, if Hashtable not supplied.
40  * This class is used extensively in calculating alignment colourschemes that
41  * depend on the amount of conservation in each alignment column.
42  * 
43  * @author $author$
44  * @version $Revision$
45  */
46 public class AAFrequency
47 {
48   private static final int TO_UPPER_CASE = 'A' - 'a'; // -32
49
50   public static final String MAXCOUNT = "C";
51
52   public static final String MAXRESIDUE = "R";
53
54   public static final String PID_GAPS = "G";
55
56   public static final String PID_NOGAPS = "N";
57
58   public static final String PROFILE = "P";
59
60   public static final String ENCODED_CHARS = "E";
61
62   /*
63    * Quick look-up of String value of char 'A' to 'Z'
64    */
65   private static final String[] CHARS = new String['Z' - 'A' + 1];
66
67   static
68   {
69     for (char c = 'A'; c <= 'Z'; c++)
70     {
71       CHARS[c - 'A'] = String.valueOf(c);
72     }
73   }
74
75   public static final Hashtable[] calculate(List<SequenceI> list,
76           int start, int end)
77   {
78     return calculate(list, start, end, false);
79   }
80
81   public static final Hashtable[] calculate(List<SequenceI> sequences,
82           int start, int end, boolean profile)
83   {
84     SequenceI[] seqs = new SequenceI[sequences.size()];
85     int width = 0;
86     synchronized (sequences)
87     {
88       for (int i = 0; i < sequences.size(); i++)
89       {
90         seqs[i] = sequences.get(i);
91         if (seqs[i].getLength() > width)
92         {
93           width = seqs[i].getLength();
94         }
95       }
96
97       Hashtable[] reply = new Hashtable[width];
98
99       if (end >= width)
100       {
101         end = width;
102       }
103
104       calculate(seqs, start, end, reply, profile);
105       return reply;
106     }
107   }
108
109   public static final void calculate(SequenceI[] sequences, int start,
110           int end, Hashtable[] result, boolean profile)
111   {
112     Hashtable residueHash;
113     int maxCount, nongap, i, j, v;
114     int jSize = sequences.length;
115     String maxResidue;
116     char c = '-';
117     float percentage;
118
119     // int[] values = new int[255];
120
121     char[] seq;
122
123     for (i = start; i < end; i++)
124     {
125       residueHash = new Hashtable();
126       maxCount = 0;
127       maxResidue = "";
128       nongap = 0;
129       // values = new int[255];
130       SparseIntArray values = new SparseIntArray();
131
132       for (j = 0; j < jSize; j++)
133       {
134         if (sequences[j] == null)
135         {
136           System.err
137                   .println("WARNING: Consensus skipping null sequence - possible race condition.");
138           continue;
139         }
140         seq = sequences[j].getSequence();
141         if (seq.length > i)
142         {
143           c = seq[i];
144
145           if (c == '.' || c == ' ')
146           {
147             c = '-';
148           }
149
150           if (c == '-')
151           {
152             // values['-']++;
153             values.put('-', values.get('-') + 1);
154             continue;
155           }
156           else if ('a' <= c && c <= 'z')
157           {
158             c += TO_UPPER_CASE;
159           }
160
161           nongap++;
162           // values[c]++;
163           values.put(c, values.get(c) + 1);
164
165         }
166         else
167         {
168           // values['-']++;
169           values.put('-', values.get('-') + 1);
170         }
171       }
172       if (jSize == 1)
173       {
174         maxResidue = String.valueOf(c);
175         maxCount = 1;
176       }
177       else
178       {
179         // FIXME iterate over values keys instead
180         for (v = 'A'; v <= 'Z'; v++)
181         {
182           // TODO why ignore values[v] == 1?
183           int count = values.get(v); // values[v];
184           if (count < 1 /* 2 */|| count < maxCount)
185           {
186             continue;
187           }
188
189           if (count > maxCount)
190           {
191             maxResidue = CHARS[v - 'A'];
192           }
193           else if (count == maxCount)
194           {
195             maxResidue += CHARS[v - 'A'];
196           }
197           maxCount = count;
198         }
199       }
200       if (maxResidue.length() == 0)
201       {
202         maxResidue = "-";
203       }
204       if (profile)
205       {
206         // TODO use a 1-dimensional array with jSize, nongap in [0] and [1]
207         // residueHash.put(PROFILE, new int[][] { values,
208         // new int[] { jSize, nongap } });
209         residueHash.put(PROFILE, new Profile(values, jSize, nongap));
210       }
211       residueHash.put(MAXCOUNT, new Integer(maxCount));
212       residueHash.put(MAXRESIDUE, maxResidue);
213
214       percentage = ((float) maxCount * 100) / jSize;
215       residueHash.put(PID_GAPS, new Float(percentage));
216
217       if (nongap > 0)
218       {
219         // calculate for non-gapped too
220         percentage = ((float) maxCount * 100) / nongap;
221       }
222       residueHash.put(PID_NOGAPS, new Float(percentage));
223
224       result[i] = residueHash;
225     }
226   }
227
228   /**
229    * Compute all or part of the annotation row from the given consensus
230    * hashtable
231    * 
232    * @param consensus
233    *          - pre-allocated annotation row
234    * @param hconsensus
235    * @param iStart
236    * @param width
237    * @param ignoreGapsInConsensusCalculation
238    * @param includeAllConsSymbols
239    * @param nseq
240    */
241   public static void completeConsensus(AlignmentAnnotation consensus,
242           Hashtable[] hconsensus, int iStart, int width,
243           boolean ignoreGapsInConsensusCalculation,
244           boolean includeAllConsSymbols, long nseq)
245   {
246     completeConsensus(consensus, hconsensus, iStart, width,
247             ignoreGapsInConsensusCalculation, includeAllConsSymbols, null,
248             nseq);
249   }
250
251   /**
252    * Derive the consensus annotations to be added to the alignment for display.
253    * This does not recompute the raw data, but may be called on a change in
254    * display options, such as 'show logo', which may in turn result in a change
255    * in the derived values.
256    * 
257    * @param consensus
258    *          the annotation row to add annotations to
259    * @param hconsensus
260    *          the source consensus data
261    * @param iStart
262    *          start column
263    * @param width
264    *          end column
265    * @param ignoreGapsInConsensusCalculation
266    *          if true, use the consensus calculated ignoring gaps
267    * @param includeAllConsSymbols
268    *          if true include all consensus symbols, else just show modal
269    *          residue
270    * @param alphabet
271    * @param nseq
272    *          number of sequences
273    */
274   public static void completeConsensus(AlignmentAnnotation consensus,
275           Hashtable[] hconsensus, int iStart, int width,
276           boolean ignoreGapsInConsensusCalculation,
277           boolean includeAllConsSymbols, char[] alphabet, long nseq)
278   {
279     if (consensus == null || consensus.annotations == null
280             || consensus.annotations.length < width)
281     {
282       // called with a bad alignment annotation row - wait for it to be
283       // initialised properly
284       return;
285     }
286
287     final Format fmt = getPercentageFormat(nseq);
288
289     for (int i = iStart; i < width; i++)
290     {
291       Hashtable hci;
292       if (i >= hconsensus.length || ((hci = hconsensus[i]) == null))
293       {
294         // happens if sequences calculated over were shorter than alignment
295         // width
296         consensus.annotations[i] = null;
297         continue;
298       }
299       Float fv = (Float) hci
300               .get(ignoreGapsInConsensusCalculation ? PID_NOGAPS : PID_GAPS);
301       if (fv == null)
302       {
303         consensus.annotations[i] = null;
304         // data has changed below us .. give up and
305         continue;
306       }
307       float value = fv.floatValue();
308       String maxRes = hci.get(AAFrequency.MAXRESIDUE).toString();
309       StringBuilder mouseOver = new StringBuilder(64);
310       if (maxRes.length() > 1)
311       {
312         mouseOver.append("[").append(maxRes).append("] ");
313         maxRes = "+";
314       }
315       else
316       {
317         mouseOver.append(hci.get(AAFrequency.MAXRESIDUE) + " ");
318       }
319       // int[][] profile = (int[][]) hci.get(AAFrequency.PROFILE);
320       Profile profile = (Profile) hci.get(AAFrequency.PROFILE);
321       if (profile != null && includeAllConsSymbols)
322       {
323         int sequenceCount = profile.height;// profile[1][0];
324         int nonGappedCount = profile.nonGapped;// [1][1];
325         int normalisedBy = ignoreGapsInConsensusCalculation ? nonGappedCount
326                 : sequenceCount;
327         mouseOver.setLength(0);
328         // TODO do this sort once only in calculate()?
329         // char[][] ca = new char[profile[0].length][];
330         // /int length = profile[0].length;
331         int length = profile.profile.size();
332         char[] ca = new char[length];
333         // float[] vl = new float[length];
334         int[] vl = new int[length];
335         for (int c = 0; c < ca.length; c++)
336         {
337           int theChar = profile.profile.keyAt(c);
338           ca[c] = (char) theChar;// c;
339           // ca[c] = new char[]
340           // { (char) c };
341           vl[c] = profile.profile.get(theChar);// profile[0][c];
342         }
343
344         /*
345          * sort characters into ascending order of their counts
346          */
347         QuickSort.sort(vl, ca);
348
349         /*
350          * traverse in reverse order (highest count first) to build tooltip
351          */
352         // for (int p = 0, c = ca.length - 1; profile[0][ca[c]] > 0; c--)
353         for (int p = 0, c = ca.length - 1; c >= 0; c--)
354         {
355           final char residue = ca[c];
356           if (residue != '-')
357           {
358             // float tval = profile[0][residue] * 100f / normalisedBy;
359             // float tval = profile[0][residue] * 100f / normalisedBy;
360             float tval = (vl[c] * 100f) / normalisedBy;
361             mouseOver
362                     .append((((p == 0) ? "" : "; ")))
363                     .append(residue)
364                     .append(" ")
365                     .append(((fmt != null) ? fmt.form(tval) : ((int) tval)))
366                     .append("%");
367             p++;
368           }
369         }
370       }
371       else
372       {
373         mouseOver.append(
374                 (((fmt != null) ? fmt.form(value) : ((int) value))))
375                 .append("%");
376       }
377       consensus.annotations[i] = new Annotation(maxRes,
378               mouseOver.toString(), ' ', value);
379     }
380   }
381
382   /**
383    * Returns a Format designed to show all significant figures for profile
384    * percentages. For less than 100 sequences, returns null (the integer
385    * percentage value will be displayed). For 100-999 sequences, returns "%3.1f"
386    * 
387    * @param nseq
388    * @return
389    */
390   protected static Format getPercentageFormat(long nseq)
391   {
392     int scale = 0;
393     while (nseq >= 10)
394     {
395       scale++;
396       nseq /= 10;
397     }
398     return scale <= 1 ? null : new Format("%3." + (scale - 1) + "f");
399   }
400
401   /**
402    * Returns the sorted profile for the given consensus data. The returned array
403    * contains
404    * 
405    * <pre>
406    *    [profileType, numberOfValues, nonGapCount, charValue1, percentage1, charValue2, percentage2, ...]
407    * in descending order of percentage value
408    * </pre>
409    * 
410    * @param hconsensus
411    *          the data table from which to extract and sort values
412    * @param ignoreGaps
413    *          if true, only non-gapped values are included in percentage
414    *          calculations
415    * @return
416    */
417   public static int[] extractProfile(Hashtable hconsensus,
418           boolean ignoreGaps)
419   {
420     int[] rtnval = new int[64];
421     // int[][] profile = (int[][]) hconsensus.get(AAFrequency.PROFILE);
422     Profile profile = (Profile) hconsensus.get(AAFrequency.PROFILE);
423     if (profile == null)
424     {
425       return null;
426     }
427     // int profileLength = profile[0].length;
428     int profileLength = profile.profile.size();
429     char[] ca = new char[profileLength];
430     float[] vl = new float[profileLength];
431     // for (int c = 0; c < ca.length; c++)
432     // {
433     // ca[c] = (char) c;
434     // vl[c] = profile[0][c];
435     // }
436     for (int i = 0; i < profileLength; i++)
437     {
438       int c = profile.profile.keyAt(i);
439       ca[i] = (char) c;
440       vl[i] = profile.profile.get(c);
441     }
442     QuickSort.sort(vl, ca);
443     int nextArrayPos = 2;
444     int totalPercentage = 0;
445     int distinctValuesCount = 0;
446     final int divisor = ignoreGaps ? profile.nonGapped : profile.height;
447     // final int divisor = profile[1][ignoreGaps ? 1 : 0];
448     int j = profile.profile.size();
449     for (int i = 0; i < j; i++)
450 //    for (int c = ca.length - 1; profile[0][ca[c]] > 0; c--)
451     {
452       int theChar = profile.profile.keyAt(i);
453       int charCount = profile.profile.get(theChar);
454     
455 //      if (ca[c] != '-')
456         if (theChar != '-')
457       {
458 //        rtnval[nextArrayPos++] = ca[c];
459         rtnval[nextArrayPos++] = theChar;
460 //        final int percentage = (int) (profile[0][ca[c]] * 100f / divisor);
461         final int percentage = (charCount * 100) / divisor;
462         rtnval[nextArrayPos++] = percentage;
463         totalPercentage += percentage;
464         distinctValuesCount++;
465       }
466     }
467     rtnval[0] = distinctValuesCount;
468     rtnval[1] = totalPercentage;
469     int[] result = new int[rtnval.length + 1];
470     result[0] = AlignmentAnnotation.SEQUENCE_PROFILE;
471     System.arraycopy(rtnval, 0, result, 1, rtnval.length);
472
473     return result;
474   }
475
476   /**
477    * Extract a sorted extract of cDNA codon profile data. The returned array
478    * contains
479    * 
480    * <pre>
481    *    [profileType, numberOfValues, totalCount, charValue1, percentage1, charValue2, percentage2, ...]
482    * in descending order of percentage value, where the character values encode codon triplets
483    * </pre>
484    * 
485    * @param hashtable
486    * @return
487    */
488   public static int[] extractCdnaProfile(Hashtable hashtable,
489           boolean ignoreGaps)
490   {
491     // this holds #seqs, #ungapped, and then codon count, indexed by encoded
492     // codon triplet
493     int[] codonCounts = (int[]) hashtable.get(PROFILE);
494     int[] sortedCounts = new int[codonCounts.length - 2];
495     System.arraycopy(codonCounts, 2, sortedCounts, 0,
496             codonCounts.length - 2);
497
498     int[] result = new int[3 + 2 * sortedCounts.length];
499     // first value is just the type of profile data
500     result[0] = AlignmentAnnotation.CDNA_PROFILE;
501
502     char[] codons = new char[sortedCounts.length];
503     for (int i = 0; i < codons.length; i++)
504     {
505       codons[i] = (char) i;
506     }
507     QuickSort.sort(sortedCounts, codons);
508     int totalPercentage = 0;
509     int distinctValuesCount = 0;
510     int j = 3;
511     int divisor = ignoreGaps ? codonCounts[1] : codonCounts[0];
512     for (int i = codons.length - 1; i >= 0; i--)
513     {
514       final int codonCount = sortedCounts[i];
515       if (codonCount == 0)
516       {
517         break; // nothing else of interest here
518       }
519       distinctValuesCount++;
520       result[j++] = codons[i];
521       final int percentage = codonCount * 100 / divisor;
522       result[j++] = percentage;
523       totalPercentage += percentage;
524     }
525     result[2] = totalPercentage;
526
527     /*
528      * Just return the non-zero values
529      */
530     // todo next value is redundant if we limit the array to non-zero counts
531     result[1] = distinctValuesCount;
532     return Arrays.copyOfRange(result, 0, j);
533   }
534
535   /**
536    * Compute a consensus for the cDNA coding for a protein alignment.
537    * 
538    * @param alignment
539    *          the protein alignment (which should hold mappings to cDNA
540    *          sequences)
541    * @param hconsensus
542    *          the consensus data stores to be populated (one per column)
543    */
544   public static void calculateCdna(AlignmentI alignment,
545           Hashtable[] hconsensus)
546   {
547     final char gapCharacter = alignment.getGapCharacter();
548     List<AlignedCodonFrame> mappings = alignment.getCodonFrames();
549     if (mappings == null || mappings.isEmpty())
550     {
551       return;
552     }
553
554     int cols = alignment.getWidth();
555     for (int col = 0; col < cols; col++)
556     {
557       // todo would prefer a Java bean for consensus data
558       Hashtable<String, int[]> columnHash = new Hashtable<String, int[]>();
559       // #seqs, #ungapped seqs, counts indexed by (codon encoded + 1)
560       int[] codonCounts = new int[66];
561       codonCounts[0] = alignment.getSequences().size();
562       int ungappedCount = 0;
563       for (SequenceI seq : alignment.getSequences())
564       {
565         if (seq.getCharAt(col) == gapCharacter)
566         {
567           continue;
568         }
569         List<char[]> codons = MappingUtils
570                 .findCodonsFor(seq, col, mappings);
571         for (char[] codon : codons)
572         {
573           int codonEncoded = CodingUtils.encodeCodon(codon);
574           if (codonEncoded >= 0)
575           {
576             codonCounts[codonEncoded + 2]++;
577             ungappedCount++;
578           }
579         }
580       }
581       codonCounts[1] = ungappedCount;
582       // todo: sort values here, save counts and codons?
583       columnHash.put(PROFILE, codonCounts);
584       hconsensus[col] = columnHash;
585     }
586   }
587
588   /**
589    * Derive displayable cDNA consensus annotation from computed consensus data.
590    * 
591    * @param consensusAnnotation
592    *          the annotation row to be populated for display
593    * @param consensusData
594    *          the computed consensus data
595    * @param showProfileLogo
596    *          if true show all symbols present at each position, else only the
597    *          modal value
598    * @param nseqs
599    *          the number of sequences in the alignment
600    */
601   public static void completeCdnaConsensus(
602           AlignmentAnnotation consensusAnnotation,
603           Hashtable[] consensusData, boolean showProfileLogo, int nseqs)
604   {
605     if (consensusAnnotation == null
606             || consensusAnnotation.annotations == null
607             || consensusAnnotation.annotations.length < consensusData.length)
608     {
609       // called with a bad alignment annotation row - wait for it to be
610       // initialised properly
611       return;
612     }
613
614     // ensure codon triplet scales with font size
615     consensusAnnotation.scaleColLabel = true;
616     for (int col = 0; col < consensusData.length; col++)
617     {
618       Hashtable hci = consensusData[col];
619       if (hci == null)
620       {
621         // gapped protein column?
622         continue;
623       }
624       // array holds #seqs, #ungapped, then codon counts indexed by codon
625       final int[] codonCounts = (int[]) hci.get(PROFILE);
626       int totalCount = 0;
627
628       /*
629        * First pass - get total count and find the highest
630        */
631       final char[] codons = new char[codonCounts.length - 2];
632       for (int j = 2; j < codonCounts.length; j++)
633       {
634         final int codonCount = codonCounts[j];
635         codons[j - 2] = (char) (j - 2);
636         totalCount += codonCount;
637       }
638
639       /*
640        * Sort array of encoded codons by count ascending - so the modal value
641        * goes to the end; start by copying the count (dropping the first value)
642        */
643       int[] sortedCodonCounts = new int[codonCounts.length - 2];
644       System.arraycopy(codonCounts, 2, sortedCodonCounts, 0,
645               codonCounts.length - 2);
646       QuickSort.sort(sortedCodonCounts, codons);
647
648       int modalCodonEncoded = codons[codons.length - 1];
649       int modalCodonCount = sortedCodonCounts[codons.length - 1];
650       String modalCodon = String.valueOf(CodingUtils
651               .decodeCodon(modalCodonEncoded));
652       if (sortedCodonCounts.length > 1
653               && sortedCodonCounts[codons.length - 2] == sortedCodonCounts[codons.length - 1])
654       {
655         /*
656          * two or more codons share the modal count
657          */
658         modalCodon = "+";
659       }
660       float pid = sortedCodonCounts[sortedCodonCounts.length - 1] * 100
661               / (float) totalCount;
662
663       /*
664        * todo ? Replace consensus hashtable with sorted arrays of codons and
665        * counts (non-zero only). Include total count in count array [0].
666        */
667
668       /*
669        * Scan sorted array backwards for most frequent values first. Show
670        * repeated values compactly.
671        */
672       StringBuilder mouseOver = new StringBuilder(32);
673       StringBuilder samePercent = new StringBuilder();
674       String percent = null;
675       String lastPercent = null;
676       Format fmt = getPercentageFormat(nseqs);
677
678       for (int j = codons.length - 1; j >= 0; j--)
679       {
680         int codonCount = sortedCodonCounts[j];
681         if (codonCount == 0)
682         {
683           /*
684            * remaining codons are 0% - ignore, but finish off the last one if
685            * necessary
686            */
687           if (samePercent.length() > 0)
688           {
689             mouseOver.append(samePercent).append(": ").append(percent)
690                     .append("% ");
691           }
692           break;
693         }
694         int codonEncoded = codons[j];
695         final int pct = codonCount * 100 / totalCount;
696         String codon = String
697                 .valueOf(CodingUtils.decodeCodon(codonEncoded));
698         percent = fmt == null ? Integer.toString(pct) : fmt.form(pct);
699         if (showProfileLogo || codonCount == modalCodonCount)
700         {
701           if (percent.equals(lastPercent) && j > 0)
702           {
703             samePercent.append(samePercent.length() == 0 ? "" : ", ");
704             samePercent.append(codon);
705           }
706           else
707           {
708             if (samePercent.length() > 0)
709             {
710               mouseOver.append(samePercent).append(": ")
711                       .append(lastPercent).append("% ");
712             }
713             samePercent.setLength(0);
714             samePercent.append(codon);
715           }
716           lastPercent = percent;
717         }
718       }
719
720       consensusAnnotation.annotations[col] = new Annotation(modalCodon,
721               mouseOver.toString(), ' ', pid);
722     }
723   }
724 }