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