Skip to content

Commit ef7f873

Browse files
authored
Merge pull request #3116 from antgonza/add-default-processing
Add default processing
2 parents e30107c + 4a97f92 commit ef7f873

File tree

11 files changed

+277
-8
lines changed

11 files changed

+277
-8
lines changed

qiita_db/metadata_template/prep_template.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,3 +713,196 @@ def modification_timestamp(self):
713713
@staticmethod
714714
def max_samples():
715715
return qdb.util.max_preparation_samples()
716+
717+
def add_default_workflow(self, user):
718+
"""The modification timestamp of the prep information
719+
720+
Parameters
721+
----------
722+
user : qiita_db.user.User
723+
The user that requested to add the default workflows
724+
725+
Returns
726+
-------
727+
ProcessingWorkflow
728+
The workflow created
729+
730+
Raises
731+
------
732+
ValueError
733+
a. If this preparation doesn't have valid workflows
734+
b. This preparation has been fully processed (no new steps needed)
735+
c. If there is no valid initial artifact to start the workflow
736+
"""
737+
# helper functions to avoid duplication of code
738+
739+
def _get_node_info(workflow, node):
740+
# retrieves the merging scheme of a node
741+
parent = list(workflow.graph.predecessors(node))
742+
if parent:
743+
parent = parent.pop()
744+
pdp = parent.default_parameter
745+
pcmd = pdp.command
746+
pparams = pdp.values
747+
else:
748+
pcmd = None
749+
pparams = {}
750+
751+
dp = node.default_parameter
752+
cparams = dp.values
753+
ccmd = dp.command
754+
755+
parent_cmd_name = None
756+
parent_merging_scheme = None
757+
if pcmd is not None:
758+
parent_cmd_name = pcmd.name
759+
parent_merging_scheme = pcmd.merging_scheme
760+
761+
return qdb.util.human_merging_scheme(
762+
ccmd.name, ccmd.merging_scheme, parent_cmd_name,
763+
parent_merging_scheme, cparams, [], pparams)
764+
765+
def _get_predecessors(workflow, node):
766+
# recursive method to get predecessors of a given node
767+
pred = []
768+
for pnode in workflow.graph.predecessors(node):
769+
pred = _get_predecessors(workflow, pnode)
770+
cxns = {x[0]: x[2]
771+
for x in workflow.graph.get_edge_data(
772+
pnode, node)['connections'].connections}
773+
data = [pnode, node, cxns]
774+
if pred is None:
775+
pred = [data]
776+
else:
777+
pred.append(data)
778+
return pred
779+
780+
# Note: we are going to use the final BIOMs to figure out which
781+
# processing is missing from the back/end to the front, as this
782+
# will prevent generating unnecessary steps (AKA already provided
783+
# by another command), like "Split Library of Demuxed",
784+
# when "Split per Sample" is alrady generated
785+
#
786+
# The steps to generate the default workflow are as follow:
787+
# 1. retrieve all valid merging schemes from valid jobs in the
788+
# current preparation
789+
# 2. retrive all the valid workflows for the preparation data type and
790+
# find the final BIOM missing from the valid available merging
791+
# schemes
792+
# 3. loop over the missing merging schemes and create the commands
793+
# missing to get to those processed samples and add them to a new
794+
# workflow
795+
796+
# 1.
797+
prep_jobs = [j for c in self.artifact.descendants.nodes()
798+
for j in c.jobs(show_hidden=True)
799+
if j.command.software.type == 'artifact transformation']
800+
merging_schemes = {
801+
qdb.archive.Archive.get_merging_scheme_from_job(j): {
802+
x: y.id for x, y in j.outputs.items()}
803+
for j in prep_jobs if j.status == 'success' and not j.hidden}
804+
805+
# 2.
806+
pt_dt = self.data_type()
807+
workflows = [wk for wk in qdb.software.DefaultWorkflow.iter()
808+
if pt_dt in wk.data_type]
809+
if not workflows:
810+
# raises option a.
811+
raise ValueError(f'This preparation data type: "{pt_dt}" does not '
812+
'have valid workflows')
813+
missing_artifacts = dict()
814+
for wk in workflows:
815+
missing_artifacts[wk] = dict()
816+
for node, degree in wk.graph.out_degree():
817+
if degree != 0:
818+
continue
819+
mscheme = _get_node_info(wk, node)
820+
if mscheme not in merging_schemes:
821+
missing_artifacts[wk][mscheme] = node
822+
if not missing_artifacts[wk]:
823+
del missing_artifacts[wk]
824+
if not missing_artifacts:
825+
# raises option b.
826+
raise ValueError('This preparation is complete')
827+
828+
# 3.
829+
workflow = None
830+
for wk, wk_data in missing_artifacts.items():
831+
previous_jobs = dict()
832+
for ma, node in wk_data.items():
833+
predecessors = _get_predecessors(wk, node)
834+
predecessors.reverse()
835+
cmds_to_create = []
836+
init_artifacts = None
837+
for i, (pnode, cnode, cxns) in enumerate(predecessors):
838+
cdp = cnode.default_parameter
839+
cdp_cmd = cdp.command
840+
params = cdp.values.copy()
841+
842+
icxns = {y: x for x, y in cxns.items()}
843+
reqp = {x: icxns[y[1][0]]
844+
for x, y in cdp_cmd.required_parameters.items()}
845+
cmds_to_create.append([cdp_cmd, params, reqp])
846+
847+
info = _get_node_info(wk, pnode)
848+
if info in merging_schemes:
849+
if set(merging_schemes[info]) >= set(cxns):
850+
init_artifacts = merging_schemes[info]
851+
break
852+
if init_artifacts is None:
853+
pdp = pnode.default_parameter
854+
pdp_cmd = pdp.command
855+
params = pdp.values.copy()
856+
reqp = {x: y[1][0]
857+
for x, y in pdp_cmd.required_parameters.items()}
858+
cmds_to_create.append([pdp_cmd, params, reqp])
859+
860+
init_artifacts = {
861+
self.artifact.artifact_type: self.artifact.id}
862+
863+
cmds_to_create.reverse()
864+
current_job = None
865+
for i, (cmd, params, rp) in enumerate(cmds_to_create):
866+
previous_job = current_job
867+
if previous_job is None:
868+
req_params = dict()
869+
for iname, dname in rp.items():
870+
if dname not in init_artifacts:
871+
msg = (f'Missing Artifact type: "{dname}" in '
872+
'this preparation; are you missing a '
873+
'step to start?')
874+
# raises option c.
875+
raise ValueError(msg)
876+
req_params[iname] = init_artifacts[dname]
877+
else:
878+
req_params = dict()
879+
connections = dict()
880+
for iname, dname in rp.items():
881+
req_params[iname] = f'{previous_job.id}{dname}'
882+
connections[dname] = iname
883+
params.update(req_params)
884+
job_params = qdb.software.Parameters.load(
885+
cmd, values_dict=params)
886+
887+
if job_params in previous_jobs.values():
888+
for x, y in previous_jobs.items():
889+
if job_params == y:
890+
current_job = x
891+
continue
892+
893+
if workflow is None:
894+
PW = qdb.processing_job.ProcessingWorkflow
895+
workflow = PW.from_scratch(user, job_params)
896+
current_job = [j for j in workflow.graph.nodes()][0]
897+
else:
898+
if previous_job is None:
899+
current_job = workflow.add(
900+
job_params, req_params=req_params)
901+
else:
902+
current_job = workflow.add(
903+
job_params, req_params=req_params,
904+
connections={previous_job: connections})
905+
906+
previous_jobs[current_job] = job_params
907+
908+
return workflow

