From 7f48546129e5b43589b7a2b22f80b26e5b6726c5 Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Wed, 12 Jun 2024 12:31:41 -0700 Subject: [PATCH] [minor] Replace while with a macro pattern (#364) * 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. * :bug: 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 --- notebooks/deepdive.ipynb | 1304 ++++++++++++++++++--------- pyiron_workflow/__init__.py | 1 - pyiron_workflow/create.py | 2 - pyiron_workflow/nodes/standard.py | 38 + pyiron_workflow/nodes/while_loop.py | 198 ---- tests/integration/test_while.py | 79 ++ tests/integration/test_workflow.py | 63 -- 7 files changed, 1012 insertions(+), 673 deletions(-) delete mode 100644 pyiron_workflow/nodes/while_loop.py create mode 100644 tests/integration/test_while.py diff --git a/notebooks/deepdive.ipynb b/notebooks/deepdive.ipynb index 0bb175fa..e075dde2 100644 --- a/notebooks/deepdive.ipynb +++ b/notebooks/deepdive.ipynb @@ -477,25 +477,25 @@ "[__main__.adder,\n", " pyiron_snippets.factory._FactoryMade,\n", " pyiron_workflow.nodes.function.Function,\n", - " pyiron_workflow.io_preview.StaticNode,\n", + " pyiron_workflow.nodes.static_io.StaticNode,\n", " pyiron_workflow.node.Node,\n", - " pyiron_workflow.has_to_dict.HasToDict,\n", - " pyiron_workflow.semantics.Semantic,\n", - " pyiron_workflow.run.Runnable,\n", - " pyiron_workflow.injection.HasIOWithInjection,\n", + " pyiron_workflow.mixin.has_to_dict.HasToDict,\n", + " pyiron_workflow.mixin.semantics.Semantic,\n", + " pyiron_workflow.mixin.run.Runnable,\n", + " pyiron_workflow.mixin.injection.HasIOWithInjection,\n", " pyiron_workflow.io.HasIO,\n", - " pyiron_workflow.has_interface_mixins.UsesState,\n", - " pyiron_workflow.single_output.ExploitsSingleOutput,\n", - " pyiron_workflow.working.HasWorkingDirectory,\n", - " pyiron_workflow.storage.HasH5ioStorage,\n", - " pyiron_workflow.storage.HasTinybaseStorage,\n", - " pyiron_workflow.storage.HasStorage,\n", - " pyiron_workflow.has_interface_mixins.HasLabel,\n", - " pyiron_workflow.has_interface_mixins.HasParent,\n", - " pyiron_workflow.has_interface_mixins.HasRun,\n", - " pyiron_workflow.has_interface_mixins.HasChannel,\n", - " pyiron_workflow.io_preview.ScrapesIO,\n", - " pyiron_workflow.io_preview.HasIOPreview,\n", + " pyiron_workflow.mixin.has_interface_mixins.UsesState,\n", + " pyiron_workflow.mixin.single_output.ExploitsSingleOutput,\n", + " pyiron_workflow.mixin.working.HasWorkingDirectory,\n", + " pyiron_workflow.mixin.storage.HasH5ioStorage,\n", + " pyiron_workflow.mixin.storage.HasTinybaseStorage,\n", + " pyiron_workflow.mixin.storage.HasStorage,\n", + " pyiron_workflow.mixin.has_interface_mixins.HasLabel,\n", + " pyiron_workflow.mixin.has_interface_mixins.HasParent,\n", + " pyiron_workflow.mixin.has_interface_mixins.HasRun,\n", + " pyiron_workflow.mixin.has_interface_mixins.HasChannel,\n", + " pyiron_workflow.mixin.preview.ScrapesIO,\n", + " pyiron_workflow.mixin.preview.HasIOPreview,\n", " abc.ABC,\n", " object]" ] @@ -1690,7 +1690,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 41, @@ -2435,7 +2435,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 50, @@ -2469,425 +2469,425 @@ "clusterAddThenCount\n", "\n", "AddThenCount: AddThenCount\n", - "\n", - "clusterAddThenCountadd_to_n2\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "add_to_n2: AddThree\n", - "\n", - "\n", - "clusterAddThenCountadd_to_n2Inputs\n", + "\n", + "clusterAddThenCountInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountadd_to_n2OutputsWithInjection\n", + "\n", + "clusterAddThenCountOutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountadd_oneadd_to_n2\n", + "\n", + "clusterAddThenCountadd_to_n1\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", - "add_one: AddOne\n", + "\n", + "add_to_n1: AddThree\n", "\n", - "\n", - "clusterAddThenCountadd_oneadd_to_n2Inputs\n", + "\n", + "clusterAddThenCountadd_to_n1Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountadd_oneadd_to_n2OutputsWithInjection\n", + "\n", + "clusterAddThenCountadd_to_n1OutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountadd_twoadd_to_n2\n", + "\n", + "clusterAddThenCountadd_oneadd_to_n1\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "add_two: AddOne\n", + "\n", + "add_one: AddOne\n", "\n", - "\n", - "clusterAddThenCountadd_twoadd_to_n2Inputs\n", + "\n", + "clusterAddThenCountadd_oneadd_to_n1Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountadd_twoadd_to_n2OutputsWithInjection\n", + "\n", + "clusterAddThenCountadd_oneadd_to_n1OutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountadd_threeadd_to_n2\n", + "\n", + "clusterAddThenCountadd_twoadd_to_n1\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "add_three: AddOne\n", + "\n", + "add_two: AddOne\n", "\n", - "\n", - "clusterAddThenCountadd_threeadd_to_n2Inputs\n", + "\n", + "clusterAddThenCountadd_twoadd_to_n1Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountadd_threeadd_to_n2OutputsWithInjection\n", + "\n", + "clusterAddThenCountadd_twoadd_to_n1OutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountsum_plus_5\n", + "\n", + "clusterAddThenCountadd_threeadd_to_n1\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "sum_plus_5: Add\n", + "\n", + "add_three: AddOne\n", "\n", - "\n", - "clusterAddThenCountsum_plus_5Inputs\n", + "\n", + "clusterAddThenCountadd_threeadd_to_n1Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountsum_plus_5OutputsWithInjection\n", + "\n", + "clusterAddThenCountadd_threeadd_to_n1OutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountdigit_counter\n", + "\n", + "clusterAddThenCountadd_to_n2\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "digit_counter: HowManyDigits\n", + "\n", + "add_to_n2: AddThree\n", "\n", - "\n", - "clusterAddThenCountdigit_counterInputs\n", + "\n", + "clusterAddThenCountadd_to_n2Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountdigit_counterOutputsWithInjection\n", + "\n", + "clusterAddThenCountadd_to_n2OutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountas_strdigit_counter\n", + "\n", + "clusterAddThenCountadd_threeadd_to_n2\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "as_str: String\n", + "\n", + "add_three: AddOne\n", "\n", - "\n", - "clusterAddThenCountas_strdigit_counterInputs\n", + "\n", + "clusterAddThenCountadd_threeadd_to_n2Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountas_strdigit_counterOutputsWithInjection\n", + "\n", + "clusterAddThenCountadd_threeadd_to_n2OutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountdigitsdigit_counter\n", + "\n", + "clusterAddThenCountadd_oneadd_to_n2\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "digits: Length\n", + "\n", + "add_one: AddOne\n", "\n", - "\n", - "clusterAddThenCountdigitsdigit_counterInputs\n", + "\n", + "clusterAddThenCountadd_oneadd_to_n2Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountdigitsdigit_counterOutputsWithInjection\n", + "\n", + "clusterAddThenCountadd_oneadd_to_n2OutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountInputs\n", + "\n", + "clusterAddThenCountadd_twoadd_to_n2\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "add_two: AddOne\n", "\n", - "\n", - "clusterAddThenCountOutputsWithInjection\n", + "\n", + "clusterAddThenCountadd_twoadd_to_n2OutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", - "\n", - "\n", - "clusterAddThenCountadd_to_n1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "add_to_n1: AddThree\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountadd_to_n1OutputsWithInjection\n", + "\n", + "clusterAddThenCountadd_twoadd_to_n2Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountadd_oneadd_to_n1\n", + "\n", + "clusterAddThenCountsum_plus_5\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "add_one: AddOne\n", + "\n", + "sum_plus_5: Add\n", "\n", - "\n", - "clusterAddThenCountadd_oneadd_to_n1Inputs\n", + "\n", + "clusterAddThenCountsum_plus_5Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountadd_oneadd_to_n1OutputsWithInjection\n", + "\n", + "clusterAddThenCountsum_plus_5OutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountadd_twoadd_to_n1\n", + "\n", + "clusterAddThenCountdigit_counter\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", - "add_two: AddOne\n", + "\n", + "digit_counter: HowManyDigits\n", "\n", - "\n", - "clusterAddThenCountadd_twoadd_to_n1Inputs\n", + "\n", + "clusterAddThenCountdigit_counterInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountadd_twoadd_to_n1OutputsWithInjection\n", + "\n", + "clusterAddThenCountdigit_counterOutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountadd_threeadd_to_n1\n", + "\n", + "clusterAddThenCountas_strdigit_counter\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "add_three: AddOne\n", + "\n", + "as_str: String\n", "\n", - "\n", - "clusterAddThenCountadd_threeadd_to_n1Inputs\n", + "\n", + "clusterAddThenCountas_strdigit_counterInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", - "clusterAddThenCountadd_threeadd_to_n1OutputsWithInjection\n", + "\n", + "clusterAddThenCountas_strdigit_counterOutputsWithInjection\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "OutputsWithInjection\n", + "\n", + "OutputsWithInjection\n", "\n", - "\n", - "clusterAddThenCountadd_to_n1Inputs\n", + "\n", + "clusterAddThenCountdigitsdigit_counter\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "digits: Length\n", + "\n", + "\n", + "clusterAddThenCountdigitsdigit_counterInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "Inputs\n", + "\n", + "Inputs\n", "\n", - "\n", + "\n", + "clusterAddThenCountdigitsdigit_counterOutputsWithInjection\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "OutputsWithInjection\n", + "\n", + "\n", "\n", "clusterAddThenCountInputsrun\n", "\n", @@ -3507,7 +3507,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 51, @@ -3845,7 +3845,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 52, @@ -3899,7 +3899,7 @@ "output_type": "stream", "text": [ "None 1\n", - " 5\n" + " 5\n" ] } ], @@ -3985,7 +3985,7 @@ "output_type": "stream", "text": [ "None 1\n", - " 5\n", + " 5\n", "Finally 5\n", "b (Add):\n", "Inputs ['obj', 'other']\n", @@ -4046,7 +4046,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "6.01345855499676\n" + "6.017223608003405\n" ] } ], @@ -4078,7 +4078,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "5.336432242002047\n" + "4.0332591559999855\n" ] } ], @@ -4690,6 +4690,662 @@ "n.zip(obj=list(range(5)), other=list(range(10,15)))" ] }, + { + "cell_type": "markdown", + "id": "e09e6b40-6cbe-4198-817c-a9f1e75e44b3", + "metadata": {}, + "source": [ + "# While loops\n", + "\n", + "We don't have the same helper interfaces for \"while\" loops, but these can be built from a macro node as long as care is taken to specify the macro's starting nodes and the execution order.\n", + "\n", + "By encapsulating your while-loop logic into body and condition nodes, the example below is extremely generic and can be extended to any number of cyclic applications:" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "e0916c46-132c-4046-827e-a75fff6dbc5b", + "metadata": {}, + "outputs": [], + "source": [ + "@Workflow.wrap.as_macro_node(\"greater\")\n", + "def AddWhileLessThan(self, a, b, cap):\n", + " \"\"\"\n", + " Add :param:`b` to :param:`a` while the sum is less than or equal to :param:`cap`.\n", + "\n", + " A simple but complete demonstrator for how to construct cyclic flows, including\n", + " logging key outputs during the loop.\n", + " \"\"\"\n", + " # Bespoke logic\n", + " self.body = Workflow.create.standard.Add(obj=a, other=b)\n", + " self.body.inputs.obj = self.body.outputs.add # Higher priority connection\n", + " # The output is NOT_DATA on the first pass and `a` gets used, \n", + " # But after that the node will find and use its own output\n", + " self.condition = Workflow.create.standard.LessThan(self.body, cap)\n", + "\n", + " # Universal logic\n", + " self.switch = Workflow.create.standard.If()\n", + " self.switch.inputs.condition = self.condition\n", + "\n", + " self.starting_nodes = [self.body]\n", + " self.body >> self.condition >> self.switch\n", + " self.switch.signals.output.true >> self.body\n", + "\n", + " # Bespoke logging\n", + " self.history = Workflow.create.standard.AppendToList()\n", + " self.history.inputs.existing = self.history\n", + " self.history.inputs.new_element = self.body\n", + " self.body >> self.history\n", + "\n", + " # Returns are pretty universal for single-value body nodes,\n", + " # assuming a log of the history is not desired as output,\n", + " # but in general return values are also bespoke\n", + " return self.body" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "ce8a3c1f-64e9-4da1-b29b-d16717d03b22", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThan\n", + "\n", + "AddWhileLessThan: AddWhileLessThan\n", + "\n", + "clusterAddWhileLessThanInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "clusterAddWhileLessThanOutputsWithInjection\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "OutputsWithInjection\n", + "\n", + "\n", + "clusterAddWhileLessThanbody\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "body: Add\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyOutputsWithInjection\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "OutputsWithInjection\n", + "\n", + "\n", + "clusterAddWhileLessThancondition\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "condition: LessThan\n", + "\n", + "\n", + "clusterAddWhileLessThanconditionInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "clusterAddWhileLessThanconditionOutputsWithInjection\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "OutputsWithInjection\n", + "\n", + "\n", + "clusterAddWhileLessThanswitch\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "switch: If\n", + "\n", + "\n", + "clusterAddWhileLessThanswitchInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "clusterAddWhileLessThanswitchOutputsWithInjection\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "OutputsWithInjection\n", + "\n", + "\n", + "clusterAddWhileLessThanhistory\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "history: AppendToList\n", + "\n", + "\n", + "clusterAddWhileLessThanhistoryInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", + "\n", + "clusterAddWhileLessThanhistoryOutputsWithInjection\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "OutputsWithInjection\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanInputsrun\n", + "\n", + "run\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanOutputsWithInjectionran\n", + "\n", + "ran\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanInputsaccumulate_and_run\n", + "\n", + "accumulate_and_run\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanInputsa\n", + "\n", + "a\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyInputsobj\n", + "\n", + "obj\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanInputsa->clusterAddWhileLessThanbodyInputsobj\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanInputsb\n", + "\n", + "b\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyInputsother\n", + "\n", + "other\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanInputsb->clusterAddWhileLessThanbodyInputsother\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanInputscap\n", + "\n", + "cap\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanconditionInputsother\n", + "\n", + "other\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanInputscap->clusterAddWhileLessThanconditionInputsother\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanOutputsWithInjectiongreater\n", + "\n", + "greater\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyInputsrun\n", + "\n", + "run\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyOutputsWithInjectionran\n", + "\n", + "ran\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyInputsaccumulate_and_run\n", + "\n", + "accumulate_and_run\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanconditionInputsrun\n", + "\n", + "run\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyOutputsWithInjectionran->clusterAddWhileLessThanconditionInputsrun\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanhistoryInputsrun\n", + "\n", + "run\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyOutputsWithInjectionran->clusterAddWhileLessThanhistoryInputsrun\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyOutputsWithInjectionadd\n", + "\n", + "add\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyOutputsWithInjectionadd->clusterAddWhileLessThanOutputsWithInjectiongreater\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyOutputsWithInjectionadd->clusterAddWhileLessThanbodyInputsobj\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanconditionInputsobj\n", + "\n", + "obj\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyOutputsWithInjectionadd->clusterAddWhileLessThanconditionInputsobj\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanhistoryInputsnew_element\n", + "\n", + "new_element\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanbodyOutputsWithInjectionadd->clusterAddWhileLessThanhistoryInputsnew_element\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanconditionOutputsWithInjectionran\n", + "\n", + "ran\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanconditionInputsaccumulate_and_run\n", + "\n", + "accumulate_and_run\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanswitchInputsrun\n", + "\n", + "run\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanconditionOutputsWithInjectionran->clusterAddWhileLessThanswitchInputsrun\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanconditionOutputsWithInjectionlt\n", + "\n", + "lt\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanswitchInputscondition\n", + "\n", + "condition\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanconditionOutputsWithInjectionlt->clusterAddWhileLessThanswitchInputscondition\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanswitchOutputsWithInjectionran\n", + "\n", + "ran\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanswitchInputsaccumulate_and_run\n", + "\n", + "accumulate_and_run\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanswitchOutputsWithInjectiontrue\n", + "\n", + "true\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanswitchOutputsWithInjectiontrue->clusterAddWhileLessThanbodyInputsrun\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanswitchOutputsWithInjectionfalse\n", + "\n", + "false\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanswitchOutputsWithInjectiontruth\n", + "\n", + "truth\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanhistoryOutputsWithInjectionran\n", + "\n", + "ran\n", + "\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanhistoryInputsaccumulate_and_run\n", + "\n", + "accumulate_and_run\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanhistoryInputsexisting\n", + "\n", + "existing\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanhistoryOutputsWithInjectionlist\n", + "\n", + "list\n", + "\n", + "\n", + "\n", + "clusterAddWhileLessThanhistoryOutputsWithInjectionlist->clusterAddWhileLessThanhistoryInputsexisting\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cyclic = AddWhileLessThan()\n", + "cyclic.draw(size=(10, 10))" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "id": "32d1c20d-d484-434f-9b10-02ee17df4eee", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'greater': 6}" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cyclic(0, 2, 5)" + ] + }, + { + "cell_type": "markdown", + "id": "90bfa6fa-6218-41c1-8dc4-630f511cffb6", + "metadata": {}, + "source": [ + "We can examine the provenance as usual. In this case, because the graph is cyclic, the same node appears multiple times" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "d44e90ad-4a96-4c0a-a4d2-0e3ed394479c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['body',\n", + " 'history',\n", + " 'condition',\n", + " 'switch',\n", + " 'body',\n", + " 'history',\n", + " 'condition',\n", + " 'switch',\n", + " 'body',\n", + " 'history',\n", + " 'condition',\n", + " 'switch']" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cyclic.provenance_by_execution" + ] + }, + { + "cell_type": "markdown", + "id": "686fbf70-de7d-4040-9840-a6c8fffbd036", + "metadata": {}, + "source": [ + "Since the nodes only store their instantaneous IO state and not its history, we've added a \"history\" node into the workflow. It's not part of the macro output in this example, but you can always dig into the node to examine its state" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "5814f830-fe47-4bce-b8f4-35df63565e6b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[2, 4, 6]" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cyclic.history.outputs.list.value" + ] + }, + { + "cell_type": "markdown", + "id": "46af7a97-57dc-4d68-886f-c6f00de97597", + "metadata": {}, + "source": [ + "And, of course, such a macro can be (un)pickled as usual" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "4a7eece3-20dd-4f57-88f2-5a1076daeb08", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pickle\n", + "\n", + "reloaded = pickle.loads(pickle.dumps(cyclic))\n", + "reloaded.outputs.greater.value" + ] + }, { "cell_type": "markdown", "id": "f447531e-3e8c-4c7e-a579-5f9c56b75a5b", @@ -4734,7 +5390,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 70, "id": "c8196054-aff3-4d39-a872-b428d329dac9", "metadata": {}, "outputs": [], @@ -4744,7 +5400,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 71, "id": "ffd741a3-b086-4ed0-9a62-76143a3705b2", "metadata": {}, "outputs": [], @@ -4761,27 +5417,10 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 72, "id": "3a22c622-f8c1-449b-a910-c52beb6a09c3", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/node.py:377: UserWarning: A saved file was found for the node save_demo -- attempting to load it...(To delete the saved file instead, use `overwrite_save=True`)\n", - " warnings.warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/node.py:377: UserWarning: A saved file was found for the node inp -- attempting to load it...(To delete the saved file instead, use `overwrite_save=True`)\n", - " warnings.warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/node.py:377: UserWarning: A saved file was found for the node middle -- attempting to load it...(To delete the saved file instead, use `overwrite_save=True`)\n", - " warnings.warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/node.py:377: UserWarning: A saved file was found for the node end -- attempting to load it...(To delete the saved file instead, use `overwrite_save=True`)\n", - " warnings.warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/node.py:377: UserWarning: A saved file was found for the node out -- attempting to load it...(To delete the saved file instead, use `overwrite_save=True`)\n", - " warnings.warn(\n" - ] - } - ], + "outputs": [], "source": [ "if sys.version_info >= (3, 11):\n", " reloaded = Workflow(\"save_demo\")\n", @@ -4800,7 +5439,7 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 73, "id": "0999d3e8-3a5a-451d-8667-a01dae7c1193", "metadata": {}, "outputs": [], @@ -4809,159 +5448,6 @@ " reloaded.storage.delete()" ] }, - { - "cell_type": "markdown", - "id": "4e7ed210-dbc2-4afa-825e-b91168baff25", - "metadata": {}, - "source": [ - "# While-loops\n", - "\n", - "Similar to for-loops, we can also create a while-loop, which takes both a body node and a condition node. The condition node must be a single-output `Function` node returning a `bool` type. Instead of creating copies of the body node, the body node gets re-run until the condition node returns `False`.\n", - "\n", - "You _must_ specify the data connection so that the body node passes information to the condition node. You may optionally also loop output of the body node back to input of the body node to change the input at each iteration. Right now this is done with horribly ugly string tuples, but we're working on improving this interface and making it more like the for-loop.\n", - "\n", - "Note: The body (and condition) node classes passed to while-loops must be importable, i.e. they can come from a node package, or be defined here in the notebook (importable from `__main__`), but you can't use, e.g., a node defined _inside_ the scope of some other function." - ] - }, - { - "cell_type": "code", - "execution_count": 68, - "id": "0dd04b4c-e3e7-4072-ad34-58f2c1e4f596", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io_preview.py:261: OutputLabelsNotValidated: Could not find the source code to validate AddWhileLessThan_m1436776219946974525 output labels against the number of returned values -- proceeding without validation\n", - " warnings.warn(\n" - ] - } - ], - "source": [ - "@Workflow.wrap.as_function_node(\"sum\")\n", - "def Add(a, b):\n", - " print(f\"{a} + {b} = {a + b}\")\n", - " return a + b\n", - "\n", - "AddWhile = Workflow.create.meta.while_loop(\n", - " loop_body_class=Add,\n", - " condition_class=Workflow.create.standard.LessThan,\n", - " internal_connection_map=[\n", - " (\"Add\", \"sum\", \"LessThan\", \"obj\"),\n", - " (\"Add\", \"sum\", \"Add\", \"a\")\n", - " ],\n", - " inputs_map={\"Add__a\": \"a\", \"Add__b\": \"b\", \"LessThan__other\": \"threshold\"},\n", - " outputs_map={\"Add__sum\": \"total\"}\n", - ")\n", - "\n", - "wf = Workflow(\"do_while\")\n", - "wf.add_while = AddWhile(threshold=10)\n", - "\n", - "wf.inputs_map = {\n", - " \"add_while__a\": \"a\",\n", - " \"add_while__b\": \"b\"\n", - "}\n", - "wf.outputs_map = {\n", - " \"add_while__total\": \"total\", # Rename this output\n", - " \"add_while__switch__truth\": None # Disable this output\n", - "}" - ] - }, - { - "cell_type": "markdown", - "id": "eb810e1e-4d13-4cb1-94cc-6d191b8c568d", - "metadata": {}, - "source": [ - "Note that initializing the `a` and `b` input to numeric values when we call the workflow below does not destroy the connection made between the body node input and output -- so the first run of the body node uses the initial value passed, but then it updates its own input for subsequent calls!" - ] - }, - { - "cell_type": "code", - "execution_count": 69, - "id": "2dfb967b-41ac-4463-b606-3e315e617f2a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1 + 2 = 3\n", - "3 + 2 = 5\n", - "5 + 2 = 7\n", - "7 + 2 = 9\n", - "9 + 2 = 11\n", - "Finally {'total': 11}\n" - ] - } - ], - "source": [ - "response = wf(a=1, b=2)\n", - "print(\"Finally\", response)" - ] - }, - { - "cell_type": "code", - "execution_count": 70, - "id": "2e87f858-b327-4f6b-9237-c8a557f29aeb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.847 > 0.2\n", - "0.591 > 0.2\n", - "0.250 > 0.2\n", - "0.105 <= 0.2\n", - "Finally 0.105\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io_preview.py:261: OutputLabelsNotValidated: Could not find the source code to validate RandomWhileGreaterThan_6561917341211919985 output labels against the number of returned values -- proceeding without validation\n", - " warnings.warn(\n" - ] - } - ], - "source": [ - "@Workflow.wrap.as_function_node(\"random\")\n", - "def Random():\n", - " import random\n", - " return random.random()\n", - "\n", - "@Workflow.wrap.as_function_node()\n", - "def GreaterThan(x: float, threshold: float):\n", - " gt = x > threshold\n", - " symbol = \">\" if gt else \"<=\"\n", - " print(f\"{x:.3f} {symbol} {threshold}\")\n", - " return gt\n", - "\n", - "RandomWhile = Workflow.create.meta.while_loop(\n", - " loop_body_class=Random,\n", - " condition_class=GreaterThan,\n", - " internal_connection_map=[(\"Random\", \"random\", \"GreaterThan\", \"x\")],\n", - " inputs_map={\"GreaterThan__threshold\": \"threshold\"},\n", - " outputs_map={\"Random__random\": \"capped_result\"}\n", - ")\n", - "\n", - "# Define workflow\n", - "\n", - "wf = Workflow(\"random_until_small_enough\")\n", - "\n", - "## Wire together the while loop and its condition\n", - "\n", - "wf.random_while = RandomWhile()\n", - "\n", - "## Give convenient labels\n", - "wf.inputs_map = {\"random_while__threshold\": \"threshold\"}\n", - "wf.outputs_map = {\"random_while__capped_result\": \"capped_result\"}\n", - "\n", - "print(f\"Finally {wf(threshold=0.2).capped_result:.3f}\")" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/pyiron_workflow/__init__.py b/pyiron_workflow/__init__.py index f4700b4c..cd856745 100644 --- a/pyiron_workflow/__init__.py +++ b/pyiron_workflow/__init__.py @@ -53,4 +53,3 @@ inputs_to_list, list_to_outputs, ) -from pyiron_workflow.nodes.while_loop import while_loop diff --git a/pyiron_workflow/create.py b/pyiron_workflow/create.py index b1ac9364..e8f7b6d6 100644 --- a/pyiron_workflow/create.py +++ b/pyiron_workflow/create.py @@ -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, } ) diff --git a/pyiron_workflow/nodes/standard.py b/pyiron_workflow/nodes/standard.py index 9b1a5fb2..a4dc003b 100644 --- a/pyiron_workflow/nodes/standard.py +++ b/pyiron_workflow/nodes/standard.py @@ -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(): """ @@ -652,6 +689,7 @@ def Round(obj): Absolute, Add, And, + AppendToList, Bool, Bytes, Contains, diff --git a/pyiron_workflow/nodes/while_loop.py b/pyiron_workflow/nodes/while_loop.py deleted file mode 100644 index 2899cd6a..00000000 --- a/pyiron_workflow/nodes/while_loop.py +++ /dev/null @@ -1,198 +0,0 @@ -from __future__ import annotations - -from textwrap import dedent -from typing import Optional - -import pyiron_workflow -from pyiron_workflow.nodes.function import Function -from pyiron_workflow.nodes.macro import Macro -from pyiron_workflow.node import Node - - -def while_loop( - loop_body_class: type[Node], - condition_class: type[Function], - internal_connection_map: dict[str, str], - inputs_map: Optional[dict[str, str]], - outputs_map: Optional[dict[str, str]], -) -> type[Macro]: - """ - An _extremely rough_ second draft of a for-loop meta-node. - - Takes body and condition node classes and builds a macro that makes a cyclic signal - connection between them and an "if" switch, i.e. when the body node finishes it - runs the condtion, which runs the switch, and as long as the condition result was - `True`, the switch loops back to run the body again. - We additionally allow four-tuples of (input node, input channel, output node, - output channel) labels to wire data connections inside the macro, e.g. to pass data - from the body to the condition. This is beastly syntax, but it will suffice for now. - Finally, you can set input and output maps as normal. - - Args: - loop_body_class (type[pyiron_workflow.node.Node]): The class for the - body of the while-loop. - condition_class (type[pyiron_workflow.function.Function]): A - single-output function node returning a `bool` controlling the while loop - exit condition (exits on False) - internal_connection_map (list[tuple[str, str, str, str]]): String tuples - giving (input node, input channel, output node, output channel) labels - connecting channel pairs inside the macro. - inputs_map (dict[str, str]): Define the inputs for the new macro like - `{body/condition class name}__{input channel}: {macro input channel name}` - outputs_map (dict[str, str]): Define the outputs for the new macro like - `{body/condition class name}__{output channel}: {macro output channel name}` - - Warnings: - The loop body and condition classes must be importable. E.g. they can come from - a node package or be defined in `__main__`, but not defined inside the scope of - some other function. - - Examples: - - >>> from pyiron_workflow import Workflow - >>> - >>> AddWhile = Workflow.create.meta.while_loop( - ... loop_body_class=Workflow.create.standard.Add, - ... condition_class=Workflow.create.standard.LessThan, - ... internal_connection_map=[ - ... ("Add", "add", "LessThan", "obj"), - ... ("Add", "add", "Add", "obj") - ... ], - ... inputs_map={ - ... "Add__obj": "a", - ... "Add__other": "b", - ... "LessThan__other": "cap" - ... }, - ... outputs_map={"Add__add": "total"} - ... ) - >>> - >>> wf = Workflow("do_while") - >>> wf.add_while = AddWhile(cap=10) - >>> - >>> wf.inputs_map = { - ... "add_while__a": "a", - ... "add_while__b": "b" - ... } - >>> wf.outputs_map = {"add_while__total": "total"} - >>> - >>> print(f"Finally, {wf(a=1, b=2).total}") - Finally, 11 - - >>> import random - >>> - >>> from pyiron_workflow import Workflow - >>> - >>> random.seed(0) # Set the seed so the output is consistent and doctest runs - >>> - >>> RandomWhile = Workflow.create.meta.while_loop( - ... loop_body_class=Workflow.create.standard.RandomFloat, - ... condition_class=Workflow.create.standard.GreaterThan, - ... internal_connection_map=[ - ... ("RandomFloat", "random", "GreaterThan", "obj") - ... ], - ... inputs_map={"GreaterThan__other": "threshold"}, - ... outputs_map={"RandomFloat__random": "capped_result"} - ... ) - >>> - >>> # Define workflow - >>> - >>> wf = Workflow("random_until_small_enough") - >>> - >>> ## Wire together the while loop and its condition - >>> - >>> wf.random_while = RandomWhile() - >>> - >>> ## Give convenient labels - >>> wf.inputs_map = {"random_while__threshold": "threshold"} - >>> wf.outputs_map = {"random_while__capped_result": "capped_result"} - >>> - >>> # Set a threshold and run - >>> print(f"Finally {wf(threshold=0.3).capped_result:.3f}") - Finally 0.259 - """ - - # Make sure each dynamic class is getting a unique name - io_hash = hash( - ",".join( - [ - "_".join(s for conn in internal_connection_map for s in conn), - "".join(f"{k}:{v}" for k, v in sorted(inputs_map.items())), - "".join(f"{k}:{v}" for k, v in sorted(outputs_map.items())), - ] - ) - ) - io_hash = str(io_hash).replace("-", "m") - node_name = f"{loop_body_class.__name__}While{condition_class.__name__}_{io_hash}" - - # Build code components that need an f-string, slash, etc. - output_labels = ", ".join(f'"{l}"' for l in outputs_map.values()).rstrip(" ") - input_args = ", ".join(l for l in inputs_map.values()).rstrip(" ") - - def get_kwargs(io_map: dict[str, str], node_class: type[Node]): - return ", ".join( - f'{k.split("__")[1]}={v}' - for k, v in io_map.items() - if k.split("__")[0] == node_class.__name__ - ).rstrip(" ") - - returns = ", ".join( - f'self.{l.split("__")[0]}.outputs.{l.split("__")[1]}' - for l in outputs_map.keys() - ).rstrip(" ") - - # Assemble components into a decorated while-loop macro - while_loop_code = dedent( - f""" - @Macro.wrap.as_macro_node({output_labels}) - def {node_name}(self, {input_args}): - from {loop_body_class.__module__} import {loop_body_class.__name__} - from {condition_class.__module__} import {condition_class.__name__} - - body = self.add_child( - {loop_body_class.__name__}( - label="{loop_body_class.__name__}", - {get_kwargs(inputs_map, loop_body_class)} - ) - ) - - condition = self.add_child( - {condition_class.__name__}( - label="{condition_class.__name__}", - {get_kwargs(inputs_map, condition_class)} - ) - ) - - self.switch = self.create.standard.If(condition=condition) - - for out_n, out_c, in_n, in_c in {str(internal_connection_map)}: - self.children[in_n].inputs[in_c] = self.children[out_n].outputs[out_c] - - - self.switch.signals.output.true >> body >> condition >> self.switch - self.starting_nodes = [body] - - return {returns} - """ - ) - - exec(while_loop_code) - return locals()[node_name] - - # def make_loop(macro): - # body_node = macro.add_child(loop_body_class(label=loop_body_class.__name__)) - # condition_node = macro.add_child( - # condition_class(label=condition_class.__name__) - # ) - # switch = macro.create.standard.If(label="switch", parent=macro) - # - # switch.inputs.condition = condition_node - # for out_n, out_c, in_n, in_c in internal_connection_map: - # macro.children[in_n].inputs[in_c] = macro.children[out_n].outputs[out_c] - # - # switch.signals.output.true >> body_node >> condition_node >> switch - # macro.starting_nodes = [body_node] - # - # macro.inputs_map = {} if inputs_map is None else inputs_map - # macro.outputs_map = {} if outputs_map is None else outputs_map - # - # return as_macro_node()(make_loop) diff --git a/tests/integration/test_while.py b/tests/integration/test_while.py new file mode 100644 index 00000000..2535dc19 --- /dev/null +++ b/tests/integration/test_while.py @@ -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" + ) diff --git a/tests/integration/test_workflow.py b/tests/integration/test_workflow.py index e283bc5d..12886ed3 100644 --- a/tests/integration/test_workflow.py +++ b/tests/integration/test_workflow.py @@ -6,7 +6,6 @@ from pyiron_workflow._tests import ensure_tests_in_python_path from pyiron_workflow.channels import OutputSignal from pyiron_workflow.nodes.function import Function -from pyiron_workflow.nodes.while_loop import while_loop from pyiron_workflow.workflow import Workflow @@ -123,68 +122,6 @@ def test_for_loop(self): expectation, ) - def test_while_loop(self): - - with self.subTest("Random"): - random.seed(0) - - RandomWhile = while_loop( - loop_body_class=RandomFloat, - condition_class=GreaterThan, - internal_connection_map=[ - ("RandomFloat", "random", "GreaterThan", "x") - ], - inputs_map={"GreaterThan__threshold": "threshold"}, - outputs_map={"RandomFloat__random": "capped_result"} - ) - - # Define workflow - - wf = Workflow("random_until_small_enough") - - ## Wire together the while loop and its condition - - wf.random_while = RandomWhile() - - ## Give convenient labels - wf.inputs_map = {"random_while__threshold": "threshold"} - wf.outputs_map = {"random_while__capped_result": "capped_result"} - - self.assertAlmostEqual( - wf(threshold=0.1).capped_result, - 0.014041700164018955, # For this reason we set the random seed - ) - - with self.subTest("Self-data-loop"): - - AddWhile = while_loop( - loop_body_class=Workflow.create.standard.Add, - condition_class=Workflow.create.standard.LessThan, - internal_connection_map=[ - ("Add", "add", "LessThan", "obj"), - ("Add", "add", "Add", "obj") - ], - inputs_map={ - "Add__obj": "a", - "Add__other": "b", - "LessThan__other": "cap", - }, - outputs_map={"Add__add": "total"} - ) - - wf = Workflow("do_while") - wf.add_while = AddWhile() - - wf.inputs_map = { - "add_while__a": "a", - "add_while__b": "b", - "add_while__cap": "cap" - } - wf.outputs_map = {"add_while__total": "total"} - - out = wf(a=1, b=2, cap=10) - self.assertEqual(out.total, 11) - def test_executor_and_creator_interaction(self): """ Make sure that submitting stuff to a parallel processor doesn't stop us from