42a2caa3246b7e6a56b4db85dd778f20a9ec7761
[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
34 /**
35  * A file parse for T-Coffee score ascii format. This file contains the
36  * alignment consensus for each resude in any sequence.
37  * <p>
38  * This file is procuded by <code>t_coffee</code> providing the option
39  * <code>-output=score_ascii </code> to the program command line
40  * 
41  * An example file is the following
42  * 
43  * <pre>
44  * T-COFFEE, Version_9.02.r1228 (2012-02-16 18:15:12 - Revision 1228 - Build 336)
45  * Cedric Notredame 
46  * CPU TIME:0 sec.
47  * SCORE=90
48  * *
49  *  BAD AVG GOOD
50  * *
51  * 1PHT   :  89
52  * 1BB9   :  90
53  * 1UHC   :  94
54  * 1YCS   :  94
55  * 1OOT   :  93
56  * 1ABO   :  94
57  * 1FYN   :  94
58  * 1QCF   :  94
59  * cons   :  90
60  * 
61  * 1PHT   999999999999999999999999998762112222543211112134
62  * 1BB9   99999999999999999999999999987-------4322----2234
63  * 1UHC   99999999999999999999999999987-------5321----2246
64  * 1YCS   99999999999999999999999999986-------4321----1-35
65  * 1OOT   999999999999999999999999999861-------3------1135
66  * 1ABO   99999999999999999999999999986-------422-------34
67  * 1FYN   99999999999999999999999999985-------32--------35
68  * 1QCF   99999999999999999999999999974-------2---------24
69  * cons   999999999999999999999999999851000110321100001134
70  * 
71  * 
72  * 1PHT   ----------5666642367889999999999889
73  * 1BB9   1111111111676653-355679999999999889
74  * 1UHC   ----------788774--66789999999999889
75  * 1YCS   ----------78777--356789999999999889
76  * 1OOT   ----------78877--356789999999997-67
77  * 1ABO   ----------687774--56779999999999889
78  * 1FYN   ----------6888842356789999999999889
79  * 1QCF   ----------6878742356789999999999889
80  * cons   00100000006877641356789999999999889
81  * </pre>
82  * 
83  * 
84  * @author Paolo Di Tommaso
85  * 
86  */
87 public class TCoffeeScoreFile extends AlignFile
88 {
89
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    *         teh score value are supposd 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   /**
397    * Read a scores block ihe provided stream.
398    * 
399    * @param reader
400    *          The stream to parse
401    * @param size
402    *          The expected number of the sequence to be read
403    * @return The {@link Block} instance read or {link null} null if the end of
404    *         file has reached.
405    * @throws IOException
406    *           Something went wrong on the 'wire'
407    */
408   static Block readBlock(FileParse reader, int size) throws IOException
409   {
410     Block result = new Block(size);
411     String line;
412
413     /*
414      * read blank lines (eventually)
415      */
416     while ((line = reader.nextLine()) != null && "".equals(line.trim()))
417     {
418       // consume blank lines
419     }
420
421     if (line == null)
422     {
423       return null;
424     }
425
426     /*
427      * read the scores block
428      */
429     do
430     {
431       if ("".equals(line.trim()))
432       {
433         // terminated
434         break;
435       }
436
437       // split the line on the first blank
438       // the first part have to contain the sequence id
439       // the remaining part are the scores values
440       int p = line.indexOf(" ");
441       if (p == -1)
442       {
443         if (reader.warningMessage == null)
444         {
445           reader.warningMessage = "";
446         }
447         reader.warningMessage += "Possible parsing error - expected to find a space in line: '"
448                 + line + "'\n";
449         continue;
450       }
451
452       String id = line.substring(0, p).trim();
453       String val = line.substring(p + 1).trim();
454
455       result.items.put(id, val);
456
457     } while ((line = reader.nextLine()) != null);
458
459     return result;
460   }
461
462   /*
463    * The score file header
464    */
465   static class Header
466   {
467     String head;
468
469     int score;
470
471     LinkedHashMap<String, Integer> scores = new LinkedHashMap<String, Integer>();
472
473     public int getScoreAvg()
474     {
475       return score;
476     }
477
478     public int getScoreFor(String ID)
479     {
480
481       return scores.containsKey(ID) ? scores.get(ID) : -1;
482
483     }
484   }
485
486   /*
487    * Hold a single block values block in the score file
488    */
489   static class Block
490   {
491     int size;
492
493     Map<String, String> items;
494
495     public Block(int size)
496     {
497       this.size = size;
498       this.items = new HashMap<String, String>(size);
499     }
500
501     String getScoresFor(String id)
502     {
503       return items.get(id);
504     }
505
506     String getConsensus()
507     {
508       return items.get("cons");
509     }
510   }
511
512   /**
513    * TCOFFEE score colourscheme
514    */
515   static final Color[] colors =
516   { new Color(102, 102, 255), // #6666FF
517       new Color(0, 255, 0), // #00FF00
518       new Color(102, 255, 0), // #66FF00
519       new Color(204, 255, 0), // #CCFF00
520       new Color(255, 255, 0), // #FFFF00
521       new Color(255, 204, 0), // #FFCC00
522       new Color(255, 153, 0), // #FF9900
523       new Color(255, 102, 0), // #FF6600
524       new Color(255, 51, 0), // #FF3300
525       new Color(255, 34, 0) // #FF2000
526   };
527
528   public final static String TCOFFEE_SCORE = "TCoffeeScore";
529
530   /**
531    * generate annotation for this TCoffee score set on the given alignment
532    * 
533    * @param al
534    *          alignment to annotate
535    * @param matchids
536    *          if true, annotate sequences based on matching sequence names
537    * @return true if alignment annotation was modified, false otherwise.
538    */
539   public boolean annotateAlignment(AlignmentI al, boolean matchids)
540   {
541     if (al.getHeight() != getHeight() || al.getWidth() != getWidth())
542     {
543       warningMessage = "Alignment shape does not match T-Coffee score file shape.";
544       return false;
545     }
546     boolean added = false;
547     int i = 0;
548     SequenceIdMatcher sidmatcher = new SequenceIdMatcher(
549             al.getSequencesArray());
550     byte[][] scoreMatrix = getScoresArray();
551     // for 2.8 - we locate any existing TCoffee annotation and remove it first
552     // before adding this.
553     for (Map.Entry<String, StringBuilder> id : scores.entrySet())
554     {
555       byte[] srow = scoreMatrix[i];
556       SequenceI s;
557       if (matchids)
558       {
559         s = sidmatcher.findIdMatch(id.getKey());
560       }
561       else
562       {
563         s = al.getSequenceAt(i);
564       }
565       i++;
566       if (s == null && i != scores.size() && !id.getKey().equals("cons"))
567       {
568         System.err.println("No "
569                 + (matchids ? "match " : " sequences left ")
570                 + " for TCoffee score set : " + id.getKey());
571         continue;
572       }
573       int jSize = al.getWidth() < srow.length ? al.getWidth() : srow.length;
574       Annotation[] annotations = new Annotation[al.getWidth()];
575       for (int j = 0; j < jSize; j++)
576       {
577         byte val = srow[j];
578         if (s != null && jalview.util.Comparison.isGap(s.getCharAt(j)))
579         {
580           annotations[j] = null;
581           if (val > 0)
582           {
583             System.err
584                     .println("Warning: non-zero value for positional T-COFFEE score for gap at "
585                             + j + " in sequence " + s.getName());
586           }
587         }
588         else
589         {
590           annotations[j] = new Annotation(s == null ? "" + val : null,
591                   s == null ? "" + val : null, '\0', val * 1f, val >= 0
592                           && val < colors.length ? colors[val]
593                           : Color.white);
594         }
595       }
596       // this will overwrite any existing t-coffee scores for the alignment
597       AlignmentAnnotation aa = al.findOrCreateAnnotation(TCOFFEE_SCORE,
598               TCOFFEE_SCORE, false, s, null);
599       if (s != null)
600       {
601         aa.label = "T-COFFEE";
602         aa.description = "" + id.getKey();
603         aa.annotations = annotations;
604         aa.visible = false;
605         aa.belowAlignment = false;
606         aa.setScore(header.getScoreFor(id.getKey()));
607         aa.createSequenceMapping(s, s.getStart(), true);
608         s.addAlignmentAnnotation(aa);
609         aa.adjustForAlignment();
610       }
611       else
612       {
613         aa.graph = AlignmentAnnotation.NO_GRAPH;
614         aa.label = "T-COFFEE";
615         aa.description = "TCoffee column reliability score";
616         aa.annotations = annotations;
617         aa.belowAlignment = true;
618         aa.visible = true;
619         aa.setScore(header.getScoreAvg());
620       }
621       aa.showAllColLabels = true;
622       aa.validateRangeAndDisplay();
623       added = true;
624     }
625
626     return added;
627   }
628
629   @Override
630   public String print()
631   {
632     // TODO Auto-generated method stub
633     return "Not valid.";
634   }
635 }