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