JAL-3878 Add multiple sequence operation.
[jalview.git] / src / jalview / ws2 / operations / AlignmentWorker.java
1 package jalview.ws2.operations;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Collections;
6 import java.util.HashMap;
7 import java.util.LinkedHashMap;
8 import java.util.List;
9 import java.util.Map;
10 import java.util.Objects;
11 import java.util.function.Consumer;
12
13 import jalview.analysis.AlignSeq;
14 import jalview.analysis.AlignmentSorter;
15 import jalview.analysis.SeqsetUtils;
16 import jalview.analysis.SeqsetUtils.SequenceInfo;
17 import jalview.bin.Cache;
18 import jalview.datamodel.AlignedCodonFrame;
19 import jalview.datamodel.Alignment;
20 import jalview.datamodel.AlignmentI;
21 import jalview.datamodel.AlignmentOrder;
22 import jalview.datamodel.AlignmentView;
23 import jalview.datamodel.HiddenColumns;
24 import jalview.datamodel.Sequence;
25 import jalview.datamodel.SequenceI;
26 import jalview.gui.AlignViewport;
27 import jalview.util.MessageManager;
28 import jalview.ws.params.ArgumentI;
29 import jalview.ws2.WSJob;
30 import jalview.ws2.WSJobStatus;
31
32 public class AlignmentWorker extends AbstractPollableWorker
33 {
34   private AlignmentOperation operation;
35   
36   private Consumer<AlignmentResult> resultConsumer;
37   
38   private AlignmentView msa;
39
40   private AlignmentI dataset;
41
42   private AlignViewport viewport;
43
44   private List<AlignedCodonFrame> codonFrame = new ArrayList<>();
45
46   private List<ArgumentI> args = Collections.emptyList();
47
48   private boolean submitGaps = false;
49
50   private boolean preserveOrder = false;
51
52   private char gapCharacter;
53
54   private WSJobList<AlignmentJob> jobs = new WSJobList<>();
55
56   private Map<Long, Integer> exceptionCount = new HashMap<>();
57
58   private static final int MAX_RETRY = 5;
59
60   private static class JobInput
61   {
62     final List<SequenceI> inputSequences;
63
64     final List<SequenceI> emptySequences;
65
66     final Map<String, SequenceInfo> sequenceNames;
67
68     private JobInput(List<SequenceI> inputSequences,
69         List<SequenceI> emptySequences,
70         Map<String, SequenceInfo> names)
71     {
72       this.inputSequences = Collections.unmodifiableList(inputSequences);
73       this.emptySequences = Collections.unmodifiableList(emptySequences);
74       this.sequenceNames = names;
75     }
76
77     boolean isInputValid()
78     {
79       return inputSequences.size() >= 2;
80     }
81   }
82   
83   public class AlignmentJob extends WSJob
84   {
85     private List<SequenceI> inputSequences;
86     private List<SequenceI> emptySequences;
87     private Map<String, SequenceInfo> sequenceNames;
88     
89     private AlignmentJob(String serviceProvider, String serviceName, 
90         String hostName)
91     {
92       super(serviceProvider, serviceName, hostName);
93     }
94     
95     private void setInput(JobInput input) {
96       inputSequences = input.inputSequences;
97       emptySequences = input.emptySequences;
98       sequenceNames = input.sequenceNames;
99     }
100   }
101
102   public class AlignmentResult
103   {
104     AlignmentI aln;
105
106     List<AlignmentOrder> alorders;
107
108     HiddenColumns hidden;
109
110     AlignmentResult(AlignmentI aln, List<AlignmentOrder> alorders,
111         HiddenColumns hidden)
112     {
113       this.aln = aln;
114       this.alorders = alorders;
115       this.hidden = hidden;
116     }
117
118     public AlignmentI getAlignment()
119     {
120       return aln;
121     }
122
123     public List<AlignmentOrder> getAlignmentOrders()
124     {
125       return alorders;
126     }
127
128     public HiddenColumns getHiddenColumns()
129     {
130       return hidden;
131     }
132   }
133
134   public AlignmentWorker(AlignmentOperation operation,
135       AlignmentView msa, List<ArgumentI> args,
136       boolean submitGaps, boolean preserveOrder, AlignViewport viewport)
137   {
138     this.operation = operation;
139     this.msa = msa;
140     this.dataset = viewport.getAlignment().getDataset();
141     List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
142         viewport.getAlignment().getCodonFrames(), Collections.emptyList());
143     this.codonFrame.addAll(cf);
144     this.args = args;
145     this.submitGaps = submitGaps;
146     this.preserveOrder = preserveOrder;
147     this.viewport = viewport;
148     this.gapCharacter = viewport.getGapCharacter();
149   }
150
151   @Override
152   public Operation getOperation()
153   {
154     return operation;
155   }
156
157   @Override
158   public WSJobList<? extends WSJob> getJobs()
159   {
160     return jobs;
161   }
162   
163   public void setResultConsumer(Consumer<AlignmentResult> consumer)
164   {
165     this.resultConsumer = consumer;
166   }
167   
168   @Override
169   public void start() throws IOException
170   {
171     Cache.log.info(String.format("Starting new %s job.", operation.getName()));
172     SequenceI[][] conmsa = msa.getVisibleContigs('-');
173     if (conmsa == null)
174     {
175       return;
176     }
177     int numValid = 0;
178     for (int i =  0; i < conmsa.length; i++)
179     {
180       JobInput input = prepareInputData(conmsa[i], 2, submitGaps);
181       AlignmentJob job = new AlignmentJob(operation.service.getProviderName(),
182           operation.getName(), operation.getHostName());
183       job.setJobNum(i);
184       job.setInput(input);
185       jobs.add(job);
186       listeners.fireJobCreated(job);
187       if (input.isInputValid())
188       {
189         int count;
190         String jobId = null;
191         do
192         {
193           count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
194           try
195           {
196             jobId = operation.service.submit(input.inputSequences, args);
197             Cache.log.debug((String.format("Job %s submitted", job)));
198             exceptionCount.remove(job.getUid());
199           } catch (IOException e)
200           {
201             exceptionCount.put(job.getUid(), --count);
202           }
203         } while (jobId == null && count > 0);
204         if (jobId != null)
205         {
206           job.setJobId(jobId);
207           job.setStatus(WSJobStatus.SUBMITTED);
208           numValid++;
209         }
210         else
211         {
212           job.setStatus(WSJobStatus.SERVER_ERROR);
213         }
214       }
215       else
216       {
217         job.setStatus(WSJobStatus.INVALID);
218         job.setErrorLog(
219             MessageManager.getString("label.empty_alignment_job"));
220       }
221     }
222     if (numValid > 0)
223     {
224       listeners.fireWorkerStarted();
225     }
226     else
227     {
228       listeners.fireWorkerNotStarted();
229     }
230   }
231   
232
233   private static JobInput prepareInputData(SequenceI[] sequences,
234       int minLength, boolean submitGaps)
235   {
236     assert minLength >= 0 : MessageManager.getString(
237         "error.implementation_error_minlen_must_be_greater_zero");
238     int numSeq = 0;
239     for (SequenceI seq : sequences)
240     {
241       if (seq.getEnd() - seq.getStart() >= minLength)
242       {
243         numSeq++;
244       }
245     }
246
247     List<SequenceI> inputSequences = new ArrayList<>();
248     List<SequenceI> emptySequences = new ArrayList<>();
249     Map<String, SequenceInfo> names = new LinkedHashMap<>();
250     for (int i = 0; i < sequences.length; i++)
251     {
252       SequenceI seq = sequences[i];
253       String newName = SeqsetUtils.unique_name(i);
254       var hash = SeqsetUtils.SeqCharacterHash(seq);
255       names.put(newName, hash);
256       if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
257       {
258         String seqString = seq.getSequenceAsString();
259         if (!submitGaps)
260         {
261           seqString = AlignSeq.extractGaps(
262               jalview.util.Comparison.GapChars, seqString);
263         }
264         inputSequences.add(new Sequence(newName, seqString));
265       }
266       else
267       {
268         String seqString = "";
269         if (seq.getEnd() >= seq.getStart()) // true if gaps only
270         {
271           seqString = seq.getSequenceAsString();
272           if (!submitGaps)
273           {
274             seqString = AlignSeq.extractGaps(
275                 jalview.util.Comparison.GapChars, seqString);
276           }
277         }
278         emptySequences.add(new Sequence(newName, seqString));
279       }
280     }
281
282     return new JobInput(inputSequences, emptySequences, names);
283   }
284
285   @Override
286   public void done()
287   {
288     listeners.fireWorkerCompleting();
289     Map<Long, AlignmentI> results = new LinkedHashMap<>();
290     for (WSJob job : getJobs())
291     {
292       if (job.getStatus().isFailed())
293         continue;
294       try
295       {
296         AlignmentI alignment = operation.getAlignmentSupplier().getAlignment(job);
297         if (alignment != null)
298         {
299           results.put(job.getUid(), alignment);
300         }
301       } catch (Exception e)
302       {
303         if (!operation.getWebService().handleCollectionError(job, e))
304         {
305           Cache.log.error("Couldn't get alignment for job.", e);
306           // TODO: Increment exception count and retry.
307           job.setStatus(WSJobStatus.SERVER_ERROR);
308         }
309       }
310     }
311     if (results.size() > 0)
312     {
313       AlignmentResult out = prepareResult(results);
314       resultConsumer.accept(out);
315     }
316     else
317     {
318       resultConsumer.accept(null);
319     }
320     listeners.fireWorkerCompleted();
321   }
322
323   private AlignmentResult prepareResult(Map<Long, AlignmentI> alignments)
324   {
325     List<AlignmentOrder> alorders = new ArrayList<>();
326     SequenceI[][] results = new SequenceI[jobs.size()][];
327     AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
328     for (int i = 0; i < jobs.size(); i++)
329     {
330       AlignmentJob job = jobs.get(i);
331       AlignmentI aln = alignments.get(job.getUid());
332       if (aln != null) // equivalent of job.hasResults()
333       {
334         /* Get the alignment including any empty sequences in the original
335          * order with original ids. */
336         char gapChar = aln.getGapCharacter();
337         List<SequenceI> emptySeqs = job.emptySequences;
338         List<SequenceI> alnSeqs = aln.getSequences();
339         // find the width of the longest sequence
340         int width = 0;
341         for (var seq : alnSeqs)
342           width = Integer.max(width, seq.getLength());
343         for (var emptySeq : emptySeqs)
344           width = Integer.max(width, emptySeq.getLength());
345         // pad shorter sequences with gaps
346         String gapSeq = String.join("",
347             Collections.nCopies(width, Character.toString(gapChar)));
348         List<SequenceI> seqs = new ArrayList<>(
349             alnSeqs.size() + emptySeqs.size());
350         seqs.addAll(alnSeqs);
351         seqs.addAll(emptySeqs);
352         for (var seq : seqs)
353         {
354           if (seq.getLength() < width)
355             seq.setSequence(seq.getSequenceAsString()
356                 + gapSeq.substring(seq.getLength()));
357         }
358         SequenceI[] result = seqs.toArray(new SequenceI[0]);
359         AlignmentOrder msaOrder = new AlignmentOrder(result);
360         AlignmentSorter.recoverOrder(result);
361         Map<String, SequenceInfo> names = new HashMap<>(job.sequenceNames);
362         // FIXME first call to deuniquify alters original alignment
363         SeqsetUtils.deuniquify(names, result);
364         alorders.add(msaOrder);
365         results[i] = result;
366         orders[i] = msaOrder;
367       }
368       else
369       {
370         results[i] = null;
371       }
372     }
373
374     Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
375     // free references to original data
376     for (int i = 0; i < jobs.size(); i++)
377     {
378       results[i] = null;
379       orders[i] = null;
380     }
381     SequenceI[] alignment = (SequenceI[]) newView[0];
382     HiddenColumns hidden = (HiddenColumns) newView[1];
383     Alignment aln = new Alignment(alignment);
384     aln.setProperty("Alignment Program", operation.getName());
385     if (dataset != null)
386       aln.setDataset(dataset);
387
388     propagateDatasetMappings(aln);
389     return new AlignmentResult(aln, alorders, hidden);
390     // displayNewFrame(aln, alorders, hidden);
391   }
392
393   /*
394    * conserves dataset references to sequence objects returned from web
395    * services. propagate codon frame data to alignment.
396    */
397   private void propagateDatasetMappings(Alignment aln)
398   {
399     if (codonFrame != null)
400     {
401       SequenceI[] alignment = aln.getSequencesArray();
402       for (SequenceI seq : alignment)
403       {
404         for (AlignedCodonFrame acf : codonFrame)
405         {
406           if (acf != null && acf.involvesSequence(seq))
407           {
408             aln.addCodonFrame(acf);
409             break;
410           }
411         }
412       }
413     }
414   }
415 }