JAL-2599 fix issue where parser exported map annotation incorrectly
[jalview.git] / src / jalview / io / HMMFile.java
1 package jalview.io;
2
3 import jalview.datamodel.HMMNode;
4 import jalview.datamodel.HiddenMarkovModel;
5 import jalview.datamodel.SequenceI;
6
7 import java.io.BufferedReader;
8 import java.io.FileNotFoundException;
9 import java.io.IOException;
10 import java.io.PrintWriter;
11 import java.io.UnsupportedEncodingException;
12 import java.util.ArrayList;
13 import java.util.List;
14 import java.util.Scanner;
15
16
17 /**
18  * Adds capability to read in and write out HMMER3 files. Currently only supports HMMER3/f.
19  * 
20  * 
21  * @author TZVanaalten
22  *
23  */
24 public class HMMFile extends AlignFile
25         implements AlignmentFileReaderI, AlignmentFileWriterI
26 {
27   // HMM to store file data
28   private HiddenMarkovModel hmm = new HiddenMarkovModel();
29
30
31
32
33   // number of possible transitions
34   private final int NUMBER_OF_TRANSITIONS = 7;
35
36   private final String NEW_LINE = "\n";
37
38   //number of symbols in the alphabet used in the hidden Markov model
39   int numberOfSymbols;
40
41   private final String SPACE = " ";
42
43   private final String COMPO = "COMPO";
44
45   private final String EMPTY = "";
46
47   //This is a line that needs to be added to each HMMER£ file. It is purely for readability.
48   private static final String TRANSITIONTYPELINE = "m->m     m->i     m->d     i->m     i->i     d->m     d->d";
49
50   /**
51    * Constructor for HMMFile
52    * @param source
53    * @throws IOException
54    */
55   public HMMFile(FileParse source) throws IOException
56   {
57     super(false, source);
58   }
59
60   /**
61    * Default constructor, do not use!
62    */
63   public HMMFile()
64   {
65
66   }
67
68   /**
69    * Constructor for HMMFile used for exporting.
70    * 
71    * @param hmm
72    * @param exportImmediately
73    */
74   public HMMFile(HiddenMarkovModel markov)
75   {
76     hmm = markov;
77   }
78
79   /**
80    * Returns the HMM produced by reading in a HMMER3 file.
81    * 
82    * @return
83    */
84   public HiddenMarkovModel getHMM()
85   {
86     return hmm;
87   }
88
89   /**
90    * Sets the HMM used in this file.
91    * 
92    * @param model
93    */
94   public void setHMM(HiddenMarkovModel model)
95   {
96     this.hmm = model;
97   }
98
99   /**
100    * Gets the name of the hidden Markov model.
101    * 
102    * @return
103    */
104   public String getName()
105   {
106     return hmm.getName();
107   }
108
109   /**
110    * Reads the data from HMM file into the HMM field on this object.
111    * 
112    * @throws IOException
113    */
114   @Override
115   public void parse() throws IOException
116   {
117     parseFileProperties(dataIn);
118     parseModel(dataIn);
119   }
120
121   /**
122    * Reads the data from HMM file into the HMM field on this object.
123    * 
124    * @throws IOException
125    */
126
127   public void parse(BufferedReader br) throws IOException
128   {
129     parseFileProperties(br);
130     parseModel(br);
131   }
132
133
134
135   /**
136    * Imports the file properties from a HMMER3 file.
137    * 
138    * @param input
139    *          The buffered reader used to read in the file.
140    * @throws IOException
141    */
142   void parseFileProperties(BufferedReader input) throws IOException
143   {
144     boolean readingFile = true;
145     hmm.setFileHeader(input.readLine());
146     String line = input.readLine();
147     while (readingFile)
148     {
149       if (line != null)
150       {
151         Scanner parser = new Scanner(line);
152         String next = parser.next();
153         if ("HMM".equals(next)) // indicates start of HMM data (end of file
154                               // properties)
155         {
156           readingFile = false;
157           hmm.fillSymbols(parser);
158           numberOfSymbols = hmm.getNumberOfSymbols();
159         }
160         else if ("STATS".equals(next))
161         {
162           parser.next();
163           String key;
164           String value;
165           key = parser.next();
166           value = parser.next() + SPACE + SPACE + parser.next();
167           hmm.addFileProperty(key, value);
168         }
169         else
170         {
171           String key = next;
172           String value = parser.next();
173           while (parser.hasNext())
174           {
175             value = value + SPACE + parser.next();
176           }
177           hmm.addFileProperty(key, value);
178         }
179         parser.close();
180       }
181       line = input.readLine();
182       if (line == null)
183       {
184         readingFile = false;
185       }
186     }
187
188   }
189
190   /**
191    * Parses the model data from the HMMER3 file
192    * 
193    * @param input
194    *          The buffered reader used to read the file.
195    * @throws IOException
196    */
197   void parseModel(BufferedReader input) throws IOException
198   {
199     String line = input.readLine();
200     int node = 0;
201     while (!"//".equals(line))
202     {
203       hmm.getNodes().add(new HMMNode());
204       String next;
205       Scanner matchReader = new Scanner(line);
206       next = matchReader.next();
207       if (next.equals(COMPO) || node > 0)
208       {
209         // stores match emission line in list
210         List<Double> matches = new ArrayList<>();
211         matches = fillList(matchReader, numberOfSymbols);
212         hmm.getNodes().get(node).setMatchEmissions(matches);
213         if (node > 0)
214         {
215           parseAnnotations(matchReader, node);
216         }
217       }
218       matchReader.close();
219       // stores insert emission line in list
220       line = input.readLine();
221       Scanner insertReader = new Scanner(line);
222       List<Double> inserts = new ArrayList<>();
223       inserts = fillList(insertReader, numberOfSymbols);
224       hmm.getNodes().get(node).setInsertEmissions(inserts);
225       insertReader.close();
226
227       // stores state transition line in list
228       line = input.readLine();
229       Scanner transitionReader = new Scanner(line);
230       List<Double> transitions = new ArrayList<>();
231       transitions = fillList(transitionReader, NUMBER_OF_TRANSITIONS);
232       hmm.getNodes().get(node).setStateTransitions(transitions);
233       transitionReader.close();
234       line = input.readLine();
235       node++;
236     }
237
238   }
239
240   /**
241    * Parses the annotations on the match emission line.
242    * 
243    * @param scanner
244    *          The scanner which is processing match emission line.
245    * @param index
246    *          The index of node which is being scanned.
247    */
248   void parseAnnotations(Scanner scanner, int index)
249   {
250     if (hmm.mapIsActive())
251     {
252       int column;
253       column = scanner.nextInt();
254       hmm.getNodes().get(index).setAlignmentColumn(column);
255       hmm.getNodeLookup().put(column, index);
256     }
257     else
258     {
259       scanner.next();
260     }
261
262     if (scanner.hasNext())
263     {
264       char consensusR;
265       consensusR = charValue(scanner.next());
266       hmm.getNodes().get(index).setConsensusResidue(consensusR);
267     }
268
269     if (scanner.hasNext())
270     {
271       char reference;
272       reference = charValue(scanner.next());
273       hmm.getNodes().get(index).setReferenceAnnotation(reference);
274     }
275
276     if (scanner.hasNext())
277     {
278       char value;
279       value = charValue(scanner.next());
280       hmm.getNodes().get(index).setMaskValue(value);
281     }
282     if (scanner.hasNext())
283     {
284       char consensusS;
285       consensusS = charValue(scanner.next());
286       hmm.getNodes().get(index).setConsensusStructure(consensusS);
287     }
288   }
289
290
291
292   /**
293    * Fills a list of doubles based on an input line.
294    * 
295    * @param input
296    *          The scanner for the line containing the data to be transferred to
297    *          the list.
298    * @param numberOfElements
299    *          The number of elements in the list to be filled.
300    * @return filled list Returns the list of doubles.
301    */
302   static List<Double> fillList(Scanner input,
303           int numberOfElements)
304   {
305     List<Double> list = new ArrayList<>();
306     for (int i = 0; i < numberOfElements; i++)
307     {
308
309       String next = input.next();
310       if (next.contains("*")) // state transitions to or from delete states
311                               // occasionally have values of -infinity. These
312                               // values are represented by an * in the .hmm
313                               // file.
314       {
315         list.add(Double.NEGATIVE_INFINITY);
316       }
317       else
318       {
319         double prob = Double.valueOf(next);
320         prob = Math.pow(Math.E, -prob);
321         list.add(prob);
322       }
323     }
324     return list;
325   }
326
327   
328   /**
329    * Writes a HMM to a file/
330    * 
331    * @param exportLocation
332    *          Filename, URL or Pasted String to write to.
333    * @throws FileNotFoundException
334    * @throws UnsupportedEncodingException
335    *
336    **/
337   
338   public void exportFile(String exportLocation) throws IOException
339   {
340     PrintWriter writer = new PrintWriter(exportLocation);
341     appendFileProperties(writer);
342     appendModel(writer);
343     writer.println("//");
344
345     writer.close();
346
347   }
348
349   /**
350    * Returns a string to be added to the StringBuilder containing the entire
351    * output String.
352    * 
353    * @param initialColumnSeparation
354    *          The initial whitespace separation between the left side of the
355    *          file and first character.
356    * @param columnSeparation
357    *          The separation between subsequent data entries.
358    * @param data
359    *          The list fo data to be added to the String.
360    * @return
361    */
362   String addData(int initialColumnSeparation,
363           int columnSeparation, List<String> data)
364   {
365     String line = EMPTY;
366     int index = 0;
367     for (String value : data)
368     {
369       if (index == 0)
370       {
371         line += String.format("%" + initialColumnSeparation + "s", value);
372       }
373       else
374       {
375         line += String.format("%" + columnSeparation + "s", value);
376       }
377       index++;
378     }
379     return line;
380   }
381
382   /**
383    * Converts list of characters into a list of Strings.
384    * 
385    * @param list
386    * @return Returns the list of Strings.
387    */
388   List<String> charListToStringList(List<Character> list)
389   {
390     List<String> strList = new ArrayList<>();
391     for (char value : list)
392     {
393       String strValue = Character.toString(value);
394       strList.add(strValue);
395     }
396     return strList;
397   }
398
399   /**
400    * Converts a list of doubles into a list of Strings, rounded to the nearest
401    * 5th decimal place.
402    * 
403    * @param list
404    * @param noOfDecimals
405    * @return
406    */
407   List<String> doubleListToStringList(List<Double> list)
408   {
409     List<String> strList = new ArrayList<>();
410     for (double value : list)
411     {
412       String strValue;
413       if (value > 0)
414       {
415         strValue = String.format("%.5f", value);
416
417       }
418       else if (value == -0.00000d)
419       {
420         strValue = "0.00000";
421       }
422       else
423       {
424         strValue = "*";
425       }
426
427       strList.add(strValue);
428     }
429     return strList;
430   }
431
432   /**
433    * Converts a primitive array of Strings to a list of Strings.
434    * 
435    * @param array
436    * @return
437    */
438   List<String> stringArrayToStringList(String[] array)
439   {
440     List<String> list = new ArrayList<>();
441     for (String value : array)
442     {
443       list.add(value);
444     }
445
446     return list;
447   }
448
449   /**
450    * Appends the hidden Markov model data to the StringBuilder containing the
451    * output
452    * 
453    * @param file
454    *          The StringBuilder containing the output.
455    */
456   void appendModel(PrintWriter writer)
457   {
458     String symbolLine = "HMM";
459     List<Character> charSymbols = hmm.getSymbols();
460     List<String> strSymbols;
461     strSymbols = charListToStringList(charSymbols);
462     symbolLine += addData(11, 9, strSymbols);
463     writer.println(symbolLine);
464     writer.println(TRANSITIONTYPELINE);
465
466     int length = hmm.getLength();
467
468     for (int node = 0; node <= length; node++)
469     {
470       String matchLine;
471       if (node == 0)
472       {
473         matchLine = String.format("%7s", "COMPO");
474       }
475       else
476       {
477         matchLine = String.format("%7s", node);
478       }
479
480       List<String> strMatches;
481       List<Double> doubleMatches;
482       doubleMatches = convertListToLogSpace(
483               hmm.getNode(node).getMatchEmissions());
484       strMatches = doubleListToStringList(doubleMatches);
485       matchLine += addData(10, 9, strMatches);
486
487
488       if (node != 0)
489       {
490         matchLine += SPACE + (hmm.getNodeAlignmentColumn(node) + 1);
491         matchLine += SPACE + hmm.getConsensusResidue(node);
492         matchLine += SPACE + hmm.getReferenceAnnotation(node);
493         if (hmm.getFileHeader().contains("HMMER3/f"))
494         {
495           matchLine += SPACE + hmm.getMaskedValue(node);
496           matchLine += SPACE + hmm.getConsensusStructure(node);
497         }
498
499       }
500
501       writer.println(matchLine);
502       
503       String insertLine = EMPTY;
504       List<String> strInserts;
505       List<Double> doubleInserts;
506       doubleInserts = convertListToLogSpace(
507               hmm.getNode(node).getInsertEmissions());
508       strInserts = doubleListToStringList(doubleInserts);
509       insertLine += addData(17, 9, strInserts);
510
511       writer.println(insertLine);
512
513       String transitionLine = EMPTY;
514       List<String> strTransitions;
515       List<Double> doubleTransitions;
516       doubleTransitions = convertListToLogSpace(
517               hmm.getNode(node).getStateTransitions());
518       strTransitions = doubleListToStringList(doubleTransitions);
519       transitionLine += addData(17, 9, strTransitions);
520
521       writer.println(transitionLine);
522     }
523   }
524
525   /**
526    * Appends the hidden Markov model file properties to the StringBuilder
527    * containing the output
528    * 
529    * @param file
530    *          The StringBuilder containing the output.
531    */
532   void appendFileProperties(PrintWriter writer)
533   {
534     String line;
535
536     writer.println(hmm.getFileHeader());
537     
538     line = String.format("%-5s %1s", "NAME", hmm.getName());
539     writer.println((line));
540
541     if (hmm.getAccessionNumber() != null)
542     {
543     line = String.format("%-5s %1s", "ACC", hmm.getAccessionNumber());
544       writer.println((line));
545     }
546
547     if (hmm.getDescription() != null)
548     {
549     line = String.format("%-5s %1s", "DESC", hmm.getDescription());
550       writer.println((line));
551     }
552     line = String.format("%-5s %1s", "LENG", hmm.getLength());
553     writer.println((line));
554
555     if (hmm.getMaxInstanceLength() != null)
556     {
557     line = String.format("%-5s %1s", "MAXL", hmm.getMaxInstanceLength());
558       writer.println((line));
559     }
560     line = String.format("%-5s %1s", "ALPH", hmm.getAlphabetType());
561     writer.println((line));
562
563     boolean status;
564     String statusStr;
565
566     status = hmm.referenceAnnotationIsActive();
567     statusStr = HiddenMarkovModel.findStringFromBoolean(status);
568     line = String.format("%-5s %1s", "RF",
569             statusStr);
570     writer.println((line));
571
572     status = hmm.maskValueIsActive();
573     statusStr = HiddenMarkovModel.findStringFromBoolean(status);
574     line = String.format("%-5s %1s", "MM",
575             statusStr);
576     writer.println((line));
577     
578     status = hmm.consensusResidueIsActive();
579     statusStr = HiddenMarkovModel.findStringFromBoolean(status);
580     line = String.format("%-5s %1s", "CONS",
581             statusStr);
582     writer.println((line));
583
584     status = hmm.consensusStructureIsActive();
585     statusStr = HiddenMarkovModel.findStringFromBoolean(status);
586     line = String.format("%-5s %1s", "CS",
587             statusStr);
588     writer.println((line));
589
590     status = hmm.mapIsActive();
591     statusStr = HiddenMarkovModel.findStringFromBoolean(status);
592     line = String.format("%-5s %1s", "MAP",
593             statusStr);
594     writer.println((line));
595
596
597     if (hmm.getDate() != null)
598     {
599     line = String.format("%-5s %1s", "DATE", hmm.getDate());
600       writer.println((line));
601     }
602     if (hmm.getNumberOfSequences() != null)
603     {
604     line = String.format("%-5s %1s", "NSEQ", hmm.getNumberOfSequences());
605       writer.println((line));
606     }
607     if (hmm.getEffectiveNumberOfSequences() != null)
608     {
609     line = String.format("%-5s %1s", "EFFN",
610             hmm.getEffectiveNumberOfSequences());
611       writer.println((line));
612     }
613     if (hmm.getCheckSum() != null)
614     {
615     line = String.format("%-5s %1s", "CKSUM", hmm.getCheckSum());
616       writer.println((line));
617     }
618     if (hmm.getGatheringThreshold() != null)
619     {
620     line = String.format("%-5s %1s", "GA", hmm.getGatheringThreshold());
621       writer.println((line));
622     }
623
624     if (hmm.getTrustedCutoff() != null)
625     {
626     line = String.format("%-5s %1s", "TC", hmm.getTrustedCutoff());
627       writer.println((line));
628     }
629     if (hmm.getNoiseCutoff() != null)
630     {
631     line = String.format("%-5s %1s", "NC", hmm.getNoiseCutoff());
632       writer.println((line));
633     }
634     if (hmm.getMSV() != null)
635     {
636       line = String.format("%-19s %18s", "STATS LOCAL MSV", hmm.getMSV());
637       writer.println((line));
638
639       line = String.format("%-19s %18s", "STATS LOCAL VITERBI",
640               hmm.getViterbi());
641       writer.println((line));
642     
643       line = String.format("%-19s %18s", "STATS LOCAL FORWARD",
644               hmm.getForward());
645       writer.println((line));
646     }
647   }
648
649
650   /**
651    * Returns the char value of a single lettered String.
652    * 
653    * @param string
654    * @return
655    */
656   char charValue(String string)
657   {
658     char character;
659     character = string.charAt(0);
660     return character;
661
662   }
663
664   @Override
665   public String print(SequenceI[] seqs, boolean jvsuffix)
666   {
667
668     return null;
669   }
670
671   /**
672    * Converts the probabilities contained in a list into log space.
673    * 
674    * @param list
675    */
676   List<Double> convertListToLogSpace(List<Double> list)
677   {
678
679     List<Double> convertedList = new ArrayList<>();
680     for (int i = 0; i < list.size(); i++)
681     {
682       double prob = list.get(i);
683       double logProb = -1 * Math.log(prob);
684
685       convertedList.add(logProb);
686     }
687     return convertedList;
688
689
690   }
691 }
692