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