Skip to content

Commit a2e7f5e

Browse files
authored
Support arbitrary nested dynamic socket (#712)
* allow arbitrary nested dynamic socket * Bump node-graph to 0.4.2 * Bump aiida-pythonjob to 0.4.6 * Update docs
1 parent f75656f commit a2e7f5e

12 files changed

Lines changed: 38 additions & 85 deletions

File tree

docs/gallery/howto/autogen/annotate_inputs_outputs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ def SquareNumbersGenerator(n: int):
171171
# The graph shows that the ``generate_square_numbers`` task has multiple output nodes, one for each entry in the dynamically generated dictionary.
172172
# The ``dynamic(typing.Any)`` specification instructs the workgraph to treat each value in the returned dictionary as a separate output node of any type.
173173
#
174+
# .. note::
175+
#
176+
# If no item type is specified (i.e., just ``dynamic()``), the namespace becomes fully dynamic,
177+
# enabling arbitrary nested structures (e.g., dictionaries). Each value will then be recursively treated as a leaf node.
178+
#
174179
# Nested namespaces
175180
# ~~~~~~~~~~~~~~~~~
176181
#

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ classifiers = [
2020
dependencies = [
2121
"numpy",
2222
"scipy",
23-
"node-graph==0.3.16",
23+
"node-graph~=0.4.2",
2424
"node-graph-widget>=0.0.5",
2525
"aiida-core~=2.7.1",
2626
"cloudpickle",
2727
"aiida-shell~=0.8",
28-
"aiida-pythonjob==0.4.5",
28+
"aiida-pythonjob~=0.4.6",
2929
"jsonschema"
3030
]
3131
description = "Design flexible node-based workflow for AiiDA calculation."

src/aiida_workgraph/config.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,6 @@
1515
WorkChain: 'WORKCHAIN',
1616
}
1717

18-
builtin_inputs = {
19-
'_wait': {
20-
'link_limit': 1e6,
21-
'metadata': {'arg_type': 'none', 'builtin_socket': True},
22-
}
23-
}
24-
builtin_outputs = {
25-
'_wait': {'metadata': {'arg_type': 'none', 'builtin_socket': True}},
26-
'_outputs': {'metadata': {'arg_type': 'none', 'builtin_socket': True}},
27-
}
28-
2918

3019
def load_config() -> dict:
3120
"""Load the configuration from the config file."""

src/aiida_workgraph/engine/task_state.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ def update_normal_task_state(self, name, results, success=True):
108108
"""Set the results of a normal task.
109109
A normal task is created by decorating a function with @task().
110110
"""
111-
from aiida_workgraph.config import builtin_outputs
112111

113112
if success:
114113
task = self.process.wg.tasks[name]
@@ -117,7 +116,9 @@ def update_normal_task_state(self, name, results, success=True):
117116
if len(task.outputs) - 2 != len(results):
118117
self.on_task_failed(name)
119118
return self.process.exit_codes.OUTPUS_NOT_MATCH_RESULTS
120-
output_names = [output._name for output in task.outputs if output._name not in builtin_outputs]
119+
output_names = [
120+
output._name for output in task.outputs if output._metadata.extra.get('builtin_socket') is not True
121+
]
121122
for i, output_name in enumerate(output_names):
122123
self.ctx._task_results[name][output_name] = results[i]
123124
elif isinstance(results, dict):

src/aiida_workgraph/socket_spec.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from dataclasses import replace
33
from typing import Any, Tuple, Dict
44
from node_graph.socket_spec import (
5-
SocketSpecMeta,
5+
SocketMeta,
66
SocketSpecSelect,
77
SocketSpec,
88
SocketSpecAPI as _SocketSpecAPI,
@@ -70,7 +70,7 @@ def _from_port(cls, port: Port | PortNamespace, *, parent_required: bool, role:
7070
ns = SocketSpec(
7171
identifier=cls.NAMESPACE,
7272
fields=fields,
73-
meta=SocketSpecMeta(
73+
meta=SocketMeta(
7474
required=required_here,
7575
is_metadata=getattr(port, 'is_metadata', False),
7676
call_role=('kwargs' if role == 'input' else None),
@@ -81,8 +81,11 @@ def _from_port(cls, port: Port | PortNamespace, *, parent_required: bool, role:
8181
is_dyn = bool(getattr(port, 'dynamic', False))
8282
if is_dyn:
8383
valid_type = getattr(port, 'valid_type', None)
84-
item_ident = cls._identifier_from_valid_type(valid_type)
85-
ns = replace(ns, dynamic=True, item=SocketSpec(identifier=item_ident))
84+
if valid_type:
85+
item_ident = cls._identifier_from_valid_type(valid_type)
86+
ns = replace(ns, meta=replace(ns.meta, dynamic=True), item=SocketSpec(identifier=item_ident))
87+
else:
88+
ns = replace(ns, meta=replace(ns.meta, dynamic=True), item=None)
8689
return ns
8790

8891
# Leaf Port (InputPort/OutputPort)
@@ -91,7 +94,7 @@ def _from_port(cls, port: Port | PortNamespace, *, parent_required: bool, role:
9194
ident = cls._identifier_from_valid_type(valid_type)
9295
return SocketSpec(
9396
identifier=ident,
94-
meta=SocketSpecMeta(
97+
meta=SocketMeta(
9598
required=required_here,
9699
is_metadata=getattr(port, 'is_metadata', False),
97100
call_role=('kwargs' if role == 'input' else None),

src/aiida_workgraph/task.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,6 @@ def set_from_builder(self, builder: Any) -> None:
7474
data = get_dict_from_builder(builder)
7575
self.set_inputs(data)
7676

77-
def set_from_protocol(self, *args: Any, **kwargs: Any) -> None:
78-
"""Set the task inputs from protocol data."""
79-
80-
executor = self.get_executor().callable
81-
# check if the executor has the get_builder_from_protocol method
82-
if not hasattr(executor, 'get_builder_from_protocol'):
83-
raise AttributeError(f'Executor {executor.__name__} does not have the get_builder_from_protocol method.')
84-
builder = executor.get_builder_from_protocol(*args, **kwargs)
85-
self.set_from_builder(builder)
86-
8777
@classmethod
8878
def new(cls, identifier: Union[str, Callable], name: Optional[str] = None) -> 'Task':
8979
"""Create a task from a identifier."""
@@ -177,21 +167,6 @@ def to_widget_value(self):
177167
wgdata = workgraph_to_short_json(wgdata)
178168
return wgdata
179169

180-
def _repr_mimebundle_(self, *args: Any, **kwargs: Any) -> any:
181-
# if ipywdigets > 8.0.0, use _repr_mimebundle_ instead of _ipython_display_
182-
self.widget.value = self.to_widget_value()
183-
if hasattr(self.widget, '_repr_mimebundle_'):
184-
return self.widget._repr_mimebundle_(*args, **kwargs)
185-
else:
186-
return self.widget._ipython_display_(*args, **kwargs)
187-
188-
def to_html(self, output: str = None, show_socket_depth: Optional[int] = None, **kwargs):
189-
"""Write a standalone html file to visualize the task."""
190-
if show_socket_depth is None:
191-
show_socket_depth = self.show_socket_depth
192-
self.widget.value = self.to_widget_value()
193-
return self.widget.to_html(output=output, **kwargs)
194-
195170

196171
class TaskSet:
197172
"""

src/aiida_workgraph/tasks/builtins.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from node_graph import RuntimeExecutor
88
from aiida import orm
99
from node_graph.node_spec import NodeSpec
10-
from node_graph.socket_spec import SocketSpec, SocketSpecMeta
10+
from node_graph.socket_spec import SocketSpec, SocketMeta
1111
from typing import Annotated
1212
from aiida_workgraph.executors.builtins import update_ctx, get_context, select, return_input
1313
from node_graph.node import BuiltinPolicy
@@ -140,8 +140,8 @@ class MapItem(Task):
140140
node_type='Normal',
141141
catalog='Control',
142142
inputs=namespace(
143-
source=SocketSpec('workgraph.any', link_limit=100000, meta=SocketSpecMeta(required=False)),
144-
key=SocketSpec('workgraph.string', meta=SocketSpecMeta(required=False)),
143+
source=SocketSpec('workgraph.any', link_limit=100000, meta=SocketMeta(required=False)),
144+
key=SocketSpec('workgraph.string', meta=SocketMeta(required=False)),
145145
),
146146
outputs=namespace(key=str, value=any),
147147
base_class_path='aiida_workgraph.tasks.builtins.MapItem',
@@ -173,7 +173,7 @@ class SetContext(Task):
173173
node_type='Normal',
174174
catalog='Control',
175175
inputs=namespace(
176-
context=SocketSpec('workgraph.any', meta=SocketSpecMeta(required=False)),
176+
context=SocketSpec('workgraph.any', meta=SocketMeta(required=False)),
177177
key=any,
178178
value=any,
179179
),
@@ -189,7 +189,7 @@ class GetContext(Task):
189189
identifier='workgraph.get_context',
190190
node_type='Normal',
191191
catalog='Control',
192-
inputs=namespace(context=SocketSpec('workgraph.any', meta=SocketSpecMeta(required=False)), key=any),
192+
inputs=namespace(context=SocketSpec('workgraph.any', meta=SocketMeta(required=False)), key=any),
193193
outputs=namespace(result=any),
194194
executor=RuntimeExecutor.from_callable(get_context),
195195
base_class_path='aiida_workgraph.tasks.builtins.GetContext',

src/aiida_workgraph/tasks/pythonjob_tasks.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from aiida_pythonjob import pyfunction, PythonJob, PyFunction, MonitorPyFunction
99
from aiida_pythonjob.utils import serialize_ports
1010
from aiida_workgraph.task import Task
11-
from node_graph.socket_spec import SocketSpec, SocketSpecSelect, SocketSpecMeta
11+
from node_graph.socket_spec import SocketSpec, SocketSpecSelect, SocketMeta
1212
from node_graph.node_spec import NodeSpec
1313
from aiida_workgraph.socket_spec import namespace
1414
from .function_task import build_callable_nodespec
@@ -291,9 +291,9 @@ def build_pythonjob_nodespec(
291291

292292
# additions specific to PythonJob
293293
add_in = namespace(
294-
computer=Annotated[str, SocketSpecMeta(required=False)],
295-
command_info=Annotated[dict, SocketSpecMeta(required=False)],
296-
register_pickle_by_value=Annotated[bool, SocketSpecMeta(required=False)],
294+
computer=Annotated[str, SocketMeta(required=False)],
295+
command_info=Annotated[dict, SocketMeta(required=False)],
296+
register_pickle_by_value=Annotated[bool, SocketMeta(required=False)],
297297
)
298298

299299
return build_callable_nodespec(

src/aiida_workgraph/tasks/shelljob_task.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from aiida_shell.launch import prepare_shell_job_inputs
88
from node_graph.node_spec import NodeSpec
99
from node_graph.executor import RuntimeExecutor
10-
from node_graph.socket_spec import SocketSpec, merge_specs, SocketSpecMeta
10+
from node_graph.socket_spec import SocketSpec, merge_specs, SocketMeta
1111
from aiida_workgraph.socket_spec import from_aiida_process, namespace
1212
from aiida_workgraph.task import Task, TaskHandle
1313
from aiida import orm
@@ -127,7 +127,7 @@ def _build_shelljob_nodespec(
127127
in_spec = replace(in_spec, fields={**in_spec.fields, 'code': patched_code})
128128

129129
# Add additional inputs
130-
additions_in = namespace(command=Any, resolve_command=Annotated[bool, SocketSpecMeta(required=False)])
130+
additions_in = namespace(command=Any, resolve_command=Annotated[bool, SocketMeta(required=False)])
131131
in_spec = merge_specs(in_spec, additions_in)
132132

133133
# Ensure stdout/stderr outputs

src/aiida_workgraph/workgraph.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,6 @@ def update(self) -> None:
331331
def pk(self) -> Optional[int]:
332332
return self.process.pk if self.process else None
333333

334-
def update_ctx(self, value: Dict[str, Any]) -> None:
335-
self.ctx._set_socket_value(value, link_limit=100000)
336-
337334
@classmethod
338335
def from_dict(cls, wgdata: Dict[str, Any]) -> 'WorkGraph':
339336
if 'tasks' in wgdata:
@@ -603,19 +600,6 @@ def to_widget_value(self) -> Dict[str, Any]:
603600
wgdata = workgraph_to_short_json(wgdata)
604601
return wgdata
605602

606-
def _repr_mimebundle_(self, *args, **kwargs):
607-
# if ipywdigets > 8.0.0, use _repr_mimebundle_ instead of _ipython_display_
608-
self.widget.value = self.to_widget_value()
609-
if hasattr(self.widget, '_repr_mimebundle_'):
610-
return self.widget._repr_mimebundle_(*args, **kwargs)
611-
else:
612-
return self.widget._ipython_display_(*args, **kwargs)
613-
614-
def to_html(self, output: str = None, **kwargs):
615-
"""Write a standalone html file to visualize the workgraph."""
616-
self.widget.value = self.to_widget_value()
617-
return self.widget.to_html(output=output, **kwargs)
618-
619603
def generate_provenance_graph(self):
620604
"""Generate the provenance graph of the workgraph process."""
621605
from aiida_workgraph.utils import generate_provenance_graph

0 commit comments

Comments
 (0)