From a492c1fb10ec918c0f60aa601d4ee5710b3261f0 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 11 Apr 2024 11:45:07 -0700 Subject: [PATCH 1/5] Use `self` canonically as the first macro argument in the source code --- docs/README.md | 12 +++--- pyiron_workflow/macro.py | 58 ++++++++++++------------- pyiron_workflow/meta.py | 26 +++++------ tests/static/demo_nodes.py | 10 ++--- tests/unit/test_macro.py | 86 ++++++++++++++++++------------------- tests/unit/test_workflow.py | 10 ++--- 6 files changed, 101 insertions(+), 101 deletions(-) diff --git a/docs/README.md b/docs/README.md index 70a38c77d..c08fb187a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -60,13 +60,13 @@ But the intent is to collect them together into a workflow and leverage existing ... return np.arange(n) >>> >>> @Workflow.wrap.as_macro_node("fig") -... def PlotShiftedSquare(macro, n: int, shift: int = 0): -... macro.arange = Arange(n) -... macro.plot = macro.create.plotting.Scatter( -... x=macro.arange + shift, -... y=macro.arange**2 +... def PlotShiftedSquare(self, n: int, shift: int = 0): +... self.arange = Arange(n) +... self.plot = self.create.plotting.Scatter( +... x=self.arange + shift, +... y=self.arange**2 ... ) -... return macro.plot +... return self.plot >>> >>> wf = Workflow("plot_with_and_without_shift") >>> wf.n = wf.create.standard.UserInput() diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 563c70007..99a5e8a2f 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -90,13 +90,13 @@ class Macro(Composite, DecoratedNode, ABC): ... result = x + 1 ... return result >>> - >>> def add_three_macro(macro, one__x): - ... macro.one = macro.create.function_node(add_one, x=one__x) - ... macro.two = macro.create.function_node(add_one, macro.one) - ... macro.three = macro.create.function_node(add_one, macro.two) - ... macro.one >> macro.two >> macro.three - ... macro.starting_nodes = [macro.one] - ... return macro.three + >>> def add_three_macro(self, one__x): + ... self.one = self.create.function_node(add_one, x=one__x) + ... self.two = self.create.function_node(add_one, self.one) + ... self.three = self.create.function_node(add_one, self.two) + ... self.one >> self.two >> self.three + ... self.starting_nodes = [self.one] + ... return self.three In this case we had _no need_ to specify the execution order and starting nodes --it's just an extremely simple DAG after all! -- but it's done here to @@ -116,13 +116,13 @@ class Macro(Composite, DecoratedNode, ABC): We can also nest macros, rename their IO, and provide access to internally-connected IO by inputs and outputs maps: - >>> def nested_macro(macro, inp): - ... macro.a = macro.create.function_node(add_one, x=inp) - ... macro.b = macro.create.macro_node( - ... add_three_macro, one__x=macro.a, output_labels="three__result" + >>> def nested_macro(self, inp): + ... self.a = self.create.function_node(add_one, x=inp) + ... self.b = self.create.macro_node( + ... add_three_macro, one__x=self.a, output_labels="three__result" ... ) - ... macro.c = macro.create.function_node(add_one, x=macro.b) - ... return macro.c, macro.b + ... self.c = self.create.function_node(add_one, x=self.b) + ... return self.c, self.b >>> >>> macro = macro_node( ... nested_macro, output_labels=("out", "intermediate") @@ -134,11 +134,11 @@ class Macro(Composite, DecoratedNode, ABC): is acyclic. Let's build a simple macro with two independent tracks: - >>> def modified_flow_macro(macro, a__x=0, b__x=0): - ... macro.a = macro.create.function_node(add_one, x=a__x) - ... macro.b = macro.create.function_node(add_one, x=b__x) - ... macro.c = macro.create.function_node(add_one, x=macro.b) - ... return macro.a, macro.c + >>> def modified_flow_macro(self, a__x=0, b__x=0): + ... self.a = self.create.function_node(add_one, x=a__x) + ... self.b = self.create.function_node(add_one, x=b__x) + ... self.c = self.create.function_node(add_one, x=self.b) + ... return self.a, self.c >>> >>> m = macro_node(modified_flow_macro, output_labels=("a", "c")) >>> m(a__x=1, b__x=2) @@ -176,10 +176,10 @@ class Macro(Composite, DecoratedNode, ABC): is ignored): >>> @Macro.wrap.as_macro_node() - ... def AddThreeMacro(macro, x): - ... add_three_macro(macro, one__x=x) + ... def AddThreeMacro(self, x): + ... add_three_macro(self, one__x=x) ... # We could also simply have decorated that function to begin with - ... return macro.three + ... return self.three >>> >>> macro = AddThreeMacro() >>> macro(x=0).three @@ -193,9 +193,9 @@ class Macro(Composite, DecoratedNode, ABC): ... _output_labels = ["three"] ... ... @staticmethod - ... def graph_creator(macro, x): - ... add_three_macro(macro, one__x=x) - ... return macro.three + ... def graph_creator(self, x): + ... add_three_macro(self, one__x=x) + ... return self.three >>> >>> macro = AddThreeMacro() >>> macro(x=0).three @@ -228,11 +228,11 @@ class Macro(Composite, DecoratedNode, ABC): data, e.g.: >>> @Macro.wrap.as_macro_node("lout", "n_plus_2") - ... def LikeAFunction(macro, lin: list, n: int = 1): - ... macro.plus_two = n + 2 - ... macro.sliced_list = lin[n:macro.plus_two] - ... macro.double_fork = 2 * n - ... return macro.sliced_list, macro.plus_two.channel + ... def LikeAFunction(self, lin: list, n: int = 1): + ... self.plus_two = n + 2 + ... self.sliced_list = lin[n:self.plus_two] + ... self.double_fork = 2 * n + ... return self.sliced_list, self.plus_two.channel >>> >>> like_functions = LikeAFunction(lin=[1,2,3,4,5,6], n=3) >>> sorted(like_functions().items()) diff --git a/pyiron_workflow/meta.py b/pyiron_workflow/meta.py index 28824e073..7f8b41bde 100644 --- a/pyiron_workflow/meta.py +++ b/pyiron_workflow/meta.py @@ -135,7 +135,7 @@ def for_loop( ).rstrip(" ") input_label = 'f"inp{n}"' returns = ", ".join( - f'macro.children["{label.upper()}"]' for label in output_preview.keys() + f'self.children["{label.upper()}"]' for label in output_preview.keys() ) node_name = f'{loop_body_class.__name__}For{"".join([l.title() for l in sorted(iterate_on)])}{length}' @@ -143,20 +143,20 @@ def for_loop( for_loop_code = dedent( f""" @Macro.wrap.as_macro_node({output_labels}) - def {node_name}(macro, {macro_args}): + def {node_name}(self, {macro_args}): from {loop_body_class.__module__} import {loop_body_class.__name__} for label in [{output_labels}]: - input_to_list({length})(label=label, parent=macro) + input_to_list({length})(label=label, parent=self) for n in range({length}): body_node = {loop_body_class.__name__}( {body_kwargs}, label={body_label}, - parent=macro + parent=self ) for label in {list(output_preview.keys())}: - macro.children[label.upper()].inputs[{input_label}] = body_node.outputs[label] + self.children[label.upper()].inputs[{input_label}] = body_node.outputs[label] return {returns} """ @@ -293,7 +293,7 @@ def get_kwargs(io_map: dict[str, str], node_class: type[Node]): ).rstrip(" ") returns = ", ".join( - f'macro.{l.split("__")[0]}.outputs.{l.split("__")[1]}' + f'self.{l.split("__")[0]}.outputs.{l.split("__")[1]}' for l in outputs_map.keys() ).rstrip(" ") @@ -301,32 +301,32 @@ def get_kwargs(io_map: dict[str, str], node_class: type[Node]): while_loop_code = dedent( f""" @Macro.wrap.as_macro_node({output_labels}) - def {node_name}(macro, {input_args}): + 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 = macro.add_child( + body = self.add_child( {loop_body_class.__name__}( label="{loop_body_class.__name__}", {get_kwargs(inputs_map, loop_body_class)} ) ) - condition = macro.add_child( + condition = self.add_child( {condition_class.__name__}( label="{condition_class.__name__}", {get_kwargs(inputs_map, condition_class)} ) ) - macro.switch = macro.create.standard.If(condition=condition) + self.switch = self.create.standard.If(condition=condition) for out_n, out_c, in_n, in_c in {str(internal_connection_map)}: - macro.children[in_n].inputs[in_c] = macro.children[out_n].outputs[out_c] + self.children[in_n].inputs[in_c] = self.children[out_n].outputs[out_c] - macro.switch.signals.output.true >> body >> condition >> macro.switch - macro.starting_nodes = [body] + self.switch.signals.output.true >> body >> condition >> self.switch + self.starting_nodes = [body] return {returns} """ diff --git a/tests/static/demo_nodes.py b/tests/static/demo_nodes.py index 2e27411bb..8f876a77e 100644 --- a/tests/static/demo_nodes.py +++ b/tests/static/demo_nodes.py @@ -14,11 +14,11 @@ def OptionallyAdd(x: int, y: Optional[int] = None) -> int: @Workflow.wrap.as_macro_node("add_three") -def AddThree(macro, x: int) -> int: - macro.one = macro.create.standard.Add(x, 1) - macro.two = macro.create.standard.Add(macro.one, 1) - macro.three = macro.create.standard.Add(macro.two, 1) - return macro.three +def AddThree(self, x: int) -> int: + self.one = self.create.standard.Add(x, 1) + self.two = self.create.standard.Add(self.one, 1) + self.three = self.create.standard.Add(self.two, 1) + return self.three @Workflow.wrap.as_function_node("add") diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 27ef17855..71d7c8421 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -18,13 +18,13 @@ def add_one(x): return result -def add_three_macro(macro, one__x): - macro.one = function_node(add_one, x=one__x) - function_node(add_one, macro.one, label="two", parent=macro) - macro.add_child(function_node(add_one, macro.two, label="three")) +def add_three_macro(self, one__x): + self.one = function_node(add_one, x=one__x) + function_node(add_one, self.one, label="two", parent=self) + self.add_child(function_node(add_one, self.two, label="three")) # Cover a handful of addition methods, # although these are more thoroughly tested in Workflow tests - return macro.three + return self.three def wrong_return_macro(macro): @@ -84,21 +84,21 @@ def test_value_links(self): def test_execution_automation(self): fully_automatic = add_three_macro - def fully_defined(macro, one__x): - add_three_macro(macro, one__x=one__x) - macro.one >> macro.two >> macro.three - macro.starting_nodes = [macro.one] - return macro.three + def fully_defined(self, one__x): + add_three_macro(self, one__x=one__x) + self.one >> self.two >> self.three + self.starting_nodes = [self.one] + return self.three - def only_order(macro, one__x): - add_three_macro(macro, one__x=one__x) - macro.two >> macro.three - return macro.three + def only_order(self, one__x): + add_three_macro(self, one__x=one__x) + self.two >> self.three + return self.three - def only_starting(macro, one__x): - add_three_macro(macro, one__x=one__x) - macro.starting_nodes = [macro.one] - return macro.three + def only_starting(self, one__x): + add_three_macro(self, one__x=one__x) + self.starting_nodes = [self.one] + return self.three m_auto = macro_node(fully_automatic, output_labels="three__result") m_user = macro_node(fully_defined, output_labels="three__result") @@ -181,28 +181,28 @@ def graph_creator(self, one__x): ) def test_nesting(self): - def nested_macro(macro, a__x): - macro.a = function_node(add_one, a__x) - macro.b = macro_node( + def nested_macro(self, a__x): + self.a = function_node(add_one, a__x) + self.b = macro_node( add_three_macro, - one__x=macro.a, + one__x=self.a, output_labels="three__result" ) - macro.c = macro_node( + self.c = macro_node( add_three_macro, - one__x=macro.b.outputs.three__result, + one__x=self.b.outputs.three__result, output_labels="three__result" ) - macro.d = function_node( + self.d = function_node( add_one, - x=macro.c.outputs.three__result, + x=self.c.outputs.three__result, ) - macro.a >> macro.b >> macro.c >> macro.d - macro.starting_nodes = [macro.a] + self.a >> self.b >> self.c >> self.d + self.starting_nodes = [self.a] # This definition of the execution graph is not strictly necessary in this # simple DAG case; we just do it to make sure nesting definied/automatic - # macros works ok - return macro.d + # selfs works ok + return self.d m = macro_node(nested_macro, output_labels="d__result") self.assertEqual(m(a__x=0).d__result, 8) @@ -421,10 +421,10 @@ def fail_at_zero(x): def test_efficient_signature_interface(self): with self.subTest("Forked input"): @as_macro_node("output") - def MutlipleUseInput(macro, x): - macro.n1 = macro.create.standard.UserInput(x) - macro.n2 = macro.create.standard.UserInput(x) - return macro.n1 + def MutlipleUseInput(self, x): + self.n1 = self.create.standard.UserInput(x) + self.n2 = self.create.standard.UserInput(x) + return self.n1 m = MutlipleUseInput() self.assertEqual( @@ -437,9 +437,9 @@ def MutlipleUseInput(macro, x): with self.subTest("Single destination input"): @as_macro_node("output") - def SingleUseInput(macro, x): - macro.n = macro.create.standard.UserInput(x) - return macro.n + def SingleUseInput(self, x): + self.n = self.create.standard.UserInput(x) + return self.n m = SingleUseInput() self.assertEqual( @@ -451,11 +451,11 @@ def SingleUseInput(macro, x): with self.subTest("Mixed input"): @as_macro_node("output") - def MixedUseInput(macro, x, y): - macro.n1 = macro.create.standard.UserInput(x) - macro.n2 = macro.create.standard.UserInput(y) - macro.n3 = macro.create.standard.UserInput(y) - return macro.n1 + def MixedUseInput(self, x, y): + self.n1 = self.create.standard.UserInput(x) + self.n2 = self.create.standard.UserInput(y) + self.n3 = self.create.standard.UserInput(y) + return self.n1 m = MixedUseInput() self.assertEqual( @@ -467,7 +467,7 @@ def MixedUseInput(macro, x, y): with self.subTest("Pass through"): @as_macro_node("output") - def PassThrough(macro, x): + def PassThrough(self, x): return x m = PassThrough() diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py index ebdafc672..6a66d9dbb 100644 --- a/tests/unit/test_workflow.py +++ b/tests/unit/test_workflow.py @@ -399,11 +399,11 @@ def matches_expectations(results): def test_pull_and_executors(self): @Workflow.wrap.as_macro_node("three__result") - def add_three_macro(macro, one__x): - macro.one = Workflow.create.function_node(plus_one, x=one__x) - macro.two = Workflow.create.function_node(plus_one, x=macro.one) - macro.three = Workflow.create.function_node(plus_one, x=macro.two) - return macro.three + def add_three_macro(self, one__x): + self.one = Workflow.create.function_node(plus_one, x=one__x) + self.two = Workflow.create.function_node(plus_one, x=self.one) + self.three = Workflow.create.function_node(plus_one, x=self.two) + return self.three wf = Workflow("pulling") From 4194c257229f9c9f18deb51cf58917b7197041b8 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 11 Apr 2024 11:48:11 -0700 Subject: [PATCH 2/5] Remove unused imports --- pyiron_workflow/macro.py | 1 - tests/unit/test_function.py | 2 -- tests/unit/test_macro.py | 1 - 3 files changed, 4 deletions(-) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 99a5e8a2f..0e3c833f7 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -8,7 +8,6 @@ from abc import ABC, abstractmethod import re from typing import Literal, Optional, TYPE_CHECKING -import warnings from pyiron_workflow.channels import InputData, OutputData from pyiron_workflow.composite import Composite diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 3c484fb25..6e144c2cf 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -1,11 +1,9 @@ from typing import Optional, Union import unittest -import warnings from pyiron_workflow.channels import NOT_DATA from pyiron_workflow.function import function_node, as_function_node from pyiron_workflow.io import ConnectionCopyError, ValueCopyError -from pyiron_workflow.create import Executor def throw_error(x: Optional[int] = None): diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 71d7c8421..27956ee76 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -1,6 +1,5 @@ import sys from concurrent.futures import Future -from functools import partialmethod from time import sleep import unittest From 0b889c91eb013b086f6ff975ce41e93c81efdbee Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 11 Apr 2024 11:51:05 -0700 Subject: [PATCH 3/5] Update readme --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index c08fb187a..d6082c445 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,7 +26,7 @@ Individual node computations can be shipped off to parallel processes for scalab Once you're happy with a workflow, it can be easily turned it into a macro for use in other workflows. This allows the clean construction of increasingly complex computation graphs by composing simpler graphs. -Nodes (including macros) can be stored in plain text, and registered by future workflows for easy access. This encourages and supports an ecosystem of useful nodes, so you don't need to re-invent the wheel. (This is a beta-feature, with full support of [FAIR](https://en.wikipedia.org/wiki/FAIR_data) principles for node packages planned.) +Nodes (including macros) can be stored in plain text as python code, and registered by future workflows for easy access. This encourages and supports an ecosystem of useful nodes, so you don't need to re-invent the wheel. (This is a beta-feature, with full support of [FAIR](https://en.wikipedia.org/wiki/FAIR_data) principles for node packages planned.) Executed or partially-executed graphs can be stored to file, either by explicit call or automatically after running. When creating a new node(/macro/workflow), the working directory is automatically inspected for a save-file and the node will try to reload itself if one is found. (This is an alpha-feature, so it is currently only possible to save entire graphs at once and not individual nodes within a graph, all the child nodes in a saved graph must have been instantiated by `Workflow.create` (or equivalent, i.e. their code lives in a `.py` file that has been registered), and there are no safety rails to protect you from changing the node source code between saving and loading (which may cause errors/inconsistencies depending on the nature of the changes).) From d6edaa059e969617630d8944c37f72d0d3fd82d0 Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 11 Apr 2024 12:04:22 -0700 Subject: [PATCH 4/5] Update quickstart --- notebooks/quickstart.ipynb | 233 +++++++++++++++++++++---------------- 1 file changed, 133 insertions(+), 100 deletions(-) diff --git a/notebooks/quickstart.ipynb b/notebooks/quickstart.ipynb index d90639ce9..94dde6c60 100644 --- a/notebooks/quickstart.ipynb +++ b/notebooks/quickstart.ipynb @@ -212,6 +212,39 @@ "n3()" ] }, + { + "cell_type": "markdown", + "id": "23aa57a3-12a9-418c-ba7e-2aaa1a4ba2b0", + "metadata": {}, + "source": [ + "The names of input to nodes is pulled directly from the signature of the wrapped function. By default, we also scrape the names of the output labels this way. Sometimes you want to return something that looks \"ugly\" -- like `x + 1` in the example above. You can create a new local variable that looks \"pretty\" (`y = x + 1` above) and return that, or you can pass an output label to the decorator. Nodes also pull hints and defaults from the function they wrap. We can re-write our example above to leverage all of this:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e6d06a0c-a558-4bb0-b72e-83820a6f0186", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'inputs': {'x': (int, NOT_DATA)}, 'outputs': {'y': int}}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "@Workflow.wrap.as_function_node(\"y\")\n", + "def AddOne(x: int) -> int:\n", + " return x + 1\n", + "\n", + "AddOne.preview_io()" + ] + }, { "cell_type": "markdown", "id": "dfa3db51-31d7-43c8-820a-6e5f3525837e", @@ -223,12 +256,12 @@ "\n", "The `Workflow` class not only gives us access to the decorators for defining new nodes, but also lets us register modules of existing nodes and use them. Let's put together a workflow that uses both an existing node from a package, and another function node that has multiple return values. This function node will also exploit our ability to name outputs (in the decorator argument) and give type hints (in the function signature, as usual). \n", "\n", - "In addition to using output channels (or nodes, if they have only a single output) to make connections to input channels, we can perform many (but not all) other python operations on them to dynamically create new output nodes! Below see how we do math and indexing right on the output channels:" + "In addition to using output channels (or nodes, if they have only a single output) to make connections to input channels, we can perform many (but not all) other python operations on them to dynamically create new nodes! Below see how we do math and indexing right on the output channels:" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "4c80aee3-a8e4-444c-9260-3078f8d617a4", "metadata": {}, "outputs": [], @@ -263,7 +296,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "c1ef0cf9-131f-4abd-a1dd-d4f066fe1d32", "metadata": {}, "outputs": [ @@ -880,10 +913,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -904,7 +937,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "id": "c499c0ed-7af5-491a-b340-2d2f4f48529c", "metadata": {}, "outputs": [ @@ -919,10 +952,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 10, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" }, @@ -952,7 +985,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "id": "04a19675-c98d-4255-8583-a567cda45e08", "metadata": {}, "outputs": [ @@ -981,31 +1014,31 @@ "\n", "There's just one last step: once we have a workflow we're happy with, we can package it as a \"macro\"! This lets us make more and more complex workflows by composing sub-graphs.\n", "\n", - "We don't yet have an automated tool for converting workflows into macros, but we can create them by decorating a function that takes a macro instance and builds its graph, so we can just copy-and-paste our workflow above into a decorated function! \n", + "We don't yet have an automated tool for converting workflows into macros, but we can create them by decorating a function that takes a macro instance and macro input, builds its graph, and returns the parts of it we want as macro output. We can do most of this by just copy-and-pasting our workflow above into a decorated function! \n", "\n", "Just like a function node, the IO of a macro is defined by the signature and return values of the function we're decorating. Just remember to include a `self`-like argument for the macro instance itself as the first argument, and (usually) to only return single-output nodes or output channels in the `return` statement:" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "id": "996c9e9a-ba0e-458a-9e54-331974073cca", "metadata": {}, "outputs": [], "source": [ "@Workflow.wrap.as_macro_node(\"x\", \"n\", \"fig\")\n", - "def MySquarePlot(macro, n: int):\n", - " macro.arange = Arange(n=n)\n", - " macro.plot = macro.create.plotting.Scatter(\n", - " x=macro.arange.outputs.arange[:macro.arange.outputs.length -1],\n", - " y=macro.arange.outputs.arange[:macro.arange.outputs.length -1]**2\n", + "def MySquarePlot(wf, n: int):\n", + " wf.arange = Arange(n=n)\n", + " wf.plot = wf.create.plotting.Scatter(\n", + " x=wf.arange.outputs.arange[:wf.arange.outputs.length -1],\n", + " y=wf.arange.outputs.arange[:wf.arange.outputs.length -1]**2\n", " )\n", - " return macro.arange.outputs.arange, macro.arange.outputs.length, macro.plot" + " return wf.arange.outputs.arange, wf.arange.outputs.length, wf.plot" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "id": "b43f7a86-4579-4476-89a9-9d7c5942c3fb", "metadata": {}, "outputs": [ @@ -1023,11 +1056,11 @@ "data": { "text/plain": [ "{'square_plot__n': 10,\n", - " 'square_plot__fig': ,\n", - " 'plus_one_square_plot__fig': }" + " 'square_plot__fig': ,\n", + " 'plus_one_square_plot__fig': }" ] }, - "execution_count": 13, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" }, @@ -1064,7 +1097,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "id": "370b4c4b-8a95-4a2a-8255-1574763606bb", "metadata": {}, "outputs": [ @@ -1083,226 +1116,226 @@ "clustersquare_plot\n", "\n", "square_plot: MySquarePlot\n", - "\n", - "clustersquare_plotInputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Inputs\n", - "\n", "\n", "clustersquare_plotOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", "\n", "clustersquare_plotarange\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "arange: Arange\n", "\n", "\n", "clustersquare_plotarangeInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Inputs\n", "\n", "\n", "clustersquare_plotarangeOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", - "\n", - "clustersquare_plotarange__length_Subtract_1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "arange__length_Subtract_1: Subtract\n", - "\n", - "\n", - "clustersquare_plotarange__length_Subtract_1Inputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Inputs\n", - "\n", - "\n", - "clustersquare_plotarange__length_Subtract_1Outputs\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Outputs\n", - "\n", "\n", "clustersquare_plotarange__arange_Slice_None_arange__length_Subtract_1__sub_None\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "arange__arange_Slice_None_arange__length_Subtract_1__sub_None: Slice\n", "\n", "\n", "clustersquare_plotarange__arange_Slice_None_arange__length_Subtract_1__sub_NoneInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Inputs\n", "\n", "\n", "clustersquare_plotarange__arange_Slice_None_arange__length_Subtract_1__sub_NoneOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", + "\n", + "clustersquare_plotInputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", "\n", "clustersquare_plotarange__arange_GetItem_arange__arange_Slice_None_arange__length_Subtract_1__sub_None__slice\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "arange__arange_GetItem_arange__arange_Slice_None_arange__length_Subtract_1__sub_None__slice: GetItem\n", "\n", "\n", "clustersquare_plotarange__arange_GetItem_arange__arange_Slice_None_arange__length_Subtract_1__sub_None__sliceInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Inputs\n", "\n", "\n", "clustersquare_plotarange__arange_GetItem_arange__arange_Slice_None_arange__length_Subtract_1__sub_None__sliceOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", "\n", "clustersquare_plotarange__arange_GetItem_arange__arange_Slice_None_arange__length_Subtract_1__sub_None__slice__getitem_Power_2\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "arange__arange_GetItem_arange__arange_Slice_None_arange__length_Subtract_1__sub_None__slice__getitem_Power_2: Power\n", "\n", "\n", "clustersquare_plotarange__arange_GetItem_arange__arange_Slice_None_arange__length_Subtract_1__sub_None__slice__getitem_Power_2Inputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Inputs\n", "\n", "\n", "clustersquare_plotarange__arange_GetItem_arange__arange_Slice_None_arange__length_Subtract_1__sub_None__slice__getitem_Power_2Outputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", "\n", "clustersquare_plotplot\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "plot: Scatter\n", "\n", "\n", "clustersquare_plotplotInputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Inputs\n", "\n", "\n", "clustersquare_plotplotOutputs\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "Outputs\n", "\n", + "\n", + "clustersquare_plotarange__length_Subtract_1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "arange__length_Subtract_1: Subtract\n", + "\n", + "\n", + "clustersquare_plotarange__length_Subtract_1Outputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Outputs\n", + "\n", + "\n", + "clustersquare_plotarange__length_Subtract_1Inputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Inputs\n", + "\n", "\n", "\n", "clustersquare_plotInputsrun\n", @@ -1704,10 +1737,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 14, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } From 7b2965267dd7c7efd39fb754f0b9612f8eea7e6c Mon Sep 17 00:00:00 2001 From: liamhuber Date: Thu, 11 Apr 2024 12:58:55 -0700 Subject: [PATCH 5/5] Update deepdive --- notebooks/deepdive.ipynb | 531 ++++++++++++++++++++++++--------------- 1 file changed, 334 insertions(+), 197 deletions(-) diff --git a/notebooks/deepdive.ipynb b/notebooks/deepdive.ipynb index 69f06f049..a0f0f0c7a 100644 --- a/notebooks/deepdive.ipynb +++ b/notebooks/deepdive.ipynb @@ -441,16 +441,139 @@ "source": [ "## Reusable node classes\n", "\n", - "If we're going to use a node many times, we may want to define a new sub-class of `Function` to handle this.\n", + "Under the hood, `function_node` is actually dynamically making a new sub-class of `Function` and returning us an instance of that class. If we look at the type, we'll see it's based off the wrapped function (as is the class's `__module__`), and `Function` and the other parent classes appear in the method resolution order:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "09043170-54f1-469e-8975-c013ac11aad0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(__main__.adder, '__main__')" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(adder_node), adder_node.__module__" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "f7be4aa6-5d0d-4e86-9b57-656b7bb16e30", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[__main__.adder,\n", + " pyiron_workflow.function.Function,\n", + " pyiron_workflow.io_preview.DecoratedNode,\n", + " pyiron_workflow.io_preview.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.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", + " abc.ABC,\n", + " object]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "adder_node.__class__.mro()" + ] + }, + { + "cell_type": "markdown", + "id": "50370591-e3ac-4593-a9f7-d4bab8b1376f", + "metadata": {}, + "source": [ + "However, there's lots of times where we're going to want a bunch of instances of the same type of node, and we'd really like access to this class directly so we can make new instances more succinctly.\n", + "\n", + "The can be done the traditionaly way directly by inheriting from `Function` and specifying its required `node_function` static method, and (optionally) overriding its `_output_labels` so they are defined by you instead of scraped from the `node_function`" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "4382a4cf-5e64-47a2-938b-39d0674b7ed5", + "metadata": {}, + "outputs": [], + "source": [ + "from pyiron_workflow.function import Function" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "abc26576-3473-4e31-93cb-d5cf2ea31b93", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "class name = MySubtractionChild\n", + "label = node_function\n", + "output with default input = {'diff': 1}\n" + ] + } + ], + "source": [ + "class MySubtractionChild(Function):\n", + " _output_labels = [\"diff\"]\n", + " \n", + " @staticmethod\n", + " def node_function(x: int | float = 2, y: int | float = 1) -> int | float:\n", + " return x - y\n", + "\n", + "sn = MySubtractionChild()\n", + "print(\"class name =\", sn.__class__.__name__)\n", + "print(\"label =\", sn.label)\n", "\n", - "The can be done directly by inheriting from `Function` and overriding it's `__init__` function and/or directly defining the `node_function` property so that the core functionality of the node (i.e. the node function and output labels) are set in stone, but even easier is to use the `as_function_node` decorator to do this for you! \n", + "sn() # Runs without updating input data, but we have defaults so that's fine\n", + "print(\"output with default input = \", sn.outputs.to_value_dict())" + ] + }, + { + "cell_type": "markdown", + "id": "bb875023-6fd4-4fd8-9068-d56bb2660715", + "metadata": {}, + "source": [ + "Even easier is to use the `as_function_node` decorator to do this for you! \n", "\n", - "The decorator also lets us explicitly choose the names of our output channels by passing the `output_labels` argument to the decorator -- as a string to create a single channel for the returned values, or as a list of strings equal to the number of returned values in a returned tuple." + "The decorator lets us easily choose the names of our output channels by passing the `output_labels` argument to the decorator -- as a string to create a single channel for the returned values, or as a list of strings equal to the number of returned values in a returned tuple. The decorator also does nice quality-of-life things like use the decorated function name as the default label for new instances." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 19, "id": "61b43a9b-8dad-48b7-9194-2045e465793b", "metadata": {}, "outputs": [], @@ -460,7 +583,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 20, "id": "647360a9-c971-4272-995c-aa01e5f5bb83", "metadata": {}, "outputs": [ @@ -470,13 +593,13 @@ "text": [ "class name = Subtract\n", "label = Subtract\n", - "default output = -1\n" + "output with default input = {'diff': 1}\n" ] } ], "source": [ "@as_function_node(\"diff\")\n", - "def Subtract(x: int | float = 1, y: int | float = 2) -> int | float:\n", + "def Subtract(x: int | float = 2, y: int | float = 1) -> int | float:\n", " return x - y\n", "\n", "sn = Subtract()\n", @@ -484,7 +607,7 @@ "print(\"label =\", sn.label)\n", "\n", "sn() # Runs without updating input data, but we have defaults so that's fine\n", - "print(\"default output =\", sn.outputs.diff.value)" + "print(\"output with default input = \", sn.outputs.to_value_dict())" ] }, { @@ -492,12 +615,42 @@ "id": "77642993-63c3-41a3-a963-a406de33553c", "metadata": {}, "source": [ - "The decorator is just dynamically defining a new child of the `Function` class. These children have their behaviour available in the static method `node_function` so we can access it right from the class level, e.g. to modify the behaviour:" + "Note that we break with python convention and use PascalCase to name our \"function\" here -- that's because by the time the decorator is done with it, it is actually a class! Information about the expected IO is available right at the class level:" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 21, + "id": "122ff192-8a12-4323-bd41-1c1e922a66b3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'inputs': {'x': (int | float, 2), 'y': (int | float, 1)},\n", + " 'outputs': {'diff': int | float}}" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Subtract.preview_io()" + ] + }, + { + "cell_type": "markdown", + "id": "9e40da77-98dc-45d9-bf3a-202f82b38c4a", + "metadata": {}, + "source": [ + "So is the node functionality, so we can still leverage our node as a normal function if we want:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, "id": "b8c845b7-7088-43d7-b106-7a6ba1c571ec", "metadata": {}, "outputs": [ @@ -543,7 +696,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 23, "id": "2e418abf-7059-4e1e-9b9f-b3dc0a4b5e35", "metadata": { "tags": [] @@ -563,7 +716,7 @@ "2" ] }, - "execution_count": 18, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -597,7 +750,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 24, "id": "f3b0b700-683e-43cb-b374-48735e413bc9", "metadata": {}, "outputs": [ @@ -607,7 +760,7 @@ "4" ] }, - "execution_count": 19, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -633,7 +786,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 25, "id": "59c29856-c77e-48a1-9f17-15d4c58be588", "metadata": {}, "outputs": [ @@ -677,7 +830,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 26, "id": "98312fbb-0e87-417c-9780-d22903cdb3f4", "metadata": {}, "outputs": [ @@ -687,7 +840,7 @@ "9" ] }, - "execution_count": 21, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -710,7 +863,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 27, "id": "b0e8fc87-fba1-4501-882b-f162c4eadf97", "metadata": {}, "outputs": [ @@ -720,7 +873,7 @@ "20" ] }, - "execution_count": 22, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -741,38 +894,7 @@ }, { "cell_type": "code", - "execution_count": 23, - "id": "8a195c41-233e-4076-ad77-008c93297f9c", - "metadata": {}, - "outputs": [], - "source": [ - "foo = [1, 2, 3]" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "6805b0c3-9103-49f4-bc29-569b0b4d6ed0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "foo.reverse" - ] - }, - { - "cell_type": "code", - "execution_count": 25, + "execution_count": 28, "id": "7c4cbe66-9b0a-428b-835f-31959a7f75bb", "metadata": {}, "outputs": [ @@ -782,7 +904,7 @@ "[1, 2]" ] }, - "execution_count": 25, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -794,7 +916,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 29, "id": "840d5762-ebb5-45aa-8faf-5443544bdeae", "metadata": {}, "outputs": [ @@ -804,7 +926,7 @@ "[1, 2]" ] }, - "execution_count": 26, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -815,7 +937,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 30, "id": "16c2d0de-de6f-4b33-84e4-aefbe5db4177", "metadata": {}, "outputs": [ @@ -825,7 +947,7 @@ "1" ] }, - "execution_count": 27, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -837,7 +959,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 31, "id": "30b4ed75-bb73-44bb-b6d9-fe525b924652", "metadata": {}, "outputs": [ @@ -847,7 +969,7 @@ "42" ] }, - "execution_count": 28, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } @@ -870,7 +992,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 32, "id": "786b1402-b595-4337-8872-fd58687c2725", "metadata": {}, "outputs": [ @@ -880,7 +1002,7 @@ "(True, False)" ] }, - "execution_count": 29, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -906,7 +1028,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 33, "id": "6569014a-815b-46dd-8b47-4e1cd4584b3b", "metadata": {}, "outputs": [ @@ -920,7 +1042,7 @@ }, { "data": { - "image/png": "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t99+u5555plwlBxTwnltjLqZkSEOh8PvtTEmoO2b+o/UjkDBjvWQXbt26ZFHHlFNTY1mzZo1UeVFlbGO9cDAgO68805VVFRo0aJF4SovqgTzdz04OCiHw6GdO3cqLy9Pt956q5588km9+uqrzI6MQTBj7fF4tH79ej388MNqbm5WfX292traVFpaGo5SY064ro1R98+lmTNnKj4+PiBVd3Z2BiS8IWlpaSP2T0hI0IwZMyas1kgXylgPqamp0b333qs33nhDN99880SWGRWCHeu+vj4dPnxYLS0teuCBByRduGAaY5SQkKA9e/bopptuCkvtkSaUv2u32605c+b4fVX64sWLZYzRqVOntHDhwgmtOVKFMtaVlZVasmSJHnroIUnStddeq2nTpmnp0qV69NFHmckeR+G8NkbdzEhiYqJycnLU0NDg197Q0KDCwsIRzykoKAjov2fPHuXm5mrKlCkTVmukC2WspQszInfffbdef/111nnHKNixTk5O1vvvv6+jR48OH6Wlpbrqqqt09OhR5efnh6v0iBPK3/WSJUt05swZffrpp8NtH374oeLi4jR37twJrTeShTLWn3/+ueLi/C9d8fHxkv77r3aMj7BeG8f9lthJYOhRsZdfftl4PB6zceNGM23aNPPvf//bGGPMli1bzJo1a4b7Dz2+tGnTJuPxeMzLL7/Mo71jFOxYv/766yYhIcE8++yzxuv1Dh+ffPKJrY8QMYId66/jaZqxC3as+/r6zNy5c81PfvIT88EHH5i9e/eahQsXmp///Oe2PkLECHasX3nlFZOQkGCqqqrM8ePHzf79+01ubq7Jy8uz9REiRl9fn2lpaTEtLS1GknnyySdNS0vL8GPUNq+NURlGjDHm2WefNenp6SYxMdFkZ2ebvXv3Dv/srrvuMjfeeKNf/3/84x/m+9//vklMTDTf/e53TXV1dZgrjlzBjPWNN95oJAUcd911V/gLj0DB/l1fjDASnGDH+tixY+bmm282U6dONXPnzjWbN282n3/+eZirjkzBjvXTTz9tMjMzzdSpU43b7TY//elPzalTp8JcdeT5+9//fsn//9q8NjqMYV4LAADYE3X3jAAAgMhCGAEAAFYRRgAAgFWEEQAAYBVhBAAAWEUYAQAAVhFGAACAVYQRAABgFWEEAABYRRgBAABWEUYAAIBVhBEAAGDV/wcGOUsmyu2D2gAAAABJRU5ErkJggg==", + "image/png": "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", "text/plain": [ "
" ] @@ -956,7 +1078,7 @@ "source": [ "## Edge cases\n", "\n", - "If output labels aren't provided, we try to scrape them from the source code for the function -- but this has limitations, like that the source code needs to be available (no lambda functions!) and that there's a single return value. \n", + "If output labels aren't provided, we try to scrape them from the source code for the function -- but this has limitations, like that the source code needs to be available for inspection and that there's a single return value. \n", "\n", "If explicit output labels _are_ provided, we _still_ try to scrape them from the function source code just to make sure that everything lines up nicely. However, there are a couple of edge cases where you may want to tell the workflow code that you really know what you're serious about your labels and just use them without any validation.\n", "\n", @@ -969,7 +1091,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 34, "id": "1a43985b-98d7-4c56-b8fe-e6598298b44b", "metadata": {}, "outputs": [ @@ -979,7 +1101,7 @@ "{'x0': 7, 'x1': 10.14}" ] }, - "execution_count": 31, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -1004,7 +1126,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 35, "id": "ab3ad9e6-2a5e-4b0f-82e3-9e7208970d22", "metadata": {}, "outputs": [ @@ -1037,7 +1159,7 @@ "# Workflows\n", "\n", "The case where we have groups of connected nodes working together is our normal, intended use case.\n", - "We offer a formal way to group these objects together as a `Workflow(Node)` object.\n", + "We offer a formal way to group these objects together as a `Workflow` object.\n", "`Workflow` also offers us a single point of entry to the codebase -- i.e. most of the time you shouldn't need the node imports used above, because the decorators are available right on the workflow class.\n", "\n", "We will also see here that we can rename our node output channels using the `outputs_map: dict[str, str]` kwarg, in case they don't have a convenient name to start with.\n", @@ -1056,7 +1178,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 36, "id": "1cd000bd-9b24-4c39-9cac-70a3291d0660", "metadata": {}, "outputs": [], @@ -1083,7 +1205,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 37, "id": "7964df3c-55af-4c25-afc5-9e07accb606a", "metadata": {}, "outputs": [ @@ -1130,7 +1252,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 38, "id": "809178a5-2e6b-471d-89ef-0797db47c5ad", "metadata": {}, "outputs": [ @@ -1138,7 +1260,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "['ax', 'b__x'] ['ay', 'a + b + 2']\n" + "inputs: ['a', 'b']\n", + "outputs: ['a + 1', 'a + b + 2']\n" ] } ], @@ -1157,11 +1280,12 @@ "wf.a = AddOne(0)\n", "wf.b = AddOne(0)\n", "wf.sum = Add(wf.a, wf.b) \n", - "wf.inputs_map = {\"a__x\": \"ax\"}\n", - "wf.outputs_map = {\"a__y\": \"ay\", \"sum__sum\": \"a + b + 2\"}\n", + "wf.inputs_map = {\"a__x\": \"a\", \"b__x\": \"b\"}\n", + "wf.outputs_map = {\"a__y\": \"a + 1\", \"sum__sum\": \"a + b + 2\"}\n", "# Remember, with single value nodes we can pass the whole node instead of an output channel!\n", "\n", - "print(wf.inputs.labels, wf.outputs.labels)" + "print(\"inputs:\", wf.inputs.labels)\n", + "print(\"outputs:\", wf.outputs.labels)" ] }, { @@ -1184,23 +1308,23 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 39, "id": "52c48d19-10a2-4c48-ae81-eceea4129a60", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'ay': 3, 'a + b + 2': 7}" + "{'a + 1': 3, 'a + b + 2': 7}" ] }, - "execution_count": 36, + "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "out = wf(ax=2, b__x=3)\n", + "out = wf(a=2, b=3)\n", "out" ] }, @@ -1217,28 +1341,28 @@ "id": "e3f4b51b-7c28-47f7-9822-b4755e12bd4d", "metadata": {}, "source": [ - "We can see now why we've been trying to give succinct string labels to our `Function` node outputs instead of just arbitrary expressions! The expressions are typically not dot-accessible:" + "We can see now why we've been trying to give succinct string labels to our `Function` node outputs instead of just arbitrary expressions! The expressions are typically not dot-accessible, but can still be grabbed with a key:" ] }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 40, "id": "bb35ba3e-602d-4c9c-b046-32da9401dd1c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(7, 3)" + "7" ] }, - "execution_count": 37, + "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "out[\"a + b + 2\"], out.ay" + "out[\"a + b + 2\"]" ] }, { @@ -1246,12 +1370,12 @@ "id": "c67ddcd9-cea0-4f3f-96aa-491da0a4c459", "metadata": {}, "source": [ - "We can also look at our graph:" + "We can also visualize our graph!" ] }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 41, "id": "2b0d2c85-9049-417b-8739-8a8432a1efbe", "metadata": {}, "outputs": [ @@ -1410,11 +1534,11 @@ "\n", "accumulate_and_run\n", "\n", - "\n", + "\n", "\n", - "clustersimpleInputsax\n", + "clustersimpleInputsa\n", "\n", - "ax\n", + "a\n", "\n", "\n", "\n", @@ -1422,18 +1546,18 @@ "\n", "x\n", "\n", - "\n", + "\n", "\n", - "clustersimpleInputsax->clustersimpleaInputsx\n", + "clustersimpleInputsa->clustersimpleaInputsx\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "clustersimpleInputsb__x\n", - "\n", - "b__x\n", + "clustersimpleInputsb\n", + "\n", + "b\n", "\n", "\n", "\n", @@ -1441,18 +1565,18 @@ "\n", "x\n", "\n", - "\n", + "\n", "\n", - "clustersimpleInputsb__x->clustersimplebInputsx\n", - "\n", - "\n", - "\n", + "clustersimpleInputsb->clustersimplebInputsx\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "clustersimpleOutputsay\n", - "\n", - "ay\n", + "clustersimpleOutputsa + 1\n", + "\n", + "a + 1\n", "\n", "\n", "\n", @@ -1498,12 +1622,12 @@ "\n", "y\n", "\n", - "\n", + "\n", "\n", - "clustersimpleaOutputsy->clustersimpleOutputsay\n", - "\n", - "\n", - "\n", + "clustersimpleaOutputsy->clustersimpleOutputsa + 1\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -1593,10 +1717,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 38, + "execution_count": 41, "metadata": {}, "output_type": "execute_result" } @@ -1623,14 +1747,14 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 42, "id": "ae500d5e-e55b-432c-8b5f-d5892193cdf5", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a8d34d23d0594e9a892c2d00c3733bf1", + "model_id": "4f46ea7f33474ee0af08d2858f68a5e7", "version_major": 2, "version_minor": 0 }, @@ -1649,7 +1773,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "96ca190eaee147f3b6268748a635acc1", + "model_id": "133a67d4fbbf4f9ca3bae2f56b8d04d1", "version_major": 2, "version_minor": 0 }, @@ -1663,10 +1787,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 39, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" }, @@ -1709,7 +1833,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 43, "id": "2114d0c3-cdad-43c7-9ffa-50c36d56d18f", "metadata": {}, "outputs": [ @@ -1722,9 +1846,9 @@ "\n", "\n", - "\n", - "\n", + "\n", + "\n", "clusterwith_prebuilt\n", "\n", "with_prebuilt: Workflow\n", @@ -1923,16 +2047,16 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 40, + "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "wf.draw(depth=0)" + "wf.draw(depth=0, size=(10, 10))" ] }, { @@ -1958,12 +2082,12 @@ "source": [ "# Macros\n", "\n", - "Once you have a workflow that you're happy with, you may want to store it as a macro so it can be stored in a human-readable way, reused, shared, and executed with more efficiency than the \"living\" `Workflow` instance. Automated conversion of an existing `Workflow` instance into a `Macro` subclass is still on the TODO list, but defining a new macro is pretty easy -- they are just composite nodes that have a function defining their graph setup analogous to how `Function` nodes define their node function" + "Once you have a workflow that you're happy with, you may want to store it as a macro so it can be stored in a human-readable way, reused, shared, and executed with more efficiency than the \"living\" `Workflow` instance. Defining a new macro is pretty easy -- they are just composite nodes that have a function defining their input, graph setup, and output analogous to how `Function` nodes define their node function" ] }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 44, "id": "c71a8308-f8a1-4041-bea0-1c841e072a6d", "metadata": {}, "outputs": [], @@ -1973,7 +2097,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 45, "id": "2b9bb21a-73cd-444e-84a9-100e202aa422", "metadata": {}, "outputs": [ @@ -1995,7 +2119,7 @@ "13" ] }, - "execution_count": 42, + "execution_count": 45, "metadata": {}, "output_type": "execute_result" } @@ -2005,7 +2129,7 @@ "def AddOne(x):\n", " return x + 1\n", "\n", - "def add_three_macro(macro, add_one__x) -> None:\n", + "def add_three_macro(self, x):\n", " \"\"\"\n", " The macro constructor `macro_node` expects the provided function \n", " to take a `Macro` instance as its first argument, followed by any input data,\n", @@ -2013,18 +2137,28 @@ " `Node` or an output data channel) \n", " In the function body, it should add nodes to the macro, wire their connections, etc.\n", " \"\"\"\n", - " macro.add_one = AddOne(add_one__x)\n", - " macro.add_two = AddOne(macro.add_one)\n", - " macro.add_three = AddOne(macro.add_two)\n", + " self.add_one = AddOne(x)\n", + " self.add_two = AddOne(self.add_one)\n", + " self.add_three = AddOne(self.add_two)\n", " # Just like workflows, for simple DAG macros we don't _need_\n", " # to set signals and starting nodes -- the macro will build them\n", " # automatically. But, if you do set both then the macro will use them\n", - " macro.add_one >> macro.add_two >> macro.add_three\n", - " macro.starting_nodes = [macro.add_one] \n", - " return macro.add_three\n", + " self.add_one >> self.add_two >> self.add_three\n", + " self.starting_nodes = [self.add_one] \n", + " # We _do_ need to specify the output of our macro,\n", + " # which will typically be output channel(s) and/or single-return node(s)\n", + " return self.add_three\n", " \n", - "macro = macro_node(add_three_macro, output_labels=\"add_three__result\")\n", - "macro(add_one__x=10).add_three__result" + "macro = macro_node(add_three_macro)\n", + "macro(x=10).add_three" + ] + }, + { + "cell_type": "markdown", + "id": "28323b14-22c2-4bab-bea7-168505677ebf", + "metadata": {}, + "source": [ + "Note that the input and output channel labels are scraped from the decorated function. This is just like for `function_node`, but with one big exception: the `\"self.\"` has been stripped off the returned value! Since the most likely thing to return is some child node, this is just a quality of life shortcut. Passing `output_labels=...` still works just like for `function_node`." ] }, { @@ -2032,7 +2166,7 @@ "id": "d4f797d6-8d88-415f-bb9c-00f3e1b15e37", "metadata": {}, "source": [ - "Even in the abscence of an automated converter, it should be easy to take the workflow you've been developing and copy-paste that code into a function, use the signature, returns, and `output_labels` to define the IO for the macro, then bam you've got a macro!" + "It will often be the case that this new macro will be made by copying and pasting some `wf = Workflow(...); ...` code that was explored. The use of `self` here reflects the canonical name for the own-instance argument, but for the sake of defining the function any variable will do! So you can use defintions like `def add_three_macro(wf, x):` just fine if that makes copy-pasting easier." ] }, { @@ -2040,14 +2174,12 @@ "id": "bd5099c4-1c01-4a45-a5bb-e5087595db9f", "metadata": {}, "source": [ - "Of course, we can also use a decorator like for other node types. Just like workflows, we can use `inputs_map` and `outputs_map` to control macro-level IO, but macros also allow us to use a more function-like interface where the callable that creates the graph has args and/or kwargs, and/or has return values and output labels. In these cases, the I/O switches over to a \"whitelist\" paradigm where all the child IO we _don't explicitly mention_ gets _disabled and hidden_. The maps always take precedence, and both approaches are equivalent, so it's really just a question of whether it's less typing to use the maps to turn off/relabel stuff you _don't_ want, or use the callable definition to specify which stuff you _do_ want. Typically the latter is easier.\n", - "\n", - "Let's take a look at this, where we use the function defintion to control most of our IO, but still leverage the map to expose something that would normally be hidden (even in the workflows, since it's connected):" + "Of course, we can also use a decorator like for funciton nodes:" ] }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 46, "id": "3668f9a9-adca-48a4-84ea-13add965897c", "metadata": {}, "outputs": [ @@ -2057,7 +2189,7 @@ "{'add_two': 102, 'add_three': 103}" ] }, - "execution_count": 43, + "execution_count": 46, "metadata": {}, "output_type": "execute_result" } @@ -2102,27 +2234,26 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 47, "id": "9aaeeec0-5f88-4c94-a6cc-45b56d2f0111", "metadata": {}, "outputs": [], "source": [ - "@Workflow.wrap.as_macro_node()\n", - "def LammpsMinimize(macro, element: str, crystalstructure: str, lattice_guess: float | int):\n", - " macro.structure = macro.create.pyiron_atomistics.Bulk(\n", + "@Workflow.wrap.as_macro_node(\"structure\", \"energy\")\n", + "def LammpsMinimize(self, element: str, crystalstructure: str, lattice_guess: float | int):\n", + " self.structure = self.create.pyiron_atomistics.Bulk(\n", " name=element,\n", " crystalstructure=crystalstructure,\n", " a=lattice_guess\n", " )\n", - " macro.engine = macro.create.pyiron_atomistics.Lammps(structure=macro.structure)\n", - " macro.calc = macro.create.pyiron_atomistics.CalcMin(job=macro.engine, pressure=0)\n", - " energy = macro.calc.outputs.energy_pot\n", - " return macro.structure, energy" + " self.engine = self.create.pyiron_atomistics.Lammps(structure=self.structure)\n", + " self.calc = self.create.pyiron_atomistics.CalcMin(job=self.engine, pressure=0)\n", + " return self.structure, self.calc.outputs.energy_pot" ] }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 48, "id": "26a080dc-acaf-45bb-9935-7a42ff8d9552", "metadata": {}, "outputs": [ @@ -2132,7 +2263,7 @@ "{'structure': None, 'energy': None}" ] }, - "execution_count": 45, + "execution_count": 48, "metadata": {}, "output_type": "execute_result" } @@ -2146,12 +2277,12 @@ "id": "4dfe9c0c-e9e7-4d5f-ad34-e19fd0382670", "metadata": {}, "source": [ - "Note that we didn't include any output labels, but they still come out looking OK. Here, we're exploiting a shortcut that the `macro.` (or whatever your `self`-like variable is called) gets left-stripped off the output label, since it will be very common to return children of the macro. However, other \".\" are not permissible, so for the energy we create and return a well-named local variable." + "Note that while `\"self.\"` will get stripped off our return channel names, we're not allowed to have other `\".\"` characters in what remains -- so here where we're mixing and matching a returned (single-return-value) node and an explicit output channel (from a node with more than one output), we need to provide output labels. We could alternatively have given a nicely named local variable, e.g. `energy = self.calc.outputs.energy_pot; return return self.structure, energy` to get the same result." ] }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 49, "id": "a832e552-b3cc-411a-a258-ef21574fc439", "metadata": {}, "outputs": [ @@ -2207,7 +2338,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 50, "id": "b764a447-236f-4cb7-952a-7cba4855087d", "metadata": {}, "outputs": [ @@ -3165,10 +3296,10 @@ "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 47, + "execution_count": 50, "metadata": {}, "output_type": "execute_result" } @@ -3179,7 +3310,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 51, "id": "b51bef25-86c5-4d57-80c1-ab733e703caf", "metadata": {}, "outputs": [ @@ -3193,7 +3324,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f9b77a718b1b4077ac2e758d30c38ba6", + "model_id": "7ea0eab58353438a8f69c11e7a59165a", "version_major": 2, "version_minor": 0 }, @@ -3214,7 +3345,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "6c257b32d747497c892a58aa415bca7b", + "model_id": "eff7fe719236428597a4ece2b2d107b1", "version_major": 2, "version_minor": 0 }, @@ -3240,7 +3371,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 52, "id": "091e2386-0081-436c-a736-23d019bd9b91", "metadata": {}, "outputs": [ @@ -3262,7 +3393,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "15e6378664df4929829581c69baac4d7", + "model_id": "6ecb6b52c9d14c81b19b318373951a0a", "version_major": 2, "version_minor": 0 }, @@ -3283,7 +3414,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "84c434ce4fba48bd86bb4d255f5261f3", + "model_id": "3702a04c828846e8b28d506ab794461c", "version_major": 2, "version_minor": 0 }, @@ -3321,7 +3452,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 53, "id": "4cdffdca-48d3-4486-9045-48102c7e5f31", "metadata": {}, "outputs": [ @@ -3353,7 +3484,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 54, "id": "ed4a3a22-fc3a-44c9-9d4f-c65bc1288889", "metadata": {}, "outputs": [ @@ -3375,7 +3506,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "b7b5833fddcd4320bf509f699a2b9d39", + "model_id": "2bec85d8876a48d8aea0db7d4536c335", "version_major": 2, "version_minor": 0 }, @@ -3396,7 +3527,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ae6530375bc446cbaf029850dd9f3e44", + "model_id": "856b987061304207be21f413a81594ed", "version_major": 2, "version_minor": 0 }, @@ -3423,7 +3554,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 55, "id": "5a985cbf-c308-4369-9223-b8a37edb8ab1", "metadata": {}, "outputs": [ @@ -3445,7 +3576,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ef0fd622423c4e2590cdd813e25134fd", + "model_id": "82f1bad0b20943d3bb94fcb0767f9ee7", "version_major": 2, "version_minor": 0 }, @@ -3466,7 +3597,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4bd16ff817754d67820801fa49810a5e", + "model_id": "700b60e00e7f40a68d4ace67bb333dab", "version_major": 2, "version_minor": 0 }, @@ -3521,7 +3652,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 56, "id": "aa575249-b209-4e0c-9ea6-a82bc69dc833", "metadata": {}, "outputs": [ @@ -3530,7 +3661,7 @@ "output_type": "stream", "text": [ "None 1\n", - " NOT_DATA\n" + " NOT_DATA\n" ] } ], @@ -3557,7 +3688,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 57, "id": "c1b7b4e9-1c76-470c-ba6e-a58ea3f611f6", "metadata": {}, "outputs": [ @@ -3589,7 +3720,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 58, "id": "7e98058b-a791-4cb1-ae2c-864ad7e56cee", "metadata": {}, "outputs": [], @@ -3607,7 +3738,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 59, "id": "0d1b4005-488e-492f-adcb-8ad7235e4fe3", "metadata": {}, "outputs": [ @@ -3616,7 +3747,7 @@ "output_type": "stream", "text": [ "None 1\n", - " NOT_DATA\n", + " NOT_DATA\n", "Finally 5\n", "b (Add):\n", "Inputs ['obj', 'other']\n", @@ -3655,7 +3786,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 60, "id": "d03ca074-35a0-4e0d-9377-d4eaa5521f85", "metadata": {}, "outputs": [], @@ -3674,7 +3805,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 61, "id": "a7c07aa0-84fc-4f43-aa4f-6498c0837d76", "metadata": {}, "outputs": [ @@ -3682,7 +3813,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "6.01592511900526\n" + "6.01380590700137\n" ] } ], @@ -3706,7 +3837,7 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 62, "id": "b062ab5f-9b98-4843-8925-b93bf4c173f8", "metadata": {}, "outputs": [ @@ -3714,7 +3845,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "2.5806497349985875\n" + "2.925749412010191\n" ] } ], @@ -3805,7 +3936,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 63, "id": "c8196054-aff3-4d39-a872-b428d329dac9", "metadata": {}, "outputs": [], @@ -3815,7 +3946,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 64, "id": "ffd741a3-b086-4ed0-9a62-76143a3705b2", "metadata": {}, "outputs": [], @@ -3832,7 +3963,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 65, "id": "3a22c622-f8c1-449b-a910-c52beb6a09c3", "metadata": {}, "outputs": [ @@ -3863,7 +3994,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 66, "id": "0999d3e8-3a5a-451d-8667-a01dae7c1193", "metadata": {}, "outputs": [], @@ -3897,7 +4028,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 67, "id": "0b373764-b389-4c24-8086-f3d33a4f7fd7", "metadata": {}, "outputs": [ @@ -3905,11 +4036,11 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io_preview.py:237: OutputLabelsNotValidated: Could not find the source code to validate BulkForA5 output labels against the number of returned values -- proceeding without validation\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io_preview.py:245: OutputLabelsNotValidated: Could not find the source code to validate BulkForA5 output labels against the number of returned values -- proceeding without validation\n", " warnings.warn(\n", "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io.py:404: UserWarning: The keyword 'type_hint' was not found among input labels. If you are trying to update a class instance keyword, please use attribute assignment directly instead of calling this method\n", " warnings.warn(\n", - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io_preview.py:237: OutputLabelsNotValidated: Could not find the source code to validate __many_to_list output labels against the number of returned values -- proceeding without validation\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io_preview.py:245: OutputLabelsNotValidated: Could not find the source code to validate __many_to_list output labels against the number of returned values -- proceeding without validation\n", " warnings.warn(\n", "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io.py:404: UserWarning: The keyword 'a' was not found among input labels. If you are trying to update a class instance keyword, please use attribute assignment directly instead of calling this method\n", " warnings.warn(\n" @@ -3925,7 +4056,7 @@ " 17.230249999999995]" ] }, - "execution_count": 64, + "execution_count": 67, "metadata": {}, "output_type": "execute_result" } @@ -3972,7 +4103,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 68, "id": "0dd04b4c-e3e7-4072-ad34-58f2c1e4f596", "metadata": {}, "outputs": [ @@ -3980,7 +4111,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io_preview.py:237: OutputLabelsNotValidated: Could not find the source code to validate AddWhileLessThan_122886440957850675 output labels against the number of returned values -- proceeding without validation\n", + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io_preview.py:245: OutputLabelsNotValidated: Could not find the source code to validate AddWhileLessThan_m717539476586907336 output labels against the number of returned values -- proceeding without validation\n", " warnings.warn(\n", "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:176: UserWarning: The channel user_input was not connected to a, andthus could not disconnect from it.\n", " warn(\n", @@ -4035,7 +4166,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 69, "id": "2dfb967b-41ac-4463-b606-3e315e617f2a", "metadata": {}, "outputs": [ @@ -4059,25 +4190,31 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 70, "id": "2e87f858-b327-4f6b-9237-c8a557f29aeb", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io_preview.py:245: OutputLabelsNotValidated: Could not find the source code to validate RandomWhileGreaterThan_m9123908231955290806 output labels against the number of returned values -- proceeding without validation\n", + " warnings.warn(\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "0.588 > 0.2\n", - "0.144 <= 0.2\n", - "Finally 0.144\n" + "0.400 > 0.2\n", + "0.081 <= 0.2\n", + "Finally 0.081\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/io_preview.py:237: OutputLabelsNotValidated: Could not find the source code to validate RandomWhileGreaterThan_m2124235887652166674 output labels against the number of returned values -- proceeding without validation\n", - " warnings.warn(\n", "/Users/huber/work/pyiron/pyiron_workflow/pyiron_workflow/channels.py:176: UserWarning: The channel user_input was not connected to threshold, andthus could not disconnect from it.\n", " warn(\n" ]