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