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