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