Merge branch 'jims_annotate3d_update' into menard_finalsep2012
[jalview.git] / src / jalview / io / TCoffeeScoreFile.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8)
3  * Copyright (C) 2012 J Procter, AM Waterhouse, LM Lui, J Engelhardt, G Barton, M Clamp, S Searle
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 of the License, or (at your option) any later version.
10  *  
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.io;
19
20 import jalview.analysis.SequenceIdMatcher;
21 import jalview.datamodel.AlignmentAnnotation;
22 import jalview.datamodel.AlignmentI;
23 import jalview.datamodel.Annotation;
24 import jalview.datamodel.SequenceI;
25
26 import java.awt.Color;
27 import java.io.IOException;
28 import java.util.ArrayList;
29 import java.util.HashMap;
30 import java.util.LinkedHashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35
36 /**
37  * A file parse for T-Coffee score ascii format. This file contains the
38  * alignment consensus for each resude in any sequence.
39  * <p>
40  * This file is procuded by <code>t_coffee</code> providing the option
41  * <code>-output=score_ascii </code> to the program command line
42  * 
43  * An example file is the following
44  * 
45  * <pre>
46  * T-COFFEE, Version_9.02.r1228 (2012-02-16 18:15:12 - Revision 1228 - Build 336)
47  * Cedric Notredame 
48  * CPU TIME:0 sec.
49  * SCORE=90
50  * *
51  *  BAD AVG GOOD
52  * *
53  * 1PHT   :  89
54  * 1BB9   :  90
55  * 1UHC   :  94
56  * 1YCS   :  94
57  * 1OOT   :  93
58  * 1ABO   :  94
59  * 1FYN   :  94
60  * 1QCF   :  94
61  * cons   :  90
62  * 
63  * 1PHT   999999999999999999999999998762112222543211112134
64  * 1BB9   99999999999999999999999999987-------4322----2234
65  * 1UHC   99999999999999999999999999987-------5321----2246
66  * 1YCS   99999999999999999999999999986-------4321----1-35
67  * 1OOT   999999999999999999999999999861-------3------1135
68  * 1ABO   99999999999999999999999999986-------422-------34
69  * 1FYN   99999999999999999999999999985-------32--------35
70  * 1QCF   99999999999999999999999999974-------2---------24
71  * cons   999999999999999999999999999851000110321100001134
72  * 
73  * 
74  * 1PHT   ----------5666642367889999999999889
75  * 1BB9   1111111111676653-355679999999999889
76  * 1UHC   ----------788774--66789999999999889
77  * 1YCS   ----------78777--356789999999999889
78  * 1OOT   ----------78877--356789999999997-67
79  * 1ABO   ----------687774--56779999999999889
80  * 1FYN   ----------6888842356789999999999889
81  * 1QCF   ----------6878742356789999999999889
82  * cons   00100000006877641356789999999999889
83  * </pre>
84  * 
85  * 
86  * @author Paolo Di Tommaso
87  * 
88  */
89 public class TCoffeeScoreFile extends AlignFile {
90   public TCoffeeScoreFile(String inFile, String type) throws IOException
91   {
92     super(inFile, type);
93
94   }
95
96   public TCoffeeScoreFile(FileParse source) throws IOException
97   {
98     super(source);
99   }
100
101   /** The {@link Header} structure holder */
102   Header header;
103
104   /**
105    * Holds the consensues values for each sequences. It uses a LinkedHashMap to
106    * maintaint the insertion order.
107    */
108   LinkedHashMap<String, StringBuilder> scores;
109
110   Integer fWidth;
111
112   /**
113    * Parse the provided reader for the T-Coffee scores file format
114    * 
115    * @param reader
116    *          public static TCoffeeScoreFile load(Reader reader) {
117    * 
118    *          try { BufferedReader in = (BufferedReader) (reader instanceof
119    *          BufferedReader ? reader : new BufferedReader(reader));
120    *          TCoffeeScoreFile result = new TCoffeeScoreFile();
121    *          result.doParsing(in); return result.header != null &&
122    *          result.scores != null ? result : null; } catch( Exception e) {
123    *          throw new RuntimeException(e); } }
124    */
125
126   /**
127    * @return The 'height' of the score matrix i.e. the numbers of score rows
128    *         that should matches the number of sequences in the alignment
129    */
130   public int getHeight()
131   {
132     // the last entry will always be the 'global' alingment consensus scores, so
133     // it is removed
134     // from the 'height' count to make this value compatible with the number of
135     // sequences in the MSA
136     return scores != null && scores.size() > 0 ? scores.size() - 1 : 0;
137   }
138
139   /**
140    * @return The 'width' of the score matrix i.e. the number of columns. Since
141    *         the score value are supposed to be calculated for an 'aligned' MSA,
142    *         all the entries have to have the same width.
143    */
144   public int getWidth()
145   {
146     return fWidth != null ? fWidth : 0;
147   }
148
149   /**
150    * Get the string of score values for the specified seqeunce ID.
151    * 
152    * @param id
153    *          The sequence ID
154    * @return The scores as a string of values e.g. {@code 99999987-------432}.
155    *         It return an empty string when the specified ID is missing.
156    */
157   public String getScoresFor(String id)
158   {
159     return scores != null && scores.containsKey(id) ? scores.get(id)
160             .toString() : "";
161   }
162
163   /**
164    * @return The list of score string as a {@link List} object, in the same
165    *         ordeer of the insertion i.e. in the MSA
166    */
167   public List<String> getScoresList()
168   {
169     if (scores == null)
170     {
171       return null;
172     }
173     List<String> result = new ArrayList<String>(scores.size());
174     for (Map.Entry<String, StringBuilder> it : scores.entrySet())
175     {
176       result.add(it.getValue().toString());
177     }
178
179     return result;
180   }
181
182   /**
183    * @return The parsed score values a matrix of bytes
184    */
185   public byte[][] getScoresArray()
186   {
187     if (scores == null)
188     {
189       return null;
190     }
191     byte[][] result = new byte[scores.size()][];
192
193     int rowCount = 0;
194     for (Map.Entry<String, StringBuilder> it : scores.entrySet())
195     {
196       String line = it.getValue().toString();
197       byte[] seqValues = new byte[line.length()];
198       for (int j = 0, c = line.length(); j < c; j++)
199       {
200
201         byte val = (byte) (line.charAt(j) - '0');
202
203         seqValues[j] = (val >= 0 && val <= 9) ? val : -1;
204       }
205
206       result[rowCount++] = seqValues;
207     }
208
209     return result;
210   }
211
212   public void parse() throws IOException
213   {
214     /*
215      * read the header
216      */
217     header = readHeader(this);
218
219     if (header == null)
220     {
221       error = true;
222       return;
223     }
224     scores = new LinkedHashMap<String, StringBuilder>();
225
226     /*
227      * initilize the structure
228      */
229     for (Map.Entry<String, Integer> entry : header.scores.entrySet())
230     {
231       scores.put(entry.getKey(), new StringBuilder());
232     }
233
234     /*
235      * go with the reading
236      */
237     Block block;
238     while ((block = readBlock(this, header.scores.size())) != null)
239     {
240
241       /*
242        * append sequences read in the block
243        */
244       for (Map.Entry<String, String> entry : block.items.entrySet())
245       {
246         StringBuilder scoreStringBuilder = scores.get(entry.getKey());
247         if (scoreStringBuilder == null)
248         {
249           error = true;
250           errormessage = String
251                   .format("Invalid T-Coffee score file: Sequence ID '%s' is not declared in header section",
252                           entry.getKey());
253           return;
254         }
255
256         scoreStringBuilder.append(entry.getValue());
257       }
258     }
259
260     /*
261      * verify that all rows have the same width
262      */
263     for (StringBuilder str : scores.values())
264     {
265       if (fWidth == null)
266       {
267         fWidth = str.length();
268       }
269       else if (fWidth != str.length())
270       {
271         error = true;
272         errormessage = "Invalid T-Coffee score file: All the score sequences must have the same length";
273         return;
274       }
275     }
276
277     return;
278   }
279
280   static int parseInt(String str)
281   {
282     try
283     {
284       return Integer.parseInt(str);
285     } catch (NumberFormatException e)
286     {
287       // TODO report a warning ?
288       return 0;
289     }
290   }
291
292   /**
293    * Reaad the header section in the T-Coffee score file format
294    * 
295    * @param reader
296    *          The scores reader
297    * @return The parser {@link Header} instance
298    * @throws RuntimeException
299    *           when the header is not in the expected format
300    */
301   static Header readHeader(FileParse reader) throws IOException
302   {
303
304     Header result = null;
305     try
306     {
307       result = new Header();
308       result.head = reader.nextLine();
309
310       String line;
311
312       while ((line = reader.nextLine()) != null)
313       {
314         if (line.startsWith("SCORE="))
315         {
316           result.score = parseInt(line.substring(6).trim());
317           break;
318         }
319       }
320
321       if ((line = reader.nextLine()) == null || !"*".equals(line.trim()))
322       {
323         error(reader,
324                 "Invalid T-COFFEE score format (NO BAD/AVG/GOOD header)");
325         return null;
326       }
327       if ((line = reader.nextLine()) == null
328               || !"BAD AVG GOOD".equals(line.trim()))
329       {
330         error(reader,
331                 "Invalid T-COFFEE score format (NO BAD/AVG/GOOD header)");
332         return null;
333       }
334       if ((line = reader.nextLine()) == null || !"*".equals(line.trim()))
335       {
336         error(reader,
337                 "Invalid T-COFFEE score format (NO BAD/AVG/GOOD header)");
338         return null;
339       }
340
341       /*
342        * now are expected a list if sequences ID up to the first blank line
343        */
344       while ((line = reader.nextLine()) != null)
345       {
346         if ("".equals(line))
347         {
348           break;
349         }
350
351         int p = line.indexOf(":");
352         if (p == -1)
353         {
354           // TODO report a warning
355           continue;
356         }
357
358         String id = line.substring(0, p).trim();
359         int val = parseInt(line.substring(p + 1).trim());
360         if ("".equals(id))
361         {
362           // TODO report warning
363           continue;
364         }
365
366         result.scores.put(id, val);
367       }
368
369       if (result == null)
370       {
371         error(reader, "T-COFFEE score file had no per-sequence scores");
372       }
373
374     } catch (IOException e)
375     {
376       error(reader, "Unexpected problem parsing T-Coffee score ascii file");
377       throw e;
378     }
379
380     return result;
381   }
382
383   private static void error(FileParse reader, String errm)
384   {
385     reader.error = true;
386     if (reader.errormessage == null)
387     {
388       reader.errormessage = errm;
389     }
390     else
391     {
392       reader.errormessage += "\n" + errm;
393     }
394   }
395
396   static Pattern SCORES_WITH_RESIDUE_NUMS = Pattern.compile("^\\d+\\s([^\\s]+)\\s+\\d+$");
397   
398   /**
399    * Read a scores block ihe provided stream.
400    * 
401    * @param reader
402    *          The stream to parse
403    * @param size
404    *          The expected number of the sequence to be read
405    * @return The {@link Block} instance read or {link null} null if the end of
406    *         file has reached.
407    * @throws IOException
408    *           Something went wrong on the 'wire'
409    */
410   static Block readBlock(FileParse reader, int size) throws IOException
411   {
412     Block result = new Block(size);
413     String line;
414
415     /*
416      * read blank lines (eventually)
417      */
418     while ((line = reader.nextLine()) != null && "".equals(line.trim()))
419     {
420       // consume blank lines
421     }
422
423     if (line == null)
424     {
425       return null;
426     }
427
428     /*
429      * read the scores block
430      */
431     do
432     {
433       if ("".equals(line.trim()))
434       {
435         // terminated
436         break;
437       }
438
439       // split the line on the first blank
440       // the first part have to contain the sequence id
441       // the remaining part are the scores values
442       int p = line.indexOf(" ");
443       if (p == -1)
444       {
445         if (reader.warningMessage == null)
446         {
447           reader.warningMessage = "";
448         }
449         reader.warningMessage += "Possible parsing error - expected to find a space in line: '"
450                 + line + "'\n";
451         continue;
452       }
453
454       String id = line.substring(0, p).trim();
455       String val = line.substring(p + 1).trim();
456
457       Matcher m = SCORES_WITH_RESIDUE_NUMS.matcher(val);
458       if( m.matches() ) {
459           val = m.group(1);
460       }
461       
462       result.items.put(id, val);
463
464     } while ((line = reader.nextLine()) != null);
465
466     return result;
467   }
468
469   /*
470    * The score file header
471    */
472   static class Header
473   {
474     String head;
475
476     int score;
477
478     LinkedHashMap<String, Integer> scores = new LinkedHashMap<String, Integer>();
479
480     public int getScoreAvg()
481     {
482       return score;
483     }
484
485     public int getScoreFor(String ID)
486     {
487
488       return scores.containsKey(ID) ? scores.get(ID) : -1;
489
490     }
491   }
492
493   /*
494    * Hold a single block values block in the score file
495    */
496   static class Block
497   {
498     int size;
499
500     Map<String, String> items;
501
502     public Block(int size)
503     {
504       this.size = size;
505       this.items = new HashMap<String, String>(size);
506     }
507
508     String getScoresFor(String id)
509     {
510       return items.get(id);
511     }
512
513     String getConsensus()
514     {
515       return items.get("cons");
516     }
517   }
518
519   /**
520    * TCOFFEE score colourscheme
521    */
522   static final Color[] colors =
523   { new Color(102, 102, 255), // #6666FF
524       new Color(0, 255, 0), // #00FF00
525       new Color(102, 255, 0), // #66FF00
526       new Color(204, 255, 0), // #CCFF00
527       new Color(255, 255, 0), // #FFFF00
528       new Color(255, 204, 0), // #FFCC00
529       new Color(255, 153, 0), // #FF9900
530       new Color(255, 102, 0), // #FF6600
531       new Color(255, 51, 0), // #FF3300
532       new Color(255, 34, 0) // #FF2000
533   };
534
535   public final static String TCOFFEE_SCORE = "TCoffeeScore";
536
537   /**
538    * generate annotation for this TCoffee score set on the given alignment
539    * 
540    * @param al
541    *          alignment to annotate
542    * @param matchids
543    *          if true, annotate sequences based on matching sequence names
544    * @return true if alignment annotation was modified, false otherwise.
545    */
546   public boolean annotateAlignment(AlignmentI al, boolean matchids)
547   {
548     if (al.getHeight() != getHeight() || al.getWidth() != getWidth())
549     {
550       String info = String.format("align w: %s, h: %s; score: w: %s; h: %s ", al.getWidth(), al.getHeight(), getWidth(), getHeight() );
551       warningMessage = "Alignment shape does not match T-Coffee score file shape -- " + info;
552       return false;
553     }
554     boolean added = false;
555     int i = 0;
556     SequenceIdMatcher sidmatcher = new SequenceIdMatcher(
557             al.getSequencesArray());
558     byte[][] scoreMatrix = getScoresArray();
559     // for 2.8 - we locate any existing TCoffee annotation and remove it first
560     // before adding this.
561     for (Map.Entry<String, StringBuilder> id : scores.entrySet())
562     {
563       byte[] srow = scoreMatrix[i];
564       SequenceI s;
565       if (matchids)
566       {
567         s = sidmatcher.findIdMatch(id.getKey());
568       }
569       else
570       {
571         s = al.getSequenceAt(i);
572       }
573       i++;
574       if (s == null && i != scores.size() && !id.getKey().equals("cons"))
575       {
576         System.err.println("No "
577                 + (matchids ? "match " : " sequences left ")
578                 + " for TCoffee score set : " + id.getKey());
579         continue;
580       }
581       int jSize = al.getWidth() < srow.length ? al.getWidth() : srow.length;
582       Annotation[] annotations = new Annotation[al.getWidth()];
583       for (int j = 0; j < jSize; j++)
584       {
585         byte val = srow[j];
586         if (s != null && jalview.util.Comparison.isGap(s.getCharAt(j)))
587         {
588           annotations[j] = null;
589           if (val > 0)
590           {
591             System.err
592                     .println("Warning: non-zero value for positional T-COFFEE score for gap at "
593                             + j + " in sequence " + s.getName());
594           }
595         }
596         else
597         {
598           annotations[j] = new Annotation(s == null ? "" + val : null,
599                   s == null ? "" + val : null, '\0', val * 1f, val >= 0
600                           && val < colors.length ? colors[val]
601                           : Color.white);
602         }
603       }
604       // this will overwrite any existing t-coffee scores for the alignment
605       AlignmentAnnotation aa = al.findOrCreateAnnotation(TCOFFEE_SCORE,
606               TCOFFEE_SCORE, false, s, null);
607       if (s != null)
608       {
609         aa.label = "T-COFFEE";
610         aa.description = "" + id.getKey();
611         aa.annotations = annotations;
612         aa.visible = false;
613         aa.belowAlignment = false;
614         aa.setScore(header.getScoreFor(id.getKey()));
615         aa.createSequenceMapping(s, s.getStart(), true);
616         s.addAlignmentAnnotation(aa);
617         aa.adjustForAlignment();
618       }
619       else
620       {
621         aa.graph = AlignmentAnnotation.NO_GRAPH;
622         aa.label = "T-COFFEE";
623         aa.description = "TCoffee column reliability score";
624         aa.annotations = annotations;
625         aa.belowAlignment = true;
626         aa.visible = true;
627         aa.setScore(header.getScoreAvg());
628       }
629       aa.showAllColLabels = true;
630       aa.validateRangeAndDisplay();
631       added = true;
632     }
633
634     return added;
635   }
636
637   @Override
638   public String print()
639   {
640     // TODO Auto-generated method stub
641     return "Not valid.";
642   }
643 }