JAL-2629 hmm search UI and structural improvements, inclusion thresholds
[jalview.git] / src / jalview / hmmer / HmmerCommand.java
1 package jalview.hmmer;
2
3 import jalview.analysis.SeqsetUtils;
4 import jalview.bin.Cache;
5 import jalview.datamodel.Alignment;
6 import jalview.datamodel.AlignmentAnnotation;
7 import jalview.datamodel.AlignmentI;
8 import jalview.datamodel.AnnotatedCollectionI;
9 import jalview.datamodel.Annotation;
10 import jalview.datamodel.HiddenMarkovModel;
11 import jalview.datamodel.SequenceGroup;
12 import jalview.datamodel.SequenceI;
13 import jalview.gui.AlignFrame;
14 import jalview.gui.JvOptionPane;
15 import jalview.gui.Preferences;
16 import jalview.io.FastaFile;
17 import jalview.io.HMMFile;
18 import jalview.io.StockholmFile;
19 import jalview.util.FileUtils;
20 import jalview.util.MessageManager;
21 import jalview.util.Platform;
22 import jalview.ws.params.ArgumentI;
23
24 import java.io.BufferedReader;
25 import java.io.File;
26 import java.io.IOException;
27 import java.io.InputStreamReader;
28 import java.io.PrintWriter;
29 import java.nio.file.Paths;
30 import java.util.ArrayList;
31 import java.util.Hashtable;
32 import java.util.List;
33
34 /**
35  * Base class for hmmbuild, hmmalign and hmmsearch
36  * 
37  * @author TZVanaalten
38  *
39  */
40 public abstract class HmmerCommand implements Runnable
41 {
42   public static final String HMMBUILD = "hmmbuild";
43
44   protected final AlignFrame af;
45
46   protected final AlignmentI alignment;
47
48   protected final List<ArgumentI> params;
49
50   /*
51    * constants for i18n lookup of passed parameter names
52    */
53   static final String DATABASE_KEY = "label.database";
54
55   static final String THIS_ALIGNMENT_KEY = "label.this_alignment";
56
57   static final String USE_ACCESSIONS_KEY = "label.use_accessions";
58
59   static final String AUTO_ALIGN_SEQS_KEY = "label.auto_align_seqs";
60
61   static final String NUMBER_OF_RESULTS_KEY = "label.number_of_results";
62
63   static final String TRIM_TERMINI_KEY = "label.trim_termini";
64
65   static final String RETURN_N_NEW_SEQ = "label.check_for_new_sequences";
66
67   static final String REPORTING_CUTOFF_KEY = "label.reporting_cutoff";
68
69   static final String CUTOFF_NONE = "label.default";
70
71   static final String CUTOFF_SCORE = "label.score";
72
73   static final String CUTOFF_EVALUE = "label.evalue";
74
75   static final String REPORTING_SEQ_EVALUE_KEY = "label.reporting_seq_evalue";
76
77   static final String REPORTING_DOM_EVALUE_KEY = "label.reporting_dom_evalue";
78
79   static final String REPORTING_SEQ_SCORE_KEY = "label.reporting_seq_score";
80
81   static final String REPORTING_DOM_SCORE_KEY = "label.reporting_dom_score";
82
83   static final String INCLUSION_SEQ_EVALUE_KEY = "label.inclusion_seq_evalue";
84
85   static final String INCLUSION_DOM_EVALUE_KEY = "label.inclusion_dom_evalue";
86
87   static final String INCLUSION_SEQ_SCORE_KEY = "label.inclusion_seq_score";
88
89   static final String INCLUSION_DOM_SCORE_KEY = "label.inclusion_dom_score";
90
91   static final String ARG_TRIM = "--trim";
92
93   static final String INCLUSION_THRESHOLD_KEY = "label.inclusion_threshold";
94
95   /**
96    * Constructor
97    * 
98    * @param alignFrame
99    * @param args
100    */
101   public HmmerCommand(AlignFrame alignFrame, List<ArgumentI> args)
102   {
103     af = alignFrame;
104     alignment = af.getViewport().getAlignment();
105     params = args;
106   }
107
108   /**
109    * Answers true if preference HMMER_PATH is set, and its value is the path to
110    * a directory that contains an executable <code>hmmbuild</code> or
111    * <code>hmmbuild.exe</code>, else false
112    * 
113    * @return
114    */
115   public static boolean isHmmerAvailable()
116   {
117     File exec = FileUtils.getExecutable(HMMBUILD,
118             Cache.getProperty(Preferences.HMMER_PATH));
119     return exec != null;
120   }
121
122   /**
123    * Uniquifies the sequences when exporting and stores their details in a
124    * hashtable
125    * 
126    * @param seqs
127    */
128   protected Hashtable stashSequences(SequenceI[] seqs)
129   {
130     return SeqsetUtils.uniquify(seqs, true);
131   }
132
133   /**
134    * Restores the sequence data lost by uniquifying
135    * 
136    * @param hashtable
137    * @param seqs
138    */
139   protected void recoverSequences(Hashtable hashtable, SequenceI[] seqs)
140   {
141     SeqsetUtils.deuniquify(hashtable, seqs);
142   }
143
144   /**
145    * Runs a command as a separate process and waits for it to complete. Answers
146    * true if the process return status is zero, else false.
147    * 
148    * @param commands
149    *          the executable command and any arguments to it
150    * @throws IOException
151    */
152   public boolean runCommand(List<String> commands)
153           throws IOException
154   {
155     List<String> args = Platform.isWindows() ? wrapWithCygwin(commands)
156             : commands;
157
158     try
159     {
160       ProcessBuilder pb = new ProcessBuilder(args);
161       pb.redirectErrorStream(true); // merge syserr to sysout
162       if (Platform.isWindows())
163       {
164         String path = pb.environment().get("Path");
165         path = jalview.bin.Cache.getProperty("CYGWIN_PATH") + ";" + path;
166         pb.environment().put("Path", path);
167       }
168       final Process p = pb.start();
169       new Thread(new Runnable()
170       {
171         @Override
172         public void run()
173         {
174           BufferedReader input = new BufferedReader(
175                   new InputStreamReader(p.getInputStream()));
176           try
177           {
178             String line = input.readLine();
179             while (line != null)
180             {
181               System.out.println(line);
182               line = input.readLine();
183             }
184           } catch (IOException e)
185           {
186             e.printStackTrace();
187           }
188         }
189       }).start();
190
191       p.waitFor();
192       int exitValue = p.exitValue();
193       if (exitValue != 0)
194       {
195         Cache.log.error("Command failed, return code = " + exitValue);
196         Cache.log.error("Command/args were: " + args.toString());
197       }
198       return exitValue == 0; // 0 is success, by convention
199     } catch (Exception e)
200     {
201       e.printStackTrace();
202       return false;
203     }
204   }
205
206   /**
207    * Converts the given command to a Cygwin "bash" command wrapper. The hmmer
208    * command and any arguments to it are converted into a single parameter to the
209    * bash command.
210    * 
211    * @param commands
212    */
213   protected List<String> wrapWithCygwin(List<String> commands)
214   {
215     File bash = FileUtils.getExecutable("bash",
216             Cache.getProperty(Preferences.CYGWIN_PATH));
217     if (bash == null)
218     {
219       Cache.log.error("Cygwin shell not found");
220       return commands;
221     }
222
223     List<String> wrapped = new ArrayList<>();
224     // wrapped.add("C:\Users\tva\run");
225     wrapped.add(bash.getAbsolutePath());
226     wrapped.add("-c");
227
228     /*
229      * combine hmmbuild/search/align and arguments to a single string
230      */
231     StringBuilder sb = new StringBuilder();
232     for (String cmd : commands)
233     {
234       sb.append(" ").append(cmd);
235     }
236     wrapped.add(sb.toString());
237
238     return wrapped;
239   }
240
241   /**
242    * Exports an alignment, and reference (RF) annotation if present, to the
243    * specified file, in Stockholm format, removing all HMM sequences
244    * 
245    * @param seqs
246    * @param toFile
247    * @param annotated
248    * @throws IOException
249    */
250   public void exportStockholm(SequenceI[] seqs, File toFile,
251           AnnotatedCollectionI annotated)
252           throws IOException
253   {
254     if (seqs == null)
255     {
256       return;
257     }
258     AlignmentI newAl = new Alignment(seqs);
259
260     if (!newAl.isAligned())
261     {
262       newAl.padGaps();
263     }
264
265     if (toFile != null && annotated != null)
266     {
267       AlignmentAnnotation[] annots = annotated.getAlignmentAnnotation();
268       if (annots != null)
269       {
270         for (AlignmentAnnotation annot : annots)
271         {
272           if (annot.label.contains("Reference") || "RF".equals(annot.label))
273           {
274             AlignmentAnnotation newRF;
275             if (annot.annotations.length > newAl.getWidth())
276             {
277               Annotation[] rfAnnots = new Annotation[newAl.getWidth()];
278               System.arraycopy(annot.annotations, 0, rfAnnots, 0,
279                       rfAnnots.length);
280               newRF = new AlignmentAnnotation("RF", "Reference Positions",
281                       rfAnnots);
282             }
283             else
284             {
285               newRF = new AlignmentAnnotation(annot);
286             }
287             newAl.addAnnotation(newRF);
288           }
289         }
290       }
291     }
292
293     for (SequenceI seq : newAl.getSequencesArray())
294     {
295       if (seq.getAnnotation() != null)
296       {
297         for (AlignmentAnnotation ann : seq.getAnnotation())
298         {
299           seq.removeAlignmentAnnotation(ann);
300         }
301       }
302     }
303
304     StockholmFile file = new StockholmFile(newAl);
305     String output = file.print(seqs, false);
306     PrintWriter writer = new PrintWriter(toFile);
307     writer.println(output);
308     writer.close();
309   }
310
311   /**
312    * Answers the full path to the given hmmer executable, or null if file cannot
313    * be found or is not executable
314    * 
315    * @param cmd
316    *          command short name e.g. hmmalign
317    * @return
318    * @throws IOException
319    */
320   protected String getCommandPath(String cmd)
321           throws IOException
322   {
323     String binariesFolder = Cache.getProperty(Preferences.HMMER_PATH);
324     // ensure any symlink to the directory is resolved:
325     binariesFolder = Paths.get(binariesFolder).toRealPath().toString();
326     File file = FileUtils.getExecutable(cmd, binariesFolder);
327     if (file == null && af != null)
328     {
329       JvOptionPane.showInternalMessageDialog(af, MessageManager
330               .formatMessage("label.executable_not_found", cmd));
331     }
332
333     return file == null ? null : getFilePath(file, true);
334   }
335
336   /**
337    * Exports an HMM to the specified file
338    * 
339    * @param hmm
340    * @param hmmFile
341    * @throws IOException
342    */
343   public void exportHmm(HiddenMarkovModel hmm, File hmmFile)
344           throws IOException
345   {
346     if (hmm != null)
347     {
348       HMMFile file = new HMMFile(hmm);
349       PrintWriter writer = new PrintWriter(hmmFile);
350       writer.print(file.print());
351       writer.close();
352     }
353   }
354
355   // TODO is needed?
356   /**
357    * Exports a sequence to the specified file
358    * 
359    * @param hmm
360    * @param hmmFile
361    * @throws IOException
362    */
363   public void exportSequence(SequenceI seq, File seqFile) throws IOException
364   {
365     if (seq != null)
366     {
367       FastaFile file = new FastaFile();
368       PrintWriter writer = new PrintWriter(seqFile);
369       writer.print(file.print(new SequenceI[] { seq }, false));
370       writer.close();
371     }
372   }
373
374   /**
375    * Answers the HMM profile for the profile sequence the user selected (default
376    * is just the first HMM sequence in the alignment)
377    * 
378    * @return
379    */
380   protected HiddenMarkovModel getHmmProfile()
381   {
382     String alignToParamName = MessageManager.getString("label.use_hmm");
383     for (ArgumentI arg : params)
384     {
385       String name = arg.getName();
386       if (name.equals(alignToParamName))
387       {
388         String seqName = arg.getValue();
389         SequenceI hmmSeq = alignment.findName(seqName);
390         if (hmmSeq.hasHMMProfile())
391         {
392           return hmmSeq.getHMM();
393         }
394       }
395     }
396     return null;
397   }
398
399   /**
400    * Answers the HMM profile for the profile sequence the user selected (default
401    * is just the first HMM sequence in the alignment)
402    * 
403    * @return
404    */
405   protected SequenceI getSequence()
406   {
407     String alignToParamName = MessageManager
408             .getString("label.use_sequence");
409     for (ArgumentI arg : params)
410     {
411       String name = arg.getName();
412       if (name.equals(alignToParamName))
413       {
414         String seqName = arg.getValue();
415         SequenceI seq = alignment.findName(seqName);
416         return seq;
417       }
418     }
419     return null;
420   }
421
422   /**
423    * Answers an absolute path to the given file, in a format suitable for
424    * processing by a hmmer command. On a Windows platform, the native Windows file
425    * path is converted to Cygwin format, by replacing '\'with '/' and drive letter
426    * X with /cygdrive/x.
427    * 
428    * @param resultFile
429    * @param isInCygwin
430    *                     True if file is to be read/written from within the Cygwin
431    *                     shell. Should be false for any imports.
432    * @return
433    */
434   protected String getFilePath(File resultFile, boolean isInCygwin)
435   {
436     String path = resultFile.getAbsolutePath();
437     if (Platform.isWindows() && isInCygwin)
438     {
439       // the first backslash escapes '\' for the regular expression argument
440       path = path.replaceAll("\\" + File.separator, "/");
441       int colon = path.indexOf(':');
442       if (colon > 0)
443       {
444         String drive = path.substring(0, colon);
445         path = path.replaceAll(drive + ":", "/cygdrive/" + drive);
446       }
447     }
448
449     return path;
450   }
451
452   /**
453    * A helper method that deletes any HMM consensus sequence from the given
454    * collection, and from the parent alignment if <code>ac</code> is a subgroup
455    * 
456    * @param ac
457    */
458   void deleteHmmSequences(AnnotatedCollectionI ac)
459   {
460     List<SequenceI> hmmSeqs = ac.getHmmSequences();
461     for (SequenceI hmmSeq : hmmSeqs)
462     {
463       if (ac instanceof SequenceGroup)
464       {
465         ((SequenceGroup) ac).deleteSequence(hmmSeq, false);
466         AnnotatedCollectionI context = ac.getContext();
467         if (context != null && context instanceof AlignmentI)
468         {
469           ((AlignmentI) context).deleteSequence(hmmSeq);
470         }
471       }
472       else
473       {
474         ((AlignmentI) ac).deleteSequence(hmmSeq);
475       }
476     }
477   }
478
479   /**
480    * Sets the names of any duplicates within the given sequences to include their
481    * respective lengths. Deletes any duplicates that have the same name after this
482    * step
483    * 
484    * @param seqs
485    */
486   void renameDuplicates(AlignmentI al)
487   {
488
489     SequenceI[] seqs = al.getSequencesArray();
490     List<Boolean> wasRenamed = new ArrayList<>();
491
492     for (SequenceI seq : seqs)
493     {
494       wasRenamed.add(false);
495     }
496
497     for (int i = 0; i < seqs.length; i++)
498     {
499       for (int j = 0; j < seqs.length; j++)
500       {
501         if (seqs[i].getName().equals(seqs[j].getName()) && i != j
502                 && !wasRenamed.get(j))
503         {
504
505           wasRenamed.set(i, true);
506           String range = "/" + seqs[j].getStart() + "-" + seqs[j].getEnd();
507           // setting sequence name to include range - to differentiate between
508           // sequences of the same name. Currently have to include the range twice
509           // because the range is removed (once) when setting the name
510           // TODO come up with a better way of doing this
511           seqs[j].setName(seqs[j].getName() + range + range);
512         }
513
514       }
515       if (wasRenamed.get(i))
516       {
517         String range = "/" + seqs[i].getStart() + "-" + seqs[i].getEnd();
518         seqs[i].setName(seqs[i].getName() + range + range);
519       }
520     }
521
522     for (int i = 0; i < seqs.length; i++)
523     {
524       for (int j = 0; j < seqs.length; j++)
525       {
526         if (seqs[i].getName().equals(seqs[j].getName()) && i != j)
527         {
528           al.deleteSequence(j);
529         }
530       }
531     }
532   }
533
534 }