Skip to content

Commit

Permalink
[minor] Replace while with a macro pattern (#364)
Browse files Browse the repository at this point in the history
* Add a new standard node for appending to a list

* Demonstrate and test while loops as a macro

This is pickleable and clear to write and read. I don't have a convenience wrapper yet to get rid of the "universal" stuff though.

* Remove the while-loop metanode

It was janky, the UI was not even particularly nice, and it wouldn't pickle. Just axe it.

* 🐛 include the new standard node in the exported `nodes` list

* Improve test comments

* Update deepdive to use the new while example

Instead of the old while metanode

* Remove unused import
  • Loading branch information
liamhuber committed Jun 12, 2024
1 parent 612b4ee commit 7f48546
Show file tree
Hide file tree
Showing 7 changed files with 1,012 additions and 673 deletions.
1,304 changes: 895 additions & 409 deletions notebooks/deepdive.ipynb

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion pyiron_workflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,3 @@
inputs_to_list,
list_to_outputs,
)
from pyiron_workflow.nodes.while_loop import while_loop
2 changes: 0 additions & 2 deletions pyiron_workflow/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,11 @@ def Workflow(self):
@lru_cache(maxsize=1)
def meta(self):
from pyiron_workflow.nodes.transform import inputs_to_list, list_to_outputs
from pyiron_workflow.nodes.while_loop import while_loop

return DotDict(
{
inputs_to_list.__name__: inputs_to_list,
list_to_outputs.__name__: list_to_outputs,
while_loop.__name__: while_loop,
}
)

Expand Down
38 changes: 38 additions & 0 deletions pyiron_workflow/nodes/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,43 @@ def process_run_result(self, function_output):
self.signals.output.false()


@as_function_node("list")
def AppendToList(existing: list | None = None, new_element=NOT_DATA):
"""
Append a new element to a list.
Args:
existing (list | None): The list to append to. Defaults to None, which creates
an empty list.
new_element: The new element to append. This is mandatory input as the default
value of `NOT_DATA` will cause a readiness error.
Returns:
list: The input list with the new element appended
Examples:
This node is particularly useful for acyclic flows, where you want to bend the
suggestions of idempotency by looping the node's output back on itself:
>>> from pyiron_workflow import standard_nodes as std
>>>
>>> n = std.AppendToList()
>>> n.run(new_element="foo")
['foo']
>>> n.run(existing=n, new_element="bar")
['foo', 'bar']
>>> n.run(existing=n, new_element="baz")
['foo', 'bar', 'baz']
"""
existing = [] if existing is None else existing
if new_element is not NOT_DATA:
existing.append(new_element)
return existing


@as_function_node("random")
def RandomFloat():
"""
Expand Down Expand Up @@ -652,6 +689,7 @@ def Round(obj):
Absolute,
Add,
And,
AppendToList,
Bool,
Bytes,
Contains,
Expand Down
198 changes: 0 additions & 198 deletions pyiron_workflow/nodes/while_loop.py

This file was deleted.

79 changes: 79 additions & 0 deletions tests/integration/test_while.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import pickle
import unittest

from pyiron_workflow import as_macro_node, standard_nodes as std


@as_macro_node("greater")
def AddWhileLessThan(self, a, b, cap):
"""
Add :param:`b` to :param:`a` while the sum is less than or equal to :param:`cap`.
A simple but complete demonstrator for how to construct cyclic flows, including
logging key outputs during the loop.
"""
# Bespoke logic
self.body = std.Add(obj=a, other=b)
self.body.inputs.obj = self.body.outputs.add # Higher priority connection
# The output is NOT_DATA on the first pass and `a` gets used,
# But after that the node will find and use its own output
self.condition = std.LessThan(self.body, cap)

# Universal logic
self.switch = std.If()
self.switch.inputs.condition = self.condition

self.starting_nodes = [self.body]
self.body >> self.condition >> self.switch
self.switch.signals.output.true >> self.body

# Bespoke logging
self.history = std.AppendToList()
self.history.inputs.existing = self.history
self.history.inputs.new_element = self.body
self.body >> self.history

# Returns are pretty universal for single-value body nodes,
# assuming a log of the history is not desired as output,
# but in general return values are also bespoke
return self.body


class TestWhileLoop(unittest.TestCase):
def test_while_loop(self):
a, b, cap = 0, 2, 5
n = AddWhileLessThan(a, b, cap, run_after_init=True)
self.assertGreaterEqual(
6,
n.outputs.greater.value,
msg="Verify output"
)
self.assertListEqual(
[2, 4, 6],
n.history.outputs.list.value,
msg="Verify loop history logging"
)
self.assertListEqual(
[
'body',
'history',
'condition',
'switch',
'body',
'history',
'condition',
'switch',
'body',
'history',
'condition',
'switch'
],
n.provenance_by_execution,
msg="Verify execution order -- the same nodes get run repeatedly in acyclic"
)
reloaded = pickle.loads(pickle.dumps(n))
self.assertListEqual(
reloaded.history.outputs.list.value,
n.history.outputs.list.value,
msg="Should be able to save and re-load cyclic graphs just like usual"
)

0 comments on commit 7f48546

Please sign in to comment.