def test_get_node_info_multiple_call_links(self): """Test the `get_node_info` utility. Regression test for #2868: Verify that all `CALL` links are included in the formatted string even if link labels are identical. """ from aiida.cmdline.utils.common import get_node_info workflow = orm.WorkflowNode().store() node_one = orm.CalculationNode() node_two = orm.CalculationNode() node_one.add_incoming(workflow, link_type=LinkType.CALL_CALC, link_label='CALL_IDENTICAL') node_two.add_incoming(workflow, link_type=LinkType.CALL_CALC, link_label='CALL_IDENTICAL') node_one.store() node_two.store() node_info = get_node_info(workflow) self.assertTrue('CALL_IDENTICAL' in node_info) self.assertTrue(str(node_one.pk) in node_info) self.assertTrue(str(node_two.pk) in node_info)
def test_double_return_links_for_workflows(self, temp_dir): """ This test checks that double return links to a node can be exported and imported without problems, """ work1 = orm.WorkflowNode() work2 = orm.WorkflowNode().store() data_in = orm.Int(1).store() data_out = orm.Int(2).store() work1.add_incoming(data_in, LinkType.INPUT_WORK, 'input_i1') work1.add_incoming(work2, LinkType.CALL_WORK, 'call') work1.store() data_out.add_incoming(work1, LinkType.RETURN, 'return1') data_out.add_incoming(work2, LinkType.RETURN, 'return2') links_count = 4 work1.seal() work2.seal() uuids_wanted = set(_.uuid for _ in (work1, data_out, data_in, work2)) links_wanted = get_all_node_links() export_file = os.path.join(temp_dir, 'export.tar.gz') export([data_out, work1, work2, data_in], outfile=export_file, silent=True) self.reset_database() import_data(export_file, silent=True) uuids_in_db = [ str(uuid) for [uuid] in orm.QueryBuilder().append( orm.Node, project='uuid').all() ] self.assertListEqual(sorted(uuids_wanted), sorted(uuids_in_db)) links_in_db = get_all_node_links() self.assertListEqual(sorted(links_wanted), sorted(links_in_db)) # Assert number of links, checking both RETURN links are included self.assertEqual(len(links_wanted), links_count) # Before export self.assertEqual(len(links_in_db), links_count) # After import
def test_previous_workchain(run_cli_command): """Test the ``options.PREVIOUS_WORKCHAIN`` option.""" node = orm.WorkflowNode().store() @click.command() @options.PREVIOUS_WORKCHAIN() def command(previous_workchain): assert previous_workchain.pk == node.pk run_cli_command(command, ['--previous-workchain', str(node.pk)])
def test_reference_workchain(run_cli_command): """Test the ``options.REFERENCE_WORKCHAIN`` option.""" node = orm.WorkflowNode().store() @click.command() @options.REFERENCE_WORKCHAIN() def command(reference_workchain): assert reference_workchain.pk == node.pk run_cli_command(command, ['--reference-workchain', str(node.pk)])
def test_exposed_outputs(self): """Test the ``Process.exposed_outputs`` method.""" from aiida.common import AttributeDict from aiida.common.links import LinkType from aiida.engine.utils import instantiate_process from aiida.manage.manager import get_manager runner = get_manager().get_runner() class ChildProcess(Process): """Dummy process with normal output and output namespace.""" _node_class = orm.WorkflowNode @classmethod def define(cls, spec): super(ChildProcess, cls).define(spec) spec.input('input', valid_type=orm.Int) spec.output('output', valid_type=orm.Int) spec.output('name.space', valid_type=orm.Int) class ParentProcess(Process): """Dummy process that exposes the outputs of ``ChildProcess``.""" _node_class = orm.WorkflowNode @classmethod def define(cls, spec): super(ParentProcess, cls).define(spec) spec.input('input', valid_type=orm.Int) spec.expose_outputs(ChildProcess) node_child = orm.WorkflowNode().store() node_output = orm.Int(1).store() node_output.add_incoming(node_child, link_label='output', link_type=LinkType.RETURN) node_name_space = orm.Int(1).store() node_name_space.add_incoming(node_child, link_label='name__space', link_type=LinkType.RETURN) process = instantiate_process(runner, ParentProcess, input=orm.Int(1)) exposed_outputs = process.exposed_outputs(node_child, ChildProcess) expected = AttributeDict({ 'name': { 'space': node_name_space, }, 'output': node_output, }) self.assertEqual(exposed_outputs, expected)
def test_cycle(self): """ Testing the case of a cycle (workflow node with a data node that is both an input and an output): - Update rules with no max iterations should not get stuck. - Replace rules should return alternating results. """ data_node = orm.Data().store() work_node = orm.WorkflowNode() work_node.add_incoming(data_node, link_type=LinkType.INPUT_WORK, link_label='input_link') work_node.store() data_node.add_incoming(work_node, link_type=LinkType.RETURN, link_label='return_link') basket = Basket(nodes=[data_node.id]) queryb = orm.QueryBuilder() queryb.append(orm.Node).append(orm.Node) uprule = UpdateRule(queryb, max_iterations=np.inf) obtained = uprule.run(basket.copy())['nodes'].keyset expected = set([data_node.id, work_node.id]) self.assertEqual(obtained, expected) rerule1 = ReplaceRule(queryb, max_iterations=1) result1 = rerule1.run(basket.copy())['nodes'].keyset self.assertEqual(result1, set([work_node.id])) rerule2 = ReplaceRule(queryb, max_iterations=2) result2 = rerule2.run(basket.copy())['nodes'].keyset self.assertEqual(result2, set([data_node.id])) rerule3 = ReplaceRule(queryb, max_iterations=3) result3 = rerule3.run(basket.copy())['nodes'].keyset self.assertEqual(result3, set([work_node.id])) rerule4 = ReplaceRule(queryb, max_iterations=4) result4 = rerule4.run(basket.copy())['nodes'].keyset self.assertEqual(result4, set([data_node.id]))
def _create_basic_graph(): """ Creates a basic graph which has one parent workflow (work_2) that calls a child workflow (work_1) which calls a calculation function (calc_0). There is one input (data_i) and one output (data_o). It has at least one link of each class: * CALL_WORK from work_2 to work_1. * CALL_CALC from work_1 to calc_0. * INPUT_CALC from data_i to calc_0 and CREATE from calc_0 to data_o. * INPUT_WORK from data_i to work_1 and RETURN from work_1 to data_o. * INPUT_WORK from data_i to work_2 and RETURN from work_2 to data_o. This graph looks like this:: input_work +--------+ return +-------------------> | work_2 | --------------------+ | +--------+ | | | | | | call_work | | | | | v | | input_work +--------+ return | | +----------------> | work_1 | -----------------+ | | | +--------+ | | | | | | | | | | call_calc | | | | | | | | | v v v +--------+ input_calc +--------+ create +--------+ | data_i | ------------> | calc_0 | ------------> | data_o | +--------+ +--------+ +--------+ """ data_i = orm.Data().store() work_2 = orm.WorkflowNode() work_2.add_incoming(data_i, link_type=LinkType.INPUT_WORK, link_label='inpwork2') work_2.store() work_1 = orm.WorkflowNode() work_1.add_incoming(data_i, link_type=LinkType.INPUT_WORK, link_label='inpwork1') work_1.add_incoming(work_2, link_type=LinkType.CALL_WORK, link_label='callwork') work_1.store() calc_0 = orm.CalculationNode() calc_0.add_incoming(data_i, link_type=LinkType.INPUT_CALC, link_label='inpcalc0') calc_0.add_incoming(work_1, link_type=LinkType.CALL_CALC, link_label='callcalc') calc_0.store() data_o = orm.Data() data_o.add_incoming(calc_0, link_type=LinkType.CREATE, link_label='create0') data_o.store() data_o.add_incoming(work_2, link_type=LinkType.RETURN, link_label='return2') data_o.add_incoming(work_1, link_type=LinkType.RETURN, link_label='return1') output_dict = { 'data_i': data_i, 'data_o': data_o, 'calc_0': calc_0, 'work_1': work_1, 'work_2': work_2, } return output_dict
def test_multiple_post_return_links(self, temp_dir): # pylint: disable=too-many-locals """Check extra RETURN links can be added to existing Nodes, when label is not unique""" data = orm.Int(1).store() calc = orm.CalculationNode().store() work = orm.WorkflowNode().store() link_label = 'output_data' data.add_incoming(calc, LinkType.CREATE, link_label) data.add_incoming(work, LinkType.RETURN, link_label) calc.seal() work.seal() data_uuid = data.uuid calc_uuid = calc.uuid work_uuid = work.uuid before_links = get_all_node_links() data_provenance = os.path.join(temp_dir, 'data.aiida') all_provenance = os.path.join(temp_dir, 'all.aiida') export([data], filename=data_provenance, return_backward=False) export([data], filename=all_provenance, return_backward=True) self.clean_db() # import data provenance import_data(data_provenance) no_of_work = orm.QueryBuilder().append(orm.WorkflowNode).count() self.assertEqual( no_of_work, 0, msg= f'{no_of_work} WorkflowNode(s) was/were found, however, none should be present' ) nodes = orm.QueryBuilder().append(orm.Node, project='uuid') self.assertEqual( nodes.count(), 2, msg= f'{no_of_work} Node(s) was/were found, however, exactly two should be present' ) for node in nodes.iterall(): self.assertIn(node[0], [data_uuid, calc_uuid]) links = get_all_node_links() self.assertEqual( len(links), 1, msg='Only a single Link (from Calc. to Data) is expected, ' 'instead {} were found (in, out, label, type): {}'.format( len(links), links)) for from_uuid, to_uuid, found_label, found_type in links: self.assertEqual(from_uuid, calc_uuid) self.assertEqual(to_uuid, data_uuid) self.assertEqual(found_label, link_label) self.assertEqual(found_type, LinkType.CREATE.value) # import data+logic provenance import_data(all_provenance) no_of_work = orm.QueryBuilder().append(orm.WorkflowNode).count() self.assertEqual( no_of_work, 1, msg= f'{no_of_work} WorkflowNode(s) was/were found, however, exactly one should be present' ) nodes = orm.QueryBuilder().append(orm.Node, project='uuid') self.assertEqual( nodes.count(), 3, msg= f'{no_of_work} Node(s) was/were found, however, exactly three should be present' ) for node in nodes.iterall(): self.assertIn(node[0], [data_uuid, calc_uuid, work_uuid]) links = get_all_node_links() self.assertEqual( len(links), 2, msg= f'Exactly two Links are expected, instead {len(links)} were found (in, out, label, type): {links}' ) self.assertListEqual(sorted(links), sorted(before_links))
def test_traversal_cycle(self): """ This will test that cycles don't go into infinite loops by testing a graph with two data nodes data_take and data_drop and a work_select that takes both as input but returns only data_take """ from aiida import orm data_take = orm.Data().store() data_drop = orm.Data().store() work_select = orm.WorkflowNode() work_select.add_incoming(data_take, link_type=LinkType.INPUT_WORK, link_label='input_take') work_select.add_incoming(data_drop, link_type=LinkType.INPUT_WORK, link_label='input_drop') work_select.store() data_take.add_incoming(work_select, link_type=LinkType.RETURN, link_label='return_link') data_take = data_take.pk data_drop = data_drop.pk work_select = work_select.pk every_node = [data_take, data_drop, work_select] for single_node in every_node: expected_nodes = set([single_node]) obtained_nodes = traverse_graph([single_node])['nodes'] self.assertEqual(obtained_nodes, expected_nodes) links_forward = [LinkType.INPUT_WORK, LinkType.RETURN] links_backward = [] # Forward: data_drop to (input) work_select to (return) data_take obtained_nodes = traverse_graph([data_drop], links_forward=links_forward, links_backward=links_backward)['nodes'] expected_nodes = set(every_node) self.assertEqual(obtained_nodes, expected_nodes) # Forward: data_take to (input) work_select (data_drop is not returned) obtained_nodes = traverse_graph([data_take], links_forward=links_forward, links_backward=links_backward)['nodes'] expected_nodes = set([work_select, data_take]) self.assertEqual(obtained_nodes, expected_nodes) # Forward: work_select to (return) data_take (data_drop is not returned) obtained_nodes = traverse_graph([work_select], links_forward=links_forward, links_backward=links_backward)['nodes'] self.assertEqual(obtained_nodes, expected_nodes) links_forward = [] links_backward = [LinkType.INPUT_WORK, LinkType.RETURN] # Backward: data_drop is not returned so it has no backward link expected_nodes = set([data_drop]) obtained_nodes = traverse_graph([data_drop], links_forward=links_forward, links_backward=links_backward)['nodes'] self.assertEqual(obtained_nodes, expected_nodes) # Backward: data_take to (return) work_select to (input) data_drop expected_nodes = set(every_node) obtained_nodes = traverse_graph([data_take], links_forward=links_forward, links_backward=links_backward)['nodes'] self.assertEqual(obtained_nodes, expected_nodes) # Backward: work_select to (input) data_take and data_drop obtained_nodes = traverse_graph([work_select], links_forward=links_forward, links_backward=links_backward)['nodes'] self.assertEqual(obtained_nodes, expected_nodes)
def create_minimal_graph(): """ Creates a minimal graph which has one parent workflow (W2) that calls a child workflow (W1) which calls a calculation function (C0). There is one input (DI) and one output (DO). It has at least one link of each class: * CALL_WORK from W2 to W1. * CALL_CALC from W1 to C0. * INPUT_CALC from DI to C0 and CREATE from C0 to DO. * INPUT_WORK from DI to W1 and RETURN from W1 to DO. * INPUT_WORK from DI to W2 and RETURN from W2 to DO. This graph looks like this:: input_work +----+ return +----------------> | W2 | ---------------+ | +----+ | | | | | | call_work | | | | | v | | input_work +----+ return | | +-------------> | W1 | ------------+ | | | +----+ | | | | | | | | | | call_calc | | | | | | | | | v v v +------+ input_calc +----+ create +------+ | DI | ----------> | C0 | --------> | DO | +------+ +----+ +------+ """ from aiida import orm data_i = orm.Data().store() data_o = orm.Data().store() calc_0 = orm.CalculationNode() work_1 = orm.WorkflowNode() work_2 = orm.WorkflowNode() calc_0.add_incoming(data_i, link_type=LinkType.INPUT_CALC, link_label='inpcalc') work_1.add_incoming(data_i, link_type=LinkType.INPUT_WORK, link_label='inpwork') work_2.add_incoming(data_i, link_type=LinkType.INPUT_WORK, link_label='inpwork') calc_0.add_incoming(work_1, link_type=LinkType.CALL_CALC, link_label='callcalc') work_1.add_incoming(work_2, link_type=LinkType.CALL_WORK, link_label='callwork') work_2.store() work_1.store() calc_0.store() data_o.add_incoming(calc_0, link_type=LinkType.CREATE, link_label='create0') data_o.add_incoming(work_1, link_type=LinkType.RETURN, link_label='return1') data_o.add_incoming(work_2, link_type=LinkType.RETURN, link_label='return2') output_dict = { 'data_i': data_i, 'data_o': data_o, 'calc_0': calc_0, 'work_1': work_1, 'work_2': work_2, } return output_dict