Skip to content

Commit 1cd8fe5

Browse files
committed
Fix test and update doc
1 parent 96c2f1e commit 1cd8fe5

10 files changed

Lines changed: 155 additions & 102 deletions

File tree

docs/gallery/advanced/autogen/context_manager.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,9 @@ def generate_add_multiply_workgraph():
485485
# ``Map`` zone
486486
# ============
487487
# .. warning::
488-
# **This feature is experimental.** The API for ``Map`` zone is subject to change in future releases. We welcome your feedback on its functionality.
488+
# **This feature is experimental.** The API for ``Map`` zone is subject to change in future releases.
489+
# We welcome your feedback on its functionality.
490+
# **ctx** does not work inside a ``Map`` zone yet.
489491
#
490492
# The ``Map`` context manager works similarly to Python's built-in ``map`` function.
491493
# By accessing the ``item`` member of the ``Map`` context, we can pass each individual element (e.g. a dictionary entry) to tasks.
@@ -522,10 +524,13 @@ def get_value(data, key):
522524

523525
with WorkGraph('AddMap') as wg:
524526
with Map(data) as map_zone:
525-
wg.outputs.result = add(
526-
x=get_value(map_zone.item, 'x').result,
527-
y=get_value(map_zone.item, 'y').result,
527+
result = add(
528+
x=get_value(map_zone.item.value, 'x').result,
529+
y=get_value(map_zone.item.value, 'y').result,
528530
).result
531+
map_zone.gather({'result': result})
532+
wg.ctx.result = map_zone.outputs.result
533+
wg.outputs.result = wg.ctx.result
529534

530535
wg.run()
531536

@@ -561,17 +566,18 @@ def get_value(data, key):
561566

562567

563568
@task
564-
def aggregate_sum(data):
569+
def aggregate_sum(data: spec.dynamic(Any)) -> int:
565570
return sum(data.values())
566571

567572

568573
with WorkGraph('AddAggregate') as wg:
569574
with Map(data) as map_zone:
570575
added_numbers = add(
571-
x=get_value(map_zone.item, 'x').result,
572-
y=get_value(map_zone.item, 'y').result,
576+
x=get_value(map_zone.item.value, 'x').result,
577+
y=get_value(map_zone.item.value, 'y').result,
573578
).result
574-
wg.outputs.result = aggregate_sum(added_numbers).result
579+
map_zone.gather({'result': added_numbers})
580+
wg.outputs.result = aggregate_sum(map_zone.outputs.result).result
575581

576582
wg.run()
577583

@@ -598,6 +604,10 @@ def aggregate_sum(data):
598604
#
599605
# Let's run an add-multiply workflow with a hardcoded multiplication factor:
600606

607+
608+
# In order to restart a workflow, the tasks should be importable from a module (i.e. not defined on-the-fly).
609+
from aiida_workgraph.tasks.tests import add, multiply
610+
601611
with WorkGraph('AddMultiplyToBeContinued', inputs=spec.namespace(x=Any, y=Any)) as wg1:
602612
the_sum = add(
603613
x=wg1.inputs.x,

docs/gallery/advanced/autogen/node_graph_programming.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,9 @@ def generate_data(N) -> spec.namespace(result=spec.dynamic(Any)):
195195

196196

197197
@task
198-
def calc_sum(**kwargs):
198+
def calc_sum(data: spec.dynamic(Any)) -> int:
199199
"""Calculates the sum of all keyword arguments' values."""
200-
return sum(kwargs.values())
200+
return sum(data.values())
201201

202202

203203
# %%
@@ -215,11 +215,13 @@ def calc_sum(**kwargs):
215215
map_task = wg.add_task('workgraph.map_zone', source=data_task.outputs.result)
216216

217217
# Inside the Map Zone, add 1 to each item
218-
add_task_in_map = map_task.add_task(add, x=map_task.item, y=1)
218+
add_task_in_map = map_task.add_task(add, x=map_task.item.value, y=1)
219+
220+
map_task.gather({'result': add_task_in_map.outputs.result})
219221

220222
# After the Map Zone, sum all the results from the add_task_in_map
221223
# The 'kwargs' input allows collecting all dynamic outputs from the mapped tasks.
222-
sum_task = wg.add_task(calc_sum, kwargs=add_task_in_map.outputs.result)
224+
sum_task = wg.add_task(calc_sum, data=map_task.outputs.result)
223225

224226
# Set the final output of the workgraph
225227
wg.outputs.result = sum_task.outputs.result

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.12",
23+
"node-graph==0.3.13",
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.4",
28+
"aiida-pythonjob==0.4.5",
2929
"jsonschema"
3030
]
3131
description = "Design flexible node-based workflow for AiiDA calculation."

src/aiida_workgraph/engine/task_manager.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,9 @@ def execute_map_task(self, task, kwargs):
303303
map_info['children'] = list(new_tasks.keys())
304304
map_info['links'] = new_links
305305
self.state_manager.set_task_runtime_info(name, 'map_info', map_info)
306+
# gather task finishes immediately
307+
gather_task = task.gather_item_task
308+
self.state_manager.set_task_runtime_info(gather_task.name, 'state', 'FINISHED')
306309

307310
self.continue_workgraph()
308311

@@ -472,7 +475,8 @@ def generate_mapped_tasks(self, zone_task: Task, prefix: str) -> None:
472475

473476
def update_map_item_task_state(self, item_task, prefix, value: Any):
474477
new_name = f'{prefix}_{item_task.name}'
475-
self.ctx._task_results[new_name]['item'] = value
478+
self.ctx._task_results[new_name]['key'] = prefix
479+
self.ctx._task_results[new_name]['value'] = value
476480
self.state_manager.set_task_runtime_info(new_name, 'state', 'FINISHED')
477481

478482
def copy_task(self, name: str, prefix: str) -> 'Task':

src/aiida_workgraph/engine/task_state.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,10 @@ def update_parent_task_state(self, name: str) -> None:
249249
node_type = parent_task.node_type.upper()
250250
if node_type == 'WHILE':
251251
self.update_while_task_state(parent_task.name)
252-
elif node_type in ['IF', 'ZONE', 'MAP']:
252+
elif node_type in ['IF', 'ZONE']:
253253
self.update_zone_task_state(parent_task.name)
254+
elif node_type == 'MAP':
255+
self.update_map_task_state(parent_task.name)
254256

255257
# If the task is a mapped child, update its parent's "template" (the original map node)
256258
if self.process.wg.tasks[name].map_data:
@@ -283,24 +285,49 @@ def update_zone_task_state(self, name: str) -> None:
283285
self.process.report(f'Task: {name} finished.')
284286
self.update_parent_task_state(name)
285287

286-
def update_template_task_state(self, name: str) -> None:
287-
"""Update the template task state.
288+
def update_map_task_state(self, name: str) -> None:
289+
"""Update the map task state.
288290
1) check if all child tasks are finished.
289291
2) gather the results of all the mapped tasks.
290292
3) update the parent task state.
291293
"""
292294
finished, _ = self.are_childen_finished(name)
293295
if finished:
296+
map_zone = self.process.wg.tasks[name]
294297
# gather the results of all the mapped tasks
295-
results = {}
296-
for prefix, mapped_task in self.process.wg.tasks[name].mapped_tasks.items():
297-
for output in mapped_task.outputs:
298-
if output._name in self.ctx._task_results[mapped_task.name]:
299-
results.setdefault(output._name, {})
300-
results[output._name][prefix] = self.ctx._task_results[mapped_task.name][output._name]
301-
self.ctx._task_results[name] = results
298+
gather_task = map_zone.gather_item_task
299+
for input in gather_task.inputs:
300+
if input._name.startswith('_'):
301+
continue
302+
results = {}
303+
link = input._links[0]
304+
for prefix, mapped_task in self.process.wg.tasks[gather_task.name].mapped_tasks.items():
305+
results[prefix] = self.ctx._task_results[mapped_task.name][link.to_socket._name]
306+
self.ctx._task_results[name][link.to_socket._name] = results
302307
self.set_task_runtime_info(name, 'state', 'FINISHED')
308+
# self.update_meta_tasks(name)
309+
self.process.report(f'Task: {name} finished.')
303310
self.update_meta_tasks(name)
311+
self.update_parent_task_state(name)
312+
313+
def update_template_task_state(self, name: str) -> None:
314+
"""Update the template task state.
315+
1) check if all child tasks are finished.
316+
2) gather the results of all the mapped tasks.
317+
3) update the parent task state.
318+
"""
319+
finished, _ = self.are_childen_finished(name)
320+
if finished:
321+
# # gather the results of all the mapped tasks
322+
# results = {}
323+
# for prefix, mapped_task in self.process.wg.tasks[name].mapped_tasks.items():
324+
# for output in mapped_task.outputs:
325+
# if output._name in self.ctx._task_results[mapped_task.name]:
326+
# results.setdefault(output._name, {})
327+
# results[output._name][prefix] = self.ctx._task_results[mapped_task.name][output._name]
328+
# self.ctx._task_results[name] = results
329+
self.set_task_runtime_info(name, 'state', 'FINISHED')
330+
# self.update_meta_tasks(name)
304331
self.process.report(f'Task: {name} finished.')
305332
self.update_parent_task_state(name)
306333

src/aiida_workgraph/executors/builtins.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ def get_item(data: dict, key: str) -> Any:
2828
return data.get(key, None)
2929

3030

31-
def return_inputs(**inputs: dict) -> dict:
32-
"""Return the inputs"""
33-
inputs['items'] = inputs.pop('item', {})
34-
return inputs
31+
def return_input(**kwargs: Any) -> dict:
32+
"""Return the input"""
33+
return kwargs

src/aiida_workgraph/tasks/builtins.py

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import annotations
12
from typing import Any, Dict
23
from aiida_workgraph.task import ChildTaskSet, Task
34
from aiida_workgraph import task, namespace, meta
@@ -8,7 +9,8 @@
89
from node_graph.node_spec import NodeSpec
910
from node_graph.socket_spec import SocketSpec, SocketSpecMeta
1011
from typing import Annotated
11-
from aiida_workgraph.executors.builtins import get_item, update_ctx, get_context, select
12+
from aiida_workgraph.executors.builtins import update_ctx, get_context, select, return_input
13+
from node_graph.node import BuiltinPolicy
1214

1315

1416
class GraphLevelTask(_GraphIOSharedMixin, Task):
@@ -41,7 +43,7 @@ def __init__(self, *args, **kwargs):
4143
super().__init__(*args, **kwargs)
4244
self.children = ChildTaskSet(parent=self)
4345

44-
def add_task(self, *args, **kwargs):
46+
def add_task(self, *args, **kwargs) -> Task:
4547
"""Syntactic sugar to add a task to the zone."""
4648
task = self.graph.add_task(*args, **kwargs)
4749
self.children.add(task)
@@ -92,8 +94,9 @@ class Map(Zone):
9294
node_type='MAP',
9395
catalog='Control',
9496
inputs=namespace(
95-
source=Annotated[Any, SocketSpec('workgraph.any', link_limit=100000)],
97+
source=SocketSpec('workgraph.any', link_limit=100000),
9698
),
99+
outputs=namespace(),
97100
base_class_path='aiida_workgraph.tasks.builtins.Map',
98101
)
99102

@@ -109,14 +112,29 @@ def item(self):
109112
map_item_task = self.add_task('workgraph.map_item')
110113
return map_item_task.outputs
111114

112-
def gather(self, socket: BaseSocket) -> None:
113-
gather_item = self.graph.add_task('workgraph.gather_item')
114-
self.graph.add_link(socket, gather_item.inputs.value)
115-
return gather_item.outputs.values
115+
@property
116+
def gather_item_task(self) -> Task | None:
117+
for child in self.children:
118+
if child.identifier == 'workgraph.gather_item':
119+
return child
120+
gather_item = self.add_task('workgraph.gather_item')
121+
return gather_item
122+
123+
def gather(self, sockets: Dict[str, BaseSocket]) -> None:
124+
gather_item = self.gather_item_task
125+
for name in sockets:
126+
gather_item.add_input_spec('workgraph.any', name=name)
127+
self.add_output_spec('workgraph.any', name=name)
128+
gather_item.set_inputs(sockets)
129+
return gather_item.outputs
130+
116131

117132
class MapItem(Task):
118133
"""MapItem"""
119134

135+
# turn off framework builtins for these graph-level nodes
136+
_BUILTINS_POLICY = BuiltinPolicy(input_wait=False, output_wait=False, default_output=False)
137+
120138
_default_spec = NodeSpec(
121139
identifier='workgraph.map_item',
122140
node_type='Normal',
@@ -125,32 +143,26 @@ class MapItem(Task):
125143
source=SocketSpec('workgraph.any', link_limit=100000, meta=SocketSpecMeta(required=False)),
126144
key=SocketSpec('workgraph.string', meta=SocketSpecMeta(required=False)),
127145
),
128-
outputs=namespace(item=SocketSpec('workgraph.any')),
129-
executor=RuntimeExecutor.from_callable(get_item),
146+
outputs=namespace(key=str, value=any),
130147
base_class_path='aiida_workgraph.tasks.builtins.MapItem',
131148
)
132149

133150

134151
class GatherItem(Task):
135152
"""GatherItem"""
136153

137-
identifier = 'workgraph.gather_item'
138-
name = 'GatherItem'
139-
node_type = 'Normal'
140-
catalog = 'Control'
141-
142-
def create_sockets(self) -> None:
143-
self.inputs._clear()
144-
self.outputs._clear()
154+
# turn off framework builtins for these graph-level nodes
155+
_BUILTINS_POLICY = BuiltinPolicy(input_wait=True, output_wait=False, default_output=False)
145156

146-
self.add_input('workgraph.any', 'value')
147-
self.add_output('workgraph.namespace', 'values')
148-
self.add_output('workgraph.any', '_wait')
149-
150-
def get_executor(self):
151-
from aiida_workgraph.executors.builtins import return_inputs
152-
153-
return RuntimeExecutor.from_callable(return_inputs)
157+
_default_spec = NodeSpec(
158+
identifier='workgraph.gather_item',
159+
node_type='Normal',
160+
catalog='Control',
161+
inputs=namespace(),
162+
outputs=namespace(),
163+
executor=RuntimeExecutor.from_callable(return_input),
164+
base_class_path='aiida_workgraph.tasks.builtins.GatherItem',
165+
)
154166

155167

156168
class SetContext(Task):

src/aiida_workgraph/tasks/tests.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from aiida_workgraph import task
2+
3+
4+
@task
5+
def add(x, y):
6+
"""Add two numbers."""
7+
return x + y
8+
9+
10+
@task
11+
def multiply(x, y):
12+
"""Multiply two numbers."""
13+
return x * y

0 commit comments

Comments
 (0)