79fa3d6189862332d1bf3eac235a0878cd41e14a
[jalview.git] / src / jalview / analysis / Conservation.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.analysis.scoremodels.ScoreModels;
24 import jalview.datamodel.AlignmentAnnotation;
25 import jalview.datamodel.Annotation;
26 import jalview.datamodel.ResidueCount;
27 import jalview.datamodel.ResidueCount.SymbolCounts;
28 import jalview.datamodel.Sequence;
29 import jalview.datamodel.SequenceI;
30 import jalview.schemes.ResidueProperties;
31 import jalview.util.Comparison;
32
33 import java.awt.Color;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Map.Entry;
37 import java.util.SortedMap;
38 import java.util.TreeMap;
39 import java.util.Vector;
40
41 /**
42  * Calculates conservation values for a given set of sequences
43  */
44 public class Conservation
45 {
46   /*
47    * need to have a minimum of 3% of sequences with a residue
48    * for it to be included in the conservation calculation
49    */
50   private static final int THRESHOLD_PERCENT = 3;
51
52   private static final int TOUPPERCASE = 'a' - 'A';
53
54   SequenceI[] sequences;
55
56   int start;
57
58   int end;
59
60   Vector<int[]> seqNums; // vector of int vectors where first is sequence
61                          // checksum
62
63   int maxLength = 0; // used by quality calcs
64
65   boolean seqNumsChanged = false; // updated after any change via calcSeqNum;
66
67   /*
68    * a map per column with {property, conservation} where conservation value is
69    * 1 (property is conserved), 0 (absence of property is conserved) or -1
70    * (property is not conserved i.e. column has residues with and without it)
71    */
72   Map<String, Integer>[] total;
73
74   boolean canonicaliseAa = true; // if true then conservation calculation will
75
76   // map all symbols to canonical aa numbering
77   // rather than consider conservation of that
78   // symbol
79
80   /** Stores calculated quality values */
81   private Vector<Double> quality;
82
83   /** Stores maximum and minimum values of quality values */
84   private double[] qualityRange = new double[2];
85
86   private Sequence consSequence;
87
88   /*
89    * percentage of residues in a column to qualify for counting conservation
90    */
91   private int threshold;
92
93   private String name = "";
94
95   private int[][] cons2;
96
97   private String[] consSymbs;
98
99   /**
100    * Constructor using default threshold of 3%
101    * 
102    * @param name
103    *          Name of conservation
104    * @param sequences
105    *          sequences to be used in calculation
106    * @param start
107    *          start residue position
108    * @param end
109    *          end residue position
110    */
111   public Conservation(String name, List<SequenceI> sequences, int start,
112           int end)
113   {
114     this(name, THRESHOLD_PERCENT, sequences, start, end);
115   }
116
117   /**
118    * Constructor
119    * 
120    * @param name
121    *          Name of conservation
122    * @param threshold
123    *          percentage of sequences at or below which property conservation is
124    *          ignored
125    * @param sequences
126    *          sequences to be used in calculation
127    * @param start
128    *          start column position
129    * @param end
130    *          end column position
131    */
132   public Conservation(String name, int threshold,
133           List<SequenceI> sequences, int start, int end)
134   {
135     this.name = name;
136     this.threshold = threshold;
137     this.start = start;
138     this.end = end;
139
140     maxLength = end - start + 1; // default width includes bounds of
141     // calculation
142
143     int s, sSize = sequences.size();
144     SequenceI[] sarray = new SequenceI[sSize];
145     this.sequences = sarray;
146     try
147     {
148       for (s = 0; s < sSize; s++)
149       {
150         sarray[s] = sequences.get(s);
151         if (sarray[s].getLength() > maxLength)
152         {
153           maxLength = sarray[s].getLength();
154         }
155       }
156     } catch (ArrayIndexOutOfBoundsException ex)
157     {
158       // bail - another thread has modified the sequence array, so the current
159       // calculation is probably invalid.
160       this.sequences = new SequenceI[0];
161       maxLength = 0;
162     }
163   }
164
165   /**
166    * Translate sequence i into a numerical representation and store it in the
167    * i'th position of the seqNums array.
168    * 
169    * @param i
170    */
171   private void calcSeqNum(int i)
172   {
173     String sq = null; // for dumb jbuilder not-inited exception warning
174     int[] sqnum = null;
175
176     int sSize = sequences.length;
177
178     if ((i > -1) && (i < sSize))
179     {
180       sq = sequences[i].getSequenceAsString();
181
182       if (seqNums.size() <= i)
183       {
184         seqNums.addElement(new int[sq.length() + 1]);
185       }
186
187       if (sq.hashCode() != seqNums.elementAt(i)[0])
188       {
189         int j;
190         int len;
191         seqNumsChanged = true;
192         len = sq.length();
193
194         if (maxLength < len)
195         {
196           maxLength = len;
197         }
198
199         sqnum = new int[len + 1]; // better to always make a new array -
200         // sequence can change its length
201         sqnum[0] = sq.hashCode();
202
203         for (j = 1; j <= len; j++)
204         {
205           sqnum[j] = jalview.schemes.ResidueProperties.aaIndex[sq
206                   .charAt(j - 1)];
207         }
208
209         seqNums.setElementAt(sqnum, i);
210       }
211       else
212       {
213         System.out.println("SEQUENCE HAS BEEN DELETED!!!");
214       }
215     }
216     else
217     {
218       // JBPNote INFO level debug
219       System.err
220               .println("ERROR: calcSeqNum called with out of range sequence index for Alignment\n");
221     }
222   }
223
224   /**
225    * Calculates the conservation values for given set of sequences
226    */
227   public void calculate()
228   {
229     int height = sequences.length;
230
231     total = new Map[maxLength];
232
233     for (int column = start; column <= end; column++)
234     {
235       ResidueCount values = countResidues(column);
236
237       /*
238        * percentage count at or below which we ignore residues
239        */
240       int thresh = (threshold * height) / 100;
241
242       /*
243        * check observed residues in column and record whether each 
244        * physico-chemical property is conserved (+1), absence conserved (0),
245        * or not conserved (-1)
246        * Using TreeMap means properties are displayed in alphabetical order
247        */
248       SortedMap<String, Integer> resultHash = new TreeMap<String, Integer>();
249       SymbolCounts symbolCounts = values.getSymbolCounts();
250       char[] symbols = symbolCounts.symbols;
251       int[] counts = symbolCounts.values;
252       for (int j = 0; j < symbols.length; j++)
253       {
254         char c = symbols[j];
255         if (counts[j] > thresh)
256         {
257           recordConservation(resultHash, String.valueOf(c));
258         }
259       }
260       if (values.getGapCount() > thresh)
261       {
262         recordConservation(resultHash, "-");
263       }
264
265       if (total.length > 0)
266       {
267         total[column - start] = resultHash;
268       }
269     }
270   }
271
272   /**
273    * Updates the conservation results for an observed residue
274    * 
275    * @param resultMap
276    *          a map of {property, conservation} where conservation value is +1
277    *          (all residues have the property), 0 (no residue has the property)
278    *          or -1 (some do, some don't)
279    * @param res
280    */
281   protected static void recordConservation(Map<String, Integer> resultMap,
282           String res)
283   {
284     res = res.toUpperCase();
285     for (Entry<String, Map<String, Integer>> property : ResidueProperties.propHash
286             .entrySet())
287     {
288       String propertyName = property.getKey();
289       Integer residuePropertyValue = property.getValue().get(res);
290
291       if (!resultMap.containsKey(propertyName))
292       {
293         /*
294          * first time we've seen this residue - note whether it has this property
295          */
296         if (residuePropertyValue != null)
297         {
298           resultMap.put(propertyName, residuePropertyValue);
299         }
300         else
301         {
302           /*
303            * unrecognised residue - use default value for property
304            */
305           resultMap.put(propertyName, property.getValue().get("-"));
306         }
307       }
308       else
309       {
310         Integer currentResult = resultMap.get(propertyName);
311         if (currentResult.intValue() != -1
312                 && !currentResult.equals(residuePropertyValue))
313         {
314           /*
315            * property is unconserved - residues seen both with and without it
316            */
317           resultMap.put(propertyName, Integer.valueOf(-1));
318         }
319       }
320     }
321   }
322
323   /**
324    * Counts residues (upper-cased) and gaps in the given column
325    * 
326    * @param column
327    * @return
328    */
329   protected ResidueCount countResidues(int column)
330   {
331     ResidueCount values = new ResidueCount(false);
332
333     for (int row = 0; row < sequences.length; row++)
334     {
335       if (sequences[row].getLength() > column)
336       {
337         char c = sequences[row].getCharAt(column);
338         if (canonicaliseAa)
339         {
340           int index = ResidueProperties.aaIndex[c];
341           c = index > 20 ? '-' : ResidueProperties.aa[index].charAt(0);
342         }
343         else
344         {
345           c = toUpperCase(c);
346         }
347         if (Comparison.isGap(c))
348         {
349           values.addGap();
350         }
351         else
352         {
353           values.add(c);
354         }
355       }
356       else
357       {
358         values.addGap();
359       }
360     }
361     return values;
362   }
363
364   /**
365    * Counts conservation and gaps for a column of the alignment
366    * 
367    * @return { 1 if fully conserved, else 0, gap count }
368    */
369   public int[] countConservationAndGaps(int column)
370   {
371     int gapCount = 0;
372     boolean fullyConserved = true;
373     int iSize = sequences.length;
374
375     if (iSize == 0)
376     {
377       return new int[] { 0, 0 };
378     }
379
380     char lastRes = '0';
381     for (int i = 0; i < iSize; i++)
382     {
383       if (column >= sequences[i].getLength())
384       {
385         gapCount++;
386         continue;
387       }
388
389       char c = sequences[i].getCharAt(column); // gaps do not have upper/lower case
390
391       if (Comparison.isGap((c)))
392       {
393         gapCount++;
394       }
395       else
396       {
397         c = toUpperCase(c);
398         if (lastRes == '0')
399         {
400           lastRes = c;
401         }
402         if (c != lastRes)
403         {
404           fullyConserved = false;
405         }
406       }
407     }
408
409     int[] r = new int[] { fullyConserved ? 1 : 0, gapCount };
410     return r;
411   }
412
413   /**
414    * Returns the upper-cased character if between 'a' and 'z', else the
415    * unchanged value
416    * 
417    * @param c
418    * @return
419    */
420   char toUpperCase(char c)
421   {
422     if ('a' <= c && c <= 'z')
423     {
424       c -= TOUPPERCASE;
425     }
426     return c;
427   }
428
429   /**
430    * Calculates the conservation sequence
431    * 
432    * @param positiveOnly
433    *          if true, calculate positive conservation; else calculate both
434    *          positive and negative conservation
435    * @param maxPercentageGaps
436    *          the percentage of gaps in a column, at or above which no
437    *          conservation is asserted
438    */
439   public void verdict(boolean positiveOnly, float maxPercentageGaps)
440   {
441     // TODO call this at the end of calculate(), should not be a public method
442
443     StringBuilder consString = new StringBuilder(end);
444
445     // NOTE THIS SHOULD CHECK IF THE CONSEQUENCE ALREADY
446     // EXISTS AND NOT OVERWRITE WITH '-', BUT THIS CASE
447     // DOES NOT EXIST IN JALVIEW 2.1.2
448     for (int i = 0; i < start; i++)
449     {
450       consString.append('-');
451     }
452     consSymbs = new String[end - start + 1];
453     for (int i = start; i <= end; i++)
454     {
455       int[] gapcons = countConservationAndGaps(i);
456       boolean fullyConserved = gapcons[0] == 1;
457       int totGaps = gapcons[1];
458       float pgaps = (totGaps * 100f) / sequences.length;
459
460       if (maxPercentageGaps > pgaps)
461       {
462         Map<String, Integer> resultHash = total[i - start];
463         int count = 0;
464         StringBuilder positives = new StringBuilder(64);
465         StringBuilder negatives = new StringBuilder(32);
466         for (String type : resultHash.keySet())
467         {
468           int result = resultHash.get(type).intValue();
469           if (result == -1)
470           {
471             /*
472              * not conserved (present or absent)
473              */
474             continue;
475           }
476           count++;
477           if (result == 1)
478           {
479             /*
480              * positively conserved property (all residues have it)
481              */
482             positives.append(positives.length() == 0 ? "" : " ");
483             positives.append(type);
484           }
485           if (result == 0 && !positiveOnly)
486           {
487             /*
488              * absense of property is conserved (all residues lack it)
489              */
490             negatives.append(negatives.length() == 0 ? "" : " ");
491             negatives.append("!").append(type);
492           }
493         }
494         if (negatives.length() > 0)
495         {
496           positives.append(" ").append(negatives);
497         }
498         consSymbs[i - start] = positives.toString();
499
500         if (count < 10)
501         {
502           consString.append(count); // Conserved props!=Identity
503         }
504         else
505         {
506           consString.append(fullyConserved ? "*" : "+");
507         }
508       }
509       else
510       {
511         consString.append('-');
512       }
513     }
514
515     consSequence = new Sequence(name, consString.toString(), start, end);
516   }
517
518   /**
519    * 
520    * 
521    * @return Conservation sequence
522    */
523   public SequenceI getConsSequence()
524   {
525     return consSequence;
526   }
527
528   // From Alignment.java in jalview118
529   public void findQuality()
530   {
531     findQuality(0, maxLength - 1);
532   }
533
534   /**
535    * DOCUMENT ME!
536    */
537   private void percentIdentity2()
538   {
539     seqNums = new Vector<int[]>();
540     // calcSeqNum(s);
541     int i = 0, iSize = sequences.length;
542     // Do we need to calculate this again?
543     for (i = 0; i < iSize; i++)
544     {
545       calcSeqNum(i);
546     }
547
548     if ((cons2 == null) || seqNumsChanged)
549     {
550       cons2 = new int[maxLength][24];
551
552       // Initialize the array
553       for (int j = 0; j < 24; j++)
554       {
555         for (i = 0; i < maxLength; i++)
556         {
557           cons2[i][j] = 0;
558         }
559       }
560
561       int[] sqnum;
562       int j = 0;
563
564       while (j < sequences.length)
565       {
566         sqnum = seqNums.elementAt(j);
567
568         for (i = 1; i < sqnum.length; i++)
569         {
570           cons2[i - 1][sqnum[i]]++;
571         }
572
573         for (i = sqnum.length - 1; i < maxLength; i++)
574         {
575           cons2[i][23]++; // gap count
576         }
577
578         j++;
579       }
580
581       // unnecessary ?
582
583       /*
584        * for (int i=start; i <= end; i++) { int max = -1000; int maxi = -1; int
585        * maxj = -1;
586        * 
587        * for (int j=0;j<24;j++) { if (cons2[i][j] > max) { max = cons2[i][j];
588        * maxi = i; maxj = j; } } }
589        */
590     }
591   }
592
593   /**
594    * Calculates the quality of the set of sequences
595    * 
596    * @param startRes
597    *          Start residue
598    * @param endRes
599    *          End residue
600    */
601   public void findQuality(int startRes, int endRes)
602   {
603     quality = new Vector<Double>();
604
605     double max = -10000;
606     float[][] BLOSUM62 = ScoreModels.getInstance().getBlosum62()
607             .getMatrix();
608
609     // Loop over columns // JBPNote Profiling info
610     // long ts = System.currentTimeMillis();
611     // long te = System.currentTimeMillis();
612     percentIdentity2();
613
614     int size = seqNums.size();
615     int[] lengths = new int[size];
616     double tot, bigtot, sr, tmp;
617     double[] x, xx;
618     int l, j, i, ii, i2, k, seqNum;
619
620     for (l = 0; l < size; l++)
621     {
622       lengths[l] = seqNums.elementAt(l).length - 1;
623     }
624
625     for (j = startRes; j <= endRes; j++)
626     {
627       bigtot = 0;
628
629       // First Xr = depends on column only
630       x = new double[24];
631
632       for (ii = 0; ii < 24; ii++)
633       {
634         x[ii] = 0;
635
636         for (i2 = 0; i2 < 24; i2++)
637         {
638           x[ii] += (((double) cons2[j][i2] * BLOSUM62[ii][i2]) + 4);
639         }
640
641         x[ii] /= size;
642       }
643
644       // Now calculate D for each position and sum
645       for (k = 0; k < size; k++)
646       {
647         tot = 0;
648         xx = new double[24];
649         seqNum = (j < lengths[k]) ? seqNums.elementAt(k)[j + 1] : 23; // Sequence,
650                                                                       // or gap
651                                                                       // at the
652                                                                       // end
653
654         // This is a loop over r
655         for (i = 0; i < 23; i++)
656         {
657           sr = 0;
658
659           sr = (double) BLOSUM62[i][seqNum] + 4;
660
661           // Calculate X with another loop over residues
662           // System.out.println("Xi " + i + " " + x[i] + " " + sr);
663           xx[i] = x[i] - sr;
664
665           tot += (xx[i] * xx[i]);
666         }
667
668         bigtot += Math.sqrt(tot);
669       }
670
671       // This is the quality for one column
672       if (max < bigtot)
673       {
674         max = bigtot;
675       }
676
677       // bigtot = bigtot * (size-cons2[j][23])/size;
678       quality.addElement(new Double(bigtot));
679
680       // Need to normalize by gaps
681     }
682
683     double newmax = -10000;
684
685     for (j = startRes; j <= endRes; j++)
686     {
687       tmp = quality.elementAt(j).doubleValue();
688       tmp = ((max - tmp) * (size - cons2[j][23])) / size;
689
690       // System.out.println(tmp+ " " + j);
691       quality.setElementAt(new Double(tmp), j);
692
693       if (tmp > newmax)
694       {
695         newmax = tmp;
696       }
697     }
698
699     // System.out.println("Quality " + s);
700     qualityRange[0] = 0D;
701     qualityRange[1] = newmax;
702   }
703
704   /**
705    * Complete the given consensus and quuality annotation rows. Note: currently
706    * this method will enlarge the given annotation row if it is too small,
707    * otherwise will leave its length unchanged.
708    * 
709    * @param conservation
710    *          conservation annotation row
711    * @param quality2
712    *          (optional - may be null)
713    * @param istart
714    *          first column for conservation
715    * @param alWidth
716    *          extent of conservation
717    */
718   public void completeAnnotations(AlignmentAnnotation conservation,
719           AlignmentAnnotation quality2, int istart, int alWidth)
720   {
721     char[] sequence = getConsSequence().getSequence();
722     float minR;
723     float minG;
724     float minB;
725     float maxR;
726     float maxG;
727     float maxB;
728     minR = 0.3f;
729     minG = 0.0f;
730     minB = 0f;
731     maxR = 1.0f - minR;
732     maxG = 0.9f - minG;
733     maxB = 0f - minB; // scalable range for colouring both Conservation and
734     // Quality
735
736     float min = 0f;
737     float max = 11f;
738     float qmin = 0f;
739     float qmax = 0f;
740
741     char c;
742
743     if (conservation != null && conservation.annotations != null
744             && conservation.annotations.length < alWidth)
745     {
746       conservation.annotations = new Annotation[alWidth];
747     }
748
749     if (quality2 != null)
750     {
751       quality2.graphMax = (float) qualityRange[1];
752       if (quality2.annotations != null
753               && quality2.annotations.length < alWidth)
754       {
755         quality2.annotations = new Annotation[alWidth];
756       }
757       qmin = (float) qualityRange[0];
758       qmax = (float) qualityRange[1];
759     }
760
761     for (int i = istart; i < alWidth; i++)
762     {
763       float value = 0;
764
765       c = sequence[i];
766
767       if (Character.isDigit(c))
768       {
769         value = c - '0';
770       }
771       else if (c == '*')
772       {
773         value = 11;
774       }
775       else if (c == '+')
776       {
777         value = 10;
778       }
779
780       if (conservation != null)
781       {
782         float vprop = value - min;
783         vprop /= max;
784         int consp = i - start;
785         String conssym = (value > 0 && consp > -1 && consp < consSymbs.length) ? consSymbs[consp]
786                 : "";
787         conservation.annotations[i] = new Annotation(String.valueOf(c),
788                 conssym, ' ', value, new Color(minR + (maxR * vprop), minG
789                         + (maxG * vprop), minB + (maxB * vprop)));
790       }
791
792       // Quality calc
793       if (quality2 != null)
794       {
795         value = quality.elementAt(i).floatValue();
796         float vprop = value - qmin;
797         vprop /= qmax;
798         quality2.annotations[i] = new Annotation(" ",
799                 String.valueOf(value), ' ', value, new Color(minR
800                         + (maxR * vprop), minG + (maxG * vprop), minB
801                         + (maxB * vprop)));
802       }
803     }
804   }
805
806   /**
807    * construct and call the calculation methods on a new Conservation object
808    * 
809    * @param name
810    *          - name of conservation
811    * @param seqs
812    * @param start
813    *          first column in calculation window
814    * @param end
815    *          last column in calculation window
816    * @param positiveOnly
817    *          calculate positive (true) or positive and negative (false)
818    *          conservation
819    * @param maxPercentGaps
820    *          percentage of gaps tolerated in column
821    * @param calcQuality
822    *          flag indicating if alignment quality should be calculated
823    * @return Conservation object ready for use in visualization
824    */
825   public static Conservation calculateConservation(String name,
826           List<SequenceI> seqs, int start, int end, boolean positiveOnly,
827           int maxPercentGaps, boolean calcQuality)
828   {
829     Conservation cons = new Conservation(name, seqs, start, end);
830     cons.calculate();
831     cons.verdict(positiveOnly, maxPercentGaps);
832
833     if (calcQuality)
834     {
835       cons.findQuality();
836     }
837
838     return cons;
839   }
840
841   /**
842    * Returns the computed tooltip (annotation description) for a given column.
843    * The tip is empty if the conservation score is zero, otherwise holds the
844    * conserved properties (and, optionally, properties whose absence is
845    * conserved).
846    * 
847    * @param column
848    * @return
849    */
850   String getTooltip(int column)
851   {
852     char[] sequence = getConsSequence().getSequence();
853     char val = column < sequence.length ? sequence[column] : '-';
854     boolean hasConservation = val != '-' && val != '0';
855     int consp = column - start;
856     String tip = (hasConservation && consp > -1 && consp < consSymbs.length) ? consSymbs[consp]
857             : "";
858     return tip;
859   }
860 }