qiita_db/metadata_template/test/test_prep_template.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1340,11 +1340,36 @@ def test_artifact_setter_error(self):
13401340

13411341
def test_artifact_setter(self):
13421342
pt = qdb.metadata_template.prep_template.PrepTemplate.create(
1343-
self.metadata, self.test_study, self.data_type_id)
1343+
self.metadata, self.test_study, '16S')
13441344
self.assertEqual(pt.artifact, None)
13451345
artifact = qdb.artifact.Artifact.create(
13461346
self.filepaths, "FASTQ", prep_template=pt)
13471347
self.assertEqual(pt.artifact, artifact)
1348+
1349+
# here we can test that we can properly create a workflow
1350+
wk = pt.add_default_workflow(qdb.user.User('[email protected]'))
1351+
self.assertEqual(len(wk.graph.nodes), 2)
1352+
self.assertEqual(len(wk.graph.edges), 1)
1353+
self.assertEqual(
1354+
[x.command.name for x in wk.graph.nodes],
1355+
['Split libraries FASTQ', 'Pick closed-reference OTUs'])
1356+
1357+
# now let's try to generate again and it should fail cause the jobs
1358+
# are alrady created
1359+
with self.assertRaisesRegex(ValueError, "Cannot create job because "
1360+
"the parameters are the same as jobs"):
1361+
pt.add_default_workflow(qdb.user.User('[email protected]'))
1362+
1363+
# now let's test that an error is raised when there is no valid initial
1364+
# input data; this moves the data type from FASTQ to taxa_summary
1365+
qdb.sql_connection.perform_as_transaction(
1366+
'UPDATE qiita.artifact SET artifact_type_id = 10 WHERE '
1367+
f'artifact_id = {pt.artifact.id}')
1368+
with self.assertRaisesRegex(ValueError, 'Missing Artifact type: '
1369+
'"FASTQ" in this preparation; are you '
1370+
'missing a step to start?'):
1371+
pt.add_default_workflow(qdb.user.User('[email protected]'))
1372+
13481373
# cleaning
13491374
qdb.artifact.Artifact.delete(artifact.id)
13501375
qdb.metadata_template.prep_template.PrepTemplate.delete(pt.id)

