JAL-2663 flag and filter to exclude non-standard amino acids from submission
[jalview.git] / src / jalview / ws / jws2 / AbstractJabaCalcWorker.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
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
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ws.jws2;
22
23 import jalview.analysis.AlignSeq;
24 import jalview.analysis.SeqsetUtils;
25 import jalview.api.AlignViewportI;
26 import jalview.api.AlignmentViewPanel;
27 import jalview.datamodel.AlignmentAnnotation;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.AnnotatedCollectionI;
30 import jalview.datamodel.SequenceI;
31 import jalview.gui.AlignFrame;
32 import jalview.gui.IProgressIndicator;
33 import jalview.schemes.ResidueProperties;
34 import jalview.workers.AlignCalcWorker;
35 import jalview.ws.jws2.dm.AAConSettings;
36 import jalview.ws.jws2.dm.JabaWsParamSet;
37 import jalview.ws.jws2.jabaws2.Jws2Instance;
38 import jalview.ws.params.WsParamSetI;
39
40 import java.util.ArrayList;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44
45 import compbio.data.sequence.FastaSequence;
46 import compbio.metadata.Argument;
47 import compbio.metadata.ChunkHolder;
48 import compbio.metadata.JobStatus;
49 import compbio.metadata.JobSubmissionException;
50 import compbio.metadata.Option;
51 import compbio.metadata.ResultNotAvailableException;
52
53 public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
54 {
55
56   protected Jws2Instance service;
57
58   protected WsParamSetI preset;
59
60   protected List<Argument> arguments;
61
62   protected IProgressIndicator guiProgress;
63
64   protected boolean submitGaps = true;
65
66   /**
67    * by default, we filter out non-standard residues before submission
68    */
69   private boolean filterNonStandardResidues = true;
70
71   /**
72    * Recover any existing parameters for this service
73    */
74   protected void initViewportParams()
75   {
76     if (getCalcId() != null)
77     {
78       ((jalview.gui.AlignViewport) alignViewport).setCalcIdSettingsFor(
79               getCalcId(),
80               new AAConSettings(true, service, this.preset,
81                       (arguments != null) ? JabaParamStore
82                               .getJwsArgsfromJaba(arguments) : null), true);
83     }
84   }
85
86   /**
87    * 
88    * @return null or a string used to recover all annotation generated by this
89    *         worker
90    */
91   public abstract String getCalcId();
92
93   public WsParamSetI getPreset()
94   {
95     return preset;
96   }
97
98   public List<Argument> getArguments()
99   {
100     return arguments;
101   }
102
103   /**
104    * reconfigure and restart the AAConClient. This method will spawn a new
105    * thread that will wait until any current jobs are finished, modify the
106    * parameters and restart the conservation calculation with the new values.
107    * 
108    * @param newpreset
109    * @param newarguments
110    */
111   public void updateParameters(final WsParamSetI newpreset,
112           final List<Argument> newarguments)
113   {
114     preset = newpreset;
115     arguments = newarguments;
116     calcMan.startWorker(this);
117     initViewportParams();
118   }
119
120   public List<Option> getJabaArguments()
121   {
122     List<Option> newargs = new ArrayList<>();
123     if (preset != null && preset instanceof JabaWsParamSet)
124     {
125       newargs.addAll(((JabaWsParamSet) preset).getjabaArguments());
126     }
127     if (arguments != null && arguments.size() > 0)
128     {
129       for (Argument rg : arguments)
130       {
131         if (Option.class.isAssignableFrom(rg.getClass()))
132         {
133           newargs.add((Option) rg);
134         }
135       }
136     }
137     return newargs;
138   }
139
140   protected boolean alignedSeqs = true;
141
142   protected boolean nucleotidesAllowed = false;
143
144   protected boolean proteinAllowed = false;
145
146   /**
147    * record sequences for mapping result back to afterwards
148    */
149   protected boolean bySequence = false;
150
151   protected Map<String, SequenceI> seqNames;
152
153   protected boolean[] gapMap;
154
155   int realw;
156
157   protected int start;
158
159   int end;
160
161   public AbstractJabaCalcWorker(AlignViewportI alignViewport,
162           AlignmentViewPanel alignPanel)
163   {
164     super(alignViewport, alignPanel);
165   }
166
167   public AbstractJabaCalcWorker(Jws2Instance service,
168           AlignFrame alignFrame, WsParamSetI preset, List<Argument> paramset)
169   {
170     this(alignFrame.getCurrentView(), alignFrame.alignPanel);
171     this.guiProgress = alignFrame;
172     this.preset = preset;
173     this.arguments = paramset;
174     this.service = service;
175   }
176
177   /**
178    * 
179    * @return true if the submission thread should attempt to submit data
180    */
181   abstract boolean hasService();
182
183   volatile String rslt = "JOB NOT DEFINED";
184
185   @Override
186   public void run()
187   {
188     if (!hasService())
189     {
190       return;
191     }
192     long progressId = -1;
193
194     int serverErrorsLeft = 3;
195
196     StringBuffer msg = new StringBuffer();
197     try
198     {
199       if (checkDone())
200       {
201         return;
202       }
203       List<compbio.data.sequence.FastaSequence> seqs = getInputSequences(
204               alignViewport.getAlignment(),
205               bySequence ? alignViewport.getSelectionGroup() : null);
206
207       if (seqs == null || !checkValidInputSeqs(true, seqs))
208       {
209         calcMan.workerComplete(this);
210         return;
211       }
212
213       AlignmentAnnotation[] aa = alignViewport.getAlignment()
214               .getAlignmentAnnotation();
215       if (guiProgress != null)
216       {
217         guiProgress.setProgressBar("JABA " + getServiceActionText(),
218                 progressId = System.currentTimeMillis());
219       }
220       rslt = submitToService(seqs);
221
222       boolean finished = false;
223       long rpos = 0;
224       do
225       {
226         JobStatus status = getJobStatus(rslt);
227         if (status.equals(JobStatus.FINISHED))
228         {
229           finished = true;
230         }
231         if (calcMan.isPending(this) && isInteractiveUpdate())
232         {
233           finished = true;
234           // cancel this job and yield to the new job
235           try
236           {
237             if (cancelJob(rslt))
238             {
239               System.err.println("Cancelled AACon job: " + rslt);
240             }
241             else
242             {
243               System.err.println("FAILED TO CANCEL AACon job: " + rslt);
244             }
245
246           } catch (Exception x)
247           {
248
249           }
250           rslt = "CANCELLED JOB";
251           return;
252         }
253         long cpos;
254         ChunkHolder stats = null;
255         do
256         {
257           cpos = rpos;
258           boolean retry = false;
259           do
260           {
261             try
262             {
263               stats = pullExecStatistics(rslt, rpos);
264             } catch (Exception x)
265             {
266
267               if (x.getMessage().contains(
268                       "Position in a file could not be negative!"))
269               {
270                 // squash index out of bounds exception- seems to happen for
271                 // disorder predictors which don't (apparently) produce any
272                 // progress information and JABA server throws an exception
273                 // because progress length is -1.
274                 stats = null;
275               }
276               else
277               {
278                 if (--serverErrorsLeft > 0)
279                 {
280                   retry = true;
281                   try
282                   {
283                     Thread.sleep(200);
284                   } catch (InterruptedException q)
285                   {
286                   }
287                   ;
288                 }
289                 else
290                 {
291                   throw x;
292                 }
293               }
294             }
295           } while (retry);
296           if (stats != null)
297           {
298             System.out.print(stats.getChunk());
299             msg.append(stats);
300             rpos = stats.getNextPosition();
301           }
302         } while (stats != null && rpos > cpos);
303
304         if (!finished && status.equals(JobStatus.FAILED))
305         {
306           try
307           {
308             Thread.sleep(200);
309           } catch (InterruptedException x)
310           {
311           }
312           ;
313         }
314       } while (!finished);
315       if (serverErrorsLeft > 0)
316       {
317         try
318         {
319           Thread.sleep(200);
320         } catch (InterruptedException x)
321         {
322         }
323         if (collectAnnotationResultsFor(rslt))
324         {
325           jalview.bin.Cache.log
326                   .debug("Updating result annotation from Job " + rslt
327                           + " at " + service.getUri());
328           updateResultAnnotation(true);
329           ap.adjustAnnotationHeight();
330         }
331       }
332     }
333
334     catch (JobSubmissionException x)
335     {
336
337       System.err.println("submission error with " + getServiceActionText()
338               + " :");
339       x.printStackTrace();
340       calcMan.disableWorker(this);
341     } catch (ResultNotAvailableException x)
342     {
343       System.err.println("collection error:\nJob ID: " + rslt);
344       x.printStackTrace();
345       calcMan.disableWorker(this);
346
347     } catch (OutOfMemoryError error)
348     {
349       calcMan.disableWorker(this);
350
351       // consensus = null;
352       // hconsensus = null;
353       ap.raiseOOMWarning(getServiceActionText(), error);
354     } catch (Exception x)
355     {
356       calcMan.disableWorker(this);
357
358       // consensus = null;
359       // hconsensus = null;
360       System.err
361               .println("Blacklisting worker due to unexpected exception:");
362       x.printStackTrace();
363     } finally
364     {
365
366       calcMan.workerComplete(this);
367       if (ap != null)
368       {
369         calcMan.workerComplete(this);
370         if (guiProgress != null && progressId != -1)
371         {
372           guiProgress.setProgressBar("", progressId);
373         }
374         ap.paintAlignment(true);
375       }
376       if (msg.length() > 0)
377       {
378         // TODO: stash message somewhere in annotation or alignment view.
379         // code below shows result in a text box popup
380         /*
381          * jalview.gui.CutAndPasteTransfer cap = new
382          * jalview.gui.CutAndPasteTransfer(); cap.setText(msg.toString());
383          * jalview.gui.Desktop.addInternalFrame(cap,
384          * "Job Status for "+getServiceActionText(), 600, 400);
385          */
386       }
387     }
388
389   }
390
391   /**
392    * validate input for dynamic/non-dynamic update context
393    * 
394    * @param dynamic
395    * @param seqs
396    * @return true if input is valid
397    */
398   abstract boolean checkValidInputSeqs(boolean dynamic,
399           List<FastaSequence> seqs);
400
401   abstract String submitToService(
402           List<compbio.data.sequence.FastaSequence> seqs)
403           throws JobSubmissionException;
404
405   abstract boolean cancelJob(String rslt) throws Exception;
406
407   abstract JobStatus getJobStatus(String rslt) throws Exception;
408
409   abstract ChunkHolder pullExecStatistics(String rslt, long rpos);
410
411   abstract boolean collectAnnotationResultsFor(String rslt)
412           throws ResultNotAvailableException;
413
414   public void cancelCurrentJob()
415   {
416     try
417     {
418       String id = rslt;
419       if (cancelJob(rslt))
420       {
421         System.err.println("Cancelled job " + id);
422       }
423       else
424       {
425         System.err.println("Job " + id + " couldn't be cancelled.");
426       }
427     } catch (Exception q)
428     {
429       q.printStackTrace();
430     }
431   }
432
433   /**
434    * Interactive updating. Analysis calculations that work on the currently
435    * displayed alignment data should cancel existing jobs when the input data
436    * has changed.
437    * 
438    * @return true if a running job should be cancelled because new input data is
439    *         available for analysis
440    */
441   abstract boolean isInteractiveUpdate();
442
443   public List<FastaSequence> getInputSequences(AlignmentI alignment,
444           AnnotatedCollectionI inputSeqs)
445   {
446     if (alignment == null || alignment.getWidth() <= 0
447             || alignment.getSequences() == null || alignment.isNucleotide() ? !nucleotidesAllowed
448             : !proteinAllowed)
449     {
450       return null;
451     }
452     if (inputSeqs == null || inputSeqs.getWidth() <= 0
453             || inputSeqs.getSequences() == null
454             || inputSeqs.getSequences().size() < 1)
455     {
456       inputSeqs = alignment;
457     }
458
459     List<compbio.data.sequence.FastaSequence> seqs = new ArrayList<>();
460
461     int minlen = 10;
462     int ln = -1;
463     if (bySequence)
464     {
465       seqNames = new HashMap<>();
466     }
467     gapMap = new boolean[0];
468     start = inputSeqs.getStartRes();
469     end = inputSeqs.getEndRes();
470
471     for (SequenceI sq : (inputSeqs.getSequences()))
472     {
473       if (bySequence ? sq.findPosition(end + 1)
474               - sq.findPosition(start + 1) > minlen - 1 : sq.getEnd()
475               - sq.getStart() > minlen - 1)
476       {
477         String newname = SeqsetUtils.unique_name(seqs.size() + 1);
478         // make new input sequence with or without gaps
479         if (seqNames != null)
480         {
481           seqNames.put(newname, sq);
482         }
483         FastaSequence seq;
484         if (submitGaps)
485         {
486           seqs.add(seq = new compbio.data.sequence.FastaSequence(newname,
487                   sq.getSequenceAsString()));
488           if (gapMap == null || gapMap.length < seq.getSequence().length())
489           {
490             boolean[] tg = gapMap;
491             gapMap = new boolean[seq.getLength()];
492             System.arraycopy(tg, 0, gapMap, 0, tg.length);
493             for (int p = tg.length; p < gapMap.length; p++)
494             {
495               gapMap[p] = false; // init as a gap
496             }
497           }
498           for (int apos : sq.gapMap())
499           {
500             if (!filterNonStandardResidues
501                     || ResidueProperties.aaIndex[sq.getCharAt(apos)] < 20)
502             {
503               gapMap[apos] = true; // aligned and real amino acid residue
504             }
505             ;
506           }
507         }
508         else
509         {
510           seqs.add(seq = new compbio.data.sequence.FastaSequence(newname,
511                   AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
512                           sq.getSequenceAsString(start, end + 1))));
513         }
514         if (seq.getSequence().length() > ln)
515         {
516           ln = seq.getSequence().length();
517         }
518       }
519     }
520     if (alignedSeqs && submitGaps)
521     {
522       realw = 0;
523       for (int i = 0; i < gapMap.length; i++)
524       {
525         if (gapMap[i])
526         {
527           realw++;
528         }
529       }
530       // try real hard to return something submittable
531       // TODO: some of AAcon measures need a minimum of two or three amino
532       // acids at each position, and AAcon doesn't gracefully degrade.
533       for (int p = 0; p < seqs.size(); p++)
534       {
535         FastaSequence sq = seqs.get(p);
536         int l = sq.getSequence().length();
537         // strip gapped columns
538         char[] padded = new char[realw], orig = sq.getSequence()
539                 .toCharArray();
540         for (int i = 0, pp = 0; i < realw; pp++)
541         {
542           if (gapMap[pp])
543           {
544             if (orig.length > pp)
545             {
546               padded[i++] = orig[pp];
547             }
548             else
549             {
550               padded[i++] = '-';
551             }
552           }
553         }
554         seqs.set(p, new compbio.data.sequence.FastaSequence(sq.getId(),
555                 new String(padded)));
556       }
557     }
558     return seqs;
559   }
560
561   @Override
562   public void updateAnnotation()
563   {
564     updateResultAnnotation(false);
565   }
566
567   public abstract void updateResultAnnotation(boolean immediate);
568
569   public abstract String getServiceActionText();
570
571   /**
572    * notify manager that we have started, and wait for a free calculation slot
573    * 
574    * @return true if slot is obtained and work still valid, false if another
575    *         thread has done our work for us.
576    */
577   protected boolean checkDone()
578   {
579     calcMan.notifyStart(this);
580     ap.paintAlignment(false);
581     while (!calcMan.notifyWorking(this))
582     {
583       if (calcMan.isWorking(this))
584       {
585         return true;
586       }
587       try
588       {
589         if (ap != null)
590         {
591           ap.paintAlignment(false);
592         }
593
594         Thread.sleep(200);
595       } catch (Exception ex)
596       {
597         ex.printStackTrace();
598       }
599     }
600     if (alignViewport.isClosed())
601     {
602       abortAndDestroy();
603       return true;
604     }
605     return false;
606   }
607
608   protected void updateOurAnnots(List<AlignmentAnnotation> ourAnnot)
609   {
610     List<AlignmentAnnotation> our = ourAnnots;
611     ourAnnots = ourAnnot;
612     AlignmentI alignment = alignViewport.getAlignment();
613     if (our != null)
614     {
615       if (our.size() > 0)
616       {
617         for (AlignmentAnnotation an : our)
618         {
619           if (!ourAnnots.contains(an))
620           {
621             // remove the old annotation
622             alignment.deleteAnnotation(an);
623           }
624         }
625       }
626       our.clear();
627
628       ap.adjustAnnotationHeight();
629     }
630   }
631
632 }