Commit c339afc1 authored by Wout De Nolf's avatar Wout De Nolf
Browse files

allow adding default link attributes to the input and output nodes of a graph

parent 4c96cf04
Pipeline #60896 passed with stages
in 38 seconds
......@@ -72,37 +72,31 @@ Graph attributes
* *output_nodes* (optional): nodes that are expected to be used as link sources when the graph
is used as a subgraph.
The *input_nodes* and *output_nodes* define nodes which refer to nodes from the node attributes. For example
.. code-block:: json
"graph": {
"input_nodes": [
{"id": "alias1", "node": "name1"},
{"id": "alias2", "node": "name2"},
The *input_nodes* and *output_nodes* have these attributes
* *id*: node identifier which will be used in links with a super graph
* *node*: node identifier which should be in the node attributes of this graphs
* *sub_node* (optional): in case *node* is a graph we need to specify the node *id* inside
that graph. The *sub_node* can be an *id* from the node attributes of the sub-graph or
from sub-graph attributes *input_nodes* or *output_nodes*.
* *link_attributes* (optional): default link attributes used in links with a super graph. The
link attributes specified in the super graph have priority over these defaults.
In case the referenced nodes are graphs, the node inside that graph needs to be references with the `"sub_node"` key.
For example
For example for a graph with nodes `"id1"` (normal node) and `"id2"` (graph with an input node `"alias3"`):
.. code-block:: json
"graph": {
"input_nodes": [
{"id": "alias1", "node": "name1", "sub_node": "name3"},
{"id": "alias2", "node": "name2", "sub_node": "name4"},
{"id": "alias1", "node": "id1"},
{"id": "alias2", "node": "id2", "sub_node": "alias3"},
Note that `"alias1"`, `"name1"`, `"name3"`, ... are all node id's. The `"sub_node"` *id* could be an *id* in the
node attributes of the sub-graph or it could be an *id* in the graph attributes *input_nodes* or *output_nodes*
of the sub-graph.
Note that `"alias1"`, `"alias2"`, `"alias3"`, `"id1"` and `"id2"` are all node id's. The `"alias3"`
is an input node id but normal node id's can also be used here.
Node attributes
import itertools
from typing import Tuple, Union, Any
from typing import Optional, Tuple, Union, Any
import networkx
from .utils import dict_merge
from .node import flatten_node_id
......@@ -40,30 +40,33 @@ def _get_subgraph(node_id: NodeIdType, subgraphs: dict):
def _resolve_node_alias(
node_id: NodeIdType, graph_attrs: dict, input_nodes: bool
) -> NodeIdType:
) -> Tuple[NodeIdType, dict]:
link_attrs = dict()
if input_nodes:
aliases = graph_attrs.get("input_nodes", None)
aliases = graph_attrs.get("output_nodes", None)
if not aliases:
return node_id
return node_id, link_attrs
alias_attrs = None
for alias_attrsi in aliases:
if alias_attrsi["id"] == node_id:
alias_attrs = alias_attrsi
if not alias_attrs:
return node_id
return node_id, link_attrs
sub_node = alias_attrs.get("sub_node", None)
if sub_node:
return alias_attrs["node"], sub_node
sub_node_id = alias_attrs["node"], sub_node
return alias_attrs["node"]
sub_node_id = alias_attrs["node"]
link_attrs = alias_attrs.get("link_attributes", link_attrs)
return sub_node_id, link_attrs
def _get_subnode_id(
node_id: NodeIdType, sub_graph_nodes: dict, subgraphs: dict, source: bool
) -> Tuple[NodeIdType, bool]:
) -> Tuple[NodeIdType, Optional[dict]]:
if source:
key = "sub_source"
......@@ -75,7 +78,7 @@ def _get_subnode_id(
raise ValueError(
f"'{node_id}' is not a graph so 'sub_source' should not be specified"
return node_id, False
return node_id, None
sub_node_id = sub_graph_nodes[key]
......@@ -83,11 +86,11 @@ def _get_subnode_id(
raise ValueError(
f"The '{key}' attribute to specify a node in subgraph '{node_id}' is missing"
) from None
sub_node_id = _resolve_node_alias(
sub_node_id, link_attrs = _resolve_node_alias(
sub_node_id, subgraph.graph.graph, input_nodes=not source
new_node_id = _append_subnode_id(node_id, sub_node_id)
return new_node_id, True
return new_node_id, link_attrs
def _get_subnode_info(
......@@ -95,12 +98,21 @@ def _get_subnode_info(
target_id: NodeIdType,
sub_graph_nodes: dict,
subgraphs: dict,
) -> Tuple[NodeIdType, NodeIdType, bool]:
sub_source, _ = _get_subnode_id(source_id, sub_graph_nodes, subgraphs, source=True)
sub_target, target_is_graph = _get_subnode_id(
) -> Tuple[NodeIdType, NodeIdType, dict, bool]:
sub_source, source_link_attrs = _get_subnode_id(
source_id, sub_graph_nodes, subgraphs, source=True
sub_target, target_link_attrs = _get_subnode_id(
target_id, sub_graph_nodes, subgraphs, source=False
return sub_source, sub_target, target_is_graph
if source_link_attrs:
link_attrs = source_link_attrs
link_attrs = dict()
if target_link_attrs:
target_is_graph = target_link_attrs is not None
return sub_source, sub_target, link_attrs, target_is_graph
def _replace_aliases(
......@@ -124,11 +136,15 @@ def _replace_aliases(
sub_node = alias_attrs.pop("sub_node", None)
if sub_node:
node_id = node_id, sub_node
link_attrs = None
if isinstance(node_id, tuple):
parent, child = node_id
node_id, _ = _get_subnode_id(
node_id, link_attrs = _get_subnode_id(
parent, {key: child}, subgraphs=subgraphs, source=source
if link_attrs:
link_attrs.update(alias_attrs.get("link_attributes", dict()))
alias_attrs["link_attributes"] = link_attrs
alias_attrs["node"] = node_id
......@@ -171,9 +187,11 @@ def extract_graph_nodes(graph: networkx.DiGraph, subgraphs) -> Tuple[list, dict]
if not sub_graph_nodes:
source, target, target_is_graph = _get_subnode_info(
source, target, default_link_attrs, target_is_graph = _get_subnode_info(
source_id, target_id, sub_graph_nodes, subgraphs
if default_link_attrs:
link_attrs = {**default_link_attrs, **link_attrs}
sub_target_attributes = sub_graph_nodes.get(
"sub_target_attributes", None
......@@ -8,7 +8,7 @@ def myfunc(name=None, value=0):
return value + 1
def test_sub_graph():
def test_sub_graph_execute():
subsubgraph = {
"graph": {"input_nodes": [{"id": "in", "node": "subsubnode1"}]},
"nodes": [
......@@ -63,3 +63,85 @@ def test_sub_graph():
("node2", ("subnode1", "subsubnode1")): {"return_value": 2},
assert_taskgraph_result(ewoksgraph, expected, tasks=tasks)
def test_sub_graph_link_attributes():
subsubgraph = {
"graph": {
"input_nodes": [
{"id": "in1", "node": "subsubnode1", "link_attributes": {1: 1}},
{"id": "in2", "node": "subsubnode1", "link_attributes": {2: 2}},
"output_nodes": [
{"id": "out1", "node": "subsubnode1", "link_attributes": {3: 3}},
{"id": "out2", "node": "subsubnode1", "link_attributes": {4: 4}},
"nodes": [
{"id": "subsubnode1", "task_type": "method", "task_identifier": "dummy"}
subgraph = {
"graph": {
"input_nodes": [
"id": "in1",
"node": "subnode1",
"sub_node": "in1",
"link_attributes": {5: 5},
"id": "in2",
"node": "subnode1",
"sub_node": "in2",
"output_nodes": [
"id": "out1",
"node": "subnode1",
"sub_node": "out1",
"link_attributes": {6: 6},
"id": "out2",
"node": "subnode1",
"sub_node": "out2",
"nodes": [
{"id": "subnode1", "task_type": "graph", "task_identifier": subsubgraph}
graph = {
"nodes": [
{"id": "node1", "task_type": "method", "task_identifier": "dummy"},
{"id": "node2", "task_type": "method", "task_identifier": "dummy"},
{"id": "node3", "task_type": "method", "task_identifier": "dummy"},
{"id": "node4", "task_type": "method", "task_identifier": "dummy"},
{"id": "graphnode", "task_type": "graph", "task_identifier": subgraph},
"links": [
{"source": "node1", "target": "graphnode", "sub_target": "in1"},
{"source": "node2", "target": "graphnode", "sub_target": "in2"},
{"source": "graphnode", "target": "node3", "sub_source": "out1"},
{"source": "graphnode", "target": "node4", "sub_source": "out2"},
ewoksgraph = load_graph(graph)
for link, attrs in ewoksgraph.graph.edges.items():
numbers = {i for i in attrs if isinstance(i, int)}
if "node1" in link:
assert numbers == {1, 5}
elif "node2" in link:
assert numbers == {2}
elif "node3" in link:
assert numbers == {3, 6}
elif "node4" in link:
assert numbers == {4}
assert False, "unexpected link"
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment