Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[minor] Replace while with a macro pattern #364

Merged
merged 7 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
)
Loading
Loading