qiita_pet/handlers/api_proxy/studies.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ def study_prep_get_req(study_id, user_id):
236236
info['start_artifact'] = None
237237
info['start_artifact_id'] = None
238238
info['youngest_artifact'] = None
239+
info['num_artifact_children'] = 0
240+
info['youngest_artifact_name'] = None
241+
info['youngest_artifact_type'] = None
239242
info['ebi_experiment'] = 0
240243

241244
dtype_infos.append(info)

qiita_pet/handlers/api_proxy/tests/test_studies.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ def test_study_prep_get_req_failed_EBI(self):
238238
'start_artifact_id': None,
239239
'creation_timestamp': pt.creation_timestamp,
240240
'modification_timestamp': pt.modification_timestamp,
241+
'num_artifact_children': 0,
242+
'youngest_artifact_name': None,
243+
'youngest_artifact_type': None,
241244
'total_samples': 3}]
242245

243246
exp = {
@@ -577,6 +580,9 @@ def test_study_prep_get_req(self):
577580
'start_artifact_id': None,
578581
'start_artifact': None,
579582
'youngest_artifact': None,
583+
'num_artifact_children': 0,
584+
'youngest_artifact_name': None,
585+
'youngest_artifact_type': None,
580586
'ebi_experiment': 0}]
581587
exp = {'status': 'success',
582588
'message': '',

qiita_pet/handlers/study_handlers/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
DataTypesMenuAJAX, StudyFilesAJAX, StudyGetTags, StudyTags,
1717
Study)
1818
from .prep_template import (
19-
PrepTemplateAJAX, PrepFilesHandler,
19+
PrepTemplateAJAX, PrepFilesHandler, AddDefaultWorkflowHandler,
2020
NewPrepTemplateAjax, PrepTemplateSummaryAJAX)
2121
from .processing import (ListCommandsHandler, ListOptionsHandler,
2222
WorkflowHandler, WorkflowRunHandler, JobAJAX)
@@ -31,7 +31,7 @@
3131
'VAMPSHandler', 'ListStudiesAJAX', 'ArtifactGraphAJAX',
3232
'ArtifactAdminAJAX', 'StudyIndexHandler', 'StudyBaseInfoAJAX',
3333
'SampleTemplateHandler', 'SampleTemplateOverviewHandler',
34-
'SampleTemplateColumnsHandler',
34+
'SampleTemplateColumnsHandler', 'AddDefaultWorkflowHandler',
3535
'PrepTemplateAJAX', 'NewArtifactHandler', 'PrepFilesHandler',
3636
'ListCommandsHandler', 'ListOptionsHandler', 'SampleAJAX',
3737
'StudyDeleteAjax', 'NewPrepTemplateAjax',

