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