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