qiita_pet/handlers/study_handlers/prep_template.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from qiita_pet.handlers.base_handlers import BaseHandler
1616
from qiita_db.util import (get_files_from_uploads_folders, get_mountpoint,
1717
supported_filepath_types)
18+
from qiita_db.metadata_template.prep_template import PrepTemplate
1819
from qiita_pet.handlers.api_proxy import (
1920
prep_template_ajax_get_req, new_prep_template_get_req,
2021
prep_template_summary_get_req)
@@ -32,6 +33,22 @@ def get(self):
3233
study_id=study_id)
3334

3435

36+
class AddDefaultWorkflowHandler(BaseHandler):
37+
@authenticated
38+
def post(self):
39+
prep_id = self.get_argument('prep_id')
40+
msg_error = None
41+
data = None
42+
try:
43+
workflow = PrepTemplate(prep_id).add_default_workflow(
44+
self.current_user)
45+
data = workflow.id
46+
except Exception as error:
47+
msg_error = str(error)
48+
49+
self.write({'data': data, 'msg_error': msg_error})
50+
51+
3552
class PrepTemplateSummaryAJAX(BaseHandler):
3653
@authenticated
3754
def get(self):

qiita_pet/static/js/networkVue.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Vue.component('processing-graph', {
110110
'</div>' +
111111
'</div>' +
112112
'</div>',
113-
props: ['portal', 'graph-endpoint', 'jobs-endpoint', 'no-init-jobs-callback', 'is-analysis-pipeline'],
113+
props: ['portal', 'graph-endpoint', 'jobs-endpoint', 'no-init-jobs-callback', 'is-analysis-pipeline', 'element-id'],
114114
methods: {
115115
/**
116116
*
@@ -997,6 +997,11 @@ Vue.component('processing-graph', {
997997
$("#processing-network-instructions-div").show();
998998
$("#show-hide-network-btn").show();
999999
$("#processing-job-div").hide();
1000+
if (vm.workflowId === null && vm.isAnalysisPipeline === false) {
1001+
$("#add-default-workflow").show();
1002+
} else {
1003+
$("#add-default-workflow").hide();
1004+
}
10001005
}
10011006
})
10021007
.fail(function(object, status, error_msg) {
@@ -1138,6 +1143,10 @@ Vue.component('processing-graph', {
11381143
'<tr>' +
11391144
'<td><small>Job status (circles):</small></td>' +
11401145
'<td>' + circle_statuses.join('') + '</td>' +
1146+
'<td rowspan="2" width="20px">&nbsp;</td>' +
1147+
'<td rowspan="2">' +
1148+
'<a class="btn btn-success form-control" id="add-default-workflow"><span class="glyphicon glyphicon-flash"></span> Add Default Workflow</a>' +
1149+
'</td>' +
11411150
'</tr>' +
11421151
'<tr>' +
11431152
'<td><small>Artifact status (triangles):</small>' +
@@ -1146,6 +1155,20 @@ Vue.component('processing-graph', {
11461155
'</table>';
11471156
$('#circle-explanation').html(full_text);
11481157

1158+
$('#add-default-workflow').on('click', function () {
1159+
$('#add-default-workflow').attr('disabled', true);
1160+
document.getElementById('add-default-workflow').innerHTML = 'Submitting!';
1161+
$.post(vm.portal + '/study/process/workflow/default/', {prep_id: vm.elementId}, function(data) {
1162+
if (data['msg_error'] !== null){
1163+
$('#add-default-workflow').attr('disabled', false);
1164+
bootstrapAlert('Error generating workflow: ' + data['msg_error'].replace("\n", "<br/>"));
1165+
} else {
1166+
vm.updateGraph();
1167+
}
1168+
});
1169+
document.getElementById('add-default-workflow').innerHTML = ' Add Default Workflow';
1170+
});
1171+
11491172
// This call to udpate graph will take care of updating the jobs
11501173
// if the graph is not available
11511174
vm.updateGraph();

qiita_pet/templates/analysis_description.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ <h2>
9191
<hr/>
9292
</div>
9393
<div id='analysis-graph-vue' style="margin-left: 15px">
94-
<processing-graph v-bind:is-analysis-pipeline='true' ref="procGraph" portal="{% raw qiita_config.portal_dir %}" graph-endpoint="/analysis/description/{{analysis_id}}/graph/" jobs-endpoint="/analysis/description/{{analysis_id}}/jobs/"></processing-graph>
94+
<processing-graph v-bind:is-analysis-pipeline='true' ref="procGraph" portal="{% raw qiita_config.portal_dir %}" graph-endpoint="/analysis/description/{{analysis_id}}/graph/" jobs-endpoint="/analysis/description/{{analysis_id}}/jobs/" element-id="{{analysis_id}}"></processing-graph>
9595
</div>
9696
<div class="row" id='processing-content-div'></div>
9797

qiita_pet/templates/study_ajax/data_type_menu.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ <h4 class="panel-title">
6262
<span id="prep-header-{{prep['id']}}">
6363
{{prep['name']}} - ID: {{prep['id']}} - {{prep['status']}}
6464
</span><br/>
65-
Raw files {% if prep['start_artifact'] == 'FASTQ' %}<i class="fa fa-check green"></i>{% else %}<i class="fa fa-times red"></i>{% end %}, processed {% if prep['num_artifact_children'] > 0 %}<i class="fa fa-check green"></i> {% if prep['num_artifact_children'] > 1 %} <i class="fa fa-check green"></i>{% end%}{% else %}<i class="fa fa-times red"></i>{% end %}, BIOM {% if prep['youngest_artifact_type'] == 'BIOM' %}<i class="fa fa-check green"></i>{% else %}<i class="fa fa-times red"></i>{% end %}
65+
Raw files {% if prep['start_artifact'] in ('per_sample_FASTQ', 'FASTA', 'FASTQ') %}<i class="fa fa-check green"></i>{% else %}<i class="fa fa-times red"></i>{% end %}, processed {% if prep['num_artifact_children'] > 0 %}<i class="fa fa-check green"></i> {% if prep['num_artifact_children'] > 1 %} <i class="fa fa-check green"></i>{% end%}{% else %}<i class="fa fa-times red"></i>{% end %}, BIOM {% if prep['youngest_artifact_type'] == 'BIOM' %}<i class="fa fa-check green"></i>{% else %}<i class="fa fa-times red"></i>{% end %}
6666
<br />
6767
Created: {{prep['creation_timestamp'].strftime('%B %-d, %Y')}}, last updated: {{prep['modification_timestamp'].strftime('%B %-d, %Y')}}
6868
</a>

qiita_pet/templates/study_ajax/prep_summary.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ <h5>
557557
</div>
558558
</div>
559559
<div id="processing-graph-vue" class="tab-pane fade in active">
560-
<processing-graph ref="procGraph" v-bind:is-analysis-pipeline='false' v-bind:no-init-jobs-callback="load_new_artifact" portal="{% raw qiita_config.portal_dir %}" graph-endpoint="/prep_template/{{prep_id}}/graph/" jobs-endpoint="/prep_template/{{prep_id}}/jobs/"></processing-graph>
560+
<processing-graph ref="procGraph" v-bind:is-analysis-pipeline='false' v-bind:no-init-jobs-callback="load_new_artifact" portal="{% raw qiita_config.portal_dir %}" graph-endpoint="/prep_template/{{prep_id}}/graph/" jobs-endpoint="/prep_template/{{prep_id}}/jobs/" element-id="{{prep_id}}"></processing-graph>
561561
</div>
562562
</div>
563563
</div>

0 commit comments

Comments
 (0)