JAL-2629 some tidying of Hmmer commands (wip)
[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.HiddenMarkovModel;
28 import jalview.datamodel.Profile;
29 import jalview.datamodel.ProfileI;
30 import jalview.datamodel.Profiles;
31 import jalview.datamodel.ProfilesI;
32 import jalview.datamodel.ResidueCount;
33 import jalview.datamodel.ResidueCount.SymbolCounts;
34 import jalview.datamodel.SequenceI;
35 import jalview.ext.android.SparseIntArray;
36 import jalview.schemes.ResidueProperties;
37 import jalview.util.Comparison;
38 import jalview.util.Format;
39 import jalview.util.MappingUtils;
40 import jalview.util.QuickSort;
41
42 import java.awt.Color;
43 import java.util.Arrays;
44 import java.util.Hashtable;
45 import java.util.List;
46
47 /**
48  * Takes in a vector or array of sequences and column start and column end and
49  * returns a new Hashtable[] of size maxSeqLength, if Hashtable not supplied.
50  * This class is used extensively in calculating alignment colourschemes that
51  * depend on the amount of conservation in each alignment column.
52  * 
53  */
54 public class AAFrequency
55 {
56   private static final double LOG2 = Math.log(2);
57
58   public static final String PROFILE = "P";
59
60   public static final ProfilesI calculate(List<SequenceI> list, int start,
61           int end)
62   {
63     return calculate(list, start, end, false);
64   }
65
66   public static final ProfilesI calculate(List<SequenceI> sequences,
67           int start, int end, boolean profile)
68   {
69     SequenceI[] seqs = new SequenceI[sequences.size()];
70     int width = 0;
71     synchronized (sequences)
72     {
73       for (int i = 0; i < sequences.size(); i++)
74       {
75         seqs[i] = sequences.get(i);
76         int length = seqs[i].getLength();
77         if (length > width)
78         {
79           width = length;
80         }
81       }
82
83       if (end >= width)
84       {
85         end = width;
86       }
87
88       ProfilesI reply = calculate(seqs, width, start, end, profile);
89       return reply;
90     }
91   }
92
93   /**
94    * Calculate the consensus symbol(s) for each column in the given range.
95    * 
96    * @param sequences
97    * @param width
98    *          the full width of the alignment
99    * @param start
100    *          start column (inclusive, base zero)
101    * @param end
102    *          end column (exclusive)
103    * @param saveFullProfile
104    *          if true, store all symbol counts
105    */
106   public static final ProfilesI calculate(final SequenceI[] sequences,
107           int width, int start, int end, boolean saveFullProfile)
108   {
109     // long now = System.currentTimeMillis();
110     int seqCount = sequences.length;
111     boolean nucleotide = false;
112     int nucleotideCount = 0;
113     int peptideCount = 0;
114
115     ProfileI[] result = new ProfileI[width];
116
117     for (int column = start; column < end; column++)
118     {
119       /*
120        * Apply a heuristic to detect nucleotide data (which can
121        * be counted in more compact arrays); here we test for
122        * more than 90% nucleotide; recheck every 10 columns in case
123        * of misleading data e.g. highly conserved Alanine in peptide!
124        * Mistakenly guessing nucleotide has a small performance cost,
125        * as it will result in counting in sparse arrays.
126        * Mistakenly guessing peptide has a small space cost, 
127        * as it will use a larger than necessary array to hold counts. 
128        */
129       if (nucleotideCount > 100 && column % 10 == 0)
130       {
131         nucleotide = (9 * peptideCount < nucleotideCount);
132       }
133       ResidueCount residueCounts = new ResidueCount(nucleotide);
134
135       for (int row = 0; row < seqCount; row++)
136       {
137         if (sequences[row] == null)
138         {
139           System.err.println(
140                   "WARNING: Consensus skipping null sequence - possible race condition.");
141           continue;
142         }
143         if (sequences[row].getLength() > column)
144         {
145           char c = sequences[row].getCharAt(column);
146           residueCounts.add(c);
147           if (Comparison.isNucleotide(c))
148           {
149             nucleotideCount++;
150           }
151           else if (!Comparison.isGap(c))
152           {
153             peptideCount++;
154           }
155         }
156         else
157         {
158           /*
159            * count a gap if the sequence doesn't reach this column
160            */
161           residueCounts.addGap();
162         }
163       }
164
165       int maxCount = residueCounts.getModalCount();
166       String maxResidue = residueCounts.getResiduesForCount(maxCount);
167       int gapCount = residueCounts.getGapCount();
168       ProfileI profile = new Profile(seqCount, gapCount, maxCount,
169               maxResidue);
170
171       if (saveFullProfile)
172       {
173         profile.setCounts(residueCounts);
174       }
175
176       result[column] = profile;
177     }
178     return new Profiles(result);
179     // long elapsed = System.currentTimeMillis() - now;
180     // System.out.println(elapsed);
181   }
182
183   /**
184    * Returns the full set of profiles for a hidden Markov model. The underlying
185    * data is the raw probabilities of a residue being emitted at each node,
186    * however the profiles returned by this function contain the percentage
187    * chance of a residue emission.
188    * 
189    * @param hmm
190    * @param width
191    *          The width of the Profile array (Profiles) to be returned.
192    * @param start
193    *          The alignment column on which the first profile is based.
194    * @param end
195    *          The alignment column on which the last profile is based.
196    * @param saveFullProfile
197    *          Flag for saving the counts for each profile
198    * @param removeBelowBackground
199    *          Flag for removing any characters with a match emission probability
200    *          less than its background frequency
201    * @return
202    */
203   public static ProfilesI calculateHMMProfiles(final HiddenMarkovModel hmm,
204           int width, int start, int end, boolean saveFullProfile,
205           boolean removeBelowBackground, boolean infoLetterHeight)
206   {
207     ProfileI[] result = new ProfileI[width];
208     char[] symbols = hmm.getSymbols().toCharArray();
209     int symbolCount = symbols.length;
210     for (int column = start; column < end; column++)
211     {
212       ResidueCount counts = new ResidueCount();
213       for (char symbol : symbols)
214       {
215         int value = getAnalogueCount(hmm, column, symbol,
216                 removeBelowBackground, infoLetterHeight);
217         counts.put(symbol, value);
218       }
219       int maxCount = counts.getModalCount();
220       String maxResidue = counts.getResiduesForCount(maxCount);
221       int gapCount = counts.getGapCount();
222       ProfileI profile = new Profile(symbolCount, gapCount, maxCount,
223               maxResidue);
224
225       if (saveFullProfile)
226       {
227         profile.setCounts(counts);
228       }
229
230       result[column] = profile;
231     }
232     return new Profiles(result);
233   }
234
235   /**
236    * Make an estimate of the profile size we are going to compute i.e. how many
237    * different characters may be present in it. Overestimating has a cost of
238    * using more memory than necessary. Underestimating has a cost of needing to
239    * extend the SparseIntArray holding the profile counts.
240    * 
241    * @param profileSizes
242    *          counts of sizes of profiles so far encountered
243    * @return
244    */
245   static int estimateProfileSize(SparseIntArray profileSizes)
246   {
247     if (profileSizes.size() == 0)
248     {
249       return 4;
250     }
251
252     /*
253      * could do a statistical heuristic here e.g. 75%ile
254      * for now just return the largest value
255      */
256     return profileSizes.keyAt(profileSizes.size() - 1);
257   }
258
259   /**
260    * Derive the consensus annotations to be added to the alignment for display.
261    * This does not recompute the raw data, but may be called on a change in
262    * display options, such as 'ignore gaps', which may in turn result in a
263    * change in the derived values.
264    * 
265    * @param consensus
266    *          the annotation row to add annotations to
267    * @param profiles
268    *          the source consensus data
269    * @param startCol
270    *          start column (inclusive)
271    * @param endCol
272    *          end column (exclusive)
273    * @param ignoreGaps
274    *          if true, normalise residue percentages ignoring gaps
275    * @param showSequenceLogo
276    *          if true include all consensus symbols, else just show modal
277    *          residue
278    * @param nseq
279    *          number of sequences
280    */
281   public static void completeConsensus(AlignmentAnnotation consensus,
282           ProfilesI profiles, int startCol, int endCol, boolean ignoreGaps,
283           boolean showSequenceLogo, long nseq)
284   {
285     // long now = System.currentTimeMillis();
286     if (consensus == null || consensus.annotations == null
287             || consensus.annotations.length < endCol)
288     {
289       /*
290        * called with a bad alignment annotation row 
291        * wait for it to be initialised properly
292        */
293       return;
294     }
295
296     for (int i = startCol; i < endCol; i++)
297     {
298       ProfileI profile = profiles.get(i);
299       if (profile == null)
300       {
301         /*
302          * happens if sequences calculated over were 
303          * shorter than alignment width
304          */
305         consensus.annotations[i] = null;
306         return;
307       }
308
309       final int dp = getPercentageDp(nseq);
310
311       float value = profile.getPercentageIdentity(ignoreGaps);
312
313       String description = getTooltip(profile, value, showSequenceLogo,
314               ignoreGaps, dp);
315
316       String modalResidue = profile.getModalResidue();
317       if ("".equals(modalResidue))
318       {
319         modalResidue = "-";
320       }
321       else if (modalResidue.length() > 1)
322       {
323         modalResidue = "+";
324       }
325       consensus.annotations[i] = new Annotation(modalResidue, description,
326               ' ', value);
327     }
328     // long elapsed = System.currentTimeMillis() - now;
329     // System.out.println(-elapsed);
330   }
331
332   /**
333    * Derive the information annotations to be added to the alignment for
334    * display. This does not recompute the raw data, but may be called on a
335    * change in display options, such as 'ignore below background frequency',
336    * which may in turn result in a change in the derived values.
337    * 
338    * @param information
339    *          the annotation row to add annotations to
340    * @param profiles
341    *          the source information data
342    * @param startCol
343    *          start column (inclusive)
344    * @param endCol
345    *          end column (exclusive)
346    * @param ignoreGaps
347    *          if true, normalise residue percentages 
348    * @param showSequenceLogo
349    *          if true include all information symbols, else just show modal
350    *          residue
351    * @param nseq
352    *          number of sequences
353    */
354   public static float completeInformation(AlignmentAnnotation information,
355           ProfilesI profiles, int startCol, int endCol, long nseq,
356           float currentMax)
357   {
358     // long now = System.currentTimeMillis();
359     if (information == null || information.annotations == null
360             || information.annotations.length < endCol)
361     {
362       /*
363        * called with a bad alignment annotation row 
364        * wait for it to be initialised properly
365        */
366       return 0;
367     }
368
369     float max = 0f;
370     SequenceI hmmSeq = information.sequenceRef;
371     HiddenMarkovModel hmm = hmmSeq.getHMM();
372
373     for (int column = startCol; column < endCol; column++)
374     {
375       ProfileI profile = profiles.get(column);
376       if (profile == null)
377       {
378         /*
379          * happens if sequences calculated over were 
380          * shorter than alignment width
381          */
382         information.annotations[column] = null;
383         return 0f;
384       }
385       
386       float value = hmm.getInformationContent(column);
387       if (!Float.isNaN(value))
388       {
389         max = Math.max(max, value);
390       }
391
392       String description = Float.isNaN(value) ? null
393               : String.format("%.4f bits", value);
394       information.annotations[column] = new Annotation(
395               Character.toString(Character
396                       .toUpperCase(hmm.getConsensusAtAlignColumn(column))),
397               description, ' ', value);
398     }
399
400     max = Math.max(max, currentMax);
401     information.graphMax = max;
402     return max;
403   }
404
405   /**
406    * Derive the gap count annotation row.
407    * 
408    * @param gaprow
409    *          the annotation row to add annotations to
410    * @param profiles
411    *          the source consensus data
412    * @param startCol
413    *          start column (inclusive)
414    * @param endCol
415    *          end column (exclusive)
416    */
417   public static void completeGapAnnot(AlignmentAnnotation gaprow,
418           ProfilesI profiles, int startCol, int endCol, long nseq)
419   {
420     if (gaprow == null || gaprow.annotations == null
421             || gaprow.annotations.length < endCol)
422     {
423       /*
424        * called with a bad alignment annotation row 
425        * wait for it to be initialised properly
426        */
427       return;
428     }
429     // always set ranges again
430     gaprow.graphMax = nseq;
431     gaprow.graphMin = 0;
432     double scale = 0.8 / nseq;
433     for (int i = startCol; i < endCol; i++)
434     {
435       ProfileI profile = profiles.get(i);
436       if (profile == null)
437       {
438         /*
439          * happens if sequences calculated over were 
440          * shorter than alignment width
441          */
442         gaprow.annotations[i] = null;
443         return;
444       }
445
446       final int gapped = profile.getNonGapped();
447
448       String description = "" + gapped;
449
450       gaprow.annotations[i] = new Annotation("", description, '\0', gapped,
451               jalview.util.ColorUtils.bleachColour(Color.DARK_GRAY,
452                       (float) scale * gapped));
453     }
454   }
455
456   /**
457    * Returns a tooltip showing either
458    * <ul>
459    * <li>the full profile (percentages of all residues present), if
460    * showSequenceLogo is true, or</li>
461    * <li>just the modal (most common) residue(s), if showSequenceLogo is
462    * false</li>
463    * </ul>
464    * Percentages are as a fraction of all sequence, or only ungapped sequences
465    * if ignoreGaps is true.
466    * 
467    * @param profile
468    * @param pid
469    * @param showSequenceLogo
470    * @param ignoreGaps
471    * @param dp
472    *          the number of decimal places to format percentages to
473    * @return
474    */
475   static String getTooltip(ProfileI profile, float pid,
476           boolean showSequenceLogo, boolean ignoreGaps, int dp)
477   {
478     ResidueCount counts = profile.getCounts();
479
480     String description = null;
481     if (counts != null && showSequenceLogo)
482     {
483       int normaliseBy = ignoreGaps ? profile.getNonGapped()
484               : profile.getHeight();
485       description = counts.getTooltip(normaliseBy, dp);
486     }
487     else
488     {
489       StringBuilder sb = new StringBuilder(64);
490       String maxRes = profile.getModalResidue();
491       if (maxRes.length() > 1)
492       {
493         sb.append("[").append(maxRes).append("]");
494       }
495       else
496       {
497         sb.append(maxRes);
498       }
499       if (maxRes.length() > 0)
500       {
501         sb.append(" ");
502         Format.appendPercentage(sb, pid, dp);
503         sb.append("%");
504       }
505       description = sb.toString();
506     }
507     return description;
508   }
509
510   /**
511    * Returns the sorted profile for the given consensus data. The returned array
512    * contains
513    * 
514    * <pre>
515    *    [profileType, numberOfValues, nonGapCount, charValue1, percentage1, charValue2, percentage2, ...]
516    * in descending order of percentage value
517    * </pre>
518    * 
519    * @param profile
520    *          the data object from which to extract and sort values
521    * @param ignoreGaps
522    *          if true, only non-gapped values are included in percentage
523    *          calculations
524    * @return
525    */
526   public static int[] extractProfile(ProfileI profile, boolean ignoreGaps)
527   {
528     int[] rtnval = new int[64];
529     ResidueCount counts = profile.getCounts();
530     if (counts == null)
531     {
532       return null;
533     }
534
535     SymbolCounts symbolCounts = counts.getSymbolCounts();
536     char[] symbols = symbolCounts.symbols;
537     int[] values = symbolCounts.values;
538     QuickSort.sort(values, symbols);
539     int nextArrayPos = 2;
540     int totalPercentage = 0;
541     final int divisor = ignoreGaps ? profile.getNonGapped()
542             : profile.getHeight();
543
544     /*
545      * traverse the arrays in reverse order (highest counts first)
546      */
547     for (int i = symbols.length - 1; i >= 0; i--)
548     {
549       int theChar = symbols[i];
550       int charCount = values[i];
551
552       rtnval[nextArrayPos++] = theChar;
553       final int percentage = (charCount * 100) / divisor;
554       rtnval[nextArrayPos++] = percentage;
555       totalPercentage += percentage;
556     }
557     rtnval[0] = symbols.length;
558     rtnval[1] = totalPercentage;
559     int[] result = new int[rtnval.length + 1];
560     result[0] = AlignmentAnnotation.SEQUENCE_PROFILE;
561     System.arraycopy(rtnval, 0, result, 1, rtnval.length);
562
563     return result;
564   }
565
566
567   /**
568    * Extract a sorted extract of cDNA codon profile data. The returned array
569    * contains
570    * 
571    * <pre>
572    *    [profileType, numberOfValues, totalCount, charValue1, percentage1, charValue2, percentage2, ...]
573    * in descending order of percentage value, where the character values encode codon triplets
574    * </pre>
575    * 
576    * @param hashtable
577    * @return
578    */
579   public static int[] extractCdnaProfile(Hashtable hashtable,
580           boolean ignoreGaps)
581   {
582     // this holds #seqs, #ungapped, and then codon count, indexed by encoded
583     // codon triplet
584     int[] codonCounts = (int[]) hashtable.get(PROFILE);
585     int[] sortedCounts = new int[codonCounts.length - 2];
586     System.arraycopy(codonCounts, 2, sortedCounts, 0,
587             codonCounts.length - 2);
588
589     int[] result = new int[3 + 2 * sortedCounts.length];
590     // first value is just the type of profile data
591     result[0] = AlignmentAnnotation.CDNA_PROFILE;
592
593     char[] codons = new char[sortedCounts.length];
594     for (int i = 0; i < codons.length; i++)
595     {
596       codons[i] = (char) i;
597     }
598     QuickSort.sort(sortedCounts, codons);
599     int totalPercentage = 0;
600     int distinctValuesCount = 0;
601     int j = 3;
602     int divisor = ignoreGaps ? codonCounts[1] : codonCounts[0];
603     for (int i = codons.length - 1; i >= 0; i--)
604     {
605       final int codonCount = sortedCounts[i];
606       if (codonCount == 0)
607       {
608         break; // nothing else of interest here
609       }
610       distinctValuesCount++;
611       result[j++] = codons[i];
612       final int percentage = codonCount * 100 / divisor;
613       result[j++] = percentage;
614       totalPercentage += percentage;
615     }
616     result[2] = totalPercentage;
617
618     /*
619      * Just return the non-zero values
620      */
621     // todo next value is redundant if we limit the array to non-zero counts
622     result[1] = distinctValuesCount;
623     return Arrays.copyOfRange(result, 0, j);
624   }
625
626   /**
627    * Compute a consensus for the cDNA coding for a protein alignment.
628    * 
629    * @param alignment
630    *          the protein alignment (which should hold mappings to cDNA
631    *          sequences)
632    * @param hconsensus
633    *          the consensus data stores to be populated (one per column)
634    */
635   public static void calculateCdna(AlignmentI alignment,
636           Hashtable[] hconsensus)
637   {
638     final char gapCharacter = alignment.getGapCharacter();
639     List<AlignedCodonFrame> mappings = alignment.getCodonFrames();
640     if (mappings == null || mappings.isEmpty())
641     {
642       return;
643     }
644
645     int cols = alignment.getWidth();
646     for (int col = 0; col < cols; col++)
647     {
648       // todo would prefer a Java bean for consensus data
649       Hashtable<String, int[]> columnHash = new Hashtable<>();
650       // #seqs, #ungapped seqs, counts indexed by (codon encoded + 1)
651       int[] codonCounts = new int[66];
652       codonCounts[0] = alignment.getSequences().size();
653       int ungappedCount = 0;
654       for (SequenceI seq : alignment.getSequences())
655       {
656         if (seq.getCharAt(col) == gapCharacter)
657         {
658           continue;
659         }
660         List<char[]> codons = MappingUtils.findCodonsFor(seq, col,
661                 mappings);
662         for (char[] codon : codons)
663         {
664           int codonEncoded = CodingUtils.encodeCodon(codon);
665           if (codonEncoded >= 0)
666           {
667             codonCounts[codonEncoded + 2]++;
668             ungappedCount++;
669           }
670         }
671       }
672       codonCounts[1] = ungappedCount;
673       // todo: sort values here, save counts and codons?
674       columnHash.put(PROFILE, codonCounts);
675       hconsensus[col] = columnHash;
676     }
677   }
678
679   /**
680    * Derive displayable cDNA consensus annotation from computed consensus data.
681    * 
682    * @param consensusAnnotation
683    *          the annotation row to be populated for display
684    * @param consensusData
685    *          the computed consensus data
686    * @param showProfileLogo
687    *          if true show all symbols present at each position, else only the
688    *          modal value
689    * @param nseqs
690    *          the number of sequences in the alignment
691    */
692   public static void completeCdnaConsensus(
693           AlignmentAnnotation consensusAnnotation,
694           Hashtable[] consensusData, boolean showProfileLogo, int nseqs)
695   {
696     if (consensusAnnotation == null
697             || consensusAnnotation.annotations == null
698             || consensusAnnotation.annotations.length < consensusData.length)
699     {
700       // called with a bad alignment annotation row - wait for it to be
701       // initialised properly
702       return;
703     }
704
705     // ensure codon triplet scales with font size
706     consensusAnnotation.scaleColLabel = true;
707     for (int col = 0; col < consensusData.length; col++)
708     {
709       Hashtable hci = consensusData[col];
710       if (hci == null)
711       {
712         // gapped protein column?
713         continue;
714       }
715       // array holds #seqs, #ungapped, then codon counts indexed by codon
716       final int[] codonCounts = (int[]) hci.get(PROFILE);
717       int totalCount = 0;
718
719       /*
720        * First pass - get total count and find the highest
721        */
722       final char[] codons = new char[codonCounts.length - 2];
723       for (int j = 2; j < codonCounts.length; j++)
724       {
725         final int codonCount = codonCounts[j];
726         codons[j - 2] = (char) (j - 2);
727         totalCount += codonCount;
728       }
729
730       /*
731        * Sort array of encoded codons by count ascending - so the modal value
732        * goes to the end; start by copying the count (dropping the first value)
733        */
734       int[] sortedCodonCounts = new int[codonCounts.length - 2];
735       System.arraycopy(codonCounts, 2, sortedCodonCounts, 0,
736               codonCounts.length - 2);
737       QuickSort.sort(sortedCodonCounts, codons);
738
739       int modalCodonEncoded = codons[codons.length - 1];
740       int modalCodonCount = sortedCodonCounts[codons.length - 1];
741       String modalCodon = String
742               .valueOf(CodingUtils.decodeCodon(modalCodonEncoded));
743       if (sortedCodonCounts.length > 1 && sortedCodonCounts[codons.length
744               - 2] == sortedCodonCounts[codons.length - 1])
745       {
746         /*
747          * two or more codons share the modal count
748          */
749         modalCodon = "+";
750       }
751       float pid = sortedCodonCounts[sortedCodonCounts.length - 1] * 100
752               / (float) totalCount;
753
754       /*
755        * todo ? Replace consensus hashtable with sorted arrays of codons and
756        * counts (non-zero only). Include total count in count array [0].
757        */
758
759       /*
760        * Scan sorted array backwards for most frequent values first. Show
761        * repeated values compactly.
762        */
763       StringBuilder mouseOver = new StringBuilder(32);
764       StringBuilder samePercent = new StringBuilder();
765       String percent = null;
766       String lastPercent = null;
767       int percentDecPl = getPercentageDp(nseqs);
768
769       for (int j = codons.length - 1; j >= 0; j--)
770       {
771         int codonCount = sortedCodonCounts[j];
772         if (codonCount == 0)
773         {
774           /*
775            * remaining codons are 0% - ignore, but finish off the last one if
776            * necessary
777            */
778           if (samePercent.length() > 0)
779           {
780             mouseOver.append(samePercent).append(": ").append(percent)
781                     .append("% ");
782           }
783           break;
784         }
785         int codonEncoded = codons[j];
786         final int pct = codonCount * 100 / totalCount;
787         String codon = String
788                 .valueOf(CodingUtils.decodeCodon(codonEncoded));
789         StringBuilder sb = new StringBuilder();
790         Format.appendPercentage(sb, pct, percentDecPl);
791         percent = sb.toString();
792         if (showProfileLogo || codonCount == modalCodonCount)
793         {
794           if (percent.equals(lastPercent) && j > 0)
795           {
796             samePercent.append(samePercent.length() == 0 ? "" : ", ");
797             samePercent.append(codon);
798           }
799           else
800           {
801             if (samePercent.length() > 0)
802             {
803               mouseOver.append(samePercent).append(": ").append(lastPercent)
804                       .append("% ");
805             }
806             samePercent.setLength(0);
807             samePercent.append(codon);
808           }
809           lastPercent = percent;
810         }
811       }
812
813       consensusAnnotation.annotations[col] = new Annotation(modalCodon,
814               mouseOver.toString(), ' ', pid);
815     }
816   }
817
818   /**
819    * Returns the number of decimal places to show for profile percentages. For
820    * less than 100 sequences, returns zero (the integer percentage value will be
821    * displayed). For 100-999 sequences, returns 1, for 1000-9999 returns 2, etc.
822    * 
823    * @param nseq
824    * @return
825    */
826   protected static int getPercentageDp(long nseq)
827   {
828     int scale = 0;
829     while (nseq >= 100)
830     {
831       scale++;
832       nseq /= 10;
833     }
834     return scale;
835   }
836
837   /**
838    * Returns the sorted HMM profile for the given column of the alignment. The
839    * returned array contains
840    * 
841    * <pre>
842    *    [profileType=0, numberOfValues, 100, charValue1, percentage1, charValue2, percentage2, ...]
843    * in descending order of percentage value
844    * </pre>
845    * 
846    * @param hmm
847    * @param column
848    * @param removeBelowBackground
849    *          if true, ignores residues with probability less than their
850    *          background frequency
851    * @param infoHeight
852    *          if true, uses the log ratio 'information' measure to scale the
853    *          value
854    * @return
855    */
856   public static int[] extractHMMProfile(HiddenMarkovModel hmm, int column,
857           boolean removeBelowBackground, boolean infoHeight)
858   {
859     if (hmm == null)
860     {
861       return null;
862     }
863     String alphabet = hmm.getSymbols();
864     int size = alphabet.length();
865     char symbols[] = new char[size];
866     int values[] = new int[size];
867     int totalCount = 0;
868
869     for (int i = 0; i < size; i++)
870     {
871       char symbol = alphabet.charAt(i);
872       symbols[i] = symbol;
873       int value = getAnalogueCount(hmm, column, symbol,
874               removeBelowBackground, infoHeight);
875       values[i] = value;
876       totalCount += value;
877     }
878
879     /*
880      * sort symbols by increasing emission probability
881      */
882     QuickSort.sort(values, symbols);
883
884     int[] profile = new int[3 + size * 2];
885
886     profile[0] = AlignmentAnnotation.SEQUENCE_PROFILE;
887     profile[1] = size;
888     profile[2] = 100;
889
890     /*
891      * order symbol/count profile by decreasing emission probability
892      */
893     if (totalCount != 0)
894     {
895       int arrayPos = 3;
896       for (int k = size - 1; k >= 0; k--)
897       {
898         Float percentage;
899         int value = values[k];
900         if (removeBelowBackground)
901         {
902           percentage = ((float) value) / totalCount * 100f;
903         }
904         else
905         {
906           percentage = value / 100f;
907         }
908         int intPercent = Math.round(percentage);
909         profile[arrayPos] = symbols[k];
910         profile[arrayPos + 1] = intPercent;
911         arrayPos += 2;
912       }
913     }
914     return profile;
915   }
916
917   /**
918    * Converts the emission probability of a residue at a column in the alignment
919    * to a 'count', suitable for rendering as an annotation value
920    * 
921    * @param hmm
922    * @param column
923    * @param symbol
924    * @param removeBelowBackground
925    *          if true, returns 0 for any symbol with a match emission
926    *          probability less than the background frequency
927    * @infoHeight if true, uses the log ratio 'information content' to scale the
928    *             value
929    * @return
930    */
931   static int getAnalogueCount(HiddenMarkovModel hmm, int column,
932           char symbol, boolean removeBelowBackground, boolean infoHeight)
933   {
934     double value = hmm.getMatchEmissionProbability(column, symbol);
935     double freq = ResidueProperties.backgroundFrequencies
936             .get(hmm.getAlphabetType()).get(symbol);
937     if (value < freq && removeBelowBackground)
938     {
939       return 0;
940     }
941
942     if (infoHeight)
943     {
944       value = value * (Math.log(value / freq) / LOG2);
945     }
946
947     value = value * 10000d;
948     return Math.round((float) value);
949   }
950 }